diff --git a/README-extensions.rst b/README-extensions.rst index da9ce85b..c283acc8 100644 --- a/README-extensions.rst +++ b/README-extensions.rst @@ -579,6 +579,31 @@ This adds the subfolder to the node name and the structure above can then be use If the subfolder path starts with the underscore character ``_``, then the subfolder path is NOT added to the node name. +Override node environment +------------------------- + +The environment of a node is defined in it's node file. This can be overridden on the command line with the option +'--environment'. For example: + +``reclass.py --nodeinfo node1 --environment test`` + +will return the node information for node1 as if the node was in the test environment, regardless of the environment value +in the node file. + +When the node envrionment is overridden inventory queries that do not have the AllEnvs flag set will still return data for +other nodes matching the original, none overridden, environment. + +When using reclass with salt the reclass node environment can be overridden on the salt command line. This is controlled by the +configuration option allow_adapter_env_override. When False (the default) no override is done. If allow_adapter_env_override is +true and either saltenv or pillarenv (depending on the salt command) is set on the salt command line the node environment will +be overridden and set to the value of either saltenv oe pillarenv. Should a salt command allow both saltenv and pillarenv to be +set the value of pillarenv takes precedence. + +Currently in order to use this functionality the default salt reclass adapter provided with salt must be overridden. Place the +file contrib/modules/pillar/reclass_adapter.py in the pillar directory of the salt master extension_modules directory and the +file contrib/modules/tops/reclass_adapter.py in the tops directory of the salt master extension_modules directory. + + Git storage type ---------------- diff --git a/contrib/modules/pillar/reclass_adapter.py b/contrib/modules/pillar/reclass_adapter.py new file mode 100644 index 00000000..e16098ac --- /dev/null +++ b/contrib/modules/pillar/reclass_adapter.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +''' +*** 2020-06-19 saltenv environment override: +This reclass adapter is only required to enable salt commands setting saltenv or +pillarenv to pass that value to reclass. If the configuration setting +allow_adapter_env_override is False (the default) the value of saltenv or pillarenv +is ignored and the environment of the node is taken from the node file as normal. +If allow_adapter_env_override is True and saltenv or pillarenv is set (depending +on which salt command is used) the value of saltenv/pillarenv will be used as the +environment of the node. + +This file should be placed in the pillar directory of the extension_modules +directory on the salt master. +*** + +Use the "reclass" database as a Pillar source + +.. |reclass| replace:: **reclass** + +This ``ext_pillar`` plugin provides access to the |reclass| database, such +that Pillar data for a specific minion are fetched using |reclass|. + +You can find more information about |reclass| at +http://reclass.pantsfullofunix.net. + +To use the plugin, add it to the ``ext_pillar`` list in the Salt master config +and tell |reclass| by way of a few options how and where to find the +inventory: + +.. code-block:: yaml + + ext_pillar: + - reclass: + storage_type: yaml_fs + inventory_base_uri: /srv/salt + +This would cause |reclass| to read the inventory from YAML files in +``/srv/salt/nodes`` and ``/srv/salt/classes``. + +If you are also using |reclass| as ``master_tops`` plugin, and you want to +avoid having to specify the same information for both, use YAML anchors (take +note of the differing data types for ``ext_pillar`` and ``master_tops``): + +.. code-block:: yaml + + reclass: &reclass + storage_type: yaml_fs + inventory_base_uri: /srv/salt + reclass_source_path: ~/code/reclass + + ext_pillar: + - reclass: *reclass + + master_tops: + reclass: *reclass + +If you want to run reclass from source, rather than installing it, you can +either let the master know via the ``PYTHONPATH`` environment variable, or by +setting the configuration option, like in the example above. +''' + + +# This file cannot be called reclass.py, because then the module import would +# not work. Thanks to the __virtual__ function, however, the plugin still +# responds to the name 'reclass'. + +# Import python libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import salt libs +from salt.exceptions import SaltInvocationError +from salt.utils.reclass import ( + prepend_reclass_source_path, + filter_out_source_path_option, + set_inventory_base_uri_default +) + +# Import 3rd-party libs +from salt.ext import six + +# Define the module's virtual name +__virtualname__ = 'reclass' + + +def __virtual__(retry=False): + try: + import reclass + return __virtualname__ + + except ImportError as e: + if retry: + return False + + for pillar in __opts__.get('ext_pillar', []): + if 'reclass' not in pillar: + continue + + # each pillar entry is a single-key hash of name -> options + opts = next(six.itervalues(pillar)) + prepend_reclass_source_path(opts) + break + + return __virtual__(retry=True) + + +def ext_pillar(minion_id, pillar, **kwargs): + ''' + Obtain the Pillar data from **reclass** for the given ``minion_id``. + ''' + + # If reclass is installed, __virtual__ put it onto the search path, so we + # don't need to protect against ImportError: + # pylint: disable=3rd-party-module-not-gated + from reclass.adapters.salt import ext_pillar as reclass_ext_pillar + from reclass.errors import ReclassException + # pylint: enable=3rd-party-module-not-gated + + try: + # the source path we used above isn't something reclass needs to care + # about, so filter it: + filter_out_source_path_option(kwargs) + + # if no inventory_base_uri was specified, initialize it to the first + # file_roots of class 'base' (if that exists): + set_inventory_base_uri_default(__opts__, kwargs) + + # if saltenv or pillarenv has been set add it to the kwargs, this allows + # reclass to override a nodes environment + env_override = None + if __opts__.get('saltenv', None): + env_override = __opts__['saltenv'] + if __opts__.get('pillarenv', None): + env_override = __opts__['pillarenv'] + + # I purposely do not pass any of __opts__ or __salt__ or __grains__ + # to reclass, as I consider those to be Salt-internal and reclass + # should not make any assumptions about it. + return reclass_ext_pillar(minion_id, pillar, pillarenv=env_override, **kwargs) + + except TypeError as e: + if 'unexpected keyword argument' in six.text_type(e): + arg = six.text_type(e).split()[-1] + raise SaltInvocationError('ext_pillar.reclass: unexpected option: ' + + arg) + else: + raise + + except KeyError as e: + if 'id' in six.text_type(e): + raise SaltInvocationError('ext_pillar.reclass: __opts__ does not ' + 'define minion ID') + else: + raise + + except ReclassException as e: + raise SaltInvocationError('ext_pillar.reclass: {0}'.format(e)) diff --git a/contrib/modules/tops/reclass_adapter.py b/contrib/modules/tops/reclass_adapter.py new file mode 100644 index 00000000..4f19cfbf --- /dev/null +++ b/contrib/modules/tops/reclass_adapter.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +''' +Read tops data from a reclass database + +.. |reclass| replace:: **reclass** + +This :ref:`master_tops ` plugin provides access to +the |reclass| database, such that state information (top data) are retrieved +from |reclass|. + +You can find more information about |reclass| at +http://reclass.pantsfullofunix.net. + +To use the plugin, add it to the ``master_tops`` list in the Salt master config +and tell |reclass| by way of a few options how and where to find the +inventory: + +.. code-block:: yaml + + master_tops: + reclass: + storage_type: yaml_fs + inventory_base_uri: /srv/salt + +This would cause |reclass| to read the inventory from YAML files in +``/srv/salt/nodes`` and ``/srv/salt/classes``. + +If you are also using |reclass| as ``ext_pillar`` plugin, and you want to +avoid having to specify the same information for both, use YAML anchors (take +note of the differing data types for ``ext_pillar`` and ``master_tops``): + +.. code-block:: yaml + + reclass: &reclass + storage_type: yaml_fs + inventory_base_uri: /srv/salt + reclass_source_path: ~/code/reclass + + ext_pillar: + - reclass: *reclass + + master_tops: + reclass: *reclass + +If you want to run reclass from source, rather than installing it, you can +either let the master know via the ``PYTHONPATH`` environment variable, or by +setting the configuration option, like in the example above. +''' +from __future__ import absolute_import, print_function, unicode_literals + +# This file cannot be called reclass.py, because then the module import would +# not work. Thanks to the __virtual__ function, however, the plugin still +# responds to the name 'reclass'. + +import sys +from salt.utils.reclass import ( + prepend_reclass_source_path, + filter_out_source_path_option, + set_inventory_base_uri_default +) + +from salt.exceptions import SaltInvocationError +from salt.ext import six + +# Define the module's virtual name +__virtualname__ = 'reclass' + +import logging +log = logging.getLogger(__name__) + +def __virtual__(retry=False): + try: + import reclass + return __virtualname__ + except ImportError: + if retry: + return False + + opts = __opts__.get('master_tops', {}).get('reclass', {}) + prepend_reclass_source_path(opts) + return __virtual__(retry=True) + + +def top(**kwargs): + ''' + Query |reclass| for the top data (states of the minions). + ''' + + # If reclass is installed, __virtual__ put it onto the search path, so we + # don't need to protect against ImportError: + # pylint: disable=3rd-party-module-not-gated + from reclass.adapters.salt import top as reclass_top + from reclass.errors import ReclassException + # pylint: enable=3rd-party-module-not-gated + + try: + # Salt's top interface is inconsistent with ext_pillar (see #5786) and + # one is expected to extract the arguments to the master_tops plugin + # by parsing the configuration file data. I therefore use this adapter + # to hide this internality. + reclass_opts = __opts__['master_tops']['reclass'] + + # the source path we used above isn't something reclass needs to care + # about, so filter it: + filter_out_source_path_option(reclass_opts) + + # if no inventory_base_uri was specified, initialise it to the first + # file_roots of class 'base' (if that exists): + set_inventory_base_uri_default(__opts__, kwargs) + + # Salt expects the top data to be filtered by minion_id, so we better + # let it know which minion it is dealing with. Unfortunately, we must + # extract these data (see #6930): + minion_id = kwargs['opts']['id'] + + # if saltenv or pillarenv has been set add it to the kwargs, this allows + # reclass to override a nodes environment + env_override = None + if kwargs['opts'].get('saltenv', None): + env_override = kwargs['opts']['saltenv'] + if kwargs['opts'].get('pillarenv', None): + env_override = kwargs['opts']['pillarenv'] + + # I purposely do not pass any of __opts__ or __salt__ or __grains__ + # to reclass, as I consider those to be Salt-internal and reclass + # should not make any assumptions about it. Reclass only needs to know + # how it's configured, so: + return reclass_top(minion_id, pillarenv=env_override, **reclass_opts) + + except ImportError as e: + if 'reclass' in six.text_type(e): + raise SaltInvocationError( + 'master_tops.reclass: cannot find reclass module ' + 'in {0}'.format(sys.path) + ) + else: + raise + + except TypeError as e: + if 'unexpected keyword argument' in six.text_type(e): + arg = six.text_type(e).split()[-1] + raise SaltInvocationError( + 'master_tops.reclass: unexpected option: {0}'.format(arg) + ) + else: + raise + + except KeyError as e: + if 'reclass' in six.text_type(e): + raise SaltInvocationError('master_tops.reclass: no configuration ' + 'found in master config') + else: + raise + + except ReclassException as e: + raise SaltInvocationError('master_tops.reclass: {0}'.format(six.text_type(e))) diff --git a/reclass/adapters/salt.py b/reclass/adapters/salt.py index 523b0c46..1f3d50af 100755 --- a/reclass/adapters/salt.py +++ b/reclass/adapters/salt.py @@ -19,12 +19,13 @@ from reclass.core import Core from reclass.errors import ReclassException from reclass.config import find_and_read_configfile, get_options -from reclass.constants import MODE_NODEINFO +from reclass.constants import MODE_NODEINFO, MODE_NODEAPPS from reclass.defaults import * from reclass.settings import Settings from reclass.version import * def ext_pillar(minion_id, pillar, + pillarenv=None, storage_type=OPT_STORAGE_TYPE, inventory_base_uri=OPT_INVENTORY_BASE_URI, nodes_uri=OPT_NODES_URI, @@ -32,6 +33,7 @@ def ext_pillar(minion_id, pillar, class_mappings=None, propagate_pillar_data_to_reclass=False, compose_node_name=OPT_COMPOSE_NODE_NAME, + allow_adapter_env_override=OPT_ALLOW_ADAPTER_ENV_OVERRIDE, **kwargs): path_mangler = get_path_mangler(storage_type) @@ -40,10 +42,13 @@ def ext_pillar(minion_id, pillar, input_data = None if propagate_pillar_data_to_reclass: input_data = pillar + if not allow_adapter_env_override: + pillarenv = None + settings = Settings(kwargs) reclass = Core(storage, class_mappings, settings, input_data=input_data) - data = reclass.nodeinfo(minion_id) + data = reclass.nodeinfo(minion_id, override_environment=pillarenv) params = data.get('parameters', {}) params['__reclass__'] = {} params['__reclass__']['nodename'] = minion_id @@ -53,9 +58,15 @@ def ext_pillar(minion_id, pillar, return params -def top(minion_id, storage_type=OPT_STORAGE_TYPE, - inventory_base_uri=OPT_INVENTORY_BASE_URI, nodes_uri=OPT_NODES_URI, - classes_uri=OPT_CLASSES_URI, class_mappings=None, compose_node_name=OPT_COMPOSE_NODE_NAME, +def top(minion_id, + pillarenv=None, + storage_type=OPT_STORAGE_TYPE, + inventory_base_uri=OPT_INVENTORY_BASE_URI, + nodes_uri=OPT_NODES_URI, + classes_uri=OPT_CLASSES_URI, + class_mappings=None, + compose_node_name=OPT_COMPOSE_NODE_NAME, + allow_adapter_env_override=OPT_ALLOW_ADAPTER_ENV_OVERRIDE, **kwargs): path_mangler = get_path_mangler(storage_type) @@ -64,11 +75,14 @@ def top(minion_id, storage_type=OPT_STORAGE_TYPE, settings = Settings(kwargs) reclass = Core(storage, class_mappings, settings, input_data=None) + if not allow_adapter_env_override: + pillarenv = None + # if the minion_id is not None, then return just the applications for the # specific minion, otherwise return the entire top data (which we need for # CLI invocations of the adapter): if minion_id is not None: - data = reclass.nodeinfo(minion_id) + data = reclass.nodeinfo(minion_id, override_environment=pillarenv) applications = data.get('applications', []) env = data['environment'] return {env: applications} @@ -98,6 +112,10 @@ def cli(): inventory_shortopt='-t', inventory_longopt='--top', inventory_help='output the state tops (inventory)', + nodeapps_shortopt='-f', + nodeapps_longopt='--formulas', + nodeapps_dest='nodename', + nodeapps_help='output applications list for a specific node', nodeinfo_shortopt='-p', nodeinfo_longopt='--pillar', nodeinfo_dest='nodename', @@ -110,23 +128,38 @@ def cli(): defaults.pop("nodes_uri", None) defaults.pop("classes_uri", None) defaults.pop("class_mappings", None) + defaults.pop("pillarenv", None) if options.mode == MODE_NODEINFO: - data = ext_pillar(options.nodename, {}, - storage_type=options.storage_type, - inventory_base_uri=options.inventory_base_uri, - nodes_uri=options.nodes_uri, - classes_uri=options.classes_uri, - class_mappings=class_mappings, - **defaults) + data = ext_pillar( + options.nodename, {}, + pillarenv=options.environment, + storage_type=options.storage_type, + inventory_base_uri=options.inventory_base_uri, + nodes_uri=options.nodes_uri, + classes_uri=options.classes_uri, + class_mappings=class_mappings, + **defaults) + elif options.mode == MODE_NODEAPPS: + data = top( + minion_id=options.nodename, + pillarenv=options.environment, + storage_type=options.storage_type, + inventory_base_uri=options.inventory_base_uri, + nodes_uri=options.nodes_uri, + classes_uri=options.classes_uri, + class_mappings=class_mappings, + **defaults) else: - data = top(minion_id=None, - storage_type=options.storage_type, - inventory_base_uri=options.inventory_base_uri, - nodes_uri=options.nodes_uri, - classes_uri=options.classes_uri, - class_mappings=class_mappings, - **defaults) + data = top( + minion_id=None, + pillarenv=None, + storage_type=options.storage_type, + inventory_base_uri=options.inventory_base_uri, + nodes_uri=options.nodes_uri, + classes_uri=options.classes_uri, + class_mappings=class_mappings, + **defaults) print(output(data, options.output, options.pretty_print, options.no_refs)) diff --git a/reclass/cli.py b/reclass/cli.py index 38bd5fc4..0cd9ea22 100644 --- a/reclass/cli.py +++ b/reclass/cli.py @@ -19,7 +19,7 @@ from reclass.config import find_and_read_configfile, get_options from reclass.defaults import * from reclass.errors import ReclassException -from reclass.constants import MODE_NODEINFO +from reclass.constants import MODE_NODEINFO, MODE_NODEAPPS from reclass.version import * def main(): @@ -41,7 +41,10 @@ def main(): reclass = Core(storage, class_mappings, settings) if options.mode == MODE_NODEINFO: - data = reclass.nodeinfo(options.nodename) + data = reclass.nodeinfo(options.nodename, override_environment=options.environment) + elif options.mode == MODE_NODEAPPS: + nodeinfo = reclass.nodeinfo(options.nodename, override_environment=options.environment) + data = { 'applications': nodeinfo['applications'] } else: data = reclass.inventory() diff --git a/reclass/config.py b/reclass/config.py index d24f7fd3..ce6a6fef 100644 --- a/reclass/config.py +++ b/reclass/config.py @@ -15,7 +15,7 @@ from . import errors, get_path_mangler from .defaults import * -from .constants import MODE_NODEINFO, MODE_INVENTORY +from .constants import MODE_NODEINFO, MODE_INVENTORY, MODE_NODEAPPS def make_db_options_group(parser, defaults={}): @@ -42,12 +42,16 @@ def make_db_options_group(parser, defaults={}): ret.add_option('-x', '--ignore-class-notfound-regexp', dest='ignore_class_notfound_regexp', default=defaults.get('ignore_class_notfound_regexp', OPT_IGNORE_CLASS_NOTFOUND_REGEXP), help='regexp for not found classes [%default]') + ret.add_option('-e', '--environment', dest='environment', + default=None, + help='override the environment of the node for nodeinfo requests') return ret def make_output_options_group(parser, defaults={}): - ret = optparse.OptionGroup(parser, 'Output options', - 'Configure the way {0} prints data'.format(parser.prog)) + ret = optparse.OptionGroup( + parser, 'Output options', + 'Configure the way {0} prints data'.format(parser.prog)) ret.add_option('-o', '--output', dest='output', default=defaults.get('output', OPT_OUTPUT), help='output format (yaml or json) [%default]') @@ -65,9 +69,11 @@ def make_output_options_group(parser, defaults={}): return ret -def make_modes_options_group(parser, inventory_shortopt, inventory_longopt, - inventory_help, nodeinfo_shortopt, - nodeinfo_longopt, nodeinfo_dest, nodeinfo_help): +def make_modes_options_group( + parser, + inventory_shortopt, inventory_longopt, inventory_help, + nodeapps_shortopt, nodeapps_longopt, nodeapps_dest, nodeapps_help, + nodeinfo_shortopt, nodeinfo_longopt, nodeinfo_dest, nodeinfo_help): def _mode_checker_cb(option, opt_str, value, parser): if hasattr(parser.values, 'mode'): @@ -76,6 +82,9 @@ def _mode_checker_cb(option, opt_str, value, parser): if option == parser.get_option(nodeinfo_longopt): setattr(parser.values, 'mode', MODE_NODEINFO) setattr(parser.values, nodeinfo_dest, value) + elif option == parser.get_option(nodeapps_longopt): + setattr(parser.values, 'mode', MODE_NODEAPPS) + setattr(parser.values, nodeapps_dest, value) else: setattr(parser.values, 'mode', MODE_INVENTORY) setattr(parser.values, nodeinfo_dest, None) @@ -85,6 +94,10 @@ def _mode_checker_cb(option, opt_str, value, parser): ret.add_option(inventory_shortopt, inventory_longopt, action='callback', callback=_mode_checker_cb, help=inventory_help) + ret.add_option(nodeapps_shortopt, nodeapps_longopt, + default=None, dest=nodeapps_dest, type='string', + action='callback', callback=_mode_checker_cb, + help=nodeapps_help) ret.add_option(nodeinfo_shortopt, nodeinfo_longopt, default=None, dest=nodeinfo_dest, type='string', action='callback', callback=_mode_checker_cb, @@ -92,16 +105,12 @@ def _mode_checker_cb(option, opt_str, value, parser): return ret -def make_parser_and_checker(name, version, description, - inventory_shortopt='-i', - inventory_longopt='--inventory', - inventory_help='output the entire inventory', - nodeinfo_shortopt='-n', - nodeinfo_longopt='--nodeinfo', - nodeinfo_dest='nodename', - nodeinfo_help='output information for a specific node', - add_options_cb=None, - defaults={}): +def make_parser_and_checker( + name, version, description, + inventory_shortopt, inventory_longopt, inventory_help, + nodeapps_shortopt, nodeapps_longopt, nodeapps_dest, nodeapps_help, + nodeinfo_shortopt, nodeinfo_longopt, nodeinfo_dest, nodeinfo_help, + add_options_cb, defaults): parser = optparse.OptionParser(version=version) parser.prog = name @@ -121,21 +130,22 @@ def make_parser_and_checker(name, version, description, if callable(add_options_cb): add_options_cb(parser, defaults) - modes_group = make_modes_options_group(parser, inventory_shortopt, - inventory_longopt, inventory_help, - nodeinfo_shortopt, - nodeinfo_longopt, nodeinfo_dest, - nodeinfo_help) + modes_group = make_modes_options_group( + parser, + inventory_shortopt, inventory_longopt, inventory_help, + nodeapps_shortopt, nodeapps_longopt, nodeapps_dest, nodeapps_help, + nodeinfo_shortopt, nodeinfo_longopt, nodeinfo_dest, nodeinfo_help) parser.add_option_group(modes_group) def option_checker(options, args): if len(args) > 0: parser.error('No arguments allowed') elif not hasattr(options, 'mode') \ - or options.mode not in (MODE_NODEINFO, MODE_INVENTORY): + or options.mode not in (MODE_NODEINFO, MODE_NODEAPPS, MODE_INVENTORY): parser.error('You need to specify exactly one mode '\ - '({0} or {1})'.format(inventory_longopt, - nodeinfo_longopt)) + '({0}, {1} or {2})'.format(inventory_longopt, + nodeinfo_longopt, + nodeapps_longopt)) elif options.mode == MODE_NODEINFO \ and not getattr(options, nodeinfo_dest, None): parser.error('Mode {0} needs {1}'.format(nodeinfo_longopt, @@ -152,6 +162,10 @@ def get_options(name, version, description, inventory_shortopt='-i', inventory_longopt='--inventory', inventory_help='output the entire inventory', + nodeapps_shortopt='-f', + nodeapps_longopt='--nodeapps', + nodeapps_dest='nodename', + nodeapps_help='output apps list for a specific node', nodeinfo_shortopt='-n', nodeinfo_longopt='--nodeinfo', nodeinfo_dest='nodename', @@ -159,15 +173,12 @@ def get_options(name, version, description, add_options_cb=None, defaults={}): - parser, checker = make_parser_and_checker(name, version, description, - inventory_shortopt, - inventory_longopt, - inventory_help, - nodeinfo_shortopt, - nodeinfo_longopt, nodeinfo_dest, - nodeinfo_help, - add_options_cb, - defaults=defaults) + parser, checker = make_parser_and_checker( + name, version, description, + inventory_shortopt, inventory_longopt, inventory_help, + nodeapps_shortopt, nodeapps_longopt, nodeapps_dest, nodeapps_help, + nodeinfo_shortopt, nodeinfo_longopt, nodeinfo_dest, nodeinfo_help, + add_options_cb, defaults) options, args = parser.parse_args() checker(options, args) diff --git a/reclass/constants.py b/reclass/constants.py index 58f77697..c6bdd99c 100644 --- a/reclass/constants.py +++ b/reclass/constants.py @@ -20,4 +20,5 @@ def __init__(self, displayname): __str__ = __repr__ = lambda self: self._repr MODE_NODEINFO = _Constant('NODEINFO') +MODE_NODEAPPS = _Constant('NODEAPPS') MODE_INVENTORY = _Constant('INVENTORY') diff --git a/reclass/core.py b/reclass/core.py index 61443c2e..90ed2f4d 100644 --- a/reclass/core.py +++ b/reclass/core.py @@ -199,7 +199,7 @@ def _get_inventory(self, all_envs, environment, queries): if all_envs or node_base.environment == environment: try: - node = self._node_entity(nodename) + node = self._node_entity(nodename, None) except ClassNotFound as e: raise InvQueryClassNotFound(e) except ClassNameResolveError as e: @@ -221,26 +221,30 @@ def _get_inventory(self, all_envs, environment, queries): inventory[nodename] = NodeInventory(node.exports.as_dict(), node_base.environment == environment) return inventory - def _node_entity(self, nodename): + def _node_entity(self, nodename, override_environment): node_entity = self._storage.get_node(nodename, self._settings) - if node_entity.environment == None: + if node_entity.environment is None: node_entity.environment = self._settings.default_environment + if override_environment is not None: + node_entity.environment = override_environment base_entity = Entity(self._settings, name='base') 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)) seen = {} - merge_base = self._recurse_entity(base_entity, 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): + merge_base = self._recurse_entity( + base_entity, 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, override_environment): try: - node = self._node_entity(nodename) + node = self._node_entity(nodename, override_environment) node.initialise_interpolation() if node.parameters.has_inv_query and inventory is None: - inventory = self._get_inventory(node.parameters.needs_all_envs, node.environment, node.parameters.get_inv_queries()) + inventory = self._get_inventory(node.parameters.needs_all_envs, node.original_environment, node.parameters.get_inv_queries()) node.interpolate(inventory) return node except InterpolationError as e: @@ -258,19 +262,19 @@ def _nodeinfo_as_dict(self, nodename, entity): ret.update(entity.as_dict()) return ret - def nodeinfo(self, nodename): - return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename, None)) + def nodeinfo(self, nodename, override_environment=None): + return self._nodeinfo_as_dict(nodename, self._nodeinfo(nodename, None, override_environment)) def inventory(self): query_nodes = set() entities = {} inventory = self._get_inventory(True, '', None) for n in self._storage.enumerate_nodes(): - entities[n] = self._nodeinfo(n, inventory) + entities[n] = self._nodeinfo(n, inventory, None) if entities[n].parameters.has_inv_query: nodes.add(n) for n in query_nodes: - entities[n] = self._nodeinfo(n, inventory) + entities[n] = self._nodeinfo(n, inventory, None) nodes = {} applications = {} diff --git a/reclass/datatypes/entity.py b/reclass/datatypes/entity.py index 88b5afe5..473c3a33 100644 --- a/reclass/datatypes/entity.py +++ b/reclass/datatypes/entity.py @@ -34,6 +34,7 @@ def __init__(self, settings, classes=None, applications=None, self._parameters = self._set_field(parameters, Parameters, pars) self._exports = self._set_field(exports, Exports, pars) self._environment = environment + self._original_environment = environment name = property(lambda s: s._name) uri = property(lambda s: s._uri) @@ -42,6 +43,7 @@ def __init__(self, settings, classes=None, applications=None, applications = property(lambda s: s._applications) parameters = property(lambda s: s._parameters) exports = property(lambda s: s._exports) + original_environment = property(lambda s: s._original_environment) @property def environment(self): @@ -71,6 +73,7 @@ def merge(self, other): self._parameters._uri = other.uri if other.environment != None: self._environment = other.environment + self._original_environment = other.original_environment def merge_parameters(self, params): self._parameters.merge(params) diff --git a/reclass/defaults.py b/reclass/defaults.py index f50a8ad5..39076809 100644 --- a/reclass/defaults.py +++ b/reclass/defaults.py @@ -32,6 +32,8 @@ OPT_IGNORE_OVERWRITTEN_MISSING_REFERENCES = True OPT_STRICT_CONSTANT_PARAMETERS = True +OPT_ALLOW_ADAPTER_ENV_OVERRIDE = False + OPT_ALLOW_SCALAR_OVER_DICT = False OPT_ALLOW_SCALAR_OVER_LIST = False OPT_ALLOW_LIST_OVER_SCALAR = False diff --git a/reclass/settings.py b/reclass/settings.py index e9e8a36f..360bb210 100644 --- a/reclass/settings.py +++ b/reclass/settings.py @@ -13,6 +13,7 @@ class Settings(object): known_opts = { + 'allow_adapter_env_override': defaults.OPT_ALLOW_ADAPTER_ENV_OVERRIDE, 'allow_scalar_over_dict': defaults.OPT_ALLOW_SCALAR_OVER_DICT, 'allow_scalar_over_list': defaults.OPT_ALLOW_SCALAR_OVER_LIST, 'allow_list_over_scalar': defaults.OPT_ALLOW_LIST_OVER_SCALAR,