diff --git a/changelogs/fragments/ensure_config_always_templated.yml b/changelogs/fragments/ensure_config_always_templated.yml new file mode 100644 index 00000000000..1d91fa1e431 --- /dev/null +++ b/changelogs/fragments/ensure_config_always_templated.yml @@ -0,0 +1,2 @@ +bugfixes: + - config, ensure that pulling values from configmanager are templated if possible. diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py index 75f345e0ca9..352529fc2bf 100755 --- a/lib/ansible/cli/config.py +++ b/lib/ansible/cli/config.py @@ -35,6 +35,13 @@ from ansible.utils.path import unfrackpath display = Display() +def get_constants(): + ''' helper method to ensure we can template based on existing constants ''' + if not hasattr(get_constants, 'cvars'): + get_constants.cvars = {k: getattr(C, k) for k in dir(C) if not k.startswith('__')} + return get_constants.cvars + + class ConfigCLI(CLI): """ Config command line class """ @@ -395,9 +402,9 @@ class ConfigCLI(CLI): def _get_global_configs(self): config = self.config.get_configuration_definitions(ignore_private=True).copy() - for setting in self.config.data.get_settings(): - if setting.name in config: - config[setting.name] = setting + for setting in config.keys(): + v, o = C.config.get_config_value_and_origin(setting, cfile=self.config_file, variables=get_constants()) + config[setting] = Setting(setting, v, o, None) return self._render_settings(config) @@ -445,7 +452,7 @@ class ConfigCLI(CLI): # actually get the values for setting in config_entries[finalname].keys(): try: - v, o = C.config.get_config_value_and_origin(setting, plugin_type=ptype, plugin_name=name) + v, o = C.config.get_config_value_and_origin(setting, cfile=self.config_file, plugin_type=ptype, plugin_name=name, variables=get_constants()) except AnsibleError as e: if to_text(e).startswith('No setting was provided for required configuration'): v = None diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py index a28a84a528b..0bb4d81aaab 100644 --- a/lib/ansible/config/manager.py +++ b/lib/ansible/config/manager.py @@ -13,9 +13,9 @@ import stat import tempfile import traceback -from collections.abc import Mapping, Sequence - from collections import namedtuple +from collections.abc import Mapping, Sequence +from jinja2.nativetypes import NativeEnvironment from ansible.config.data import ConfigData from ansible.errors import AnsibleOptionsError, AnsibleError @@ -302,7 +302,8 @@ class ConfigManager(object): self._parse_config_file() # update constants - self.update_config_data() + self.update_config_data(self._base_defs, self._config_file) + self._base_defs['CONFIG_FILE'] = {'default': None, 'type': 'path'} def _read_config_yaml_file(self, yml_file): # TODO: handle relative paths as relative to the directory containing the current playbook instead of CWD @@ -447,6 +448,9 @@ class ConfigManager(object): # use default config cfile = self._config_file + if config == 'CONFIG_FILE': + return cfile, '' + # Note: sources that are lists listed in low to high precedence (last one wins) value = None origin = None @@ -457,90 +461,94 @@ class ConfigManager(object): aliases = defs[config].get('aliases', []) # direct setting via plugin arguments, can set to None so we bypass rest of processing/defaults - direct_aliases = [] if direct: - direct_aliases = [direct[alias] for alias in aliases if alias in direct] - if direct and config in direct: - value = direct[config] - origin = 'Direct' - elif direct and direct_aliases: - value = direct_aliases[0] - origin = 'Direct' + if config in direct: + value = direct[config] + origin = 'Direct' + else: + direct_aliases = [direct[alias] for alias in aliases if alias in direct] + if direct_aliases: + value = direct_aliases[0] + origin = 'Direct' - else: + if value is None and variables and defs[config].get('vars'): # Use 'variable overrides' if present, highest precedence, but only present when querying running play - if variables and defs[config].get('vars'): - value, origin = self._loop_entries(variables, defs[config]['vars']) - origin = 'var: %s' % origin - - # use playbook keywords if you have em - if value is None and defs[config].get('keyword') and keys: - value, origin = self._loop_entries(keys, defs[config]['keyword']) - origin = 'keyword: %s' % origin - - # automap to keywords - # TODO: deprecate these in favor of explicit keyword above - if value is None and keys: - if config in keys: - value = keys[config] - keyword = config - - elif aliases: - for alias in aliases: - if alias in keys: - value = keys[alias] - keyword = alias - break - - if value is not None: - origin = 'keyword: %s' % keyword - - if value is None and 'cli' in defs[config]: - # avoid circular import .. until valid - from ansible import context - value, origin = self._loop_entries(context.CLIARGS, defs[config]['cli']) - origin = 'cli: %s' % origin - - # env vars are next precedence - if value is None and defs[config].get('env'): - value, origin = self._loop_entries(py3compat.environ, defs[config]['env']) - origin = 'env: %s' % origin - - # try config file entries next, if we have one - if self._parsers.get(cfile, None) is None: - self._parse_config_file(cfile) - - if value is None and cfile is not None: - ftype = get_config_type(cfile) - if ftype and defs[config].get(ftype): - if ftype == 'ini': - # load from ini config - try: # FIXME: generalize _loop_entries to allow for files also, most of this code is dupe - for ini_entry in defs[config]['ini']: - temp_value = get_ini_config_value(self._parsers[cfile], ini_entry) - if temp_value is not None: - value = temp_value - origin = cfile - if 'deprecated' in ini_entry: - self.DEPRECATED.append(('[%s]%s' % (ini_entry['section'], ini_entry['key']), ini_entry['deprecated'])) - except Exception as e: - sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e))) - elif ftype == 'yaml': - # FIXME: implement, also , break down key from defs (. notation???) - origin = cfile - - # set default if we got here w/o a value - if value is None: - if defs[config].get('required', False): - if not plugin_type or config not in INTERNAL_DEFS.get(plugin_type, {}): - raise AnsibleError("No setting was provided for required configuration %s" % - to_native(_get_entry(plugin_type, plugin_name, config))) - else: - value = defs[config].get('default') - origin = 'default' - # skip typing as this is a templated default that will be resolved later in constants, which has needed vars - if plugin_type is None and isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')): - return value, origin + value, origin = self._loop_entries(variables, defs[config]['vars']) + origin = 'var: %s' % origin + + # use playbook keywords if you have em + if value is None and defs[config].get('keyword') and keys: + value, origin = self._loop_entries(keys, defs[config]['keyword']) + origin = 'keyword: %s' % origin + + # automap to keywords + # TODO: deprecate these in favor of explicit keyword above + if value is None and keys: + if config in keys: + value = keys[config] + keyword = config + + elif aliases: + for alias in aliases: + if alias in keys: + value = keys[alias] + keyword = alias + break + + if value is not None: + origin = 'keyword: %s' % keyword + + if value is None and 'cli' in defs[config]: + # avoid circular import .. until valid + from ansible import context + value, origin = self._loop_entries(context.CLIARGS, defs[config]['cli']) + origin = 'cli: %s' % origin + + # env vars are next precedence + if value is None and defs[config].get('env'): + value, origin = self._loop_entries(py3compat.environ, defs[config]['env']) + origin = 'env: %s' % origin + + # try config file entries next, if we have one + if self._parsers.get(cfile, None) is None: + self._parse_config_file(cfile) + + if value is None and cfile is not None: + ftype = get_config_type(cfile) + if ftype and defs[config].get(ftype): + if ftype == 'ini': + # load from ini config + try: # FIXME: generalize _loop_entries to allow for files also, most of this code is dupe + for ini_entry in defs[config]['ini']: + temp_value = get_ini_config_value(self._parsers[cfile], ini_entry) + if temp_value is not None: + value = temp_value + origin = cfile + if 'deprecated' in ini_entry: + self.DEPRECATED.append(('[%s]%s' % (ini_entry['section'], ini_entry['key']), ini_entry['deprecated'])) + except Exception as e: + sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e))) + elif ftype == 'yaml': + # FIXME: implement, also , break down key from defs (. notation???) + origin = cfile + + # set default if we got here w/o a value + if value is None: + if defs[config].get('required', False): + if not plugin_type or config not in INTERNAL_DEFS.get(plugin_type, {}): + raise AnsibleError("No setting was provided for required configuration %s" % + to_native(_get_entry(plugin_type, plugin_name, config))) + else: + origin = 'default' + value = defs[config].get('default') + if isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')) and variables is not None: + # template default values if possible + # NOTE: cannot use is_template due to circular dep + try: + t = NativeEnvironment().from_string(value) + value = t.render(variables) + except Exception: + pass # not templatable # ensure correct type, can raise exceptions on mismatched types try: @@ -598,19 +606,16 @@ class ConfigManager(object): if defs is None: defs = self._base_defs - if configfile is None: configfile = self._config_file if not isinstance(defs, dict): raise AnsibleOptionsError("Invalid configuration definition type: %s for %s" % (type(defs), defs)) - # update the constant for config file - self.data.update_setting(Setting('CONFIG_FILE', configfile, '', 'string')) - origin = None # env and config defs can have several entries, ordered in list from lowest to highest precedence for config in defs: + if not isinstance(defs[config], dict): raise AnsibleOptionsError("Invalid configuration definition '%s': type is %s" % (to_native(config), type(defs[config]))) @@ -632,3 +637,6 @@ class ConfigManager(object): # set the constant self.data.update_setting(Setting(config, value, origin, defs[config].get('type', 'string'))) + + # update the constant for config file + self.data.update_setting(Setting('CONFIG_FILE', configfile, '', 'string')) diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index afe3936baf5..36646fbef9c 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -7,11 +7,9 @@ __metaclass__ = type import re -from ast import literal_eval -from jinja2 import Template from string import ascii_letters, digits -from ansible.config.manager import ConfigManager, ensure_type +from ansible.config.manager import ConfigManager from ansible.module_utils._text import to_text from ansible.module_utils.common.collections import Sequence from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE @@ -181,25 +179,8 @@ MAGIC_VARIABLE_MAPPING = dict( config = ConfigManager() # Generate constants from config -for setting in config.data.get_settings(): - - value = setting.value - if setting.origin == 'default' and \ - isinstance(setting.value, string_types) and \ - (setting.value.startswith('{{') and setting.value.endswith('}}')): - try: - t = Template(setting.value) - value = t.render(vars()) - try: - value = literal_eval(value) - except ValueError: - pass # not a python data structure - except Exception: - pass # not templatable - - value = ensure_type(value, setting.type) - - set_constant(setting.name, value) +for setting in config.get_configuration_definitions(): + set_constant(setting, config.get_config_value(setting, variables=vars())) for warn in config.WARNINGS: _warning(warn)