diff --git a/README-extensions.rst b/README-extensions.rst index e67e441d..7b537cfe 100644 --- a/README-extensions.rst +++ b/README-extensions.rst @@ -33,7 +33,7 @@ To control the feature there are two options available: .. code-block:: yaml ignore_class_notfound: False - ignore_class_regexp: ['.*'] + ignore_class_notfound_regexp: ['.*'] If you set regexp pattern to ``service.*`` all missing classes starting 'service.' will be logged with warning, but will not fail to return rendered reclass. Assuming all parameter interpolation passes. @@ -691,3 +691,38 @@ classes for the pre-prod environment to use a directory on the local disc: storage_type: yaml_fs # options for yaml_fs storage type uri: /srv/salt/env/pre-prod/classes + + +Support to use current node parameters as references in class name +------------------------------------------------------------------ + +With the following reclass config: + +.. code-block:: + + => /etc/reclass/nodes/mynode.yml + classes: + - common + parameters: + project: myproject + + => /etc/reclass/classes/common.yml + class: + - ${project} + + => /etc/reclass/classes/myproject.yml + parameters: + some: + project: parameters + + +Will get the following result for the parameters: + +.. code-block:: yaml + + parameters: + project: myproject + some: + project: parameters + + diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index dccf34fe..249b0a4a 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -5,6 +5,10 @@ ChangeLog ========= ========== ======================================================== Version Date Changes ========= ========== ======================================================== +1.7.0 2020-10-02 Fixes and few new features: + * Allow class mappings to wildcard match against either the node name and class + * Support for .yaml along with .yml + * Support to use current node parameters as references in class name 1.6.0 2018-11-06 * Python code and parser refactoring by a-ovchinnikov * Improvements in yaml_git and mixed setup by Andrew Pickford * Relative paths in class names by Petr Michalec, Martin Polreich and Andrew Pickford diff --git a/doc/source/operations.rst b/doc/source/operations.rst index f744148a..08b34e5f 100644 --- a/doc/source/operations.rst +++ b/doc/source/operations.rst @@ -101,6 +101,13 @@ end with ``.ch`` (again, note the escaped leading asterisk). Multiple classes can be assigned to each mapping by providing a space-separated list (class names cannot contain spaces anyway). +By default the class mappings regex match is done against the node name. This can +be changed to do the match against the path of the node file from the classes +directory, but dropping the .yml extension at the end of the node file. This is +controlled with the setting class_mappings_match_path. When False (the +default) the match is done again the node name and when true the match is done +against the node file path. + .. warning:: The class mappings do not really belong in the configuration file, as they diff --git a/reclass/core.py b/reclass/core.py index 3e0ab34d..f35450f4 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -25,6 +25,7 @@ from reclass.settings import Settings from reclass.datatypes import Entity, Classes, Parameters, Exports from reclass.errors import MappingFormatError, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError +from reclass.values import NodeInventory from reclass.values.parser import Parser @@ -72,26 +73,30 @@ def _shlex_split(instr): key = '/{0}/'.format(key) return key, list(lexer) - def _get_class_mappings_entity(self, nodename): + def _get_class_mappings_entity(self, entity): if not self._class_mappings: return Entity(self._settings, name='empty (class mappings)') c = Classes() + if self._settings.class_mappings_match_path: + matchname = entity.pathname + else: + matchname = entity.name for mapping in self._class_mappings: matched = False key, klasses = Core._shlex_split(mapping) if key[0] == ('/'): - matched = Core._match_regexp(key[1:-1], nodename) + matched = Core._match_regexp(key[1:-1], matchname) if matched: for klass in klasses: c.append_if_new(matched.expand(klass)) else: - if Core._match_glob(key, nodename): + if Core._match_glob(key, matchname): for klass in klasses: c.append_if_new(klass) return Entity(self._settings, classes=c, - name='class mappings for node {0}'.format(nodename)) + name='class mappings for node {0}'.format(entity.name)) def _get_input_data_entity(self): if not self._input_data: @@ -167,7 +172,29 @@ def _get_automatic_parameters(self, nodename, environment): else: return Parameters({}, self._settings, '') + def _get_scalar_parameters(self, node_parameters): + if self._settings.scalar_parameters: + scalars = node_parameters.as_dict().get( + self._settings.scalar_parameters, {}) + return Parameters( + {self._settings.scalar_parameters: scalars}, self._settings, '__scalar__') + else: + return Parameters({}, self._settings, '') + def _get_inventory(self, all_envs, environment, queries): + ''' + Returns a dictionary of NodeInventory objects, one per matching node. Exports + which are required for the given queries (or all exports if the queries is None) + are rendered, remaining exports are left unrendered. + + Args: + all_envs - if True match export values from nodes in any environment + else if False match only for nodes in the same environment as the + environment parameter + environment - node environment to match against if all_envs is False + queries - list of inventory queries to determine required export values + or if None match all exports defined by a node + ''' inventory = {} for nodename in self._storage.enumerate_nodes(): try: @@ -187,6 +214,7 @@ def _get_inventory(self, all_envs, environment, queries): except ClassNameResolveError as e: raise InvQueryClassNameResolveError(e) if queries is None: + # This only happens if reclass is called with the --inventory option try: node.interpolate_exports() except InterpolationError as e: @@ -199,7 +227,7 @@ def _get_inventory(self, all_envs, environment, queries): except InterpolationError as e: e.nodename = nodename raise InvQueryError(q.contents, e, context=p, uri=q.uri) - inventory[nodename] = node.exports.as_dict() + inventory[nodename] = NodeInventory(node.exports.as_dict(), node_base.environment == environment) return inventory def _node_entity(self, nodename): @@ -207,9 +235,10 @@ def _node_entity(self, nodename): if node_entity.environment == None: node_entity.environment = self._settings.default_environment base_entity = Entity(self._settings, name='base') - base_entity.merge(self._get_class_mappings_entity(node_entity.name)) + base_entity.merge(self._get_class_mappings_entity(node_entity)) base_entity.merge(self._get_input_data_entity()) base_entity.merge_parameters(self._get_automatic_parameters(nodename, node_entity.environment)) + base_entity.merge_parameters(self._get_scalar_parameters(node_entity.parameters)) seen = {} merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename, environment=node_entity.environment) diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py index 2e0e1e43..88b5afe5 100644 --- a/reclass/datatypes/entity.py +++ b/reclass/datatypes/entity.py @@ -24,9 +24,10 @@ class Entity(object): ''' def __init__(self, settings, classes=None, applications=None, parameters=None, exports=None, uri=None, name=None, - environment=None): + pathname=None, environment=None): self._uri = '' if uri is None else uri self._name = '' if name is None else name + self._pathname = '' if pathname is None else pathname self._classes = self._set_field(classes, Classes) self._applications = self._set_field(applications, Applications) pars = [None, settings, uri] @@ -36,6 +37,7 @@ def __init__(self, settings, classes=None, applications=None, name = property(lambda s: s._name) uri = property(lambda s: s._uri) + pathname = property(lambda s: s._pathname) classes = property(lambda s: s._classes) applications = property(lambda s: s._applications) parameters = property(lambda s: s._parameters) @@ -101,10 +103,10 @@ def __ne__(self, other): return not self.__eq__(other) def __repr__(self): - return "%s(%r, %r, %r, %r, uri=%r, name=%r, environment=%r)" % ( + return "%s(%r, %r, %r, %r, uri=%r, name=%r, pathname=%r, environment=%r)" % ( self.__class__.__name__, self.classes, self.applications, self.parameters, self.exports, self.uri, self.name, - self.environment) + self.pathname, self.environment) def as_dict(self): return {'classes': self._classes.as_list(), diff --git a/reclass/datatypes/tests/test_entity.py b/reclass/datatypes/tests/test_entity.py index f18f3fcc..a2b71b51 100644 --- a/reclass/datatypes/tests/test_entity.py +++ b/reclass/datatypes/tests/test_entity.py @@ -11,9 +11,12 @@ from __future__ import print_function from __future__ import unicode_literals +from six import iteritems + from reclass.settings import Settings from reclass.datatypes import Entity, Classes, Parameters, Applications, Exports from reclass.errors import ResolveError +from reclass.values import NodeInventory import unittest try: @@ -167,6 +170,9 @@ def test_as_dict(self, **types): class TestEntityNoMock(unittest.TestCase): + def _make_inventory(self, nodes): + return { name: NodeInventory(node, True) for name, node in iteritems(nodes) } + def test_interpolate_list_types(self): node1_exports = Exports({'exps': [ '${one}' ] }, SETTINGS, 'first') node1_parameters = Parameters({'alpha': [ '${two}', '${three}' ], 'one': 1, 'two': 2, 'three': 3 }, SETTINGS, 'first') @@ -174,33 +180,37 @@ def test_interpolate_list_types(self): node2_exports = Exports({'exps': '${alpha}' }, SETTINGS, 'second') node2_parameters = Parameters({}, SETTINGS, 'second') node2_entity = Entity(SETTINGS, classes=None, applications=None, parameters=node2_parameters, exports=node2_exports) - r = {'exps': [ 1, 2, 3 ]} + result = {'exps': [ 1, 2, 3 ]} node1_entity.merge(node2_entity) node1_entity.interpolate(None) self.assertIs(type(node1_entity.exports.as_dict()['exps']), list) - self.assertDictEqual(node1_entity.exports.as_dict(), r) + self.assertDictEqual(node1_entity.exports.as_dict(), result) def test_exports_with_refs(self): - inventory = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}} + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}}) node3_exports = Exports({'a': '${a}', 'b': '${b}'}, SETTINGS, '') node3_parameters = Parameters({'name': 'node3', 'a': '${c}', 'b': 5}, SETTINGS, '') node3_parameters.merge({'c': 3}) node3_entity = Entity(SETTINGS, classes=None, applications=None, parameters=node3_parameters, exports=node3_exports) node3_entity.interpolate_exports() - inventory['node3'] = node3_entity.exports.as_dict() - r = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}, 'node3': {'a': 3, 'b': 5}} - self.assertDictEqual(inventory, r) + inventory['node3'] = NodeInventory(node3_entity.exports.as_dict(), True) + result = { 'node1': NodeInventory({'a': 1, 'b': 2}, True), + 'node2': NodeInventory({'a': 3, 'b': 4}, True), + 'node3': NodeInventory({'a': 3, 'b': 5}, True) } + self.assertDictEqual(inventory, result) def test_reference_to_an_export(self): - inventory = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}} + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}}) node3_exports = Exports({'a': '${a}', 'b': '${b}'}, SETTINGS, '') node3_parameters = Parameters({'name': 'node3', 'ref': '${exp}', 'a': '${c}', 'b': 5}, SETTINGS, '') node3_parameters.merge({'c': 3, 'exp': '$[ exports:a ]'}) node3_entity = Entity(SETTINGS, classes=None, applications=None, parameters=node3_parameters, exports=node3_exports) node3_entity.interpolate_exports() - inventory['node3'] = node3_entity.exports.as_dict() + inventory['node3'] = NodeInventory(node3_entity.exports.as_dict(), True) node3_entity.interpolate(inventory) - res_inv = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}, 'node3': {'a': 3, 'b': 5}} + res_inv = { 'node1': NodeInventory({'a': 1, 'b': 2}, True), + 'node2': NodeInventory({'a': 3, 'b': 4}, True), + 'node3': NodeInventory({'a': 3, 'b': 5}, True) } res_params = {'a': 3, 'c': 3, 'b': 5, 'name': 'node3', 'exp': {'node1': 1, 'node3': 3, 'node2': 3}, 'ref': {'node1': 1, 'node3': 3, 'node2': 3}} self.assertDictEqual(node3_parameters.as_dict(), res_params) self.assertDictEqual(inventory, res_inv) @@ -218,40 +228,61 @@ def test_exports_multiple_nodes(self): for p, q in queries: node1_entity.interpolate_single_export(q) node2_entity.interpolate_single_export(q) - res_inv = {'node1': {'a': {'test': 1}}, 'node2': {'a': {'test': 2}}} - res_params = {'a': {'test': 1}, 'b': 1, 'name': 'node1', 'exp': {'node1': {'test': 1}, 'node2': {'test': 2}}} - inventory = {} - inventory['node1'] = node1_entity.exports.as_dict() - inventory['node2'] = node2_entity.exports.as_dict() + res_inv = { 'node1': NodeInventory({'a': {'test': 1}}, True), + 'node2': NodeInventory({'a': {'test': 2}}, True) } + res_params = { 'name': 'node1', + 'a': {'test': 1}, + 'b': 1, + 'exp': {'node1': {'test': 1}, 'node2': {'test': 2}} } + inventory = self._make_inventory({'node1': node1_entity.exports.as_dict(), 'node2': node2_entity.exports.as_dict()}) node1_entity.interpolate(inventory) self.assertDictEqual(node1_parameters.as_dict(), res_params) self.assertDictEqual(inventory, res_inv) def test_exports_with_ancestor_references(self): - inventory = {'node1': {'alpha' : {'beta': {'a': 1, 'b': 2}}}, 'node2': {'alpha' : {'beta': {'a': 3, 'b': 4}}}} + inventory = self._make_inventory({'node1': {'alpha' : {'beta': {'a': 1, 'b': 2}}}, 'node2': {'alpha' : {'beta': {'a': 3, 'b': 4}}}}) node3_exports = Exports({'alpha': '${alpha}'}, SETTINGS, '') node3_parameters = Parameters({'name': 'node3', 'alpha': {'beta' : {'a': 5, 'b': 6}}, 'exp': '$[ exports:alpha:beta ]'}, SETTINGS, '') node3_entity = Entity(SETTINGS, classes=None, applications=None, parameters=node3_parameters, exports=node3_exports) - res_params = {'exp': {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}, 'node3': {'a': 5, 'b': 6}}, 'name': 'node3', 'alpha': {'beta': {'a': 5, 'b': 6}}} - res_inv = {'node1': {'alpha' : {'beta': {'a': 1, 'b': 2}}}, 'node2': {'alpha' : {'beta': {'a': 3, 'b': 4}}}, 'node3': {'alpha' : {'beta': {'a': 5, 'b': 6}}}} + res_params = { 'name': 'node3', + 'exp': {'node1': {'a': 1, 'b': 2}, + 'node2': {'a': 3, 'b': 4}, + 'node3': {'a': 5, 'b': 6}}, + 'alpha': {'beta': {'a': 5, 'b': 6}} } + res_inv = { 'node1': NodeInventory({'alpha' : {'beta': {'a': 1, 'b': 2}}}, True), + 'node2': NodeInventory({'alpha' : {'beta': {'a': 3, 'b': 4}}}, True), + 'node3': NodeInventory({'alpha' : {'beta': {'a': 5, 'b': 6}}}, True) } node3_entity.initialise_interpolation() queries = node3_entity.parameters.get_inv_queries() for p, q in queries: node3_entity.interpolate_single_export(q) - inventory['node3'] = node3_entity.exports.as_dict() + inventory['node3'] = NodeInventory(node3_entity.exports.as_dict(), True) node3_entity.interpolate(inventory) self.assertDictEqual(node3_parameters.as_dict(), res_params) self.assertDictEqual(inventory, res_inv) def test_exports_with_nested_references(self): - inventory = {'node1': {'alpha': {'a': 1, 'b': 2}}, 'node2': {'alpha': {'a': 3, 'b': 4}}} + inventory = self._make_inventory({'node1': {'alpha': {'a': 1, 'b': 2}}, 'node2': {'alpha': {'a': 3, 'b': 4}}}) node3_exports = Exports({'alpha': '${alpha}'}, SETTINGS, '') - node3_parameters = Parameters({'name': 'node3', 'alpha': {'a': '${one}', 'b': '${two}'}, 'beta': '$[ exports:alpha ]', 'one': '111', 'two': '${three}', 'three': '123'}, SETTINGS, '') + node3_parameters = Parameters({ 'name': 'node3', + 'alpha': {'a': '${one}', 'b': '${two}'}, + 'beta': '$[ exports:alpha ]', + 'one': '111', + 'two': '${three}', + 'three': '123'}, + SETTINGS, '') node3_entity = Entity(SETTINGS, classes=None, applications=None, parameters=node3_parameters, exports=node3_exports) - res_params = {'beta': {'node1': {'a': 1, 'b': 2}, 'node3': {'a': '111', 'b': '123'}, 'node2': {'a': 3, 'b': 4}}, 'name': 'node3', 'alpha': {'a': '111', 'b': '123'}, 'three': '123', 'two': '123', 'one': '111'} - res_inv = {'node1': {'alpha': {'a': 1, 'b': 2}}, 'node2': {'alpha': {'a': 3, 'b': 4}}, 'node3': {'alpha': {'a': '111', 'b': '123'}}} + res_params = { 'name': 'node3', + 'alpha': { 'a': '111', 'b': '123' }, + 'beta': { 'node1': {'a': 1, 'b': 2 }, 'node2': { 'a': 3, 'b': 4}, 'node3': { 'a': '111', 'b': '123' } }, + 'one': '111', + 'two': '123', + 'three': '123' } + res_inv = { 'node1': NodeInventory({'alpha': {'a': 1, 'b': 2}}, True), + 'node2': NodeInventory({'alpha': {'a': 3, 'b': 4}}, True), + 'node3': NodeInventory({'alpha': {'a': '111', 'b': '123'}}, True) } node3_entity.interpolate_exports() - inventory['node3'] = node3_entity.exports.as_dict() + inventory['node3'] = NodeInventory(node3_entity.exports.as_dict(), True) node3_entity.interpolate(inventory) self.assertDictEqual(node3_parameters.as_dict(), res_params) self.assertDictEqual(inventory, res_inv) @@ -285,11 +316,12 @@ def test_exports_failed_render_ignore(self): for p, q in queries: node1_entity.interpolate_single_export(q) node2_entity.interpolate_single_export(q) - res_inv = {'node1': {'a': 1}, 'node2': {}} - res_params = { 'a': 1, 'name': 'node1', 'exp': {'node1': 1} } - inventory = {} - inventory['node1'] = node1_entity.exports.as_dict() - inventory['node2'] = node2_entity.exports.as_dict() + res_inv = { 'node1': NodeInventory({'a': 1}, True), + 'node2': NodeInventory({}, True) } + res_params = { 'name': 'node1', + 'a': 1, + 'exp': {'node1': 1} } + inventory = self._make_inventory({'node1': node1_entity.exports.as_dict(), 'node2': node2_entity.exports.as_dict()}) node1_entity.interpolate(inventory) self.assertDictEqual(node1_parameters.as_dict(), res_params) self.assertDictEqual(inventory, res_inv) diff --git a/reclass/datatypes/tests/test_exports.py b/reclass/datatypes/tests/test_exports.py index 16a45cb4..e0c5cc12 100644 --- a/reclass/datatypes/tests/test_exports.py +++ b/reclass/datatypes/tests/test_exports.py @@ -8,33 +8,39 @@ from __future__ import print_function from __future__ import unicode_literals +from six import iteritems + from reclass.utils.parameterdict import ParameterDict from reclass.utils.parameterlist import ParameterList from reclass.settings import Settings from reclass.datatypes import Exports, Parameters from reclass.errors import ParseError +from reclass.values import NodeInventory import unittest SETTINGS = Settings() class TestInvQuery(unittest.TestCase): + def _make_inventory(self, nodes): + return { name: NodeInventory(node, True) for name, node in iteritems(nodes) } + def test_overwrite_method(self): - e = Exports({'alpha': { 'one': 1, 'two': 2}}, SETTINGS, '') - d = {'alpha': { 'three': 3, 'four': 4}} - e.overwrite(d) - e.interpolate() - self.assertEqual(e.as_dict(), d) + exports = Exports({'alpha': { 'one': 1, 'two': 2}}, SETTINGS, '') + data = {'alpha': { 'three': 3, 'four': 4}} + exports.overwrite(data) + exports.interpolate() + self.assertEqual(exports.as_dict(), data) def test_interpolate_types(self): - e = Exports({'alpha': { 'one': 1, 'two': 2}, 'beta': [ 1, 2 ]}, SETTINGS, '') - r = {'alpha': { 'one': 1, 'two': 2}, 'beta': [ 1, 2 ]} - self.assertIs(type(e.as_dict()['alpha']), ParameterDict) - self.assertIs(type(e.as_dict()['beta']), ParameterList) - e.interpolate() - self.assertIs(type(e.as_dict()['alpha']), dict) - self.assertIs(type(e.as_dict()['beta']), list) - self.assertEqual(e.as_dict(), r) + exports = Exports({'alpha': { 'one': 1, 'two': 2}, 'beta': [ 1, 2 ]}, SETTINGS, '') + result = {'alpha': { 'one': 1, 'two': 2}, 'beta': [ 1, 2 ]} + self.assertIs(type(exports.as_dict()['alpha']), ParameterDict) + self.assertIs(type(exports.as_dict()['beta']), ParameterList) + exports.interpolate() + self.assertIs(type(exports.as_dict()['alpha']), dict) + self.assertIs(type(exports.as_dict()['beta']), list) + self.assertEqual(exports.as_dict(), result) def test_malformed_invquery(self): with self.assertRaises(ParseError): @@ -51,83 +57,121 @@ def test_malformed_invquery(self): p = Parameters({'exp': '$[ exports:a if exports:b == self:test_value anddd exports:c == self:test_value2 ]'}, SETTINGS, '') def test_value_expr_invquery(self): - e = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}} - p = Parameters({'exp': '$[ exports:a ]'}, SETTINGS, '') - r = {'exp': {'node1': 1, 'node2': 3}} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}}) + parameters = Parameters({'exp': '$[ exports:a ]'}, SETTINGS, '') + result = {'exp': {'node1': 1, 'node2': 3}} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_if_expr_invquery(self): - e = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}} - p = Parameters({'exp': '$[ exports:a if exports:b == 4 ]'}, SETTINGS, '') - r = {'exp': {'node2': 3}} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}}) + parameters = Parameters({'exp': '$[ exports:a if exports:b == 4 ]'}, SETTINGS, '') + result = {'exp': {'node2': 3}} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_if_expr_invquery_with_refs(self): - e = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}} - p = Parameters({'exp': '$[ exports:a if exports:b == self:test_value ]', 'test_value': 2}, SETTINGS, '') - r = {'exp': {'node1': 1}, 'test_value': 2} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 4}}) + parameters = Parameters({'exp': '$[ exports:a if exports:b == self:test_value ]', 'test_value': 2}, SETTINGS, '') + result = {'exp': {'node1': 1}, 'test_value': 2} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_list_if_expr_invquery(self): - e = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 3}, 'node3': {'a': 3, 'b': 2}} - p = Parameters({'exp': '$[ if exports:b == 2 ]'}, SETTINGS, '') - r1 = {'exp': ['node1', 'node3']} - r2 = {'exp': ['node3', 'node1']} - p.interpolate(e) - self.assertIn(p.as_dict(), [ r1, r2 ]) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 3}, 'node3': {'a': 3, 'b': 2}}) + parameters = Parameters({'exp': '$[ if exports:b == 2 ]'}, SETTINGS, '') + result = {'exp': ['node1', 'node3']} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_if_expr_invquery_wth_and(self): - e = {'node1': {'a': 1, 'b': 4, 'c': False}, 'node2': {'a': 3, 'b': 4, 'c': True}} - p = Parameters({'exp': '$[ exports:a if exports:b == 4 and exports:c == True ]'}, SETTINGS, '') - r = {'exp': {'node2': 3}} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 4, 'c': False}, 'node2': {'a': 3, 'b': 4, 'c': True}}) + parameters = Parameters({'exp': '$[ exports:a if exports:b == 4 and exports:c == True ]'}, SETTINGS, '') + result = {'exp': {'node2': 3}} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_if_expr_invquery_wth_or(self): - e = {'node1': {'a': 1, 'b': 4}, 'node2': {'a': 3, 'b': 3}} - p = Parameters({'exp': '$[ exports:a if exports:b == 4 or exports:b == 3 ]'}, SETTINGS, '') - r = {'exp': {'node1': 1, 'node2': 3}} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1, 'b': 4}, 'node2': {'a': 3, 'b': 3}}) + parameters = Parameters({'exp': '$[ exports:a if exports:b == 4 or exports:b == 3 ]'}, SETTINGS, '') + result = {'exp': {'node1': 1, 'node2': 3}} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_list_if_expr_invquery_with_and(self): - e = {'node1': {'a': 1, 'b': 2, 'c': 'green'}, 'node2': {'a': 3, 'b': 3}, 'node3': {'a': 3, 'b': 2, 'c': 'red'}} - p = Parameters({'exp': '$[ if exports:b == 2 and exports:c == green ]'}, SETTINGS, '') - r = {'exp': ['node1']} - p.interpolate(e) - self.assertEqual(p.as_dict(), r) + inventory = self._make_inventory( + { 'node1': {'a': 1, 'b': 2, 'c': 'green'}, + 'node2': {'a': 3, 'b': 3}, + 'node3': {'a': 3, 'b': 2, 'c': 'red'} }) + parameters = Parameters({'exp': '$[ if exports:b == 2 and exports:c == green ]'}, SETTINGS, '') + result = {'exp': ['node1']} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_list_if_expr_invquery_with_and_missing(self): - inventory = {'node1': {'a': 1, 'b': 2, 'c': 'green'}, - 'node2': {'a': 3, 'b': 3}, - 'node3': {'a': 3, 'b': 2}} + inventory = self._make_inventory({'node1': {'a': 1, 'b': 2, 'c': 'green'}, + 'node2': {'a': 3, 'b': 3}, + 'node3': {'a': 3, 'b': 2}}) mapping = {'exp': '$[ if exports:b == 2 and exports:c == green ]'} expected = {'exp': ['node1']} - - pars = Parameters(mapping, SETTINGS, '') - pars.interpolate(inventory) - - self.assertEqual(pars.as_dict(), expected) - - def test_list_if_expr_invquery_with_and(self): - e = {'node1': {'a': 1, 'b': 2}, 'node2': {'a': 3, 'b': 3}, 'node3': {'a': 3, 'b': 4}} - p = Parameters({'exp': '$[ if exports:b == 2 or exports:b == 4 ]'}, SETTINGS, '') - r1 = {'exp': ['node1', 'node3']} - r2 = {'exp': ['node3', 'node1']} - p.interpolate(e) - self.assertIn(p.as_dict(), [ r1, r2 ]) + parameterss = Parameters(mapping, SETTINGS, '') + parameterss.interpolate(inventory) + self.assertEqual(parameterss.as_dict(), expected) + + def test_list_if_expr_invquery_with_or(self): + inventory = self._make_inventory( + { 'node1': {'a': 1, 'b': 2}, + 'node2': {'a': 3, 'b': 3}, + 'node3': {'a': 3, 'b': 4} }) + parameters = Parameters({'exp': '$[ if exports:b == 2 or exports:b == 4 ]'}, SETTINGS, '') + result = {'exp': ['node1', 'node3']} + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) def test_merging_inv_queries(self): - e = {'node1': {'a': 1}, 'node2': {'a': 1}, 'node3': {'a': 2}} - p1 = Parameters({'exp': '$[ if exports:a == 1 ]'}, SETTINGS, '') - p2 = Parameters({'exp': '$[ if exports:a == 2 ]'}, SETTINGS, '') - r = { 'exp': [ 'node1', 'node2', 'node3' ] } - p1.merge(p2) - p1.interpolate(e) - self.assertEqual(p1.as_dict(), r) + inventory = self._make_inventory({'node1': {'a': 1}, 'node2': {'a': 1}, 'node3': {'a': 2}}) + pars1 = Parameters({'exp': '$[ if exports:a == 1 ]'}, SETTINGS, '') + pars2 = Parameters({'exp': '$[ if exports:a == 2 ]'}, SETTINGS, '') + result = { 'exp': [ 'node1', 'node2', 'node3' ] } + pars1.merge(pars2) + pars1.interpolate(inventory) + self.assertEqual(pars1.as_dict(), result) + + def test_same_expr_invquery_different_flags(self): + inventory = { 'node1': NodeInventory({'a': 1}, True), + 'node2': NodeInventory({'a': 2}, True), + 'node3': NodeInventory({'a': 3}, False) } + parameters = Parameters({'alpha': '$[ exports:a ]', 'beta': '$[ +AllEnvs exports:a ]'}, SETTINGS, '') + result = { 'alpha': { 'node1': 1, 'node2': 2 }, + 'beta': { 'node1': 1 , 'node2': 2, 'node3': 3 } } + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) + + def test_same_if_expr_invquery_different_flags(self): + inventory = { 'node1': NodeInventory({'a': 1, 'b': 1}, True), + 'node2': NodeInventory({'a': 2, 'b': 2}, True), + 'node3': NodeInventory({'a': 3, 'b': 2}, False) } + parameters = Parameters( + { 'alpha': '$[ exports:a if exports:b == 2 ]', + 'beta': '$[ +AllEnvs exports:a if exports:b == 2]' }, + SETTINGS, '') + result = { 'alpha': { 'node2': 2 }, + 'beta': { 'node2': 2, 'node3': 3 } } + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) + + def test_same_list_if_expr_invquery_different_flags(self): + inventory = { 'node1': NodeInventory({'a': 1}, True), + 'node2': NodeInventory({'a': 2}, True), + 'node3': NodeInventory({'a': 2}, False) } + parameters = Parameters( + { 'alpha': '$[ if exports:a == 2 ]', + 'beta': '$[ +AllEnvs if exports:a == 2]' }, + SETTINGS, '') + result = { 'alpha': [ 'node2' ], + 'beta': [ 'node2', 'node3' ] } + parameters.interpolate(inventory) + self.assertEqual(parameters.as_dict(), result) if __name__ == '__main__': unittest.main() diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py index 79322e69..80fd8de1 100644 --- a/reclass/datatypes/tests/test_parameters.py +++ b/reclass/datatypes/tests/test_parameters.py @@ -194,7 +194,7 @@ def test_merge_list_into_scalar(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge list over scalar, at key, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge list over scalar, at key, in ; ") def test_merge_list_into_scalar_allow(self): settings = Settings({'allow_list_over_scalar': True}) @@ -212,7 +212,7 @@ def test_merge_scalar_over_list(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge scalar over list, at key, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge scalar over list, at key, in ; ") def test_merge_scalar_over_list_allow(self): l = ['foo', 1, 2] @@ -231,7 +231,7 @@ def test_merge_none_over_list(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge scalar over list, at key, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge scalar over list, at key, in ; ") def test_merge_none_over_list_allow(self): l = ['foo', 1, 2] @@ -249,7 +249,7 @@ def test_merge_dict_over_scalar(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge dictionary over scalar, at a, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge dictionary over scalar, at a, in ; ") def test_merge_dict_over_scalar_allow(self): settings = Settings({'allow_dict_over_scalar': True}) @@ -267,7 +267,7 @@ def test_merge_scalar_over_dict(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge scalar over dictionary, at a, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge scalar over dictionary, at a, in ; ") def test_merge_scalar_over_dict_allow(self): d = { 'one': 1, 'two': 2} @@ -284,7 +284,7 @@ def test_merge_none_over_dict(self): with self.assertRaises(TypeMergeError) as e: p1.merge(p2) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge scalar over dictionary, at key, in ; ") + self.assertEqual(e.exception.message, "-> \n Cannot merge scalar over dictionary, at key, in ; ") def test_merge_none_over_dict_allow(self): settings = Settings({'allow_none_override': True}) @@ -302,7 +302,7 @@ def test_merge_list_over_dict(self): p1.merge(p2) p1.merge(p3) p1.interpolate() - self.assertEqual(e.exception.message, "-> \n Canot merge list over dictionary, at one:a, in second; third") + self.assertEqual(e.exception.message, "-> \n Cannot merge list over dictionary, at one:a, in second; third") # def test_merge_bare_dict_over_dict(self): # settings = Settings({'allow_bare_override': True}) diff --git a/reclass/defaults.py b/reclass/defaults.py index f240f3f9..7ea797e5 100644 --- a/reclass/defaults.py +++ b/reclass/defaults.py @@ -56,4 +56,7 @@ ESCAPE_CHARACTER = '\\' AUTOMATIC_RECLASS_PARAMETERS = True +SCALAR_RECLASS_PARAMETERS = False DEFAULT_ENVIRONMENT = 'base' + +CLASS_MAPPINGS_MATCH_PATH = False diff --git a/reclass/errors.py b/reclass/errors.py index 330ad4cc..df35ef8e 100644 --- a/reclass/errors.py +++ b/reclass/errors.py @@ -117,7 +117,7 @@ def _get_message(self): class InterpolationError(ReclassException): - def __init__(self, msg, rc=posix.EX_DATAERR, nodename='', uri=None, context=None, tbFlag=True): + def __init__(self, msg=None, rc=posix.EX_DATAERR, nodename='', uri=None, context=None, tbFlag=True): super(InterpolationError, self).__init__(rc=rc, msg=msg, tbFlag=tbFlag) self.nodename = nodename self.uri = uri @@ -291,7 +291,7 @@ def __init__(self, value1, value2, uri): self.type2 = value2.item_type_str() def _get_error_message(self): - msg = [ 'Canot merge {0} over {1}'.format(self.type1, self.type2) + self._add_context_and_uri() ] + msg = [ 'Cannot merge {0} over {1}'.format(self.type1, self.type2) + self._add_context_and_uri() ] return msg @@ -330,7 +330,7 @@ def __init__(self, msg): class NameError(ReclassException): - def __init__(self, msg, rc=posix.EX_DATAERR): + def __init__(self, msg=None, rc=posix.EX_DATAERR): super(NameError, self).__init__(rc=rc, msg=msg) diff --git a/reclass/settings.py b/reclass/settings.py index b7f52526..08fdea50 100644 --- a/reclass/settings.py +++ b/reclass/settings.py @@ -19,6 +19,8 @@ class Settings(object): 'allow_dict_over_scalar': defaults.OPT_ALLOW_DICT_OVER_SCALAR, 'allow_none_override': defaults.OPT_ALLOW_NONE_OVERRIDE, 'automatic_parameters': defaults.AUTOMATIC_RECLASS_PARAMETERS, + 'class_mappings_match_path': defaults.CLASS_MAPPINGS_MATCH_PATH, + 'scalar_parameters': defaults.SCALAR_RECLASS_PARAMETERS, 'default_environment': defaults.DEFAULT_ENVIRONMENT, 'delimiter': defaults.PARAMETER_INTERPOLATION_DELIMITER, 'dict_key_override_prefix': @@ -39,7 +41,7 @@ class Settings(object): defaults.OPT_IGNORE_CLASS_NOTFOUND_REGEXP, 'ignore_class_notfound_warning': defaults.OPT_IGNORE_CLASS_NOTFOUND_WARNING, - 'ignore_overwritten_missing_referencesdefaults.': + 'ignore_overwritten_missing_references': defaults.OPT_IGNORE_OVERWRITTEN_MISSING_REFERENCES, 'group_errors': defaults.OPT_GROUP_ERRORS, 'compose_node_name': defaults.OPT_COMPOSE_NODE_NAME, diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py index 20e8eecb..ee49df39 100644 --- a/reclass/storage/yaml_fs/__init__.py +++ b/reclass/storage/yaml_fs/__init__.py @@ -12,7 +12,6 @@ from __future__ import unicode_literals import os, sys -import fnmatch import yaml from reclass.output.yaml_outputter import ExplicitDumper from reclass.storage import ExternalNodeStorageBase @@ -21,7 +20,7 @@ from reclass.datatypes import Entity import reclass.errors -FILE_EXTENSION = '.yml' +FILE_EXTENSION = ('.yml', '.yaml') STORAGE_NAME = 'yaml_fs' def vvv(msg): @@ -71,7 +70,7 @@ def __init__(self, nodes_uri, classes_uri, compose_node_name): def _enumerate_inventory(self, basedir, name_mangler): ret = {} def register_fn(dirpath, filenames): - filenames = fnmatch.filter(filenames, '*{0}'.format(FILE_EXTENSION)) + filenames = [f for f in filenames if f.endswith(FILE_EXTENSION)] vvv('REGISTER {0} in path {1}'.format(filenames, dirpath)) for f in filenames: name = os.path.splitext(f)[0] @@ -96,18 +95,20 @@ def get_node(self, name, settings): try: relpath = self._nodes[name] path = os.path.join(self.nodes_uri, relpath) + pathname = os.path.splitext(relpath)[0] except KeyError as e: raise reclass.errors.NodeNotFound(self.name, name, self.nodes_uri) - entity = YamlData.from_file(path).get_entity(name, settings) + entity = YamlData.from_file(path).get_entity(name, pathname, settings) return entity def get_class(self, name, environment, settings): vvv('GET CLASS {0}'.format(name)) try: path = os.path.join(self.classes_uri, self._classes[name]) + pathname = os.path.splitext(self._classes[name])[0] except KeyError as e: raise reclass.errors.ClassNotFound(self.name, name, self.classes_uri) - entity = YamlData.from_file(path).get_entity(name, settings) + entity = YamlData.from_file(path).get_entity(name, pathname, settings) return entity def enumerate_nodes(self): diff --git a/reclass/storage/yaml_fs/directory.py b/reclass/storage/yaml_fs/directory.py index a8916b31..4e11643d 100644 --- a/reclass/storage/yaml_fs/directory.py +++ b/reclass/storage/yaml_fs/directory.py @@ -15,7 +15,7 @@ from reclass.errors import NotFoundError SKIPDIRS = ('CVS', 'SCCS') -FILE_EXTENSION = '.yml' +FILE_EXTENSION = ('.yml', '.yaml') def vvv(msg): #print(msg, file=sys.stderr) diff --git a/reclass/storage/yaml_git/__init__.py b/reclass/storage/yaml_git/__init__.py index a28079b6..1053a951 100644 --- a/reclass/storage/yaml_git/__init__.py +++ b/reclass/storage/yaml_git/__init__.py @@ -11,7 +11,6 @@ import distutils.version import errno import fcntl -import fnmatch import os import time @@ -34,7 +33,7 @@ from reclass.storage import ExternalNodeStorageBase from reclass.storage.yamldata import YamlData -FILE_EXTENSION = '.yml' +FILE_EXTENSION = ('.yml', '.yaml') STORAGE_NAME = 'yaml_git' def path_mangler(inventory_base_uri, nodes_uri, classes_uri): @@ -43,7 +42,7 @@ def path_mangler(inventory_base_uri, nodes_uri, classes_uri): return nodes_uri, classes_uri -GitMD = collections.namedtuple('GitMD', ['name', 'path', 'id'], verbose=False, rename=False) +GitMD = collections.namedtuple('GitMD', ['name', 'path', 'id'], rename=False) class GitURI(object): @@ -213,13 +212,13 @@ def files_in_repo(self): branch = {} files = self.files_in_branch(bname) for file in files: - if fnmatch.fnmatch(file.name, '*{0}'.format(FILE_EXTENSION)): + if file.name.endswith(FILE_EXTENSION): name = os.path.splitext(file.name)[0] relpath = os.path.dirname(file.path) if callable(self._class_name_mangler): relpath, name = self._class_name_mangler(relpath, name) if name in ret: - raise reclass.errors.DuplicateNodeNameError(self.name + ' - ' + bname, name, ret[name], path) + raise reclass.errors.DuplicateNodeNameError(self.url + ' - ' + bname, name, ret[name], file) else: branch[name] = file ret[bname] = branch @@ -234,7 +233,7 @@ def nodes(self, branch, subdir): if callable(self._node_name_mangler): relpath, node_name = self._node_name_mangler(relpath, node_name) if node_name in ret: - raise reclass.errors.DuplicateNodeNameError(self.name, name, files[name], path) + raise reclass.errors.DuplicateNodeNameError(self.url, name, ret[node_name].path, file.path) else: ret[node_name] = file return ret @@ -274,7 +273,9 @@ def __init__(self, nodes_uri, classes_uri, compose_node_name): def get_node(self, name, settings): file = self._nodes[name] blob = self._repos[self._nodes_uri.repo].get(file.id) - entity = YamlData.from_string(blob.data, 'git_fs://{0} {1} {2}'.format(self._nodes_uri.repo, self._nodes_uri.branch, file.path)).get_entity(name, settings) + uri = 'git_fs://{0} {1} {2}'.format(self._nodes_uri.repo, self._nodes_uri.branch, file.path) + pathname = os.path.splitext(file.path)[0] + entity = YamlData.from_string(blob.data, uri).get_entity(name, pathname, settings) return entity def get_class(self, name, environment, settings): @@ -289,7 +290,9 @@ def get_class(self, name, environment, settings): raise reclass.errors.NotFoundError("File " + name + " missing from " + uri.repo + " branch " + uri.branch) file = self._repos[uri.repo].files[uri.branch][name] blob = self._repos[uri.repo].get(file.id) - entity = YamlData.from_string(blob.data, 'git_fs://{0} {1} {2}'.format(uri.repo, uri.branch, file.path)).get_entity(name, settings) + uri = 'git_fs://{0} {1} {2}'.format(uri.repo, uri.branch, file.path) + pathname = os.path.splitext(file.path)[0] + entity = YamlData.from_string(blob.data, uri).get_entity(name, pathname, settings) return entity def enumerate_nodes(self): diff --git a/reclass/storage/yamldata.py b/reclass/storage/yamldata.py index a38b589a..f68d8036 100644 --- a/reclass/storage/yamldata.py +++ b/reclass/storage/yamldata.py @@ -80,10 +80,7 @@ def yield_dots(self, value): def count_dots(self, value): return len(list(self.yield_dots(value))) - def get_entity(self, name, settings): - #if name is None: - # name = self._uri - + def get_entity(self, name, pathname, settings): classes = self._data.get('classes') if classes is None: classes = [] @@ -108,7 +105,7 @@ def get_entity(self, name, settings): env = self._data.get('environment', None) return datatypes.Entity(settings, classes=classes, applications=applications, parameters=parameters, - exports=exports, name=name, environment=env, uri=self.uri) + exports=exports, name=name, pathname=pathname, environment=env, uri=self.uri) def __str__(self): return '<{0} {1}, {2}>'.format(self.__class__.__name__, self._uri, diff --git a/reclass/tests/data/04/classes/one.yml b/reclass/tests/data/04/classes/one.yml new file mode 100644 index 00000000..37ee5e80 --- /dev/null +++ b/reclass/tests/data/04/classes/one.yml @@ -0,0 +1,2 @@ +parameters: + test1: 1 diff --git a/reclass/tests/data/04/classes/three.yml b/reclass/tests/data/04/classes/three.yml new file mode 100644 index 00000000..f71f8ce1 --- /dev/null +++ b/reclass/tests/data/04/classes/three.yml @@ -0,0 +1,2 @@ +parameters: + test3: 3 diff --git a/reclass/tests/data/04/classes/two.yml b/reclass/tests/data/04/classes/two.yml new file mode 100644 index 00000000..80d52099 --- /dev/null +++ b/reclass/tests/data/04/classes/two.yml @@ -0,0 +1,2 @@ +parameters: + test2: 2 diff --git a/reclass/tests/data/04/nodes/alpha/node1.yml b/reclass/tests/data/04/nodes/alpha/node1.yml new file mode 100644 index 00000000..f0f59f52 --- /dev/null +++ b/reclass/tests/data/04/nodes/alpha/node1.yml @@ -0,0 +1,2 @@ +classes: + - one diff --git a/reclass/tests/test_core.py b/reclass/tests/test_core.py index 4827177b..c1e283d2 100644 --- a/reclass/tests/test_core.py +++ b/reclass/tests/test_core.py @@ -23,13 +23,13 @@ class TestCore(unittest.TestCase): - def _core(self, dataset, opts={}): + def _core(self, dataset, opts={}, class_mappings=[]): inventory_uri = os.path.dirname(os.path.abspath(__file__)) + '/data/' + dataset path_mangler = get_path_mangler('yaml_fs') nodes_uri, classes_uri = path_mangler(inventory_uri, 'nodes', 'classes') settings = Settings(opts) storage = get_storage('yaml_fs', nodes_uri, classes_uri, settings.compose_node_name) - return Core(storage, None, settings) + return Core(storage, class_mappings, settings) def test_type_conversion(self): reclass = self._core('01') @@ -72,7 +72,7 @@ def test_top_relative_class_names(self): self.assertEqual(node['parameters'], params) def test_compose_node_names(self): - reclass = self._core('03', {'compose_node_name': True}) + reclass = self._core('03', opts={'compose_node_name': True}) alpha_one_node = reclass.nodeinfo('alpha.one') alpha_one_res = {'a': 1, 'alpha': [1, 2], 'beta': {'a': 1, 'b': 2}, 'b': 2, '_reclass_': {'environment': 'base', 'name': {'full': 'alpha.one', 'short': 'alpha'}}} alpha_two_node = reclass.nodeinfo('alpha.two') @@ -86,6 +86,18 @@ def test_compose_node_names(self): self.assertEqual(beta_one_node['parameters'], beta_one_res) self.assertEqual(beta_two_node['parameters'], beta_two_res) + def test_class_mappings_match_path_false(self): + reclass = self._core('04', opts={'class_mappings_match_path': False}, class_mappings=['node* two', 'alpha/node* three']) + node = reclass.nodeinfo('node1') + params = { 'test1': 1, 'test2': 2, '_reclass_': {'environment': u'base', 'name': {'full': 'node1', 'short': 'node1'}}} + self.assertEqual(node['parameters'], params) + + def test_class_mappings_match_path_true(self): + reclass = self._core('04', opts={'class_mappings_match_path': True}, class_mappings=['node* two', 'alpha/node* three']) + node = reclass.nodeinfo('node1') + params = { 'test1': 1, 'test3': 3, '_reclass_': {'environment': u'base', 'name': {'full': 'node1', 'short': 'node1'}}} + self.assertEqual(node['parameters'], params) + if __name__ == '__main__': unittest.main() diff --git a/reclass/values/__init__.py b/reclass/values/__init__.py index ec0f8822..0458d34e 100644 --- a/reclass/values/__init__.py +++ b/reclass/values/__init__.py @@ -3,3 +3,7 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals + +import collections + +NodeInventory = collections.namedtuple('NodeInventory', ['items', 'env_matches'], rename=False) diff --git a/reclass/values/invitem.py b/reclass/values/invitem.py index adb1cb6c..d8f38749 100644 --- a/reclass/values/invitem.py +++ b/reclass/values/invitem.py @@ -16,8 +16,7 @@ from six import iteritems from six import string_types -from reclass.values import item -from reclass.values import parser_funcs +from reclass.values import item, parser_funcs from reclass.settings import Settings from reclass.utils.dictpath import DictPath from reclass.errors import ExpressionError, ParseError, ResolveError @@ -200,10 +199,11 @@ def _resolve(self, path, dictionary): def _value_expression(self, inventory): results = {} - for (node, items) in iteritems(inventory): - if self._value_path.exists_in(items): - results[node] = copy.deepcopy(self._resolve(self._value_path, - items)) + for name, node in iteritems(inventory): + if self.needs_all_envs or node.env_matches: + if self._value_path.exists_in(node.items): + answer = self._resolve(self._value_path, node.items) + results[name] = copy.deepcopy(answer) return results def _test_expression(self, context, inventory): @@ -212,18 +212,21 @@ def _test_expression(self, context, inventory): raise ExpressionError(msg % str(self), tbFlag=False) results = {} - for node, items in iteritems(inventory): - if (self._question.value(context, items) and - self._value_path.exists_in(items)): - results[node] = copy.deepcopy( - self._resolve(self._value_path, items)) + for name, node in iteritems(inventory): + if self.needs_all_envs or node.env_matches: + if (self._question.value(context, node.items) and + self._value_path.exists_in(node.items)): + answer = self._resolve(self._value_path, node.items) + results[name] = copy.deepcopy(answer) return results def _list_test_expression(self, context, inventory): results = [] - for (node, items) in iteritems(inventory): - if self._question.value(context, items): - results.append(node) + for name, node in iteritems(inventory): + if self.needs_all_envs or node.env_matches: + if self._question.value(context, node.items): + results.append(name) + results.sort() return results def render(self, context, inventory): diff --git a/reclass/version.py b/reclass/version.py index 5a40c2ed..70774184 100644 --- a/reclass/version.py +++ b/reclass/version.py @@ -14,7 +14,7 @@ RECLASS_NAME = 'reclass' DESCRIPTION = ('merge data by recursive descent down an ancestry hierarchy ' '(forked extended version)') -VERSION = '1.6.0' +VERSION = '1.7.0' AUTHOR = 'martin f. krafft / Andrew Pickford / salt-formulas community' AUTHOR_EMAIL = 'salt-formulas@freelists.org' MAINTAINER = 'salt-formulas community'