From 8c08bfc37cb7cdec5e83005ea71f55f0cfd4259e Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 12 Jun 2012 09:08:14 -0400 Subject: added support for Puppet External Node Classifiers --- doc/server/plugins/connectors/puppetenc.txt | 123 ++++++++++++++++++++++++++++ src/lib/Bcfg2/Server/Plugins/PuppetENC.py | 117 ++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 doc/server/plugins/connectors/puppetenc.txt create mode 100644 src/lib/Bcfg2/Server/Plugins/PuppetENC.py diff --git a/doc/server/plugins/connectors/puppetenc.txt b/doc/server/plugins/connectors/puppetenc.txt new file mode 100644 index 000000000..dc472c546 --- /dev/null +++ b/doc/server/plugins/connectors/puppetenc.txt @@ -0,0 +1,123 @@ +.. -*- mode: rst -*- + +.. _server-plugins-connectors-puppetenc: + +========= +PuppetENC +========= + +PuppetENC is a connector plugin that adds support for Puppet External +Node Classifiers +(``_), or ENCs. + +Output Format +============= + +The PuppetENC plugin implements the Puppet 2.6.5+ ENC output format +with some modifications. The basic output format is described `here +`_. +The following modifications apply: + +* ``classes`` are considered to be Bcfg2 groups. (This is basically + just a difference in terminology between Puppet and Bcfg2; Bcfg2 + calls "groups" what Puppet calls "classes.") +* As an alternative to the Puppet-specific ``classes`` value, you may + use ``groups`` if you are writing an ENC from scratch specifically + for Bcfg2. +* Since Bcfg2 does not have the notion of parameterized classes, any + class parameters provided will be merged in with the ``parameters`` + dict. +* ``parameters`` are presented as connector data. (See Usage + below.) +* The ``environment`` value is not supported. If present, PuppetENC + will issue a warning and skip it. + +The ``parameters`` from separate ENCs are all merged together, +including parameters from any parameterized classes. This is a +shallow merge; in other words, only the top-level keys are +considered. For instance, assuming you had one ENC that produced:: + + parameters: + ntp_servers: + - 0.pool.ntp.org + - ntp1.example.com + +And another that produced:: + + parameters: + ntp_servers: + - ntp2.example.com + +This would result in connector data that included *either* the first +value of ``ntp_servers`` *or* the second, but not both; this would +depend on the order in which the ENCs were run, which is +non-deterministic and should not be relied upon. However, if you add +one ENC that produced:: + + parameters: + ntp_servers: + - 0.pool.ntp.org + - ntp1.example.com + +And another that produced:: + + parameters: + mail_servers: + - mail.example.com + +Then the connector data would consist of:: + + {"ntp_servers": ["0.pool.ntp.org", "ntp1.example.com"], + "mail_servers": ["mail.example.com"]} + +Usage +===== + +To use the PuppetENC plugin, first do ``mkdir +/var/lib/bcfg2/PuppetENC``. Add ``PuppetENC`` to your ``plugins`` +line in ``/etc/bcfg2.conf``. Now you can place any ENCs you wish to +run in ``/var/lib/bcfg2/PuppetENC``. Note that ENCs are run each time +client metadata is generated, so if you have a large number of ENCs or +ENCs that are very time-consuming, they could have a significant +impact on server performance. In that case, it could be worthwhile to +write a dedicated Connector plugin. + +PuppetENC parameters can be accessed in templates as +``metadata.PuppetENC``, which is a dict of all parameter data merged +together. For instance, given the following ENC output:: + + --- + classes: + common: + puppet: + ntp: + ntpserver: 0.pool.ntp.org + aptsetup: + additional_apt_repos: + - deb localrepo.example.com/ubuntu lucid production + - deb localrepo.example.com/ubuntu lucid vendor + parameters: + ntp_servers: + - 0.pool.ntp.org + - ntp.example.com + mail_server: mail.example.com + iburst: true + environment: production + +``metadata.PuppetENC`` would contain:: + + 'additional_apt_repos': ['deb localrepo.example.com/ubuntu lucid production', + 'deb localrepo.example.com/ubuntu lucid vendor'], + 'iburst': True, + 'mail_server': 'mail.example.com', + 'ntp_servers': ['0.pool.ntp.org', 'ntp.example.com'], + 'ntpserver': '0.pool.ntp.org'} + +(Note that the duplication of NTP server data doesn't make this an +especially *good* example; it's just the official Puppet example.) + +So, in a template you could do something like:: + + {% for repo in metadata.PuppetENC['additional_apt_repos'] %}\ + ${repo} + {% end %}\ diff --git a/src/lib/Bcfg2/Server/Plugins/PuppetENC.py b/src/lib/Bcfg2/Server/Plugins/PuppetENC.py new file mode 100644 index 000000000..3a8fe67fb --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/PuppetENC.py @@ -0,0 +1,117 @@ +import os +import Bcfg2.Server +import Bcfg2.Server.Plugin +from subprocess import Popen, PIPE + +try: + from syck import load as yaml_load, error as yaml_error +except ImportError: + try: + from yaml import load as yaml_load, YAMLError as yaml_error + except ImportError: + raise ImportError("No yaml library could be found") + +class PuppetENCFile(Bcfg2.Server.Plugin.FileBacked): + def HandleEvent(self, event=None): + return + + def __str__(self): + return "%s: %s" % (self.__class__.__name__, self.name) + + +class PuppetENC(Bcfg2.Server.Plugin.Plugin, + Bcfg2.Server.Plugin.Connector, + Bcfg2.Server.Plugin.ClientRunHooks, + Bcfg2.Server.Plugin.DirectoryBacked): + """ A plugin to run Puppet external node classifiers + (http://docs.puppetlabs.com/guides/external_nodes.html) """ + name = 'PuppetENC' + experimental = True + __child__ = PuppetENCFile + + def __init__(self, core, datastore): + Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) + Bcfg2.Server.Plugin.Connector.__init__(self) + Bcfg2.Server.Plugin.ClientRunHooks.__init__(self) + Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, + self.core.fam) + self.cache = dict() + + def _run_encs(self, metadata): + cache = dict(groups=[], params=dict()) + for enc in self.entries.keys(): + epath = os.path.join(self.data, enc) + self.debug_log("PuppetENC: Running ENC %s for %s" % + (enc, metadata.hostname)) + proc = Popen([epath, metadata.hostname], stdin=PIPE, stdout=PIPE, + stderr=PIPE) + (out, err) = proc.communicate() + rv = proc.wait() + if rv != 0: + msg = "PuppetENC: Error running ENC %s for %s (%s): %s" % \ + (enc, metadata.hostname, rv) + self.logger.error("%s: %s" % (msg, err)) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + if err: + self.debug_log("ENC Error: %s" % err) + + try: + yaml = yaml_load(out) + self.debug_log("Loaded data from %s for %s: %s" % + (enc, metadata.hostname, yaml)) + except yaml_error: + err = sys.exc_info()[1] + msg = "Error decoding YAML from %s for %s: %s" % \ + (enc, metadata.hostname, err) + self.logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + + groups = [] + if "classes" in yaml: + # stock Puppet ENC output format + groups = yaml['classes'] + elif "groups" in yaml: + # more Bcfg2-ish output format + groups = yaml['groups'] + if groups: + if isinstance(groups, list): + self.debug_log("ENC %s adding groups to %s: %s" % + (enc, metadata.hostname, groups)) + cache['groups'].extend(groups) + else: + self.debug_log("ENC %s adding groups to %s: %s" % + (enc, metadata.hostname, groups.keys())) + for group, params in groups.items(): + cache['groups'].append(group) + if params: + cache['params'].update(params) + if "parameters" in yaml and yaml['parameters']: + cache['params'].update(yaml['parameters']) + if "environment" in yaml: + self.logger.info("Ignoring unsupported environment section of " + "ENC %s for %s" % (enc, metadata.hostname)) + + self.cache[metadata.hostname] = cache + + def get_additional_groups(self, metadata): + if metadata.hostname not in self.cache: + self._run_encs(metadata) + return self.cache[metadata.hostname]['groups'] + + def get_additional_data(self, metadata): + if metadata.hostname not in self.cache: + self._run_encs(metadata) + return self.cache[metadata.hostname]['params'] + + def end_client_run(self, metadata): + """ clear the entire cache at the end of each client run. this + guarantees that each client will run all ENCs at or near the + start of each run; we have to clear the entire cache instead + of just the cache for this client because a client that builds + templates that use metadata for other clients will populate + the cache for those clients, which we don't want. This makes + the caching less than stellar, but it does prevent multiple + runs of ENCs for a single host a) for groups and data + separately; and b) when a single client's metadata is + generated multiple times by separate templates """ + self.cache = dict() -- cgit v1.2.3-1-g7c22