summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Kincl <kincljc@ornl.gov>2012-12-04 08:14:33 -0500
committerJason Kincl <kincljc@ornl.gov>2012-12-04 08:14:33 -0500
commit09a45d745269a419b0c5da0664912e061dc8e5d3 (patch)
treec5c0af33093087f10f0caf5ce021aa4cb0b4a879
parent648c8f6e313e684d5fadc1fdbc97e08d83eb2b16 (diff)
parentf35c38e87eafffb497338b9273fe84f284a41dcf (diff)
downloadbcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.tar.gz
bcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.tar.bz2
bcfg2-09a45d745269a419b0c5da0664912e061dc8e5d3.zip
Merge remote branch 'upstream/master' into jasons-hacking
-rw-r--r--doc/client/tools/posixusers.txt51
-rw-r--r--doc/development/compat.txt6
-rw-r--r--doc/development/documentation.txt4
-rw-r--r--doc/server/plugins/generators/cfg.txt30
-rw-r--r--doc/server/plugins/generators/rules.txt188
-rw-r--r--doc/server/plugins/generators/sslca.txt63
-rw-r--r--doc/server/selinux.txt39
-rw-r--r--schemas/bundle.xsd30
-rw-r--r--schemas/rules.xsd14
-rw-r--r--schemas/sslca-cert.xsd22
-rw-r--r--schemas/sslca-key.xsd6
-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/POSIX/base.py12
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIXUsers.py300
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py121
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py1
-rw-r--r--src/lib/Bcfg2/Compat.py15
-rw-r--r--src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html2
-rw-r--r--src/lib/Bcfg2/Reporting/views.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py65
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py5
-rw-r--r--src/lib/Bcfg2/Server/Plugins/FileProbes.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py6
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SSLCA.py491
-rw-r--r--src/lib/Bcfg2/version.py4
-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.py12
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestSEModules.py25
-rw-r--r--testsuite/common.py9
-rw-r--r--tools/README4
-rwxr-xr-xtools/posixusers_baseline.py73
35 files changed, 1647 insertions, 504 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/development/compat.txt b/doc/development/compat.txt
index d3e77b4a3..b7bf87bec 100644
--- a/doc/development/compat.txt
+++ b/doc/development/compat.txt
@@ -197,3 +197,9 @@ unicode
In Py3k, the :func:`unicode` class is not defined, because all
strings are unicode. ``Bcfg2.Compat`` defines ``unicode`` as
equivalent to :func:`str` in Python 3.
+
+oct_mode
+~~~~~~~~
+
+.. autofunction:: Bcfg2.Compat.oct_mode
+
diff --git a/doc/development/documentation.txt b/doc/development/documentation.txt
index 1e7667cc5..2a3cf46d1 100644
--- a/doc/development/documentation.txt
+++ b/doc/development/documentation.txt
@@ -103,5 +103,5 @@ Basics
Sections
--------
-Unless necessary, all the documentation follows the sections header
-rules available at http://docs.python.org/documenting/rest.html#sections.
+Unless necessary, all the documentation follows the sections header rules
+available at http://docs.python.org/devguide/documenting.html#sections
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/plugins/generators/sslca.txt b/doc/server/plugins/generators/sslca.txt
index 2b07f91fe..cabb4d730 100644
--- a/doc/server/plugins/generators/sslca.txt
+++ b/doc/server/plugins/generators/sslca.txt
@@ -146,9 +146,15 @@ cert.xml
``cert.xml`` is an XML document describing an SSL certificate
generated from an SSL key that has also been generated by SSLCA. It
-contains a top-level ``CertInfo`` tag that contains a single ``Cert``
-tag. (``Group`` and ``Client`` tags are not currently supported in
-``cert.xml``.) The ``Cert`` tag may take the following attributes:
+honors ``Group`` and ``Client`` tags much like Bundler. It must have
+a top-level ``CertInfo`` tag and can contain two types of tags:
+
+Cert
+^^^^
+
+The ``Cert`` tag explains how the certificate should be generated.
+There should be at least one ``Cert`` tag, and at most one ``Cert``
+tag should apply to any given client.
+--------------+------------------------------------------+---------+---------+
| Attribute | Description | Values | Default |
@@ -188,13 +194,42 @@ tag. (``Group`` and ``Client`` tags are not currently supported in
| | the format required by Nginx) | | |
+--------------+------------------------------------------+---------+---------+
+SubjectAltName
+^^^^^^^^^^^^^^
+
+The ``SubjectAltName`` tag contains text giving a subject alternative
+name for the certificate. Any number of ``SubjectAltName`` tags may
+be used.
+
+Example
+^^^^^^^
+
+.. code-block: xml
+
+ <CertInfo>
+ <SubjectAltName>test.example.com</SubjectAltName>
+ <Group name="apache">
+ <Cert key="/etc/pki/tls/private/foo.key" days="730"/>
+ </Group>
+ <Group name="nginx">
+ <Cert key="/etc/pki/tls/private/foo.key" days="730"
+ append_chain="true"/>
+ </Group>
+ </CertInfo>
+
key.xml
-------
-``key.xml`` is an XML document describing an SSL key. It contains a
-top-level ``KeyInfo`` tag that contains a single ``Key`` tag.
-(``Group`` and ``Client`` tags are not currently supported in
-``key.xml``.) The ``Key`` tag may take the following attributes:
+``key.xml`` is an XML document describing an SSL key. It also honors
+``Group`` and ``Client`` tags. It contains a top-level ``KeyInfo``
+tag that contains at least one ``Key`` tag.
+
+Key
+^^^
+
+The ``Cert`` tag explains how the certificate should be generated.
+There should be at least one ``Cert`` tag, and at most one ``Cert``
+tag should apply to any given client.
+--------------+------------------------------------------+---------+---------+
| Attribute | Description | Values | Default |
@@ -204,6 +239,20 @@ top-level ``KeyInfo`` tag that contains a single ``Key`` tag.
| bits | The key length | Integer | 2048 |
+--------------+------------------------------------------+---------+---------+
+Example
+^^^^^^^
+
+.. code-block: xml
+
+ <KeyInfo>
+ <Group name="fast">
+ <Key type="rsa" bits="1024"/>
+ </Group>
+ <Group name="secure">
+ <Key type="rsa" bits="4096"/>
+ </Group>
+ </KeyInfo>
+
Automated Bcfg2 SSL Authentication
==================================
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/sslca-cert.xsd b/schemas/sslca-cert.xsd
index 921c1c7c6..9e0d031a2 100644
--- a/schemas/sslca-cert.xsd
+++ b/schemas/sslca-cert.xsd
@@ -6,42 +6,24 @@
</xsd:documentation>
</xsd:annotation>
- <!-- cert.xml does not support Group or Client tags, but it should
- (and will, some day), so this is commented out for now -->
- <!--
<xsd:complexType name="GroupType">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element name="Cert" type="CertType"/>
<xsd:element name="Group" type="GroupType"/>
<xsd:element name="Client" type="GroupType"/>
+ <xsd:element name="subjectAltName" type="xsd:string"/>
</xsd:choice>
<xsd:attribute type="xsd:string" name="name" use="required"/>
<xsd:attribute type="xsd:string" name="negate"/>
</xsd:complexType>
- -->
-
- <xsd:complexType name="CertType">
- <xsd:attribute type="xsd:string" name="key" use="required"/>
- <xsd:attribute type="xsd:string" name="format"/>
- <xsd:attribute type="xsd:string" name="ca"/>
- <xsd:attribute type="xsd:integer" name="days"/>
- <xsd:attribute type="xsd:string" name="c"/>
- <xsd:attribute type="xsd:string" name="l"/>
- <xsd:attribute type="xsd:string" name="st"/>
- <xsd:attribute type="xsd:string" name="ou"/>
- <xsd:attribute type="xsd:string" name="o"/>
- <xsd:attribute type="xsd:string" name="emailaddress"/>
- <xsd:attribute type="xsd:string" name="append_chain"/>
- </xsd:complexType>
<xsd:element name="CertInfo">
<xsd:complexType>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element name="Cert" type="CertType"/>
- <!--
<xsd:element name="Group" type="GroupType"/>
<xsd:element name="Client" type="GroupType"/>
- -->
+ <xsd:element name="subjectAltName" type="xsd:string"/>
</xsd:choice>
</xsd:complexType>
</xsd:element>
diff --git a/schemas/sslca-key.xsd b/schemas/sslca-key.xsd
index 2c931fa7d..e807ea037 100644
--- a/schemas/sslca-key.xsd
+++ b/schemas/sslca-key.xsd
@@ -6,9 +6,6 @@
</xsd:documentation>
</xsd:annotation>
- <!-- key.xml does not support Group or Client tags, but it should
- (and will, some day), so this is commented out for now -->
- <!--
<xsd:complexType name="GroupType">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element name="Key" type="KeyType"/>
@@ -18,7 +15,6 @@
<xsd:attribute type="xsd:string" name="name" use="required"/>
<xsd:attribute type="xsd:string" name="negate"/>
</xsd:complexType>
- -->
<xsd:complexType name="KeyType">
<xsd:attribute type="xsd:string" name="type"/>
@@ -29,10 +25,8 @@
<xsd:complexType>
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element name="Key" type="KeyType"/>
- <!--
<xsd:element name="Group" type="GroupType"/>
<xsd:element name="Client" type="GroupType"/>
- -->
</xsd:choice>
</xsd:complexType>
</xsd:element>
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/POSIX/base.py b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
index 6388f6731..b867fa3d8 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX/base.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX/base.py
@@ -9,6 +9,7 @@ import copy
import shutil
import Bcfg2.Client.Tools
import Bcfg2.Client.XML
+from Bcfg2.Compat import oct_mode
try:
import selinux
@@ -128,7 +129,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
wanted_mode |= device_map[entry.get('dev_type')]
try:
self.logger.debug("POSIX: Setting mode on %s to %s" %
- (path, oct(wanted_mode)))
+ (path, oct_mode(wanted_mode)))
os.chmod(path, wanted_mode)
except (OSError, KeyError):
self.logger.error('POSIX: Failed to change mode on %s' %
@@ -436,7 +437,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
group = None
try:
- mode = oct(ondisk[stat.ST_MODE])[-4:]
+ mode = oct_mode(ondisk[stat.ST_MODE])[-4:]
except (OSError, KeyError, TypeError):
err = sys.exc_info()[1]
self.logger.debug("POSIX: Could not get current permissions of "
@@ -507,7 +508,7 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
(path, attrib['current_group'], entry.get('group')))
if (wanted_mode and
- oct(int(attrib['current_mode'], 8)) != oct(wanted_mode)):
+ oct_mode(int(attrib['current_mode'], 8)) != oct_mode(wanted_mode)):
errors.append("Permissions for path %s are incorrect. "
"Current permissions are %s but should be %s" %
(path, attrib['current_mode'], entry.get('mode')))
@@ -708,10 +709,11 @@ class POSIXTool(Bcfg2.Client.Tools.Tool):
for i in range(0, 3):
if newmode & (6 * pow(8, i)):
newmode |= 1 * pow(8, i)
- tmpentry.set('mode', oct(newmode))
+ tmpentry.set('mode', oct_mode(newmode))
for acl in tmpentry.findall('ACL'):
acl.set('perms',
- oct(self._norm_acl_perms(acl.get('perms')) | ACL_MAP['x']))
+ oct_mode(self._norm_acl_perms(acl.get('perms')) | \
+ ACL_MAP['x']))
for cpath in created:
rv &= self._set_perms(tmpentry, path=cpath)
return rv
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..6bd728114 100644
--- a/src/lib/Bcfg2/Client/Tools/SELinux.py
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -58,36 +58,48 @@ 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
+ # there's no need for an else here, because python checks for
+ # an attribute in the "normal" ways first. i.e., if self.txn
+ # is used, __getattr__() is never called because txn exists as
+ # a "normal" attribute of this object. See
+ # http://docs.python.org/2/reference/datamodel.html#object.__getattr__
+ # for details
+
def BundleUpdated(self, _, states):
for handler in self.handlers.values():
handler.BundleUpdated(states)
@@ -100,12 +112,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 +137,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 +265,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 +290,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 +314,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 +342,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 +376,7 @@ class SELinuxEntryHandler(object):
pass
-class SELinuxBooleanHandler(SELinuxEntryHandler):
+class SELinuxSebooleanHandler(SELinuxEntryHandler):
""" handle SELinux boolean entries """
etype = "boolean"
value_format = ("value",)
@@ -414,7 +425,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 +497,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 +567,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 +603,7 @@ class SELinuxNodeHandler(SELinuxEntryHandler):
entry.get("selinuxtype"))
-class SELinuxLoginHandler(SELinuxEntryHandler):
+class SELinuxSeloginHandler(SELinuxEntryHandler):
""" handle SELinux login entries """
etype = "login"
@@ -603,7 +614,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 +663,7 @@ class SELinuxUserHandler(SELinuxEntryHandler):
return tuple(rv)
-class SELinuxInterfaceHandler(SELinuxEntryHandler):
+class SELinuxSeinterfaceHandler(SELinuxEntryHandler):
""" handle SELinux interface entries """
etype = "interface"
@@ -663,7 +674,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 +706,7 @@ class SELinuxPermissiveHandler(SELinuxEntryHandler):
return (entry.get("name"),)
-class SELinuxModuleHandler(SELinuxEntryHandler):
+class SELinuxSemoduleHandler(SELinuxEntryHandler):
""" handle SELinux module entries """
etype = "module"
@@ -808,10 +819,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 +901,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/Compat.py b/src/lib/Bcfg2/Compat.py
index 23f7ef784..b0f0ef5cf 100644
--- a/src/lib/Bcfg2/Compat.py
+++ b/src/lib/Bcfg2/Compat.py
@@ -245,3 +245,18 @@ except ImportError:
def wraps(wrapped): # pylint: disable=W0613
""" implementation of functools.wraps() for python 2.4 """
return lambda f: f
+
+
+def oct_mode(mode):
+ """ Convert a decimal number describing a POSIX permissions mode
+ to a string giving the octal mode. In Python 2, this is a synonym
+ for :func:`oct`, but in Python 3 the octal format has changed to
+ ``0o000``, which cannot be used as an octal permissions mode, so
+ we need to strip the 'o' from the output. I.e., this function
+ acts like the Python 2 :func:`oct` regardless of what version of
+ Python is in use.
+
+ :param mode: The decimal mode to convert to octal
+ :type mode: int
+ :returns: string """
+ return oct(mode).replace('o', '')
diff --git a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
index 06c99d899..fd9a545ce 100644
--- a/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Reporting/templates/clients/detailed-list.html
@@ -5,8 +5,8 @@
{% block pagebanner %}Clients - Detailed View{% endblock %}
{% block content %}
-<div class='client_list_box'>
{% filter_navigator %}
+<div class='client_list_box'>
{% if entry_list %}
<table cellpadding="3">
<tr id='table_list_header' class='listview'>
diff --git a/src/lib/Bcfg2/Reporting/views.py b/src/lib/Bcfg2/Reporting/views.py
index 7dc216bd4..8ab3f8e59 100644
--- a/src/lib/Bcfg2/Reporting/views.py
+++ b/src/lib/Bcfg2/Reporting/views.py
@@ -276,7 +276,7 @@ def client_index(request, timestamp=None, **kwargs):
"""
list = _handle_filters(Interaction.objects.recent(timestamp), **kwargs).\
- select_related().order_by("client__name").all()
+ select_related('client').order_by("client__name").all()
return render_to_response('clients/index.html',
{'inter_list': list,
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
index 3a78b4847..73550cd9d 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
@@ -11,8 +11,39 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP
try:
import genshi.core
from genshi.template import TemplateLoader, NewTextTemplate
- from genshi.template.eval import UndefinedError
+ from genshi.template.eval import UndefinedError, Suite
+ #: True if Genshi libraries are available
HAS_GENSHI = True
+
+ def _genshi_removes_blank_lines():
+ """ Genshi 0.5 uses the Python :mod:`compiler` package to
+ compile genshi snippets to AST. Genshi 0.6 uses some bespoke
+ magic, because compiler has been deprecated.
+ :func:`compiler.parse` produces an AST that removes all excess
+ whitespace (e.g., blank lines), while
+ :func:`genshi.template.astutil.parse` does not. In order to
+ determine which actual line of code an error occurs on, we
+ need to know which is in use and how it treats blank lines.
+ I've beat my head against this for hours and the best/only way
+ I can find is to compile some genshi code with an error and
+ see which line it's on."""
+ code = """d = dict()
+
+d['a']"""
+ try:
+ Suite(code).execute(dict())
+ except KeyError:
+ line = traceback.extract_tb(sys.exc_info()[2])[-1][1]
+ if line == 2:
+ return True
+ else:
+ return False
+
+ #: True if Genshi removes all blank lines from a code block before
+ #: executing it; False indicates that Genshi only removes leading
+ #: and trailing blank lines. See
+ #: :func:`_genshi_removes_blank_lines` for an explanation of this.
+ GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines()
except ImportError:
TemplateLoader = None # pylint: disable=C0103
HAS_GENSHI = False
@@ -111,7 +142,17 @@ class CfgGenshiGenerator(CfgGenerator):
# the traceback is just the beginning of the block.
err = exc[1]
stack = traceback.extract_tb(exc[2])
- lineno, func = stack[-1][1:3]
+
+ # find the right frame of the stack
+ for frame in reversed(stack):
+ if frame[0] == self.name:
+ lineno, func = frame[1:3]
+ break
+ else:
+ # couldn't even find the stack frame, wtf.
+ raise PluginExecutionError("%s: %s" %
+ (err.__class__.__name__, err))
+
execs = [contents
for etype, contents, _ in self.template.stream
if etype == self.template.EXEC]
@@ -129,18 +170,20 @@ class CfgGenshiGenerator(CfgGenerator):
# else, no EXEC blocks -- WTF?
if contents:
# we now have the bogus block, but we need to get the
- # offending line. To get there, we do (line number
- # given in the exception) - (firstlineno from the
- # internal genshi code object of the snippet) + 1 =
- # (line number of the line with an error within the
- # block, with all multiple line breaks elided to a
- # single line break)
- real_lineno = lineno - contents.code.co_firstlineno
- src = re.sub(r'\n\n+', '\n', contents.source).splitlines()
+ # offending line. To get there, we do (line number given
+ # in the exception) - (firstlineno from the internal
+ # genshi code object of the snippet) = (line number of the
+ # line with an error within the block, with blank lines
+ # removed as appropriate for
+ # :attr:`GENSHI_REMOVES_BLANK_LINES`)
+ code = contents.source.strip().splitlines()
+ if GENSHI_REMOVES_BLANK_LINES:
+ code = [l for l in code if l.strip()]
try:
+ line = code[lineno - contents.code.co_firstlineno]
raise PluginExecutionError("%s: %s at '%s'" %
(err.__class__.__name__, err,
- src[real_lineno]))
+ line))
except IndexError:
raise PluginExecutionError("%s: %s" %
(err.__class__.__name__, err))
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index db6810e7c..f8712213e 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -11,7 +11,8 @@ import Bcfg2.Options
import Bcfg2.Server.Plugin
import Bcfg2.Server.Lint
# pylint: disable=W0622
-from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any
+from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any, \
+ oct_mode
# pylint: enable=W0622
LOGGER = logging.getLogger(__name__)
@@ -538,7 +539,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet):
entry.get("name"))
fname = os.path.join(self.path, generator.name)
entry.set('mode',
- str(oct(stat.S_IMODE(os.stat(fname).st_mode))))
+ oct_mode(stat.S_IMODE(os.stat(fname).st_mode)))
try:
return generator.get_data(entry, metadata)
except:
diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
index 8bd1d3504..5ec0d7280 100644
--- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py
+++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py
@@ -24,7 +24,7 @@ import sys
import pwd
import grp
import Bcfg2.Client.XML
-from Bcfg2.Compat import b64encode
+from Bcfg2.Compat import b64encode, oct_mode
path = "%s"
@@ -41,7 +41,7 @@ data = Bcfg2.Client.XML.Element("ProbedFileData",
name=path,
owner=pwd.getpwuid(stat[4])[0],
group=grp.getgrgid(stat[5])[0],
- mode=oct(stat[0] & 4095))
+ mode=oct_mode(stat[0] & 4095))
try:
data.text = b64encode(open(path).read())
except:
@@ -101,7 +101,7 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin,
for data in datalist:
if data.text is None:
- self.logger.error("Got null response to %s file probe from %s"
+ self.logger.error("Got null response to %s file probe from %s"
% (data.get('name'), metadata.hostname))
else:
try:
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/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
index b3a49c047..f83c04e87 100644
--- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py
+++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py
@@ -3,253 +3,164 @@ certificates and their keys. """
import os
import sys
-import Bcfg2.Server.Plugin
-import Bcfg2.Options
-import lxml.etree
+import logging
import tempfile
+import lxml.etree
from subprocess import Popen, PIPE, STDOUT
-from Bcfg2.Compat import ConfigParser, md5
+import Bcfg2.Options
+import Bcfg2.Server.Plugin
+from Bcfg2.Compat import ConfigParser
from Bcfg2.Server.Plugin import PluginExecutionError
+LOGGER = logging.getLogger(__name__)
-class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
- """ The SSLCA generator handles the creation and management of ssl
- certificates and their keys. """
- __author__ = 'g.hagger@gmail.com'
- __child__ = Bcfg2.Server.Plugin.FileBacked
- key_specs = {}
- cert_specs = {}
- CAs = {}
- def __init__(self, core, datastore):
- Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore)
- self.infoxml = dict()
+class SSLCAXMLSpec(Bcfg2.Server.Plugin.StructFile):
+ """ Base class to handle key.xml and cert.xml """
+ attrs = dict()
+ tag = None
+
+ def get_spec(self, metadata):
+ """ Get a specification for the type of object described by
+ this SSLCA XML file for the given client metadata object """
+ entries = [e for e in self.Match(metadata) if e.tag == self.tag]
+ if len(entries) == 0:
+ raise PluginExecutionError("No matching %s entry found for %s "
+ "in %s" % (self.tag,
+ metadata.hostname,
+ self.name))
+ elif len(entries) > 1:
+ LOGGER.warning("More than one matching %s entry found for %s in "
+ "%s; using first match" % (self.tag,
+ metadata.hostname,
+ self.name))
+ rv = dict()
+ for attr, default in self.attrs.items():
+ val = entries[0].get(attr.lower(), default)
+ if default in ['true', 'false']:
+ rv[attr] = val == 'true'
+ else:
+ rv[attr] = val
+ return rv
+
+
+class SSLCAKeySpec(SSLCAXMLSpec):
+ """ Handle key.xml files """
+ attrs = dict(bits='2048', type='rsa')
+ tag = 'Key'
- def HandleEvent(self, event=None):
- """
- Updates which files this plugin handles based upon filesystem events.
- Allows configuration items to be added/removed without server restarts.
- """
- action = event.code2str()
- if event.filename[0] == '/':
- return
- epath = "".join([self.data, self.handles[event.requestID],
- event.filename])
- if os.path.isdir(epath):
- ident = self.handles[event.requestID] + event.filename
- else:
- ident = self.handles[event.requestID][:-1]
- fname = os.path.join(ident, event.filename)
+class SSLCACertSpec(SSLCAXMLSpec):
+ """ Handle cert.xml files """
+ attrs = dict(ca='default',
+ format='pem',
+ key=None,
+ days='365',
+ C=None,
+ L=None,
+ ST=None,
+ OU=None,
+ O=None,
+ emailAddress=None,
+ append_chain='false')
+ tag = 'Cert'
- if event.filename.endswith('.xml'):
+ def get_spec(self, metadata):
+ rv = SSLCAXMLSpec.get_spec(self, metadata)
+ rv['subjectaltname'] = [e.text for e in self.Match(metadata)
+ if e.tag == "SubjectAltName"]
+ return rv
+
+
+class SSLCADataFile(Bcfg2.Server.Plugin.SpecificData):
+ """ Handle key and cert files """
+ def bind_entry(self, entry, _):
+ """ Bind the data in the file to the given abstract entry """
+ entry.text = self.data
+ return entry
+
+
+class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet):
+ """ Entry set to handle SSLCA entries and XML files """
+ def __init__(self, _, path, entry_type, encoding, parent=None):
+ Bcfg2.Server.Plugin.EntrySet.__init__(self, os.path.basename(path),
+ path, entry_type, encoding)
+ self.parent = parent
+ self.key = None
+ self.cert = None
+
+ def handle_event(self, event):
+ action = event.code2str()
+ fpath = os.path.join(self.path, event.filename)
+
+ if event.filename == 'key.xml':
+ if action in ['exists', 'created', 'changed']:
+ self.key = SSLCAKeySpec(fpath)
+ self.key.HandleEvent(event)
+ elif event.filename == 'cert.xml':
if action in ['exists', 'created', 'changed']:
- if event.filename.endswith('key.xml'):
- key_spec = lxml.etree.parse(epath,
- parser=Bcfg2.Server.XMLParser
- ).find('Key')
- self.key_specs[ident] = {
- 'bits': key_spec.get('bits', '2048'),
- 'type': key_spec.get('type', 'rsa')
- }
- self.Entries['Path'][ident] = self.get_key
- elif event.filename.endswith('cert.xml'):
- cert_spec = lxml.etree.parse(epath,
- parser=Bcfg2.Server.XMLParser
- ).find('Cert')
- ca = cert_spec.get('ca', 'default')
- self.cert_specs[ident] = {
- 'ca': ca,
- 'format': cert_spec.get('format', 'pem'),
- 'key': cert_spec.get('key'),
- 'days': cert_spec.get('days', '365'),
- 'C': cert_spec.get('c'),
- 'L': cert_spec.get('l'),
- 'ST': cert_spec.get('st'),
- 'OU': cert_spec.get('ou'),
- 'O': cert_spec.get('o'),
- 'emailAddress': cert_spec.get('emailaddress'),
- 'append_chain':
- cert_spec.get('append_chain',
- 'false').lower() == 'true',
- }
- self.CAs[ca] = dict(self.core.setup.cfp.items('sslca_%s' %
- ca))
- self.Entries['Path'][ident] = self.get_cert
- elif event.filename.endswith("info.xml"):
- self.infoxml[ident] = Bcfg2.Server.Plugin.InfoXML(epath)
- self.infoxml[ident].HandleEvent(event)
- if action == 'deleted':
- if ident in self.Entries['Path']:
- del self.Entries['Path'][ident]
+ self.cert = SSLCACertSpec(fpath)
+ self.cert.HandleEvent(event)
else:
- if action in ['exists', 'created']:
- if os.path.isdir(epath):
- self.AddDirectoryMonitor(epath[len(self.data):])
- if ident not in self.entries and os.path.isfile(epath):
- self.entries[fname] = self.__child__(epath)
- self.entries[fname].HandleEvent(event)
- if action == 'changed':
- self.entries[fname].HandleEvent(event)
- elif action == 'deleted':
- if fname in self.entries:
- del self.entries[fname]
- else:
- self.entries[fname].HandleEvent(event)
-
- def get_key(self, entry, metadata):
+ Bcfg2.Server.Plugin.EntrySet.handle_event(self, event)
+
+ def build_key(self, entry, metadata):
"""
either grabs a prexisting key hostfile, or triggers the generation
of a new key if one doesn't exist.
"""
- # check if we already have a hostfile, or need to generate a new key
# TODO: verify key fits the specs
- path = entry.get('name')
- filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
- metadata.hostname))
- if filename not in list(self.entries.keys()):
- self.logger.info("SSLCA: Generating new key %s" % filename)
- key = self.build_key(entry)
- open(self.data + filename, 'w').write(key)
- entry.text = key
- self.entries[filename] = self.__child__(self.data + filename)
- self.entries[filename].HandleEvent()
- else:
- entry.text = self.entries[filename].data
-
- entry.set("type", "file")
- if path in self.infoxml:
- Bcfg2.Server.Plugin.bind_info(entry, metadata,
- infoxml=self.infoxml[path])
- else:
- Bcfg2.Server.Plugin.bind_info(entry, metadata)
-
- def build_key(self, entry):
- """ generates a new key according the the specification """
- ktype = self.key_specs[entry.get('name')]['type']
- bits = self.key_specs[entry.get('name')]['bits']
+ filename = "%s.H_%s" % (os.path.basename(entry.get('name')),
+ metadata.hostname)
+ self.logger.info("SSLCA: Generating new key %s" % filename)
+ key_spec = self.key.get_spec(metadata)
+ ktype = key_spec['type']
+ bits = key_spec['bits']
if ktype == 'rsa':
cmd = ["openssl", "genrsa", bits]
elif ktype == 'dsa':
cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits]
self.debug_log("SSLCA: Generating new key: %s" % " ".join(cmd))
- return Popen(cmd, stdout=PIPE).stdout.read()
-
- def get_cert(self, entry, metadata):
- """
- either grabs a prexisting cert hostfile, or triggers the generation
- of a new cert if one doesn't exist.
- """
- path = entry.get('name')
- filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path),
- metadata.hostname))
-
- # first - ensure we have a key to work with
- key = self.cert_specs[entry.get('name')].get('key')
- key_filename = os.path.join(key, "%s.H_%s" % (os.path.basename(key),
- metadata.hostname))
- if key_filename not in self.entries:
- el = lxml.etree.Element('Path')
- el.set('name', key)
- self.core.Bind(el, metadata)
-
- # check if we have a valid hostfile
- if (filename in self.entries.keys() and
- self.verify_cert(filename, key_filename, entry)):
- entry.text = self.entries[filename].data
- else:
- self.logger.info("SSLCA: Generating new cert %s" % filename)
- cert = self.build_cert(key_filename, entry, metadata)
- open(self.data + filename, 'w').write(cert)
- self.entries[filename] = self.__child__(self.data + filename)
- self.entries[filename].HandleEvent()
- entry.text = cert
-
- entry.set("type", "file")
- if path in self.infoxml:
- Bcfg2.Server.Plugin.bind_info(entry, metadata,
- infoxml=self.infoxml[path])
- else:
- Bcfg2.Server.Plugin.bind_info(entry, metadata)
-
- def verify_cert(self, filename, key_filename, entry):
- """ Perform certification verification against the CA and
- against the key """
- ca = self.CAs[self.cert_specs[entry.get('name')]['ca']]
- do_verify = ca.get('chaincert')
- if do_verify:
- return (self.verify_cert_against_ca(filename, entry) and
- self.verify_cert_against_key(filename, key_filename))
- return True
-
- def verify_cert_against_ca(self, filename, entry):
- """
- check that a certificate validates against the ca cert,
- and that it has not expired.
- """
- ca = self.CAs[self.cert_specs[entry.get('name')]['ca']]
- chaincert = ca.get('chaincert')
- cert = self.data + filename
- cmd = ["openssl", "verify"]
- is_root = ca.get('root_ca', "false").lower() == 'true'
- if is_root:
- cmd.append("-CAfile")
- else:
- # verifying based on an intermediate cert
- cmd.extend(["-purpose", "sslserver", "-untrusted"])
- cmd.extend([chaincert, cert])
- self.debug_log("SSLCA: Verifying %s against CA: %s" %
- (entry.get("name"), " ".join(cmd)))
- res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read()
- if res == cert + ": OK\n":
- self.debug_log("SSLCA: %s verified successfully against CA" %
- entry.get("name"))
- return True
- self.logger.warning("SSLCA: %s failed verification against CA: %s" %
- (entry.get("name"), res))
- return False
-
- def verify_cert_against_key(self, filename, key_filename):
- """
- check that a certificate validates against its private key.
- """
- cert = self.data + filename
- key = self.data + key_filename
- cert_md5 = \
- md5(Popen(["openssl", "x509", "-noout", "-modulus", "-in", cert],
- stdout=PIPE,
- stderr=STDOUT).stdout.read().strip()).hexdigest()
- key_md5 = \
- md5(Popen(["openssl", "rsa", "-noout", "-modulus", "-in", key],
- stdout=PIPE,
- stderr=STDOUT).stdout.read().strip()).hexdigest()
- if cert_md5 == key_md5:
- self.debug_log("SSLCA: %s verified successfully against key %s" %
- (filename, key_filename))
- return True
- self.logger.warning("SSLCA: %s failed verification against key %s" %
- (filename, key_filename))
- return False
+ proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ key, err = proc.communicate()
+ if proc.wait():
+ raise PluginExecutionError("SSLCA: Failed to generate key %s for "
+ "%s: %s" % (entry.get("name"),
+ metadata.hostname, err))
+ open(os.path.join(self.path, filename), 'w').write(key)
+ return key
- def build_cert(self, key_filename, entry, metadata):
- """
- creates a new certificate according to the specification
- """
+ def build_cert(self, entry, metadata, keyfile):
+ """ generate a new cert """
+ filename = "%s.H_%s" % (os.path.basename(entry.get('name')),
+ metadata.hostname)
+ self.logger.info("SSLCA: Generating new cert %s" % filename)
+ cert_spec = self.cert.get_spec(metadata)
+ ca = self.parent.get_ca(cert_spec['ca'])
req_config = None
req = None
try:
- req_config = self.build_req_config(entry, metadata)
- req = self.build_request(key_filename, req_config, entry)
- ca = self.cert_specs[entry.get('name')]['ca']
- ca_config = self.CAs[ca]['config']
- days = self.cert_specs[entry.get('name')]['days']
- passphrase = self.CAs[ca].get('passphrase')
- cmd = ["openssl", "ca", "-config", ca_config, "-in", req,
+ req_config = self.build_req_config(metadata)
+ req = self.build_request(keyfile, req_config, metadata)
+ days = cert_spec['days']
+ cmd = ["openssl", "ca", "-config", ca['config'], "-in", req,
"-days", days, "-batch"]
+ passphrase = ca.get('passphrase')
if passphrase:
cmd.extend(["-passin", "pass:%s" % passphrase])
+
+ def _scrub_pass(arg):
+ """ helper to scrub the passphrase from the
+ argument list """
+ if arg.startswith("pass:"):
+ return "pass:******"
+ else:
+ return arg
+ else:
+ _scrub_pass = lambda a: a
+
self.debug_log("SSLCA: Generating new certificate: %s" %
- " ".join(cmd))
+ " ".join(_scrub_pass(a) for a in cmd))
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
(cert, err) = proc.communicate()
if proc.wait():
@@ -266,12 +177,13 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
except OSError:
self.logger.error("SSLCA: Failed to unlink temporary files: %s"
% sys.exc_info()[1])
- if (self.cert_specs[entry.get('name')]['append_chain'] and
- self.CAs[ca]['chaincert']):
- cert += open(self.CAs[ca]['chaincert']).read()
+ if cert_spec['append_chain'] and 'chaincert' in ca:
+ cert += open(self.parent.get_ca(ca)['chaincert']).read()
+
+ open(os.path.join(self.path, filename), 'w').write(cert)
return cert
- def build_req_config(self, entry, metadata):
+ def build_req_config(self, metadata):
"""
generates a temporary openssl configuration file that is
used to generate the required certificate request
@@ -298,16 +210,17 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
cfp.add_section(section)
for key in defaults[section]:
cfp.set(section, key, defaults[section][key])
+ cert_spec = self.cert.get_spec(metadata)
altnamenum = 1
- altnames = list(metadata.aliases)
+ altnames = cert_spec['subjectaltname']
+ altnames.extend(list(metadata.aliases))
altnames.append(metadata.hostname)
for altname in altnames:
cfp.set('alt_names', 'DNS.' + str(altnamenum), altname)
altnamenum += 1
for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']:
- if self.cert_specs[entry.get('name')][item]:
- cfp.set('req_distinguished_name', item,
- self.cert_specs[entry.get('name')][item])
+ if cert_spec[item]:
+ cfp.set('req_distinguished_name', item, cert_spec[item])
cfp.set('req_distinguished_name', 'CN', metadata.hostname)
self.debug_log("SSLCA: Writing temporary request config to %s" % fname)
try:
@@ -317,16 +230,15 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
"config file: %s" % sys.exc_info()[1])
return fname
- def build_request(self, key_filename, req_config, entry):
+ def build_request(self, keyfile, req_config, metadata):
"""
creates the certificate request
"""
fd, req = tempfile.mkstemp()
os.close(fd)
- days = self.cert_specs[entry.get('name')]['days']
- key = self.data + key_filename
+ days = self.cert.get_spec(metadata)['days']
cmd = ["openssl", "req", "-new", "-config", req_config,
- "-days", days, "-key", key, "-text", "-out", req]
+ "-days", days, "-key", keyfile, "-text", "-out", req]
self.debug_log("SSLCA: Generating new CSR: %s" % " ".join(cmd))
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
err = proc.communicate()[1]
@@ -334,3 +246,122 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
raise PluginExecutionError("SSLCA: Failed to generate CSR: %s" %
err)
return req
+
+ def verify_cert(self, filename, keyfile, entry, metadata):
+ """ Perform certification verification against the CA and
+ against the key """
+ ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca'])
+ do_verify = ca.get('chaincert')
+ if do_verify:
+ return (self.verify_cert_against_ca(filename, entry, metadata) and
+ self.verify_cert_against_key(filename, keyfile))
+ return True
+
+ def verify_cert_against_ca(self, filename, entry, metadata):
+ """
+ check that a certificate validates against the ca cert,
+ and that it has not expired.
+ """
+ ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca'])
+ chaincert = ca.get('chaincert')
+ cert = os.path.join(self.path, filename)
+ cmd = ["openssl", "verify"]
+ is_root = ca.get('root_ca', "false").lower() == 'true'
+ if is_root:
+ cmd.append("-CAfile")
+ else:
+ # verifying based on an intermediate cert
+ cmd.extend(["-purpose", "sslserver", "-untrusted"])
+ cmd.extend([chaincert, cert])
+ self.debug_log("SSLCA: Verifying %s against CA: %s" %
+ (entry.get("name"), " ".join(cmd)))
+ res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read()
+ if res == cert + ": OK\n":
+ self.debug_log("SSLCA: %s verified successfully against CA" %
+ entry.get("name"))
+ return True
+ self.logger.warning("SSLCA: %s failed verification against CA: %s" %
+ (entry.get("name"), res))
+ return False
+
+ def verify_cert_against_key(self, filename, keyfile):
+ """
+ check that a certificate validates against its private key.
+ """
+ def _modulus(fname, ftype="x509"):
+ """ get the modulus from the given file """
+ cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname]
+ self.debug_log("SSLCA: Getting modulus of %s for verification: %s"
+ % (fname, " ".join(cmd)))
+ proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ rv, err = proc.communicate()
+ if proc.wait():
+ self.logger.warning("SSLCA: Failed to get modulus of %s: %s" %
+ (fname, err))
+ return rv.strip() # pylint: disable=E1103
+
+ certfile = os.path.join(self.path, filename)
+ cert = _modulus(certfile)
+ key = _modulus(keyfile, ftype="rsa")
+ if cert == key:
+ self.debug_log("SSLCA: %s verified successfully against key %s" %
+ (filename, keyfile))
+ return True
+ self.logger.warning("SSLCA: %s failed verification against key %s" %
+ (filename, keyfile))
+ return False
+
+ def bind_entry(self, entry, metadata):
+ if self.key:
+ self.bind_info_to_entry(entry, metadata)
+ try:
+ return self.best_matching(metadata).bind_entry(entry, metadata)
+ except PluginExecutionError:
+ entry.text = self.build_key(entry, metadata)
+ return entry
+ elif self.cert:
+ key = self.cert.get_spec(metadata)['key']
+ cleanup_keyfile = False
+ try:
+ keyfile = self.parent.entries[key].best_matching(metadata).name
+ except PluginExecutionError:
+ cleanup_keyfile = True
+ # create a temp file with the key in it
+ fd, keyfile = tempfile.mkstemp()
+ os.chmod(keyfile, 384) # 0600
+ el = lxml.etree.Element('Path', name=key)
+ self.parent.core.Bind(el, metadata)
+ os.fdopen(fd, 'w').write(el.text)
+
+ try:
+ self.bind_info_to_entry(entry, metadata)
+ try:
+ best = self.best_matching(metadata)
+ if self.verify_cert(best.name, keyfile, entry, metadata):
+ return best.bind_entry(entry, metadata)
+ except PluginExecutionError:
+ pass
+ # if we get here, it's because either a) there was no best
+ # matching entry; or b) the existing cert did not verify
+ entry.text = self.build_cert(entry, metadata, keyfile)
+ return entry
+ finally:
+ if cleanup_keyfile:
+ try:
+ os.unlink(keyfile)
+ except OSError:
+ err = sys.exc_info()[1]
+ self.logger.error("SSLCA: Failed to unlink temporary "
+ "key %s: %s" % (keyfile, err))
+
+
+class SSLCA(Bcfg2.Server.Plugin.GroupSpool):
+ """ The SSLCA generator handles the creation and management of ssl
+ certificates and their keys. """
+ __author__ = 'g.hagger@gmail.com'
+ es_cls = lambda self, *args: SSLCAEntrySet(*args, parent=self)
+ es_child_cls = SSLCADataFile
+
+ def get_ca(self, name):
+ """ get a dict describing a CA from the config file """
+ return dict(self.core.setup.cfp.items("sslca_%s" % name))
diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py
index b4ac47769..f83863cce 100644
--- a/src/lib/Bcfg2/version.py
+++ b/src/lib/Bcfg2/version.py
@@ -29,8 +29,8 @@ class Bcfg2VersionInfo(tuple):
tuple(self)
def __repr__(self):
- return "(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" % \
- tuple(self)
+ return "%s(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" \
+ % ((self.__class__.__name__,) + tuple(self))
def _release_cmp(self, rel1, rel2): # pylint: disable=R0911
""" compare two release numbers """
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..d6313b073 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)
@@ -401,7 +401,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", empty="true")
+ expected = lxml.etree.Element("Path", name="/test.txt", empty="true")
self.assertXMLEqual(bound, expected)
self.assertEqual(bound, entry)
@@ -420,7 +420,7 @@ class TestCfgEntrySet(TestEntrySet):
filters[1].modify_data.assert_called_with(entry, metadata,
"modified data")
self.assertFalse(eset._validate_data.called)
- expected = lxml.etree.Element("Path", name="/text.txt")
+ expected = lxml.etree.Element("Path", name="/test.txt")
expected.text = "final data"
self.assertXMLEqual(bound, expected)
@@ -436,7 +436,7 @@ class TestCfgEntrySet(TestEntrySet):
self.assertFalse(eset._validate_data.called)
mock_b64encode.assert_called_with("data")
self.assertFalse(mock_u_str.called)
- expected = lxml.etree.Element("Path", name="/text.txt",
+ expected = lxml.etree.Element("Path", name="/test.txt",
encoding="base64")
expected.text = "base64 data"
self.assertXMLEqual(bound, expected)
@@ -449,7 +449,7 @@ class TestCfgEntrySet(TestEntrySet):
eset.bind_info_to_entry.assert_called_with(entry, metadata)
eset._generate_data.assert_called_with(entry, metadata)
eset._validate_data.assert_called_with(entry, metadata, "data")
- 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/Testsrc/Testlib/TestServer/TestPlugins/TestSEModules.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestSEModules.py
index 8a148b353..66492f8b2 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestSEModules.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestSEModules.py
@@ -38,34 +38,29 @@ class TestSEModules(TestGroupSpool):
def test__get_module_name(self):
modules = self.get_obj()
for mname in ["foo", "foo.pp"]:
- entry = lxml.etree.Element("SELinux", type="module", name=mname)
+ entry = lxml.etree.Element("SEModule", type="module", name=mname)
self.assertEqual(modules._get_module_name(entry), "foo")
def test__get_module_filename(self):
modules = self.get_obj()
for mname in ["foo", "foo.pp"]:
- entry = lxml.etree.Element("SELinux", type="module", name=mname)
+ entry = lxml.etree.Element("SEModule", type="module", name=mname)
self.assertEqual(modules._get_module_filename(entry), "/foo.pp")
def test_HandlesEntry(self):
modules = self.get_obj()
modules._get_module_filename = Mock()
- modules.Entries['SELinux']['/foo.pp'] = Mock()
- modules.Entries['SELinux']['/bar.pp'] = Mock()
+ modules.Entries['SEModule']['/foo.pp'] = Mock()
+ modules.Entries['SEModule']['/bar.pp'] = Mock()
for el in [lxml.etree.Element("Path", name="foo.pp"),
- lxml.etree.Element("SELinux", type="fcontext",
- name="foo.pp"),
- lxml.etree.Element("SELinux", type="module",
- name="baz.pp")]:
+ lxml.etree.Element("SEModule", name="baz.pp")]:
modules._get_module_filename.return_value = "/" + el.get("name")
self.assertFalse(modules.HandlesEntry(el, Mock()))
- if el.get("type") == "module":
+ if el.tag == "SEModule":
modules._get_module_filename.assert_called_with(el)
- for el in [lxml.etree.Element("SELinux", type="module",
- name="foo.pp"),
- lxml.etree.Element("SELinux", type="module",
- name="bar.pp")]:
+ for el in [lxml.etree.Element("SEModule", name="foo.pp"),
+ lxml.etree.Element("SEModule", name="bar.pp")]:
modules._get_module_filename.return_value = "/" + el.get("name")
self.assertTrue(modules.HandlesEntry(el, Mock()),
msg="SEModules fails to handle %s" % el.get("name"))
@@ -77,10 +72,10 @@ class TestSEModules(TestGroupSpool):
modules = self.get_obj()
modules._get_module_name = Mock()
handler = Mock()
- modules.Entries['SELinux']['/foo.pp'] = handler
+ modules.Entries['SEModule']['/foo.pp'] = handler
modules._get_module_name.return_value = "foo"
- entry = lxml.etree.Element("SELinux", type="module", name="foo")
+ entry = lxml.etree.Element("SEModule", type="module", name="foo")
metadata = Mock()
self.assertEqual(modules.HandleEntry(entry, metadata),
handler.return_value)
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())