summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Laszlo <tim.laszlo@gmail.com>2012-12-03 16:16:41 -0600
committerTim Laszlo <tim.laszlo@gmail.com>2012-12-03 16:16:41 -0600
commit350db854319af526818c2ffcac285ae445b0213d (patch)
treed13d486ab856c37ad194653f46ac329bb75b3338
parenta8c2c14b0bf39d101f3ecc4b3aafc01fabad02d5 (diff)
parent2983b0c358ef25e7c34ccdeb3ab1f8d6a6f9ae90 (diff)
downloadbcfg2-350db854319af526818c2ffcac285ae445b0213d.tar.gz
bcfg2-350db854319af526818c2ffcac285ae445b0213d.tar.bz2
bcfg2-350db854319af526818c2ffcac285ae445b0213d.zip
Merge branch 'master' of github.com:Bcfg2/bcfg2
-rw-r--r--doc/client/tools/posixusers.txt51
-rw-r--r--doc/server/plugins/generators/cfg.txt30
-rw-r--r--doc/server/plugins/generators/rules.txt188
-rw-r--r--doc/server/selinux.txt39
-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/POSIX/File.py4
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py300
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py117
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py1
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py6
-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--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py4
-rw-r--r--testsuite/common.py9
-rw-r--r--tools/README4
-rwxr-xr-xtools/posixusers_baseline.py73
20 files changed, 1216 insertions, 195 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/cfg.txt b/doc/server/plugins/generators/cfg.txt
index 7d0e0acff..94394f98f 100644
--- a/doc/server/plugins/generators/cfg.txt
+++ b/doc/server/plugins/generators/cfg.txt
@@ -136,33 +136,33 @@ by running the template manually. To do this, run ``bcfg2-info
debug``, and, once in the Python interpreter, run::
metadata = self.build_metadata("<hostname>")
- path = "<relative path to template (see note below)>"
-
-``path`` should be set to the path to the template file with a leading
-slash, relative to the Bcfg2 specification root. See `Inside Genshi
-Templates`_ for examples.
+ source_path = "<full path to template>"
+ name = source_path[len(self.setup['repo']):]
Then, run::
- import os, Bcfg2.Options
+ import os
from genshi.template import TemplateLoader, NewTextTemplate
- name = os.path.dirname(path[path.find('/', 1):])
- setup = Bcfg2.Options.OptionParser({'repo':
- Bcfg2.Options.SERVER_REPOSITORY})
- setup.parse('--')
- template = TemplateLoader().load(setup['repo'] + path, cls=NewTextTemplate)
- print template.generate(metadata=metadata, path=path, name=name).render()
+ template = TemplateLoader().load(source_path, cls=NewTextTemplate)
+ data = dict(metadata=metadata,
+ source_path=source_path,
+ path=source_path,
+ name=name,
+ repo=self.setup['repo'])
+ print(template.generate(**data).render())
This gives you more fine-grained control over how your template is
-rendered.
+rendered. E.g., you can tweak the values of the variables passed to
+the template, or evaluate the template manually, line-by-line, and so
+on.
You can also use this approach to render templates that depend on
:ref:`altsrc <server-plugins-structures-altsrc>` tags by setting
-``path`` to the path to the template, and setting ``name`` to the path
+``source_path`` to the path to the template, and setting ``name`` to the path
to the file to be generated, e.g.::
metadata = self.build_metadata("foo.example.com")
- path = "/Cfg/etc/sysconfig/network-scripts/ifcfg-template/ifcfg-template.genshi"
+ source_path = "/Cfg/etc/sysconfig/network-scripts/ifcfg-template/ifcfg-template.genshi"
name = "/etc/sysconfig/network-scripts/ifcfg-bond0"
Error handling
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index 542b38f01..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
@@ -394,14 +392,12 @@ the permissions to ``0674``. When this happens, Bcfg2 will change the
permissions and set the ACLs on every run and the entry will be
eternally marked as bad.
-SELinux Tag
------------
+SELinux Entries
+---------------
-The SELinux tag has different values depending on the *type* attribute
-of the SELinux entry specified in your configuration. Below is a set
-of tables which describe the attributes available for various SELinux
-types. The types (except for ``module``) correspond to ``semanage``
-subcommands.
+Below is a set of tables which describe the attributes available
+for various SELinux types. The entry types (except for ``module``)
+correspond to ``semanage`` subcommands.
Note that the ``selinuxtype`` attribute takes only an SELinux type,
not a full context; e.g., "``etc_t``", not
@@ -411,18 +407,10 @@ As it can be very tedious to create a baseline of all existing SELinux
entries, you can use ``selinux_baseline.py`` located in the ``tools/``
directory to do that for you.
-In certain cases, it may be necessary to create multiple SELinux
-entries with the same name. For instance, "root" is both an SELinux
-user and an SELinux login record; or a given fcontext may need two
-different SELinux types depending on whether it's a symlink or a plain
-file. In these (few) cases, it is necessary to create BoundSELinux
-entries directly in Bundler rather than using abstract SELinux entries
-in Bundler and binding them with Rules.
-
See :ref:`server-selinux` for more information.
-boolean
-^^^^^^^
+SEBoolean Tag
+^^^^^^^^^^^^^
+-------+----------------------+---------+----------+
| Name | Description | Values | Required |
@@ -432,8 +420,8 @@ boolean
| value | Value of the boolean | on|off | Yes |
+-------+----------------------+---------+----------+
-port
-^^^^
+SEPort Tag
+^^^^^^^^^^
+-------------+------------------------+---------------------------+----------+
| Name | Description | Values | Required |
@@ -445,8 +433,8 @@ port
| | to this port | | |
+-------------+------------------------+---------------------------+----------+
-fcontext
-^^^^^^^^
+SEFcontext Tag
+^^^^^^^^^^^^^^
+-------------+-------------------------+---------------------+----------+
| Name | Description | Values | Required |
@@ -462,8 +450,8 @@ fcontext
| | | socket|block|char) | |
+-------------+-------------------------+---------------------+----------+
-node
-^^^^
+SENode Tag
+^^^^^^^^^^
+-------------+------------------------------------+------------------+----------+
| Name | Description | Values | Required |
@@ -477,8 +465,8 @@ node
| proto | Protocol | (ipv4|ipv6) | Yes |
+-------------+------------------------------------+------------------+----------+
-login
-^^^^^
+SELogin Tag
+^^^^^^^^^^^
+-------------+-------------------------------+-----------+----------+
| Name | Description | Values | Required |
@@ -488,8 +476,8 @@ login
| selinuxuser | SELinux username | String | Yes |
+-------------+-------------------------------+-----------+----------+
-user
-^^^^
+SEUser Tag
+^^^^^^^^^^
+-------------+-------------------------------+-----------+----------+
| Name | Description | Values | Required |
@@ -501,8 +489,8 @@ user
| prefix | Home directory context prefix | String | Yes |
+-------------+-------------------------------+-----------+----------+
-interface
-^^^^^^^^^
+SEInterface Tag
+^^^^^^^^^^^^^^^
+-------------+-------------------------+-------------+----------+
| Name | Description | Values | Required |
@@ -513,8 +501,8 @@ interface
| | to this interface | | |
+-------------+-------------------------+-------------+----------+
-permissive
-^^^^^^^^^^
+SEPermissive Tag
+^^^^^^^^^^^^^^^^
+-------------+------------------------------------+-------------+----------+
| Name | Description | Values | Required |
@@ -522,11 +510,79 @@ permissive
| name | SELinux type to make permissive | String | Yes |
+-------------+------------------------------------+-------------+----------+
-module
-^^^^^^
+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/doc/server/selinux.txt b/doc/server/selinux.txt
index e61a09002..9f54b0d68 100644
--- a/doc/server/selinux.txt
+++ b/doc/server/selinux.txt
@@ -135,47 +135,16 @@ will be considered extra, making ``selinux_baseline.py`` quite
necessary.
``selinux_baseline.py`` writes a bundle to stdout that contains
-``BoundSELinux`` entries for the appropriate SELinux entities. It
-does this rather than separate Bundle/Rules files because of the
-:ref:`server-selinux-duplicate-entries` problem.
+``BoundSELinux`` entries for the appropriate SELinux entities.
.. _server-selinux-duplicate-entries:
Duplicate Entries
-----------------
-In certain cases, it may be necessary to create multiple SELinux
-entries with the same name. For instance, "root" is both an SELinux
-user and an SELinux login record, so to manage both, you would have
-the following in Bundler:
-
-.. code-block:: xml
-
- <SELinux name="root"/>
- <SELinux name="root"/>
-
-And in Rules:
-
-.. code-block:: xml
-
- <SELinux type="login" selinuxuser="root" name="root"/>
- <SELinux type="user" prefix="user" name="root"
- roles="system_r sysadm_r user_r"/>
-
-But Rules has no way to tell which "root" is which, and you will get
-errors. In these cases, it is necessary to use ``BoundSELinux`` tags
-directly in Bundler. (See :ref:`boundentries` for more details on
-bound entries.) For instance:
-
-.. code-block:: xml
-
- <BoundSELinux type="login" selinuxuser="root" name="root"/>
- <BoundSELinux type="user" prefix="user" name="root"
- roles="system_r sysadm_r user_r"/>
-
-It may also be necessary to use ``BoundSELinux`` tags if a single
-fcontext needs two different SELinux types depending on whether it's a
-symlink or a plain file. For instance:
+It may be necessary to use `BoundSELinux` tags if a single fcontext
+needs two different SELinux types depending on whether it's a symlink
+or a plain file. For instance:
.. code-block:: xml
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/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
index 5842c4e1f..9b95d2234 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py
@@ -188,6 +188,10 @@ class POSIXFile(POSIXTool):
prompt.append(udiff)
except UnicodeEncodeError:
prompt.append("Could not encode diff")
+ elif entry.get("empty", "true"):
+ # the file doesn't exist on disk, but there's no
+ # expected content
+ prompt.append("%s does not exist" % entry.get("name"))
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
new file mode 100644
index 000000000..78734f5c2
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py
@@ -0,0 +1,300 @@
+""" A tool to handle creating users and groups with useradd/mod/del
+and groupadd/mod/del """
+
+import sys
+import pwd
+import grp
+import Bcfg2.Client.XML
+import subprocess
+import Bcfg2.Client.Tools
+
+
+class ExecutionError(Exception):
+ """ Raised when running an external command fails """
+
+ def __init__(self, msg, retval=None):
+ Exception.__init__(self, msg)
+ self.retval = retval
+
+ def __str__(self):
+ return "%s (rv: %s)" % (Exception.__str__(self),
+ self.retval)
+
+
+class Executor(object):
+ """ A better version of Bcfg2.Client.Tool.Executor, which captures
+ stderr, raises exceptions on error, and doesn't use the shell to
+ execute by default """
+
+ def __init__(self, logger):
+ self.logger = logger
+ self.stdout = None
+ self.stderr = None
+ self.retval = None
+
+ def run(self, command, inputdata=None, shell=False):
+ """ Run a command, given as a list, optionally giving it the
+ specified input data """
+ self.logger.debug("Running: %s" % " ".join(command))
+ proc = subprocess.Popen(command, shell=shell, bufsize=16384,
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, close_fds=True)
+ if inputdata:
+ for line in inputdata.splitlines():
+ self.logger.debug('> %s' % line)
+ (self.stdout, self.stderr) = proc.communicate(inputdata)
+ else:
+ (self.stdout, self.stderr) = proc.communicate()
+ for line in self.stdout.splitlines(): # pylint: disable=E1103
+ self.logger.debug('< %s' % line)
+ self.retval = proc.wait()
+ if self.retval == 0:
+ for line in self.stderr.splitlines(): # pylint: disable=E1103
+ self.logger.warning(line)
+ return True
+ else:
+ raise ExecutionError(self.stderr, self.retval)
+
+
+class POSIXUsers(Bcfg2.Client.Tools.Tool):
+ """ A tool to handle creating users and groups with
+ useradd/mod/del and groupadd/mod/del """
+ __execs__ = ['/usr/sbin/useradd', '/usr/sbin/usermod', '/usr/sbin/userdel',
+ '/usr/sbin/groupadd', '/usr/sbin/groupmod',
+ '/usr/sbin/groupdel']
+ __handles__ = [('POSIXUser', None),
+ ('POSIXGroup', None)]
+ __req__ = dict(POSIXUser=['name'],
+ POSIXGroup=['name'])
+ experimental = True
+
+ # A mapping of XML entry attributes to the indexes of
+ # corresponding values in the get*ent data structures
+ attr_mapping = dict(POSIXUser=dict(name=0, uid=2, gecos=4, home=5,
+ shell=6),
+ POSIXGroup=dict(name=0, gid=2))
+
+ def __init__(self, logger, setup, config):
+ Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
+ self.set_defaults = dict(POSIXUser=self.populate_user_entry,
+ POSIXGroup=lambda g: g)
+ self.cmd = Executor(logger)
+ self._existing = None
+
+ @property
+ def existing(self):
+ """ Get a dict of existing users and groups """
+ if self._existing is None:
+ self._existing = dict(POSIXUser=dict([(u[0], u)
+ for u in pwd.getpwall()]),
+ POSIXGroup=dict([(g[0], g)
+ for g in grp.getgrall()]))
+ return self._existing
+
+ def Inventory(self, states, structures=None):
+ if not structures:
+ structures = self.config.getchildren()
+ # we calculate a list of all POSIXUser and POSIXGroup entries,
+ # and then add POSIXGroup entries that are required to create
+ # the primary group for each user to the structures. this is
+ # sneaky and possibly evil, but it works great.
+ groups = []
+ for struct in structures:
+ groups.extend([e.get("name")
+ for e in struct.findall("POSIXGroup")])
+ for struct in structures:
+ for entry in struct.findall("POSIXUser"):
+ group = self.set_defaults[entry.tag](entry).get('group')
+ if group and group not in groups:
+ self.logger.debug("POSIXUsers: Adding POSIXGroup entry "
+ "'%s' for user '%s'" %
+ (group, entry.get("name")))
+ struct.append(Bcfg2.Client.XML.Element("POSIXGroup",
+ name=group))
+ return Bcfg2.Client.Tools.Tool.Inventory(self, states, structures)
+
+ def FindExtra(self):
+ extra = []
+ for handles in self.__handles__:
+ tag = handles[0]
+ specified = []
+ for entry in self.getSupportedEntries():
+ if entry.tag == tag:
+ specified.append(entry.get("name"))
+ extra.extend([Bcfg2.Client.XML.Element(tag, name=e)
+ for e in self.existing[tag].keys()
+ if e not in specified])
+ return extra
+
+ def populate_user_entry(self, entry):
+ """ Given a POSIXUser entry, set all of the 'missing' attributes
+ with their defaults """
+ defaults = dict(group=entry.get('name'),
+ gecos=entry.get('name'),
+ shell='/bin/bash')
+ if entry.get('name') == 'root':
+ defaults['home'] = '/root'
+ else:
+ defaults['home'] = '/home/%s' % entry.get('name')
+ for key, val in defaults.items():
+ if entry.get(key) is None:
+ entry.set(key, val)
+ if entry.get('group') in self.existing['POSIXGroup']:
+ entry.set('gid',
+ str(self.existing['POSIXGroup'][entry.get('group')][2]))
+ return entry
+
+ def user_supplementary_groups(self, entry):
+ """ Get a list of supplmentary groups that the user in the
+ given entry is a member of """
+ return [g for g in self.existing['POSIXGroup'].values()
+ if entry.get("name") in g[3] and g[0] != entry.get("group")]
+
+ def VerifyPOSIXUser(self, entry, _):
+ """ Verify a POSIXUser entry """
+ rv = self._verify(self.populate_user_entry(entry))
+ if entry.get("current_exists", "true") == "true":
+ # verify supplemental groups
+ actual = [g[0] for g in self.user_supplementary_groups(entry)]
+ expected = [e.text for e in entry.findall("MemberOf")]
+ if set(expected) != set(actual):
+ entry.set('qtext',
+ "\n".join([entry.get('qtext', '')] +
+ ["%s %s has incorrect supplemental group "
+ "membership. Currently: %s. Should be: %s"
+ % (entry.tag, entry.get("name"),
+ actual, expected)]))
+ rv = False
+ if self.setup['interactive'] and not rv:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext', ''), entry.tag, entry.get('name')))
+ return rv
+
+ def VerifyPOSIXGroup(self, entry, _):
+ """ Verify a POSIXGroup entry """
+ rv = self._verify(entry)
+ if self.setup['interactive'] and not rv:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext', ''), entry.tag, entry.get('name')))
+ return rv
+
+ def _verify(self, entry):
+ """ Perform most of the actual work of verification """
+ errors = []
+ if entry.get("name") not in self.existing[entry.tag]:
+ entry.set('current_exists', 'false')
+ errors.append("%s %s does not exist" % (entry.tag,
+ entry.get("name")))
+ else:
+ for attr, idx in self.attr_mapping[entry.tag].items():
+ val = str(self.existing[entry.tag][entry.get("name")][idx])
+ entry.set("current_%s" % attr, val)
+ if attr in ["uid", "gid"]:
+ if entry.get(attr) is None:
+ # no uid/gid specified, so we let the tool
+ # automatically determine one -- i.e., it always
+ # verifies
+ continue
+ if val != entry.get(attr):
+ errors.append("%s for %s %s is incorrect. Current %s is "
+ "%s, but should be %s" %
+ (attr.title(), entry.tag, entry.get("name"),
+ attr, entry.get(attr), val))
+
+ if errors:
+ for error in errors:
+ self.logger.debug("%s: %s" % (self.name, error))
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return len(errors) == 0
+
+ def Install(self, entries, states):
+ for entry in entries:
+ # install groups first, so that all groups exist for
+ # users that might need them
+ if entry.tag == 'POSIXGroup':
+ states[entry] = self._install(entry)
+ for entry in entries:
+ if entry.tag == 'POSIXUser':
+ states[entry] = self._install(entry)
+ self._existing = None
+
+ def _install(self, entry):
+ """ add or modify a user or group using the appropriate command """
+ if entry.get("name") not in self.existing[entry.tag]:
+ action = "add"
+ else:
+ action = "mod"
+ try:
+ self.cmd.run(self._get_cmd(action,
+ self.set_defaults[entry.tag](entry)))
+ self.modified.append(entry)
+ return True
+ except ExecutionError:
+ self.logger.error("POSIXUsers: Error creating %s %s: %s" %
+ (entry.tag, entry.get("name"),
+ sys.exc_info()[1]))
+ return False
+
+ def _get_cmd(self, action, entry):
+ """ Get a command to perform the appropriate action (add, mod,
+ del) on the given entry. The command is always the same; we
+ set all attributes on a given user or group when modifying it
+ rather than checking which ones need to be changed. This
+ makes things fail as a unit (e.g., if a user is logged in, you
+ can't change its home dir, but you could change its GECOS, but
+ the whole operation fails), but it also makes this function a
+ lot, lot easier and simpler."""
+ cmd = ["/usr/sbin/%s%s" % (entry.tag[5:].lower(), action)]
+ if action != 'del':
+ if entry.tag == 'POSIXGroup':
+ if entry.get('gid'):
+ cmd.extend(['-g', entry.get('gid')])
+ elif entry.tag == 'POSIXUser':
+ cmd.append('-m')
+ if entry.get('uid'):
+ cmd.extend(['-u', entry.get('uid')])
+ cmd.extend(['-g', entry.get('group')])
+ extras = [e.text for e in entry.findall("MemberOf")]
+ if extras:
+ cmd.extend(['-G', ",".join(extras)])
+ cmd.extend(['-d', entry.get('home')])
+ cmd.extend(['-s', entry.get('shell')])
+ cmd.extend(['-c', entry.get('gecos')])
+ cmd.append(entry.get('name'))
+ return cmd
+
+ def Remove(self, entries):
+ for entry in entries:
+ # remove users first, so that all users have been removed
+ # from groups before we remove them
+ if entry.tag == 'POSIXUser':
+ self._remove(entry)
+ for entry in entries:
+ if entry.tag == 'POSIXGroup':
+ try:
+ grp.getgrnam(entry.get("name"))
+ self._remove(entry)
+ except KeyError:
+ # at least some versions of userdel automatically
+ # remove the primary group for a user if the group
+ # name is the same as the username, and no other
+ # users are in the group
+ self.logger.info("POSIXUsers: Group %s does not exist. "
+ "It may have already been removed when "
+ "its users were deleted" %
+ entry.get("name"))
+ self._existing = None
+ self.extra = self.FindExtra()
+
+ def _remove(self, entry):
+ """ Remove an entry """
+ try:
+ self.cmd.run(self._get_cmd("del", entry))
+ return True
+ except ExecutionError:
+ self.logger.error("POSIXUsers: Error deleting %s %s: %s" %
+ (entry.tag, entry.get("name"),
+ sys.exc_info()[1]))
+ return False
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
index fc47883c9..7aa0e8a20 100644
--- a/src/lib/Bcfg2/Client/Tools/SELinux.py
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -58,36 +58,44 @@ def netmask_itoa(netmask, proto="ipv4"):
class SELinux(Bcfg2.Client.Tools.Tool):
""" SELinux entry support """
name = 'SELinux'
- __handles__ = [('SELinux', 'boolean'),
- ('SELinux', 'port'),
- ('SELinux', 'fcontext'),
- ('SELinux', 'node'),
- ('SELinux', 'login'),
- ('SELinux', 'user'),
- ('SELinux', 'interface'),
- ('SELinux', 'permissive'),
- ('SELinux', 'module')]
- __req__ = dict(SELinux=dict(boolean=['name', 'value'],
- module=['name'],
- port=['name', 'selinuxtype'],
- fcontext=['name', 'selinuxtype'],
- node=['name', 'selinuxtype', 'proto'],
- login=['name', 'selinuxuser'],
- user=['name', 'roles', 'prefix'],
- interface=['name', 'selinuxtype'],
- permissive=['name']))
+ __handles__ = [('SEBoolean', None),
+ ('SEFcontext', None),
+ ('SEInterface', None),
+ ('SELogin', None),
+ ('SEModule', None),
+ ('SENode', None),
+ ('SEPermissive', None),
+ ('SEPort', None),
+ ('SEUser', None)]
+ __req__ = dict(SEBoolean=['name', 'value'],
+ SEFcontext=['name', 'selinuxtype'],
+ SEInterface=['name', 'selinuxtype'],
+ SELogin=['name', 'selinuxuser'],
+ SEModule=['name'],
+ SENode=['name', 'selinuxtype', 'proto'],
+ SEPermissive=['name'],
+ SEPort=['name', 'selinuxtype'],
+ SEUser=['name', 'roles', 'prefix'])
def __init__(self, logger, setup, config):
Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
self.handlers = {}
- for handles in self.__handles__:
- etype = handles[1]
+ for handler in self.__handles__:
+ etype = handler[0]
self.handlers[etype] = \
globals()["SELinux%sHandler" % etype.title()](self, logger,
setup, config)
self.txn = False
self.post_txn_queue = []
+ def __getattr__(self, attr):
+ if attr.startswith("VerifySE"):
+ return self.GenericSEVerify
+ elif attr.startswith("InstallSE"):
+ return self.GenericSEInstall
+ else:
+ return object.__getattr__(self, attr)
+
def BundleUpdated(self, _, states):
for handler in self.handlers.values():
handler.BundleUpdated(states)
@@ -100,12 +108,12 @@ class SELinux(Bcfg2.Client.Tools.Tool):
def canInstall(self, entry):
return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and
- self.handlers[entry.get('type')].canInstall(entry))
+ self.handlers[entry.tag].canInstall(entry))
def primarykey(self, entry):
""" return a string that should be unique amongst all entries
in the specification """
- return self.handlers[entry.get('type')].primarykey(entry)
+ return self.handlers[entry.tag].primarykey(entry)
def Install(self, entries, states):
# start a transaction
@@ -125,32 +133,32 @@ class SELinux(Bcfg2.Client.Tools.Tool):
for func, arg, kwargs in self.post_txn_queue:
states[arg] = func(*arg, **kwargs)
- def InstallSELinux(self, entry):
- """Dispatch install to the proper method according to type"""
- return self.handlers[entry.get('type')].Install(entry)
+ def GenericSEInstall(self, entry):
+ """Dispatch install to the proper method according to entry tag"""
+ return self.handlers[entry.tag].Install(entry)
- def VerifySELinux(self, entry, _):
- """Dispatch verify to the proper method according to type"""
- rv = self.handlers[entry.get('type')].Verify(entry)
+ def GenericSEVerify(self, entry, _):
+ """Dispatch verify to the proper method according to entry tag"""
+ rv = self.handlers[entry.tag].Verify(entry)
if entry.get('qtext') and self.setup['interactive']:
entry.set('qtext',
- '%s\nInstall SELinux %s %s: (y/N) ' %
+ '%s\nInstall %s: (y/N) ' %
(entry.get('qtext'),
- entry.get('type'),
- self.handlers[entry.get('type')].tostring(entry)))
+ self.handlers[entry.tag].tostring(entry)))
return rv
def Remove(self, entries):
- """Dispatch verify to the proper removal method according to type"""
+ """Dispatch verify to the proper removal
+ method according to entry tag"""
# sort by type
types = list()
for entry in entries:
- if entry.get('type') not in types:
- types.append(entry.get('type'))
+ if entry.tag not in types:
+ types.append(entry.tag)
for etype in types:
self.handlers[etype].Remove([e for e in entries
- if e.get('type') == etype])
+ if e.tag == etype])
class SELinuxEntryHandler(object):
@@ -253,8 +261,7 @@ class SELinuxEntryHandler(object):
def key2entry(self, key):
""" Generate an XML entry from an SELinux record key """
attrs = self._key2attrs(key)
- attrs["type"] = self.etype
- return Bcfg2.Client.XML.Element("SELinux", **attrs)
+ return Bcfg2.Client.XML.Element(self.etype, **attrs)
def _args(self, entry, method):
""" Get the argument list for invoking _modify or _add, or
@@ -279,7 +286,7 @@ class SELinuxEntryHandler(object):
""" return a string that should be unique amongst all entries
in the specification. some entry types are not universally
disambiguated by tag:type:name alone """
- return ":".join([entry.tag, entry.get("type"), entry.get("name")])
+ return ":".join([entry.tag, entry.get("name")])
def exists(self, entry):
""" return True if the entry already exists in the record list """
@@ -303,8 +310,8 @@ class SELinuxEntryHandler(object):
continue
if current_attrs[attr] != desired_attrs[attr]:
entry.set('current_%s' % attr, current_attrs[attr])
- errors.append("SELinux %s %s has wrong %s: %s, should be %s" %
- (self.etype, self.tostring(entry), attr,
+ errors.append("%s %s has wrong %s: %s, should be %s" %
+ (entry.tag, entry.get('name'), attr,
current_attrs[attr], desired_attrs[attr]))
if errors:
@@ -331,8 +338,8 @@ class SELinuxEntryHandler(object):
return True
except ValueError:
err = sys.exc_info()[1]
- self.logger.debug("Failed to %s SELinux %s %s: %s" %
- (method, self.etype, self.tostring(entry), err))
+ self.logger.info("Failed to %s SELinux %s %s: %s" %
+ (method, self.etype, self.tostring(entry), err))
return False
def Remove(self, entries):
@@ -365,7 +372,7 @@ class SELinuxEntryHandler(object):
pass
-class SELinuxBooleanHandler(SELinuxEntryHandler):
+class SELinuxSebooleanHandler(SELinuxEntryHandler):
""" handle SELinux boolean entries """
etype = "boolean"
value_format = ("value",)
@@ -414,7 +421,7 @@ class SELinuxBooleanHandler(SELinuxEntryHandler):
SELinuxEntryHandler.canInstall(self, entry))
-class SELinuxPortHandler(SELinuxEntryHandler):
+class SELinuxSeportHandler(SELinuxEntryHandler):
""" handle SELinux port entries """
etype = "port"
value_format = ('selinuxtype', None)
@@ -486,7 +493,7 @@ class SELinuxPortHandler(SELinuxEntryHandler):
return tuple(entry.get("name").split("/"))
-class SELinuxFcontextHandler(SELinuxEntryHandler):
+class SELinuxSefcontextHandler(SELinuxEntryHandler):
""" handle SELinux file context entries """
etype = "fcontext"
@@ -556,11 +563,11 @@ class SELinuxFcontextHandler(SELinuxEntryHandler):
'', '')
def primarykey(self, entry):
- return ":".join([entry.tag, entry.get("type"), entry.get("name"),
+ return ":".join([entry.tag, entry.get("name"),
entry.get("filetype", "all")])
-class SELinuxNodeHandler(SELinuxEntryHandler):
+class SELinuxSenodeHandler(SELinuxEntryHandler):
""" handle SELinux node entries """
etype = "node"
@@ -592,7 +599,7 @@ class SELinuxNodeHandler(SELinuxEntryHandler):
entry.get("selinuxtype"))
-class SELinuxLoginHandler(SELinuxEntryHandler):
+class SELinuxSeloginHandler(SELinuxEntryHandler):
""" handle SELinux login entries """
etype = "login"
@@ -603,7 +610,7 @@ class SELinuxLoginHandler(SELinuxEntryHandler):
return (entry.get("name"), entry.get("selinuxuser"), "")
-class SELinuxUserHandler(SELinuxEntryHandler):
+class SELinuxSeuserHandler(SELinuxEntryHandler):
""" handle SELinux user entries """
etype = "user"
@@ -652,7 +659,7 @@ class SELinuxUserHandler(SELinuxEntryHandler):
return tuple(rv)
-class SELinuxInterfaceHandler(SELinuxEntryHandler):
+class SELinuxSeinterfaceHandler(SELinuxEntryHandler):
""" handle SELinux interface entries """
etype = "interface"
@@ -663,7 +670,7 @@ class SELinuxInterfaceHandler(SELinuxEntryHandler):
return (entry.get("name"), '', entry.get("selinuxtype"))
-class SELinuxPermissiveHandler(SELinuxEntryHandler):
+class SELinuxSepermissiveHandler(SELinuxEntryHandler):
""" handle SELinux permissive domain entries """
etype = "permissive"
@@ -695,7 +702,7 @@ class SELinuxPermissiveHandler(SELinuxEntryHandler):
return (entry.get("name"),)
-class SELinuxModuleHandler(SELinuxEntryHandler):
+class SELinuxSemoduleHandler(SELinuxEntryHandler):
""" handle SELinux module entries """
etype = "module"
@@ -808,10 +815,9 @@ class SELinuxModuleHandler(SELinuxEntryHandler):
def Install(self, entry, _=None):
if not self.filetool.install(self._pathentry(entry)):
return False
- if hasattr(self, 'records'):
+ if hasattr(seobject, 'moduleRecords'):
# if seobject has the moduleRecords attribute, install the
# module using the seobject library
- self.records # pylint: disable=W0104
return self._install_seobject(entry)
else:
# seobject doesn't have the moduleRecords attribute, so
@@ -891,8 +897,7 @@ class SELinuxModuleHandler(SELinuxEntryHandler):
def FindExtra(self):
specified = [self._key(e)
- for e in self.tool.getSupportedEntries()
- if e.get("type") == self.etype]
+ for e in self.tool.getSupportedEntries()]
rv = []
for module in self._all_records_from_filesystem().keys():
if module not in specified:
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index 927b25ba8..d5f55759f 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -61,6 +61,7 @@ class Tool(object):
__req__ = {}
__important__ = []
deprecated = False
+ experimental = False
def __init__(self, logger, setup, config):
self.setup = setup
diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py
index 3edfb72a3..fa47f9496 100644
--- a/src/lib/Bcfg2/Server/Plugins/SEModules.py
+++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py
@@ -40,8 +40,8 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool):
#: objects as its EntrySet children.
es_child_cls = SEModuleData
- #: SEModules manages ``SELinux`` entries
- entry_type = 'SELinux'
+ #: SEModules manages ``SEModule`` entries
+ entry_type = 'SEModule'
#: The SEModules plugin is experimental
experimental = True
@@ -68,7 +68,7 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool):
return name.lstrip("/")
def HandlesEntry(self, entry, metadata):
- if entry.tag in self.Entries and entry.get('type') == 'module':
+ if entry.tag in self.Entries:
return self._get_module_filename(entry) in self.Entries[entry.tag]
return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry,
metadata)
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/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
index 58f844b3b..92d710f7d 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
@@ -88,7 +88,7 @@ class TestCfgBaseFileMatcher(TestSpecificData):
mock_get_regex.reset_mock()
match.reset_mock()
match.return_value = True
- self.assertTrue(self.test_obj.handles(evt))
+ self.assertTrue(self.test_obj.handles(evt))
match.assert_called_with(evt.filename)
else:
match.return_value = False
@@ -389,7 +389,7 @@ class TestCfgEntrySet(TestEntrySet):
eset.bind_info_to_entry.assert_called_with(entry, metadata)
eset._generate_data.assert_called_with(entry, metadata)
self.assertFalse(eset._validate_data.called)
- expected = lxml.etree.Element("Path", name="/text.txt")
+ expected = lxml.etree.Element("Path", name="/test.txt")
expected.text = "data"
self.assertXMLEqual(bound, expected)
self.assertEqual(bound, entry)
diff --git a/testsuite/common.py b/testsuite/common.py
index 0cb457461..e26d0be61 100644
--- a/testsuite/common.py
+++ b/testsuite/common.py
@@ -267,7 +267,7 @@ class Bcfg2TestCase(unittest.TestCase):
attributes. """
self.assertEqual(el1.tag, el2.tag, msg=msg)
self.assertEqual(el1.text, el2.text, msg=msg)
- self.assertItemsEqual(el1.attrib, el2.attrib, msg=msg)
+ self.assertItemsEqual(el1.attrib.items(), el2.attrib.items(), msg=msg)
self.assertEqual(len(el1.getchildren()),
len(el2.getchildren()))
for child1 in el1.getchildren():
@@ -275,10 +275,11 @@ class Bcfg2TestCase(unittest.TestCase):
self.assertIsNotNone(cname,
msg="Element %s has no 'name' attribute" %
child1.tag)
- children2 = el2.xpath("*[@name='%s']" % cname)
+ children2 = el2.xpath("%s[@name='%s']" % (child1.tag, cname))
self.assertEqual(len(children2), 1,
- msg="More than one element named %s" % cname)
- self.assertXMLEqual(child1, children2[0], msg=msg)
+ msg="More than one %s element named %s" % \
+ (child1.tag, cname))
+ self.assertXMLEqual(child1, children2[0], msg=msg)
class DBModelTestCase(Bcfg2TestCase):
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())