diff options
author | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-12-03 10:51:34 -0600 |
---|---|---|
committer | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-12-03 10:52:13 -0600 |
commit | 33234d5dae565e6520bbdb65d67fbaed03df4d43 (patch) | |
tree | 232ec275370a5d186095bf289897395d329c7232 | |
parent | 1d4b0118ced1b198587fd75c549e2b394ff71531 (diff) | |
download | bcfg2-33234d5dae565e6520bbdb65d67fbaed03df4d43.tar.gz bcfg2-33234d5dae565e6520bbdb65d67fbaed03df4d43.tar.bz2 bcfg2-33234d5dae565e6520bbdb65d67fbaed03df4d43.zip |
added builtin support for creating users and groups
-rw-r--r-- | doc/client/tools/posixusers.txt | 51 | ||||
-rw-r--r-- | doc/server/plugins/generators/rules.txt | 132 | ||||
-rw-r--r-- | schemas/bundle.xsd | 30 | ||||
-rw-r--r-- | schemas/rules.xsd | 14 | ||||
-rw-r--r-- | schemas/types.xsd | 17 | ||||
-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/POSIXUsers.py | 300 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/__init__.py | 1 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py | 20 | ||||
-rw-r--r-- | testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py | 489 | ||||
-rw-r--r-- | tools/README | 4 | ||||
-rwxr-xr-x | tools/posixusers_baseline.py | 73 |
13 files changed, 1099 insertions, 47 deletions
diff --git a/doc/client/tools/posixusers.txt b/doc/client/tools/posixusers.txt new file mode 100644 index 000000000..884edc2b7 --- /dev/null +++ b/doc/client/tools/posixusers.txt @@ -0,0 +1,51 @@ +.. -*- mode: rst -*- + +.. _client-tools-posixusers: + +========== +POSIXUsers +========== + +The POSIXUsers tool handles the creation of users and groups as +defined by ``POSIXUser`` and ``POSIXGroup`` entries. For a full +description of those tags, see :ref:`server-plugins-generators-rules`. + +The POSIXUsers tool relies on the ``useradd``, ``usermod``, +``userdel``, ``groupadd``, ``groupmod``, and ``groupdel`` tools, since +there is no Python library to manage users and groups. It expects +those tools to be in ``/usr/sbin``. + +Primary group creation +====================== + +Each user must have a primary group, which can be specified with the +``group`` attribute of the ``POSIXUser`` tag. (If the ``group`` +attribute is not specified, then a group with the same name as the +user will be used.) If that group does not exist, the POSIXUsers tool +will create it automatically. It does this by adding a ``POSIXGroup`` +entry on the fly; this has a few repercussions: + +* When run in interactive mode (``-I``), Bcfg2 will prompt for + installation of the group separately from the user. +* The ``POSIXGroup`` entry is added to the same bundle as the + ``POSIXUser`` entry, so if the group is created, the bundle is + considered to have been modified and consequently Actions will be + run and Services will be restarted. This should never be a concern, + since the group can only be created, not modified (it has no + attributes other than its name), and if the group is being created + then the user will certainly be created or modified as well. +* The group is created with no specified GID number. If you need to + specify a particular GID number, you must explicitly define a + ``POSIXGroup`` entry for the group. + +Creating a baseline configuration +================================= + +The majority of users on many systems are created by the packages that +are installed, but currently Bcfg2 cannot query the package database +to determine these users. (In some cases, this is a limitation of the +packaging system.) The often-tedious task of creating a baseline that +defines all users and groups can be simplified by use of the +``tools/posixusers_baseline.py`` script, which outputs a bundle +containing all users and groups on the machine it's run on. + diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt index 65eb0c5d9..cdde65960 100644 --- a/doc/server/plugins/generators/rules.txt +++ b/doc/server/plugins/generators/rules.txt @@ -62,10 +62,10 @@ The Rules Tag may have the following attributes: | | Rules list.The higher value wins. | | +----------+-------------------------------------+--------+ -Rules Group Tag ---------------- +Group Tag +--------- -The Rules Group Tag may have the following attributes: +The Group Tag may have the following attributes: +--------+-------------------------+--------------+ | Name | Description | Values | @@ -76,6 +76,27 @@ The Rules Group Tag may have the following attributes: | | (is not a member of) | | +--------+-------------------------+--------------+ +Client Tag +---------- + +The Client Tag is used in Rules for selecting the package entries to +include in the clients literal configuration. Its function is similar +to the Group tag in this context. It can be thought of as:: + + if client is name then + assign to literal config + +The Client Tag may have the following attributes: + ++--------+-------------------------+--------------+ +| Name | Description | Values | ++========+=========================+==============+ +| name | Client Name | String | ++--------+-------------------------+--------------+ +| negate | Negate client selection | (true|false) | +| | (if not client name) | | ++--------+-------------------------+--------------+ + Package Tag ----------- @@ -84,8 +105,7 @@ The Package Tag may have the following attributes: +------------+----------------------------------------------+----------+ | Name | Description | Values | +============+==============================================+==========+ -| name | Package name or regular expression | String | -| | | or regex | +| name | Package name | String | +------------+----------------------------------------------+----------+ | version | Package Version or version='noverify' to | String | | | not do version checking in the Yum driver | | @@ -131,8 +151,7 @@ Service Tag | | service (new in 1.3; replaces | | | | "mode" attribute) | | +------------+-------------------------------+---------------------------------------------------------+ -| name | Service name or regular | String or regex | -| | expression | | +| name | Service name | String | +------------+-------------------------------+---------------------------------------------------------+ | status | Should the service be on or | (on | off | ignore) | | | off (default: off). | | @@ -193,27 +212,6 @@ Service status descriptions * Don't perform service status checks. -Client Tag ----------- - -The Client Tag is used in Rules for selecting the package entries to -include in the clients literal configuration. Its function is similar -to the Group tag in this context. It can be thought of as:: - - if client is name then - assign to literal config - -The Client Tag may have the following attributes: - -+--------+-------------------------+--------------+ -| Name | Description | Values | -+========+=========================+==============+ -| name | Client Name | String | -+--------+-------------------------+--------------+ -| negate | Negate client selection | (true|false) | -| | (if not client name) | | -+--------+-------------------------+--------------+ - Path Tag -------- @@ -229,11 +227,11 @@ the context of the file to the default set by policy. See Attributes common to all Path tags: -+----------+---------------------------------------------------+-----------------+ -| Name | Description | Values | -+==========+===================================================+=================+ -| name | Full path or regular expression matching the path | String or regex | -+----------+---------------------------------------------------+-----------------+ ++----------+-------------+--------+ +| Name | Description | Values | ++==========+=============+========+ +| name | Full path | String | ++----------+-------------+--------+ device @@ -517,6 +515,74 @@ SEModule Tag See :ref:`server-plugins-generators-semodules` +POSIXUser Tag +------------- + +The POSIXUser tag allows you to create users on client machines. It +takes the following attributes: + ++-------+-----------------------+---------+-------------------------------+ +| Name | Description | Values | Default | ++=======+=======================+=========+===============================+ +| name | Username | String | None | ++-------+-----------------------+---------+-------------------------------+ +| uid | User ID number | Integer | The client sets the uid | ++-------+-----------------------+---------+-------------------------------+ +| group | Name of the user's | String | The username | +| | primary group | | | ++-------+-----------------------+---------+-------------------------------+ +| gecos | Human-readable user | String | The username | +| | name or comment | | | ++-------+-----------------------+---------+-------------------------------+ +| home | User's home directory | String | /root (for "root"); | +| | | | /home/<username> otherwise | ++-------+-----------------------+---------+-------------------------------+ +| shell | User's shell | String | /bin/bash | ++-------+-----------------------+---------+-------------------------------+ + +The group specified will automatically be created if it does not +exist, even if there is no `POSIXGroup Tag`_ for it. If you need to +specify a particular GID for the group, you must specify that in a +``POSIXGroup`` tag. + +If you with to change the default shell, you can do so with :ref:`the +Defaults plugin <server-plugins-structures-defaults>`. + +Additionally, a user may be a member of supplementary groups. These +can be specified with the ``MemberOf`` child tag of the ``POSIXUser`` +tag. + +For example: + +.. code-block:: xml + + <POSIXUser name="daemon" home="/sbin" shell="/sbin/nologin" + gecos="daemon" uid="2" group="daemon"> + <MemberOf>lp</MemberOf> + <MemberOf>adm</MemberOf> + <MemberOf>bin</MemberOf> + </BoundPOSIXUser> + +See :ref:`client-tools-posixusers` for more information on managing +users and groups. + +POSIXGroup Tag +-------------- + +The POSIXGroup tag allows you to create groups on client machines. It +takes the following attributes: + ++-------+-------------------+---------+-------------------------+ +| Name | Description | Values | Default | ++=======+===================+=========+=========================+ +| name | Name of the group | String | None | ++-------+-------------------+---------+-------------------------+ +| gid | Group ID number | Integer | The client sets the gid | ++-------+-------------------+---------+-------------------------+ + +See :ref:`client-tools-posixusers` for more information on managing +users and groups. + Rules Directory =============== diff --git a/schemas/bundle.xsd b/schemas/bundle.xsd index 6306b6da4..1fcf82c27 100644 --- a/schemas/bundle.xsd +++ b/schemas/bundle.xsd @@ -36,7 +36,7 @@ <xsd:documentation> Abstract implementation of a Path entry. The entry will either be handled by Cfg, TGenshi, or another - DirectoryBacked plugin; or handled by Rules, in which case + Generator plugin; or handled by Rules, in which case the full specification of this entry will be included in Rules. </xsd:documentation> @@ -66,6 +66,20 @@ </xsd:documentation> </xsd:annotation> </xsd:element> + <xsd:element name='POSIXUser' type='StructureEntry'> + <xsd:annotation> + <xsd:documentation> + Abstract description of a POSIXUser entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> + <xsd:element name='POSIXGroup' type='StructureEntry'> + <xsd:annotation> + <xsd:documentation> + Abstract description of a POSIXGroup entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> <xsd:element name='PostInstall' type='StructureEntry'> <xsd:annotation> <xsd:documentation> @@ -111,6 +125,20 @@ </xsd:documentation> </xsd:annotation> </xsd:element> + <xsd:element name='BoundPOSIXUser' type='POSIXUserType'> + <xsd:annotation> + <xsd:documentation> + Fully bound description of a POSIXUser entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> + <xsd:element name='BoundPOSIXGroup' type='POSIXGroupType'> + <xsd:annotation> + <xsd:documentation> + Fully bound description of a POSIXGroup entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> <xsd:element name='Group' type='GroupType'> <xsd:annotation> <xsd:documentation> diff --git a/schemas/rules.xsd b/schemas/rules.xsd index 2f4f805c0..241ffe5bf 100644 --- a/schemas/rules.xsd +++ b/schemas/rules.xsd @@ -57,6 +57,20 @@ </xsd:documentation> </xsd:annotation> </xsd:element> + <xsd:element name='POSIXUser' type='POSIXUserType'> + <xsd:annotation> + <xsd:documentation> + Fully bound description of a POSIXUser entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> + <xsd:element name='POSIXGroup' type='POSIXGroupType'> + <xsd:annotation> + <xsd:documentation> + Fully bound description of a POSIXGroup entry. + </xsd:documentation> + </xsd:annotation> + </xsd:element> <xsd:element name='PostInstall' type='PostInstallType'> <xsd:annotation> <xsd:documentation> diff --git a/schemas/types.xsd b/schemas/types.xsd index 1edde8754..a36693b2d 100644 --- a/schemas/types.xsd +++ b/schemas/types.xsd @@ -220,4 +220,21 @@ <xsd:attribute type="xsd:string" name="selinuxuser"/> <xsd:attributeGroup ref="py:genshiAttrs"/> </xsd:complexType> + + <xsd:complexType name="POSIXUserType"> + <xsd:choice minOccurs='0' maxOccurs='unbounded'> + <xsd:element name='MemberOf' type='xsd:string'/> + </xsd:choice> + <xsd:attribute type="xsd:string" name="name" use="required"/> + <xsd:attribute type="xsd:integer" name="uid"/> + <xsd:attribute type="xsd:string" name="group"/> + <xsd:attribute type="xsd:string" name="gecos"/> + <xsd:attribute type="xsd:string" name="home"/> + <xsd:attribute type="xsd:string" name="shell"/> + </xsd:complexType> + + <xsd:complexType name="POSIXGroupType"> + <xsd:attribute type="xsd:string" name="name" use="required"/> + <xsd:attribute type="xsd:integer" name="gid"/> + </xsd:complexType> </xsd:schema> 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/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/__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 diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py index e503ebd38..4048be7ca 100644 --- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py @@ -16,12 +16,14 @@ while path != "/": path = os.path.dirname(path) from common import * + def get_config(entries): config = lxml.etree.Element("Configuration") bundle = lxml.etree.SubElement(config, "Bundle", name="test") bundle.extend(entries) return config + def get_posix_object(logger=None, setup=None, config=None): if config is None: config = lxml.etree.Element("Configuration") @@ -36,7 +38,7 @@ def get_posix_object(logger=None, setup=None, config=None): if not setup: setup = MagicMock() return Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config) - + class TestPOSIX(Bcfg2TestCase): def setUp(self): @@ -55,7 +57,7 @@ class TestPOSIX(Bcfg2TestCase): self.assertGreater(len(posix.__req__['Path']), 0) self.assertGreater(len(posix.__handles__), 0) self.assertItemsEqual(posix.handled, entries) - + @patch("Bcfg2.Client.Tools.Tool.canVerify") def test_canVerify(self, mock_canVerify): entry = lxml.etree.Element("Path", name="test", type="file") @@ -64,7 +66,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canVerify.return_value = False self.assertFalse(self.posix.canVerify(entry)) mock_canVerify.assert_called_with(self.posix, entry) - + # next, test fully_specified failure self.posix.logger.error.reset_mock() mock_canVerify.reset_mock() @@ -77,7 +79,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canVerify.assert_called_with(self.posix, entry) mock_fully_spec.assert_called_with(entry) self.assertTrue(self.posix.logger.error.called) - + # finally, test success self.posix.logger.error.reset_mock() mock_canVerify.reset_mock() @@ -96,7 +98,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canInstall.return_value = False self.assertFalse(self.posix.canInstall(entry)) mock_canInstall.assert_called_with(self.posix, entry) - + # next, test fully_specified failure self.posix.logger.error.reset_mock() mock_canInstall.reset_mock() @@ -109,7 +111,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canInstall.assert_called_with(self.posix, entry) mock_fully_spec.assert_called_with(entry) self.assertTrue(self.posix.logger.error.called) - + # finally, test success self.posix.logger.error.reset_mock() mock_canInstall.reset_mock() @@ -177,7 +179,7 @@ class TestPOSIX(Bcfg2TestCase): posix._prune_old_backups(entry) mock_listdir.assert_called_with(setup['ppath']) - self.assertItemsEqual(mock_remove.call_args_list, + self.assertItemsEqual(mock_remove.call_args_list, [call(os.path.join(setup['ppath'], p)) for p in remove]) @@ -189,7 +191,7 @@ class TestPOSIX(Bcfg2TestCase): # need to be removed even if we get an error posix._prune_old_backups(entry) mock_listdir.assert_called_with(setup['ppath']) - self.assertItemsEqual(mock_remove.call_args_list, + self.assertItemsEqual(mock_remove.call_args_list, [call(os.path.join(setup['ppath'], p)) for p in remove]) self.assertTrue(posix.logger.error.called) @@ -203,7 +205,7 @@ class TestPOSIX(Bcfg2TestCase): entry = lxml.etree.Element("Path", name="/etc/foo", type="file") setup = dict(ppath='/', max_copies=5, paranoid=False) posix = get_posix_object(setup=setup) - + # paranoid false globally posix._paranoid_backup(entry) self.assertFalse(mock_prune.called) diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py new file mode 100644 index 000000000..46ae4e47b --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py @@ -0,0 +1,489 @@ +import os +import sys +import copy +import lxml.etree +import subprocess +from mock import Mock, MagicMock, patch +import Bcfg2.Client.Tools +from Bcfg2.Client.Tools.POSIXUsers import * + +# add all parent testsuite directories to sys.path to allow (most) +# relative imports in python 2.4 +path = os.path.dirname(__file__) +while path != "/": + if os.path.basename(path).lower().startswith("test"): + sys.path.append(path) + if os.path.basename(path) == "testsuite": + break + path = os.path.dirname(path) +from common import * + + +class TestExecutor(Bcfg2TestCase): + test_obj = Executor + + def get_obj(self, logger=None): + if not logger: + def print_msg(msg): + print(msg) + logger = Mock() + logger.error = Mock(side_effect=print_msg) + logger.warning = Mock(side_effect=print_msg) + logger.info = Mock(side_effect=print_msg) + logger.debug = Mock(side_effect=print_msg) + return self.test_obj(logger) + + @patch("subprocess.Popen") + def test_run(self, mock_Popen): + exc = self.get_obj() + cmd = ["/bin/test", "-a", "foo"] + proc = Mock() + proc.wait = Mock() + proc.wait.return_value = 0 + proc.communicate = Mock() + proc.communicate.return_value = (MagicMock(), MagicMock()) + mock_Popen.return_value = proc + + self.assertTrue(exc.run(cmd)) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], False) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with() + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + mock_Popen.reset_mock() + inputdata = "foo\n\nbar" + self.assertTrue(exc.run(cmd, inputdata=inputdata, shell=True)) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], True) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with(inputdata) + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + mock_Popen.reset_mock() + proc.wait.return_value = 1 + self.assertRaises(ExecutionError, exc.run, cmd) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], False) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with() + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + +class TestPOSIXUsers(Bcfg2TestCase): + test_obj = POSIXUsers + + def get_obj(self, logger=None, setup=None, config=None): + if config is None: + config = lxml.etree.Element("Configuration") + if not logger: + def print_msg(msg): + print(msg) + logger = Mock() + logger.error = Mock(side_effect=print_msg) + logger.warning = Mock(side_effect=print_msg) + logger.info = Mock(side_effect=print_msg) + logger.debug = Mock(side_effect=print_msg) + if not setup: + setup = MagicMock() + return self.test_obj(logger, setup, config) + + @patch("pwd.getpwall") + @patch("grp.getgrall") + def test_existing(self, mock_getgrall, mock_getpwall): + users = self.get_obj() + mock_getgrall.return_value = MagicMock() + mock_getpwall.return_value = MagicMock() + + def reset(): + mock_getgrall.reset_mock() + mock_getpwall.reset_mock() + + # make sure we start clean + self.assertIsNone(users._existing) + self.assertIsInstance(users.existing, dict) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + mock_getgrall.assert_called_with() + mock_getpwall.assert_called_with() + + reset() + self.assertIsInstance(users._existing, dict) + self.assertIsInstance(users.existing, dict) + self.assertEqual(users.existing, users._existing) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + self.assertFalse(mock_getgrall.called) + self.assertFalse(mock_getpwall.called) + + reset() + users._existing = None + self.assertIsInstance(users.existing, dict) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + mock_getgrall.assert_called_with() + mock_getpwall.assert_called_with() + + @patch("Bcfg2.Client.Tools.Tool.Inventory") + def test_Inventory(self, mock_Inventory): + config = lxml.etree.Element("Configuration") + bundle = lxml.etree.SubElement(config, "Bundle", name="test") + lxml.etree.SubElement(bundle, "POSIXUser", name="test", group="test") + lxml.etree.SubElement(bundle, "POSIXUser", name="test2", group="test2") + lxml.etree.SubElement(bundle, "POSIXGroup", name="test2") + + orig_bundle = copy.deepcopy(bundle) + + users = self.get_obj(config=config) + users.set_defaults['POSIXUser'] = Mock() + users.set_defaults['POSIXUser'].side_effect = lambda e: e + + states = dict() + self.assertEqual(users.Inventory(states), + mock_Inventory.return_value) + mock_Inventory.assert_called_with(users, states, config.getchildren()) + lxml.etree.SubElement(orig_bundle, "POSIXGroup", name="test") + self.assertXMLEqual(orig_bundle, bundle) + + def test_FindExtra(self): + users = self.get_obj() + + def getSupportedEntries(): + return [lxml.etree.Element("POSIXUser", name="test1"), + lxml.etree.Element("POSIXGroup", name="test1")] + + users.getSupportedEntries = Mock() + users.getSupportedEntries.side_effect = getSupportedEntries + + users._existing = dict(POSIXUser=dict(test1=(), + test2=()), + POSIXGroup=dict(test2=())) + extra = users.FindExtra() + self.assertEqual(len(extra), 2) + self.assertItemsEqual([e.tag for e in extra], + ["POSIXUser", "POSIXGroup"]) + self.assertItemsEqual([e.get("name") for e in extra], + ["test2", "test2"]) + + def test_populate_user_entry(self): + users = self.get_obj() + users._existing = dict(POSIXUser=dict(), + POSIXGroup=dict(root=('root', 'x', 0, []))) + + cases = [(lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="test", shell="/bin/bash", + home="/home/test")), + (lxml.etree.Element("POSIXUser", name="root", gecos="Root", + shell="/bin/zsh"), + lxml.etree.Element("POSIXUser", name="root", group='root', + gid='0', gecos="Root", shell="/bin/zsh", + home='/root')), + (lxml.etree.Element("POSIXUser", name="test2", gecos="", + shell="/bin/zsh"), + lxml.etree.Element("POSIXUser", name="test2", group='test2', + gecos="", shell="/bin/zsh", + home='/home/test2'))] + + for initial, expected in cases: + actual = users.populate_user_entry(initial) + self.assertXMLEqual(actual, expected) + + def test_user_supplementary_groups(self): + users = self.get_obj() + users._existing = \ + dict(POSIXUser=dict(), + POSIXGroup=dict(root=('root', 'x', 0, []), + wheel=('wheel', 'x', 10, ['test']), + users=('users', 'x', 100, ['test']))) + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertItemsEqual(users.user_supplementary_groups(entry), + [users.existing['POSIXGroup']['wheel'], + users.existing['POSIXGroup']['users']]) + entry.set('name', 'test2') + self.assertItemsEqual(users.user_supplementary_groups(entry), []) + + def test_VerifyPOSIXUser(self): + users = self.get_obj() + users._verify = Mock() + users._verify.return_value = True + users.populate_user_entry = Mock() + users.user_supplementary_groups = Mock() + users.user_supplementary_groups.return_value = \ + [('wheel', 'x', 10, ['test']), ('users', 'x', 100, ['test'])] + + def reset(): + users._verify.reset_mock() + users.populate_user_entry.reset_mock() + users.user_supplementary_groups.reset_mock() + + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + m1 = lxml.etree.SubElement(entry, "MemberOf") + m1.text = "wheel" + m2 = lxml.etree.SubElement(entry, "MemberOf") + m2.text = "users" + self.assertTrue(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + m3 = lxml.etree.SubElement(entry, "MemberOf") + m3.text = "extra" + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + def _verify(entry): + entry.set("current_exists", "false") + return False + + users._verify.side_effect = _verify + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + + def test_VerifyPOSIXGroup(self): + users = self.get_obj() + users._verify = Mock() + entry = lxml.etree.Element("POSIXGroup", name="test") + self.assertEqual(users._verify.return_value, + users.VerifyPOSIXGroup(entry, [])) + + def test__verify(self): + users = self.get_obj() + users._existing = \ + dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest', + '/home/test', '/bin/zsh')), + POSIXGroup=dict(test=('test', 'x', 1000, []))) + + entry = lxml.etree.Element("POSIXUser", name="nonexistent") + self.assertFalse(users._verify(entry)) + self.assertEqual(entry.get("current_exists"), "false") + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Bogus", shell="/bin/bash", + home="/home/test") + self.assertFalse(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test") + self.assertTrue(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test", uid="1000", gid="1000") + self.assertTrue(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test", uid="1001") + self.assertFalse(users._verify(entry)) + + def test_Install(self): + users = self.get_obj() + users._install = Mock() + users._existing = MagicMock() + + + entries = [lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXGroup", name="test"), + lxml.etree.Element("POSIXUser", name="test2")] + states = dict() + + users.Install(entries, states) + self.assertItemsEqual(entries, states.keys()) + for state in states.values(): + self.assertEqual(state, users._install.return_value) + # need to verify two things about _install calls: + # 1) _install was called for each entry; + # 2) _install was called for all groups before any users + self.assertItemsEqual(users._install.call_args_list, + [call(e) for e in entries]) + users_started = False + for args in users._install.call_args_list: + if args[0][0].tag == "POSIXUser": + users_started = True + elif users_started: + assert False, "_install() called on POSIXGroup after installing one or more POSIXUsers" + + def test__install(self): + users = self.get_obj() + users._get_cmd = Mock() + users.cmd = Mock() + users.set_defaults = dict(POSIXUser=Mock(), POSIXGroup=Mock()) + users._existing = \ + dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest', + '/home/test', '/bin/zsh')), + POSIXGroup=dict(test=('test', 'x', 1000, []))) + + def reset(): + users._get_cmd.reset_mock() + users.cmd.reset_mock() + for setter in users.set_defaults.values(): + setter.reset_mock() + users.modified = [] + + reset() + entry = lxml.etree.Element("POSIXUser", name="test2") + self.assertTrue(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("add", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertIn(entry, users.modified) + + reset() + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertTrue(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("mod", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertIn(entry, users.modified) + + reset() + users.cmd.run.side_effect = ExecutionError(None) + self.assertFalse(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("mod", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertNotIn(entry, users.modified) + + def test__get_cmd(self): + users = self.get_obj() + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest") + m1 = lxml.etree.SubElement(entry, "MemberOf") + m1.text = "wheel" + m2 = lxml.etree.SubElement(entry, "MemberOf") + m2.text = "users" + + cases = [(lxml.etree.Element("POSIXGroup", name="test"), []), + (lxml.etree.Element("POSIXGroup", name="test", gid="1001"), + ["-g", "1001"]), + (lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest"), + ["-m", "-g", "test", "-d", "/home/test", "-s", "/bin/zsh", + "-c", "Test McTest"]), + (lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest", uid="1001"), + ["-m", "-u", "1001", "-g", "test", "-d", "/home/test", + "-s", "/bin/zsh", "-c", "Test McTest"]), + (entry, + ["-m", "-g", "test", "-G", "wheel,users", "-d", "/home/test", + "-s", "/bin/zsh", "-c", "Test McTest"])] + for entry, expected in cases: + for action in ["add", "mod", "del"]: + actual = users._get_cmd(action, entry) + if entry.tag == "POSIXGroup": + etype = "group" + else: + etype = "user" + self.assertEqual(actual[0], "/usr/sbin/%s%s" % (etype, action)) + self.assertEqual(actual[-1], entry.get("name")) + if action != "del": + self.assertItemsEqual(actual[1:-1], expected) + + @patch("grp.getgrnam") + def test_Remove(self, mock_getgrnam): + users = self.get_obj() + users._remove = Mock() + users.FindExtra = Mock() + users._existing = MagicMock() + users.extra = MagicMock() + + def reset(): + users._remove.reset_mock() + users.FindExtra.reset_mock() + users._existing = MagicMock() + users.extra = MagicMock() + mock_getgrnam.reset_mock() + + entries = [lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXGroup", name="test"), + lxml.etree.Element("POSIXUser", name="test2")] + + users.Remove(entries) + self.assertIsNone(users._existing) + users.FindExtra.assert_called_with() + self.assertEqual(users.extra, users.FindExtra.return_value) + mock_getgrnam.assert_called_with("test") + # need to verify two things about _remove calls: + # 1) _remove was called for each entry; + # 2) _remove was called for all users before any groups + self.assertItemsEqual(users._remove.call_args_list, + [call(e) for e in entries]) + groups_started = False + for args in users._remove.call_args_list: + if args[0][0].tag == "POSIXGroup": + groups_started = True + elif groups_started: + assert False, "_remove() called on POSIXUser after removing one or more POSIXGroups" + + reset() + mock_getgrnam.side_effect = KeyError + users.Remove(entries) + self.assertIsNone(users._existing) + users.FindExtra.assert_called_with() + self.assertEqual(users.extra, users.FindExtra.return_value) + mock_getgrnam.assert_called_with("test") + self.assertItemsEqual(users._remove.call_args_list, + [call(e) for e in entries + if e.tag == "POSIXUser"]) + + def test__remove(self): + users = self.get_obj() + users._get_cmd = Mock() + users.cmd = Mock() + + def reset(): + users._get_cmd.reset_mock() + users.cmd.reset_mock() + + + entry = lxml.etree.Element("POSIXUser", name="test2") + self.assertTrue(users._remove(entry)) + users._get_cmd.assert_called_with("del", entry) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + + reset() + users.cmd.run.side_effect = ExecutionError(None) + self.assertFalse(users._remove(entry)) + users._get_cmd.assert_called_with("del", entry) + users.cmd.run.assert_called_with(users._get_cmd.return_value) diff --git a/tools/README b/tools/README index 400cfc55c..335363898 100644 --- a/tools/README +++ b/tools/README @@ -82,6 +82,10 @@ pkgmgr_update.py - Update Pkgmgr XML files from a list of directories that contain RPMS +posixusers_baseline.py + - Create a Bundle with all base POSIXUser/POSIXGroup entries on a + client. + rpmlisting.py - Generate Pkgmgr XML files for RPM packages diff --git a/tools/posixusers_baseline.py b/tools/posixusers_baseline.py new file mode 100755 index 000000000..a4abca42d --- /dev/null +++ b/tools/posixusers_baseline.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import grp +import sys +import logging +import lxml.etree +import Bcfg2.Logger +from Bcfg2.Client.Tools.POSIXUsers import POSIXUsers +from Bcfg2.Options import OptionParser, Option, get_bool, CLIENT_COMMON_OPTIONS + + +def get_setup(): + optinfo = CLIENT_COMMON_OPTIONS + optinfo['nouids'] = Option("Do not include UID numbers for users", + default=False, + cmd='--no-uids', + long_arg=True, + cook=get_bool) + optinfo['nogids'] = Option("Do not include GID numbers for groups", + default=False, + cmd='--no-gids', + long_arg=True, + cook=get_bool) + setup = OptionParser(optinfo) + setup.parse(sys.argv[1:]) + + if setup['args']: + print("posixuser_[baseline.py takes no arguments, only options") + print(setup.buildHelpMessage()) + raise SystemExit(1) + level = 30 + if setup['verbose']: + level = 20 + if setup['debug']: + level = 0 + Bcfg2.Logger.setup_logging('posixusers_baseline.py', + to_syslog=False, + level=level, + to_file=setup['logging']) + return setup + + +def main(): + setup = get_setup() + if setup['file']: + config = lxml.etree.parse(setup['file']).getroot() + else: + config = lxml.etree.Element("Configuration") + users = POSIXUsers(logging.getLogger('posixusers_baseline.py'), + setup, config) + + baseline = lxml.etree.Element("Bundle", name="posixusers_baseline") + for entry in users.FindExtra(): + data = users.existing[entry.tag][entry.get("name")] + for attr, idx in users.attr_mapping[entry.tag].items(): + if (entry.get(attr) or + (attr == 'uid' and setup['nouids']) or + (attr == 'gid' and setup['nogids'])): + continue + entry.set(attr, str(data[idx])) + if entry.tag == 'POSIXUser': + entry.set("group", grp.getgrgid(data[3])[0]) + for group in users.user_supplementary_groups(entry): + memberof = lxml.etree.SubElement(entry, "MemberOf") + memberof.text = group[0] + + entry.tag = "Bound" + entry.tag + baseline.append(entry) + + print(lxml.etree.tostring(baseline, pretty_print=True)) + +if __name__ == "__main__": + sys.exit(main()) |