Config, ensure templating happens at functions (#77483)

move away from special templating in constants.py
 simplified main get, unified paths to get info

* Let native jinja deal with eval for type
* make constants available for templating defaults
* ensure we use config file
pull/77544/head
Brian Coca 4 years ago committed by GitHub
parent c3a3bc1d0d
commit 6e5f1d781d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
bugfixes:
- config, ensure that pulling values from configmanager are templated if possible.

@ -35,6 +35,13 @@ from ansible.utils.path import unfrackpath
display = Display() 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): class ConfigCLI(CLI):
""" Config command line class """ """ Config command line class """
@ -395,9 +402,9 @@ class ConfigCLI(CLI):
def _get_global_configs(self): def _get_global_configs(self):
config = self.config.get_configuration_definitions(ignore_private=True).copy() config = self.config.get_configuration_definitions(ignore_private=True).copy()
for setting in self.config.data.get_settings(): for setting in config.keys():
if setting.name in config: v, o = C.config.get_config_value_and_origin(setting, cfile=self.config_file, variables=get_constants())
config[setting.name] = setting config[setting] = Setting(setting, v, o, None)
return self._render_settings(config) return self._render_settings(config)
@ -445,7 +452,7 @@ class ConfigCLI(CLI):
# actually get the values # actually get the values
for setting in config_entries[finalname].keys(): for setting in config_entries[finalname].keys():
try: 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: except AnsibleError as e:
if to_text(e).startswith('No setting was provided for required configuration'): if to_text(e).startswith('No setting was provided for required configuration'):
v = None v = None

