From 557377e8a1d0492f4c26b95c4a74172a9210ac3e Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 13 Nov 2013 13:33:31 -0500 Subject: Options: make "public" default pgsql database schema --- src/lib/Bcfg2/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 673fb125c..95abe64ae 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -680,7 +680,7 @@ DB_OPTIONS = \ cook=dict_split) DB_SCHEMA = \ Option('Database schema', - default='', + default='public', cf=('database', 'schema')) # Django options -- cgit v1.2.3-1-g7c22 From d8bbfbdf8b503538fff01bff80c5e6e12bfb44b3 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Tue, 12 Nov 2013 23:48:25 +0100 Subject: Add probes.allowed_groups option to restrict group assignments. --- src/lib/Bcfg2/Options.py | 17 ++++++++++++++++- src/lib/Bcfg2/Server/Plugins/Probes.py | 10 +++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 95abe64ae..f9b6a998b 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -311,6 +311,14 @@ def list_split(c_string): return re.split(r'\s*,\s*', c_string) return [] +def list_split_anchored_regex(c_string): + """ like list_split but split on whitespace and compile each element as + anchored regex """ + try: + return [re.compile('^' + x + '$') for x in re.split(r'\s+', c_string)] + except re.error: + raise ValueError("Not a list of regexes", c_string) + def colon_split(c_string): """ split an option string on colons, returning a list """ @@ -641,6 +649,12 @@ SERVER_CHILDREN = \ cf=('server', 'children'), cook=get_int, long_arg=True) +SERVER_PROBE_ALLOWED_GROUPS = \ + Option('Whitespace-separated list of group names (as regex) to which ' + 'probes can assign a client by writing "group:" to stdout.', + default=['.*'], + cf=('probes', 'allowed_groups'), + cook=list_split_anchored_regex) # database options DB_ENGINE = \ @@ -1225,7 +1239,8 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY, perflog=LOG_PERFORMANCE, perflog_interval=PERFLOG_INTERVAL, children=SERVER_CHILDREN, - client_timeout=CLIENT_TIMEOUT) + client_timeout=CLIENT_TIMEOUT, + probe_allowed_groups=SERVER_PROBE_ALLOWED_GROUPS) CRYPT_OPTIONS = dict(encrypt=ENCRYPT, decrypt=DECRYPT, diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index 84e1638d6..59a73c4aa 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -204,6 +204,7 @@ class Probes(Bcfg2.Server.Plugin.Probing, err = sys.exc_info()[1] raise Bcfg2.Server.Plugin.PluginInitError(err) + self.allowed_cgroups = core.setup['probe_allowed_groups'] self.probedata = dict() self.cgroups = dict() self.load_data() @@ -391,11 +392,18 @@ class Probes(Bcfg2.Server.Plugin.Probing, if line.split(':')[0] == 'group': newgroup = line.split(':')[1].strip() if newgroup not in cgroups: - cgroups.append(newgroup) + if self._group_allowed(newgroup): + cgroups.append(newgroup) + else: + self.logger.info("Disallowed group assignment %s from %s" + % (newgroup, client.hostname)) dlines.remove(line) dobj = ProbeData("\n".join(dlines)) cprobedata[data.get('name')] = dobj + def _group_allowed(self, group): + return any(r.match(group) for r in self.allowed_cgroups) + def get_additional_groups(self, meta): return self.cgroups.get(meta.hostname, list()) get_additional_groups.__doc__ = \ -- cgit v1.2.3-1-g7c22 From bf3adbb11ef36591d80b9b6f4d9768caf516b4e3 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 14 Nov 2013 09:35:06 -0500 Subject: testsuite: fixed unit tests for Probes allowed_groups option --- src/lib/Bcfg2/Options.py | 1 + src/lib/Bcfg2/Server/Plugins/Probes.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index f9b6a998b..f47ac00d2 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -311,6 +311,7 @@ def list_split(c_string): return re.split(r'\s*,\s*', c_string) return [] + def list_split_anchored_regex(c_string): """ like list_split but split on whitespace and compile each element as anchored regex """ diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index 59a73c4aa..0df88d522 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -395,8 +395,9 @@ class Probes(Bcfg2.Server.Plugin.Probing, if self._group_allowed(newgroup): cgroups.append(newgroup) else: - self.logger.info("Disallowed group assignment %s from %s" - % (newgroup, client.hostname)) + self.logger.info( + "Disallowed group assignment %s from %s" % + (newgroup, client.hostname)) dlines.remove(line) dobj = ProbeData("\n".join(dlines)) cprobedata[data.get('name')] = dobj -- cgit v1.2.3-1-g7c22 From 881205322035f9ca7375ab0c67ab339c430dbe01 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 14 Nov 2013 09:51:59 -0500 Subject: Probes: added missing docstring --- src/lib/Bcfg2/Server/Plugins/Probes.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index 0df88d522..fdc047283 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -403,6 +403,9 @@ class Probes(Bcfg2.Server.Plugin.Probing, cprobedata[data.get('name')] = dobj def _group_allowed(self, group): + """ Determine if the named group can be set as a probe group + by checking the regexes listed in the [probes] groups_allowed + setting """ return any(r.match(group) for r in self.allowed_cgroups) def get_additional_groups(self, meta): -- cgit v1.2.3-1-g7c22 From 264b9486bec017a4771c437efa9231a6cf7cf0ef Mon Sep 17 00:00:00 2001 From: Sol Jerome Date: Sat, 16 Nov 2013 17:43:44 -0600 Subject: Reporting: Remove wildcard imports Signed-off-by: Sol Jerome --- src/lib/Bcfg2/Reporting/Compat.py | 6 ++---- src/lib/Bcfg2/Reporting/urls.py | 2 +- src/lib/Bcfg2/Reporting/utils.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Reporting/Compat.py b/src/lib/Bcfg2/Reporting/Compat.py index 57261970d..9113fdb91 100644 --- a/src/lib/Bcfg2/Reporting/Compat.py +++ b/src/lib/Bcfg2/Reporting/Compat.py @@ -10,9 +10,7 @@ if VERSION[0] == 1 and VERSION[1] < 6: try: # Django < 1.6 - from django.conf.urls import defaults - django_urls = defaults + from django.conf.urls.defaults import url, patterns except ImportError: # Django > 1.6 - from django.conf import urls - django_urls = urls + from django.conf.urls import url, patterns diff --git a/src/lib/Bcfg2/Reporting/urls.py b/src/lib/Bcfg2/Reporting/urls.py index a9e5690be..3a40cb932 100644 --- a/src/lib/Bcfg2/Reporting/urls.py +++ b/src/lib/Bcfg2/Reporting/urls.py @@ -1,4 +1,4 @@ -from Bcfg2.Reporting.Compat.django_urls import * +from Bcfg2.Reporting.Compat import url, patterns # django compat imports from django.core.urlresolvers import reverse, NoReverseMatch from django.http import HttpResponsePermanentRedirect from Bcfg2.Reporting.utils import filteredUrls, paginatedUrls, timeviewUrls diff --git a/src/lib/Bcfg2/Reporting/utils.py b/src/lib/Bcfg2/Reporting/utils.py index d9b8213b1..0d394fcd8 100755 --- a/src/lib/Bcfg2/Reporting/utils.py +++ b/src/lib/Bcfg2/Reporting/utils.py @@ -1,5 +1,4 @@ """Helper functions for reports""" -from Bcfg2.Reporting.Compat.django_urls import * import re """List of filters provided by filteredUrls""" -- cgit v1.2.3-1-g7c22 From 504741f24c050684d1707cac057fdd4677afdd64 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 18 Nov 2013 09:19:34 -0500 Subject: bcfg2-lint: resolve XIncludes when parsing XML for validation --- src/lib/Bcfg2/Server/Lint/Validate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py index c537877a0..ced16770b 100644 --- a/src/lib/Bcfg2/Server/Lint/Validate.py +++ b/src/lib/Bcfg2/Server/Lint/Validate.py @@ -107,9 +107,16 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): :type filename: string :returns: lxml.etree._ElementTree - the parsed data""" try: - return lxml.etree.parse(filename) - except SyntaxError: - lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT) + xdata = lxml.etree.parse(filename) + if self.files is None: + xdata.xinclude() + return xdata + except (lxml.etree.XIncludeError, SyntaxError): + cmd = ["xmllint", "--noout"] + if self.files is None: + cmd.append("--xinclude") + cmd.append(filename) + lint = Popen(cmd, stdout=PIPE, stderr=STDOUT) self.LintError("xml-failed-to-parse", "%s fails to parse:\n%s" % (filename, lint.communicate()[0])) @@ -141,6 +148,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): if not schema: return False datafile = self.parse(filename) + if not datafile: + return False if not schema.validate(datafile): cmd = ["xmllint"] if self.files is None: -- cgit v1.2.3-1-g7c22 From 0f8d403d1a86cfbfe8222662dc445e16e8f7eff9 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 21 Nov 2013 13:03:04 -0500 Subject: Bundler: Fix parsing XML template output with encoding declaration lxml 3.2.1 complains when you try to parse a unicode (in Python 2) or string (in Python 3) containing an XML document with an encoding declaration. Traceback: ValueError: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration. This encodes the document as a string (in Python 2) or bytes (in Python 3) to avoid the lxml error. There may be other places this happens, too, although in most other cases we should use lxml.etree.parse() to parse a file, or we parse strings (in Python 2) instead of unicode objects. --- src/lib/Bcfg2/Server/Plugins/Bundler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugins/Bundler.py b/src/lib/Bcfg2/Server/Plugins/Bundler.py index fb327f7ef..58f8f4430 100644 --- a/src/lib/Bcfg2/Server/Plugins/Bundler.py +++ b/src/lib/Bcfg2/Server/Plugins/Bundler.py @@ -53,9 +53,9 @@ if HAS_GENSHI: stream = self.template.generate( metadata=metadata, repo=SETUP['repo']).filter(removecomment) - data = lxml.etree.XML(stream.render('xml', - strip_whitespace=False), - parser=Bcfg2.Server.XMLParser) + data = lxml.etree.XML( + stream.render('xml', strip_whitespace=False).encode(), + 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): -- cgit v1.2.3-1-g7c22 From a7c4e87f7501439a93807c119f611c8be23194eb Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Nov 2013 07:27:50 -0500 Subject: bcfg2-crypt: handle error when encrypting properties with multiple keys --- src/sbin/bcfg2-crypt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt index 3c2d0f2b6..f3b96909e 100755 --- a/src/sbin/bcfg2-crypt +++ b/src/sbin/bcfg2-crypt @@ -54,6 +54,10 @@ class DecryptError(Exception): """ Exception raised when decryption fails. """ +class EncryptError(Exception): + """ Exception raised when encryption fails. """ + + class CryptoTool(object): """ Generic decryption/encryption interface base object """ def __init__(self, filename, setup): @@ -270,8 +274,7 @@ class PropertiesEncryptor(Encryptor, PropertiesCryptoMixin): try: pname, passphrase = self._get_element_passphrase(elt) except PassphraseError: - self.logger.error(str(sys.exc_info()[1])) - return False + raise EncryptError(str(sys.exc_info()[1])) self.logger.debug("Encrypting %s" % print_xml(elt)) elt.text = Bcfg2.Encryption.ssl_encrypt( elt.text, passphrase, @@ -454,8 +457,9 @@ def main(): # pylint: disable=R0912,R0915 if data is None: try: data = getattr(tool, mode)() - except DecryptError: - logger.error("Failed to %s %s, skipping" % (mode, fname)) + except (EncryptError, DecryptError): + logger.error("Failed to %s %s, skipping: %s" % + (mode, fname, sys.exc_info()[1])) continue if setup['crypt_stdout']: if len(setup['args']) > 1: -- cgit v1.2.3-1-g7c22 From a87c59d6f23b4c4fd4e38e380994f2193ca24588 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Nov 2013 10:48:43 -0500 Subject: bcfg2-admin: Restored missing "bcfg2-admin client add ... attrib=val" functionality --- src/lib/Bcfg2/Server/Admin/Client.py | 23 ++++++++++++++++++++--- src/lib/Bcfg2/Server/Plugins/Metadata.py | 5 +++++ 2 files changed, 25 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py index 187ccfd71..4eb67a9de 100644 --- a/src/lib/Bcfg2/Server/Admin/Client.py +++ b/src/lib/Bcfg2/Server/Admin/Client.py @@ -5,6 +5,18 @@ import Bcfg2.Server.Admin from Bcfg2.Server.Plugin import MetadataConsistencyError +def get_attribs(args): + attr_d = {} + for i in args[2:]: + attr, val = i.split('=', 1) + if attr not in ['profile', 'uuid', 'password', 'floating', 'secure', + 'address', 'auth']: + print("Attribute %s unknown" % attr) + raise SystemExit(1) + attr_d[attr] = val + return attr_d + + class Client(Bcfg2.Server.Admin.MetadataCore): """ Create, delete, or list client entries """ __usage__ = "[options] [add|del|list] [attr=val]" @@ -16,14 +28,19 @@ class Client(Bcfg2.Server.Admin.MetadataCore): "Usage: %s" % self.__usage__) if args[0] == 'add': try: - self.metadata.add_client(args[1]) + self.metadata.add_client(args[1], get_attribs(args)) + except MetadataConsistencyError: + self.errExit("Error adding client: %s" % sys.exc_info()[1]) + elif args[0] in ['update', 'up']: + try: + self.metadata.update_client(args[1], get_attribs(args)) except MetadataConsistencyError: - self.errExit("Error in adding client: %s" % sys.exc_info()[1]) + self.errExit("Error updating client: %s" % sys.exc_info()[1]) elif args[0] in ['delete', 'remove', 'del', 'rm']: try: self.metadata.remove_client(args[1]) except MetadataConsistencyError: - self.errExit("Error in deleting client: %s" % + self.errExit("Error deleting client: %s" % sys.exc_info()[1]) elif args[0] in ['list', 'ls']: for client in self.metadata.list_clients(): diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 047dd4f4e..343e14162 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -658,6 +658,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if attribs is None: attribs = dict() if self._use_db: + if attribs: + msg = "Metadata does not support setting client attributes " +\ + "with use_database enabled" + self.logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) try: client = MetadataClientModel.objects.get(hostname=client_name) except MetadataClientModel.DoesNotExist: -- cgit v1.2.3-1-g7c22 From bcea1949fa2a84e87c51d128a17a25a70d50ca13 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Nov 2013 11:11:16 -0500 Subject: bcfg2-admin: added missing docstring --- src/lib/Bcfg2/Server/Admin/Client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Admin/Client.py b/src/lib/Bcfg2/Server/Admin/Client.py index 4eb67a9de..325b7ae6e 100644 --- a/src/lib/Bcfg2/Server/Admin/Client.py +++ b/src/lib/Bcfg2/Server/Admin/Client.py @@ -6,6 +6,7 @@ from Bcfg2.Server.Plugin import MetadataConsistencyError def get_attribs(args): + """ Get a list of attributes to set on a client when adding/updating it """ attr_d = {} for i in args[2:]: attr, val = i.split('=', 1) @@ -19,7 +20,7 @@ def get_attribs(args): class Client(Bcfg2.Server.Admin.MetadataCore): """ Create, delete, or list client entries """ - __usage__ = "[options] [add|del|list] [attr=val]" + __usage__ = "[options] [add|del|update|list] [attr=val]" __plugin_whitelist__ = ["Metadata"] def __call__(self, args): -- cgit v1.2.3-1-g7c22 From 16b4744544ab140c4ab9bc733a7dfa76cf4e578c Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Nov 2013 11:24:35 -0500 Subject: Core: Avoid starting server if database is enabled but cannot be used Plugins that use the database often act quite differently depending on whether or not the database is enabled. If we start the server without the database (e.g., the connection failed), then Very Strange Things can happen. --- src/lib/Bcfg2/Server/Core.py | 8 ++++---- src/lib/Bcfg2/Server/Plugin/helpers.py | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 5ec1b5bce..c2cf6b7a4 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -228,11 +228,11 @@ class BaseCore(object): verbosity=0) self._database_available = True except ImproperlyConfigured: - err = sys.exc_info()[1] - self.logger.error("Django configuration problem: %s" % err) + self.logger.error("Django configuration problem: %s" % + sys.exc_info()[1]) except: - err = sys.exc_info()[1] - self.logger.error("Database update failed: %s" % err) + self.logger.error("Database update failed: %s" % + sys.exc_info()[1]) if do_chown and self._database_available: try: diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index d9e208746..f39609a5b 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -16,7 +16,7 @@ from Bcfg2.Compat import CmpMixin, wraps from Bcfg2.Server.Plugin.base import Debuggable, Plugin from Bcfg2.Server.Plugin.interfaces import Generator from Bcfg2.Server.Plugin.exceptions import SpecificityError, \ - PluginExecutionError + PluginExecutionError, PluginInitError try: import django # pylint: disable=W0611 @@ -131,6 +131,18 @@ class DatabaseBacked(Plugin): #: conform to the possible values that function can handle. option = "use_database" + def __init__(self, core, datastore): + Plugin.__init__(self, core, datastore) + use_db = self.core.setup.cfp.getboolean(self.section, + self.option, + default=False) + if use_db and not HAS_DJANGO: + raise PluginInitError("%s is True but Django not found" % + self.option) + elif use_db and not self.core.database_available: + raise PluginInitError("%s is True but the database is unavailable " + "due to prior errors" % self.option) + def _section(self): """ The section to look in for :attr:`DatabaseBacked.option` """ @@ -146,10 +158,7 @@ class DatabaseBacked(Plugin): default=False) if use_db and HAS_DJANGO and self.core.database_available: return True - elif not use_db: - return False else: - self.logger.error("%s is true but django not found" % self.option) return False @property -- cgit v1.2.3-1-g7c22 From 1c7488e85200e50429b1248d36a6815bedc49f02 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 25 Nov 2013 12:55:50 -0500 Subject: testsuite: fixed unit tests for database fixes --- src/lib/Bcfg2/Server/Plugin/helpers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index f39609a5b..be9c9e8ae 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -137,11 +137,12 @@ class DatabaseBacked(Plugin): self.option, default=False) if use_db and not HAS_DJANGO: - raise PluginInitError("%s is True but Django not found" % - self.option) + raise PluginInitError("%s.%s is True but Django not found" % + (self.section, self.option)) elif use_db and not self.core.database_available: - raise PluginInitError("%s is True but the database is unavailable " - "due to prior errors" % self.option) + raise PluginInitError("%s.%s is True but the database is " + "unavailable due to prior errors" % + (self.section, self.option)) def _section(self): """ The section to look in for :attr:`DatabaseBacked.option` -- cgit v1.2.3-1-g7c22 From 53f8eb67378f6a8054cb107e72b094f070d40c83 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 09:58:18 -0500 Subject: Tools: new Augeas driver --- src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py | 211 +++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py (limited to 'src') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py new file mode 100644 index 000000000..cda9a1e3b --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py @@ -0,0 +1,211 @@ +""" Augeas driver """ + +import sys +import augeas +import Bcfg2.Client.XML +from Bcfg2.Client.Tools.POSIX.base import POSIXTool + + +class AugeasCommand(object): + def __init__(self, command, augeas, logger): + self.augeas = augeas + self.command = command + self.entry = self.command.getparent() + self.logger = logger + + def get_path(self, attr="path"): + return "/files/%s/%s" % (self.entry.get("name").strip("/"), + self.command.get(attr).lstrip("/")) + + def _exists(self, path): + return len(self.augeas.match(path)) > 1 + + def _verify_exists(self, path=None): + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying that '%s' exists" % path) + return self._exists(path) + + def _verify_not_exists(self, path=None): + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying that '%s' does not exist" % path) + return not self._exists(path) + + def _verify_set(self, expected, path=None): + if path is None: + path = self.get_path() + self.logger.debug("Augeas: Verifying '%s' == '%s'" % (path, expected)) + actual = self.augeas.get(path) + if actual == expected: + return True + else: + self.logger.debug("Augeas: '%s' failed verification: '%s' != '%s'" + % (path, actual, expected)) + return False + + def __str__(self): + return Bcfg2.Client.XML.tostring(self.command) + + +class Remove(AugeasCommand): + def verify(self): + return self._verify_not_exists() + + def install(self): + self.logger.debug("Augeas: Removing %s" % self.get_path()) + return self.augeas.remove(self.get_path()) + + +class Move(AugeasCommand): + def __init__(self, command, augeas, logger): + AugeasCommand.__init__(self, command, augeas, logger) + self.source = self.get_path("source") + self.dest = self.get_path("destination") + + def verify(self): + return (self._verify_not_exists(self.source), + self._verify_exists(self.dest)) + + def install(self): + self.logger.debug("Augeas: Moving %s to %s" % (self.source, self.dest)) + return self.augeas.move(self.source, self.dest) + + +class Set(AugeasCommand): + def __init__(self, command, augeas, logger): + AugeasCommand.__init__(self, command, augeas, logger) + self.value = self.command.get("value") + + def verify(self): + return self._verify_set(self.value) + + def install(self): + self.logger.debug("Augeas: Setting %s to %s" % (self.get_path(), + self.value)) + return self.augeas.set(self.get_path(), self.value) + + +class Clear(Set): + def __init__(self, command, augeas, logger): + Set.__init__(self, command, augeas, logger) + self.value = None + + +class SetMulti(AugeasCommand): + def __init__(self, command, augeas, logger): + AugeasCommand.__init__(self, command, augeas, logger) + self.sub = self.command.get("sub") + self.value = self.command.get("value") + self.base = self.get_path("base") + + def verify(self): + return all(self._verify_set(self.value, + path="%s/%s" % (path, self.sub)) + for path in self.augeas.match(self.base)) + + def install(self): + return self.augeas.setm(self.base, self.sub, self.value) + + +class Insert(AugeasCommand): + def __init__(self, command, augeas, logger): + AugeasCommand.__init__(self, command, augeas, logger) + self.label = self.command.get("label") + self.where = self.command.get("where", "before") + self.before = self.where == "before" + + def verify(self): + return self._verify_exists("%s/../%s" % (self.get_path(), self.label)) + + def install(self): + self.logger.debug("Augeas: Inserting new %s %s %s" % + (self.label, self.where, self.get_path())) + return self.augeas.insert(self.get_path(), self.label, self.before) + + +class POSIXAugeas(POSIXTool): + """ Handle entries. See + :ref:`client-tools-augeas`. """ + + __handles__ = [('Path', 'augeas')] + __req__ = {'Path': ['type', 'name', 'setting', 'value']} + + def __init__(self, logger, setup, config): + POSIXTool.__init__(self, logger, setup, config) + self._augeas = dict() + + def get_augeas(self, entry): + if entry.get("name") not in self._augeas: + aug = augeas.augeas() + if entry.get("lens"): + self.logger.debug("Augeas: Adding %s to include path for %s" % + (entry.get("name"), entry.get("lens"))) + incl = "/augeas/load/%s/incl" % entry.get("lens") + ilen = len(aug.match(incl)) + if ilen == 0: + self.logger.error("Augeas: Lens %s does not exist" % + entry.get("lens")) + else: + aug.set("%s[%s]" % (incl, ilen + 1), entry.get("name")) + aug.load() + self._augeas[entry.get("name")] = aug + return self._augeas[entry.get("name")] + + def fully_specified(self, entry): + return entry.text is not None + + def get_commands(self, entry, unverified=False): + rv = [] + for cmd in entry.iterchildren(): + if cmd.tag in globals(): + rv.append(globals()[cmd.tag](cmd, self.get_augeas(entry), + self.logger)) + else: + err = "Augeas: Unknown command %s in %s" % (cmd.tag, + entry.get("name")) + self.logger.error(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), err])) + return rv + + def verify(self, entry, modlist): + rv = True + for cmd in self.get_commands(entry): + try: + if not cmd.verify(): + err = "Augeas: Command has not been applied to %s: %s" % \ + (entry.get("name"), cmd) + self.logger.debug(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), + err])) + rv = False + cmd.command.set("verified", "false") + else: + cmd.command.set("verified", "true") + except: # pylint: disable=W0702 + err = "Augeas: Unexpected error verifying %s: %s: %s" % \ + (entry.get("name"), cmd, sys.exc_info()[1]) + self.logger.error(err) + entry.set('qtext', "\n".join([entry.get('qtext', ''), err])) + rv = False + cmd.command.set("verified", "false") + return POSIXTool.verify(self, entry, modlist) and rv + + def install(self, entry): + rv = True + for cmd in self.get_commands(entry, unverified=True): + try: + cmd.install() + except: # pylint: disable=W0702 + self.logger.error( + "Failure running Augeas command on %s: %s: %s" % + (entry.get("name"), cmd, sys.exc_info()[1])) + rv = False + try: + self.get_augeas(entry).save() + except: # pylint: disable=W0702 + self.logger.error( + "Failure saving Augeas changes to %s: %s" % + (entry.get("name"), sys.exc_info()[1])) + rv = False + return POSIXTool.install(self, entry) and rv -- cgit v1.2.3-1-g7c22 From 96f1e72d5da30bb4d3c9b63cff010c8048d9e51d Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 14:47:09 -0500 Subject: Encryption: fixed unit tests --- src/lib/Bcfg2/Encryption.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py index b4674d72f..6d16748d5 100755 --- a/src/lib/Bcfg2/Encryption.py +++ b/src/lib/Bcfg2/Encryption.py @@ -116,11 +116,11 @@ def ssl_decrypt(data, passwd, algorithm=ALGORITHM): # base64-decode the data data = b64decode(data) salt = data[8:16] - # pylint: disable=E1101 + # pylint: disable=E1101,E1121 hashes = [md5(passwd + salt).digest()] for i in range(1, 3): hashes.append(md5(hashes[i - 1] + passwd + salt).digest()) - # pylint: enable=E1101 + # pylint: enable=E1101,E1121 key = hashes[0] + hashes[1] iv = hashes[2] @@ -146,11 +146,11 @@ def ssl_encrypt(plaintext, passwd, algorithm=ALGORITHM, salt=None): if salt is None: salt = Rand.rand_bytes(8) - # pylint: disable=E1101 + # pylint: disable=E1101,E1121 hashes = [md5(passwd + salt).digest()] for i in range(1, 3): hashes.append(md5(hashes[i - 1] + passwd + salt).digest()) - # pylint: enable=E1101 + # pylint: enable=E1101,E1121 key = hashes[0] + hashes[1] iv = hashes[2] -- cgit v1.2.3-1-g7c22 From c425991254dd11163455882fb8aaf918c9274c10 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 14:47:52 -0500 Subject: POSIX: skip loading POSIX sub-tools that raise ImportError This mimics the behavior for "real" tools --- src/lib/Bcfg2/Client/Tools/POSIX/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py index 7708c4f72..8d64cf84d 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/__init__.py @@ -47,8 +47,11 @@ class POSIX(Bcfg2.Client.Tools.Tool): mname = submodule[1].rsplit('.', 1)[-1] if mname == 'base': continue - module = getattr(__import__(submodule[1]).Client.Tools.POSIX, - mname) + try: + module = getattr(__import__(submodule[1]).Client.Tools.POSIX, + mname) + except ImportError: + continue hdlr = getattr(module, "POSIX" + mname) if POSIXTool in hdlr.__mro__: # figure out what entry type this handler handles -- cgit v1.2.3-1-g7c22 From 5050cdeb3e7635b1d32d354c30c7acef5f1c9c43 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 14:48:35 -0500 Subject: Augeas: Only install unverified commands --- src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py index cda9a1e3b..55f3d5cf7 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py @@ -158,6 +158,8 @@ class POSIXAugeas(POSIXTool): def get_commands(self, entry, unverified=False): rv = [] for cmd in entry.iterchildren(): + if unverified and cmd.get("verified", "false") != "false": + continue if cmd.tag in globals(): rv.append(globals()[cmd.tag](cmd, self.get_augeas(entry), self.logger)) -- cgit v1.2.3-1-g7c22 From 81166aa3b38fe6f4554c72f3733f2d0153f18978 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 14:48:42 -0500 Subject: Augeas: Added docstrings, fixed some minor pylint issues --- src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py | 111 +++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py index 55f3d5cf7..5e5412fed 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py @@ -7,36 +7,82 @@ from Bcfg2.Client.Tools.POSIX.base import POSIXTool class AugeasCommand(object): - def __init__(self, command, augeas, logger): - self.augeas = augeas + """ Base class for all Augeas command objects """ + + def __init__(self, command, augeas_obj, logger): + self._augeas = augeas_obj self.command = command self.entry = self.command.getparent() self.logger = logger def get_path(self, attr="path"): + """ Get a fully qualified path from the name of the parent entry and + the path given in this command tag. + + @param attr: The attribute to get the relative path from + @type attr: string + @returns: string - the fully qualified Augeas path + + """ return "/files/%s/%s" % (self.entry.get("name").strip("/"), self.command.get(attr).lstrip("/")) def _exists(self, path): - return len(self.augeas.match(path)) > 1 + """ Return True if a path exists in Augeas, False otherwise. + + Note that a False return can mean many things: A file that + doesn't exist, a node within the file that doesn't exist, no + lens to parse the file, etc. """ + return len(self._augeas.match(path)) > 1 def _verify_exists(self, path=None): + """ Verify that the given path exists, with friendly debug + logging. + + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path exists + """ if path is None: path = self.get_path() self.logger.debug("Augeas: Verifying that '%s' exists" % path) return self._exists(path) def _verify_not_exists(self, path=None): + """ Verify that the given path does not exist, with friendly + debug logging. + + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path does not exist. + (I.e., True if it does not exist, False if it does + exist.) + """ if path is None: path = self.get_path() self.logger.debug("Augeas: Verifying that '%s' does not exist" % path) return not self._exists(path) def _verify_set(self, expected, path=None): + """ Verify that the given path is set to the given value, with + friendly debug logging. + + @param expected: The expected value of the node. + @param path: The path to verify existence of. Defaults to the + result of + :func:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand.getpath`. + @type path: string + @returns: bool - Whether or not the path matches the expected value. + + """ if path is None: path = self.get_path() self.logger.debug("Augeas: Verifying '%s' == '%s'" % (path, expected)) - actual = self.augeas.get(path) + actual = self._augeas.get(path) if actual == expected: return True else: @@ -47,19 +93,29 @@ class AugeasCommand(object): def __str__(self): return Bcfg2.Client.XML.tostring(self.command) + def verify(self): + """ Verify that the command has been applied. """ + raise NotImplementedError + + def install(self): + """ Run the command. """ + raise NotImplementedError + class Remove(AugeasCommand): + """ Augeas ``rm`` command """ def verify(self): return self._verify_not_exists() def install(self): self.logger.debug("Augeas: Removing %s" % self.get_path()) - return self.augeas.remove(self.get_path()) + return self._augeas.remove(self.get_path()) class Move(AugeasCommand): - def __init__(self, command, augeas, logger): - AugeasCommand.__init__(self, command, augeas, logger) + """ Augeas ``move`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) self.source = self.get_path("source") self.dest = self.get_path("destination") @@ -69,12 +125,13 @@ class Move(AugeasCommand): def install(self): self.logger.debug("Augeas: Moving %s to %s" % (self.source, self.dest)) - return self.augeas.move(self.source, self.dest) + return self._augeas.move(self.source, self.dest) class Set(AugeasCommand): - def __init__(self, command, augeas, logger): - AugeasCommand.__init__(self, command, augeas, logger) + """ Augeas ``set`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) self.value = self.command.get("value") def verify(self): @@ -83,18 +140,20 @@ class Set(AugeasCommand): def install(self): self.logger.debug("Augeas: Setting %s to %s" % (self.get_path(), self.value)) - return self.augeas.set(self.get_path(), self.value) + return self._augeas.set(self.get_path(), self.value) class Clear(Set): - def __init__(self, command, augeas, logger): - Set.__init__(self, command, augeas, logger) + """ Augeas ``clear`` command """ + def __init__(self, command, augeas_obj, logger): + Set.__init__(self, command, augeas_obj, logger) self.value = None class SetMulti(AugeasCommand): - def __init__(self, command, augeas, logger): - AugeasCommand.__init__(self, command, augeas, logger) + """ Augeas ``setm`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) self.sub = self.command.get("sub") self.value = self.command.get("value") self.base = self.get_path("base") @@ -102,15 +161,16 @@ class SetMulti(AugeasCommand): def verify(self): return all(self._verify_set(self.value, path="%s/%s" % (path, self.sub)) - for path in self.augeas.match(self.base)) + for path in self._augeas.match(self.base)) def install(self): - return self.augeas.setm(self.base, self.sub, self.value) + return self._augeas.setm(self.base, self.sub, self.value) class Insert(AugeasCommand): - def __init__(self, command, augeas, logger): - AugeasCommand.__init__(self, command, augeas, logger) + """ Augeas ``ins`` command """ + def __init__(self, command, augeas_obj, logger): + AugeasCommand.__init__(self, command, augeas_obj, logger) self.label = self.command.get("label") self.where = self.command.get("where", "before") self.before = self.where == "before" @@ -121,7 +181,7 @@ class Insert(AugeasCommand): def install(self): self.logger.debug("Augeas: Inserting new %s %s %s" % (self.label, self.where, self.get_path())) - return self.augeas.insert(self.get_path(), self.label, self.before) + return self._augeas.insert(self.get_path(), self.label, self.before) class POSIXAugeas(POSIXTool): @@ -136,6 +196,7 @@ class POSIXAugeas(POSIXTool): self._augeas = dict() def get_augeas(self, entry): + """ Get an augeas object for the given entry. """ if entry.get("name") not in self._augeas: aug = augeas.augeas() if entry.get("lens"): @@ -156,6 +217,16 @@ class POSIXAugeas(POSIXTool): return entry.text is not None def get_commands(self, entry, unverified=False): + """ Get a list of commands to verify or install. + + @param entry: The entry to get commands from. + @type entry: lxml.etree._Element + @param unverified: Only get commands that failed verification. + @type unverified: bool + @returns: list of + :class:`Bcfg2.Client.Tools.POSIX.Augeas.AugeasCommand` + objects representing the commands. + """ rv = [] for cmd in entry.iterchildren(): if unverified and cmd.get("verified", "false") != "false": -- cgit v1.2.3-1-g7c22 From 1c335d124031b28d240559770c2a45059bb4e273 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 Dec 2013 15:01:30 -0500 Subject: Augeas: avoid deprecation warning --- src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py index 5e5412fed..81c948d0d 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX/Augeas.py @@ -1,8 +1,8 @@ """ Augeas driver """ import sys -import augeas import Bcfg2.Client.XML +from augeas import Augeas from Bcfg2.Client.Tools.POSIX.base import POSIXTool @@ -198,7 +198,7 @@ class POSIXAugeas(POSIXTool): def get_augeas(self, entry): """ Get an augeas object for the given entry. """ if entry.get("name") not in self._augeas: - aug = augeas.augeas() + aug = Augeas() if entry.get("lens"): self.logger.debug("Augeas: Adding %s to include path for %s" % (entry.get("name"), entry.get("lens"))) -- cgit v1.2.3-1-g7c22 From e972ae4e3dfc127bb64d3a72b2ff72594712b2a0 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 6 Dec 2013 11:23:57 -0500 Subject: Probes: fixed default list of allowed probe groups --- src/lib/Bcfg2/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index f47ac00d2..206c63d4f 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -653,7 +653,7 @@ SERVER_CHILDREN = \ SERVER_PROBE_ALLOWED_GROUPS = \ Option('Whitespace-separated list of group names (as regex) to which ' 'probes can assign a client by writing "group:" to stdout.', - default=['.*'], + default=[re.compile('.*')], cf=('probes', 'allowed_groups'), cook=list_split_anchored_regex) -- cgit v1.2.3-1-g7c22 From 2695e7a9af097596527edb52a722d17ea44601cc Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Sun, 8 Dec 2013 21:21:11 -0500 Subject: Cfg: let EncryptedGenerator load setup object whenever the plugin is imported Previously, if CfgEncryptedGenerator was imported before the Cfg object was instantiated, it would finalize the Bcfg2.Server.Plugins.Cfg.SETUP object with a value of None, and would be unable to access the options dict. --- src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py index 3b4703ddb..cf7eae75b 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py @@ -1,8 +1,9 @@ """ CfgEncryptedGenerator lets you encrypt your plaintext :ref:`server-plugins-generators-cfg` files on the server. """ +import Bcfg2.Server.Plugins.Cfg from Bcfg2.Server.Plugin import PluginExecutionError -from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP +from Bcfg2.Server.Plugins.Cfg import CfgGenerator try: from Bcfg2.Encryption import bruteforce_decrypt, EVPError, \ get_algorithm @@ -34,8 +35,10 @@ class CfgEncryptedGenerator(CfgGenerator): return # todo: let the user specify a passphrase by name try: - self.data = bruteforce_decrypt(self.data, setup=SETUP, - algorithm=get_algorithm(SETUP)) + self.data = bruteforce_decrypt( + self.data, + setup=Bcfg2.Server.Plugins.Cfg.SETUP, + algorithm=get_algorithm(Bcfg2.Server.Plugins.Cfg.SETUP)) except EVPError: raise PluginExecutionError("Failed to decrypt %s" % self.name) handle_event.__doc__ = CfgGenerator.handle_event.__doc__ -- cgit v1.2.3-1-g7c22 From 91558c72f6905991c7fb3f24057ab9e41ecce434 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Sun, 8 Dec 2013 21:21:42 -0500 Subject: XMLSrc: Load XML in one step instead of separate read and parse --- src/lib/Bcfg2/Server/Plugin/helpers.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py index be9c9e8ae..3e7d68cd8 100644 --- a/src/lib/Bcfg2/Server/Plugin/helpers.py +++ b/src/lib/Bcfg2/Server/Plugin/helpers.py @@ -840,15 +840,10 @@ class XMLSrc(XMLFileBacked): def HandleEvent(self, _=None): """Read file upon update.""" - try: - data = open(self.name).read() - except IOError: - msg = "Failed to read file %s: %s" % (self.name, sys.exc_info()[1]) - self.logger.error(msg) - raise PluginExecutionError(msg) self.items = {} try: - xdata = lxml.etree.XML(data, parser=Bcfg2.Server.XMLParser) + xdata = lxml.etree.parse(self.name, + parser=Bcfg2.Server.XMLParser).getroot() except lxml.etree.XMLSyntaxError: msg = "Failed to parse file %s: %s" % (self.name, sys.exc_info()[1]) @@ -865,8 +860,6 @@ class XMLSrc(XMLFileBacked): self.logger.error(msg) raise PluginExecutionError(msg) - del xdata, data - def Cache(self, metadata): """Build a package dict for a given host.""" if self.cache is None or self.cache[0] != metadata: -- cgit v1.2.3-1-g7c22 From 9a4bc278b4cebf7dfe292951204e760ef70b828e Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Sun, 8 Dec 2013 21:21:50 -0500 Subject: bcfg2-lint: New TemplateAbuse plugin detects templated scripts TemplateAbuse detects templated scripts (either files that end with a known extension, or that start with a shebang line) and executables (based off of their permissions in info.xml) and warns about them, since templating scripts is dicey at best, and almost always better done by templating a config file for the script to read instead. --- src/lib/Bcfg2/Server/Lint/TemplateAbuse.py | 75 ++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/lib/Bcfg2/Server/Lint/TemplateAbuse.py (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py new file mode 100644 index 000000000..fca9d14a9 --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py @@ -0,0 +1,75 @@ +""" Check for templated scripts or executables. """ + +import os +import stat +import Bcfg2.Server.Lint +from Bcfg2.Compat import any # pylint: disable=W0622 +from Bcfg2.Server.Plugin import DEFAULT_FILE_METADATA +from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML +from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator +from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator +from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator import \ + CfgEncryptedGenshiGenerator +from Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator import \ + CfgEncryptedCheetahGenerator + + +class TemplateAbuse(Bcfg2.Server.Lint.ServerPlugin): + """ Check for templated scripts or executables. """ + templates = [CfgGenshiGenerator, CfgCheetahGenerator, + CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator] + extensions = [".pl", ".py", ".sh", ".rb"] + + def Run(self): + if 'Cfg' in self.core.plugins: + for entryset in self.core.plugins['Cfg'].entries.values(): + for entry in entryset.entries.values(): + if (self.HandlesFile(entry.name) and + any(isinstance(entry, t) for t in self.templates)): + self.check_template(entryset, entry) + + @classmethod + def Errors(cls): + return {"templated-script": "warning", + "templated-executable": "warning"} + + def check_template(self, entryset, entry): + """ Check a template to see if it's a script or an executable. """ + # first, check for a known script extension + ext = os.path.splitext(entryset.path)[1] + if ext in self.extensions: + self.LintError("templated-script", + "Templated script found: %s\n" + "File has a known script extension: %s\n" + "Template a config file for the script instead" % + (entry.name, ext)) + return + + # next, check for a shebang line + firstline = open(entry.name).readline() + if firstline.startswith("#!"): + self.LintError("templated-script", + "Templated script found: %s\n" + "File starts with a shebang: %s\n" + "Template a config file for the script instead" % + (entry.name, firstline)) + return + + # finally, check for executable permissions in info.xml + for entry in entryset.entries.values(): + if isinstance(entry, CfgInfoXML): + for pinfo in entry.infoxml.pnode.data.xpath("//FileInfo"): + try: + mode = int(pinfo.get("mode", + DEFAULT_FILE_METADATA['mode']), 8) + except ValueError: + # LintError will be produced by RequiredAttrs plugin + self.logger.warning("Non-octal mode: %s" % mode) + continue + if mode & stat.S_IXUSR != 0: + self.LintError( + "templated-executable", + "Templated executable found: %s\n" + "Template a config file for the executable instead" + % entry.name) + return -- cgit v1.2.3-1-g7c22 From 14578f009d568850a918930180600807baa6c194 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Sun, 8 Dec 2013 21:57:04 -0500 Subject: bcfg2-lint: New ValidateJSON plugin This plugin validates the JSON files that can be used by Properties and Ohai. --- src/lib/Bcfg2/Server/Lint/ValidateJSON.py | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/lib/Bcfg2/Server/Lint/ValidateJSON.py (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Lint/ValidateJSON.py b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py new file mode 100644 index 000000000..c4a82a5d2 --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py @@ -0,0 +1,70 @@ +"""Ensure that all JSON files in the Bcfg2 repository are +valid. Currently, the only plugins that uses JSON are Ohai and +Properties.""" + +import os +import sys +import glob +import fnmatch +import Bcfg2.Server.Lint + +try: + import json +except ImportError: + import simplejson as json + + +class ValidateJSON(Bcfg2.Server.Lint.ServerlessPlugin): + """Ensure that all JSON files in the Bcfg2 repository are + valid. Currently, the only plugins that uses JSON are Ohai and + Properties. """ + + def __init__(self, *args, **kwargs): + Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs) + + #: A list of file globs that give the path to JSON files. The + #: globs are extended :mod:`fnmatch` globs that also support + #: ``**``, which matches any number of any characters, + #: including forward slashes. + self.globs = ["Properties/*.json", "Ohai/*.json"] + self.files = self.get_files() + + def Run(self): + for path in self.files: + self.logger.debug("Validating JSON in %s" % path) + try: + json.load(open(path)) + except ValueError: + self.LintError("json-failed-to-parse", + "%s does not contain valid JSON: %s" % + (path, sys.exc_info()[1])) + + @classmethod + def Errors(cls): + return {"json-failed-to-parse": "error"} + + def get_files(self): + """Return a list of all JSON files to validate, based on + :attr:`Bcfg2.Server.Lint.ValidateJSON.ValidateJSON.globs`. """ + if self.files is not None: + listfiles = lambda p: fnmatch.filter(self.files, + os.path.join('*', p)) + else: + listfiles = lambda p: glob.glob(os.path.join(self.config['repo'], + p)) + + rv = [] + for path in self.globs: + if '/**/' in path: + if self.files is not None: + rv.extend(listfiles(path)) + else: # self.files is None + fpath, fname = path.split('/**/') + for root, _, files in \ + os.walk(os.path.join(self.config['repo'], + fpath)): + rv.extend([os.path.join(root, f) + for f in files if f == fname]) + else: + rv.extend(listfiles(path)) + return rv -- cgit v1.2.3-1-g7c22