diff --git a/README-extentions.rst b/README-extentions.rst index 97d78afc..61364a42 100644 --- a/README-extentions.rst +++ b/README-extentions.rst @@ -208,6 +208,82 @@ Instead of failing on the first undefinded reference error all missing reference group_errors: True +Use references in class names +----------------------------- + +Allows to use references in the class names. + +References pointed to in class names cannot themselves reference another key, they should be simple strings. + +To avoid pitfalls do not over-engineer your class references. They should be used only for core conditions and only for them. +A short example: `- system.wrodpress.db.${_class:database_backend}`. + +Best practices: +- use references in class names always load your global class specification prior the reference is used. +- structure your class references under parameters under one key (for example `_class`). +- use class references as a kind of "context" or "global" available options you always know what they are set. + +Class referencing for existing reclass users. Frequently when constructing your models you had to load or not load some +classes based on your setup. In most cases this lead to fork of a model or introducing kind of template generator (like cookiecutter) to +create a model based on the base "context" or "global" variables. Class referencing is a simple way how to avoid +"pre-processors" like this and if/else conditions around class section. + + +Assuming following class setup: + +* node is loading `third.yml` class only + + +Classes: + +.. code-block:: yaml + #/etc/reclass/classes/global.yml + parameters: + _class: + env: + override: 'env.dev' + lab: + name: default + + #/etc/reclass/classes/lab/env/dev.yml + parameters: + lab: + name: dev + + #/etc/reclass/classes/second.yml + classes: + - global + - lab.${_class:env:override} + + #/etc/reclass/classes/third.yml + classes: + - global + - second + + +Reclass --nodeinfo then returns: + +.. code-block:: yaml + + ... + ... + applications: [] + environment: base + exports: {} + classes: + - global + - lab.${_class:env:override} + - second + parameters: + _class: + env: + override: env.dev + lab: + name: dev + ... + ... + + Inventory Queries ----------------- diff --git a/reclass/core.py b/reclass/core.py index 9a23d895..1a08db89 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -21,7 +21,8 @@ from reclass.settings import Settings from reclass.output.yaml_outputter import ExplicitDumper from reclass.datatypes import Entity, Classes, Parameters, Exports -from reclass.errors import MappingFormatError, ClassNotFound, InvQueryClassNotFound, InvQueryError, InterpolationError +from reclass.errors import MappingFormatError, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError +from reclass.values.parser import Parser try: basestring @@ -30,6 +31,8 @@ class Core(object): + _parser = Parser() + def __init__(self, storage, class_mappings, settings, input_data=None): self._storage = storage self._class_mappings = class_mappings @@ -96,7 +99,7 @@ def _get_input_data_entity(self): p = Parameters(self._input_data, self._settings) return Entity(self._settings, parameters=p, name='input data') - def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, environment=None): + def _recurse_entity(self, entity, merge_base=None, context=None, seen=None, nodename=None, environment=None): if seen is None: seen = {} @@ -106,7 +109,19 @@ def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, env if merge_base is None: merge_base = Entity(self._settings, name='empty (@{0})'.format(nodename)) + if context is None: + context = Entity(self._settings, name='empty (@{0})'.format(nodename)) + for klass in entity.classes.as_list(): + if klass.count('$') > 0: + try: + klass = str(self._parser.parse(klass, self._settings).render(merge_base.parameters.as_dict(), {})) + except ResolveError as e: + try: + klass = str(self._parser.parse(klass, self._settings).render(context.parameters.as_dict(), {})) + except ResolveError as e: + raise ClassNameResolveError(klass, nodename, entity.uri) + if klass not in seen: try: class_entity = self._storage.get_class(klass, environment, self._settings) @@ -121,7 +136,7 @@ def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, env e.uri = entity.uri raise - descent = self._recurse_entity(class_entity, seen=seen, + descent = self._recurse_entity(class_entity, context=merge_base, seen=seen, nodename=nodename, environment=environment) # on every iteration, we merge the result of the recursive # descent into what we have so far… @@ -159,6 +174,8 @@ def _get_inventory(self, all_envs, environment, queries): node = self._node_entity(nodename) except ClassNotFound as e: raise InvQueryClassNotFound(e) + except ClassNameResolveError as e: + raise InvQueryClassNameResolveError(e) if queries is None: try: node.interpolate_exports() @@ -186,8 +203,8 @@ def _node_entity(self, nodename): seen = {} merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename, environment=node_entity.environment) - return self._recurse_entity(node_entity, merge_base, seen=seen, nodename=nodename, - environment=node_entity.environment) + return self._recurse_entity(node_entity, merge_base=merge_base, context=merge_base, seen=seen, + nodename=nodename, environment=node_entity.environment) def _nodeinfo(self, nodename, inventory): try: diff --git a/reclass/errors.py b/reclass/errors.py index a96c47bf..349e2425 100644 --- a/reclass/errors.py +++ b/reclass/errors.py @@ -158,6 +158,17 @@ def _get_error_message(self): return msg +class ClassNameResolveError(InterpolationError): + def __init__(self, classname, nodename, uri): + super(ClassNameResolveError, self).__init__(msg=None, uri=uri, nodename=nodename) + self.name = classname + + def _get_error_message(self): + msg = [ 'In {0}'.format(self.uri), + 'Class name {0} not resolvable'.format(self.name) ] + return msg + + class InvQueryClassNotFound(InterpolationError): def __init__(self, classNotFoundError, nodename=''): @@ -172,6 +183,19 @@ def _get_error_message(self): return msg +class InvQueryClassNameResolveError(InterpolationError): + def __init__(self, classNameResolveError, nodename=''): + super(InvQueryClassNameResolveError, self).__init__(msg=None, nodename=nodename) + self.classNameResolveError = classNameResolveError + self._traceback = self.classNameResolveError._traceback + + def _get_error_message(self): + msg = [ 'Inventory Queries:', + '-> {0}'.format(self.classNameResolveError.nodename) ] + msg.append(self.classNameResolveError._get_error_message()) + return msg + + class ResolveError(InterpolationError): def __init__(self, reference, uri=None, context=None):