diff options
Diffstat (limited to 'src/lib/Bcfg2/Client')
-rw-r--r-- | src/lib/Bcfg2/Client/Client.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Frame.py | 11 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX/File.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIXUsers.py | 300 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/SELinux.py | 117 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/__init__.py | 1 |
6 files changed, 377 insertions, 60 deletions
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py index f197a9074..45e0b64e6 100644 --- a/src/lib/Bcfg2/Client/Client.py +++ b/src/lib/Bcfg2/Client/Client.py @@ -56,8 +56,8 @@ class Client(object): self.logger.error("Service removal is nonsensical; " "removed services will only be disabled") if (self.setup['remove'] and - self.setup['remove'].lower() not in ['all', 'services', - 'packages']): + self.setup['remove'].lower() not in ['all', 'services', 'packages', + 'users']): self.logger.error("Got unknown argument %s for -r" % self.setup['remove']) if self.setup["file"] and self.setup["cache"]: diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py index 53180ab68..4f3ff1820 100644 --- a/src/lib/Bcfg2/Client/Frame.py +++ b/src/lib/Bcfg2/Client/Frame.py @@ -105,6 +105,10 @@ class Frame(object): if deprecated: self.logger.warning("Loaded deprecated tool drivers:") self.logger.warning(deprecated) + experimental = [tool.name for tool in self.tools if tool.experimental] + if experimental: + self.logger.warning("Loaded experimental tool drivers:") + self.logger.warning(experimental) # find entries not handled by any tools self.unhandled = [entry for struct in config @@ -281,12 +285,15 @@ class Frame(object): if self.setup['remove']: if self.setup['remove'] == 'all': self.removal = self.extra - elif self.setup['remove'] in ['services', 'Services']: + elif self.setup['remove'].lower() == 'services': self.removal = [entry for entry in self.extra if entry.tag == 'Service'] - elif self.setup['remove'] in ['packages', 'Packages']: + elif self.setup['remove'].lower() == 'packages': self.removal = [entry for entry in self.extra if entry.tag == 'Package'] + elif self.setup['remove'].lower() == 'users': + self.removal = [entry for entry in self.extra + if entry.tag in ['POSIXUser', 'POSIXGroup']] candidates = [entry for entry in self.states if not self.states[entry]] diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py index 5842c4e1f..9b95d2234 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py @@ -188,6 +188,10 @@ class POSIXFile(POSIXTool): prompt.append(udiff) except UnicodeEncodeError: prompt.append("Could not encode diff") + elif entry.get("empty", "true"): + # the file doesn't exist on disk, but there's no + # expected content + prompt.append("%s does not exist" % entry.get("name")) else: prompt.append("Diff took too long to compute, no " "printable diff") diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py new file mode 100644 index 000000000..78734f5c2 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py @@ -0,0 +1,300 @@ +""" A tool to handle creating users and groups with useradd/mod/del +and groupadd/mod/del """ + +import sys +import pwd +import grp +import Bcfg2.Client.XML +import subprocess +import Bcfg2.Client.Tools + + +class ExecutionError(Exception): + """ Raised when running an external command fails """ + + def __init__(self, msg, retval=None): + Exception.__init__(self, msg) + self.retval = retval + + def __str__(self): + return "%s (rv: %s)" % (Exception.__str__(self), + self.retval) + + +class Executor(object): + """ A better version of Bcfg2.Client.Tool.Executor, which captures + stderr, raises exceptions on error, and doesn't use the shell to + execute by default """ + + def __init__(self, logger): + self.logger = logger + self.stdout = None + self.stderr = None + self.retval = None + + def run(self, command, inputdata=None, shell=False): + """ Run a command, given as a list, optionally giving it the + specified input data """ + self.logger.debug("Running: %s" % " ".join(command)) + proc = subprocess.Popen(command, shell=shell, bufsize=16384, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + if inputdata: + for line in inputdata.splitlines(): + self.logger.debug('> %s' % line) + (self.stdout, self.stderr) = proc.communicate(inputdata) + else: + (self.stdout, self.stderr) = proc.communicate() + for line in self.stdout.splitlines(): # pylint: disable=E1103 + self.logger.debug('< %s' % line) + self.retval = proc.wait() + if self.retval == 0: + for line in self.stderr.splitlines(): # pylint: disable=E1103 + self.logger.warning(line) + return True + else: + raise ExecutionError(self.stderr, self.retval) + + +class POSIXUsers(Bcfg2.Client.Tools.Tool): + """ A tool to handle creating users and groups with + useradd/mod/del and groupadd/mod/del """ + __execs__ = ['/usr/sbin/useradd', '/usr/sbin/usermod', '/usr/sbin/userdel', + '/usr/sbin/groupadd', '/usr/sbin/groupmod', + '/usr/sbin/groupdel'] + __handles__ = [('POSIXUser', None), + ('POSIXGroup', None)] + __req__ = dict(POSIXUser=['name'], + POSIXGroup=['name']) + experimental = True + + # A mapping of XML entry attributes to the indexes of + # corresponding values in the get*ent data structures + attr_mapping = dict(POSIXUser=dict(name=0, uid=2, gecos=4, home=5, + shell=6), + POSIXGroup=dict(name=0, gid=2)) + + def __init__(self, logger, setup, config): + Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + self.set_defaults = dict(POSIXUser=self.populate_user_entry, + POSIXGroup=lambda g: g) + self.cmd = Executor(logger) + self._existing = None + + @property + def existing(self): + """ Get a dict of existing users and groups """ + if self._existing is None: + self._existing = dict(POSIXUser=dict([(u[0], u) + for u in pwd.getpwall()]), + POSIXGroup=dict([(g[0], g) + for g in grp.getgrall()])) + return self._existing + + def Inventory(self, states, structures=None): + if not structures: + structures = self.config.getchildren() + # we calculate a list of all POSIXUser and POSIXGroup entries, + # and then add POSIXGroup entries that are required to create + # the primary group for each user to the structures. this is + # sneaky and possibly evil, but it works great. + groups = [] + for struct in structures: + groups.extend([e.get("name") + for e in struct.findall("POSIXGroup")]) + for struct in structures: + for entry in struct.findall("POSIXUser"): + group = self.set_defaults[entry.tag](entry).get('group') + if group and group not in groups: + self.logger.debug("POSIXUsers: Adding POSIXGroup entry " + "'%s' for user '%s'" % + (group, entry.get("name"))) + struct.append(Bcfg2.Client.XML.Element("POSIXGroup", + name=group)) + return Bcfg2.Client.Tools.Tool.Inventory(self, states, structures) + + def FindExtra(self): + extra = [] + for handles in self.__handles__: + tag = handles[0] + specified = [] + for entry in self.getSupportedEntries(): + if entry.tag == tag: + specified.append(entry.get("name")) + extra.extend([Bcfg2.Client.XML.Element(tag, name=e) + for e in self.existing[tag].keys() + if e not in specified]) + return extra + + def populate_user_entry(self, entry): + """ Given a POSIXUser entry, set all of the 'missing' attributes + with their defaults """ + defaults = dict(group=entry.get('name'), + gecos=entry.get('name'), + shell='/bin/bash') + if entry.get('name') == 'root': + defaults['home'] = '/root' + else: + defaults['home'] = '/home/%s' % entry.get('name') + for key, val in defaults.items(): + if entry.get(key) is None: + entry.set(key, val) + if entry.get('group') in self.existing['POSIXGroup']: + entry.set('gid', + str(self.existing['POSIXGroup'][entry.get('group')][2])) + return entry + + def user_supplementary_groups(self, entry): + """ Get a list of supplmentary groups that the user in the + given entry is a member of """ + return [g for g in self.existing['POSIXGroup'].values() + if entry.get("name") in g[3] and g[0] != entry.get("group")] + + def VerifyPOSIXUser(self, entry, _): + """ Verify a POSIXUser entry """ + rv = self._verify(self.populate_user_entry(entry)) + if entry.get("current_exists", "true") == "true": + # verify supplemental groups + actual = [g[0] for g in self.user_supplementary_groups(entry)] + expected = [e.text for e in entry.findall("MemberOf")] + if set(expected) != set(actual): + entry.set('qtext', + "\n".join([entry.get('qtext', '')] + + ["%s %s has incorrect supplemental group " + "membership. Currently: %s. Should be: %s" + % (entry.tag, entry.get("name"), + actual, expected)])) + rv = False + if self.setup['interactive'] and not rv: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext', ''), entry.tag, entry.get('name'))) + return rv + + def VerifyPOSIXGroup(self, entry, _): + """ Verify a POSIXGroup entry """ + rv = self._verify(entry) + if self.setup['interactive'] and not rv: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext', ''), entry.tag, entry.get('name'))) + return rv + + def _verify(self, entry): + """ Perform most of the actual work of verification """ + errors = [] + if entry.get("name") not in self.existing[entry.tag]: + entry.set('current_exists', 'false') + errors.append("%s %s does not exist" % (entry.tag, + entry.get("name"))) + else: + for attr, idx in self.attr_mapping[entry.tag].items(): + val = str(self.existing[entry.tag][entry.get("name")][idx]) + entry.set("current_%s" % attr, val) + if attr in ["uid", "gid"]: + if entry.get(attr) is None: + # no uid/gid specified, so we let the tool + # automatically determine one -- i.e., it always + # verifies + continue + if val != entry.get(attr): + errors.append("%s for %s %s is incorrect. Current %s is " + "%s, but should be %s" % + (attr.title(), entry.tag, entry.get("name"), + attr, entry.get(attr), val)) + + if errors: + for error in errors: + self.logger.debug("%s: %s" % (self.name, error)) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + return len(errors) == 0 + + def Install(self, entries, states): + for entry in entries: + # install groups first, so that all groups exist for + # users that might need them + if entry.tag == 'POSIXGroup': + states[entry] = self._install(entry) + for entry in entries: + if entry.tag == 'POSIXUser': + states[entry] = self._install(entry) + self._existing = None + + def _install(self, entry): + """ add or modify a user or group using the appropriate command """ + if entry.get("name") not in self.existing[entry.tag]: + action = "add" + else: + action = "mod" + try: + self.cmd.run(self._get_cmd(action, + self.set_defaults[entry.tag](entry))) + self.modified.append(entry) + return True + except ExecutionError: + self.logger.error("POSIXUsers: Error creating %s %s: %s" % + (entry.tag, entry.get("name"), + sys.exc_info()[1])) + return False + + def _get_cmd(self, action, entry): + """ Get a command to perform the appropriate action (add, mod, + del) on the given entry. The command is always the same; we + set all attributes on a given user or group when modifying it + rather than checking which ones need to be changed. This + makes things fail as a unit (e.g., if a user is logged in, you + can't change its home dir, but you could change its GECOS, but + the whole operation fails), but it also makes this function a + lot, lot easier and simpler.""" + cmd = ["/usr/sbin/%s%s" % (entry.tag[5:].lower(), action)] + if action != 'del': + if entry.tag == 'POSIXGroup': + if entry.get('gid'): + cmd.extend(['-g', entry.get('gid')]) + elif entry.tag == 'POSIXUser': + cmd.append('-m') + if entry.get('uid'): + cmd.extend(['-u', entry.get('uid')]) + cmd.extend(['-g', entry.get('group')]) + extras = [e.text for e in entry.findall("MemberOf")] + if extras: + cmd.extend(['-G', ",".join(extras)]) + cmd.extend(['-d', entry.get('home')]) + cmd.extend(['-s', entry.get('shell')]) + cmd.extend(['-c', entry.get('gecos')]) + cmd.append(entry.get('name')) + return cmd + + def Remove(self, entries): + for entry in entries: + # remove users first, so that all users have been removed + # from groups before we remove them + if entry.tag == 'POSIXUser': + self._remove(entry) + for entry in entries: + if entry.tag == 'POSIXGroup': + try: + grp.getgrnam(entry.get("name")) + self._remove(entry) + except KeyError: + # at least some versions of userdel automatically + # remove the primary group for a user if the group + # name is the same as the username, and no other + # users are in the group + self.logger.info("POSIXUsers: Group %s does not exist. " + "It may have already been removed when " + "its users were deleted" % + entry.get("name")) + self._existing = None + self.extra = self.FindExtra() + + def _remove(self, entry): + """ Remove an entry """ + try: + self.cmd.run(self._get_cmd("del", entry)) + return True + except ExecutionError: + self.logger.error("POSIXUsers: Error deleting %s %s: %s" % + (entry.tag, entry.get("name"), + sys.exc_info()[1])) + return False diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py index fc47883c9..7aa0e8a20 100644 --- a/src/lib/Bcfg2/Client/Tools/SELinux.py +++ b/src/lib/Bcfg2/Client/Tools/SELinux.py @@ -58,36 +58,44 @@ def netmask_itoa(netmask, proto="ipv4"): class SELinux(Bcfg2.Client.Tools.Tool): """ SELinux entry support """ name = 'SELinux' - __handles__ = [('SELinux', 'boolean'), - ('SELinux', 'port'), - ('SELinux', 'fcontext'), - ('SELinux', 'node'), - ('SELinux', 'login'), - ('SELinux', 'user'), - ('SELinux', 'interface'), - ('SELinux', 'permissive'), - ('SELinux', 'module')] - __req__ = dict(SELinux=dict(boolean=['name', 'value'], - module=['name'], - port=['name', 'selinuxtype'], - fcontext=['name', 'selinuxtype'], - node=['name', 'selinuxtype', 'proto'], - login=['name', 'selinuxuser'], - user=['name', 'roles', 'prefix'], - interface=['name', 'selinuxtype'], - permissive=['name'])) + __handles__ = [('SEBoolean', None), + ('SEFcontext', None), + ('SEInterface', None), + ('SELogin', None), + ('SEModule', None), + ('SENode', None), + ('SEPermissive', None), + ('SEPort', None), + ('SEUser', None)] + __req__ = dict(SEBoolean=['name', 'value'], + SEFcontext=['name', 'selinuxtype'], + SEInterface=['name', 'selinuxtype'], + SELogin=['name', 'selinuxuser'], + SEModule=['name'], + SENode=['name', 'selinuxtype', 'proto'], + SEPermissive=['name'], + SEPort=['name', 'selinuxtype'], + SEUser=['name', 'roles', 'prefix']) def __init__(self, logger, setup, config): Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) self.handlers = {} - for handles in self.__handles__: - etype = handles[1] + for handler in self.__handles__: + etype = handler[0] self.handlers[etype] = \ globals()["SELinux%sHandler" % etype.title()](self, logger, setup, config) self.txn = False self.post_txn_queue = [] + def __getattr__(self, attr): + if attr.startswith("VerifySE"): + return self.GenericSEVerify + elif attr.startswith("InstallSE"): + return self.GenericSEInstall + else: + return object.__getattr__(self, attr) + def BundleUpdated(self, _, states): for handler in self.handlers.values(): handler.BundleUpdated(states) @@ -100,12 +108,12 @@ class SELinux(Bcfg2.Client.Tools.Tool): def canInstall(self, entry): return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and - self.handlers[entry.get('type')].canInstall(entry)) + self.handlers[entry.tag].canInstall(entry)) def primarykey(self, entry): """ return a string that should be unique amongst all entries in the specification """ - return self.handlers[entry.get('type')].primarykey(entry) + return self.handlers[entry.tag].primarykey(entry) def Install(self, entries, states): # start a transaction @@ -125,32 +133,32 @@ class SELinux(Bcfg2.Client.Tools.Tool): for func, arg, kwargs in self.post_txn_queue: states[arg] = func(*arg, **kwargs) - def InstallSELinux(self, entry): - """Dispatch install to the proper method according to type""" - return self.handlers[entry.get('type')].Install(entry) + def GenericSEInstall(self, entry): + """Dispatch install to the proper method according to entry tag""" + return self.handlers[entry.tag].Install(entry) - def VerifySELinux(self, entry, _): - """Dispatch verify to the proper method according to type""" - rv = self.handlers[entry.get('type')].Verify(entry) + def GenericSEVerify(self, entry, _): + """Dispatch verify to the proper method according to entry tag""" + rv = self.handlers[entry.tag].Verify(entry) if entry.get('qtext') and self.setup['interactive']: entry.set('qtext', - '%s\nInstall SELinux %s %s: (y/N) ' % + '%s\nInstall %s: (y/N) ' % (entry.get('qtext'), - entry.get('type'), - self.handlers[entry.get('type')].tostring(entry))) + self.handlers[entry.tag].tostring(entry))) return rv def Remove(self, entries): - """Dispatch verify to the proper removal method according to type""" + """Dispatch verify to the proper removal + method according to entry tag""" # sort by type types = list() for entry in entries: - if entry.get('type') not in types: - types.append(entry.get('type')) + if entry.tag not in types: + types.append(entry.tag) for etype in types: self.handlers[etype].Remove([e for e in entries - if e.get('type') == etype]) + if e.tag == etype]) class SELinuxEntryHandler(object): @@ -253,8 +261,7 @@ class SELinuxEntryHandler(object): def key2entry(self, key): """ Generate an XML entry from an SELinux record key """ attrs = self._key2attrs(key) - attrs["type"] = self.etype - return Bcfg2.Client.XML.Element("SELinux", **attrs) + return Bcfg2.Client.XML.Element(self.etype, **attrs) def _args(self, entry, method): """ Get the argument list for invoking _modify or _add, or @@ -279,7 +286,7 @@ class SELinuxEntryHandler(object): """ return a string that should be unique amongst all entries in the specification. some entry types are not universally disambiguated by tag:type:name alone """ - return ":".join([entry.tag, entry.get("type"), entry.get("name")]) + return ":".join([entry.tag, entry.get("name")]) def exists(self, entry): """ return True if the entry already exists in the record list """ @@ -303,8 +310,8 @@ class SELinuxEntryHandler(object): continue if current_attrs[attr] != desired_attrs[attr]: entry.set('current_%s' % attr, current_attrs[attr]) - errors.append("SELinux %s %s has wrong %s: %s, should be %s" % - (self.etype, self.tostring(entry), attr, + errors.append("%s %s has wrong %s: %s, should be %s" % + (entry.tag, entry.get('name'), attr, current_attrs[attr], desired_attrs[attr])) if errors: @@ -331,8 +338,8 @@ class SELinuxEntryHandler(object): return True except ValueError: err = sys.exc_info()[1] - self.logger.debug("Failed to %s SELinux %s %s: %s" % - (method, self.etype, self.tostring(entry), err)) + self.logger.info("Failed to %s SELinux %s %s: %s" % + (method, self.etype, self.tostring(entry), err)) return False def Remove(self, entries): @@ -365,7 +372,7 @@ class SELinuxEntryHandler(object): pass -class SELinuxBooleanHandler(SELinuxEntryHandler): +class SELinuxSebooleanHandler(SELinuxEntryHandler): """ handle SELinux boolean entries """ etype = "boolean" value_format = ("value",) @@ -414,7 +421,7 @@ class SELinuxBooleanHandler(SELinuxEntryHandler): SELinuxEntryHandler.canInstall(self, entry)) -class SELinuxPortHandler(SELinuxEntryHandler): +class SELinuxSeportHandler(SELinuxEntryHandler): """ handle SELinux port entries """ etype = "port" value_format = ('selinuxtype', None) @@ -486,7 +493,7 @@ class SELinuxPortHandler(SELinuxEntryHandler): return tuple(entry.get("name").split("/")) -class SELinuxFcontextHandler(SELinuxEntryHandler): +class SELinuxSefcontextHandler(SELinuxEntryHandler): """ handle SELinux file context entries """ etype = "fcontext" @@ -556,11 +563,11 @@ class SELinuxFcontextHandler(SELinuxEntryHandler): '', '') def primarykey(self, entry): - return ":".join([entry.tag, entry.get("type"), entry.get("name"), + return ":".join([entry.tag, entry.get("name"), entry.get("filetype", "all")]) -class SELinuxNodeHandler(SELinuxEntryHandler): +class SELinuxSenodeHandler(SELinuxEntryHandler): """ handle SELinux node entries """ etype = "node" @@ -592,7 +599,7 @@ class SELinuxNodeHandler(SELinuxEntryHandler): entry.get("selinuxtype")) -class SELinuxLoginHandler(SELinuxEntryHandler): +class SELinuxSeloginHandler(SELinuxEntryHandler): """ handle SELinux login entries """ etype = "login" @@ -603,7 +610,7 @@ class SELinuxLoginHandler(SELinuxEntryHandler): return (entry.get("name"), entry.get("selinuxuser"), "") -class SELinuxUserHandler(SELinuxEntryHandler): +class SELinuxSeuserHandler(SELinuxEntryHandler): """ handle SELinux user entries """ etype = "user" @@ -652,7 +659,7 @@ class SELinuxUserHandler(SELinuxEntryHandler): return tuple(rv) -class SELinuxInterfaceHandler(SELinuxEntryHandler): +class SELinuxSeinterfaceHandler(SELinuxEntryHandler): """ handle SELinux interface entries """ etype = "interface" @@ -663,7 +670,7 @@ class SELinuxInterfaceHandler(SELinuxEntryHandler): return (entry.get("name"), '', entry.get("selinuxtype")) -class SELinuxPermissiveHandler(SELinuxEntryHandler): +class SELinuxSepermissiveHandler(SELinuxEntryHandler): """ handle SELinux permissive domain entries """ etype = "permissive" @@ -695,7 +702,7 @@ class SELinuxPermissiveHandler(SELinuxEntryHandler): return (entry.get("name"),) -class SELinuxModuleHandler(SELinuxEntryHandler): +class SELinuxSemoduleHandler(SELinuxEntryHandler): """ handle SELinux module entries """ etype = "module" @@ -808,10 +815,9 @@ class SELinuxModuleHandler(SELinuxEntryHandler): def Install(self, entry, _=None): if not self.filetool.install(self._pathentry(entry)): return False - if hasattr(self, 'records'): + if hasattr(seobject, 'moduleRecords'): # if seobject has the moduleRecords attribute, install the # module using the seobject library - self.records # pylint: disable=W0104 return self._install_seobject(entry) else: # seobject doesn't have the moduleRecords attribute, so @@ -891,8 +897,7 @@ class SELinuxModuleHandler(SELinuxEntryHandler): def FindExtra(self): specified = [self._key(e) - for e in self.tool.getSupportedEntries() - if e.get("type") == self.etype] + for e in self.tool.getSupportedEntries()] rv = [] for module in self._all_records_from_filesystem().keys(): if module not in specified: diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index 927b25ba8..d5f55759f 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -61,6 +61,7 @@ class Tool(object): __req__ = {} __important__ = [] deprecated = False + experimental = False def __init__(self, logger, setup, config): self.setup = setup |