diff options
Diffstat (limited to 'src/lib/Bcfg2/Client/Tools/POSIX')
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py | 296 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX/File.py | 44 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX/__init__.py | 7 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX/base.py | 116 |
4 files changed, 385 insertions, 78 deletions
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py new file mode 100644 index 000000000..fc4e16904 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py @@ -0,0 +1,296 @@ +""" Augeas driver """ + +import sys +import Bcfg2.Client.XML +from augeas import Augeas +from Bcfg2.Client.Tools.POSIX.base import POSIXTool +from Bcfg2.Client.Tools.POSIX.File import POSIXFile + + +class AugeasCommand(object): + """ Base class for all Augeas command objects """ + + def __init__(self, command, augeas_obj, logger): + self._augeas = augeas_obj + self.command = command + self.entry = self.command.getparent() + self.logger = logger + + def get_path(self, attr="path"): + """ Get a fully qualified path from the name of the parent entry and + the path given in this command tag. + + @param attr: The attribute to get the relative path from + @type attr: string + @returns: string - the fully qualified Augeas path + + """ + return "/files/%s/%s" % (self.entry.get("name").strip("/"), + self.command.get(attr).lstrip("/")) + + def _exists(self, path): + """ Return True if a path exists in Augeas, False otherwise. + + Note that a False return can mean many things: A file that + doesn't exist, a node within the file that doesn't exist, no + lens to parse the file, etc. """ + return len(self._augeas.match(path)) > 1 + + def _verify_exists(self, path=None): + """ Verify that the given path exists, with friendly debug + logging. + + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path exists + """ + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying that '%s' exists" % path) + return self._exists(path) + + def _verify_not_exists(self, path=None): + """ Verify that the given path does not exist, with friendly + debug logging. + + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path does not exist. + (I.e., True if it does not exist, False if it does + exist.) + """ + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying that '%s' does not exist" % path) + return not self._exists(path) + + def _verify_set(self, expected, path=None): + """ Verify that the given path is set to the given value, with + friendly debug logging. + + @param expected: The expected value of the node. + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path matches the expected value. + + """ + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying '%s' == '%s'" % (path, expected)) + actual = self._augeas.get(path) + if actual == expected: + return True + else: + self.logger.debug("Augeas: '%s' failed verification: '%s' != '%s'" + % (path, actual, expected)) + return False + + def __str__(self): + return Bcfg2.Client.XML.tostring(self.command) + + def verify(self): + """ Verify that the command has been applied. """ + raise NotImplementedError + + def install(self): + """ Run the command. """ + raise NotImplementedError + + +class Remove(AugeasCommand): + """ Augeas ``rm`` command """ + def verify(self): + return self._verify_not_exists() + + def install(self): + self.logger.debug("Augeas: Removing %s" % self.get_path()) + return self._augeas.remove(self.get_path()) + + +class Move(AugeasCommand): + """ Augeas ``move`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) + self.source = self.get_path("source") + self.dest = self.get_path("destination") + + def verify(self): + return (self._verify_not_exists(self.source), + self._verify_exists(self.dest)) + + def install(self): + self.logger.debug("Augeas: Moving %s to %s" % (self.source, self.dest)) + return self._augeas.move(self.source, self.dest) + + +class Set(AugeasCommand): + """ Augeas ``set`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) + self.value = self.command.get("value") + + def verify(self): + return self._verify_set(self.value) + + def install(self): + self.logger.debug("Augeas: Setting %s to %s" % (self.get_path(), + self.value)) + return self._augeas.set(self.get_path(), self.value) + + +class Clear(Set): + """ Augeas ``clear`` command """ + def __init__(self, command, augeas_obj, logger): + Set.__init__(self, command, augeas_obj, logger) + self.value = None + + +class SetMulti(AugeasCommand): + """ Augeas ``setm`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) + self.sub = self.command.get("sub") + self.value = self.command.get("value") + self.base = self.get_path("base") + + def verify(self): + return all(self._verify_set(self.value, + path="%s/%s" % (path, self.sub)) + for path in self._augeas.match(self.base)) + + def install(self): + return self._augeas.setm(self.base, self.sub, self.value) + + +class Insert(AugeasCommand): + """ Augeas ``ins`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) + self.label = self.command.get("label") + self.where = self.command.get("where", "before") + self.before = self.where == "before" + + def verify(self): + return self._verify_exists("%s/../%s" % (self.get_path(), self.label)) + + def install(self): + self.logger.debug("Augeas: Inserting new %s %s %s" % + (self.label, self.where, self.get_path())) + return self._augeas.insert(self.get_path(), self.label, self.before) + + +class POSIXAugeas(POSIXTool): + """ Handle <Path type='augeas'...> entries. See + :ref:`client-tools-augeas`. """ + __req__ = ['name', 'mode', 'owner', 'group'] + + def __init__(self, config): + POSIXTool.__init__(self, config) + self._augeas = dict() + # file tool for setting initial values of files that don't + # exist + self.filetool = POSIXFile(config) + + def get_augeas(self, entry): + """ Get an augeas object for the given entry. """ + if entry.get("name") not in self._augeas: + aug = Augeas() + if entry.get("lens"): + self.logger.debug("Augeas: Adding %s to include path for %s" % + (entry.get("name"), entry.get("lens"))) + incl = "/augeas/load/%s/incl" % entry.get("lens") + ilen = len(aug.match(incl)) + if ilen == 0: + self.logger.error("Augeas: Lens %s does not exist" % + entry.get("lens")) + else: + aug.set("%s[%s]" % (incl, ilen + 1), entry.get("name")) + aug.load() + self._augeas[entry.get("name")] = aug + return self._augeas[entry.get("name")] + + def fully_specified(self, entry): + return len(entry.getchildren()) != 0 + + def get_commands(self, entry): + """ Get a list of commands to verify or install. + + @param entry: The entry to get commands from. + @type entry: lxml.etree._Element + @param unverified: Only get commands that failed verification. + @type unverified: bool + @returns: list of + :class:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand` + objects representing the commands. + """ + rv = [] + for cmd in entry.iterchildren(): + if cmd.tag == "Initial": + continue + if cmd.tag in globals(): + rv.append(globals()[cmd.tag](cmd, self.get_augeas(entry), + self.logger)) + else: + err = "Augeas: Unknown command %s in %s" % (cmd.tag, + entry.get("name")) + self.logger.error(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), err])) + return rv + + def verify(self, entry, modlist): + rv = True + for cmd in self.get_commands(entry): + try: + if not cmd.verify(): + err = "Augeas: Command has not been applied to %s: %s" % \ + (entry.get("name"), cmd) + self.logger.debug(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), + err])) + rv = False + cmd.command.set("verified", "false") + else: + cmd.command.set("verified", "true") + except: # pylint: disable=W0702 + err = "Augeas: Unexpected error verifying %s: %s: %s" % \ + (entry.get("name"), cmd, sys.exc_info()[1]) + self.logger.error(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), err])) + rv = False + cmd.command.set("verified", "false") + return POSIXTool.verify(self, entry, modlist) and rv + + def install(self, entry): + rv = True + if entry.get("current_exists", "true") == "false": + initial = entry.find("Initial") + if initial is not None: + self.logger.debug("Augeas: Setting initial data for %s" % + entry.get("name")) + file_entry = Bcfg2.Client.XML.Element("Path", + **dict(entry.attrib)) + file_entry.text = initial.text + self.filetool.install(file_entry) + # re-parse the file + self.get_augeas(entry).load() + for cmd in self.get_commands(entry): + try: + cmd.install() + except: # pylint: disable=W0702 + self.logger.error( + "Failure running Augeas command on %s: %s: %s" % + (entry.get("name"), cmd, sys.exc_info()[1])) + rv = False + try: + self.get_augeas(entry).save() + except: # pylint: disable=W0702 + self.logger.error("Failure saving Augeas changes to %s: %s" % + (entry.get("name"), sys.exc_info()[1])) + rv = False + return POSIXTool.install(self, entry) and rv diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py index d7a70e202..0452ea258 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py @@ -3,7 +3,6 @@ import os import sys import stat -import time import difflib import tempfile import Bcfg2.Options @@ -189,12 +188,11 @@ class POSIXFile(POSIXTool): prompt.append('Binary file, no printable diff') attrs['current_bfile'] = b64encode(content) else: + diff = self._diff(content, self._get_data(entry)[0], + filename=entry.get("name")) if interactive: - diff = self._diff(content, self._get_data(entry)[0], - difflib.unified_diff, - filename=entry.get("name")) if diff: - udiff = '\n'.join(l.rstrip('\n') for l in diff) + udiff = '\n'.join(diff) if hasattr(udiff, "decode"): udiff = udiff.decode(Bcfg2.Options.setup.encoding) try: @@ -209,8 +207,6 @@ class POSIXFile(POSIXTool): prompt.append("Diff took too long to compute, no " "printable diff") if not sensitive: - diff = self._diff(content, self._get_data(entry)[0], - difflib.ndiff, filename=entry.get("name")) if diff: attrs["current_bdiff"] = b64encode("\n".join(diff)) else: @@ -221,28 +217,12 @@ class POSIXFile(POSIXTool): for attr, val in attrs.items(): entry.set(attr, val) - def _diff(self, content1, content2, difffunc, filename=None): - """ Return a diff of the two strings, as produced by difffunc. - warns after 5 seconds and times out after 30 seconds. """ - rv = [] - start = time.time() - longtime = False - for diffline in difffunc(content1.split('\n'), - content2.split('\n')): - now = time.time() - rv.append(diffline) - if now - start > 5 and not longtime: - if filename: - self.logger.info("POSIX: Diff of %s taking a long time" % - filename) - else: - self.logger.info("POSIX: Diff taking a long time") - longtime = True - elif now - start > 30: - if filename: - self.logger.error("POSIX: Diff of %s took too long; " - "giving up" % filename) - else: - self.logger.error("POSIX: Diff took too long; giving up") - return False - return rv + def _diff(self, content1, content2, filename=None): + """ Return a unified diff of the two strings """ + + fromfile = "%s (on disk)" % filename if filename else "" + tofile = "%s (from bcfg2)" % filename if filename else "" + return difflib.unified_diff(content1.split('\n'), + content2.split('\n'), + fromfile=fromfile, + tofile=tofile) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py index 13b45a759..c27c7559d 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py @@ -58,8 +58,11 @@ class POSIX(Bcfg2.Client.Tools.Tool): mname = submodule[1].rsplit('.', 1)[-1] if mname == 'base': continue - module = getattr(__import__(submodule[1]).Client.Tools.POSIX, - mname) + try: + module = getattr(__import__(submodule[1]).Client.Tools.POSIX, + mname) + except ImportError: + continue hdlr = getattr(module, "POSIX" + mname) if POSIXTool in hdlr.__mro__: # figure out what entry type this handler handles diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py index 712620206..8895eaae1 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py @@ -217,18 +217,13 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): acl.delete_entry(aclentry) if os.path.isdir(path): defacl = posix1e.ACL(filedef=path) - if not defacl.valid(): - # when a default ACL is queried on a directory that - # has no default ACL entries at all, you get an empty - # ACL, which is not valid. in this circumstance, we - # just copy the access ACL to get a base valid ACL - # that we can add things to. - defacl = posix1e.ACL(acl=acl) - else: - for aclentry in defacl: - if aclentry.tag_type in [posix1e.ACL_USER, - posix1e.ACL_GROUP]: - defacl.delete_entry(aclentry) + for aclentry in defacl: + if aclentry.tag_type in [posix1e.ACL_USER, + posix1e.ACL_USER_OBJ, + posix1e.ACL_GROUP, + posix1e.ACL_GROUP_OBJ, + posix1e.ACL_OTHER]: + defacl.delete_entry(aclentry) else: defacl = None @@ -254,10 +249,16 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): try: if scope == posix1e.ACL_USER: scopename = "user" - aclentry.qualifier = self._norm_uid(qualifier) + if qualifier: + aclentry.qualifier = self._norm_uid(qualifier) + else: + aclentry.tag_type = posix1e.ACL_USER_OBJ elif scope == posix1e.ACL_GROUP: scopename = "group" - aclentry.qualifier = self._norm_gid(qualifier) + if qualifier: + aclentry.qualifier = self._norm_gid(qualifier) + else: + aclentry.tag_type = posix1e.ACL_GROUP_OBJ except (OSError, KeyError): err = sys.exc_info()[1] self.logger.error("POSIX: Could not resolve %s %s: %s" % @@ -358,7 +359,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): try: # single octal digit rv = int(perms) - if rv > 0 and rv < 8: + if rv >= 0 and rv < 8: return rv else: self.logger.error("POSIX: Permissions digit out of range in " @@ -388,13 +389,17 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): """ Get a string representation of the given ACL. aclkey must be a tuple of (<acl type>, <acl scope>, <qualifier>) """ atype, scope, qualifier = aclkey + if not qualifier: + qualifier = '' acl_str = [] if atype == 'default': acl_str.append(atype) - if scope == posix1e.ACL_USER: + if scope == posix1e.ACL_USER or scope == posix1e.ACL_USER_OBJ: acl_str.append("user") - elif scope == posix1e.ACL_GROUP: + elif scope == posix1e.ACL_GROUP or scope == posix1e.ACL_GROUP_OBJ: acl_str.append("group") + elif scope == posix1e.ACL_OTHER: + acl_str.append("other") acl_str.append(qualifier) acl_str.append(self._acl_perm2string(perms)) return ":".join(acl_str) @@ -414,7 +419,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): """ Get data on the existing state of <path> -- e.g., whether or not it exists, owner, group, permissions, etc. """ try: - ondisk = os.stat(path) + ondisk = os.lstat(path) except OSError: self.logger.debug("POSIX: %s does not exist" % path) return (False, None, None, None, None, None) @@ -451,7 +456,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): if HAS_SELINUX: try: - secontext = selinux.getfilecon(path)[1].split(":")[2] + secontext = selinux.lgetfilecon(path)[1].split(":")[2] except (OSError, KeyError): err = sys.exc_info()[1] self.logger.debug("POSIX: Could not get current SELinux " @@ -460,7 +465,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): else: secontext = None - if HAS_ACLS: + if HAS_ACLS and not stat.S_ISLNK(ondisk[stat.ST_MODE]): acls = self._list_file_acls(path) else: acls = None @@ -562,9 +567,17 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): wanted = dict() for acl in entry.findall("ACL"): if acl.get("scope") == "user": - scope = posix1e.ACL_USER + if acl.get("user"): + scope = posix1e.ACL_USER + else: + scope = posix1e.ACL_USER_OBJ elif acl.get("scope") == "group": - scope = posix1e.ACL_GROUP + if acl.get("group"): + scope = posix1e.ACL_GROUP + else: + scope = posix1e.ACL_GROUP_OBJ + elif acl.get("scope") == "other": + scope = posix1e.ACL_OTHER else: self.logger.error("POSIX: Unknown ACL scope %s" % acl.get("scope")) @@ -573,7 +586,10 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): self.logger.error("POSIX: No permissions set for ACL: %s" % Bcfg2.Client.XML.tostring(acl)) continue - wanted[(acl.get("type"), scope, acl.get(acl.get("scope")))] = \ + qual = acl.get(acl.get("scope")) + if not qual: + qual = '' + wanted[(acl.get("type"), scope, qual)] = \ self._norm_acl_perms(acl.get('perms')) return wanted @@ -587,11 +603,12 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): """ Given an ACL object, process it appropriately and add it to the return value """ try: + qual = '' if acl.tag_type == posix1e.ACL_USER: qual = pwd.getpwuid(acl.qualifier)[0] elif acl.tag_type == posix1e.ACL_GROUP: qual = grp.getgrgid(acl.qualifier)[0] - else: + elif atype == "access" or acl.tag_type == posix1e.ACL_MASK: return except (OSError, KeyError): err = sys.exc_info()[1] @@ -621,9 +638,38 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): _process_acl(acl, "default") return existing - def _verify_acls(self, entry, path=None): + def _verify_acls(self, entry, path=None): # pylint: disable=R0912 """ verify POSIX ACLs on the given entry. return True if all ACLS are correct, false otherwise """ + def _verify_acl(aclkey, perms): + """ Given ACL data, process it appropriately and add it to + missing or wrong lists if appropriate """ + if aclkey not in existing: + missing.append(self._acl2string(aclkey, perms)) + elif existing[aclkey] != perms: + wrong.append((self._acl2string(aclkey, perms), + self._acl2string(aclkey, existing[aclkey]))) + if path == entry.get("name"): + atype, scope, qual = aclkey + aclentry = Bcfg2.Client.XML.Element("ACL", type=atype, + perms=str(perms)) + if (scope == posix1e.ACL_USER or + scope == posix1e.ACL_USER_OBJ): + aclentry.set("scope", "user") + elif (scope == posix1e.ACL_GROUP or + scope == posix1e.ACL_GROUP_OBJ): + aclentry.set("scope", "group") + elif scope == posix1e.ACL_OTHER: + aclentry.set("scope", "other") + else: + self.logger.debug("POSIX: Unknown ACL scope %s on %s" % + (scope, path)) + return + + if scope != posix1e.ACL_OTHER: + aclentry.set(aclentry.get("scope"), qual) + entry.append(aclentry) + if not HAS_ACLS: if entry.findall("ACL"): self.logger.debug("POSIX: ACLs listed for %s but no pylibacl " @@ -644,25 +690,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool): extra = [] wrong = [] for aclkey, perms in wanted.items(): - if aclkey not in existing: - missing.append(self._acl2string(aclkey, perms)) - elif existing[aclkey] != perms: - wrong.append((self._acl2string(aclkey, perms), - self._acl2string(aclkey, existing[aclkey]))) - if path == entry.get("name"): - atype, scope, qual = aclkey - aclentry = Bcfg2.Client.XML.Element("ACL", type=atype, - perms=str(perms)) - if scope == posix1e.ACL_USER: - aclentry.set("scope", "user") - elif scope == posix1e.ACL_GROUP: - aclentry.set("scope", "group") - else: - self.logger.debug("POSIX: Unknown ACL scope %s on %s" % - (scope, path)) - continue - aclentry.set(aclentry.get("scope"), qual) - entry.append(aclentry) + _verify_acl(aclkey, perms) for aclkey, perms in existing.items(): if aclkey not in wanted: |