Merge pull request #43 from salt-formulas/pr/38
Pr/38 - rebased
diff --git a/README-extentions.rst b/README-extentions.rst
index 97d78af..61364a4 100644
--- a/README-extentions.rst
+++ b/README-extentions.rst
@@ -208,6 +208,82 @@
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 9a23d89..1a08db8 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 @@
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 @@
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 @@
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 @@
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 @@
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 a96c47b..349e242 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -158,6 +158,17 @@
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 @@
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):