summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2012-07-03 08:56:47 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2012-07-03 08:56:47 -0400
commit09e934512dc053a96bd7b16c2c95563e055720f7 (patch)
treee1351268921fb0fc3b64df8d565044df25196930
parent9fe65b2fe9323da6583625cde1b2494352207d51 (diff)
downloadbcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.gz
bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.bz2
bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.zip
added selinux support
-rw-r--r--doc/server/configurationentries.txt167
-rw-r--r--doc/server/index.txt1
-rw-r--r--doc/server/info.txt45
-rw-r--r--doc/server/plugins/generators/rules.txt241
-rw-r--r--doc/server/plugins/generators/semodules.txt66
-rw-r--r--doc/server/selinux.txt97
-rw-r--r--schemas/base.xsd3
-rw-r--r--schemas/bundle.xsd127
-rw-r--r--schemas/defaults.xsd27
-rw-r--r--schemas/info.xsd1
-rw-r--r--schemas/pathentry.xsd21
-rw-r--r--schemas/rules.xsd113
-rw-r--r--schemas/servicetype.xsd2
-rw-r--r--schemas/types.xsd113
-rw-r--r--src/lib/Bcfg2/Client/Frame.py76
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX.py798
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py716
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py23
-rw-r--r--src/lib/Bcfg2/Options.py40
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py3
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py134
-rw-r--r--src/lib/Bcfg2/Server/Plugin.py12
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py46
-rwxr-xr-xsrc/sbin/bcfg234
-rwxr-xr-xtools/selinux_baseline.py51
25 files changed, 2084 insertions, 873 deletions
diff --git a/doc/server/configurationentries.txt b/doc/server/configurationentries.txt
index 8e669b90a..fb1589926 100644
--- a/doc/server/configurationentries.txt
+++ b/doc/server/configurationentries.txt
@@ -1,138 +1,13 @@
.. -*- mode: rst -*-
-.. NOTE: these are relative links (change when directory structure
-.. changes)
-
-.. _Base: plugins/structures/base
-.. _Bundler: plugins/structures/bundler
-.. _Cfg: plugins/generators/cfg.html
-.. _TGenshi: plugins/generators/tgenshi
-.. _TCheetah: plugins/generators/tcheetah.html
-.. _Rules: plugins/generators/rules.html
-
.. _server-configurationentries:
=====================
Configuration Entries
=====================
-This page describes the names and semantics of each of the configuration
-entries used by Bcfg2.
-
-Non-POSIX entries
-=================
-
-+-------------+---------------------+--------------------------------------------+
-| TagName | Description | Attributes |
-+=============+=====================+============================================+
-| Action | Command | name, command, when, timing, status, build |
-+-------------+---------------------+--------------------------------------------+
-| Package | Software Packages | name, type, version, url |
-+-------------+---------------------+--------------------------------------------+
-| PostInstall | PostInstall command | name |
-+-------------+---------------------+--------------------------------------------+
-| Service | System Services | name, type, status, target |
-+-------------+---------------------+--------------------------------------------+
-
-.. note::
-
- PostInstall entries are deprecated in favor of Action entries. In
- fact, a PostInstall entry is simply a specific type of Action.
- Basically, the following are equivalent:
-
- .. code-block:: xml
-
- <PostInstall name='foo'/>
-
- and
-
- .. code-block:: xml
-
- <Action timing='post' when='modified' name='n' command='foo' status='ignore'/>
-
-POSIX entries
-=============
-
-.. versionadded:: 1.0.0
-
-The unified POSIX Path entries prevent inconsistent configuration
-specifications of multiple entries for a given path. The following table
-describes the various types available for new **Path** entries.
-
-The abstract specification of these entries (i.e. In `Bundler`_)
-will only contain a *name* attribute. The type will be added by the
-plugin that handles the entry in the case of `Cfg`_, `TGenshi`_, or
-`TCheetah`_. If the entry is handled by the `Rules`_ plugin (i.e. it is
-a device, directory, hardlink, symlink, etc), then you will specify both
-the *type* and any other necessary attributes in `Rules`_.
-
-Running ``bcfg2-lint`` will check your configuration specification for
-the presence of any mandatory attributes that are necessary for the
-Path type specified.
-
-.. note:: A tool for converting old POSIX entries is available in the
- Bcfg2 source directory at tools/posixunified.py
-
-+-------------+----------------------+-----------------+--------------------------+
-| Type | Replacement/New | Description | Attributes |
-+=============+======================+=================+==========================+
-| device | New | Create block, | name, owner, group, |
-| | | character, and | dev_type |
-| | | fifo devices | (block, char, fifo), |
-| | | | major/minor |
-| | | | (for block/char devices) |
-+-------------+----------------------+-----------------+--------------------------+
-| directory | Replaces Directory | Directories | name, owner, group, |
-| | entries | | perms, prune |
-+-------------+----------------------+-----------------+--------------------------+
-| file | Replaces ConfigFile | Configuration | name, owner, group, |
-| | entries | File | perms, encoding, empty |
-| | | | |
-| | | | **Note:** see below |
-+-------------+----------------------+-----------------+--------------------------+
-| hardlink | New | Create | name, to |
-| | | hardlinks | |
-+-------------+----------------------+-----------------+--------------------------+
-| symlink | Replaces SymLink | SymLinks | name, to |
-| | entries | | |
-+-------------+----------------------+-----------------+--------------------------+
-| ignore | New | Ignore files | name |
-| | | that cause | |
-| | | package | |
-| | | verification | |
-| | | failures | |
-| | | (currently | |
-| | | applies to only | |
-| | | APT and YUMng) | |
-+-------------+----------------------+-----------------+--------------------------+
-| nonexistent | New | Specify a path | name, recursive |
-| | | that should not | |
-| | | exist | |
-+-------------+----------------------+-----------------+--------------------------+
-| permissions | Replaces Permissions | Permissions of | name, owner, group, |
-| | entries | POSIX entities | perms, recursive |
-| | | | |
-+-------------+----------------------+-----------------+--------------------------+
-| vcs | New | Create version | vcstype (git), |
-| | | control | sourceurl, revision |
-| | | checkout | |
-+-------------+----------------------+-----------------+--------------------------+
-
-Keep in mind that permissions for files handled by Cfg/TGenshi/TCheetah
-are still handled via the traditional :ref:`server-info` mechanisms.
-
-Additional information
-----------------------
-
-This section describes some additional behavior relating to POSIX entry
-attributes.
-
-Recursive permissions
-^^^^^^^^^^^^^^^^^^^^^
-
-As per the request in ticket 871, Path type='permissions' entries allow you to
-set a recursive attribute which allows the owner/group to be set recursively
-for a directory.
+The full semantics of each configuration entry is documented with the
+:ref:`server-plugins-generators-rules` plugin.
.. _boundentries:
@@ -178,13 +53,14 @@ Use Cases
Examples
--------
-* Consider the case of ``/etc/hosts`` on linux and ``/etc/inet/hosts`` on
- solaris. These files contain the same data in the same format,
+* Consider the case of ``/etc/hosts`` on linux and ``/etc/inet/hosts``
+ on solaris. These files contain the same data in the same format,
and should typically be synchronized, however, exist in different
locations. Classically, one would need to create one entry for each
- in `Cfg`_ or `TCheetah`_ and perform manual synchronization. Or,
- you could use symlinks and pray. Altsrc is driven from the bundle
- side. For example:
+ in :ref:`server-plugins-generators-cfg` or
+ :ref:`server-plugins-generators-tcheetah` and perform manual
+ synchronization. Or, you could use symlinks and pray. Altsrc is
+ driven from the bundle side. For example:
.. code-block:: xml
@@ -220,10 +96,12 @@ Examples
named "openssl" with different types.
* Finally, consider the case where there exist complicated, but
- completely independent specifications for the same configuration entry
- but different groups of clients. The following bundle will allow the use
- of two different `TCheetah`_ templates ``/etc/firewall-rules-external``
- and ``/etc/firewall-rules-internal`` for different clients based on
+ completely independent specifications for the same configuration
+ entry but different groups of clients. The following bundle will
+ allow the use of two different
+ :ref:`server-plugins-generators-tcheetah` templates
+ ``/etc/firewall-rules-external`` and
+ ``/etc/firewall-rules-internal`` for different clients based on
their group membership.
.. code-block:: xml
@@ -239,11 +117,13 @@ Examples
</Bundle>
* Consider the case where a variety of files can be constructed by a
- single template (`TCheetah`_ or `TGenshi`_). It would be possible to
- copy this template into the proper location for each file, but that
- requires proper synchronization upon modification and knowing up front
- what the files will all be called. Instead, the following bundle allows
- the use of a single template for all proper config file instances.
+ single template (:ref:`server-plugins-generators-tcheetah` or
+ :ref:`server-plugins-generators-tgenshi-index`). It would be
+ possible to copy this template into the proper location for each
+ file, but that requires proper synchronization upon modification and
+ knowing up front what the files will all be called. Instead, the
+ following bundle allows the use of a single template for all proper
+ config file instances.
.. code-block:: xml
@@ -253,5 +133,6 @@ Examples
<Path name='/etc/sysconfig/network-scripts/ifcfg-eth2' altsrc='/etc/ifcfg-template'/>
</Bundle>
- altsrc can be used as a parameter for any entry type, and can be used
- in any structure, including `Bundler`_ and `Base`_.
+ altsrc can be used as a parameter for any entry type, and can be
+ used in any structure, including
+ :ref:`server-plugins-structures-bundler-index`.
diff --git a/doc/server/index.txt b/doc/server/index.txt
index 9c427a0f4..fb1c95444 100644
--- a/doc/server/index.txt
+++ b/doc/server/index.txt
@@ -28,3 +28,4 @@ clients.
info
snapshots/index
bcfg2-info
+ selinux
diff --git a/doc/server/info.txt b/doc/server/info.txt
index d949aab68..d6bcf67e2 100644
--- a/doc/server/info.txt
+++ b/doc/server/info.txt
@@ -1,8 +1,5 @@
.. -*- mode: rst -*-
-.. NOTE: these are relative links (change when directory structure
-.. changes)
-
.. _server-info:
====
@@ -26,24 +23,29 @@ possible fields in an info file are:
+------------+-------------------+----------------------------------+---------+
| Field | Possible values | Description | Default |
+============+===================+==================================+=========+
-| encoding: | ascii | base64 | Encoding of the file. Use | ascii |
+| encoding | ascii | base64 | Encoding of the file. Use | ascii |
| | | base64 for binary files | |
+------------+-------------------+----------------------------------+---------+
-| group: | Any valid group | Sets group of the file | root |
+| owner | Any valid user | Sets owner of the file | root |
+------------+-------------------+----------------------------------+---------+
-| important: | true | false | Important entries are | false |
-| | | installed first during client | |
-| | | execution | |
+| group | Any valid group | Sets group of the file | root |
+------------+-------------------+----------------------------------+---------+
-| owner: | Any valid user | Sets owner of the file | root |
+| perms | Numeric file mode | Sets the permissions of the file | 0644 |
+| | | 'inherit' | (or inherits from the files on | |
+| | | disk if set to 'inherit') | |
+------------+-------------------+----------------------------------+---------+
-| paranoid: | true | false | Backup file before replacement? | true |
+| secontext | A valid SELinux | Sets the SELinux context of the | default |
+| | context string or | file, or sets to the default | |
+| | '__default__' | context set by policy if set to | |
+| | | '__default__' | |
+------------+-------------------+----------------------------------+---------+
-| perms: | Numeric file mode | Sets the permissions of the file | 0644 |
-| | | 'inherit' | (or inherits from the files on | |
-| | | disk if set to inherit) | |
+| important | true | false | Important entries are | false |
+| | | installed first during client | |
+| | | execution | |
+------------+-------------------+----------------------------------+---------+
-| sensitive: | true | false | The contents of sensitive | false |
+| paranoid | true | false | Backup file before replacement? | true |
++------------+-------------------+----------------------------------+---------+
+| sensitive | true | false | The contents of sensitive | false |
| | | entries aren't included in | |
| | | reports | |
+------------+-------------------+----------------------------------+---------+
@@ -54,15 +56,26 @@ A sample info file for CGI script on a web server might look like::
group: www
perms: 0755
+The equivalent ``info.xml`` file would be:
+
+.. code-block:: xml
+
+ <FileInfo>
+ <Info owner="www" group="www" perms="0755"/>
+ </FileInfo>
+
Back to the fstab example again, our final ``Cfg/etc/fstab/`` directory
might look like::
- :info
+ info.xml
fstab
fstab.G50_server
fstab.G99_fileserver
fstab.H_host.example.com
+See :ref:`server-selinux` for more information on the ``secontext``
+attribute and managing SELinux in general.
+
Important attribute
===================
@@ -76,7 +89,7 @@ specification.
+------------+-------------------+----------------------------------+---------+
| Field | Possible values | Description | Default |
+============+===================+==================================+=========+
-| important: | true | false | Important entries are | root |
+| important | true | false | Important entries are | root |
| | | installed first during client | |
| | | execution | |
+------------+-------------------+----------------------------------+---------+
diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt
index 3b1d94480..f693f6e62 100644
--- a/doc/server/plugins/generators/rules.txt
+++ b/doc/server/plugins/generators/rules.txt
@@ -46,6 +46,10 @@ Group membership may be negated.
Tag Attributes in Rules
=======================
+Running ``bcfg2-lint`` will check your configuration specification for
+the presence of any mandatory attributes that are necessary for the
+entry specified.
+
Rules Tag
---------
@@ -217,6 +221,12 @@ The Path tag has different values depending on the *type* attribute of
the path specified in your configuration. Below is a set of tables which
describe the attributes available for various Path types.
+Note that ``secontext`` below expects a full context, not just the
+type. For instance, "``system_u:object_r:etc_t:s0``", not just
+``etc_t``. You can also specify "``__default__``", which will restore
+the context of the file to the default set by policy. See
+:ref:`server-selinux` for more information.
+
Attributes common to all Path tags:
+----------+---------------------------------------------------+-----------------+
@@ -229,46 +239,58 @@ Attributes common to all Path tags:
device
^^^^^^
-+----------+---------------------+-------------------+
-| Name | Description | Values |
-+==========+=====================+===================+
-| dev_type | Type of device | (block|char|fifo) |
-+----------+---------------------+-------------------+
-| owner | Device owner | String |
-+----------+---------------------+-------------------+
-| group | Device group | String |
-+----------+---------------------+-------------------+
-| major | Major number (block | integer |
-| | or char devices) | |
-+----------+---------------------+-------------------+
-| minor | Minor number (block | integer |
-| | or char devices) | |
-+----------+---------------------+-------------------+
++-----------+---------------------+-------------------+
+| Name | Description | Values |
++===========+=====================+===================+
+| dev_type | Type of device | (block|char|fifo) |
++-----------+---------------------+-------------------+
+| owner | Device owner | String |
++-----------+---------------------+-------------------+
+| group | Device group | String |
++-----------+---------------------+-------------------+
+| secontext | SELinux context | String |
++-----------+---------------------+-------------------+
+| major | Major number (block | integer |
+| | or char devices) | |
++-----------+---------------------+-------------------+
+| minor | Minor number (block | integer |
+| | or char devices) | |
++-----------+---------------------+-------------------+
directory
^^^^^^^^^
-+-------+------------------------------+------------+
-| Name | Description | Values |
-+=======+==============================+============+
-| perms | Permissions of the directory | String |
-+-------+------------------------------+------------+
-| owner | Owner of the directory | String |
-+-------+------------------------------+------------+
-| group | Group Owner of the directory | String |
-+-------+------------------------------+------------+
-| prune | prune unspecified entries | true|false |
-| | from the Directory | |
-+-------+------------------------------+------------+
++-----------+------------------------------+------------+
+| Name | Description | Values |
++===========+==============================+============+
+| perms | Permissions of the directory | String |
++-----------+------------------------------+------------+
+| owner | Owner of the directory | String |
++-----------+------------------------------+------------+
+| group | Group Owner of the directory | String |
++-----------+------------------------------+------------+
+| secontext | SELinux context | String |
++-----------+------------------------------+------------+
+| prune | prune unspecified entries | true|false |
+| | from the Directory | |
++-----------+------------------------------+------------+
hardlink
^^^^^^^^
-+------+----------------------+--------+
-| Name | Description | Values |
-+======+======================+========+
-| to | File to link to | String |
-+------+----------------------+--------+
++-----------+------------------------------+--------+
+| Name | Description | Values |
++===========+==============================+========+
+| to | File to link to | String |
++-----------+------------------------------+--------+
+| perms | Permissions of the directory | String |
++-----------+------------------------------+--------+
+| owner | Owner of the directory | String |
++-----------+------------------------------+--------+
+| group | Group Owner of the directory | String |
++-----------+------------------------------+--------+
+| secontext | SELinux context | String |
++-----------+------------------------------+--------+
nonexistent
^^^^^^^^^^^
@@ -285,15 +307,17 @@ nonexistent
permissions
^^^^^^^^^^^
-+-------+--------------------------+--------+
-| Name | Description | Values |
-+=======+==========================+========+
-| perms | Permissions of the file. | String |
-+-------+--------------------------+--------+
-| owner | Owner of the file. | String |
-+-------+--------------------------+--------+
-| group | Group of the file. | String |
-+-------+--------------------------+--------+
++-----------+--------------------------+--------+
+| Name | Description | Values |
++===========+==========================+========+
+| perms | Permissions of the file. | String |
++-----------+--------------------------+--------+
+| owner | Owner of the file. | String |
++-----------+--------------------------+--------+
+| group | Group of the file. | String |
++-----------+--------------------------+--------+
+| secontext | SELinux context | String |
++-----------+--------------------------+--------+
symlink
^^^^^^^
@@ -304,6 +328,141 @@ symlink
| to | File to link to | String |
+------+----------------------+--------+
+SELinux Tag
+-----------
+
+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.
+
+Note that the ``selinuxtype`` attribute takes only an SELinux type,
+not a full context; e.g., "``etc_t``", not
+"``system_u:object_r:etc_t:s0``".
+
+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
+^^^^^^^
+
++-------+----------------------+---------+----------+
+| Name | Description | Values | Required |
++=======+======================+=========+==========+
+| name | Name of the boolean | String | Yes |
++-------+----------------------+---------+----------+
+| value | Value of the boolean | on|off | Yes |
++-------+----------------------+---------+----------+
+
+port
+^^^^
+
++-------------+------------------------+---------------------------+----------+
+| Name | Description | Values | Required |
++=============+========================+===========================+==========+
+| name | Port number or range | ``<port>/<proto>`` or | Yes |
+| | and protocol (tcp|udp) | ``<start>-<end>/<proto>`` | |
++-------------+------------------------+---------------------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to this port | | |
++-------------+------------------------+---------------------------+----------+
+
+fcontext
+^^^^^^^^
+
++-------------+-------------------------+---------------------+----------+
+| Name | Description | Values | Required |
++=============+=========================+=====================+==========+
+| name | File specification | String | Yes |
++-------------+-------------------------+---------------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to files matching this | | |
+| | specification | | |
++-------------+-------------------------+---------------------+----------+
+| filetype | File type to match. | (regular|directory| | No |
+| | Default: all | symlink|pipe|all| | |
+| | | socket|block|char) | |
++-------------+-------------------------+---------------------+----------+
+
+node
+^^^^
+
++-------------+------------------------------------+------------------+----------+
+| Name | Description | Values | Required |
++=============+====================================+==================+==========+
+| name | IP address and netmask of node. | <addr>/<netmask> | Yes |
+| | Netmask can be numeric (/16) or | | |
+| | dotted-quad (/255.255.0.0) | | |
++-------------+------------------------------------+------------------+----------+
+| selinuxtype | SELinux type to apply to this node | String | Yes |
++-------------+------------------------------------+------------------+----------+
+| proto | Protocol | (ipv4|ipv6) | Yes |
++-------------+------------------------------------+------------------+----------+
+| netmask | Netmask | String | Yes |
++-------------+------------------------------------+------------------+----------+
+
+login
+^^^^^
+
++-------------+-------------------------------+-----------+----------+
+| Name | Description | Values | Required |
++=============+===============================+===========+==========+
+| name | Unix username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+| selinuxuser | SELinux username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+
+user
+^^^^
+
++-------------+-------------------------------+-----------+----------+
+| Name | Description | Values | Required |
++=============+===============================+===========+==========+
+| name | SELinux username | String | Yes |
++-------------+-------------------------------+-----------+----------+
+| roles | Space-separated list of roles | String | No |
++-------------+-------------------------------+-----------+----------+
+| prefix | Home directory context prefix | String | No |
++-------------+-------------------------------+-----------+----------+
+
+interface
+^^^^^^^^^
+
++-------------+-------------------------+-------------+----------+
+| Name | Description | Values | Required |
++=============+=========================+=============+==========+
+| name | Interface name | String | Yes |
++-------------+-------------------------+-------------+----------+
+| selinuxtype | SELinux type to apply | String | Yes |
+| | to this interface | | |
++-------------+-------------------------+-------------+----------+
+
+permissive
+^^^^^^^^^^
+
++-------------+------------------------------------+-------------+----------+
+| Name | Description | Values | Required |
++=============+====================================+=============+==========+
+| name | SELinux type to make permissive | String | Yes |
++-------------+------------------------------------+-------------+----------+
+
+module
+^^^^^^
+
+See :ref:`server-plugins-generators-semodules`
+
Rules Directory
===============
diff --git a/doc/server/plugins/generators/semodules.txt b/doc/server/plugins/generators/semodules.txt
new file mode 100644
index 000000000..0d725fc1a
--- /dev/null
+++ b/doc/server/plugins/generators/semodules.txt
@@ -0,0 +1,66 @@
+.. -*- mode: rst -*-
+
+.. _server-plugins-generators-semodules:
+
+=========
+SEModules
+=========
+
+.. versionadded:: 1.3.0
+
+The SEModules plugin handles SELinux module entries. It supports
+group- and host-specific module versions, and enabling/disabling
+modules.
+
+You can use ``selinux_baseline.py`` located in the tools/ directory to
+create a baseline of all of your installed modules.
+
+See :ref:`server-selinux` for more information.
+
+Usage
+=====
+
+To use the SEModules plugin, first do ``mkdir
+/var/lib/bcfg2/SEModules``. Add ``SEModules`` to your ``plugins``
+line in ``/etc/bcfg2.conf`` and restart bcfg2-server.
+
+The SEModules directory contains modules in a layout similar to the
+Cfg plugin: at the top level, SEModules should contain directories
+named after the modules you want to install, and each of those
+directories can contain a global module, plus any number of group- and
+host-specific modules. For instance:
+
+ $ ls -F SEModules
+ foo.pp/ bar.pp/
+ $ ls SEModules/foo.pp/
+ foo.pp
+ foo.pp.G50_server
+ foo.pp.H_baz.example.com
+
+For more information on this directory layout, see
+:ref:`server-plugins-generators-cfg`.
+
+Entries
+=======
+
+SEModules handles ``<SELinux>`` entries with the ``module`` type. For
+instance:
+
+.. code-block:: xml
+
+ <Bundle name="foo">
+ <SELinux type="module" name="foo.pp"/>
+ </Bundle>
+
+The ``.pp`` extension is optional.
+
+.. note::
+
+ If you use a ``BoundSELinux`` tag, you must *not* include the
+ ``.pp`` extension. This is not recommend, though.
+
+You can also install a disabled module:
+
+.. code-block:: xml
+
+ <SELinux type="module" name="foo" disabled="true"/>
diff --git a/doc/server/selinux.txt b/doc/server/selinux.txt
new file mode 100644
index 000000000..0cbf0985e
--- /dev/null
+++ b/doc/server/selinux.txt
@@ -0,0 +1,97 @@
+.. -*- mode: rst -*-
+
+.. _server-selinux:
+
+=======
+SELinux
+=======
+
+.. versionadded:: 1.3.0
+
+Bcfg2 has the ability to handle the majority of SELinux entries with
+the ``SELinux`` entry type, which handles modules (with the
+:ref:`server-plugins-generators-semodules` plugin), file contexts,
+users and user mappings, permissive domains, nodes, and interfaces.
+In addition, ``info.xml`` files and most types of the ``Path`` tag can
+accept an ``secontext`` attribute to set the context of that entry.
+The full semantics of each configuration entry is documented with the
+:ref:`server-plugins-generators-rules` plugin.
+
+.. note:: The ``secontext`` attribute takes a *full* context,
+ e.g., "``system_u:object_r:etc_t:s0``"; the ``selinuxtype``
+ attribute always takes *only* an SELinux type, e.g.,
+ "``etc_t``". ``secontext`` (but not ``selinuxtype``) can
+ also accept the special value "``__default__``", which will
+ restore the context on the Path entry in question to the
+ default supplied by the SELinux policy.
+
+In its current version, the SELinux support in Bcfg2 is not sufficient
+to manage MCS/MLS policies.
+
+Extra Entries
+=============
+
+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.
+
+The actual definition of an "extra" entry actually depends on the
+version of SELinux available; the SELinux APIs have been extremely
+fluid, so many features available in newer versions are not available
+in older versions. Newer SELinux versions (e.g., in recent versions
+of Fedora) can be queried for only entries that have been locally
+modified; on these versions of SELinux, only locally modified entries
+will be considered extra. On older SELinux versions (e.g., on RHEL
+5), however, that functionality is missing, so *all* SELinux entries
+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.
+
+.. _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:
+
+.. code-block:: xml
+
+ <BoundSELinux type="fcontext" filetype="symlink"
+ name="/etc/localtime" selinuxtype="etc_t"/>
+ <BoundSELinux type="fcontext" filetype="regular"
+ name="/etc/localtime" selinuxtype="locale_t"/>
diff --git a/schemas/base.xsd b/schemas/base.xsd
index cca665b38..98682fdb5 100644
--- a/schemas/base.xsd
+++ b/schemas/base.xsd
@@ -10,6 +10,7 @@
<xsd:include schemaLocation="atom.xsd"/>
<xsd:include schemaLocation="pathentry.xsd"/>
<xsd:include schemaLocation="rules.xsd"/>
+ <xsd:include schemaLocation="types.xsd"/>
<xsd:group name='BaseEntries'>
<xsd:choice>
@@ -19,7 +20,7 @@
<xsd:element name='Path' type='PathEntry'/>
<xsd:element name='Service' type='StructureEntry'/>
<xsd:element name='BoundPackage' type='PackageType'/>
- <xsd:element name='BoundPath' type='BoundPathEntry'/>
+ <xsd:element name='BoundPath' type='PathType'/>
<xsd:element name='BoundService' type='ServiceType'/>
</xsd:choice>
</xsd:group>
diff --git a/schemas/bundle.xsd b/schemas/bundle.xsd
index 4e034ee3c..b72b174e2 100644
--- a/schemas/bundle.xsd
+++ b/schemas/bundle.xsd
@@ -16,10 +16,12 @@
<xsd:include schemaLocation="atom.xsd"/>
<xsd:include schemaLocation="pathentry.xsd"/>
<xsd:include schemaLocation="rules.xsd"/>
+ <xsd:include schemaLocation="types.xsd"/>
<xsd:include schemaLocation="services.xsd"/>
- <xsd:complexType name='GroupType'>
- <xsd:choice minOccurs='0' maxOccurs='unbounded'>
+ <xsd:group name="bundleElements">
+ <xsd:choice>
+ <xsd:group ref="py:genshiElements"/>
<xsd:element name='Package' type='PackageStructure'>
<xsd:annotation>
<xsd:documentation>
@@ -56,12 +58,20 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
+ <xsd:element name='SELinux' type='SELinuxStructure'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Abstract implementation of an SELinux entry. The
+ full specification will be included in Rules.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
<xsd:element name='PostInstall' type='StructureEntry'>
<xsd:annotation>
<xsd:documentation>
PostInstall entries are deprecated in favor of Action
- entries. Actions can do everything PostInstall entries can
- do and more.
+ entries. Actions can do everything PostInstall entries can
+ do and more.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
@@ -72,7 +82,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
- <xsd:element name='BoundPath' type='BoundPathEntry'>
+ <xsd:element name='BoundPath' type='PathType'>
<xsd:annotation>
<xsd:documentation>
Fully bound description of a filesystem path to be handled
@@ -94,6 +104,13 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
+ <xsd:element name='BoundSELinux' type='SELinuxType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of an SELinux entry.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
<xsd:element name='Group' type='GroupType'>
<xsd:annotation>
<xsd:documentation>
@@ -107,7 +124,7 @@
<xsd:annotation>
<xsd:documentation>
Elements within Client tags only apply to the named client
- (or vice-versa; see #element_negate below)
+ (or vice-versa; see #element_negate below)
</xsd:documentation>
</xsd:annotation>
</xsd:element>
@@ -119,7 +136,12 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
- <xsd:group ref="py:genshiElements"/>
+ </xsd:choice>
+ </xsd:group>
+
+ <xsd:complexType name='GroupType'>
+ <xsd:choice minOccurs='0' maxOccurs='unbounded'>
+ <xsd:group ref="bundleElements"/>
</xsd:choice>
<xsd:attribute type='xsd:string' name='name' use='required'>
<xsd:annotation>
@@ -140,96 +162,7 @@
<xsd:complexType name='BundleType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Package' type='PackageStructure'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of a Package entry. The full
- specification will be generated by a plugin such as
- Packages.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Path' type='PathEntry'>
- <xsd:annotation>
- <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
- the full specification of this entry will be included in
- Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Service' type='StructureEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of a Service entry. The full
- specification will be included in Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Action' type='StructureEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Abstract implementation of an Action entry. The full
- specification will be included in Rules.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundPackage' type='PackageType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a software package to be managed.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundPath' type='BoundPathEntry'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a filesystem path to be handled
- by the POSIX driver.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundService' type='ServiceType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a system service to be managed.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='BoundAction' type='ActionType'>
- <xsd:annotation>
- <xsd:documentation>
- Fully bound description of a command to be run.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Group' type='GroupType'>
- <xsd:annotation>
- <xsd:documentation>
- Elements within Group tags only apply to clients that are
- members of that group
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Client' type='GroupType'>
- <xsd:annotation>
- <xsd:documentation>
- Elements within Client tags only apply to the named client
- (or vice-versa; see #element_negate below)
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:element name='Bundle' type='BundleType'>
- <xsd:annotation>
- <xsd:documentation>
- Nesting Bundle tags is allowed in order to support
- XInclude within Bundles.
- </xsd:documentation>
- </xsd:annotation>
- </xsd:element>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="bundleElements"/>
</xsd:choice>
<xsd:attribute type='xsd:string' name='description' />
<xsd:attribute type='xsd:string' name='name'/>
diff --git a/schemas/defaults.xsd b/schemas/defaults.xsd
index c7e2edc7e..17ae84366 100644
--- a/schemas/defaults.xsd
+++ b/schemas/defaults.xsd
@@ -11,33 +11,6 @@
<xsd:include schemaLocation="types.xsd"/>
<xsd:include schemaLocation="pkgtype.xsd"/>
- <xsd:complexType name="ActionType">
- <xsd:attribute type="ActionTimingEnum" name="timing"/>
- <xsd:attribute type="ActionWhenEnum" name="when"/>
- <xsd:attribute type="ActionStatusEnum" name="status"/>
- <xsd:attribute type="xsd:boolean" name="build"/>
- <xsd:attribute type="xsd:string" name="name" use="required"/>
- <xsd:attribute type="xsd:string" name="command"/>
- </xsd:complexType>
-
- <xsd:complexType name="PathType">
- <xsd:attribute type="PathTypeEnum" name="type"/>
- <xsd:attribute type="xsd:string" name="name" use="required"/>
- <xsd:attribute type="xsd:string" name="dev_type"/>
- <xsd:attribute type="xsd:string" name="major"/>
- <xsd:attribute type="xsd:string" name="minor"/>
- <xsd:attribute type="xsd:string" name="mode"/>
- <xsd:attribute type="xsd:string" name="perms"/>
- <xsd:attribute type="xsd:string" name="owner"/>
- <xsd:attribute type="xsd:string" name="group"/>
- <xsd:attribute type="xsd:string" name="recursive"/>
- <xsd:attribute type="xsd:string" name="prune"/>
- <xsd:attribute type="xsd:string" name="to"/>
- <xsd:attribute type="xsd:string" name="vcstype"/>
- <xsd:attribute type="xsd:string" name="revision"/>
- <xsd:attribute type="xsd:string" name="sourceurl"/>
- </xsd:complexType>
-
<xsd:complexType name="DContainerType">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Service" type="ServiceType"/>
diff --git a/schemas/info.xsd b/schemas/info.xsd
index 37232ab23..c45c40ebc 100644
--- a/schemas/info.xsd
+++ b/schemas/info.xsd
@@ -14,6 +14,7 @@
<xsd:attribute name='important' type='xsd:string'/>
<xsd:attribute name='owner' type='xsd:string'/>
<xsd:attribute name='perms' type='xsd:string'/>
+ <xsd:attribute name='secontext' type='xsd:string'/>
<xsd:attribute name='paranoid' type='xsd:boolean'/>
<xsd:attribute name='sensitive' type='xsd:boolean'/>
</xsd:complexType>
diff --git a/schemas/pathentry.xsd b/schemas/pathentry.xsd
index 080758d0b..e5d2ef6af 100644
--- a/schemas/pathentry.xsd
+++ b/schemas/pathentry.xsd
@@ -16,25 +16,4 @@
<xsd:attribute type='xsd:string' name='altsrc' use='optional'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
-
- <xsd:complexType name='BoundPathEntry'>
- <xsd:attribute type='xsd:string' name='name' use='required'/>
- <xsd:attribute type='xsd:string' name='group' use='optional'/>
- <xsd:attribute type='xsd:string' name='important' use='optional'/>
- <xsd:attribute type='xsd:string' name='owner' use='optional'/>
- <xsd:attribute type='xsd:string' name='paranoid' use='optional'/>
- <xsd:attribute type='xsd:string' name='perms' use='optional'/>
- <xsd:attribute type='xsd:string' name='prune' use='optional'/>
- <xsd:attribute type='xsd:string' name='recursive' use='optional'/>
- <xsd:attribute type='xsd:string' name='sensitive' use='optional'/>
- <xsd:attribute type='xsd:string' name='to' use='optional'/>
- <xsd:attribute type='xsd:string' name='type' use='optional'/>
- <!-- device attributes -->
- <xsd:attribute type='xsd:string' name='dev_type' use='optional'/>
- <xsd:attribute type='xsd:string' name='major' use='optional'/>
- <xsd:attribute type='xsd:string' name='minor' use='optional'/>
- <xsd:attribute type='xsd:string' name='mode' use='optional'/>
- <!-- end device attributes -->
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
</xsd:schema>
diff --git a/schemas/rules.xsd b/schemas/rules.xsd
index 924792b18..2f4f805c0 100644
--- a/schemas/rules.xsd
+++ b/schemas/rules.xsd
@@ -14,66 +14,91 @@
<xsd:import namespace="http://genshi.edgewall.org/"
schemaLocation="genshi.xsd"/>
- <xsd:complexType name='ActionType'>
- <xsd:attribute type='ActionTimingEnum' name='timing'/>
- <xsd:attribute type='ActionWhenEnum' name='when'/>
- <xsd:attribute type='ActionStatusEnum' name='status'/>
- <xsd:attribute type="xsd:boolean" name="build"/>
- <xsd:attribute type='xsd:string' name='name'/>
- <xsd:attribute type='xsd:string' name='command'/>
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
-
<xsd:complexType name='PostInstallType'>
<xsd:attribute type='xsd:string' name='name' use='required'/>
</xsd:complexType>
- <xsd:complexType name='PathType'>
- <xsd:attribute type='PathTypeEnum' name='type' use='required'/>
- <xsd:attribute type='xsd:string' name='name' use='required'/>
- <xsd:attribute type='xsd:string' name='dev_type'/>
- <xsd:attribute type='xsd:string' name='major'/>
- <xsd:attribute type='xsd:string' name='minor'/>
- <xsd:attribute type='xsd:string' name='mode'/>
- <xsd:attribute type='xsd:string' name='perms'/>
- <xsd:attribute type='xsd:string' name='owner'/>
- <xsd:attribute type='xsd:string' name='group'/>
- <xsd:attribute type='xsd:string' name='recursive'/>
- <xsd:attribute type='xsd:string' name='prune'/>
- <xsd:attribute type='xsd:string' name='to'/>
- <xsd:attribute type='xsd:string' name='vcstype'/>
- <xsd:attribute type='xsd:string' name='revision'/>
- <xsd:attribute type='xsd:string' name='sourceurl'/>
- <xsd:attributeGroup ref="py:genshiAttrs"/>
- </xsd:complexType>
+ <xsd:group name="rulesElements">
+ <xsd:choice>
+ <xsd:group ref="py:genshiElements"/>
+ <xsd:element name='Package' type='PackageType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a software package to be managed.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Path' type='PathType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a filesystem path to be handled
+ by the POSIX driver.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Service' type='ServiceType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a system service to be managed.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Action' type='ActionType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of a command to be run.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='SELinux' type='SELinuxType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Fully bound description of an SELinux entry.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='PostInstall' type='PostInstallType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ PostInstall entries are deprecated in favor of Action
+ entries. Actions can do everything PostInstall entries can
+ do and more.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Group' type='RContainerType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Elements within Group tags only apply to clients that are
+ members of that group (or vice-versa; see #element_negate
+ below)
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ <xsd:element name='Client' type='RContainerType'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Elements within Client tags only apply to the named client
+ (or vice-versa; see #element_negate below)
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:group>
<xsd:complexType name='RContainerType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Service' type='ServiceType'/>
- <xsd:element name='Package' type='PackageType'/>
- <xsd:element name='Path' type='PathType'/>
- <xsd:element name='Action' type='ActionType'/>
- <xsd:element name='Group' type='RContainerType'/>
- <xsd:element name='Client' type='RContainerType'/>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="rulesElements"/>
</xsd:choice>
<xsd:attribute name='name' type='xsd:string'/>
<xsd:attribute name='negate' type='xsd:boolean'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
</xsd:complexType>
-
<xsd:element name='Rules'>
<xsd:complexType>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Service' type='ServiceType'/>
- <xsd:element name='Package' type='PackageType'/>
- <xsd:element name='Path' type='PathType'/>
- <xsd:element name='Action' type='ActionType'/>
- <xsd:element name='PostInstall' type='PostInstallType'/>
- <xsd:element name='Group' type='RContainerType'/>
- <xsd:element name='Client' type='RContainerType'/>
- <xsd:group ref="py:genshiElements"/>
+ <xsd:group ref="rulesElements"/>
</xsd:choice>
<xsd:attribute name='priority' type='xsd:integer' use='required'/>
<xsd:attributeGroup ref="py:genshiAttrs"/>
diff --git a/schemas/servicetype.xsd b/schemas/servicetype.xsd
index 4cab3716c..7de847c7f 100644
--- a/schemas/servicetype.xsd
+++ b/schemas/servicetype.xsd
@@ -13,7 +13,7 @@
schemaLocation="genshi.xsd"/>
<xsd:simpleType name='RestartEnum'>
- <xsd:restriction base='xsd:boolean'>
+ <xsd:restriction base='xsd:string'>
<xsd:enumeration value='true'/>
<xsd:enumeration value='false'/>
<xsd:enumeration value='1'/>
diff --git a/schemas/types.xsd b/schemas/types.xsd
index ead377192..a7dae15c9 100644
--- a/schemas/types.xsd
+++ b/schemas/types.xsd
@@ -1,5 +1,6 @@
-<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xml:lang="en">
-
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:py="http://genshi.edgewall.org/" xml:lang="en">
+
<xsd:annotation>
<xsd:documentation>
string enumeration definitions for bcfg2
@@ -7,6 +8,9 @@
</xsd:documentation>
</xsd:annotation>
+ <xsd:import namespace="http://genshi.edgewall.org/"
+ schemaLocation="genshi.xsd"/>
+
<xsd:simpleType name='PackageTypeEnum'>
<xsd:restriction base='xsd:string'>
<xsd:enumeration value='deb' />
@@ -86,4 +90,109 @@
</xsd:restriction>
</xsd:simpleType>
+ <xsd:complexType name='ActionType'>
+ <xsd:attribute type='ActionTimingEnum' name='timing'/>
+ <xsd:attribute type='ActionWhenEnum' name='when'/>
+ <xsd:attribute type='ActionStatusEnum' name='status'/>
+ <xsd:attribute type="xsd:boolean" name="build"/>
+ <xsd:attribute type='xsd:string' name='name'/>
+ <xsd:attribute type='xsd:string' name='command'/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:simpleType name="DeviceTypeEnum">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="block"/>
+ <xsd:enumeration value="char"/>
+ <xsd:enumeration value="fifo"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="PathType">
+ <xsd:attribute type="PathTypeEnum" name="type"/>
+ <xsd:attribute type="xsd:string" name="name" use="required"/>
+ <xsd:attribute type="DeviceTypeEnum" name="dev_type"/>
+ <xsd:attribute type="xsd:integer" name="major"/>
+ <xsd:attribute type="xsd:integer" name="minor"/>
+ <xsd:attribute type="xsd:string" name="mode"/>
+ <xsd:attribute type="xsd:string" name="perms"/>
+ <xsd:attribute type="xsd:string" name="owner"/>
+ <xsd:attribute type="xsd:string" name="group"/>
+ <xsd:attribute type="xsd:string" name="secontext"/>
+ <xsd:attribute type="xsd:string" name="recursive"/>
+ <xsd:attribute type="xsd:string" name="prune"/>
+ <xsd:attribute type="xsd:string" name="to"/>
+ <xsd:attribute type="xsd:string" name="vcstype"/>
+ <xsd:attribute type="xsd:string" name="revision"/>
+ <xsd:attribute type="xsd:string" name="sourceurl"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:simpleType name='SELinuxTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='boolean'/>
+ <xsd:enumeration value='module'/>
+ <xsd:enumeration value='port'/>
+ <xsd:enumeration value='fcontext'/>
+ <xsd:enumeration value='node'/>
+ <xsd:enumeration value='login'/>
+ <xsd:enumeration value='user'/>
+ <xsd:enumeration value='interface'/>
+ <xsd:enumeration value='permissive'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxFileTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='all'/>
+ <xsd:enumeration value='regular'/>
+ <xsd:enumeration value='directory'/>
+ <xsd:enumeration value='symlink'/>
+ <xsd:enumeration value='pipe'/>
+ <xsd:enumeration value='socket'/>
+ <xsd:enumeration value='block'/>
+ <xsd:enumeration value='char'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxBooleanValueEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='on'/>
+ <xsd:enumeration value='off'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:simpleType name='SELinuxEntryTypeEnum'>
+ <xsd:restriction base='xsd:string'>
+ <xsd:enumeration value='boolean'/>
+ <xsd:enumeration value='module'/>
+ <xsd:enumeration value='port'/>
+ <xsd:enumeration value='fcontext'/>
+ <xsd:enumeration value='node'/>
+ <xsd:enumeration value='login'/>
+ <xsd:enumeration value='user'/>
+ <xsd:enumeration value='interface'/>
+ <xsd:enumeration value='permissive'/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="SELinuxStructure">
+ <xsd:attribute type='xsd:string' name='name' use='required'/>
+ <xsd:attribute type="xsd:boolean" name="disabled"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
+
+ <xsd:complexType name="SELinuxType">
+ <xsd:attribute type="xsd:string" name="name" use="required"/>
+ <xsd:attribute type="SELinuxEntryTypeEnum" name="type" use="required"/>
+ <xsd:attribute type="SELinuxBooleanValueEnum" name="value"/>
+ <xsd:attribute type="xsd:boolean" name="disabled"/>
+ <xsd:attribute type="xsd:string" name="selinuxtype"/>
+ <xsd:attribute type="SELinuxFileTypeEnum" name="filetype"/>
+ <xsd:attribute type="xsd:string" name="proto"/>
+ <xsd:attribute type="xsd:string" name="roles"/>
+ <xsd:attribute type="xsd:string" name="prefix"/>
+ <xsd:attribute type="xsd:string" name="selinuxuser"/>
+ <xsd:attributeGroup ref="py:genshiAttrs"/>
+ </xsd:complexType>
</xsd:schema>
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index a8bcb69bf..51bc4aec7 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -124,33 +124,47 @@ class Frame:
self.logger.info([tool.name for tool in self.tools])
# find entries not handled by any tools
- problems = [entry for struct in config for \
- entry in struct if entry not in self.handled]
+ problems = [entry for struct in config
+ for entry in struct
+ if entry not in self.handled]
if problems:
self.logger.error("The following entries are not handled by any tool:")
- self.logger.error(["%s:%s:%s" % (entry.tag, entry.get('type'), \
- entry.get('name')) for entry in problems])
+ for entry in problems:
+ self.logger.error("%s:%s:%s" % (entry.tag, entry.get('type'),
+ entry.get('name')))
self.logger.error("")
- entries = [(entry.tag, entry.get('name'))
- for struct in config for entry in struct]
+
+ self.find_dups(config)
+
pkgs = [(entry.get('name'), entry.get('origin'))
- for struct in config for entry in struct if entry.tag == 'Package']
- multi = []
- for entry in entries[:]:
- if entries.count(entry) > 1:
- multi.append(entry)
- entries.remove(entry)
- if multi:
- self.logger.debug("The following entries are included multiple times:")
- self.logger.debug(["%s:%s" % entry for entry in multi])
- self.logger.debug("")
+ for struct in config
+ for entry in struct
+ if entry.tag == 'Package']
if pkgs:
self.logger.debug("The following packages are specified in bcfg2:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == None])
self.logger.debug("The following packages are prereqs added by Packages:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == 'Packages'])
+ def find_dups(self, config):
+ entries = dict()
+ for struct in config:
+ for entry in struct:
+ for tool in self.tools:
+ if tool.handlesEntry(entry):
+ pkey = tool.primarykey(entry)
+ if pkey in entries:
+ entries[pkey] += 1
+ else:
+ entries[pkey] = 1
+ multi = [e for e, c in entries.items() if c > 1]
+ if multi:
+ self.logger.debug("The following entries are included multiple times:")
+ for entry in multi:
+ self.logger.debug(entry)
+ self.logger.debug("")
+
def __getattr__(self, name):
if name in ['extra', 'handled', 'modified', '__important__']:
ret = []
@@ -399,16 +413,32 @@ class Frame:
def CondDisplayState(self, phase):
"""Conditionally print tracing information."""
self.logger.info('\nPhase: %s' % phase)
- self.logger.info('Correct entries:\t%d' % list(self.states.values()).count(True))
- self.logger.info('Incorrect entries:\t%d' % list(self.states.values()).count(False))
+ self.logger.info('Correct entries:\t%d' %
+ list(self.states.values()).count(True))
+ self.logger.info('Incorrect entries:\t%d' %
+ list(self.states.values()).count(False))
if phase == 'final' and list(self.states.values()).count(False):
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for \
- entry in self.states if not self.states[entry]])
- self.logger.info('Total managed entries:\t%d' % len(list(self.states.values())))
+ for entry in self.states.keys():
+ if not self.states[entry]:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
+ self.logger.info('Total managed entries:\t%d' %
+ len(list(self.states.values())))
self.logger.info('Unmanaged entries:\t%d' % len(self.extra))
if phase == 'final' and self.setup['extra']:
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) \
- for entry in self.extra])
+ for entry in self.extra:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
self.logger.info("")
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX.py b/src/lib/Bcfg2/Client/Tools/POSIX.py
index 995d82356..859d4dd81 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX.py
@@ -20,7 +20,13 @@ import Bcfg2.Client.Tools
import Bcfg2.Options
from Bcfg2.Client import XML
-log = logging.getLogger('POSIX')
+log = logging.getLogger(__name__)
+
+try:
+ import selinux
+ has_selinux = True
+except ImportError:
+ has_selinux = False
# map between dev_type attribute and stat constants
device_map = {'block': stat.S_IFBLK,
@@ -28,24 +34,7 @@ device_map = {'block': stat.S_IFBLK,
'fifo': stat.S_IFIFO}
-def calcPerms(initial, perms):
- """This compares ondisk permissions with specified ones."""
- pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID},
- {1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR},
- {1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP},
- {1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}]
- tempperms = initial
- if len(perms) == 3:
- perms = '0%s' % (perms)
- pdigits = [int(perms[digit]) for digit in range(4)]
- for index in range(4):
- for (num, perm) in list(pdisp[index].items()):
- if pdigits[index] & num:
- tempperms |= perm
- return tempperms
-
-
-def normGid(entry, logger=None):
+def normGid(entry):
"""
This takes a group name or gid and
returns the corresponding gid or False.
@@ -96,6 +85,115 @@ def isString(strng, encoding):
return False
+def secontextMatches(entry):
+ """ determine if the SELinux context of the file on disk matches
+ the desired context """
+ if not has_selinux:
+ # no selinux libraries
+ return True
+
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ if context == '__default__':
+ if selinux.getfilecon(entry.get('name'))[1] == \
+ selinux.matchpathcon(entry.get('name'), 0)[1]:
+ return True
+ else:
+ return False
+ elif selinux.getfilecon(entry.get('name'))[1] == context:
+ return True
+ else:
+ return False
+
+
+def setSEContext(entry, path=None, recursive=False):
+ """ set the SELinux context of the file on disk according to the
+ config"""
+ if not has_selinux:
+ return True
+
+ if path is None:
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ rv = True
+ if context == '__default__':
+ try:
+ selinux.restorecon(path, recursive=recursive)
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+ else:
+ try:
+ rv &= selinux.lsetfilecon(path, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for p in dirs + files:
+ try:
+ rv &= selinux.lsetfilecon(p, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s"
+ % (path, err))
+ rv = False
+ return rv
+
+
+def setPerms(entry, path=None):
+ if path is None:
+ path = entry.get("name")
+
+ if (entry.get('perms') == None or
+ entry.get('owner') == None or
+ entry.get('group') == None):
+ self.logger.error('Entry %s not completely specified. '
+ 'Try running bcfg2-lint.' % entry.get('name'))
+ return False
+
+ rv = True
+ # split this into multiple try...except blocks so that even if a
+ # chown fails, the chmod can succeed -- get as close to the
+ # desired state as we can
+ try:
+ os.chown(path, normUid(entry), normGid(entry))
+ except KeyError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+ os.chown(path, 0, 0)
+ except OSError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ try:
+ os.chmod(path, configPerms)
+ except (OSError, KeyError):
+ logger.error('Failed to change permissions mode of %s' % path)
+ rv = False
+
+ if has_selinux:
+ rv &= setSEContext(entry, path=path)
+
+ return rv
+
+
class POSIX(Bcfg2.Client.Tools.Tool):
"""POSIX File support code."""
name = 'POSIX'
@@ -106,7 +204,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
('Path', 'nonexistent'),
('Path', 'permissions'),
('Path', 'symlink')]
- __req__ = {'Path': ['name', 'type']}
+ __req__ = dict(Path=dict(
+ device=['name', 'dev_type', 'perms', 'owner', 'group'],
+ directory=['name', 'perms', 'owner', 'group'],
+ file=['name', 'perms', 'owner', 'group'],
+ hardlink=['name', 'to'],
+ nonexistent=['name'],
+ permissions=['name', 'perms', 'owner', 'group'],
+ symlink=['name', 'to']))
# grab paranoid options from /etc/bcfg2.conf
opts = {'ppath': Bcfg2.Options.PARANOID_PATH,
@@ -119,13 +224,9 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def canInstall(self, entry):
"""Check if entry is complete for installation."""
if Bcfg2.Client.Tools.Tool.canInstall(self, entry):
- if (entry.tag,
- entry.get('type'),
- entry.text,
- entry.get('empty', 'false')) == ('Path',
- 'file',
- None,
- 'false'):
+ if (entry.get('type') == 'file' and
+ entry.text is None and
+ entry.get('empty', 'false') == 'false'):
return False
return True
else:
@@ -145,69 +246,60 @@ class POSIX(Bcfg2.Client.Tools.Tool):
entry.set('current_group', str(ondisk[stat.ST_GID]))
except (OSError, KeyError):
pass
+
+ if has_selinux:
+ try:
+ entry.set('current_secontext',
+ selinux.getfilecon(entry.get('name'))[1])
+ except (OSError, KeyError):
+ pass
entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:]))
def Verifydevice(self, entry, _):
"""Verify device entry."""
- if entry.get('dev_type') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
if entry.get('dev_type') in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ (entry.get('name')))
return False
+
try:
- # check for file existence
- filestat = os.stat(entry.get('name'))
+ ondisk = os.stat(path)
except OSError:
entry.set('current_exists', 'false')
self.logger.debug("%s %s does not exist" %
- (entry.tag, entry.get('name')))
+ (entry.tag, path))
return False
- try:
- # attempt to verify device properties as specified in config
- dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
- if dev_type in ['block', 'char']:
- # check for incompletely specified entries
- if entry.get('major') == None or \
- entry.get('minor') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- major = int(entry.get('major'))
- minor = int(entry.get('minor'))
- if major == os.major(filestat.st_rdev) and \
- minor == os.minor(filestat.st_rdev) and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- return False
- elif dev_type == 'fifo' and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- self.logger.info('Device properties for %s incorrect' % \
- entry.get('name'))
- return False
- except OSError:
- self.logger.debug("%s %s failed to verify" %
- (entry.tag, entry.get('name')))
- return False
+ rv = self._verify_metadata(entry)
+
+ # attempt to verify device properties as specified in config
+ dev_type = entry.get('dev_type')
+ if dev_type in ['block', 'char']:
+ major = int(entry.get('major'))
+ minor = int(entry.get('minor'))
+ if major != os.major(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Major number for device %s is incorrect. "
+ "Current major is %s but should be %s" %
+ (path, os.major(ondisk.st_rdev), major))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ if minor != os.minor(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Minor number for device %s is incorrect. "
+ "Current minor is %s but should be %s" %
+ (path, os.minor(ondisk.st_rdev), minor))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ return rv
def Installdevice(self, entry):
"""Install device entries."""
@@ -218,7 +310,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.unlink(entry.get('name'))
exists = False
except OSError:
- self.logger.info('Failed to unlink %s' % \
+ self.logger.info('Failed to unlink %s' %
entry.get('name'))
return False
except OSError:
@@ -227,14 +319,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
if not exists:
try:
dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
+ mode = device_map[dev_type] | int(entry.get('mode', '0600'), 8)
if dev_type in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ entry.get('name'))
return False
major = int(entry.get('major'))
minor = int(entry.get('minor'))
@@ -242,17 +334,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.mknod(entry.get('name'), mode, device)
else:
os.mknod(entry.get('name'), mode)
- """
- Python uses the OS mknod(2) implementation which modifies the
- mode based on the umask of the running process. Therefore, the
- following chmod(2) call is needed to make sure the permissions
- are set as specified by the user.
- """
- os.chmod(entry.get('name'), mode)
- os.chown(entry.get('name'),
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- return True
+ return setPerms(entry)
except KeyError:
self.logger.error('Failed to install %s' % entry.get('name'))
except OSError:
@@ -261,47 +343,13 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifydirectory(self, entry, modlist):
"""Verify Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error("POSIX: Entry %s not completely specified. "
- "Try running bcfg2-lint." % (entry.get('name')))
- return False
- while len(entry.get('perms', '')) < 4:
- entry.set('perms', '0' + entry.get('perms', ''))
- try:
- ondisk = os.stat(entry.get('name'))
- except OSError:
- entry.set('current_exists', 'false')
- self.logger.info("POSIX: %s %s does not exist" %
- (entry.tag, entry.get('name')))
- return False
- try:
- owner = str(ondisk[stat.ST_UID])
- group = str(ondisk[stat.ST_GID])
- except (OSError, KeyError):
- self.logger.info("POSIX: User/Group resolution failed "
- "for path %s" % entry.get('name'))
- owner = 'root'
- group = '0'
- finfo = os.stat(entry.get('name'))
- perms = oct(finfo[stat.ST_MODE])[-4:]
- if entry.get('mtime', '-1') != '-1':
- mtime = str(finfo[stat.ST_MTIME])
- else:
- mtime = '-1'
- pTrue = ((owner == str(normUid(entry, logger=self.logger))) and
- (group == str(normGid(entry, logger=self.logger))) and
- (perms == entry.get('perms')) and
- (mtime == entry.get('mtime', '-1')))
-
pruneTrue = True
ex_ents = []
- if entry.get('prune', 'false') == 'true' \
- and (entry.tag == 'Path' and entry.get('type') == 'directory'):
+ if (entry.get('prune', 'false') == 'true'
+ and (entry.tag == 'Path' and entry.get('type') == 'directory')):
# check for any extra entries when prune='true' attribute is set
try:
- entries = ['/'.join([entry.get('name'), ent]) \
+ entries = ['/'.join([entry.get('name'), ent])
for ent in os.listdir(entry.get('name'))]
ex_ents = [e for e in entries if e not in modlist]
if ex_ents:
@@ -313,99 +361,48 @@ class POSIX(Bcfg2.Client.Tools.Tool):
nqtext += "Directory %s contains extra entries: " % \
entry.get('name')
nqtext += ":".join(ex_ents)
- entry.set('qtest', nqtext)
- [entry.append(XML.Element('Prune', path=x)) \
+ entry.set('qtext', nqtext)
+ [entry.append(XML.Element('Prune', path=x))
for x in ex_ents]
except OSError:
ex_ents = []
pruneTrue = True
- if not pTrue:
- if owner != str(normUid(entry, logger=self.logger)):
- entry.set('current_owner', owner)
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s owner wrong. is %s should be %s" % \
- (entry.get('name'), owner, entry.get('owner'))
- entry.set('qtext', nqtext)
- if group != str(normGid(entry, logger=self.logger)):
- entry.set('current_group', group)
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s group is %s should be %s" % \
- (entry.get('name'), group, entry.get('group'))
- entry.set('qtext', nqtext)
- if perms != entry.get('perms'):
- entry.set('current_perms', perms)
- self.logger.debug("%s %s permissions are %s should be %s" %
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s %s perms are %s should be %s" % \
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms'))
- entry.set('qtext', nqtext)
- if mtime != entry.get('mtime', '-1'):
- entry.set('current_mtime', mtime)
- self.logger.debug("%s %s mtime is %s should be %s" \
- % (entry.tag, entry.get('name'), mtime,
- entry.get('mtime')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s mtime is %s should be %s" % \
- (entry.get('name'), mtime, entry.get('mtime'))
- entry.set('qtext', nqtext)
- if entry.get('type') != 'file':
- nnqtext = entry.get('qtext')
- nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'),
- entry.get('name'))
- entry.set('qtext', nnqtext)
- return pTrue and pruneTrue
+ return pruneTrue and self._verify_metadata(entry)
def Installdirectory(self, entry):
"""Install Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing directory %s" % (entry.get('name')))
+ self.logger.info("Installing directory %s" % entry.get('name'))
try:
fmode = os.lstat(entry.get('name'))
- if not stat.S_ISDIR(fmode[stat.ST_MODE]):
- self.logger.debug("Found a non-directory entry at %s" % \
- (entry.get('name')))
- try:
- os.unlink(entry.get('name'))
- exists = False
- except OSError:
- self.logger.info("Failed to unlink %s" % \
- (entry.get('name')))
- return False
- else:
- self.logger.debug("Found a pre-existing directory at %s" % \
- (entry.get('name')))
- exists = True
except OSError:
# stat failed
exists = False
+ if not stat.S_ISDIR(fmode[stat.ST_MODE]):
+ self.logger.debug("Found a non-directory entry at %s" %
+ entry.get('name'))
+ try:
+ os.unlink(entry.get('name'))
+ exists = False
+ except OSError:
+ self.logger.info("Failed to unlink %s" % entry.get('name'))
+ return False
+ else:
+ self.logger.debug("Found a pre-existing directory at %s" %
+ entry.get('name'))
+ exists = True
+
if not exists:
parent = "/".join(entry.get('name').split('/')[:-1])
if parent:
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for directory %s' % (entry.get('name')))
+ self.logger.debug('Creating parent path for directory %s' %
+ entry.get('name'))
for idx in range(len(parent.split('/')[:-1])):
- current = '/'+'/'.join(parent.split('/')[1:2+idx])
+ current = '/' + '/'.join(parent.split('/')[1:2+idx])
try:
sloc = os.stat(current)
except OSError:
@@ -424,10 +421,10 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.mkdir(entry.get('name'))
except OSError:
- self.logger.error('Failed to create directory %s' % \
- (entry.get('name')))
+ self.logger.error('Failed to create directory %s' %
+ entry.get('name'))
return False
- if entry.get('prune', 'false') == 'true' and entry.get("qtest"):
+ if entry.get('prune', 'false') == 'true' and entry.get("qtext"):
for pent in entry.findall('Prune'):
pname = pent.get('path')
ulfailed = False
@@ -448,7 +445,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifyfile(self, entry, _):
"""Verify Path type='file' entry."""
# permissions check + content check
- permissionStatus = self.Verifydirectory(entry, _)
+ permissionStatus = self._verify_metadata(entry)
tbin = False
if entry.text == None and entry.get('empty', 'false') == 'false':
self.logger.error("Cannot verify incomplete Path type='%s' %s" %
@@ -466,7 +463,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
tempdata = tempdata.encode(self.setup['encoding'])
except UnicodeEncodeError:
e = sys.exc_info()[1]
- self.logger.error("Error encoding file %s:\n %s" % \
+ self.logger.error("Error encoding file %s:\n %s" %
(entry.get('name'), e))
different = False
@@ -535,8 +532,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
entry.set("qtext", "\n".join(prompt))
if entry.get('sensitive', 'false').lower() != 'true':
@@ -565,12 +560,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
binascii.b2a_base64("\n".join(diff)))
elif not tbin and isString(content, self.setup['encoding']):
entry.set('current_bfile', binascii.b2a_base64(content))
- elif permissionStatus == False and self.setup['interactive']:
- prompt = [entry.get('qtext', '')]
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
- entry.set("qtext", "\n".join(prompt))
-
return permissionStatus and not different
@@ -583,8 +572,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for config file %s' % \
- (entry.get('name')))
+ self.logger.debug('Creating parent path for config file %s' %
+ entry.get('name'))
current = '/'
for next in parent.split('/')[1:]:
current += next + '/'
@@ -592,23 +581,24 @@ class POSIX(Bcfg2.Client.Tools.Tool):
sloc = os.stat(current)
try:
if not stat.S_ISDIR(sloc[stat.ST_MODE]):
- self.logger.debug('%s is not a directory; recreating' \
- % (current))
+ self.logger.debug('%s is not a directory; recreating'
+ % current)
os.unlink(current)
os.mkdir(current)
except OSError:
return False
except OSError:
try:
- self.logger.debug("Creating non-existent path %s" % current)
+ self.logger.debug("Creating non-existent path %s" %
+ current)
os.mkdir(current)
except OSError:
return False
# If we get here, then the parent directory should exist
- if (entry.get("paranoid", False) in ['true', 'True']) and \
- self.setup.get("paranoid", False) and not \
- (entry.get('current_exists', 'true') == 'false'):
+ if (entry.get("paranoid", 'false').lower() == 'true' and
+ self.setup.get("paranoid", False) and
+ entry.get('current_exists', 'true') != 'false'):
bkupnam = entry.get('name').replace('/', '_')
# current list of backups for this file
try:
@@ -627,7 +617,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.remove("%s/%s" % (self.ppath, oldest))
except:
- self.logger.error("Failed to remove %s/%s" % \
+ self.logger.error("Failed to remove %s/%s" %
(self.ppath, oldest))
return False
try:
@@ -639,8 +629,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name'), self.ppath))
except IOError:
e = sys.exc_info()[1]
- self.logger.error("Failed to create backup file for %s" % \
- (entry.get('name')))
+ self.logger.error("Failed to create backup file for %s" %
+ entry.get('name'))
self.logger.error(e)
return False
try:
@@ -656,82 +646,59 @@ class POSIX(Bcfg2.Client.Tools.Tool):
filedata = entry.text
newfile.write(filedata)
newfile.close()
- try:
- os.chown(newfile.name,
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- except KeyError:
- self.logger.error("Failed to chown %s to %s:%s" %
- (newfile.name, entry.get('owner'),
- entry.get('group')))
- os.chown(newfile.name, 0, 0)
- except OSError:
- err = sys.exc_info()[1]
- self.logger.error("Could not chown %s: %s" % (newfile.name,
- err))
- os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms')))
+
+ rv = setPerms(entry, newfile.name)
os.rename(newfile.name, entry.get('name'))
- if entry.get('mtime', '-1') != '-1':
+ if entry.get('mtime'):
try:
os.utime(entry.get('name'), (int(entry.get('mtime')),
int(entry.get('mtime'))))
except:
- self.logger.error("File %s mtime fix failed" \
- % (entry.get('name')))
- return False
- return True
+ logger.error("Failed to set mtime of %s" % path)
+ rv = False
+ return rv
except (OSError, IOError):
err = sys.exc_info()[1]
if err.errno == errno.EACCES:
- self.logger.info("Failed to open %s for writing" % (entry.get('name')))
+ self.logger.info("Failed to open %s for writing" %
+ entry.get('name'))
else:
print(err)
return False
def Verifyhardlink(self, entry, _):
"""Verify HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
+ rv = True
+
try:
- if os.path.samefile(entry.get('name'), entry.get('to')):
- return True
- self.logger.debug("Hardlink %s is incorrect" % \
- entry.get('name'))
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
- return False
+ if not os.path.samefile(entry.get('name'), entry.get('to')):
+ msg = "Hardlink %s is incorrect." % entry.get('name')
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installhardlink(self, entry):
"""Install HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing Hardlink %s" % (entry.get('name')))
+ self.logger.info("Installing Hardlink %s" % entry.get('name'))
if os.path.lexists(entry.get('name')):
try:
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" % \
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
@@ -739,7 +706,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name')))
try:
os.link(entry.get('to'), entry.get('name'))
- return True
+ return setPerms(entry)
except OSError:
return False
@@ -789,135 +756,63 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifypermissions(self, entry, _):
"""Verify Path type='permissions' entry"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- if entry.get('recursive') in ['True', 'true']:
+ rv = self._verify_metadata(entry)
+
+ if entry.get('recursive', 'false').lower() == 'true':
# verify ownership information recursively
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid:
- # owner mismatch for path
- entry.set('current_owner', str(pstat.st_uid))
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Owner for path %s is incorrect. "
- "Current owner is %s but should be %s\n" % \
- (path, pstat.st_uid, entry.get('owner')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- if group != pstat.st_gid:
- # group mismatch for path
- entry.set('current_group', str(pstat.st_gid))
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Group for path %s is incorrect. "
- "Current group is %s but should be %s\n" % \
- (path, pstat.st_gid, entry.get('group')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- return self.Verifydirectory(entry, _)
-
- def _diff(self, content1, content2, difffunc, filename=None):
- rv = []
- start = time.time()
- longtime = False
- for diffline in difffunc(content1.split('\n'),
- content2.split('\n')):
- now = time.time()
- rv.append(diffline)
- if now - start > 5 and not longtime:
- if filename:
- self.logger.info("Diff of %s taking a long time" %
- filename)
- else:
- self.logger.info("Diff taking a long time")
- longtime = True
- elif now - start > 30:
- if filename:
- self.logger.error("Diff of %s took too long; giving up" %
- filename)
- else:
- self.logger.error("Diff took too long; giving up")
- return False
+ rv &= self._verify_metadata(entry,
+ path=os.path.join(root, p))
return rv
def Installpermissions(self, entry):
"""Install POSIX permissions"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
plist = [entry.get('name')]
if entry.get('recursive') in ['True', 'true']:
# verify ownership information recursively
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid or group != pstat.st_gid:
- # owner mismatch for path
+ if not self._verify_metadata(entry,
+ path=os.path.join(root, p),
+ checkonly=True):
plist.append(path)
- try:
- for p in plist:
- os.chown(p,
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms')))
- return True
- except (OSError, KeyError):
- self.logger.error('Permission fixup failed for %s' % \
- (entry.get('name')))
- return False
+ rv = True
+ for path in plist:
+ rv &= setPerms(entry, path)
+ return rv
def Verifysymlink(self, entry, _):
"""Verify Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
+ 'Try running bcfg2-lint.' %
(entry.get('name')))
return False
+
+ rv = True
+
try:
sloc = os.readlink(entry.get('name'))
- if sloc == entry.get('to'):
- return True
- self.logger.debug("Symlink %s points to %s, should be %s" % \
- (entry.get('name'), sloc, entry.get('to')))
- entry.set('current_to', sloc)
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
- return False
+ if sloc != entry.get('to'):
+ entry.set('current_to', sloc)
+ msg = ("Symlink %s points to %s, should be %s" %
+ (entry.get('name'), sloc, entry.get('to')))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installsymlink(self, entry):
"""Install Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
+ 'Try running bcfg2-lint.' % entry.get('name'))
return False
self.logger.info("Installing symlink %s" % (entry.get('name')))
if os.path.lexists(entry.get('name')):
@@ -925,23 +820,22 @@ class POSIX(Bcfg2.Client.Tools.Tool):
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % \
- (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" %\
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
- self.logger.info("Symlink %s cleanup failed" %\
+ self.logger.info("Symlink %s cleanup failed" %
(entry.get('name')))
try:
os.symlink(entry.get('to'), entry.get('name'))
- return True
+ return setSEContext(entry)
except OSError:
return False
@@ -952,5 +846,139 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def VerifyPath(self, entry, _):
"""Dispatch verify to the proper method according to type"""
- ret = getattr(self, 'Verify%s' % entry.get('type'))
- return ret(entry, _)
+ ret = getattr(self, 'Verify%s' % entry.get('type'))(entry, _)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'), entry.get('name')))
+ return ret
+
+ def _verify_metadata(self, entry, path=None, checkonly=False):
+ """ generic method to verify perms, owner, group, secontext,
+ and mtime """
+
+ # allow setting an alternate path for recursive permissions checking
+ if path is None:
+ path = entry.get('name')
+
+ while len(entry.get('perms', '')) < 4:
+ entry.set('perms', '0' + entry.get('perms', ''))
+
+ try:
+ ondisk = os.stat(path)
+ except OSError:
+ entry.set('current_exists', 'false')
+ self.logger.debug("POSIX: %s %s does not exist" %
+ (entry.tag, path))
+ return False
+
+ try:
+ owner = str(ondisk[stat.ST_UID])
+ group = str(ondisk[stat.ST_GID])
+ except (OSError, KeyError):
+ self.logger.error('POSIX: User/Group resolution failed for path %s'
+ % path)
+ owner = 'root'
+ group = '0'
+
+ perms = oct(ondisk[stat.ST_MODE])[-4:]
+ if entry.get('mtime', '-1') != '-1':
+ mtime = str(ondisk[stat.ST_MTIME])
+ else:
+ mtime = '-1'
+
+ configOwner = str(normUid(entry))
+ configGroup = str(normGid(entry))
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ if has_selinux:
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+
+ errors = []
+ if owner != configOwner:
+ if checkonly:
+ return False
+ entry.set('current_owner', owner)
+ errors.append("POSIX: Owner for path %s is incorrect. "
+ "Current owner is %s but should be %s" %
+ (path, ondisk.st_uid, entry.get('owner')))
+
+ if group != configGroup:
+ if checkonly:
+ return False
+ entry.set('current_group', group)
+ errors.append("POSIX: Group for path %s is incorrect. "
+ "Current group is %s but should be %s" %
+ (path, ondisk.st_gid, entry.get('group')))
+
+ if oct(int(perms, 8)) != oct(configPerms):
+ if checkonly:
+ return False
+ entry.set('current_perms', perms)
+ errors.append("POSIX: Permissions for path %s are incorrect. "
+ "Current permissions are %s but should be %s" %
+ (path, perms, entry.get('perms')))
+
+ if entry.get('mtime') and mtime != entry.get('mtime', '-1'):
+ if checkonly:
+ return False
+ entry.set('current_mtime', mtime)
+ errors.append("POSIX: mtime for path %s is incorrect. "
+ "Current mtime is %s but should be %s" %
+ (path, mtime, entry.get('mtime')))
+
+ seVerifies = self._verify_secontext(entry)
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return seVerifies
+
+ def _verify_secontext(self, entry):
+ if not secontextMatches(entry):
+ path = entry.get("name")
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+ pcontext = selinux.getfilecon(path)[1]
+ entry.set('current_secontext', pcontext)
+ msg = ("SELinux context for path %s is incorrect. "
+ "Current context is %s but should be %s" %
+ (path, pcontext, configContext))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get("qtext", ''), msg]))
+ return False
+ return True
+
+ def _diff(self, content1, content2, difffunc, filename=None):
+ rv = []
+ start = time.time()
+ longtime = False
+ for diffline in difffunc(content1.split('\n'),
+ content2.split('\n')):
+ now = time.time()
+ rv.append(diffline)
+ if now - start > 5 and not longtime:
+ if filename:
+ self.logger.info("Diff of %s taking a long time" %
+ filename)
+ else:
+ self.logger.info("Diff taking a long time")
+ longtime = True
+ elif now - start > 30:
+ if filename:
+ self.logger.error("Diff of %s took too long; giving up" %
+ filename)
+ else:
+ self.logger.error("Diff took too long; giving up")
+ return False
+ return rv
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
new file mode 100644
index 000000000..1c0db904b
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -0,0 +1,716 @@
+import os
+import re
+import sys
+import copy
+import glob
+import struct
+import socket
+import selinux
+import seobject
+import Bcfg2.Client.XML
+import Bcfg2.Client.Tools
+import Bcfg2.Client.Tools.POSIX
+
+def pack128(int_val):
+ """ pack a 128-bit integer in big-endian format """
+ max_int = 2 ** (128) - 1
+ max_word_size = 2 ** 32 - 1
+
+ if int_val <= max_word_size:
+ return struct.pack('>L', int_val)
+
+ words = []
+ for i in range(4):
+ word = int_val & max_word_size
+ words.append(int(word))
+ int_val >>= 32
+ words.reverse()
+ return struct.pack('>4I', *words)
+
+def netmask_itoa(netmask, proto="ipv4"):
+ """ convert an integer netmask (e.g., /16) to dotted-quad
+ notation (255.255.0.0) or IPv6 prefix notation (ffff::) """
+ if proto == "ipv4":
+ size = 32
+ family = socket.AF_INET
+ else: # ipv6
+ size = 128
+ family = socket.AF_INET6
+ try:
+ int(netmask)
+ except ValueError:
+ return netmask
+
+ if netmask > size:
+ raise ValueError("Netmask too large: %s" % netmask)
+
+ res = 0L
+ for n in range(netmask):
+ res |= 1 << (size - n - 1)
+ netmask = socket.inet_ntop(family, pack128(res))
+ return netmask
+
+
+class SELinux(Bcfg2.Client.Tools.Tool):
+ """ SELinux boolean and module 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']))
+
+ 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]
+ self.handlers[etype] = \
+ globals()["SELinux%sHandler" % etype.title()](self, logger,
+ setup, config)
+
+ def BundleUpdated(self, _, states):
+ for handler in self.handlers.values():
+ handler.BundleUpdated(states)
+
+ def FindExtra(self):
+ extra = []
+ for handler in self.handlers.values():
+ extra.extend(handler.FindExtra())
+ return extra
+
+ def canInstall(self, entry):
+ return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and
+ self.handlers[entry.get('type')].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)
+
+ def Install(self, entries, states):
+ # start a transaction
+ sr = seobject.semanageRecords("")
+ if hasattr(sr, "start"):
+ self.logger.debug("Starting SELinux transaction")
+ sr.start()
+ else:
+ self.logger.debug("SELinux transactions not supported; this may "
+ "slow things down considerably")
+ Bcfg2.Client.Tools.Tool.Install(self, entries, states)
+ if hasattr(sr, "finish"):
+ self.logger.debug("Committing SELinux transaction")
+ sr.finish()
+
+ def InstallSELinux(self, entry):
+ """Dispatch install to the proper method according to type"""
+ return self.handlers[entry.get('type')].Install(entry)
+
+ def VerifySELinux(self, entry, _):
+ """Dispatch verify to the proper method according to type"""
+ rv = self.handlers[entry.get('type')].Verify(entry)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall SELinux %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'),
+ self.handlers[entry.get('type')].tostring(entry)))
+ return rv
+
+ def Remove(self, entries):
+ """Dispatch verify to the proper removal method according to type"""
+ # sort by type
+ types = list()
+ for entry in entries:
+ if entry.get('type') not in types:
+ types.append(entry.get('type'))
+
+ for etype in types:
+ self.handlers[entry.get('type')].Remove([e for e in entries
+ if e.get('type') == etype])
+
+
+class SELinuxEntryHandler(object):
+ etype = None
+ key_format = ("name",)
+ value_format = ()
+ str_format = '%(name)s'
+ custom_re = re.compile(' (?P<name>\S+)$')
+ custom_format = None
+
+ def __init__(self, tool, logger, setup, config):
+ self.tool = tool
+ self.logger = logger
+ self._records = None
+ self._all = None
+ if not self.custom_format:
+ self.custom_format = self.key_format
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = getattr(seobject, "%sRecords" % self.etype)("")
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ self._all = self.records.get_all()
+ return self._all
+
+ @property
+ def custom_records(self):
+ if hasattr(self.records, "customized") and self.custom_re:
+ return dict([(k, self.all_records[k]) for k in self.custom_keys])
+ else:
+ # ValueError is really a pretty dumb exception to raise,
+ # but that's what the seobject customized() method raises
+ # if it's defined but not implemented. yeah, i know, wtf.
+ raise ValueError("custom_records")
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if (len(self.custom_format) == 1 and
+ self.custom_format[0] == "name"):
+ keys.append(match.group("name"))
+ else:
+ keys.append(tuple([match.group(k)
+ for k in self.custom_format]))
+ return keys
+
+ def tostring(self, entry):
+ return self.str_format % entry.attrib
+
+ def keytostring(self, key):
+ return self.str_format % self._key2attrs(key)
+
+ def _key(self, entry):
+ if len(self.key_format) == 1 and self.key_format[0] == "name":
+ return entry.get("name")
+ else:
+ rv = []
+ for key in self.key_format:
+ rv.append(entry.get(key))
+ return tuple(rv)
+
+ def _key2attrs(self, key):
+ if isinstance(key, tuple):
+ rv = dict((self.key_format[i], key[i])
+ for i in range(len(self.key_format))
+ if self.key_format[i])
+ else:
+ rv = dict(name=key)
+ if self.value_format:
+ vals = self.all_records[key]
+ rv.update(dict((self.value_format[i], vals[i])
+ for i in range(len(self.value_format))
+ if self.value_format[i]))
+ return rv
+
+ def key2entry(self, key):
+ attrs = self._key2attrs(key)
+ attrs["type"] = self.etype
+ return Bcfg2.Client.XML.Element("SELinux", **attrs)
+
+ def _args(self, entry, method):
+ if hasattr(self, "_%sargs" % method):
+ return getattr(self, "_%sargs" % method)(entry)
+ elif hasattr(self, "_defaultargs"):
+ # default args
+ return self._defaultargs(entry)
+ else:
+ raise NotImplementedError
+
+ def _deleteargs(self, entry):
+ return (self._key(entry))
+
+ def canInstall(self, entry):
+ return bool(self._key(entry))
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name")])
+
+ def exists(self, entry):
+ if self._key(entry) not in self.all_records:
+ self.logger.debug("SELinux %s %s does not exist" %
+ (self.etype, self.tostring(entry)))
+ return False
+ return True
+
+ def Verify(self, entry):
+ if not self.exists(entry):
+ entry.set('current_exists', 'false')
+ return False
+
+ errors = []
+ current_attrs = self._key2attrs(self._key(entry))
+ desired_attrs = entry.attrib
+ for attr in self.value_format:
+ if not attr:
+ 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,
+ current_attrs[attr], desired_attrs[attr]))
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return True
+
+ def Install(self, entry, method=None):
+ if not method:
+ if self.exists(entry):
+ method = "modify"
+ else:
+ method = "add"
+ self.logger.debug("%s SELinux %s %s" %
+ (method.title(), self.etype, self.tostring(entry)))
+
+ try:
+ getattr(self.records, method)(*self._args(entry, method))
+ self._all = None
+ 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))
+ return False
+
+ def Remove(self, entries):
+ for entry in entries:
+ try:
+ self.records.delete(*self._args(entry, "delete"))
+ self._all = None
+ except ValueError:
+ err = sys.exc_info()[1]
+ self.logger.info("Failed to remove SELinux %s %s: %s" %
+ (self.etype, self.tostring(entry), err))
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ try:
+ records = self.custom_records
+ except ValueError:
+ records = self.all_records
+ return [self.key2entry(key)
+ for key in records.keys()
+ if key not in specified]
+
+ def BundleUpdated(self, states):
+ pass
+
+
+class SELinuxBooleanHandler(SELinuxEntryHandler):
+ etype = "boolean"
+ value_format = ("value",)
+
+ @property
+ def all_records(self):
+ # older versions of selinux return a single 0/1 value for each
+ # bool, while newer versions return a list of three 0/1 values
+ # representing various states. we don't care about the latter
+ # two values, but it's easier to coerce the older format into
+ # the newer format as far as interoperation with the rest of
+ # SELinuxEntryHandler goes
+ rv = SELinuxEntryHandler.all_records.fget(self)
+ if rv.values()[0] in [0, 1]:
+ for key, val in rv.items():
+ rv[key] = [val, val, val]
+ return rv
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][0]
+ if status:
+ rv['value'] = "on"
+ else:
+ rv['value'] = "off"
+ return rv
+
+ def _defaultargs(self, entry):
+ # the only values recognized by both new and old versions of
+ # selinux are the strings "0" and "1". old selinux accepts
+ # ints or bools as well, new selinux accepts "on"/"off"
+ if entry.get("value").lower() == "on":
+ value = "1"
+ else:
+ value = "0"
+ return (entry.get("name"), value)
+
+ def canInstall(self, entry):
+ if entry.get("value").lower() not in ["on", "off"]:
+ self.logger.debug("SELinux %s %s has a bad value: %s" %
+ (self.etype, self.tostring(entry),
+ entry.get("value")))
+ return False
+ return (self.exists(entry) and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+
+class SELinuxPortHandler(SELinuxEntryHandler):
+ etype = "port"
+ value_format = ('selinuxtype', None)
+ custom_re = re.compile(r'-p (?P<proto>tcp|udp).*? (?P<start>\d+)(?:-(?P<end>\d+))?$')
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if match.group('end'):
+ keys.append((int(match.group('start')),
+ int(match.group('end')),
+ match.group('proto')))
+ else:
+ keys.append((int(match.group('start')),
+ int(match.group('start')),
+ match.group('proto')))
+ return keys
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # older versions of selinux use (startport, endport) as
+ # they key for the ports.get_all() dict, and (type, proto,
+ # level) as the value; this is obviously broken, so newer
+ # versions use (startport, endport, proto) as the key, and
+ # (type, level) as the value. abstracting around this
+ # sucks.
+ ports = self.records.get_all()
+ if len(ports.keys()[0]) == 3:
+ self._all = ports
+ else:
+ # uglist list comprehension ever?
+ self._all = dict([((k[0], k[1], v[1]), (v[0], v[2]))
+ for k, v in ports.items()])
+ return self._all
+
+ def _key(self, entry):
+ try:
+ (port, proto) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no protocol specified" %
+ entry.get("name"))
+ return
+ if "-" in port:
+ start, end = port.split("-")
+ else:
+ start = port
+ end = port
+ return (int(start), int(end), proto)
+
+ def _key2attrs(self, key):
+ if key[0] == key[1]:
+ port = str(key[0])
+ else:
+ port = "%s-%s" % (key[0], key[1])
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (port, key[2]), selinuxtype=vals[0])
+
+ def _defaultargs(self, entry):
+ (port, proto) = entry.get("name").split("/")
+ return (port, proto, '', entry.get("selinuxtype"))
+
+ def _deleteargs(self, entry):
+ return tuple(entry.get("name").split("/"))
+
+
+class SELinuxFcontextHandler(SELinuxEntryHandler):
+ etype = "fcontext"
+ key_format = ("name", "filetype")
+ value_format = (None, None, "selinuxtype", None)
+ filetypeargs = dict(all="",
+ regular="--",
+ directory="-d",
+ symlink="-l",
+ pipe="-p",
+ socket="-s",
+ block="-b",
+ char="-c",
+ door="-D")
+ filetypenames = dict(all="all files",
+ regular="regular file",
+ directory="directory",
+ symlink="symbolic link",
+ pipe="named pipe",
+ socket="socket",
+ block="block device",
+ char="character device",
+ door="door")
+ filetypeattrs = dict([v, k] for k, v in filetypenames.iteritems())
+ custom_re = re.compile(r'-f \'(?P<filetype>[a-z ]+)\'.*? \'(?P<name>.*)\'')
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # on older selinux, fcontextRecords.get_all() returns a
+ # list of tuples of (filespec, filetype, seuser, serole,
+ # setype, level); on newer selinux, get_all() returns a
+ # dict of (filespec, filetype) => (seuser, serole, setype,
+ # level).
+ fcontexts = self.records.get_all()
+ if isinstance(fcontexts, dict):
+ self._all = fcontexts
+ else:
+ self._all = dict([(f[0:2], f[2:]) for f in fcontexts])
+ return self._all
+
+ def _key(self, entry):
+ ftype = entry.get("filetype", "all")
+ return (entry.get("name"),
+ self.filetypenames.get(ftype, ftype))
+
+ def _key2attrs(self, key):
+ rv = dict(name=key[0], filetype=self.filetypeattrs[key[1]])
+ vals = self.all_records[key]
+ # in older versions of selinux, an fcontext with no selinux
+ # type is the single value None; in newer versions, it's a
+ # tuple whose 0th (and only) value is None.
+ if vals and vals[0]:
+ rv["selinuxtype"] = vals[2]
+ else:
+ rv["selinuxtype"] = "<<none>>"
+ return rv
+
+ def canInstall(self, entry):
+ return (entry.get("filetype", "all") in self.filetypeargs and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxtype"),
+ self.filetypeargs[entry.get("filetype", "all")],
+ '', '')
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name"),
+ entry.get("filetype", "all")])
+
+
+class SELinuxNodeHandler(SELinuxEntryHandler):
+ etype = "node"
+ value_format = (None, None, "selinuxtype", None)
+ str_format = '%(name)s (%(proto)s)'
+ custom_re = re.compile(r'-M (?P<netmask>\S+).*?-p (?P<proto>ipv\d).*? (?P<addr>\S+)$')
+ custom_format = ('addr', 'netmask', 'proto')
+
+ def _key(self, entry):
+ try:
+ (addr, netmask) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no netmask specified" %
+ entry.get("name"))
+ return
+ netmask = netmask_itoa(netmask, proto=entry.get("proto"))
+ return (addr, netmask, entry.get("proto"))
+
+ def _key2attrs(self, key):
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (key[0], key[1]), proto=key[2],
+ selinuxtype=vals[2])
+
+ def _defaultargs(self, entry):
+ (addr, netmask) = entry.get("name").split("/")
+ return (addr, netmask, entry.get("proto"), "", entry.get("selinuxtype"))
+
+
+class SELinuxLoginHandler(SELinuxEntryHandler):
+ etype = "login"
+ value_format = ("selinuxuser", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxuser"), "")
+
+
+class SELinuxUserHandler(SELinuxEntryHandler):
+ etype = "user"
+ value_format = ("prefix", None, None, "roles")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.needs_prefix = False
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = seobject.seluserRecords()
+ return self._records
+
+ def Install(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. however, in newer versions, prefix is _required_,
+ # so we a) try without a prefix; b) catch TypeError, which
+ # indicates that we had the wrong number of args (ValueError
+ # is thrown by the bug in older versions of selinux); and c)
+ # try with prefix.
+ try:
+ SELinuxEntryHandler.Install(self, entry)
+ except TypeError:
+ self.needs_prefix = True
+ SELinuxEntryHandler.Install(self, entry)
+
+ def _defaultargs(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. see the comment in Install() above for more
+ # details.
+ rv = [entry.get("name"),
+ entry.get("roles", "").replace(" ", ",").split(",")]
+ if self.needs_prefix:
+ rv.extend(['', '', entry.get("prefix")])
+ else:
+ key = self._key(entry)
+ if key in self.all_records:
+ attrs = self._key2attrs(key)
+ if attrs['prefix'] != entry.get("prefix"):
+ rv.extend(['', '', entry.get("prefix")])
+ return tuple(rv)
+
+
+class SELinuxInterfaceHandler(SELinuxEntryHandler):
+ etype = "interface"
+ value_format = (None, None, "selinuxtype", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), '', entry.get("selinuxtype"))
+
+
+class SELinuxPermissiveHandler(SELinuxEntryHandler):
+ etype = "permissive"
+
+ @property
+ def records(self):
+ try:
+ return SELinuxEntryHandler.records.fget(self)
+ except AttributeError:
+ self.logger.info("Permissive domains not supported by this version "
+ "of SELinux")
+ self._records = False
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ if self.records == False:
+ self._all = dict()
+ else:
+ # permissiveRecords.get_all() returns a list, so we just
+ # make it into a dict so that the rest of
+ # SELinuxEntryHandler works
+ self._all = dict([(d, d) for d in self.records.get_all()])
+ return self._all
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+
+class SELinuxModuleHandler(SELinuxEntryHandler):
+ etype = "module"
+ value_format = (None, "disabled")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.posixtool = Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config)
+ try:
+ self.setype = selinux.selinux_getpolicytype()[1]
+ except IndexError:
+ self.logger.error("Unable to determine SELinux policy type")
+ self.setype = None
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # we get a list of tuples back; coerce it into a dict
+ self._all = dict([(m[0], (m[1], m[2]))
+ for m in self.records.get_all()])
+ return self._all
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][1]
+ if status:
+ rv['disabled'] = "false"
+ else:
+ rv['disabled'] = "true"
+ return rv
+
+ def _filepath(self, entry):
+ return os.path.join("/usr/share/selinux", self.setype,
+ "%s.pp" % entry.get("name"))
+
+ def _pathentry(self, entry):
+ pathentry = copy.deepcopy(entry)
+ pathentry.set("name", self._filepath(pathentry))
+ pathentry.set("perms", "0644")
+ pathentry.set("owner", "root")
+ pathentry.set("group", "root")
+ pathentry.set("secontext", "__default__")
+ return pathentry
+
+ def Verify(self, entry):
+ if not entry.get("disabled"):
+ entry.set("disabled", "false")
+ return (SELinuxEntryHandler.Verify(self, entry) and
+ self.posixtool.Verifyfile(self._pathentry(entry), None))
+
+ def canInstall(self, entry):
+ return (entry.text and self.setype and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def Install(self, entry):
+ rv = self.posixtool.Installfile(self._pathentry(entry))
+ try:
+ rv = rv and SELinuxEntryHandler.Install(self, entry)
+ except NameError:
+ # some versions of selinux have a bug in seobject that
+ # makes modify() calls fail. add() seems to have the same
+ # effect as modify, but without the bug
+ if self.exists(entry):
+ rv = rv and SELinuxEntryHandler.Install(self, entry,
+ method="add")
+
+ if entry.get("disabled", "false").lower() == "true":
+ method = "disable"
+ else:
+ method = "enable"
+ return rv and SELinuxEntryHandler.Install(self, entry, method=method)
+
+ def _addargs(self, entry):
+ return (self._filepath(entry),)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ return [self.key2entry(os.path.basename(f)[:-3])
+ for f in glob.glob(os.path.join("/usr/share/selinux",
+ self.setype, "*.pp"))
+ if f not in specified]
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index d423b6380..e4a0ec220 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -137,6 +137,18 @@ class Tool:
"""Default implementation of the information gathering routines."""
pass
+ def missing_attrs(self, entry):
+ required = self.__req__[entry.tag]
+ if isinstance(required, dict):
+ required = ["type"]
+ try:
+ required.extend(self.__req__[entry.tag][entry.get("type")])
+ except KeyError:
+ pass
+
+ return [attr for attr in required
+ if attr not in entry.attrib or not entry.attrib[attr]]
+
def canVerify(self, entry):
"""Test if entry has enough information to be verified."""
if not self.handlesEntry(entry):
@@ -149,8 +161,7 @@ class Tool:
entry.get('failure')))
return False
- missing = [attr for attr in self.__req__[entry.tag] \
- if attr not in entry.attrib]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot verify" \
% (entry.tag, entry.get('name')))
@@ -168,6 +179,11 @@ class Tool:
"""Return a list of extra entries."""
return []
+ def primarykey(self, entry):
+ """ return a string that should be unique amongst all entries
+ in the specification """
+ return "%s:%s" % (entry.tag, entry.get("name"))
+
def canInstall(self, entry):
"""Test if entry has enough information to be installed."""
if not self.handlesEntry(entry):
@@ -178,8 +194,7 @@ class Tool:
(entry.tag, entry.get('name')))
return False
- missing = [attr for attr in self.__ireq__[entry.tag] \
- if attr not in entry.attrib or not entry.attrib[attr]]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot install" \
% (entry.tag, entry.get('name')))
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index bbbbec343..803b2755d 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -330,6 +330,11 @@ MDATA_PERMS = \
default='644',
odesc='octal permissions',
cf=('mdata', 'perms'))
+MDATA_SECONTEXT = \
+ Option('Default SELinux context',
+ default='__default__',
+ odesc='SELinux context',
+ cf=('mdata', 'secontext'))
MDATA_PARANOID = \
Option('Default Path paranoid setting',
default='true',
@@ -836,6 +841,41 @@ DRIVER_OPTIONS = \
yumng_verify_fail_action=CLIENT_YUMNG_VERIFY_FAIL_ACTION,
yumng_verify_flags=CLIENT_YUMNG_VERIFY_FLAGS)
+CLIENT_COMMON_OPTIONS = \
+ dict(extra=CLIENT_EXTRA_DISPLAY,
+ quick=CLIENT_QUICK,
+ lockfile=LOCKFILE,
+ drivers=CLIENT_DRIVERS,
+ dryrun=CLIENT_DRYRUN,
+ paranoid=CLIENT_PARANOID,
+ bundle=CLIENT_BUNDLE,
+ skipbundle=CLIENT_SKIPBUNDLE,
+ bundle_quick=CLIENT_BUNDLEQUICK,
+ indep=CLIENT_INDEP,
+ skipindep=CLIENT_SKIPINDEP,
+ file=CLIENT_FILE,
+ interactive=INTERACTIVE,
+ cache=CLIENT_CACHE,
+ profile=CLIENT_PROFILE,
+ remove=CLIENT_REMOVE,
+ server=SERVER_LOCATION,
+ user=CLIENT_USER,
+ password=SERVER_PASSWORD,
+ retries=CLIENT_RETRIES,
+ kevlar=CLIENT_KEVLAR,
+ omit_lock_check=OMIT_LOCK_CHECK,
+ decision=CLIENT_DLIST,
+ servicemode=CLIENT_SERVICE_MODE,
+ key=CLIENT_KEY,
+ certificate=CLIENT_CERT,
+ ca=CLIENT_CA,
+ serverCN=CLIENT_SCNS,
+ timeout=CLIENT_TIMEOUT,
+ decision_list=CLIENT_DECISION_LIST)
+CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS)
+CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS)
+
+
class OptionParser(OptionSet):
"""
OptionParser bootstraps option parsing,
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
index 050dd69f8..78b30120a 100644
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ b/src/lib/Bcfg2/Server/Admin/Compare.py
@@ -18,7 +18,8 @@ class Compare(Bcfg2.Server.Admin.Mode):
'important', 'paranoid', 'sensitive',
'dev_type', 'major', 'minor', 'prune',
'encoding', 'empty', 'to', 'recursive',
- 'vcstype', 'sourceurl', 'revision'],
+ 'vcstype', 'sourceurl', 'revision',
+ 'secontext'],
'Package': ['name', 'type', 'version', 'simplefile',
'verify'],
'Service': ['name', 'type', 'status', 'mode',
diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
index 6f76cf2db..0a369c841 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,50 +1,114 @@
-import os.path
+import os
+import re
import lxml.etree
import Bcfg2.Server.Lint
+import Bcfg2.Client.Tools.POSIX
+import Bcfg2.Client.Tools.VCS
from Bcfg2.Server.Plugins.Packages import Apt, Yum
+# format verifying functions
+def is_filename(val):
+ return val.startswith("/") and len(val) > 1
+
+def is_selinux_type(val):
+ return re.match(r'^[a-z_]+_t', val)
+
+def is_selinux_user(val):
+ return re.match(r'^[a-z_]+_u', val)
+
+def is_octal_mode(val):
+ return re.match(r'[0-7]{3,4}', val)
+
+def is_username(val):
+ return re.match(r'^([a-z]\w{0,30}|\d+)$', val)
+
+def is_device_mode(val):
+ try:
+ # checking upper bound seems like a good way to discover some
+ # obscure OS with >8-bit device numbers
+ return int(val) > 0
+ except:
+ return False
+
class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" verify attributes for configuration entries (as defined in
doc/server/configurationentries) """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
- self.required_attrs = {
- 'Path': {
- 'device': ['name', 'owner', 'group', 'dev_type'],
- 'directory': ['name', 'owner', 'group', 'perms'],
- 'file': ['name', 'owner', 'group', 'perms', '__text__'],
- 'hardlink': ['name', 'to'],
- 'symlink': ['name', 'to'],
- 'ignore': ['name'],
- 'nonexistent': ['name'],
- 'permissions': ['name', 'owner', 'group', 'perms'],
- 'vcs': ['vcstype', 'revision', 'sourceurl']},
- 'Service': {
- 'chkconfig': ['name'],
- 'deb': ['name'],
- 'rc-update': ['name'],
- 'smf': ['name', 'FMRI'],
- 'upstart': ['name']},
- 'Action': ['name', 'timing', 'when', 'status', 'command'],
- 'Package': ['name']}
+ self.required_attrs = dict(
+ Path=dict(
+ device=dict(name=is_filename, owner=is_username,
+ group=is_username,
+ dev_type=lambda v: \
+ v in Bcfg2.Client.Tools.POSIX.device_map),
+ directory=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ file=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode,
+ __text__=None),
+ hardlink=dict(name=is_filename, to=is_filename),
+ symlink=dict(name=is_filename, to=is_filename),
+ ignore=dict(name=is_filename),
+ nonexistent=dict(name=is_filename),
+ permissions=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ vcs=dict(vcstype=lambda v: (v != 'Path' and
+ hasattr(Bcfg2.Client.Tools.VCS,
+ "Install%s" % v)),
+ revision=None, sourceurl=None)),
+ Service={
+ "chkconfig": dict(name=None),
+ "deb": dict(name=None),
+ "rc-update": dict(name=None),
+ "smf": dict(name=None, FMRI=None),
+ "upstart": dict(name=None)},
+ Action={None: dict(name=None,
+ timing=lambda v: v in ['pre', 'post', 'both'],
+ when=lambda v: v in ['modified', 'always'],
+ status=lambda v: v in ['ignore', 'check'],
+ command=None)},
+ Package={None: dict(name=None)},
+ SELinux=dict(
+ boolean=dict(name=None,
+ value=lambda v: v in ['on', 'off']),
+ module=dict(name=None, __text__=None),
+ port=dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)', v),
+ selinuxtype=is_selinux_type),
+ fcontext=dict(name=None, selinuxtype=is_selinux_type),
+ node=dict(name=lambda v: "/" in v,
+ selinuxtype=is_selinux_type,
+ proto=lambda v: v in ['ipv6', 'ipv4']),
+ login=dict(name=is_username,
+ selinuxuser=is_selinux_user),
+ user=dict(name=is_selinux_user,
+ roles=lambda v: all(is_selinux_user(u)
+ for u in " ".split(v)),
+ prefix=None),
+ interface=dict(name=None, selinuxtype=is_selinux_type),
+ permissive=dict(name=is_selinux_type))
+ )
def Run(self):
+ print "checking packages\n"
self.check_packages()
if "Defaults" in self.core.plugins:
self.logger.info("Defaults plugin enabled; skipping required "
"attribute checks")
else:
+ print "checking rules\n"
self.check_rules()
+ print "checking bundles\n"
self.check_bundles()
+ print 'done running RequiredAttrs'
@classmethod
def Errors(cls):
return {"unknown-entry-type":"error",
"unknown-entry-tag":"error",
"required-attrs-missing":"error",
+ "required-attr-format":"error",
"extra-attrs":"warning"}
-
def check_packages(self):
""" check package sources for Source entries with missing attrs """
if 'Packages' in self.core.plugins:
@@ -85,6 +149,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" check bundles for BoundPath entries with missing attrs """
if 'Bundler' in self.core.plugins:
for bundle in self.core.plugins['Bundler'].entries.values():
+ print "checking bundle %s" % bundle.name
try:
xdata = lxml.etree.XML(bundle.data)
except (lxml.etree.XMLSyntaxError, AttributeError):
@@ -103,43 +168,52 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
if tag not in self.required_attrs:
self.LintError("unknown-entry-tag",
"Unknown entry tag '%s': %s" %
- (entry.tag, self.RenderXML(entry)))
+ (tag, self.RenderXML(entry)))
if isinstance(self.required_attrs[tag], dict):
etype = entry.get('type')
if etype in self.required_attrs[tag]:
- required_attrs = set(self.required_attrs[tag][etype] +
- ['type'])
+ required_attrs = self.required_attrs[tag][etype]
else:
self.LintError("unknown-entry-type",
"Unknown %s type %s: %s" %
(tag, etype, self.RenderXML(entry)))
return
else:
- required_attrs = set(self.required_attrs[tag])
+ required_attrs = self.required_attrs[tag]
attrs = set(entry.attrib.keys())
if 'dev_type' in required_attrs:
dev_type = entry.get('dev_type')
if dev_type in ['block', 'char']:
# check if major/minor are specified
- required_attrs |= set(['major', 'minor'])
+ required_attrs['major'] = is_device_mode
+ required_attrs['minor'] = is_device_mode
if '__text__' in required_attrs:
- required_attrs.remove('__text__')
+ del required_attrs['__text__']
if (not entry.text and
not entry.get('empty', 'false').lower() == 'true'):
self.LintError("required-attrs-missing",
"Text missing for %s %s in %s: %s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
self.RenderXML(entry)))
- if not attrs.issuperset(required_attrs):
+ if not attrs.issuperset(required_attrs.keys()):
self.LintError("required-attrs-missing",
"The following required attribute(s) are "
"missing for %s %s in %s: %s\n%s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
", ".join([attr
for attr in
required_attrs.difference(attrs)]),
self.RenderXML(entry)))
+
+ for attr, fmt in required_attrs.items():
+ if fmt and attr in attrs and not fmt(entry.attrib[attr]):
+ self.LintError("required-attr-format",
+ "The %s attribute of %s %s in %s is "
+ "malformed\n%s" %
+ (attr, tag, name, filename,
+ self.RenderXML(entry)))
+
diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py
index 98e6e6f51..d035b83d4 100644
--- a/src/lib/Bcfg2/Server/Plugin.py
+++ b/src/lib/Bcfg2/Server/Plugin.py
@@ -32,8 +32,9 @@ encoding = encparse['encoding']
# grab default metadata info from bcfg2.conf
opts = {'owner': Bcfg2.Options.MDATA_OWNER,
'group': Bcfg2.Options.MDATA_GROUP,
- 'important': Bcfg2.Options.MDATA_IMPORTANT,
'perms': Bcfg2.Options.MDATA_PERMS,
+ 'secontext': Bcfg2.Options.MDATA_SECONTEXT,
+ 'important': Bcfg2.Options.MDATA_IMPORTANT,
'paranoid': Bcfg2.Options.MDATA_PARANOID,
'sensitive': Bcfg2.Options.MDATA_SENSITIVE}
mdata_setup = Bcfg2.Options.OptionParser(opts)
@@ -52,6 +53,7 @@ info_regex = re.compile( \
'owner:(\s)*(?P<owner>\S+)|' +
'paranoid:(\s)*(?P<paranoid>\S+)|' +
'perms:(\s)*(?P<perms>\w+)|' +
+ 'secontext:(\s)*(?P<secontext>\S+)|' +
'sensitive:(\s)*(?P<sensitive>\S+)|')
def bind_info(entry, metadata, infoxml=None, default=default_file_metadata):
@@ -1162,13 +1164,14 @@ class GroupSpool(Plugin, Generator):
filename_pattern = ""
es_child_cls = object
es_cls = EntrySet
+ entry_type = 'Path'
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
if self.data[-1] == '/':
self.data = self.data[:-1]
- self.Entries['Path'] = {}
+ self.Entries[self.entry_type] = {}
self.entries = {}
self.handles = {}
self.AddDirectoryMonitor('')
@@ -1185,7 +1188,8 @@ class GroupSpool(Plugin, Generator):
dirpath,
self.es_child_cls,
self.encoding)
- self.Entries['Path'][ident] = self.entries[ident].bind_entry
+ self.Entries[self.entry_type][ident] = \
+ self.entries[ident].bind_entry
if not posixpath.isdir(epath):
# do not pass through directory events
self.entries[ident].handle_event(event)
@@ -1231,7 +1235,7 @@ class GroupSpool(Plugin, Generator):
if fbase in self.entries:
# a directory was deleted
del self.entries[fbase]
- del self.Entries['Path'][fbase]
+ del self.Entries[self.entry_type][fbase]
elif ident in self.entries:
self.entries[ident].handle_event(event)
elif ident not in self.entries:
diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py
new file mode 100644
index 000000000..2059baf60
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py
@@ -0,0 +1,46 @@
+import os
+import logging
+import binascii
+import posixpath
+
+import Bcfg2.Server.Plugin
+logger = logging.getLogger(__name__)
+
+class SEModuleData(Bcfg2.Server.Plugin.SpecificData):
+ def bind_entry(self, entry, _):
+ entry.set('encoding', 'base64')
+ entry.text = binascii.b2a_base64(self.data)
+
+
+class SEModules(Bcfg2.Server.Plugin.GroupSpool):
+ """ Handle SELinux 'module' entries """
+ name = 'SEModules'
+ __author__ = 'chris.a.st.pierre@gmail.com'
+ es_cls = Bcfg2.Server.Plugin.EntrySet
+ es_child_cls = SEModuleData
+ entry_type = 'SELinux'
+ experimental = True
+
+ def _get_module_name(self, entry):
+ """ GroupSpool stores entries as /foo.pp, but we want people
+ to be able to specify module entries as name='foo' or
+ name='foo.pp', so we put this abstraction in between """
+ if entry.get("name").endswith(".pp"):
+ name = entry.get("name")
+ else:
+ name = entry.get("name") + ".pp"
+ return "/" + name
+
+ def HandlesEntry(self, entry, metadata):
+ if entry.tag in self.Entries and entry.get('type') == 'module':
+ return self._get_module_name(entry) in self.Entries[entry.tag]
+ return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry,
+ metadata)
+
+ def HandleEntry(self, entry, metadata):
+ entry.set("name", self._get_module_name(entry))
+ return self.Entries[entry.tag][name](entry, metadata)
+
+ def add_entry(self, event):
+ self.filename_pattern = os.path.basename(event.filename)
+ Bcfg2.Server.Plugin.GroupSpool.add_entry(self, event)
diff --git a/src/sbin/bcfg2 b/src/sbin/bcfg2
index 2a7e5f585..349826676 100755
--- a/src/sbin/bcfg2
+++ b/src/sbin/bcfg2
@@ -35,39 +35,7 @@ class Client:
self.toolset = None
self.config = None
- optinfo = \
- dict(extra=Bcfg2.Options.CLIENT_EXTRA_DISPLAY,
- quick=Bcfg2.Options.CLIENT_QUICK,
- lockfile=Bcfg2.Options.LOCKFILE,
- drivers=Bcfg2.Options.CLIENT_DRIVERS,
- dryrun=Bcfg2.Options.CLIENT_DRYRUN,
- paranoid=Bcfg2.Options.CLIENT_PARANOID,
- bundle=Bcfg2.Options.CLIENT_BUNDLE,
- skipbundle=Bcfg2.Options.CLIENT_SKIPBUNDLE,
- bundle_quick=Bcfg2.Options.CLIENT_BUNDLEQUICK,
- indep=Bcfg2.Options.CLIENT_INDEP,
- skipindep=Bcfg2.Options.CLIENT_SKIPINDEP,
- file=Bcfg2.Options.CLIENT_FILE,
- interactive=Bcfg2.Options.INTERACTIVE,
- cache=Bcfg2.Options.CLIENT_CACHE,
- profile=Bcfg2.Options.CLIENT_PROFILE,
- remove=Bcfg2.Options.CLIENT_REMOVE,
- server=Bcfg2.Options.SERVER_LOCATION,
- user=Bcfg2.Options.CLIENT_USER,
- password=Bcfg2.Options.SERVER_PASSWORD,
- retries=Bcfg2.Options.CLIENT_RETRIES,
- kevlar=Bcfg2.Options.CLIENT_KEVLAR,
- omit_lock_check=Bcfg2.Options.OMIT_LOCK_CHECK,
- decision=Bcfg2.Options.CLIENT_DLIST,
- servicemode=Bcfg2.Options.CLIENT_SERVICE_MODE,
- key=Bcfg2.Options.CLIENT_KEY,
- certificate=Bcfg2.Options.CLIENT_CERT,
- ca=Bcfg2.Options.CLIENT_CA,
- serverCN=Bcfg2.Options.CLIENT_SCNS,
- timeout=Bcfg2.Options.CLIENT_TIMEOUT,
- decision_list=Bcfg2.Options.CLIENT_DECISION_LIST)
- optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
- optinfo.update(Bcfg2.Options.DRIVER_OPTIONS)
+ optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS
self.setup = Bcfg2.Options.OptionParser(optinfo)
self.setup.parse(sys.argv[1:])
diff --git a/tools/selinux_baseline.py b/tools/selinux_baseline.py
new file mode 100755
index 000000000..6ddc390a3
--- /dev/null
+++ b/tools/selinux_baseline.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+import sys
+import logging
+import lxml.etree
+
+import Bcfg2.Logger
+import Bcfg2.Options
+from Bcfg2.Client.Tools.SELinux import *
+
+LOGGER = None
+
+def get_setup():
+ global LOGGER
+ optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS
+ setup = Bcfg2.Options.OptionParser(optinfo)
+ setup.parse(sys.argv[1:])
+
+ if setup['args']:
+ print("selinux_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('selinux_base',
+ to_syslog=False,
+ level=level,
+ to_file=setup['logging'])
+ LOGGER = logging.getLogger('bcfg2')
+ return setup
+
+def main():
+ setup = get_setup()
+ config = lxml.etree.Element("Configuration")
+ selinux = SELinux(LOGGER, setup, config)
+
+ baseline = lxml.etree.Element("Bundle", name="selinux_baseline")
+ for etype, handler in selinux.handlers.items():
+ baseline.append(lxml.etree.Comment("%s entries" % etype))
+ extra = handler.FindExtra()
+ for entry in extra:
+ entry.tag = "BoundSELinux"
+ baseline.extend(extra)
+
+ print lxml.etree.tostring(baseline, pretty_print=True)
+
+if __name__ == "__main__":
+ sys.exit(main())