diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py index 0b60e88dcb6..1a12a1ff1c8 100644 --- a/lib/ansible/config/manager.py +++ b/lib/ansible/config/manager.py @@ -164,7 +164,7 @@ class ConfigManager(object): UNABLE = [] DEPRECATED = [] - def __init__(self, conf_file=None): + def __init__(self, conf_file=None, defs_file=None): self._base_defs = {} self._plugins = {} @@ -173,19 +173,24 @@ class ConfigManager(object): self._config_file = conf_file self.data = ConfigData() - # FIXME: make dynamic? scan for more? make it's own method? - # Create configuration definitions from source - bconfig_def = to_bytes('%s/base.yml' % os.path.dirname(__file__)) - if os.path.exists(bconfig_def): - with open(bconfig_def, 'rb') as config_def: + if defs_file is None: + # Create configuration definitions from source + b_defs_file = to_bytes('%s/base.yml' % os.path.dirname(__file__)) + else: + b_defs_file = to_bytes(defs_file) + + # consume definitions + if os.path.exists(b_defs_file): + with open(b_defs_file, 'rb') as config_def: self._base_defs = yaml.safe_load(config_def) else: - raise AnsibleError("Missing base configuration definition file (bad install?): %s" % to_native(bconfig_def)) + raise AnsibleError("Missing base configuration definition file (bad install?): %s" % to_native(b_defs_file)) if self._config_file is None: # set config using ini self._config_file = find_ini_config_file() + # consume configuration if self._config_file: if os.path.exists(self._config_file): # initialize parser and read config @@ -270,9 +275,12 @@ class ConfigManager(object): if cfile is None: cfile = self._config_file + else: + self._parse_config_file(cfile) # Note: sources that are lists listed in low to high precedence (last one wins) value = None + origin = None defs = {} if plugin_type is None: defs = self._base_defs @@ -281,60 +289,63 @@ class ConfigManager(object): else: defs = self._plugins[plugin_type][plugin_name] - # Use 'variable overrides' if present, highest precedence, but only present when querying running play - if variables: - value, origin = self._loop_entries(variables, defs[config]['vars']) - origin = 'var: %s' % origin - - # env vars are next precedence - if value is None and defs[config].get('env'): - value, origin = self._loop_entries(os.environ, defs[config]['env']) - origin = 'env: %s' % origin - - # try config file entries next, if we have one - 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: generaelize _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._parser, 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 - - ''' - # for plugins, try using existing constants, this is for backwards compatiblity - if plugin_name and defs[config].get('constants'): - value, origin = self._loop_entries(self.data, defs[config]['constants']) - origin = 'constant: %s' % origin - ''' - - # set default if we got here w/o a value - if value is None: - value = defs[config].get('default') - origin = 'default' - # skip typing as this is a temlated 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 - - # ensure correct type - try: - value = ensure_type(value, defs[config].get('type'), origin=origin) - except Exception as e: - self.UNABLE.append(config) - - # deal with deprecation of the setting - if 'deprecated' in defs[config] and origin != 'default': - self.DEPRECATED.append((config, defs[config].get('deprecated'))) + if config in defs: + # Use 'variable overrides' if present, highest precedence, but only present when querying running play + if variables: + value, origin = self._loop_entries(variables, defs[config]['vars']) + origin = 'var: %s' % origin + + # env vars are next precedence + if value is None and defs[config].get('env'): + value, origin = self._loop_entries(os.environ, defs[config]['env']) + origin = 'env: %s' % origin + + # try config file entries next, if we have one + 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._parser, 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 + + ''' + # for plugins, try using existing constants, this is for backwards compatiblity + if plugin_name and defs[config].get('constants'): + value, origin = self._loop_entries(self.data, defs[config]['constants']) + origin = 'constant: %s' % origin + ''' + + # set default if we got here w/o a value + if value is None: + value = defs[config].get('default') + origin = 'default' + # skip typing as this is a temlated 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 + + # ensure correct type + try: + value = ensure_type(value, defs[config].get('type'), origin=origin) + except Exception as e: + self.UNABLE.append(config) + + # deal with deprecation of the setting + if 'deprecated' in defs[config] and origin != 'default': + self.DEPRECATED.append((config, defs[config].get('deprecated'))) + else: + raise AnsibleError('Requested option %s was not defined in configuration' % to_native(config)) return value, origin diff --git a/test/units/config/test.cfg b/test/units/config/test.cfg new file mode 100644 index 00000000000..57958d87e06 --- /dev/null +++ b/test/units/config/test.cfg @@ -0,0 +1,4 @@ +[defaults] +inikey=fromini +matterless=lessfromini +mattermore=morefromini diff --git a/test/units/config/test.yml b/test/units/config/test.yml new file mode 100644 index 00000000000..384a055b2d2 --- /dev/null +++ b/test/units/config/test.yml @@ -0,0 +1,55 @@ +# mock config defs with diff use cases +config_entry: &entry + name: test config + default: DEFAULT + description: + - This does nothing, its for testing + env: + - name: ENVVAR + ini: + - section: defaults + key: inikey + type: string +config_entry_multi: &entry_multi + name: has more than one entry per config source + default: DEFAULT + description: + - This does nothing, its for testing + env: + - name: MATTERLESS + - name: MATTERMORE + ini: + - section: defaults + key: matterless + - section: defaults + key: mattermore + type: string +config_entry_bool: + <<: *entry + type: bool + default: False +config_entry_list: + <<: *entry + type: list + default: [DEFAULT] +config_entry_deprecated: + <<: *entry + deprecated: &dep + why: 'cause i wanna' + version: 9.2 + alternative: 'none whatso ever' +config_entry_multi_deprecated: + <<: *entry_multi + deprecated: *dep +config_entry_multi_deprecated_source: + <<: *entry_multi + env: + - name: MATTERLESS + deprecated: *dep + - name: MATTERMORE + ini: + - section: defaults + key: matterless + deprecated: *dep + - section: defaults + key: mattermore diff --git a/test/units/config/test2.cfg b/test/units/config/test2.cfg new file mode 100644 index 00000000000..da2d77b00b1 --- /dev/null +++ b/test/units/config/test2.cfg @@ -0,0 +1,4 @@ +[defaults] +inikey=fromini2 +matterless=lessfromini2 +mattermore=morefromini2 diff --git a/test/units/config/test_data.py b/test/units/config/test_data.py new file mode 100644 index 00000000000..9ee7d9ed388 --- /dev/null +++ b/test/units/config/test_data.py @@ -0,0 +1,41 @@ +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests import unittest + +from ansible.config.data import ConfigData +from ansible.config.manager import Setting + + +mykey = Setting('mykey', 'myvalue', 'test', 'string') +mykey2 = Setting('mykey2', 'myvalue2', ['test', 'test2'], 'list') +mykey3 = Setting('mykey3', 'myvalue3', 11111111111, 'integer') + + +class TestConfigData(unittest.TestCase): + + def setUp(self): + self.cdata = ConfigData() + + def tearDown(self): + self.cdata = None + + def test_update_setting(self): + for setting in [mykey, mykey2, mykey3]: + self.cdata.update_setting(setting) + self.assertEqual(setting, self.cdata._global_settings.get(setting.name)) + + def test_update_setting_with_plugin(self): + pass + + def test_get_setting(self): + self.cdata._global_settings = {'mykey': mykey} + self.assertEqual(mykey, self.cdata.get_setting('mykey')) + + def test_get_settings(self): + all_settings = {'mykey': mykey, 'mykey2': mykey2} + self.cdata._global_settings = all_settings + + for setting in self.cdata.get_settings(): + self.assertEqual(all_settings[setting.name], setting) diff --git a/test/units/config/test_manager.py b/test/units/config/test_manager.py new file mode 100644 index 00000000000..dd288e9e6bf --- /dev/null +++ b/test/units/config/test_manager.py @@ -0,0 +1,47 @@ +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.compat.tests import unittest + +from ansible.config.manager import ConfigManager, Setting + +curdir = os.path.dirname(__file__) +cfg_file = os.path.join(curdir, 'test.cfg') +cfg_file2 = os.path.join(curdir, 'test2.cfg') + +expected_ini = {'CONFIG_FILE': Setting(name='CONFIG_FILE', value=cfg_file, origin='', type='string'), + 'config_entry': Setting(name='config_entry', value=u'fromini', origin=cfg_file, type='string'), + 'config_entry_bool': Setting(name='config_entry_bool', value=False, origin=cfg_file, type='bool'), + 'config_entry_list': Setting(name='config_entry_list', value=['fromini'], origin=cfg_file, type='list'), + 'config_entry_deprecated': Setting(name='config_entry_deprecated', value=u'fromini', origin=cfg_file, type='string'), + 'config_entry_multi': Setting(name='config_entry_multi', value=u'morefromini', origin=cfg_file, type='string'), + 'config_entry_multi_deprecated': Setting(name='config_entry_multi_deprecated', value=u'morefromini', origin=cfg_file, type='string'), + 'config_entry_multi_deprecated_source': Setting(name='config_entry_multi_deprecated_source', value=u'morefromini', + origin=cfg_file, type='string')} + + +class TestConfigData(unittest.TestCase): + + def setUp(self): + self.manager = ConfigManager(os.path.join(curdir, 'test.cfg'), os.path.join(curdir, 'test.yml')) + + def tearDown(self): + self.manager = None + + def test_initial_load(self): + self.assertEquals(self.manager.data._global_settings, expected_ini) + + def test_value_and_origin_from_ini(self): + self.assertEquals(self.manager.get_config_value_and_origin('config_entry'), ('fromini', cfg_file)) + + def test_value_from_ini(self): + self.assertEquals(self.manager.get_config_value('config_entry'), 'fromini') + + def test_value_and_origin_from_alt_ini(self): + self.assertEquals(self.manager.get_config_value_and_origin('config_entry', cfile=cfg_file2), ('fromini2', cfg_file2)) + + def test_value_from_alt_ini(self): + self.assertEquals(self.manager.get_config_value('config_entry', cfile=cfg_file2), 'fromini2')