From 10f815f1af1935bd76b7f75c5577c6f6197d706e Mon Sep 17 00:00:00 2001 From: Sol Jerome Date: Thu, 29 Nov 2012 18:13:42 -0600 Subject: doc: Fix broken target name Signed-off-by: Sol Jerome --- doc/server/plugins/generators/cfg.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index 7d0e0acff..28d783781 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -139,8 +139,8 @@ debug``, and, once in the Python interpreter, run:: path = "" ``path`` should be set to the path to the template file with a leading -slash, relative to the Bcfg2 specification root. See `Inside Genshi -Templates`_ for examples. +slash, relative to the Bcfg2 specification root. See `Inside Templates`_ +for examples. Then, run:: -- cgit v1.2.3-1-g7c22 From 41d1d29b8b545e0f636ebf26795eecd1a46bc9fb Mon Sep 17 00:00:00 2001 From: Sol Jerome Date: Mon, 3 Dec 2012 08:45:48 -0600 Subject: SELinux: Split up selinux entries This commit splits up the all-in-one SELinux tag into various entries (formerly done using a type attribute). This helps prevent ambiguation when entries of different SELinux types have the same name. Note that there is still some ambiguation for File Context entries since there can be duplicates involved. Signed-off-by: Sol Jerome --- doc/server/plugins/generators/rules.txt | 56 ++++++-------- doc/server/selinux.txt | 39 +--------- src/lib/Bcfg2/Client/Tools/SELinux.py | 117 ++++++++++++++++-------------- src/lib/Bcfg2/Server/Plugins/SEModules.py | 6 +- 4 files changed, 91 insertions(+), 127 deletions(-) diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt index 542b38f01..65eb0c5d9 100644 --- a/doc/server/plugins/generators/rules.txt +++ b/doc/server/plugins/generators/rules.txt @@ -394,14 +394,12 @@ the permissions to ``0674``. When this happens, Bcfg2 will change the permissions and set the ACLs on every run and the entry will be eternally marked as bad. -SELinux Tag ------------ +SELinux Entries +--------------- -The SELinux tag has different values depending on the *type* attribute -of the SELinux entry specified in your configuration. Below is a set -of tables which describe the attributes available for various SELinux -types. The types (except for ``module``) correspond to ``semanage`` -subcommands. +Below is a set of tables which describe the attributes available +for various SELinux types. The entry types (except for ``module``) +correspond to ``semanage`` subcommands. Note that the ``selinuxtype`` attribute takes only an SELinux type, not a full context; e.g., "``etc_t``", not @@ -411,18 +409,10 @@ As it can be very tedious to create a baseline of all existing SELinux entries, you can use ``selinux_baseline.py`` located in the ``tools/`` directory to do that for you. -In certain cases, it may be necessary to create multiple SELinux -entries with the same name. For instance, "root" is both an SELinux -user and an SELinux login record; or a given fcontext may need two -different SELinux types depending on whether it's a symlink or a plain -file. In these (few) cases, it is necessary to create BoundSELinux -entries directly in Bundler rather than using abstract SELinux entries -in Bundler and binding them with Rules. - See :ref:`server-selinux` for more information. -boolean -^^^^^^^ +SEBoolean Tag +^^^^^^^^^^^^^ +-------+----------------------+---------+----------+ | Name | Description | Values | Required | @@ -432,8 +422,8 @@ boolean | value | Value of the boolean | on|off | Yes | +-------+----------------------+---------+----------+ -port -^^^^ +SEPort Tag +^^^^^^^^^^ +-------------+------------------------+---------------------------+----------+ | Name | Description | Values | Required | @@ -445,8 +435,8 @@ port | | to this port | | | +-------------+------------------------+---------------------------+----------+ -fcontext -^^^^^^^^ +SEFcontext Tag +^^^^^^^^^^^^^^ +-------------+-------------------------+---------------------+----------+ | Name | Description | Values | Required | @@ -462,8 +452,8 @@ fcontext | | | socket|block|char) | | +-------------+-------------------------+---------------------+----------+ -node -^^^^ +SENode Tag +^^^^^^^^^^ +-------------+------------------------------------+------------------+----------+ | Name | Description | Values | Required | @@ -477,8 +467,8 @@ node | proto | Protocol | (ipv4|ipv6) | Yes | +-------------+------------------------------------+------------------+----------+ -login -^^^^^ +SELogin Tag +^^^^^^^^^^^ +-------------+-------------------------------+-----------+----------+ | Name | Description | Values | Required | @@ -488,8 +478,8 @@ login | selinuxuser | SELinux username | String | Yes | +-------------+-------------------------------+-----------+----------+ -user -^^^^ +SEUser Tag +^^^^^^^^^^ +-------------+-------------------------------+-----------+----------+ | Name | Description | Values | Required | @@ -501,8 +491,8 @@ user | prefix | Home directory context prefix | String | Yes | +-------------+-------------------------------+-----------+----------+ -interface -^^^^^^^^^ +SEInterface Tag +^^^^^^^^^^^^^^^ +-------------+-------------------------+-------------+----------+ | Name | Description | Values | Required | @@ -513,8 +503,8 @@ interface | | to this interface | | | +-------------+-------------------------+-------------+----------+ -permissive -^^^^^^^^^^ +SEPermissive Tag +^^^^^^^^^^^^^^^^ +-------------+------------------------------------+-------------+----------+ | Name | Description | Values | Required | @@ -522,8 +512,8 @@ permissive | name | SELinux type to make permissive | String | Yes | +-------------+------------------------------------+-------------+----------+ -module -^^^^^^ +SEModule Tag +^^^^^^^^^^^^ See :ref:`server-plugins-generators-semodules` diff --git a/doc/server/selinux.txt b/doc/server/selinux.txt index e61a09002..9f54b0d68 100644 --- a/doc/server/selinux.txt +++ b/doc/server/selinux.txt @@ -135,47 +135,16 @@ will be considered extra, making ``selinux_baseline.py`` quite necessary. ``selinux_baseline.py`` writes a bundle to stdout that contains -``BoundSELinux`` entries for the appropriate SELinux entities. It -does this rather than separate Bundle/Rules files because of the -:ref:`server-selinux-duplicate-entries` problem. +``BoundSELinux`` entries for the appropriate SELinux entities. .. _server-selinux-duplicate-entries: Duplicate Entries ----------------- -In certain cases, it may be necessary to create multiple SELinux -entries with the same name. For instance, "root" is both an SELinux -user and an SELinux login record, so to manage both, you would have -the following in Bundler: - -.. code-block:: xml - - - - -And in Rules: - -.. code-block:: xml - - - - -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 - - - - -It may also be necessary to use ``BoundSELinux`` tags if a single -fcontext needs two different SELinux types depending on whether it's a -symlink or a plain file. For instance: +It may be necessary to use `BoundSELinux` tags if a single fcontext +needs two different SELinux types depending on whether it's a symlink +or a plain file. For instance: .. code-block:: xml diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py index fc47883c9..7aa0e8a20 100644 --- a/src/lib/Bcfg2/Client/Tools/SELinux.py +++ b/src/lib/Bcfg2/Client/Tools/SELinux.py @@ -58,36 +58,44 @@ def netmask_itoa(netmask, proto="ipv4"): class SELinux(Bcfg2.Client.Tools.Tool): """ SELinux entry support """ name = 'SELinux' - __handles__ = [('SELinux', 'boolean'), - ('SELinux', 'port'), - ('SELinux', 'fcontext'), - ('SELinux', 'node'), - ('SELinux', 'login'), - ('SELinux', 'user'), - ('SELinux', 'interface'), - ('SELinux', 'permissive'), - ('SELinux', 'module')] - __req__ = dict(SELinux=dict(boolean=['name', 'value'], - module=['name'], - port=['name', 'selinuxtype'], - fcontext=['name', 'selinuxtype'], - node=['name', 'selinuxtype', 'proto'], - login=['name', 'selinuxuser'], - user=['name', 'roles', 'prefix'], - interface=['name', 'selinuxtype'], - permissive=['name'])) + __handles__ = [('SEBoolean', None), + ('SEFcontext', None), + ('SEInterface', None), + ('SELogin', None), + ('SEModule', None), + ('SENode', None), + ('SEPermissive', None), + ('SEPort', None), + ('SEUser', None)] + __req__ = dict(SEBoolean=['name', 'value'], + SEFcontext=['name', 'selinuxtype'], + SEInterface=['name', 'selinuxtype'], + SELogin=['name', 'selinuxuser'], + SEModule=['name'], + SENode=['name', 'selinuxtype', 'proto'], + SEPermissive=['name'], + SEPort=['name', 'selinuxtype'], + SEUser=['name', 'roles', 'prefix']) def __init__(self, logger, setup, config): Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) self.handlers = {} - for handles in self.__handles__: - etype = handles[1] + for handler in self.__handles__: + etype = handler[0] self.handlers[etype] = \ globals()["SELinux%sHandler" % etype.title()](self, logger, setup, config) self.txn = False self.post_txn_queue = [] + def __getattr__(self, attr): + if attr.startswith("VerifySE"): + return self.GenericSEVerify + elif attr.startswith("InstallSE"): + return self.GenericSEInstall + else: + return object.__getattr__(self, attr) + def BundleUpdated(self, _, states): for handler in self.handlers.values(): handler.BundleUpdated(states) @@ -100,12 +108,12 @@ class SELinux(Bcfg2.Client.Tools.Tool): def canInstall(self, entry): return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and - self.handlers[entry.get('type')].canInstall(entry)) + self.handlers[entry.tag].canInstall(entry)) def primarykey(self, entry): """ return a string that should be unique amongst all entries in the specification """ - return self.handlers[entry.get('type')].primarykey(entry) + return self.handlers[entry.tag].primarykey(entry) def Install(self, entries, states): # start a transaction @@ -125,32 +133,32 @@ class SELinux(Bcfg2.Client.Tools.Tool): for func, arg, kwargs in self.post_txn_queue: states[arg] = func(*arg, **kwargs) - def InstallSELinux(self, entry): - """Dispatch install to the proper method according to type""" - return self.handlers[entry.get('type')].Install(entry) + def GenericSEInstall(self, entry): + """Dispatch install to the proper method according to entry tag""" + return self.handlers[entry.tag].Install(entry) - def VerifySELinux(self, entry, _): - """Dispatch verify to the proper method according to type""" - rv = self.handlers[entry.get('type')].Verify(entry) + def GenericSEVerify(self, entry, _): + """Dispatch verify to the proper method according to entry tag""" + rv = self.handlers[entry.tag].Verify(entry) if entry.get('qtext') and self.setup['interactive']: entry.set('qtext', - '%s\nInstall SELinux %s %s: (y/N) ' % + '%s\nInstall %s: (y/N) ' % (entry.get('qtext'), - entry.get('type'), - self.handlers[entry.get('type')].tostring(entry))) + self.handlers[entry.tag].tostring(entry))) return rv def Remove(self, entries): - """Dispatch verify to the proper removal method according to type""" + """Dispatch verify to the proper removal + method according to entry tag""" # sort by type types = list() for entry in entries: - if entry.get('type') not in types: - types.append(entry.get('type')) + if entry.tag not in types: + types.append(entry.tag) for etype in types: self.handlers[etype].Remove([e for e in entries - if e.get('type') == etype]) + if e.tag == etype]) class SELinuxEntryHandler(object): @@ -253,8 +261,7 @@ class SELinuxEntryHandler(object): def key2entry(self, key): """ Generate an XML entry from an SELinux record key """ attrs = self._key2attrs(key) - attrs["type"] = self.etype - return Bcfg2.Client.XML.Element("SELinux", **attrs) + return Bcfg2.Client.XML.Element(self.etype, **attrs) def _args(self, entry, method): """ Get the argument list for invoking _modify or _add, or @@ -279,7 +286,7 @@ class SELinuxEntryHandler(object): """ return a string that should be unique amongst all entries in the specification. some entry types are not universally disambiguated by tag:type:name alone """ - return ":".join([entry.tag, entry.get("type"), entry.get("name")]) + return ":".join([entry.tag, entry.get("name")]) def exists(self, entry): """ return True if the entry already exists in the record list """ @@ -303,8 +310,8 @@ class SELinuxEntryHandler(object): continue if current_attrs[attr] != desired_attrs[attr]: entry.set('current_%s' % attr, current_attrs[attr]) - errors.append("SELinux %s %s has wrong %s: %s, should be %s" % - (self.etype, self.tostring(entry), attr, + errors.append("%s %s has wrong %s: %s, should be %s" % + (entry.tag, entry.get('name'), attr, current_attrs[attr], desired_attrs[attr])) if errors: @@ -331,8 +338,8 @@ class SELinuxEntryHandler(object): return True except ValueError: err = sys.exc_info()[1] - self.logger.debug("Failed to %s SELinux %s %s: %s" % - (method, self.etype, self.tostring(entry), err)) + self.logger.info("Failed to %s SELinux %s %s: %s" % + (method, self.etype, self.tostring(entry), err)) return False def Remove(self, entries): @@ -365,7 +372,7 @@ class SELinuxEntryHandler(object): pass -class SELinuxBooleanHandler(SELinuxEntryHandler): +class SELinuxSebooleanHandler(SELinuxEntryHandler): """ handle SELinux boolean entries """ etype = "boolean" value_format = ("value",) @@ -414,7 +421,7 @@ class SELinuxBooleanHandler(SELinuxEntryHandler): SELinuxEntryHandler.canInstall(self, entry)) -class SELinuxPortHandler(SELinuxEntryHandler): +class SELinuxSeportHandler(SELinuxEntryHandler): """ handle SELinux port entries """ etype = "port" value_format = ('selinuxtype', None) @@ -486,7 +493,7 @@ class SELinuxPortHandler(SELinuxEntryHandler): return tuple(entry.get("name").split("/")) -class SELinuxFcontextHandler(SELinuxEntryHandler): +class SELinuxSefcontextHandler(SELinuxEntryHandler): """ handle SELinux file context entries """ etype = "fcontext" @@ -556,11 +563,11 @@ class SELinuxFcontextHandler(SELinuxEntryHandler): '', '') def primarykey(self, entry): - return ":".join([entry.tag, entry.get("type"), entry.get("name"), + return ":".join([entry.tag, entry.get("name"), entry.get("filetype", "all")]) -class SELinuxNodeHandler(SELinuxEntryHandler): +class SELinuxSenodeHandler(SELinuxEntryHandler): """ handle SELinux node entries """ etype = "node" @@ -592,7 +599,7 @@ class SELinuxNodeHandler(SELinuxEntryHandler): entry.get("selinuxtype")) -class SELinuxLoginHandler(SELinuxEntryHandler): +class SELinuxSeloginHandler(SELinuxEntryHandler): """ handle SELinux login entries """ etype = "login" @@ -603,7 +610,7 @@ class SELinuxLoginHandler(SELinuxEntryHandler): return (entry.get("name"), entry.get("selinuxuser"), "") -class SELinuxUserHandler(SELinuxEntryHandler): +class SELinuxSeuserHandler(SELinuxEntryHandler): """ handle SELinux user entries """ etype = "user" @@ -652,7 +659,7 @@ class SELinuxUserHandler(SELinuxEntryHandler): return tuple(rv) -class SELinuxInterfaceHandler(SELinuxEntryHandler): +class SELinuxSeinterfaceHandler(SELinuxEntryHandler): """ handle SELinux interface entries """ etype = "interface" @@ -663,7 +670,7 @@ class SELinuxInterfaceHandler(SELinuxEntryHandler): return (entry.get("name"), '', entry.get("selinuxtype")) -class SELinuxPermissiveHandler(SELinuxEntryHandler): +class SELinuxSepermissiveHandler(SELinuxEntryHandler): """ handle SELinux permissive domain entries """ etype = "permissive" @@ -695,7 +702,7 @@ class SELinuxPermissiveHandler(SELinuxEntryHandler): return (entry.get("name"),) -class SELinuxModuleHandler(SELinuxEntryHandler): +class SELinuxSemoduleHandler(SELinuxEntryHandler): """ handle SELinux module entries """ etype = "module" @@ -808,10 +815,9 @@ class SELinuxModuleHandler(SELinuxEntryHandler): def Install(self, entry, _=None): if not self.filetool.install(self._pathentry(entry)): return False - if hasattr(self, 'records'): + if hasattr(seobject, 'moduleRecords'): # if seobject has the moduleRecords attribute, install the # module using the seobject library - self.records # pylint: disable=W0104 return self._install_seobject(entry) else: # seobject doesn't have the moduleRecords attribute, so @@ -891,8 +897,7 @@ class SELinuxModuleHandler(SELinuxEntryHandler): def FindExtra(self): specified = [self._key(e) - for e in self.tool.getSupportedEntries() - if e.get("type") == self.etype] + for e in self.tool.getSupportedEntries()] rv = [] for module in self._all_records_from_filesystem().keys(): if module not in specified: diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py index 3edfb72a3..fa47f9496 100644 --- a/src/lib/Bcfg2/Server/Plugins/SEModules.py +++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py @@ -40,8 +40,8 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool): #: objects as its EntrySet children. es_child_cls = SEModuleData - #: SEModules manages ``SELinux`` entries - entry_type = 'SELinux' + #: SEModules manages ``SEModule`` entries + entry_type = 'SEModule' #: The SEModules plugin is experimental experimental = True @@ -68,7 +68,7 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool): return name.lstrip("/") def HandlesEntry(self, entry, metadata): - if entry.tag in self.Entries and entry.get('type') == 'module': + if entry.tag in self.Entries: return self._get_module_filename(entry) in self.Entries[entry.tag] return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry, metadata) -- cgit v1.2.3-1-g7c22 From 30a50cfe1959fa33cb2561ba96106e5eaa4f8ac9 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 30 Nov 2012 08:11:38 -0500 Subject: POSIX: Fixed handling of empty files that don't exist on client in interactive mode --- src/lib/Bcfg2/Client/Tools/POSIX/File.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/File.py b/src/lib/Bcfg2/Client/Tools/POSIX/File.py index 5842c4e1f..9b95d2234 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/File.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/File.py @@ -188,6 +188,10 @@ class POSIXFile(POSIXTool): prompt.append(udiff) except UnicodeEncodeError: prompt.append("Could not encode diff") + elif entry.get("empty", "true"): + # the file doesn't exist on disk, but there's no + # expected content + prompt.append("%s does not exist" % entry.get("name")) else: prompt.append("Diff took too long to compute, no " "printable diff") -- cgit v1.2.3-1-g7c22 From 1d4b0118ced1b198587fd75c549e2b394ff71531 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 30 Nov 2012 08:18:12 -0500 Subject: doc: fixed in-depth genshi debugging instructions --- doc/server/plugins/generators/cfg.txt | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index 28d783781..94394f98f 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -136,33 +136,33 @@ by running the template manually. To do this, run ``bcfg2-info debug``, and, once in the Python interpreter, run:: metadata = self.build_metadata("") - path = "" - -``path`` should be set to the path to the template file with a leading -slash, relative to the Bcfg2 specification root. See `Inside Templates`_ -for examples. + source_path = "" + name = source_path[len(self.setup['repo']):] Then, run:: - import os, Bcfg2.Options + import os from genshi.template import TemplateLoader, NewTextTemplate - name = os.path.dirname(path[path.find('/', 1):]) - setup = Bcfg2.Options.OptionParser({'repo': - Bcfg2.Options.SERVER_REPOSITORY}) - setup.parse('--') - template = TemplateLoader().load(setup['repo'] + path, cls=NewTextTemplate) - print template.generate(metadata=metadata, path=path, name=name).render() + template = TemplateLoader().load(source_path, cls=NewTextTemplate) + data = dict(metadata=metadata, + source_path=source_path, + path=source_path, + name=name, + repo=self.setup['repo']) + print(template.generate(**data).render()) This gives you more fine-grained control over how your template is -rendered. +rendered. E.g., you can tweak the values of the variables passed to +the template, or evaluate the template manually, line-by-line, and so +on. You can also use this approach to render templates that depend on :ref:`altsrc ` tags by setting -``path`` to the path to the template, and setting ``name`` to the path +``source_path`` to the path to the template, and setting ``name`` to the path to the file to be generated, e.g.:: metadata = self.build_metadata("foo.example.com") - path = "/Cfg/etc/sysconfig/network-scripts/ifcfg-template/ifcfg-template.genshi" + source_path = "/Cfg/etc/sysconfig/network-scripts/ifcfg-template/ifcfg-template.genshi" name = "/etc/sysconfig/network-scripts/ifcfg-bond0" Error handling -- cgit v1.2.3-1-g7c22 From 33234d5dae565e6520bbdb65d67fbaed03df4d43 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 3 Dec 2012 10:51:34 -0600 Subject: added builtin support for creating users and groups --- doc/client/tools/posixusers.txt | 51 +++ doc/server/plugins/generators/rules.txt | 132 ++++-- schemas/bundle.xsd | 30 +- schemas/rules.xsd | 14 + schemas/types.xsd | 17 + src/lib/Bcfg2/Client/Client.py | 4 +- src/lib/Bcfg2/Client/Frame.py | 11 +- src/lib/Bcfg2/Client/Tools/POSIXUsers.py | 300 +++++++++++++ src/lib/Bcfg2/Client/Tools/__init__.py | 1 + .../TestClient/TestTools/TestPOSIX/Test__init.py | 20 +- .../Testlib/TestClient/TestTools/TestPOSIXUsers.py | 489 +++++++++++++++++++++ tools/README | 4 + tools/posixusers_baseline.py | 73 +++ 13 files changed, 1099 insertions(+), 47 deletions(-) create mode 100644 doc/client/tools/posixusers.txt create mode 100644 src/lib/Bcfg2/Client/Tools/POSIXUsers.py create mode 100644 testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py create mode 100755 tools/posixusers_baseline.py diff --git a/doc/client/tools/posixusers.txt b/doc/client/tools/posixusers.txt new file mode 100644 index 000000000..884edc2b7 --- /dev/null +++ b/doc/client/tools/posixusers.txt @@ -0,0 +1,51 @@ +.. -*- mode: rst -*- + +.. _client-tools-posixusers: + +========== +POSIXUsers +========== + +The POSIXUsers tool handles the creation of users and groups as +defined by ``POSIXUser`` and ``POSIXGroup`` entries. For a full +description of those tags, see :ref:`server-plugins-generators-rules`. + +The POSIXUsers tool relies on the ``useradd``, ``usermod``, +``userdel``, ``groupadd``, ``groupmod``, and ``groupdel`` tools, since +there is no Python library to manage users and groups. It expects +those tools to be in ``/usr/sbin``. + +Primary group creation +====================== + +Each user must have a primary group, which can be specified with the +``group`` attribute of the ``POSIXUser`` tag. (If the ``group`` +attribute is not specified, then a group with the same name as the +user will be used.) If that group does not exist, the POSIXUsers tool +will create it automatically. It does this by adding a ``POSIXGroup`` +entry on the fly; this has a few repercussions: + +* When run in interactive mode (``-I``), Bcfg2 will prompt for + installation of the group separately from the user. +* The ``POSIXGroup`` entry is added to the same bundle as the + ``POSIXUser`` entry, so if the group is created, the bundle is + considered to have been modified and consequently Actions will be + run and Services will be restarted. This should never be a concern, + since the group can only be created, not modified (it has no + attributes other than its name), and if the group is being created + then the user will certainly be created or modified as well. +* The group is created with no specified GID number. If you need to + specify a particular GID number, you must explicitly define a + ``POSIXGroup`` entry for the group. + +Creating a baseline configuration +================================= + +The majority of users on many systems are created by the packages that +are installed, but currently Bcfg2 cannot query the package database +to determine these users. (In some cases, this is a limitation of the +packaging system.) The often-tedious task of creating a baseline that +defines all users and groups can be simplified by use of the +``tools/posixusers_baseline.py`` script, which outputs a bundle +containing all users and groups on the machine it's run on. + diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt index 65eb0c5d9..cdde65960 100644 --- a/doc/server/plugins/generators/rules.txt +++ b/doc/server/plugins/generators/rules.txt @@ -62,10 +62,10 @@ The Rules Tag may have the following attributes: | | Rules list.The higher value wins. | | +----------+-------------------------------------+--------+ -Rules Group Tag ---------------- +Group Tag +--------- -The Rules Group Tag may have the following attributes: +The Group Tag may have the following attributes: +--------+-------------------------+--------------+ | Name | Description | Values | @@ -76,6 +76,27 @@ The Rules Group Tag may have the following attributes: | | (is not a member of) | | +--------+-------------------------+--------------+ +Client Tag +---------- + +The Client Tag is used in Rules for selecting the package entries to +include in the clients literal configuration. Its function is similar +to the Group tag in this context. It can be thought of as:: + + if client is name then + assign to literal config + +The Client Tag may have the following attributes: + ++--------+-------------------------+--------------+ +| Name | Description | Values | ++========+=========================+==============+ +| name | Client Name | String | ++--------+-------------------------+--------------+ +| negate | Negate client selection | (true|false) | +| | (if not client name) | | ++--------+-------------------------+--------------+ + Package Tag ----------- @@ -84,8 +105,7 @@ The Package Tag may have the following attributes: +------------+----------------------------------------------+----------+ | Name | Description | Values | +============+==============================================+==========+ -| name | Package name or regular expression | String | -| | | or regex | +| name | Package name | String | +------------+----------------------------------------------+----------+ | version | Package Version or version='noverify' to | String | | | not do version checking in the Yum driver | | @@ -131,8 +151,7 @@ Service Tag | | service (new in 1.3; replaces | | | | "mode" attribute) | | +------------+-------------------------------+---------------------------------------------------------+ -| name | Service name or regular | String or regex | -| | expression | | +| name | Service name | String | +------------+-------------------------------+---------------------------------------------------------+ | status | Should the service be on or | (on | off | ignore) | | | off (default: off). | | @@ -193,27 +212,6 @@ Service status descriptions * Don't perform service status checks. -Client Tag ----------- - -The Client Tag is used in Rules for selecting the package entries to -include in the clients literal configuration. Its function is similar -to the Group tag in this context. It can be thought of as:: - - if client is name then - assign to literal config - -The Client Tag may have the following attributes: - -+--------+-------------------------+--------------+ -| Name | Description | Values | -+========+=========================+==============+ -| name | Client Name | String | -+--------+-------------------------+--------------+ -| negate | Negate client selection | (true|false) | -| | (if not client name) | | -+--------+-------------------------+--------------+ - Path Tag -------- @@ -229,11 +227,11 @@ the context of the file to the default set by policy. See Attributes common to all Path tags: -+----------+---------------------------------------------------+-----------------+ -| Name | Description | Values | -+==========+===================================================+=================+ -| name | Full path or regular expression matching the path | String or regex | -+----------+---------------------------------------------------+-----------------+ ++----------+-------------+--------+ +| Name | Description | Values | ++==========+=============+========+ +| name | Full path | String | ++----------+-------------+--------+ device @@ -517,6 +515,74 @@ SEModule Tag See :ref:`server-plugins-generators-semodules` +POSIXUser Tag +------------- + +The POSIXUser tag allows you to create users on client machines. It +takes the following attributes: + ++-------+-----------------------+---------+-------------------------------+ +| Name | Description | Values | Default | ++=======+=======================+=========+===============================+ +| name | Username | String | None | ++-------+-----------------------+---------+-------------------------------+ +| uid | User ID number | Integer | The client sets the uid | ++-------+-----------------------+---------+-------------------------------+ +| group | Name of the user's | String | The username | +| | primary group | | | ++-------+-----------------------+---------+-------------------------------+ +| gecos | Human-readable user | String | The username | +| | name or comment | | | ++-------+-----------------------+---------+-------------------------------+ +| home | User's home directory | String | /root (for "root"); | +| | | | /home/ otherwise | ++-------+-----------------------+---------+-------------------------------+ +| shell | User's shell | String | /bin/bash | ++-------+-----------------------+---------+-------------------------------+ + +The group specified will automatically be created if it does not +exist, even if there is no `POSIXGroup Tag`_ for it. If you need to +specify a particular GID for the group, you must specify that in a +``POSIXGroup`` tag. + +If you with to change the default shell, you can do so with :ref:`the +Defaults plugin `. + +Additionally, a user may be a member of supplementary groups. These +can be specified with the ``MemberOf`` child tag of the ``POSIXUser`` +tag. + +For example: + +.. code-block:: xml + + + lp + adm + bin + + +See :ref:`client-tools-posixusers` for more information on managing +users and groups. + +POSIXGroup Tag +-------------- + +The POSIXGroup tag allows you to create groups on client machines. It +takes the following attributes: + ++-------+-------------------+---------+-------------------------+ +| Name | Description | Values | Default | ++=======+===================+=========+=========================+ +| name | Name of the group | String | None | ++-------+-------------------+---------+-------------------------+ +| gid | Group ID number | Integer | The client sets the gid | ++-------+-------------------+---------+-------------------------+ + +See :ref:`client-tools-posixusers` for more information on managing +users and groups. + Rules Directory =============== diff --git a/schemas/bundle.xsd b/schemas/bundle.xsd index 6306b6da4..1fcf82c27 100644 --- a/schemas/bundle.xsd +++ b/schemas/bundle.xsd @@ -36,7 +36,7 @@ Abstract implementation of a Path entry. The entry will either be handled by Cfg, TGenshi, or another - DirectoryBacked plugin; or handled by Rules, in which case + Generator plugin; or handled by Rules, in which case the full specification of this entry will be included in Rules. @@ -66,6 +66,20 @@ + + + + Abstract description of a POSIXUser entry. + + + + + + + Abstract description of a POSIXGroup entry. + + + @@ -111,6 +125,20 @@ + + + + Fully bound description of a POSIXUser entry. + + + + + + + Fully bound description of a POSIXGroup entry. + + + diff --git a/schemas/rules.xsd b/schemas/rules.xsd index 2f4f805c0..241ffe5bf 100644 --- a/schemas/rules.xsd +++ b/schemas/rules.xsd @@ -57,6 +57,20 @@ + + + + Fully bound description of a POSIXUser entry. + + + + + + + Fully bound description of a POSIXGroup entry. + + + diff --git a/schemas/types.xsd b/schemas/types.xsd index 1edde8754..a36693b2d 100644 --- a/schemas/types.xsd +++ b/schemas/types.xsd @@ -220,4 +220,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py index f197a9074..45e0b64e6 100644 --- a/src/lib/Bcfg2/Client/Client.py +++ b/src/lib/Bcfg2/Client/Client.py @@ -56,8 +56,8 @@ class Client(object): self.logger.error("Service removal is nonsensical; " "removed services will only be disabled") if (self.setup['remove'] and - self.setup['remove'].lower() not in ['all', 'services', - 'packages']): + self.setup['remove'].lower() not in ['all', 'services', 'packages', + 'users']): self.logger.error("Got unknown argument %s for -r" % self.setup['remove']) if self.setup["file"] and self.setup["cache"]: diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py index 53180ab68..4f3ff1820 100644 --- a/src/lib/Bcfg2/Client/Frame.py +++ b/src/lib/Bcfg2/Client/Frame.py @@ -105,6 +105,10 @@ class Frame(object): if deprecated: self.logger.warning("Loaded deprecated tool drivers:") self.logger.warning(deprecated) + experimental = [tool.name for tool in self.tools if tool.experimental] + if experimental: + self.logger.warning("Loaded experimental tool drivers:") + self.logger.warning(experimental) # find entries not handled by any tools self.unhandled = [entry for struct in config @@ -281,12 +285,15 @@ class Frame(object): if self.setup['remove']: if self.setup['remove'] == 'all': self.removal = self.extra - elif self.setup['remove'] in ['services', 'Services']: + elif self.setup['remove'].lower() == 'services': self.removal = [entry for entry in self.extra if entry.tag == 'Service'] - elif self.setup['remove'] in ['packages', 'Packages']: + elif self.setup['remove'].lower() == 'packages': self.removal = [entry for entry in self.extra if entry.tag == 'Package'] + elif self.setup['remove'].lower() == 'users': + self.removal = [entry for entry in self.extra + if entry.tag in ['POSIXUser', 'POSIXGroup']] candidates = [entry for entry in self.states if not self.states[entry]] diff --git a/src/lib/Bcfg2/Client/Tools/POSIXUsers.py b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py new file mode 100644 index 000000000..78734f5c2 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIXUsers.py @@ -0,0 +1,300 @@ +""" A tool to handle creating users and groups with useradd/mod/del +and groupadd/mod/del """ + +import sys +import pwd +import grp +import Bcfg2.Client.XML +import subprocess +import Bcfg2.Client.Tools + + +class ExecutionError(Exception): + """ Raised when running an external command fails """ + + def __init__(self, msg, retval=None): + Exception.__init__(self, msg) + self.retval = retval + + def __str__(self): + return "%s (rv: %s)" % (Exception.__str__(self), + self.retval) + + +class Executor(object): + """ A better version of Bcfg2.Client.Tool.Executor, which captures + stderr, raises exceptions on error, and doesn't use the shell to + execute by default """ + + def __init__(self, logger): + self.logger = logger + self.stdout = None + self.stderr = None + self.retval = None + + def run(self, command, inputdata=None, shell=False): + """ Run a command, given as a list, optionally giving it the + specified input data """ + self.logger.debug("Running: %s" % " ".join(command)) + proc = subprocess.Popen(command, shell=shell, bufsize=16384, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + if inputdata: + for line in inputdata.splitlines(): + self.logger.debug('> %s' % line) + (self.stdout, self.stderr) = proc.communicate(inputdata) + else: + (self.stdout, self.stderr) = proc.communicate() + for line in self.stdout.splitlines(): # pylint: disable=E1103 + self.logger.debug('< %s' % line) + self.retval = proc.wait() + if self.retval == 0: + for line in self.stderr.splitlines(): # pylint: disable=E1103 + self.logger.warning(line) + return True + else: + raise ExecutionError(self.stderr, self.retval) + + +class POSIXUsers(Bcfg2.Client.Tools.Tool): + """ A tool to handle creating users and groups with + useradd/mod/del and groupadd/mod/del """ + __execs__ = ['/usr/sbin/useradd', '/usr/sbin/usermod', '/usr/sbin/userdel', + '/usr/sbin/groupadd', '/usr/sbin/groupmod', + '/usr/sbin/groupdel'] + __handles__ = [('POSIXUser', None), + ('POSIXGroup', None)] + __req__ = dict(POSIXUser=['name'], + POSIXGroup=['name']) + experimental = True + + # A mapping of XML entry attributes to the indexes of + # corresponding values in the get*ent data structures + attr_mapping = dict(POSIXUser=dict(name=0, uid=2, gecos=4, home=5, + shell=6), + POSIXGroup=dict(name=0, gid=2)) + + def __init__(self, logger, setup, config): + Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + self.set_defaults = dict(POSIXUser=self.populate_user_entry, + POSIXGroup=lambda g: g) + self.cmd = Executor(logger) + self._existing = None + + @property + def existing(self): + """ Get a dict of existing users and groups """ + if self._existing is None: + self._existing = dict(POSIXUser=dict([(u[0], u) + for u in pwd.getpwall()]), + POSIXGroup=dict([(g[0], g) + for g in grp.getgrall()])) + return self._existing + + def Inventory(self, states, structures=None): + if not structures: + structures = self.config.getchildren() + # we calculate a list of all POSIXUser and POSIXGroup entries, + # and then add POSIXGroup entries that are required to create + # the primary group for each user to the structures. this is + # sneaky and possibly evil, but it works great. + groups = [] + for struct in structures: + groups.extend([e.get("name") + for e in struct.findall("POSIXGroup")]) + for struct in structures: + for entry in struct.findall("POSIXUser"): + group = self.set_defaults[entry.tag](entry).get('group') + if group and group not in groups: + self.logger.debug("POSIXUsers: Adding POSIXGroup entry " + "'%s' for user '%s'" % + (group, entry.get("name"))) + struct.append(Bcfg2.Client.XML.Element("POSIXGroup", + name=group)) + return Bcfg2.Client.Tools.Tool.Inventory(self, states, structures) + + def FindExtra(self): + extra = [] + for handles in self.__handles__: + tag = handles[0] + specified = [] + for entry in self.getSupportedEntries(): + if entry.tag == tag: + specified.append(entry.get("name")) + extra.extend([Bcfg2.Client.XML.Element(tag, name=e) + for e in self.existing[tag].keys() + if e not in specified]) + return extra + + def populate_user_entry(self, entry): + """ Given a POSIXUser entry, set all of the 'missing' attributes + with their defaults """ + defaults = dict(group=entry.get('name'), + gecos=entry.get('name'), + shell='/bin/bash') + if entry.get('name') == 'root': + defaults['home'] = '/root' + else: + defaults['home'] = '/home/%s' % entry.get('name') + for key, val in defaults.items(): + if entry.get(key) is None: + entry.set(key, val) + if entry.get('group') in self.existing['POSIXGroup']: + entry.set('gid', + str(self.existing['POSIXGroup'][entry.get('group')][2])) + return entry + + def user_supplementary_groups(self, entry): + """ Get a list of supplmentary groups that the user in the + given entry is a member of """ + return [g for g in self.existing['POSIXGroup'].values() + if entry.get("name") in g[3] and g[0] != entry.get("group")] + + def VerifyPOSIXUser(self, entry, _): + """ Verify a POSIXUser entry """ + rv = self._verify(self.populate_user_entry(entry)) + if entry.get("current_exists", "true") == "true": + # verify supplemental groups + actual = [g[0] for g in self.user_supplementary_groups(entry)] + expected = [e.text for e in entry.findall("MemberOf")] + if set(expected) != set(actual): + entry.set('qtext', + "\n".join([entry.get('qtext', '')] + + ["%s %s has incorrect supplemental group " + "membership. Currently: %s. Should be: %s" + % (entry.tag, entry.get("name"), + actual, expected)])) + rv = False + if self.setup['interactive'] and not rv: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext', ''), entry.tag, entry.get('name'))) + return rv + + def VerifyPOSIXGroup(self, entry, _): + """ Verify a POSIXGroup entry """ + rv = self._verify(entry) + if self.setup['interactive'] and not rv: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext', ''), entry.tag, entry.get('name'))) + return rv + + def _verify(self, entry): + """ Perform most of the actual work of verification """ + errors = [] + if entry.get("name") not in self.existing[entry.tag]: + entry.set('current_exists', 'false') + errors.append("%s %s does not exist" % (entry.tag, + entry.get("name"))) + else: + for attr, idx in self.attr_mapping[entry.tag].items(): + val = str(self.existing[entry.tag][entry.get("name")][idx]) + entry.set("current_%s" % attr, val) + if attr in ["uid", "gid"]: + if entry.get(attr) is None: + # no uid/gid specified, so we let the tool + # automatically determine one -- i.e., it always + # verifies + continue + if val != entry.get(attr): + errors.append("%s for %s %s is incorrect. Current %s is " + "%s, but should be %s" % + (attr.title(), entry.tag, entry.get("name"), + attr, entry.get(attr), val)) + + if errors: + for error in errors: + self.logger.debug("%s: %s" % (self.name, error)) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + return len(errors) == 0 + + def Install(self, entries, states): + for entry in entries: + # install groups first, so that all groups exist for + # users that might need them + if entry.tag == 'POSIXGroup': + states[entry] = self._install(entry) + for entry in entries: + if entry.tag == 'POSIXUser': + states[entry] = self._install(entry) + self._existing = None + + def _install(self, entry): + """ add or modify a user or group using the appropriate command """ + if entry.get("name") not in self.existing[entry.tag]: + action = "add" + else: + action = "mod" + try: + self.cmd.run(self._get_cmd(action, + self.set_defaults[entry.tag](entry))) + self.modified.append(entry) + return True + except ExecutionError: + self.logger.error("POSIXUsers: Error creating %s %s: %s" % + (entry.tag, entry.get("name"), + sys.exc_info()[1])) + return False + + def _get_cmd(self, action, entry): + """ Get a command to perform the appropriate action (add, mod, + del) on the given entry. The command is always the same; we + set all attributes on a given user or group when modifying it + rather than checking which ones need to be changed. This + makes things fail as a unit (e.g., if a user is logged in, you + can't change its home dir, but you could change its GECOS, but + the whole operation fails), but it also makes this function a + lot, lot easier and simpler.""" + cmd = ["/usr/sbin/%s%s" % (entry.tag[5:].lower(), action)] + if action != 'del': + if entry.tag == 'POSIXGroup': + if entry.get('gid'): + cmd.extend(['-g', entry.get('gid')]) + elif entry.tag == 'POSIXUser': + cmd.append('-m') + if entry.get('uid'): + cmd.extend(['-u', entry.get('uid')]) + cmd.extend(['-g', entry.get('group')]) + extras = [e.text for e in entry.findall("MemberOf")] + if extras: + cmd.extend(['-G', ",".join(extras)]) + cmd.extend(['-d', entry.get('home')]) + cmd.extend(['-s', entry.get('shell')]) + cmd.extend(['-c', entry.get('gecos')]) + cmd.append(entry.get('name')) + return cmd + + def Remove(self, entries): + for entry in entries: + # remove users first, so that all users have been removed + # from groups before we remove them + if entry.tag == 'POSIXUser': + self._remove(entry) + for entry in entries: + if entry.tag == 'POSIXGroup': + try: + grp.getgrnam(entry.get("name")) + self._remove(entry) + except KeyError: + # at least some versions of userdel automatically + # remove the primary group for a user if the group + # name is the same as the username, and no other + # users are in the group + self.logger.info("POSIXUsers: Group %s does not exist. " + "It may have already been removed when " + "its users were deleted" % + entry.get("name")) + self._existing = None + self.extra = self.FindExtra() + + def _remove(self, entry): + """ Remove an entry """ + try: + self.cmd.run(self._get_cmd("del", entry)) + return True + except ExecutionError: + self.logger.error("POSIXUsers: Error deleting %s %s: %s" % + (entry.tag, entry.get("name"), + sys.exc_info()[1])) + return False diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index 927b25ba8..d5f55759f 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -61,6 +61,7 @@ class Tool(object): __req__ = {} __important__ = [] deprecated = False + experimental = False def __init__(self, logger, setup, config): self.setup = setup diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py index e503ebd38..4048be7ca 100644 --- a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py @@ -16,12 +16,14 @@ while path != "/": path = os.path.dirname(path) from common import * + def get_config(entries): config = lxml.etree.Element("Configuration") bundle = lxml.etree.SubElement(config, "Bundle", name="test") bundle.extend(entries) return config + def get_posix_object(logger=None, setup=None, config=None): if config is None: config = lxml.etree.Element("Configuration") @@ -36,7 +38,7 @@ def get_posix_object(logger=None, setup=None, config=None): if not setup: setup = MagicMock() return Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config) - + class TestPOSIX(Bcfg2TestCase): def setUp(self): @@ -55,7 +57,7 @@ class TestPOSIX(Bcfg2TestCase): self.assertGreater(len(posix.__req__['Path']), 0) self.assertGreater(len(posix.__handles__), 0) self.assertItemsEqual(posix.handled, entries) - + @patch("Bcfg2.Client.Tools.Tool.canVerify") def test_canVerify(self, mock_canVerify): entry = lxml.etree.Element("Path", name="test", type="file") @@ -64,7 +66,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canVerify.return_value = False self.assertFalse(self.posix.canVerify(entry)) mock_canVerify.assert_called_with(self.posix, entry) - + # next, test fully_specified failure self.posix.logger.error.reset_mock() mock_canVerify.reset_mock() @@ -77,7 +79,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canVerify.assert_called_with(self.posix, entry) mock_fully_spec.assert_called_with(entry) self.assertTrue(self.posix.logger.error.called) - + # finally, test success self.posix.logger.error.reset_mock() mock_canVerify.reset_mock() @@ -96,7 +98,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canInstall.return_value = False self.assertFalse(self.posix.canInstall(entry)) mock_canInstall.assert_called_with(self.posix, entry) - + # next, test fully_specified failure self.posix.logger.error.reset_mock() mock_canInstall.reset_mock() @@ -109,7 +111,7 @@ class TestPOSIX(Bcfg2TestCase): mock_canInstall.assert_called_with(self.posix, entry) mock_fully_spec.assert_called_with(entry) self.assertTrue(self.posix.logger.error.called) - + # finally, test success self.posix.logger.error.reset_mock() mock_canInstall.reset_mock() @@ -177,7 +179,7 @@ class TestPOSIX(Bcfg2TestCase): posix._prune_old_backups(entry) mock_listdir.assert_called_with(setup['ppath']) - self.assertItemsEqual(mock_remove.call_args_list, + self.assertItemsEqual(mock_remove.call_args_list, [call(os.path.join(setup['ppath'], p)) for p in remove]) @@ -189,7 +191,7 @@ class TestPOSIX(Bcfg2TestCase): # need to be removed even if we get an error posix._prune_old_backups(entry) mock_listdir.assert_called_with(setup['ppath']) - self.assertItemsEqual(mock_remove.call_args_list, + self.assertItemsEqual(mock_remove.call_args_list, [call(os.path.join(setup['ppath'], p)) for p in remove]) self.assertTrue(posix.logger.error.called) @@ -203,7 +205,7 @@ class TestPOSIX(Bcfg2TestCase): entry = lxml.etree.Element("Path", name="/etc/foo", type="file") setup = dict(ppath='/', max_copies=5, paranoid=False) posix = get_posix_object(setup=setup) - + # paranoid false globally posix._paranoid_backup(entry) self.assertFalse(mock_prune.called) diff --git a/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py new file mode 100644 index 000000000..46ae4e47b --- /dev/null +++ b/testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py @@ -0,0 +1,489 @@ +import os +import sys +import copy +import lxml.etree +import subprocess +from mock import Mock, MagicMock, patch +import Bcfg2.Client.Tools +from Bcfg2.Client.Tools.POSIXUsers import * + +# add all parent testsuite directories to sys.path to allow (most) +# relative imports in python 2.4 +path = os.path.dirname(__file__) +while path != "/": + if os.path.basename(path).lower().startswith("test"): + sys.path.append(path) + if os.path.basename(path) == "testsuite": + break + path = os.path.dirname(path) +from common import * + + +class TestExecutor(Bcfg2TestCase): + test_obj = Executor + + def get_obj(self, logger=None): + if not logger: + def print_msg(msg): + print(msg) + logger = Mock() + logger.error = Mock(side_effect=print_msg) + logger.warning = Mock(side_effect=print_msg) + logger.info = Mock(side_effect=print_msg) + logger.debug = Mock(side_effect=print_msg) + return self.test_obj(logger) + + @patch("subprocess.Popen") + def test_run(self, mock_Popen): + exc = self.get_obj() + cmd = ["/bin/test", "-a", "foo"] + proc = Mock() + proc.wait = Mock() + proc.wait.return_value = 0 + proc.communicate = Mock() + proc.communicate.return_value = (MagicMock(), MagicMock()) + mock_Popen.return_value = proc + + self.assertTrue(exc.run(cmd)) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], False) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with() + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + mock_Popen.reset_mock() + inputdata = "foo\n\nbar" + self.assertTrue(exc.run(cmd, inputdata=inputdata, shell=True)) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], True) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with(inputdata) + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + mock_Popen.reset_mock() + proc.wait.return_value = 1 + self.assertRaises(ExecutionError, exc.run, cmd) + args = mock_Popen.call_args + self.assertEqual(args[0][0], cmd) + self.assertEqual(args[1]['shell'], False) + self.assertEqual(args[1]['stdin'], subprocess.PIPE) + self.assertEqual(args[1]['stdout'], subprocess.PIPE) + self.assertEqual(args[1]['stderr'], subprocess.PIPE) + proc.communicate.assert_called_with() + proc.wait.assert_called_with() + self.assertEqual(proc.communicate.return_value, + (exc.stdout, exc.stderr)) + self.assertEqual(proc.wait.return_value, + exc.retval) + + +class TestPOSIXUsers(Bcfg2TestCase): + test_obj = POSIXUsers + + def get_obj(self, logger=None, setup=None, config=None): + if config is None: + config = lxml.etree.Element("Configuration") + if not logger: + def print_msg(msg): + print(msg) + logger = Mock() + logger.error = Mock(side_effect=print_msg) + logger.warning = Mock(side_effect=print_msg) + logger.info = Mock(side_effect=print_msg) + logger.debug = Mock(side_effect=print_msg) + if not setup: + setup = MagicMock() + return self.test_obj(logger, setup, config) + + @patch("pwd.getpwall") + @patch("grp.getgrall") + def test_existing(self, mock_getgrall, mock_getpwall): + users = self.get_obj() + mock_getgrall.return_value = MagicMock() + mock_getpwall.return_value = MagicMock() + + def reset(): + mock_getgrall.reset_mock() + mock_getpwall.reset_mock() + + # make sure we start clean + self.assertIsNone(users._existing) + self.assertIsInstance(users.existing, dict) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + mock_getgrall.assert_called_with() + mock_getpwall.assert_called_with() + + reset() + self.assertIsInstance(users._existing, dict) + self.assertIsInstance(users.existing, dict) + self.assertEqual(users.existing, users._existing) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + self.assertFalse(mock_getgrall.called) + self.assertFalse(mock_getpwall.called) + + reset() + users._existing = None + self.assertIsInstance(users.existing, dict) + self.assertIn("POSIXUser", users.existing) + self.assertIn("POSIXGroup", users.existing) + mock_getgrall.assert_called_with() + mock_getpwall.assert_called_with() + + @patch("Bcfg2.Client.Tools.Tool.Inventory") + def test_Inventory(self, mock_Inventory): + config = lxml.etree.Element("Configuration") + bundle = lxml.etree.SubElement(config, "Bundle", name="test") + lxml.etree.SubElement(bundle, "POSIXUser", name="test", group="test") + lxml.etree.SubElement(bundle, "POSIXUser", name="test2", group="test2") + lxml.etree.SubElement(bundle, "POSIXGroup", name="test2") + + orig_bundle = copy.deepcopy(bundle) + + users = self.get_obj(config=config) + users.set_defaults['POSIXUser'] = Mock() + users.set_defaults['POSIXUser'].side_effect = lambda e: e + + states = dict() + self.assertEqual(users.Inventory(states), + mock_Inventory.return_value) + mock_Inventory.assert_called_with(users, states, config.getchildren()) + lxml.etree.SubElement(orig_bundle, "POSIXGroup", name="test") + self.assertXMLEqual(orig_bundle, bundle) + + def test_FindExtra(self): + users = self.get_obj() + + def getSupportedEntries(): + return [lxml.etree.Element("POSIXUser", name="test1"), + lxml.etree.Element("POSIXGroup", name="test1")] + + users.getSupportedEntries = Mock() + users.getSupportedEntries.side_effect = getSupportedEntries + + users._existing = dict(POSIXUser=dict(test1=(), + test2=()), + POSIXGroup=dict(test2=())) + extra = users.FindExtra() + self.assertEqual(len(extra), 2) + self.assertItemsEqual([e.tag for e in extra], + ["POSIXUser", "POSIXGroup"]) + self.assertItemsEqual([e.get("name") for e in extra], + ["test2", "test2"]) + + def test_populate_user_entry(self): + users = self.get_obj() + users._existing = dict(POSIXUser=dict(), + POSIXGroup=dict(root=('root', 'x', 0, []))) + + cases = [(lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="test", shell="/bin/bash", + home="/home/test")), + (lxml.etree.Element("POSIXUser", name="root", gecos="Root", + shell="/bin/zsh"), + lxml.etree.Element("POSIXUser", name="root", group='root', + gid='0', gecos="Root", shell="/bin/zsh", + home='/root')), + (lxml.etree.Element("POSIXUser", name="test2", gecos="", + shell="/bin/zsh"), + lxml.etree.Element("POSIXUser", name="test2", group='test2', + gecos="", shell="/bin/zsh", + home='/home/test2'))] + + for initial, expected in cases: + actual = users.populate_user_entry(initial) + self.assertXMLEqual(actual, expected) + + def test_user_supplementary_groups(self): + users = self.get_obj() + users._existing = \ + dict(POSIXUser=dict(), + POSIXGroup=dict(root=('root', 'x', 0, []), + wheel=('wheel', 'x', 10, ['test']), + users=('users', 'x', 100, ['test']))) + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertItemsEqual(users.user_supplementary_groups(entry), + [users.existing['POSIXGroup']['wheel'], + users.existing['POSIXGroup']['users']]) + entry.set('name', 'test2') + self.assertItemsEqual(users.user_supplementary_groups(entry), []) + + def test_VerifyPOSIXUser(self): + users = self.get_obj() + users._verify = Mock() + users._verify.return_value = True + users.populate_user_entry = Mock() + users.user_supplementary_groups = Mock() + users.user_supplementary_groups.return_value = \ + [('wheel', 'x', 10, ['test']), ('users', 'x', 100, ['test'])] + + def reset(): + users._verify.reset_mock() + users.populate_user_entry.reset_mock() + users.user_supplementary_groups.reset_mock() + + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + m1 = lxml.etree.SubElement(entry, "MemberOf") + m1.text = "wheel" + m2 = lxml.etree.SubElement(entry, "MemberOf") + m2.text = "users" + self.assertTrue(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + m3 = lxml.etree.SubElement(entry, "MemberOf") + m3.text = "extra" + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + users.user_supplementary_groups.assert_called_with(entry) + + reset() + def _verify(entry): + entry.set("current_exists", "false") + return False + + users._verify.side_effect = _verify + self.assertFalse(users.VerifyPOSIXUser(entry, [])) + users.populate_user_entry.assert_called_with(entry) + users._verify.assert_called_with(users.populate_user_entry.return_value) + + def test_VerifyPOSIXGroup(self): + users = self.get_obj() + users._verify = Mock() + entry = lxml.etree.Element("POSIXGroup", name="test") + self.assertEqual(users._verify.return_value, + users.VerifyPOSIXGroup(entry, [])) + + def test__verify(self): + users = self.get_obj() + users._existing = \ + dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest', + '/home/test', '/bin/zsh')), + POSIXGroup=dict(test=('test', 'x', 1000, []))) + + entry = lxml.etree.Element("POSIXUser", name="nonexistent") + self.assertFalse(users._verify(entry)) + self.assertEqual(entry.get("current_exists"), "false") + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Bogus", shell="/bin/bash", + home="/home/test") + self.assertFalse(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test") + self.assertTrue(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test", uid="1000", gid="1000") + self.assertTrue(users._verify(entry)) + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + gecos="Test McTest", shell="/bin/zsh", + home="/home/test", uid="1001") + self.assertFalse(users._verify(entry)) + + def test_Install(self): + users = self.get_obj() + users._install = Mock() + users._existing = MagicMock() + + + entries = [lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXGroup", name="test"), + lxml.etree.Element("POSIXUser", name="test2")] + states = dict() + + users.Install(entries, states) + self.assertItemsEqual(entries, states.keys()) + for state in states.values(): + self.assertEqual(state, users._install.return_value) + # need to verify two things about _install calls: + # 1) _install was called for each entry; + # 2) _install was called for all groups before any users + self.assertItemsEqual(users._install.call_args_list, + [call(e) for e in entries]) + users_started = False + for args in users._install.call_args_list: + if args[0][0].tag == "POSIXUser": + users_started = True + elif users_started: + assert False, "_install() called on POSIXGroup after installing one or more POSIXUsers" + + def test__install(self): + users = self.get_obj() + users._get_cmd = Mock() + users.cmd = Mock() + users.set_defaults = dict(POSIXUser=Mock(), POSIXGroup=Mock()) + users._existing = \ + dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest', + '/home/test', '/bin/zsh')), + POSIXGroup=dict(test=('test', 'x', 1000, []))) + + def reset(): + users._get_cmd.reset_mock() + users.cmd.reset_mock() + for setter in users.set_defaults.values(): + setter.reset_mock() + users.modified = [] + + reset() + entry = lxml.etree.Element("POSIXUser", name="test2") + self.assertTrue(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("add", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertIn(entry, users.modified) + + reset() + entry = lxml.etree.Element("POSIXUser", name="test") + self.assertTrue(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("mod", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertIn(entry, users.modified) + + reset() + users.cmd.run.side_effect = ExecutionError(None) + self.assertFalse(users._install(entry)) + users.set_defaults[entry.tag].assert_called_with(entry) + users._get_cmd.assert_called_with("mod", + users.set_defaults[entry.tag].return_value) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + self.assertNotIn(entry, users.modified) + + def test__get_cmd(self): + users = self.get_obj() + + entry = lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest") + m1 = lxml.etree.SubElement(entry, "MemberOf") + m1.text = "wheel" + m2 = lxml.etree.SubElement(entry, "MemberOf") + m2.text = "users" + + cases = [(lxml.etree.Element("POSIXGroup", name="test"), []), + (lxml.etree.Element("POSIXGroup", name="test", gid="1001"), + ["-g", "1001"]), + (lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest"), + ["-m", "-g", "test", "-d", "/home/test", "-s", "/bin/zsh", + "-c", "Test McTest"]), + (lxml.etree.Element("POSIXUser", name="test", group="test", + home="/home/test", shell="/bin/zsh", + gecos="Test McTest", uid="1001"), + ["-m", "-u", "1001", "-g", "test", "-d", "/home/test", + "-s", "/bin/zsh", "-c", "Test McTest"]), + (entry, + ["-m", "-g", "test", "-G", "wheel,users", "-d", "/home/test", + "-s", "/bin/zsh", "-c", "Test McTest"])] + for entry, expected in cases: + for action in ["add", "mod", "del"]: + actual = users._get_cmd(action, entry) + if entry.tag == "POSIXGroup": + etype = "group" + else: + etype = "user" + self.assertEqual(actual[0], "/usr/sbin/%s%s" % (etype, action)) + self.assertEqual(actual[-1], entry.get("name")) + if action != "del": + self.assertItemsEqual(actual[1:-1], expected) + + @patch("grp.getgrnam") + def test_Remove(self, mock_getgrnam): + users = self.get_obj() + users._remove = Mock() + users.FindExtra = Mock() + users._existing = MagicMock() + users.extra = MagicMock() + + def reset(): + users._remove.reset_mock() + users.FindExtra.reset_mock() + users._existing = MagicMock() + users.extra = MagicMock() + mock_getgrnam.reset_mock() + + entries = [lxml.etree.Element("POSIXUser", name="test"), + lxml.etree.Element("POSIXGroup", name="test"), + lxml.etree.Element("POSIXUser", name="test2")] + + users.Remove(entries) + self.assertIsNone(users._existing) + users.FindExtra.assert_called_with() + self.assertEqual(users.extra, users.FindExtra.return_value) + mock_getgrnam.assert_called_with("test") + # need to verify two things about _remove calls: + # 1) _remove was called for each entry; + # 2) _remove was called for all users before any groups + self.assertItemsEqual(users._remove.call_args_list, + [call(e) for e in entries]) + groups_started = False + for args in users._remove.call_args_list: + if args[0][0].tag == "POSIXGroup": + groups_started = True + elif groups_started: + assert False, "_remove() called on POSIXUser after removing one or more POSIXGroups" + + reset() + mock_getgrnam.side_effect = KeyError + users.Remove(entries) + self.assertIsNone(users._existing) + users.FindExtra.assert_called_with() + self.assertEqual(users.extra, users.FindExtra.return_value) + mock_getgrnam.assert_called_with("test") + self.assertItemsEqual(users._remove.call_args_list, + [call(e) for e in entries + if e.tag == "POSIXUser"]) + + def test__remove(self): + users = self.get_obj() + users._get_cmd = Mock() + users.cmd = Mock() + + def reset(): + users._get_cmd.reset_mock() + users.cmd.reset_mock() + + + entry = lxml.etree.Element("POSIXUser", name="test2") + self.assertTrue(users._remove(entry)) + users._get_cmd.assert_called_with("del", entry) + users.cmd.run.assert_called_with(users._get_cmd.return_value) + + reset() + users.cmd.run.side_effect = ExecutionError(None) + self.assertFalse(users._remove(entry)) + users._get_cmd.assert_called_with("del", entry) + users.cmd.run.assert_called_with(users._get_cmd.return_value) diff --git a/tools/README b/tools/README index 400cfc55c..335363898 100644 --- a/tools/README +++ b/tools/README @@ -82,6 +82,10 @@ pkgmgr_update.py - Update Pkgmgr XML files from a list of directories that contain RPMS +posixusers_baseline.py + - Create a Bundle with all base POSIXUser/POSIXGroup entries on a + client. + rpmlisting.py - Generate Pkgmgr XML files for RPM packages diff --git a/tools/posixusers_baseline.py b/tools/posixusers_baseline.py new file mode 100755 index 000000000..a4abca42d --- /dev/null +++ b/tools/posixusers_baseline.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import grp +import sys +import logging +import lxml.etree +import Bcfg2.Logger +from Bcfg2.Client.Tools.POSIXUsers import POSIXUsers +from Bcfg2.Options import OptionParser, Option, get_bool, CLIENT_COMMON_OPTIONS + + +def get_setup(): + optinfo = CLIENT_COMMON_OPTIONS + optinfo['nouids'] = Option("Do not include UID numbers for users", + default=False, + cmd='--no-uids', + long_arg=True, + cook=get_bool) + optinfo['nogids'] = Option("Do not include GID numbers for groups", + default=False, + cmd='--no-gids', + long_arg=True, + cook=get_bool) + setup = OptionParser(optinfo) + setup.parse(sys.argv[1:]) + + if setup['args']: + print("posixuser_[baseline.py takes no arguments, only options") + print(setup.buildHelpMessage()) + raise SystemExit(1) + level = 30 + if setup['verbose']: + level = 20 + if setup['debug']: + level = 0 + Bcfg2.Logger.setup_logging('posixusers_baseline.py', + to_syslog=False, + level=level, + to_file=setup['logging']) + return setup + + +def main(): + setup = get_setup() + if setup['file']: + config = lxml.etree.parse(setup['file']).getroot() + else: + config = lxml.etree.Element("Configuration") + users = POSIXUsers(logging.getLogger('posixusers_baseline.py'), + setup, config) + + baseline = lxml.etree.Element("Bundle", name="posixusers_baseline") + for entry in users.FindExtra(): + data = users.existing[entry.tag][entry.get("name")] + for attr, idx in users.attr_mapping[entry.tag].items(): + if (entry.get(attr) or + (attr == 'uid' and setup['nouids']) or + (attr == 'gid' and setup['nogids'])): + continue + entry.set(attr, str(data[idx])) + if entry.tag == 'POSIXUser': + entry.set("group", grp.getgrgid(data[3])[0]) + for group in users.user_supplementary_groups(entry): + memberof = lxml.etree.SubElement(entry, "MemberOf") + memberof.text = group[0] + + entry.tag = "Bound" + entry.tag + baseline.append(entry) + + print(lxml.etree.tostring(baseline, pretty_print=True)) + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.2.3-1-g7c22 From 2983b0c358ef25e7c34ccdeb3ab1f8d6a6f9ae90 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 3 Dec 2012 10:51:55 -0600 Subject: testsuite: fixed assertXMLEqual comparison of XML attribute values --- .../Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py | 4 ++-- testsuite/common.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py index 58f844b3b..92d710f7d 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py @@ -88,7 +88,7 @@ class TestCfgBaseFileMatcher(TestSpecificData): mock_get_regex.reset_mock() match.reset_mock() match.return_value = True - self.assertTrue(self.test_obj.handles(evt)) + self.assertTrue(self.test_obj.handles(evt)) match.assert_called_with(evt.filename) else: match.return_value = False @@ -389,7 +389,7 @@ class TestCfgEntrySet(TestEntrySet): eset.bind_info_to_entry.assert_called_with(entry, metadata) eset._generate_data.assert_called_with(entry, metadata) self.assertFalse(eset._validate_data.called) - expected = lxml.etree.Element("Path", name="/text.txt") + expected = lxml.etree.Element("Path", name="/test.txt") expected.text = "data" self.assertXMLEqual(bound, expected) self.assertEqual(bound, entry) diff --git a/testsuite/common.py b/testsuite/common.py index 0cb457461..e26d0be61 100644 --- a/testsuite/common.py +++ b/testsuite/common.py @@ -267,7 +267,7 @@ class Bcfg2TestCase(unittest.TestCase): attributes. """ self.assertEqual(el1.tag, el2.tag, msg=msg) self.assertEqual(el1.text, el2.text, msg=msg) - self.assertItemsEqual(el1.attrib, el2.attrib, msg=msg) + self.assertItemsEqual(el1.attrib.items(), el2.attrib.items(), msg=msg) self.assertEqual(len(el1.getchildren()), len(el2.getchildren())) for child1 in el1.getchildren(): @@ -275,10 +275,11 @@ class Bcfg2TestCase(unittest.TestCase): self.assertIsNotNone(cname, msg="Element %s has no 'name' attribute" % child1.tag) - children2 = el2.xpath("*[@name='%s']" % cname) + children2 = el2.xpath("%s[@name='%s']" % (child1.tag, cname)) self.assertEqual(len(children2), 1, - msg="More than one element named %s" % cname) - self.assertXMLEqual(child1, children2[0], msg=msg) + msg="More than one %s element named %s" % \ + (child1.tag, cname)) + self.assertXMLEqual(child1, children2[0], msg=msg) class DBModelTestCase(Bcfg2TestCase): -- cgit v1.2.3-1-g7c22