diff options
43 files changed, 901 insertions, 458 deletions
diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index ba02b929d..829130ee4 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -35,24 +35,24 @@ templating -- see below). Group-Specific Files ==================== -It is often that you want one version of a config file for all of your -machines except those in a particular group. For example, ``/etc/fstab`` -should look alike on all of your desktop machines, but should be -different on your file servers. Bcfg2 can handle this case through use -of group-specific files. +It is often the case that you want one version of a config file for +all of your machines except those in a particular group. For example, +``/etc/fstab`` should look alike on all of your desktop machines, but +should be different on your file servers. Bcfg2 can handle this case +through use of group-specific files. As mentioned above, all Cfg entries live in like-named directories at the end of their directory tree. In the case of fstab, the file at ``Cfg/etc/fstab/fstab`` will be handed out by default to any client that asks for a copy of ``/etc/fstab``. Group-specific files are located in -the same directory and are named with the syntax:: +the same directory and are named with the following syntax:: /path/to/filename/filename.GNN_groupname -in which **NN** is a priority number where **00** is lowest and -**99** is highest, and **groupname** is the name of a group defined in +**NN** is a priority number where **00** is lowest and **99** +is highest, and **groupname** is the name of a group defined in ``Metadata/groups.xml``. Back to our fstab example, we might have a -``Cfg/etc/fstab/`` directory that looks like:: +``Cfg/etc/fstab/`` directory that looks like this:: fstab fstab.G50_server @@ -139,6 +139,84 @@ using different host-specific or group-specific files. For example: Cfg/etc/fstab/fstab.H_host.example.com.genshi Cfg/etc/fstab/fstab.G50_server.cheetah +Encrypted Files +=============== + +.. versionadded:: 1.3.0 + +Bcfg2 allows you to encrypt files stored in ``Cfg/`` to protect the +data in them from other people who need access to the repository. + +.. note:: + + This feature is *not* intended to secure the files against a + malicious attacker who has gained access to your Bcfg2 server, as + the encryption passphrases are held in plaintext in + ``bcfg2.conf``. This is only intended to make it easier to use a + single Bcfg2 repository with multiple admins who should not + necessarily have access to each other's sensitive data. + +Encrypting Files +---------------- + +An encrypted file should end with ``.crypt``, e.g.:: + + Cfg/etc/foo.conf + Cfg/etc/foo.conf/foo.conf.crypt + Cfg/etc/foo.conf/foo.conf.G10_foo.crypt + +Encrypted Genshi or Cheetah templates can have the extensions in +either order, e.g.:: + + Cfg/etc/foo.conf/foo.conf.crypt.genshi + Cfg/etc/foo.conf/foo.conf.G10_foo.genshi.crypt + Cfg/etc/foo.conf/foo.conf.H_bar.example.com.crypt.cheetah + +To encrypt a file, you can run:: + + openssl enc -aes-256-cbc -k <passphrase> -in foo.conf -out foo.conf.crypt -a + +Once you are satisfied that the file has been encrypted as you wish, +you can remove the plaintext version. + +To decrypt a file, you can run:: + + openssl enc -d -aes-256-cbc -k <passphrase> -in foo.conf.crypt -out foo.conf -a + +Configuring Encryption +---------------------- + +To configure encryption, add a ``[cfg:encryption]`` section to +``bcfg2.conf`` with any number of name-passphrase pairs. When +decrypting a file, _all_ passphrases will be tried; the passphrase +name is currently purely cosmetic, but at some point in the future the +ability to give Bcfg2 a "hint" about which passphrase to use will be +added. + +For instance:: + + [cfg:encryption] + foo_team=P4ssphr4se + bar_team=Pa55phra5e + +This would define two separate encryption passphrases, presumably for +use by two separate teams. The passphrase names are completely +arbitrary. + +Note that this does entail a chicken-and-egg problem. In order for +the Bcfg2 server to be able to decrypt encrypted files, the +passphrases must exist in ``bcfg2.conf`` in plaintext; but, if you're +encrypting data, presumably you don't want to include those plaintext +passphrases in your Bcfg2 repository, so you'll want to encrypt +``bcfg2.conf``. The best way to solve this is: + +#. On your Bcfg2 server, manually add the ``[cfg:encryption]`` section + to ``bcfg2.conf`` and restart the Bcfg2 server. +#. Update ``bcfg2.conf`` in your Bcfg2 repository with the + passphrases, and encrypt it. + +The first (manual) step breaks the mutual dependency. + Deltas ====== @@ -237,13 +315,19 @@ For ``sudoers``, a very simple validator is:: This uses the ``visudo`` command's built-in validation. -.. note: +If you wish to disable validation, this can be done with the following +setting in ``bcfg2.conf``:: + + [cfg] + validation=no - Before 1.3 is released, it will be possible to disable validation - in the configuration, but enable it for ``bcfg2-test``. This is - recommended for heavily-used servers, since running an external - command is fairly resource intensive and could quickly exhaust the - file descriptors of a server. +If you have a very large number of validators, you may wish to disable +validation by default to avoid slowing down the generation of +configurations on the server, and use ``bcfg2-test`` (for instance, as +a post-commit hook or as part of a code review process) to run +validation. You can do this by setting ``validation=no`` in +``bcfg2.conf`` as described above, and then calling ``bcfg2-test`` +with the ``--cfg-validation`` flag. File permissions ================ diff --git a/doc/server/plugins/generators/packages.txt b/doc/server/plugins/generators/packages.txt index 62574be76..3c1d304c1 100644 --- a/doc/server/plugins/generators/packages.txt +++ b/doc/server/plugins/generators/packages.txt @@ -126,17 +126,19 @@ Disabling dependency resolution .. versionadded:: 1.1.0 -Dependency resolution can be disabled by adding this to -``Packages/packages.conf`` in the ``global`` section:: +Dependency resolution can be disabled by adding the following setting +to ``bcfg2.conf`` in the ``packages`` section:: - [global] + [packages] resolver=0 All metadata processing can be disabled as well:: - [global] + [packages] metadata=0 +This setting implies disabling the resolver. + Blacklisting faulty dependencies -------------------------------- @@ -353,15 +355,6 @@ be validated using ``bcfg2-lint``. .. note:: The schema requires that elements be specified in the above order. -Limitations -=========== - -Packages does not do traditional caching as other plugins -do. Modifying sources in the Packages ``sources.xml`` file requires a -server restart for the time being. You do not have to restart the -server after changing ``packages.conf`` or after adding new sources to -``sources.xml``. - Package Checking and Verification ================================= @@ -380,10 +373,10 @@ Generating Client APT/Yum Configurations .. versionadded:: 1.2.0 The Packages plugin has native support for generating Yum configs. -You must set ``yum_config`` in ``Packages/packages.conf`` to the path -to the yum config file you want to generate:: +You must set ``yum_config`` in ``bcfg2.conf`` to the path to the yum +config file you want to generate:: - [global] + [packages] yum_config=/etc/yum.repos.d/all.repo Then add the corresponding Path entry to your Yum bundle. @@ -414,7 +407,7 @@ resolution and other routines so that the Bcfg2 server can be run on a host that does not support Yum itself. If you run the Bcfg2 server on a machine that does have Yum libraries, however, you can enable use of those native libraries in Bcfg2 by setting ``use_yum_libraries`` to -``1`` in the ``[yum]`` section of ``Packages/packages.conf``. +``1`` in the ``[packages:yum]`` section of ``bcfg2.conf``. Benefits to this include: @@ -440,10 +433,11 @@ Configuring the Yum Helper Due to poor memory management by the Yum API, the long-lived bcfg2-server process uses an external short-lived helper, ``bcfg2-yum-helper``, to do the actual Yum API calls for native yum -library support. By default, Bcfg2 looks for this helper at -``/usr/sbin/bcfg2-yum-helper``. If you have installed the helper -elsewhere, you will need to configure that location with the -``helper`` option in the ``[yum]`` section, e.g.:: +library support. By default, Bcfg2 looks for this helper in +``$PATH``, or, failing that, at ``/usr/sbin/bcfg2-yum-helper``. If +you have installed the helper elsewhere, you will need to configure +that location with the ``helper`` option in the ``[yum]`` section, +e.g.:: [yum] use_yum_libraries = 1 @@ -452,11 +446,11 @@ elsewhere, you will need to configure that location with the Setting Yum Options ------------------- -In ``Packages/packages.conf``, any options you set in the ``[yum]`` +In ``bcfg2.conf``, any options you set in the ``[packages:yum]`` section other than ``use_yum_libraries`` and ``helper`` will be passed along verbatim to the configuration of the Yum objects used in the -Bcfg2 server. The following options are set by default, and should -not generally be overridden: +Bcfg2 server. The following options are set by default, and should not +generally be overridden: * ``cachedir`` is set to a hashed value unique to each distinct Yum configuration. Don't set this unless you know what you're doing. @@ -521,8 +515,8 @@ server must have a valid ``/etc/pulp/consumer/consumer.conf`` that is readable by the user your Bcfg2 server runs as; the Pulp server, URLs, and so on, are determined from this. -Secondly, in ``Packages/packages.conf`` you must set the following -options in the ``[pulp]`` section: +Secondly, in ``bcfg2.conf`` you must set the following +options in the ``[packages:pulp]`` section: * ``username`` and ``password``: The username and password of a Pulp user that will be used to register new clients and bind them to @@ -643,26 +637,27 @@ multiple data sources need to be multiplexed. The APT source in ``src/lib/Server/Plugins/Packages.py`` provides a relatively simple implementation of a source. -packages.conf +Configuration ============= -``packages.conf`` contains miscellaneous configuration options for the +``bcfg2.conf`` contains miscellaneous configuration options for the Packages plugin. Any booleans in the config file accept the values "1", "yes", "true", and "on" for True, and "0", "no", "false", and -"off" for False +"off" for False. It understands the following directives: -[global] section +[packages] section ---------------- * ``resolver``: Enable dependency resolution. Default is ``1`` (true). For historical reasons, this also accepts "enabled" and "disabled". -* ``metadata``: Enable metadata processing. Default is ``1`` - (true). For historical reasons, this also accepts "enabled" and - "disabled". -* ``yum_config``: The path at which to generate Yum configs. No +* ``metadata``: Enable metadata processing. Default is ``1`` + (true). If ``metadata`` is disabled, it's implied that ``resolver`` + is also disabled. For historical reasons, this also accepts + "enabled" and "disabled". +* ``yum_config``: The path at which to generate Yum configs. No default. * ``apt_config``: The path at which to generate APT configs. No default. @@ -672,17 +667,20 @@ It understands the following directives: * ``version``: Set the version attribute used when binding Packages. Default is ``auto``. -[yum] section +[packages:yum] section ------------- * ``use_yum_libraries``: Whether or not to use the :ref:`native yum library support <native-yum-libraries>`. Default is ``0`` (false). +* ``helper``: Path to ``bcfg2-yum-helper``. By default, Bcfg2 looks + first in ``$PATH`` and then in ``/usr/sbin/bcfg2-yum-helper`` for + the helper. -All other options in the ``[yum]`` section will be passed along -verbatim to the Yum configuration if you are using the native Yum -library support. +All other options in the ``[packages:yum]`` section will be passed +along verbatim to the Yum configuration if you are using the native +Yum library support. -[pulp] section +[packages:pulp] section -------------- * ``username`` and ``password``: The username and password of a Pulp diff --git a/doc/server/plugins/generators/rules.txt b/doc/server/plugins/generators/rules.txt index c084c5681..03372b120 100644 --- a/doc/server/plugins/generators/rules.txt +++ b/doc/server/plugins/generators/rules.txt @@ -356,14 +356,11 @@ Using Regular Expressions in Rules If you wish, you can configure the Rules plugin to support regular expressions. This entails a small performance and memory usage -penalty. To do so, create a file, "Rules/rules.conf", and add the -following text:: +penalty. To do so, add the following setting to ``bcfg2.conf``:: [rules] regex = yes -You will have to restart the Bcfg2 server after making that change. - With regular expressions enabled, you can use a regex in the ``name`` attribute to match multiple abstract configuration entries. @@ -372,4 +369,4 @@ name="bcfg2".../>`` will *not* match a Service named ``bcfg2-server``; you'd have to explicitly specify ``<Service name="bcfg2.*".../>``. Note that only one Rule can apply to any abstract entry, so you cannot -specify multiple regexs to match the same rule. +specify multiple regexes to match the same rule. diff --git a/man/bcfg2.conf.5 b/man/bcfg2.conf.5 index 684586892..812a86c76 100644 --- a/man/bcfg2.conf.5 +++ b/man/bcfg2.conf.5 @@ -35,7 +35,13 @@ using the 'bcfg2-admin init' command. .TP .B filemonitor The file monitor used to watch for changes in the repository. -Values of 'gamin', 'fam', or 'pseudo' are valid. +Values of 'inotify', 'gamin', 'fam', or 'pseudo' are valid. The +default is the best available monitor. + +.TP +.B ignore_files +A comma-separated list of globs that should be ignored by the file +monitor. Default: '*~,.#*,*#,*.swp,SCCS,.svn,4913,.gitignore' .TP .B listen_all @@ -133,7 +139,7 @@ administrator supervision is available. (0.9.6 and later) \(bu .B Deps The Deps plugin allows you to make a series of assertions like -"Package X requires Package Y (and optionally also Package Z etc.) +"Package X requires Package Y (and optionally also Package Z etc.)" \(bu .B Editor @@ -441,6 +447,11 @@ sqlite The name of the database to use for statistics data. eg: $REPOSITORY_DIR/etc/bcfg2.sqlite +.SH PLUGIN-SPECIFIC OPTIONS + +Many plugins specify their own options in bcfg2.conf; see the online +documentation about each plugin for more information on these. + .SH SEE ALSO .BR bcfg2(1), .BR bcfg2-server(8) diff --git a/src/lib/Bcfg2/Client/Tools/YUMng.py b/src/lib/Bcfg2/Client/Tools/YUMng.py index 244b66cf4..da627ed7a 100644 --- a/src/lib/Bcfg2/Client/Tools/YUMng.py +++ b/src/lib/Bcfg2/Client/Tools/YUMng.py @@ -483,6 +483,7 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): _POs = POs if len(_POs) == 0: # Package (name, arch) not installed + entry.set('current_exists', 'false') self.logger.debug(" %s is not installed" % nevraString(nevra)) stat['installed'] = False package_fail = True @@ -513,6 +514,12 @@ class YUMng(Bcfg2.Client.Tools.PkgTool): package_fail = True stat['version_fail'] = True # Just chose the first pkg for the error message + entry.set('current_version', "%s-%s.%s" % (POs[0]['version'], + POs[0]['release'], + POs[0]['arch'])) + entry.set('version', "%s-%s.%s" % (nevra['version'], + nevra['release'], + nevra['arch'])) self.logger.info(" %s: Wrong version installed. " "Want %s, but have %s" % (entry.get("name"), nevraString(nevra), diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py new file mode 100755 index 000000000..62b22d7de --- /dev/null +++ b/src/lib/Bcfg2/Encryption.py @@ -0,0 +1,75 @@ +#!/usr/bin/python -Ott + +import os +import base64 +from M2Crypto import Rand +from M2Crypto.EVP import Cipher, EVPError +from Bcfg2.Bcfg2Py3k import StringIO + +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +ENCRYPT = 1 +DECRYPT = 0 +ALGORITHM = "aes_256_cbc" +IV = '\0' * 16 + +Rand.rand_seed(os.urandom(1024)) + +def _cipher_filter(cipher, instr): + inbuf = StringIO(instr) + outbuf = StringIO() + while 1: + buf = inbuf.read() + if not buf: + break + outbuf.write(cipher.update(buf)) + outbuf.write(cipher.final()) + rv = outbuf.getvalue() + inbuf.close() + outbuf.close() + return rv + +def str_encrypt(plaintext, key, iv=IV, algorithm=ALGORITHM, salt=None): + """ encrypt a string """ + cipher = Cipher(alg=algorithm, key=key, iv=iv, op=ENCRYPT, salt=salt) + return _cipher_filter(cipher, plaintext) + +def str_decrypt(crypted, key, iv=IV, algorithm=ALGORITHM): + """ decrypt a string """ + cipher = Cipher(alg=algorithm, key=key, iv=iv, op=DECRYPT) + return _cipher_filter(cipher, crypted) + +def ssl_decrypt(data, passwd, algorithm=ALGORITHM): + """ decrypt openssl-encrypted data """ + # base64-decode the data if necessary + try: + data = base64.b64decode(data) + except TypeError: + # already decoded + pass + + salt = data[8:16] + hashes = [md5(passwd + salt).digest()] + for i in range(1,3): + hashes.append(md5(hashes[i-1] + passwd + salt).digest()) + key = hashes[0] + hashes[1] + iv = hashes[2] + + return str_decrypt(data[16:], key=key, iv=iv) + +def ssl_encrypt(plaintext, passwd, algorithm=ALGORITHM, salt=None): + """ encrypt data in a format that is openssl compatible """ + if salt is None: + salt = Rand.rand_bytes(8) + + hashes = [md5(passwd + salt).digest()] + for i in range(1,3): + hashes.append(md5(hashes[i-1] + passwd + salt).digest()) + key = hashes[0] + hashes[1] + iv = hashes[2] + + crypted = str_encrypt(plaintext, key=key, salt=salt, iv=iv) + return base64.b64encode("Salted__" + salt + crypted) + "\n" diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index dfb062341..129a6c31e 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -1,10 +1,11 @@ """Option parsing library for utilities.""" -import getopt import re import os import sys +import copy import shlex +import getopt import Bcfg2.Client.Tools # Compatibility imports from Bcfg2.Bcfg2Py3k import ConfigParser @@ -149,6 +150,7 @@ class OptionSet(dict): self.hm = self.buildHelpMessage() if 'configfile' in kwargs: self.cfile = kwargs['configfile'] + os.environ['BCFG2_CONFIG_FILE'] = self.cfile else: self.cfile = DEFAULT_CONFIG_LOCATION self.cfp = DefaultConfigParser() @@ -307,6 +309,10 @@ SERVER_MCONNECT = Option('Server Metadata Connector list', cook=list_split, cf=('server', 'connectors'), default=['Probes'], ) SERVER_FILEMONITOR = Option('Server file monitor', cf=('server', 'filemonitor'), default='default', odesc='File monitoring driver') +SERVER_FAM_IGNORE = Option('File globs to ignore', + cf=('server', 'ignore_files'), cook=list_split, + default=['*~', '.#*', '*#', '*.swp', 'SCCS', '.svn', + '4913', '.gitignore']) SERVER_LISTEN_ALL = Option('Listen on all interfaces', cf=('server', 'listen_all'), cmd='--listen-all', @@ -425,10 +431,14 @@ class OptionParser(OptionSet): self.Bootstrap = OptionSet([('configfile', CFILE)], quiet=True) self.Bootstrap.parse(sys.argv[1:], do_getopt=False) OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile']) - self.optinfo = args + self.optinfo = copy.copy(args) def HandleEvent(self, event): - if not self['configfile'].endswith(event.filename): + if 'configfile' not in self or not isinstance(self['configfile'], str): + # we haven't parsed options yet, or CFILE wasn't included + # in the options + return + if event.filename != self['configfile']: print("Got event for unknown file: %s" % event.filename) return if event.code2str() == 'deleted': @@ -447,3 +457,10 @@ class OptionParser(OptionSet): self.do_getopt = do_getopt OptionSet.parse(self, self.argv, do_getopt=self.do_getopt) + def add_option(self, name, opt): + self[name] = opt + self.optinfo[name] = opt + + def update(self, optdict): + dict.update(self, optdict) + self.optinfo.update(optdict) diff --git a/src/lib/Bcfg2/Server/Admin/Reports.py b/src/lib/Bcfg2/Server/Admin/Reports.py index 974cdff9d..d38889b52 100644 --- a/src/lib/Bcfg2/Server/Admin/Reports.py +++ b/src/lib/Bcfg2/Server/Admin/Reports.py @@ -95,8 +95,8 @@ class Reports(Bcfg2.Server.Admin.Mode): " scrub Scrub the database for duplicate reasons and orphaned entries\n" " update Apply any updates to the reporting database\n" "\n" - " Django commands:\n " - "\n ".join(django_commands)) + " Django commands:\n " \ + + "\n ".join(django_commands)) def __init__(self, setup): Bcfg2.Server.Admin.Mode.__init__(self, setup) diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 8482925b7..c8ef5b1f7 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -16,6 +16,7 @@ except ImportError: from Bcfg2.Component import Component, exposed from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError +import Bcfg2.Server import Bcfg2.Server.FileMonitor import Bcfg2.Server.Plugins.Metadata # Compatibility imports @@ -66,17 +67,28 @@ class Core(Component): filemonitor='default', start_fam_thread=False): Component.__init__(self) self.datastore = repo - if filemonitor not in Bcfg2.Server.FileMonitor.available: + + try: + fm = Bcfg2.Server.FileMonitor.available[filemonitor] + except KeyError: logger.error("File monitor driver %s not available; " "forcing to default" % filemonitor) - filemonitor = 'default' + fm = Bcfg2.Server.FileMonitor.available['default'] + famargs = dict(ignore=[], debug=False) + if 'ignore' in setup: + famargs['ignore'] = setup['ignore'] + if 'debug' in setup: + famargs['debug'] = setup['debug'] + elif 'event debug' in setup: + famargs['debug'] = setup['event debug'] try: - self.fam = Bcfg2.Server.FileMonitor.available[filemonitor]() + self.fam = fm(**famargs) except IOError: logger.error("Failed to instantiate fam driver %s" % filemonitor, exc_info=1) - raise CoreInitError("failed to instantiate fam driver (used %s)" % \ + raise CoreInitError("Failed to instantiate fam driver (used %s)" % filemonitor) + self.pubspace = {} self.cfile = cfile self.cron = {} @@ -197,6 +209,7 @@ class Core(Component): """Shutting down the plugins.""" if not self.terminate.isSet(): self.terminate.set() + self.fam.shutdown() for plugin in list(self.plugins.values()): plugin.shutdown() @@ -423,7 +436,8 @@ class Core(Component): # clear dynamic groups self.metadata.cgroups[meta.hostname] = [] try: - xpdata = lxml.etree.XML(probedata.encode('utf-8')) + xpdata = lxml.etree.XML(probedata.encode('utf-8'), + parser=Bcfg2.Server.XMLParser) except: self.logger.error("Failed to parse probe data from client %s" % \ (address[0])) @@ -472,7 +486,8 @@ class Core(Component): @exposed def RecvStats(self, address, stats): """Act on statistics upload.""" - sdata = lxml.etree.XML(stats.encode('utf-8')) + sdata = lxml.etree.XML(stats.encode('utf-8'), + parser=Bcfg2.Server.XMLParser) client = self.metadata.resolve_client(address) self.process_statistics(client, sdata) return "<ok/>" diff --git a/src/lib/Bcfg2/Server/FileMonitor.py b/src/lib/Bcfg2/Server/FileMonitor.py deleted file mode 100644 index d6b313e6b..000000000 --- a/src/lib/Bcfg2/Server/FileMonitor.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Bcfg2.Server.FileMonitor provides the support for monitorung files.""" - -import logging -import os -import stat -from time import sleep, time - -logger = logging.getLogger('Bcfg2.Server.FileMonitor') - - -def ShouldIgnore(event): - """Test if the event should be suppresed.""" - # FIXME should move event suppression out of the core - if event.filename.split('/')[-1] == '.svn': - return True - if event.filename.endswith('~') or \ - event.filename.startswith('#') or event.filename.startswith('.#'): - #logger.error("Suppressing event for file %s" % (event.filename)) - return True - return False - - -class Event(object): - def __init__(self, request_id, filename, code): - self.requestID = request_id - self.filename = filename - self.action = code - - def code2str(self): - """return static code for event""" - return self.action - -available = {} - - -class FileMonitor(object): - """File Monitor baseclass.""" - def __init__(self, debug=False): - object.__init__(self) - self.debug = debug - self.handles = dict() - - def get_event(self): - return None - - def pending(self): - return False - - def fileno(self): - return 0 - - def handle_one_event(self, event): - if ShouldIgnore(event): - return - if event.requestID not in self.handles: - logger.info("Got event for unexpected id %s, file %s" % - (event.requestID, event.filename)) - return - if self.debug: - logger.info("Dispatching event %s %s to obj %s" \ - % (event.code2str(), event.filename, - self.handles[event.requestID])) - try: - self.handles[event.requestID].HandleEvent(event) - except: - logger.error("error in handling of gamin event for %s" % \ - (event.filename), exc_info=1) - - def handle_event_set(self, lock=None): - count = 1 - event = self.get_event() - start = time() - if lock: - lock.acquire() - try: - self.handle_one_event(event) - while self.pending(): - self.handle_one_event(self.get_event()) - count += 1 - except: - pass - if lock: - lock.release() - end = time() - logger.info("Handled %d events in %.03fs" % (count, (end - start))) - - def handle_events_in_interval(self, interval): - end = time() + interval - while time() < end: - if self.pending(): - self.handle_event_set() - end = time() + interval - else: - sleep(0.5) - - -class FamFam(object): - """The fam object is a set of callbacks for - file alteration events (FAM support). - """ - - def __init__(self): - object.__init__(self) - self.fm = _fam.open() - self.users = {} - self.handles = {} - self.debug = False - - def fileno(self): - """Return fam file handle number.""" - return self.fm.fileno() - - def handle_event_set(self, _): - self.Service() - - def handle_events_in_interval(self, interval): - now = time() - while (time() - now) < interval: - if self.Service(): - now = time() - - def AddMonitor(self, path, obj): - """Add a monitor to path, installing a callback to obj.HandleEvent.""" - mode = os.stat(path)[stat.ST_MODE] - if stat.S_ISDIR(mode): - handle = self.fm.monitorDirectory(path, None) - else: - handle = self.fm.monitorFile(path, None) - self.handles[handle.requestID()] = handle - if obj != None: - self.users[handle.requestID()] = obj - return handle.requestID() - - def Service(self, interval=0.50): - """Handle all fam work.""" - count = 0 - collapsed = 0 - rawevents = [] - start = time() - now = time() - while (time() - now) < interval: - if self.fm.pending(): - while self.fm.pending(): - count += 1 - rawevents.append(self.fm.nextEvent()) - now = time() - unique = [] - bookkeeping = [] - for event in rawevents: - if ShouldIgnore(event): - continue - if event.code2str() != 'changed': - # process all non-change events - unique.append(event) - else: - if (event.filename, event.requestID) not in bookkeeping: - bookkeeping.append((event.filename, event.requestID)) - unique.append(event) - else: - collapsed += 1 - for event in unique: - if event.requestID in self.users: - try: - self.users[event.requestID].HandleEvent(event) - except: - logger.error("handling event for file %s" % (event.filename), exc_info=1) - end = time() - logger.info("Processed %s fam events in %03.03f seconds. %s coalesced" % - (count, (end - start), collapsed)) - return count - - -class Fam(FileMonitor): - """ - The fam object is a set of callbacks for - file alteration events (FAM support). - """ - - def __init__(self, debug=False): - FileMonitor.__init__(self, debug) - self.fm = _fam.open() - - def fileno(self): - return self.fm.fileno() - - def AddMonitor(self, path, obj): - """Add a monitor to path, installing a callback to obj.HandleEvent.""" - mode = os.stat(path)[stat.ST_MODE] - if stat.S_ISDIR(mode): - handle = self.fm.monitorDirectory(path, None) - else: - handle = self.fm.monitorFile(path, None) - if obj != None: - self.handles[handle.requestID()] = obj - return handle.requestID() - - def pending(self): - return self.fm.pending() - - def get_event(self): - return self.fm.nextEvent() - - -class Pseudo(FileMonitor): - """ - The fam object is a set of callbacks for - file alteration events (static monitor support). - """ - - def __init__(self, debug=False): - FileMonitor.__init__(self, debug=False) - self.pending_events = [] - - def pending(self): - return len(self.pending_events) != 0 - - def get_event(self): - return self.pending_events.pop() - - def AddMonitor(self, path, obj): - """add a monitor to path, installing a callback to obj.HandleEvent""" - handleID = len(list(self.handles.keys())) - mode = os.stat(path)[stat.ST_MODE] - handle = Event(handleID, path, 'exists') - if stat.S_ISDIR(mode): - dirList = os.listdir(path) - self.pending_events.append(handle) - for includedFile in dirList: - self.pending_events.append(Event(handleID, - includedFile, - 'exists')) - self.pending_events.append(Event(handleID, path, 'endExist')) - else: - self.pending_events.append(Event(handleID, path, 'exists')) - if obj != None: - self.handles[handleID] = obj - return handleID - - -try: - from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \ - GAMChanged, GAMDeleted, GAMMoved - - class GaminEvent(Event): - """ - This class provides an event analogous to - python-fam events based on gamin sources. - """ - def __init__(self, request_id, filename, code): - Event.__init__(self, request_id, filename, code) - action_map = {GAMCreated: 'created', GAMExists: 'exists', - GAMChanged: 'changed', GAMDeleted: 'deleted', - GAMEndExist: 'endExist', GAMMoved: 'moved'} - if code in action_map: - self.action = action_map[code] - - class Gamin(FileMonitor): - """ - The fam object is a set of callbacks for - file alteration events (Gamin support) - """ - def __init__(self, debug=False): - FileMonitor.__init__(self, debug) - self.mon = WatchMonitor() - self.counter = 0 - self.events = [] - - def fileno(self): - return self.mon.get_fd() - - def queue(self, path, action, request_id): - """queue up the event for later handling""" - self.events.append(GaminEvent(request_id, path, action)) - - def AddMonitor(self, path, obj): - """Add a monitor to path, installing a callback to obj.HandleEvent.""" - handle = self.counter - self.counter += 1 - mode = os.stat(path)[stat.ST_MODE] - - # Flush queued gamin events - while self.mon.event_pending(): - self.mon.handle_one_event() - - if stat.S_ISDIR(mode): - self.mon.watch_directory(path, self.queue, handle) - else: - self.mon.watch_file(path, self.queue, handle) - self.handles[handle] = obj - return handle - - def pending(self): - return len(self.events) > 0 or self.mon.event_pending() - - def get_event(self): - if self.mon.event_pending(): - self.mon.handle_one_event() - return self.events.pop(0) - - available['gamin'] = Gamin -except ImportError: - # fall back to _fam - pass - -try: - import _fam - available['fam'] = FamFam -except ImportError: - pass -available['pseudo'] = Pseudo - -for fdrv in ['gamin', 'fam', 'pseudo']: - if fdrv in available: - available['default'] = available[fdrv] - break diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py new file mode 100644 index 000000000..1a00fffa0 --- /dev/null +++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py @@ -0,0 +1,82 @@ +""" Fam provides FAM support for file alteration events """ + +import os +import _fam +import stat +import logging +from time import time +from Bcfg2.Server.FileMonitor import FileMonitor + +logger = logging.getLogger(__name__) + +class Fam(FileMonitor): + __priority__ = 90 + + def __init__(self, ignore=None, debug=False): + FileMonitor.__init__(self, ignore=ignore, debug=debug) + self.fm = _fam.open() + self.users = {} + + def fileno(self): + """Return fam file handle number.""" + return self.fm.fileno() + + def handle_event_set(self, _): + self.Service() + + def handle_events_in_interval(self, interval): + now = time() + while (time() - now) < interval: + if self.Service(): + now = time() + + def AddMonitor(self, path, obj): + """Add a monitor to path, installing a callback to obj.HandleEvent.""" + mode = os.stat(path)[stat.ST_MODE] + if stat.S_ISDIR(mode): + handle = self.fm.monitorDirectory(path, None) + else: + handle = self.fm.monitorFile(path, None) + self.handles[handle.requestID()] = handle + if obj != None: + self.users[handle.requestID()] = obj + return handle.requestID() + + def Service(self, interval=0.50): + """Handle all fam work.""" + count = 0 + collapsed = 0 + rawevents = [] + start = time() + now = time() + while (time() - now) < interval: + if self.fm.pending(): + while self.fm.pending(): + count += 1 + rawevents.append(self.fm.nextEvent()) + now = time() + unique = [] + bookkeeping = [] + for event in rawevents: + if self.should_ignore(event): + continue + if event.code2str() != 'changed': + # process all non-change events + unique.append(event) + else: + if (event.filename, event.requestID) not in bookkeeping: + bookkeeping.append((event.filename, event.requestID)) + unique.append(event) + else: + collapsed += 1 + for event in unique: + if event.requestID in self.users: + try: + self.users[event.requestID].HandleEvent(event) + except: + logger.error("Handling event for file %s" % event.filename, + exc_info=1) + end = time() + logger.info("Processed %s fam events in %03.03f seconds. %s coalesced" % + (count, (end - start), collapsed)) + return count diff --git a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py new file mode 100644 index 000000000..60f80c9c3 --- /dev/null +++ b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py @@ -0,0 +1,64 @@ +""" Gamin driver for file alteration events """ + +import os +import stat +import logging +from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \ + GAMChanged, GAMDeleted +from Bcfg2.Server.FileMonitor import Event, FileMonitor + +logger = logging.getLogger(__name__) + +class GaminEvent(Event): + """ + This class provides an event analogous to + python-fam events based on gamin sources. + """ + action_map = {GAMCreated: 'created', GAMExists: 'exists', + GAMChanged: 'changed', GAMDeleted: 'deleted', + GAMEndExist: 'endExist'} + + def __init__(self, request_id, filename, code): + Event.__init__(self, request_id, filename, code) + if code in self.action_map: + self.action = self.action_map[code] + +class Gamin(FileMonitor): + __priority__ = 10 + + def __init__(self, ignore=None, debug=False): + FileMonitor.__init__(self, ignore=ignore, debug=debug) + self.mon = WatchMonitor() + self.counter = 0 + + def fileno(self): + return self.mon.get_fd() + + def queue(self, path, action, request_id): + """queue up the event for later handling""" + self.events.append(GaminEvent(request_id, path, action)) + + def AddMonitor(self, path, obj): + """Add a monitor to path, installing a callback to obj.""" + handle = self.counter + self.counter += 1 + mode = os.stat(path)[stat.ST_MODE] + + # Flush queued gamin events + while self.mon.event_pending(): + self.mon.handle_one_event() + + if stat.S_ISDIR(mode): + self.mon.watch_directory(path, self.queue, handle) + else: + self.mon.watch_file(path, self.queue, handle) + self.handles[handle] = obj + return handle + + def pending(self): + return FileMonitor.pending(self) or self.mon.event_pending() + + def get_event(self): + if self.mon.event_pending(): + self.mon.handle_one_event() + return FileMonitor.get_event(self) diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py new file mode 100644 index 000000000..9743d868d --- /dev/null +++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py @@ -0,0 +1,60 @@ +""" Inotify driver for file alteration events """ + +import os +import stat +import logging +import operator +import pyinotify +from Bcfg2.Server.FileMonitor import FileMonitor, Event + +logger = logging.getLogger(__name__) + +class InotifyEvent(Event): + action_map = {pyinotify.IN_CREATE: 'created', + pyinotify.IN_DELETE: 'deleted', + pyinotify.IN_MODIFY: 'changed'} + + def __init__(self, event): + Event.__init__(self, event.wd, event.pathname, event.mask) + if event.mask in self.action_map: + self.action = self.action_map[event.mask] + + +class Inotify(FileMonitor, pyinotify.ProcessEvent): + __priority__ = 1 + mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY + + def __init__(self, ignore=None, debug=False): + FileMonitor.__init__(self, ignore=ignore, debug=debug) + self.wm = pyinotify.WatchManager() + self.notifier = pyinotify.ThreadedNotifier(self.wm, self) + self.notifier.start() + + def fileno(self): + return self.wm.get_fd() + + def process_default(self, event): + self.events.append(InotifyEvent(event)) + + def AddMonitor(self, path, obj): + res = self.wm.add_watch(path, self.mask, quiet=False) + if not res: + # if we didn't get a return, but we also didn't get an + # exception, we're already watching this directory, so we + # need to find the watch descriptor for it + for wd, watch in self.wm.watches.items(): + if watch.path == path: + wd = watch.wd + else: + wd = res[path] + self.handles[wd] = obj + self.events.append(Event(wd, path, "exists")) + + mode = os.stat(path)[stat.ST_MODE] + if stat.S_ISDIR(mode): + for wname in os.listdir(path): + self.events.append(Event(wd, wname, "exists")) + return wd + + def shutdown(self): + self.notifier.stop() diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py new file mode 100644 index 000000000..4c2d90250 --- /dev/null +++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py @@ -0,0 +1,30 @@ +""" Pseudo provides static monitor support for file alteration events """ + +import os +import stat +import logging +from Bcfg2.Server.FileMonitor import FileMonitor, Event + +logger = logging.getLogger(__name__) + +class Pseudo(FileMonitor): + __priority__ = 99 + + def AddMonitor(self, path, obj): + """add a monitor to path, installing a callback to obj.HandleEvent""" + handleID = len(list(self.handles.keys())) + mode = os.stat(path)[stat.ST_MODE] + handle = Event(handleID, path, 'exists') + if stat.S_ISDIR(mode): + dirList = os.listdir(path) + self.pending_events.append(handle) + for includedFile in dirList: + self.pending_events.append(Event(handleID, + includedFile, + 'exists')) + self.pending_events.append(Event(handleID, path, 'endExist')) + else: + self.pending_events.append(Event(handleID, path, 'exists')) + if obj != None: + self.handles[handleID] = obj + return handleID diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py new file mode 100644 index 000000000..40c3253b9 --- /dev/null +++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py @@ -0,0 +1,143 @@ +"""Bcfg2.Server.FileMonitor provides the support for monitoring files.""" + +import os +import fnmatch +import logging +import pkgutil +from time import sleep, time + +logger = logging.getLogger(__name__) + +class Event(object): + def __init__(self, request_id, filename, code): + self.requestID = request_id + self.filename = filename + self.action = code + + def code2str(self): + """return static code for event""" + return self.action + + def __str__(self): + return "%s: %s %s" % (self.__class__.__name__, + self.filename, self.action) + + def __repr__(self): + return "%s (request ID %s)" % (str(self), self.requestID) + + +class FileMonitor(object): + """File Monitor baseclass.""" + def __init__(self, ignore=None, debug=False): + object.__init__(self) + self.debug = debug + self.handles = dict() + self.events = [] + if ignore is None: + ignore = [] + self.ignore = ignore + + def __str__(self): + return "%s: %s" % (__name__, self.__class__.__name__) + + def __repr__(self): + if self.pending(): + events = "has" + else: + events = "no" + return "%s (%s events, fd %s)" % (str(self), events, self.fileno) + + def should_ignore(self, event): + for pattern in self.ignore: + if (fnmatch.fnmatch(event.filename, pattern) or + fnmatch.fnmatch(os.path.split(event.filename)[-1], pattern)): + if self.debug: + logger.info("Ignoring %s" % event) + return True + return False + + def pending(self): + return bool(self.events) + + def get_event(self): + return self.events.pop(0) + + def fileno(self): + return 0 + + def handle_one_event(self, event): + if self.should_ignore(event): + return + if event.requestID not in self.handles: + logger.info("Got event for unexpected id %s, file %s" % + (event.requestID, event.filename)) + return + if self.debug: + logger.info("Dispatching event %s %s to obj %s" % + (event.code2str(), event.filename, + self.handles[event.requestID])) + try: + self.handles[event.requestID].HandleEvent(event) + except: + logger.error("Error in handling of event for %s" % + event.filename, exc_info=1) + + def handle_event_set(self, lock=None): + count = 1 + event = self.get_event() + start = time() + if lock: + lock.acquire() + try: + self.handle_one_event(event) + while self.pending(): + self.handle_one_event(self.get_event()) + count += 1 + except: + pass + if lock: + lock.release() + end = time() + logger.info("Handled %d events in %.03fs" % (count, (end - start))) + + def handle_events_in_interval(self, interval): + end = time() + interval + while time() < end: + if self.pending(): + self.handle_event_set() + end = time() + interval + else: + sleep(0.5) + + def shutdown(self): + pass + + +available = dict() + +# todo: loading the monitor drivers should be automatic +from Bcfg2.Server.FileMonitor.Pseudo import Pseudo +available['pseudo'] = Pseudo + +try: + from Bcfg2.Server.FileMonitor.Fam import Fam + available['fam'] = Fam +except ImportError: + pass + +try: + from Bcfg2.Server.FileMonitor.Gamin import Gamin + available['gamin'] = Gamin +except ImportError: + pass + +try: + from Bcfg2.Server.FileMonitor.Inotify import Inotify + available['inotify'] = Inotify +except ImportError: + pass + +for fdrv in sorted(available.keys(), key=lambda k: available[k].__priority__): + if fdrv in available: + available['default'] = available[fdrv] + break diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py index ca37431a2..90e39e52d 100644 --- a/src/lib/Bcfg2/Server/Plugin.py +++ b/src/lib/Bcfg2/Server/Plugin.py @@ -480,8 +480,8 @@ class DirectoryBacked(object): return if event.requestID not in self.handles: - logger.warn("Got %s event with unknown handle (%s) for %s" - % (action, event.requestID, abspath)) + logger.warn("Got %s event with unknown handle (%s) for %s" % + (action, event.requestID, event.filename)) return # Calculate the absolute and relative paths this event refers to @@ -522,21 +522,13 @@ class DirectoryBacked(object): # didn't know about. Go ahead and treat it like a # "created" event, but log a warning, because this # is unexpected. - logger.warn("Got %s event for unexpected dir %s" % (action, - abspath)) + logger.warn("Got %s event for unexpected dir %s" % + (action, abspath)) self.add_directory_monitor(relpath) else: - logger.warn("Got unknown dir event %s %s %s" % (event.requestID, - event.code2str(), - abspath)) + logger.warn("Got unknown dir event %s %s %s" % + (event.requestID, event.code2str(), abspath)) else: - # Deal with events for non-directories - if ((event.filename[-1] == '~') or - (event.filename[:2] == '.#') or - (event.filename[-4:] == '.swp') or - (event.filename in ['SCCS', '.svn', '4913']) or - (not self.patterns.match(event.filename))): - return if action in ['exists', 'created']: self.add_entry(relpath, event) elif action == 'changed': @@ -547,13 +539,13 @@ class DirectoryBacked(object): # know about. Go ahead and treat it like a # "created" event, but log a warning, because this # is unexpected. - logger.warn("Got %s event for unexpected file %s" % (action, - abspath)) + logger.warn("Got %s event for unexpected file %s" % + (action, + abspath)) self.add_entry(relpath, event) else: - logger.warn("Got unknown file event %s %s %s" % (event.requestID, - event.code2str(), - abspath)) + logger.warn("Got unknown file event %s %s %s" % + (event.requestID, event.code2str(), abspath)) class XMLFileBacked(FileBacked): @@ -583,16 +575,18 @@ class XMLFileBacked(FileBacked): return iter(self.entries) def __str__(self): - return "%s: %s" % (self.name, lxml.etree.tostring(self.xdata)) + return "%s at %s" % (self.__class__.__name__, self.name) class SingleXMLFileBacked(XMLFileBacked): """This object is a coherent cache for an independent XML file.""" - def __init__(self, filename, fam): + def __init__(self, filename, fam, should_monitor=True): XMLFileBacked.__init__(self, filename) self.extras = [] self.fam = fam - self.fam.AddMonitor(filename, self) + self.should_monitor = should_monitor + if should_monitor: + self.fam.AddMonitor(filename, self) def _follow_xincludes(self, fname=None, xdata=None): ''' follow xincludes, adding included files to fam and to @@ -614,8 +608,9 @@ class SingleXMLFileBacked(XMLFileBacked): self._follow_xincludes(fname=fpath) def add_monitor(self, fpath, fname): - self.fam.AddMonitor(fpath, self) - self.extras.append(fname) + if self.should_monitor: + self.fam.AddMonitor(fpath, self) + self.extras.append(fname) def Index(self): """Build local data structures.""" @@ -1207,7 +1202,7 @@ class GroupSpool(Plugin, Generator): name = self.data + relative if relative not in list(self.handles.values()): if not posixpath.isdir(name): - print("Failed to open directory %s" % (name)) + self.logger.error("Failed to open directory %s" % name) return reqid = self.core.fam.AddMonitor(name, self) self.handles[reqid] = relative diff --git a/src/lib/Bcfg2/Server/Plugins/BB.py b/src/lib/Bcfg2/Server/Plugins/BB.py index c015ec47c..bd518ad19 100644 --- a/src/lib/Bcfg2/Server/Plugins/BB.py +++ b/src/lib/Bcfg2/Server/Plugins/BB.py @@ -1,4 +1,5 @@ import lxml.etree +import Bcfg2.Server import Bcfg2.Server.Plugin import glob import os @@ -16,7 +17,8 @@ class BBfile(Bcfg2.Server.Plugin.XMLFileBacked): """Build data into an xml object.""" try: - self.data = lxml.etree.XML(self.data) + self.data = lxml.etree.XML(self.data, + parser=Bcfg2.Server.XMLParser) except lxml.etree.XMLSyntaxError: Bcfg2.Server.Plugin.logger.error("Failed to parse %s" % self.name) return diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py index ccb99481e..cbc452608 100644 --- a/src/lib/Bcfg2/Server/Plugins/Bundler.py +++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py @@ -6,7 +6,7 @@ import os import os.path import re import sys - +import Bcfg2.Server import Bcfg2.Server.Plugin try: @@ -51,7 +51,8 @@ class Bundler(Bcfg2.Server.Plugin.Plugin, raise Bcfg2.Server.Plugin.PluginInitError def template_dispatch(self, name): - bundle = lxml.etree.parse(name) + bundle = lxml.etree.parse(name, + parser=Bcfg2.Server.XMLParser) nsmap = bundle.getroot().nsmap if name.endswith('.xml'): if have_genshi and \ diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py index 3edd1d8cb..e74b77e83 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgCheetahGenerator.py @@ -6,8 +6,7 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator logger = logging.getLogger(__name__) try: - import Cheetah.Template - import Cheetah.Parser + from Cheetah.Template import Template have_cheetah = True except ImportError: have_cheetah = False @@ -25,9 +24,8 @@ class CfgCheetahGenerator(CfgGenerator): raise Bcfg2.Server.Plugin.PluginExecutionError(msg) def get_data(self, entry, metadata): - template = Cheetah.Template.Template(self.data, - compilerSettings=self.settings) + template = Template(self.data, compilerSettings=self.settings) template.metadata = metadata template.path = entry.get('realname', entry.get('name')) - template.source_path = self.path + template.source_path = self.name return template.respond() diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py new file mode 100644 index 000000000..a75329d2a --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedCheetahGenerator.py @@ -0,0 +1,14 @@ +import logging +from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator +from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import CfgEncryptedGenerator + +logger = logging.getLogger(__name__) + +class CfgEncryptedCheetahGenerator(CfgCheetahGenerator, CfgEncryptedGenerator): + __extensions__ = ['cheetah.crypt', 'crypt.cheetah'] + + def handle_event(self, event): + CfgEncryptedGenerator.handle_event(self, event) + + def get_data(self, entry, metadata): + return CfgCheetahGenerator.get_data(self, entry, metadata) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py new file mode 100644 index 000000000..0839e3536 --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py @@ -0,0 +1,63 @@ +import logging +import Bcfg2.Server.Plugin +from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP +try: + from Bcfg2.Encryption import ssl_decrypt, EVPError + have_crypto = True +except ImportError: + have_crypto = False + +logger = logging.getLogger(__name__) + +def passphrases(): + section = "cfg:encryption" + if SETUP.cfp.has_section(section): + return dict([(o, SETUP.cfp.get(section, o)) + for o in SETUP.cfp.options(section)]) + else: + return dict() + +def decrypt(crypted): + if not have_crypto: + msg = "Cfg: M2Crypto is not available: %s" % entry.get("name") + logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + for passwd in passphrases().values(): + try: + return ssl_decrypt(crypted, passwd) + except EVPError: + pass + raise EVPError("Failed to decrypt") + +class CfgEncryptedGenerator(CfgGenerator): + __extensions__ = ["crypt"] + + def __init__(self, fname, spec, encoding): + CfgGenerator.__init__(self, fname, spec, encoding) + if not have_crypto: + msg = "Cfg: M2Crypto is not available: %s" % entry.get("name") + logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + + def handle_event(self, event): + if event.code2str() == 'deleted': + return + try: + crypted = open(self.name).read() + except UnicodeDecodeError: + crypted = open(self.name, mode='rb').read() + except: + logger.error("Failed to read %s" % self.name) + return + # todo: let the user specify a passphrase by name + try: + self.data = decrypt(crypted) + except EVPError: + msg = "Failed to decrypt %s" % self.name + logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + + def get_data(self, entry, metadata): + if self.data is None: + raise Bcfg2.Server.Plugin.PluginExecutionError("Failed to decrypt %s" % self.name) + return CfgGenerator.get_data(self, entry, metadata) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py new file mode 100644 index 000000000..649bc38a5 --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenshiGenerator.py @@ -0,0 +1,26 @@ +import logging +from Bcfg2.Bcfg2Py3k import StringIO +from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator +from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator import decrypt, \ + CfgEncryptedGenerator + +logger = logging.getLogger(__name__) + +try: + from genshi.template import TemplateLoader +except ImportError: + # CfgGenshiGenerator will raise errors if genshi doesn't exist + pass + + +class EncryptedTemplateLoader(TemplateLoader): + def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): + plaintext = StringIO(decrypt(fileobj.read())) + return TemplateLoader._instantiate(self, cls, plaintext, filepath, + filename, encoding=encoding) + + +class CfgEncryptedGenshiGenerator(CfgGenshiGenerator): + __extensions__ = ['genshi.crypt', 'crypt.genshi'] + __loader_cls__ = EncryptedTemplateLoader + diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py index 2c0a076d7..7c84648d7 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py @@ -23,10 +23,12 @@ def removecomment(stream): class CfgGenshiGenerator(CfgGenerator): __extensions__ = ['genshi'] + __loader_cls__ = TemplateLoader def __init__(self, fname, spec, encoding): CfgGenerator.__init__(self, fname, spec, encoding) - self.loader = TemplateLoader() + self.loader = self.__loader_cls__() + self.template = None if not have_genshi: msg = "Cfg: Genshi is not available: %s" % entry.get("name") logger.error(msg) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py index 54c17c6c5..85c13c1ac 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgLegacyInfo.py @@ -7,6 +7,10 @@ logger = logging.getLogger(__name__) class CfgLegacyInfo(CfgInfo): __basenames__ = ['info', ':info'] + def __init__(self, path): + CfgInfo.__init__(self, path) + self.path = path + def bind_info_to_entry(self, entry, metadata): self._set_info(entry, self.metadata) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py index 6c7585993..af6e1c7b1 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py @@ -152,7 +152,19 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): global PROCESSORS if PROCESSORS is None: PROCESSORS = [] - for submodule in pkgutil.walk_packages(path=__path__): + if hasattr(pkgutil, 'walk_packages'): + submodules = pkgutil.walk_packages(path=__path__) + else: + #python 2.4 + import glob + submodules = [] + for path in __path__: + for submodule in glob.glob("%s/*.py" % path): + mod = '.'.join(submodule.split("/")[-1].split('.')[:-1]) + if mod != '__init__': + submodules.append((None, mod, True)) + + for submodule in submodules: module = getattr(__import__("%s.%s" % (__name__, submodule[1])).Server.Plugins.Cfg, @@ -287,6 +299,10 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): logger.error("You need to specify base64 encoding for %s." % entry.get('name')) raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + except TypeError: + # data is already unicode; newer versions of Cheetah + # seem to return unicode + pass if data: entry.text = data @@ -298,7 +314,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): generators = [ent for ent in list(self.entries.values()) if (isinstance(ent, CfgGenerator) and ent.specific.matches(metadata))] - if not matching: + if not generators: msg = "No base file found for %s" % entry.get('name') logger.error(msg) raise Bcfg2.Server.Plugin.PluginExecutionError(msg) @@ -385,7 +401,7 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool, SETUP = core.setup if 'validate' not in SETUP: - SETUP['validate'] = Bcfg2.Options.CFG_VALIDATION + SETUP.add_option('validate', Bcfg2.Options.CFG_VALIDATION) SETUP.reparse() def AcceptChoices(self, entry, metadata): diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py index 5beec7be0..556965fca 100644 --- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py +++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py @@ -10,6 +10,7 @@ import errno import binascii import lxml.etree import Bcfg2.Options +import Bcfg2.Server import Bcfg2.Server.Plugin probecode = """#!/usr/bin/env python @@ -102,7 +103,9 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin, (data.get('name'), metadata.hostname)) else: try: - self.write_data(lxml.etree.XML(data.text), metadata) + self.write_data(lxml.etree.XML(data.text, + parser=Bcfg2.Server.XMLParser), + metadata) except lxml.etree.XMLSyntaxError: # if we didn't get XML back from the probe, assume # it's an error message diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 970126b80..50604f5cb 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -6,11 +6,10 @@ import copy import fcntl import lxml.etree import os -import os.path import socket import sys import time - +import Bcfg2.Server import Bcfg2.Server.FileMonitor import Bcfg2.Server.Plugin @@ -39,13 +38,18 @@ class MetadataRuntimeError(Exception): class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked): """Handles xml config files and all XInclude statements""" def __init__(self, metadata, watch_clients, basefile): - Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self, - os.path.join(metadata.data, - basefile), - metadata.core.fam) + # we tell SingleXMLFileBacked _not_ to add a monitor for this + # file, because the main Metadata plugin has already added + # one. then we immediately set should_monitor to the proper + # value, so that XIinclude'd files get properly watched + fpath = os.path.join(metadata.data, basefile) + Bcfg2.Server.Plugin.SingleXMLFileBacked.__init__(self, fpath, + metadata.core.fam, + should_monitor=False) + self.should_monitor = watch_clients self.metadata = metadata + self.fam = metadata.core.fam self.basefile = basefile - self.should_monitor = watch_clients self.data = None self.basedata = None self.basedir = metadata.data @@ -65,16 +69,11 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked): raise MetadataRuntimeError return self.basedata - def add_monitor(self, fpath, fname): - """Add a fam monitor for an included file""" - if self.should_monitor: - self.metadata.core.fam.AddMonitor(fpath, self.metadata) - self.extras.append(fname) - def load_xml(self): """Load changes from XML""" try: - xdata = lxml.etree.parse(os.path.join(self.basedir, self.basefile)) + xdata = lxml.etree.parse(os.path.join(self.basedir, self.basefile), + parser=Bcfg2.Server.XMLParser) except lxml.etree.XMLSyntaxError: self.logger.error('Failed to parse %s' % self.basefile) return @@ -145,7 +144,8 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.SingleXMLFileBacked): for included in self.extras: try: xdata = lxml.etree.parse(os.path.join(self.basedir, - included)) + included), + parser=Bcfg2.Server.XMLParser) cli = xdata.xpath(xpath) if len(cli) > 0: return {'filename': os.path.join(self.basedir, @@ -282,7 +282,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, def get_groups(self): '''return groups xml tree''' - groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml")) + groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"), + parser=Bcfg2.Server.XMLParser) root = groups_tree.getroot() return root @@ -792,7 +793,8 @@ class Metadata(Bcfg2.Server.Plugin.Plugin, def include_group(group): return not only_client or group in clientmeta.groups - groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml")) + groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"), + parser=Bcfg2.Server.XMLParser) try: groups_tree.xinclude() except lxml.etree.XIncludeError: diff --git a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py index 4dbd57d16..d132b0ff4 100644 --- a/src/lib/Bcfg2/Server/Plugins/NagiosGen.py +++ b/src/lib/Bcfg2/Server/Plugins/NagiosGen.py @@ -7,7 +7,7 @@ import glob import socket import logging import lxml.etree - +import Bcfg2.Server import Bcfg2.Server.Plugin LOGGER = logging.getLogger('Bcfg2.Plugins.NagiosGen') @@ -84,7 +84,8 @@ class NagiosGen(Bcfg2.Server.Plugin.Plugin, LOGGER.warn("Parsing deprecated NagiosGen/parents.xml. " "Update to the new-style config with " "nagiosgen-convert.py.") - parents = lxml.etree.parse(pfile) + parents = lxml.etree.parse(pfile, + parser=Bcfg2.Server.XMLParser) for el in parents.xpath("//Depend[@name='%s']" % metadata.hostname): if 'parent' in xtra: xtra['parent'] += "," + el.get("on") diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py index 7796b9e34..2e035a785 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/PackagesSources.py @@ -106,4 +106,7 @@ class PackagesSources(Bcfg2.Server.Plugin.SingleXMLFileBacked, return "PackagesSources: %s" % repr(self.entries) def __str__(self): - return "PackagesSources: %s" % str(self.entries) + return "PackagesSources: %s sources" % len(self.entries) + + def __len__(self): + return len(self.entries) diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py index edcdcd9f2..2bc4b4dc2 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/Source.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/Source.py @@ -169,6 +169,9 @@ class Source(Bcfg2.Server.Plugin.Debuggable): else: return self.__class__.__name__ + def __repr__(self): + return str(self) + def get_urls(self): return [] urls = property(get_urls) diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py index 53344e200..effec1c0e 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/Yum.py @@ -105,10 +105,24 @@ class YumCollection(Collection): if has_pulp and self.has_pulp_sources: _setup_pulp(self.setup) + self._helper = None + @property def helper(self): - return self.setup.cfp.get("packages:yum", "helper", - default="/usr/sbin/bcfg2-yum-helper") + try: + return self.config.get("yum", "helper") + except: + pass + + if not self._helper: + # first see if bcfg2-yum-helper is in PATH + try: + Popen(['bcfg2-yum-helper'], + stdin=PIPE, stdout=PIPE, stderr=PIPE).wait() + self._helper = 'bcfg2-yum-helper' + except OSError: + self._helper = "/usr/sbin/bcfg2-yum-helper" + return self._helper @property def use_yum(self): diff --git a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py index d789a6d39..72913a60a 100644 --- a/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Packages/__init__.py @@ -45,6 +45,10 @@ class Packages(Bcfg2.Server.Plugin.Plugin, @property def disableResolver(self): + if self.disableMetaData: + # disabling metadata without disabling the resolver Breaks + # Things + return True try: return not self.core.setup.cfp.getboolean("packages", "resolver") except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index af908eee8..22cacde55 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -2,6 +2,8 @@ import time import lxml.etree import operator import re +import os +import Bcfg2.Server try: import json @@ -93,7 +95,8 @@ class ProbeData(object): def xdata(self): if self._xdata is None: try: - self._xdata = lxml.etree.XML(self.data) + self._xdata = lxml.etree.XML(self.data, + parser=Bcfg2.Server.XMLParser) except lxml.etree.XMLSyntaxError: pass return self._xdata @@ -220,7 +223,8 @@ class Probes(Bcfg2.Server.Plugin.Plugin, def load_data(self): try: - data = lxml.etree.parse(self.data + '/probed.xml').getroot() + data = lxml.etree.parse(os.path.join(self.data, 'probed.xml'), + parser=Bcfg2.Server.XMLParser).getroot() except: self.logger.error("Failed to read file probed.xml") return diff --git a/src/lib/Bcfg2/Server/Plugins/SGenshi.py b/src/lib/Bcfg2/Server/Plugins/SGenshi.py index 0ba08125e..12c125c62 100644 --- a/src/lib/Bcfg2/Server/Plugins/SGenshi.py +++ b/src/lib/Bcfg2/Server/Plugins/SGenshi.py @@ -7,7 +7,7 @@ import logging import copy import sys import os.path - +import Bcfg2.Server import Bcfg2.Server.Plugin import Bcfg2.Server.Plugins.TGenshi @@ -28,7 +28,8 @@ class SGenshiTemplateFile(Bcfg2.Server.Plugins.TGenshi.TemplateFile, try: stream = self.template.generate(metadata=metadata).filter( \ Bcfg2.Server.Plugins.TGenshi.removecomment) - data = lxml.etree.XML(stream.render('xml', strip_whitespace=False)) + data = lxml.etree.XML(stream.render('xml', strip_whitespace=False), + parser=Bcfg2.Server.XMLParser) bundlename = os.path.splitext(os.path.basename(self.name))[0] bundle = lxml.etree.Element('Bundle', name=bundlename) for item in self.Match(metadata, data): diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py index 0072dc62d..1091fc2c8 100644 --- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py +++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py @@ -42,14 +42,16 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): if event.filename.endswith('.xml'): if action in ['exists', 'created', 'changed']: if event.filename.endswith('key.xml'): - key_spec = dict(list(lxml.etree.parse(epath).find('Key').items())) + key_spec = dict(list(lxml.etree.parse(epath, + parser=Bcfg2.Server.XMLParser).find('Key').items())) self.key_specs[ident] = { 'bits': key_spec.get('bits', 2048), 'type': key_spec.get('type', 'rsa') } self.Entries['Path'][ident] = self.get_key elif event.filename.endswith('cert.xml'): - cert_spec = dict(list(lxml.etree.parse(epath).find('Cert').items())) + cert_spec = dict(list(lxml.etree.parse(epath, + parser=Bcfg2.Server.XMLParser).find('Cert').items())) ca = cert_spec.get('ca', 'default') self.cert_specs[ident] = { 'ca': ca, diff --git a/src/lib/Bcfg2/Server/Reports/reports/models.py b/src/lib/Bcfg2/Server/Reports/reports/models.py index 35f2a4393..9a71fb16a 100644 --- a/src/lib/Bcfg2/Server/Reports/reports/models.py +++ b/src/lib/Bcfg2/Server/Reports/reports/models.py @@ -154,7 +154,7 @@ class InteractiveManager(models.Manager): cursor.execute(sql) return [item[0] for item in cursor.fetchall()] except: - '''FIXME - really need some error hadling''' + '''FIXME - really need some error handling''' pass return [] diff --git a/src/lib/Bcfg2/Server/Reports/settings.py b/src/lib/Bcfg2/Server/Reports/settings.py index 4d567f1a2..9eb558ac8 100644 --- a/src/lib/Bcfg2/Server/Reports/settings.py +++ b/src/lib/Bcfg2/Server/Reports/settings.py @@ -1,11 +1,16 @@ import django +import os import sys # Compatibility import from Bcfg2.Bcfg2Py3k import ConfigParser # Django settings for bcfg2 reports project. c = ConfigParser.ConfigParser() -if len(c.read(['/etc/bcfg2.conf', '/etc/bcfg2-web.conf'])) == 0: +if 'BCFG2_CONFIG_FILE' in os.environ: + cfiles=os.environ['BCFG2_CONFIG_FILE'] +else: + cfiles=['/etc/bcfg2.conf', '/etc/bcfg2-web.conf'] +if len(c.read(cfiles)) == 0: raise ImportError("Please check that bcfg2.conf or bcfg2-web.conf exists " "and is readable by your web server.") @@ -43,6 +48,9 @@ DATABASES = { } } +if db_engine == 'ibm_db_django': + DATABASES['default']['ENGINE'] = db_engine + if db_engine != 'sqlite3': DATABASES['default']['USER'] = c.get('statistics', 'database_user') DATABASES['default']['PASSWORD'] = c.get('statistics', 'database_password') diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py index 96777b0bf..320371284 100644 --- a/src/lib/Bcfg2/Server/__init__.py +++ b/src/lib/Bcfg2/Server/__init__.py @@ -1,4 +1,8 @@ """This is the set of modules for Bcfg2.Server.""" +import lxml.etree + __all__ = ["Admin", "Core", "FileMonitor", "Plugin", "Plugins", - "Hostbase", "Reports", "Snapshots"] + "Hostbase", "Reports", "Snapshots", "XMLParser"] + +XMLParser = lxml.etree.XMLParser(remove_blank_text=True) diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin index 007dd0af3..7cc19be8f 100755 --- a/src/sbin/bcfg2-admin +++ b/src/sbin/bcfg2-admin @@ -44,6 +44,7 @@ def main(): 'plugins': Bcfg2.Options.SERVER_PLUGINS, 'event debug': Bcfg2.Options.DEBUG, 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR, + 'ignore': Bcfg2.Options.SERVER_FAM_IGNORE, 'password': Bcfg2.Options.SERVER_PASSWORD, 'encoding': Bcfg2.Options.ENCODING, } diff --git a/src/sbin/bcfg2-info b/src/sbin/bcfg2-info index 8598a58eb..fdcf9ac17 100755 --- a/src/sbin/bcfg2-info +++ b/src/sbin/bcfg2-info @@ -648,6 +648,7 @@ if __name__ == '__main__': 'password': Bcfg2.Options.SERVER_PASSWORD, 'mconnect': Bcfg2.Options.SERVER_MCONNECT, 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR, + 'ignore': Bcfg2.Options.SERVER_FAM_IGNORE, 'location': Bcfg2.Options.SERVER_LOCATION, 'static': Bcfg2.Options.SERVER_STATIC, 'key': Bcfg2.Options.SERVER_KEY, diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint index 78b833f02..bc1e5b70e 100755 --- a/src/sbin/bcfg2-lint +++ b/src/sbin/bcfg2-lint @@ -82,6 +82,7 @@ if __name__ == '__main__': 'plugins': Bcfg2.Options.SERVER_PLUGINS, 'mconnect': Bcfg2.Options.SERVER_MCONNECT, 'filemonitor': Bcfg2.Options.SERVER_FILEMONITOR, + 'ignore': Bcfg2.Options.SERVER_FAM_IGNORE, 'location': Bcfg2.Options.SERVER_LOCATION, 'static': Bcfg2.Options.SERVER_STATIC, 'key': Bcfg2.Options.SERVER_KEY, diff --git a/src/sbin/bcfg2-server b/src/sbin/bcfg2-server index 757172464..1b8b0d158 100755 --- a/src/sbin/bcfg2-server +++ b/src/sbin/bcfg2-server @@ -26,6 +26,7 @@ if __name__ == '__main__': 'plugins' : Bcfg2.Options.SERVER_PLUGINS, 'password' : Bcfg2.Options.SERVER_PASSWORD, 'fm' : Bcfg2.Options.SERVER_FILEMONITOR, + 'ignore' : Bcfg2.Options.SERVER_FAM_IGNORE, 'key' : Bcfg2.Options.SERVER_KEY, 'cert' : Bcfg2.Options.SERVER_CERT, 'ca' : Bcfg2.Options.SERVER_CA, diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test index e3cfd27cc..7ddbb3509 100755 --- a/src/sbin/bcfg2-test +++ b/src/sbin/bcfg2-test @@ -70,7 +70,8 @@ def main(): 'password': Bcfg2.Options.SERVER_PASSWORD, 'verbose': Bcfg2.Options.VERBOSE, 'noseopts': Bcfg2.Options.TEST_NOSEOPTS, - 'ignore': Bcfg2.Options.TEST_IGNORE, + 'ignore': Bcfg2.Options.SERVER_FAM_IGNORE, + 'test_ignore': Bcfg2.Options.TEST_IGNORE, 'validate': Bcfg2.Options.CFG_VALIDATION, } setup = Bcfg2.Options.OptionParser(optinfo) @@ -92,7 +93,7 @@ def main(): ) ignore = dict() - for entry in setup['ignore']: + for entry in setup['test_ignore']: tag, name = entry.split(":") try: ignore[tag].append(name) |