@ -13,9 +13,9 @@ import stat
import tempfile import tempfile
import traceback import traceback
from collections.abc import Mapping, Sequence
from collections import namedtuple from collections import namedtuple
from collections.abc import Mapping, Sequence
from jinja2.nativetypes import NativeEnvironment
from ansible.config.data import ConfigData from ansible.config.data import ConfigData
from ansible.errors import AnsibleOptionsError, AnsibleError from ansible.errors import AnsibleOptionsError, AnsibleError
@ -302,7 +302,8 @@ class ConfigManager(object):
self._parse_config_file() self._parse_config_file()
# update constants # 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): def _read_config_yaml_file(self, yml_file):
# TODO: handle relative paths as relative to the directory containing the current playbook instead of CWD # 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 # use default config
cfile = self._config_file cfile = self._config_file
if config == 'CONFIG_FILE':
return cfile, ''
# Note: sources that are lists listed in low to high precedence (last one wins) # Note: sources that are lists listed in low to high precedence (last one wins)
value = None value = None
origin = None origin = None
@ -457,90 +461,94 @@ class ConfigManager(object):
aliases = defs[config].get('aliases', []) aliases = defs[config].get('aliases', [])
# direct setting via plugin arguments, can set to None so we bypass rest of processing/defaults # direct setting via plugin arguments, can set to None so we bypass rest of processing/defaults
direct_aliases = []
if direct: if direct:
direct_aliases = [direct[alias] for alias in aliases if alias in direct] if config in direct:
if direct and config in direct: value = direct[config]
value = direct[config] origin = 'Direct'
origin = 'Direct' else:
elif direct and direct_aliases: direct_aliases = [direct[alias] for alias in aliases if alias in direct]
value = direct_aliases[0] if direct_aliases:
origin = 'Direct' 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 # 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'])
value, origin = self._loop_entries(variables, defs[config]['vars']) origin = 'var: %s' % origin
origin = 'var: %s' % origin
# use playbook keywords if you have em
# use playbook keywords if you have em if value is None and defs[config].get('keyword') and keys:
if value is None and defs[config].get('keyword') and keys: value, origin = self._loop_entries(keys, defs[config]['keyword'])
value, origin = self._loop_entries(keys, defs[config]['keyword']) origin = 'keyword: %s' % origin
origin = 'keyword: %s' % origin
# automap to keywords
# automap to keywords # TODO: deprecate these in favor of explicit keyword above
# TODO: deprecate these in favor of explicit keyword above if value is None and keys:
if value is None and keys: if config in keys:
if config in keys: value = keys[config]
value = keys[config] keyword = config
keyword = config
elif aliases:
elif aliases: for alias in aliases:
for alias in aliases: if alias in keys:
if alias in keys: value = keys[alias]
value = keys[alias] keyword = alias
keyword = alias break
break
if value is not None:
if value is not None: origin = 'keyword: %s' % keyword
origin = 'keyword: %s' % keyword
if value is None and 'cli' in defs[config]:
if value is None and 'cli' in defs[config]: # avoid circular import .. until valid
# avoid circular import .. until valid from ansible import context
from ansible import context value, origin = self._loop_entries(context.CLIARGS, defs[config]['cli'])
value, origin = self._loop_entries(context.CLIARGS, defs[config]['cli']) origin = 'cli: %s' % origin
origin = 'cli: %s' % origin
# env vars are next precedence
# env vars are next precedence if value is None and defs[config].get('env'):
if value is None and defs[config].get('env'): value, origin = self._loop_entries(py3compat.environ, defs[config]['env'])
value, origin = self._loop_entries(py3compat.environ, defs[config]['env']) origin = 'env: %s' % origin
origin = 'env: %s' % origin
# try config file entries next, if we have one
# try config file entries next, if we have one if self._parsers.get(cfile, None) is None:
if self._parsers.get(cfile, None) is None: self._parse_config_file(cfile)
self._parse_config_file(cfile)
if value is None and cfile is not None:
if value is None and cfile is not None: ftype = get_config_type(cfile)
ftype = get_config_type(cfile) if ftype and defs[config].get(ftype):
if ftype and defs[config].get(ftype): if ftype == 'ini':
if ftype == 'ini': # load from ini config
# load from ini config try: # FIXME: generalize _loop_entries to allow for files also, most of this code is dupe
try: # FIXME: generalize _loop_entries to allow for files also, most of this code is dupe for ini_entry in defs[config]['ini']:
for ini_entry in defs[config]['ini']: temp_value = get_ini_config_value(self._parsers[cfile], ini_entry)
temp_value = get_ini_config_value(self._parsers[cfile], ini_entry) if temp_value is not None:
if temp_value is not None: value = temp_value
value = temp_value origin = cfile
origin = cfile if 'deprecated' in ini_entry:
if 'deprecated' in ini_entry: self.DEPRECATED.append(('[%s]%s' % (ini_entry['section'], ini_entry['key']), ini_entry['deprecated']))
self.DEPRECATED.append(('[%s]%s' % (ini_entry['section'], ini_entry['key']), ini_entry['deprecated'])) except Exception as e:
except Exception as e: sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e)))
sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e))) elif ftype == 'yaml':
elif ftype == 'yaml': # FIXME: implement, also , break down key from defs (. notation???)
# FIXME: implement, also , break down key from defs (. notation???) origin = cfile
origin = cfile
# set default if we got here w/o a value
# set default if we got here w/o a value if value is None:
if value is None: if defs[config].get('required', False):
if defs[config].get('required', False): if not plugin_type or config not in INTERNAL_DEFS.get(plugin_type, {}):
if not plugin_type or config not in INTERNAL_DEFS.get(plugin_type, {}): raise AnsibleError("No setting was provided for required configuration %s" %
raise AnsibleError("No setting was provided for required configuration %s" % to_native(_get_entry(plugin_type, plugin_name, config)))
to_native(_get_entry(plugin_type, plugin_name, config))) else:
else: origin = 'default'
value = defs[config].get('default') value = defs[config].get('default')
origin = 'default' if isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')) and variables is not None:
# skip typing as this is a templated default that will be resolved later in constants, which has needed vars # template default values if possible
if plugin_type is None and isinstance(value, string_types) and (value.startswith('{{') and value.endswith('}}')): # NOTE: cannot use is_template due to circular dep
return value, origin try:
t = NativeEnvironment().from_string(value)
value = t.render(variables)
except Exception:
pass # not templatable
# ensure correct type, can raise exceptions on mismatched types # ensure correct type, can raise exceptions on mismatched types
try: try:
@ -598,19 +606,16 @@ class ConfigManager(object):
if defs is None: if defs is None:
defs = self._base_defs defs = self._base_defs
if configfile is None: if configfile is None:
configfile = self._config_file configfile = self._config_file
if not isinstance(defs, dict): if not isinstance(defs, dict):
raise AnsibleOptionsError("Invalid configuration definition type: %s for %s" % (type(defs), defs)) 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 origin = None
# env and config defs can have several entries, ordered in list from lowest to highest precedence # env and config defs can have several entries, ordered in list from lowest to highest precedence
for config in defs: for config in defs:
if not isinstance(defs[config], dict): if not isinstance(defs[config], dict):
raise AnsibleOptionsError("Invalid configuration definition '%s': type is %s" % (to_native(config), type(defs[config]))) 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 # set the constant
self.data.update_setting(Setting(config, value, origin, defs[config].get('type', 'string'))) 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'))

@ -7,11 +7,9 @@ __metaclass__ = type
import re import re
from ast import literal_eval
from jinja2 import Template
from string import ascii_letters, digits 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._text import to_text
from ansible.module_utils.common.collections import Sequence from ansible.module_utils.common.collections import Sequence
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
@ -181,25 +179,8 @@ MAGIC_VARIABLE_MAPPING = dict(
config = ConfigManager() config = ConfigManager()
# Generate constants from config # Generate constants from config
for setting in config.data.get_settings(): for setting in config.get_configuration_definitions():
set_constant(setting, config.get_config_value(setting, variables=vars()))
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 warn in config.WARNINGS: for warn in config.WARNINGS:
_warning(warn) _warning(warn)

Loading…
Cancel
Save