From 32536152850a683e18935eb5223a5bd1410e9258 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 15 Oct 2012 10:27:47 -0400 Subject: added support for JSON and YAML properties files --- src/lib/Bcfg2/Server/Plugins/Probes.py | 12 +- src/lib/Bcfg2/Server/Plugins/Properties.py | 229 ++++++++++++++++++++++++++--- 2 files changed, 209 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index c63f015e5..c2a928e0f 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -41,16 +41,10 @@ except ImportError: HAS_JSON = False try: - import syck as yaml - import syck.error as YAMLError + import yaml HAS_YAML = True except ImportError: - try: - import yaml - from yaml import YAMLError - HAS_YAML = True - except ImportError: - HAS_YAML = False + HAS_YAML = False class ClientProbeDataSet(dict): @@ -110,7 +104,7 @@ class ProbeData(str): if self._yaml is None and HAS_YAML: try: self._yaml = yaml.load(self.data) - except YAMLError: + except yaml.YAMLError: pass return self._yaml diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py index d9e622645..aef5238c6 100644 --- a/src/lib/Bcfg2/Server/Plugins/Properties.py +++ b/src/lib/Bcfg2/Server/Plugins/Properties.py @@ -16,17 +16,42 @@ try: except ImportError: HAS_CRYPTO = False +try: + import json + HAS_JSON = True +except ImportError: + try: + import simplejson as json + HAS_JSON = True + except ImportError: + HAS_JSON = False + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + LOGGER = logging.getLogger(__name__) SETUP = None -class PropertyFile(Bcfg2.Server.Plugin.StructFile): - """ Class for properties files. """ +class PropertyFile(object): + """ Base Properties file handler """ + + def __init__(self, name): + """ + :param name: The filename of this properties file. + + .. automethod:: _write + """ + self.name = name def write(self): """ Write the data in this data structure back to the property - file """ + file. This public method performs checking to ensure that + writing is possible and then calls :func:`_write`. """ if not SETUP.cfp.getboolean("properties", "writes_enabled", default=True): msg = "Properties files write-back is disabled in the " + \ @@ -39,19 +64,123 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile): msg = "Cannot write %s: %s" % (self.name, sys.exc_info()[1]) LOGGER.error(msg) raise PluginExecutionError(msg) - try: - open(self.name, "wb").write( - lxml.etree.tostring(self.xdata, - xml_declaration=False, - pretty_print=True).decode('UTF-8')) - return True + return self._write() except IOError: err = sys.exc_info()[1] msg = "Failed to write %s: %s" % (self.name, err) LOGGER.error(msg) raise PluginExecutionError(msg) + def _write(self): + """ Write the data in this data structure back to the property + file. """ + raise NotImplementedError + + def validate_data(self): + """ Verify that the data in this file is valid. """ + raise NotImplementedError + + def get_additional_data(self, metadata): # pylint: disable=W0613 + """ Get file data for inclusion in client metadata. """ + return copy.copy(self) + + +class JSONPropertyFile(Bcfg2.Server.Plugin.FileBacked, PropertyFile): + """ Handle JSON Properties files. """ + + def __init__(self, name, fam=None): + Bcfg2.Server.Plugin.FileBacked.__init__(self, name, fam=fam) + PropertyFile.__init__(self, name) + self.json = None + __init__.__doc__ = Bcfg2.Server.Plugin.FileBacked.__init__.__doc__ + + def Index(self): + try: + self.json = json.loads(self.data) + except ValueError: + err = sys.exc_info()[1] + raise PluginExecutionError("Could not load JSON data from %s: %s" % + (self.name, err)) + Index.__doc__ = Bcfg2.Server.Plugin.FileBacked.Index.__doc__ + + def _write(self): + json.dump(self.json, open(self.name, 'wb')) + return True + _write.__doc__ = PropertyFile._write.__doc__ + + def validate_data(self): + try: + json.dumps(self.json) + except: + err = sys.exc_info()[1] + raise PluginExecutionError("Data for %s cannot be dumped to JSON: " + "%s" % (self.name, err)) + validate_data.__doc__ = PropertyFile.validate_data.__doc__ + + def __str__(self): + return str(self.json) + + def __repr__(self): + return repr(self.json) + + +class YAMLPropertyFile(Bcfg2.Server.Plugin.FileBacked, PropertyFile): + """ Handle YAML Properties files. """ + + def __init__(self, name, fam=None): + Bcfg2.Server.Plugin.FileBacked.__init__(self, name, fam=fam) + PropertyFile.__init__(self, name) + self.yaml = None + __init__.__doc__ = Bcfg2.Server.Plugin.FileBacked.__init__.__doc__ + + def Index(self): + try: + self.yaml = yaml.load(self.data) + except yaml.YAMLError: + err = sys.exc_info()[1] + raise PluginExecutionError("Could not load YAML data from %s: %s" % + (self.name, err)) + Index.__doc__ = Bcfg2.Server.Plugin.FileBacked.Index.__doc__ + + def _write(self): + yaml.dump(self.yaml, open(self.name, 'wb')) + return True + _write.__doc__ = PropertyFile._write.__doc__ + + def validate_data(self): + try: + yaml.dump(self.yaml) + except yaml.YAMLError: + err = sys.exc_info()[1] + raise PluginExecutionError("Data for %s cannot be dumped to YAML: " + "%s" % (self.name, err)) + validate_data.__doc__ = PropertyFile.validate_data.__doc__ + + def __str__(self): + return str(self.yaml) + + def __repr__(self): + return repr(self.yaml) + + +class XMLPropertyFile(Bcfg2.Server.Plugin.StructFile, PropertyFile): + """ Handle XML Properties files. """ + + def __init__(self, name, fam=None, should_monitor=False): + Bcfg2.Server.Plugin.StructFile.__init__(self, name, fam=fam, + should_monitor=should_monitor) + PropertyFile.__init__(self, name) + __init__.__doc__ = Bcfg2.Server.Plugin.StructFile.__init__.__doc__ + + def _write(self): + open(self.name, "wb").write( + lxml.etree.tostring(self.xdata, + xml_declaration=False, + pretty_print=True).decode('UTF-8')) + return True + _write.__doc__ = PropertyFile._write.__doc__ + def validate_data(self): """ ensure that the data in this object validates against the XML schema for this property file (if a schema exists) """ @@ -73,6 +202,7 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile): self.name) else: return True + validate_data.__doc__ = PropertyFile.validate_data.__doc__ def Index(self): Bcfg2.Server.Plugin.StructFile.Index(self) @@ -89,6 +219,7 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile): self.name) LOGGER.error(msg) raise PluginExecutionError(msg) + Index.__doc__ = Bcfg2.Server.Plugin.StructFile.Index.__doc__ def _decrypt(self, element): """ Decrypt a single encrypted properties file element """ @@ -111,19 +242,77 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile): algorithm=get_algorithm(SETUP)) raise EVPError("Failed to decrypt") + def get_additional_data(self, metadata): + if SETUP.cfp.getboolean("properties", "automatch", default=False): + default_automatch = "true" + else: + default_automatch = "false" + + if self.xdata.get("automatch", default_automatch).lower() == "true": + return self.XMLMatch(metadata) + else: + return copy.copy(self) + + def __str__(self): + return str(self.xdata) + + def __repr__(self): + return repr(self.xdata) + class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked): - """ A collection of properties files """ - __child__ = PropertyFile - patterns = re.compile(r'.*\.xml$') + """ A collection of properties files. """ + extensions = ["xml"] + if HAS_JSON: + extensions.append("json") + if HAS_YAML: + extensions.extend(["yaml", "yml"]) + + #: Only track and include files whose names and paths match this + #: regex. Created on-the-fly based on which libraries are + #: installed (and thus which data formats are supported). + #: Candidates are ``.xml`` (always supported), ``.json``, + #: ``.yaml``, and ``.yml``. + patterns = re.compile(r'.*\.%s$' % '|'.join(extensions)) + + #: Ignore XML schema (``.xsd``) files ignore = re.compile(r'.*\.xsd$') + def __init__(self, data, fam): + Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, data, fam) + + #: Instead of creating children of this object with a static + #: object, we use :func:`property_dispatcher` to create a + #: child of the appropriate subclass of :class:`PropertyFile` + self.__child__ = self.property_dispatcher + __init__.__doc__ = Bcfg2.Server.Plugin.DirectoryBacked.__init__.__doc__ + + def property_dispatcher(self, fname, fam): + """ Dispatch an event on a Properties file to the + appropriate object. + + :param fname: The name of the file that received the event + :type fname: string + :param fam: The file monitor the event was received by + :type fam: Bcfg2.Server.FileMonitor.FileMonitor + :returns: An object of the appropriate subclass of + :class:`PropertyFile` + """ + if fname.endswith(".xml"): + return XMLPropertyFile(fname, fam) + elif HAS_JSON and fname.endswith(".json"): + return JSONPropertyFile(fname, fam) + elif HAS_YAML and (fname.endswith(".yaml") or fname.endswith(".yml")): + return YAMLPropertyFile(fname, fam) + else: + raise Bcfg2.Server.Plugin.PluginExecutionError( + "Properties: Unknown extension %s" % fname) + class Properties(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Connector): """ The properties plugin maps property files into client metadata instances. """ - name = 'Properties' def __init__(self, core, datastore): global SETUP # pylint: disable=W0603 @@ -138,18 +327,12 @@ class Properties(Bcfg2.Server.Plugin.Plugin, raise Bcfg2.Server.Plugin.PluginInitError SETUP = core.setup + __init__.__doc__ = Bcfg2.Server.Plugin.Plugin.__init__.__doc__ def get_additional_data(self, metadata): - if self.core.setup.cfp.getboolean("properties", "automatch", - default=False): - default_automatch = "true" - else: - default_automatch = "false" rv = dict() for fname, pfile in self.store.entries.items(): - if pfile.xdata.get("automatch", - default_automatch).lower() == "true": - rv[fname] = pfile.XMLMatch(metadata) - else: - rv[fname] = copy.copy(pfile) + rv[fname] = pfile.get_additional_data(metadata) return rv + get_additional_data.__doc__ = \ + Bcfg2.Server.Plugin.Connector.get_additional_data.__doc__ -- cgit v1.2.3-1-g7c22