summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2012-12-03 10:51:34 -0600
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2012-12-03 10:52:13 -0600
commit33234d5dae565e6520bbdb65d67fbaed03df4d43 (patch)
tree232ec275370a5d186095bf289897395d329c7232
parent1d4b0118ced1b198587fd75c549e2b394ff71531 (diff)
downloadbcfg2-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.txt51
-rw-r--r--doc/server/plugins/generators/rules.txt132
-rw-r--r--schemas/bundle.xsd30
-rw-r--r--schemas/rules.xsd14
-rw-r--r--schemas/types.xsd17
-rw-r--r--src/lib/Bcfg2/Client/Client.py4
-rw-r--r--src/lib/Bcfg2/Client/Frame.py11
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py300
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py1
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py20
-rw-r--r--testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py489
-rw-r--r--tools/README4
-rwxr-xr-xtools/posixusers_baseline.py73
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())