From 75cb8670fb1b796e83e1a0f4a2b1bdf0b9c8c794 Mon Sep 17 00:00:00 2001 From: Andy McCrae Date: Fri, 4 May 2018 12:16:39 +0100 Subject: [PATCH] Allow default section in ini to be specified The default section in an ini file will always be 'DEFAULT', but for some configuration files you may want to be able to set this to ensure another section is at the top. 'global' for example, when working with fio. This PR adds the ability to set the section to be at the top, but will default to 'DEFAULT' so no change will happen to existing tasks running config_template. Change-Id: Ifbc0b91aef46f3c2d98e73fdc9ab888244550bab --- action/config_template.py | 62 ++++++++++++------- library/config_template | 7 +++ .../top_ini_section-c28d7acadf5fe836.yaml | 5 ++ tests/files/test_default_section.ini.expected | 8 +++ tests/templates/test_default_section.ini | 5 ++ tests/test.yml | 20 ++++++ 6 files changed, 85 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/top_ini_section-c28d7acadf5fe836.yaml create mode 100644 tests/files/test_default_section.ini.expected create mode 100644 tests/templates/test_default_section.ini diff --git a/action/config_template.py b/action/config_template.py index 74efb9f..f4f7cae 100644 --- a/action/config_template.py +++ b/action/config_template.py @@ -72,6 +72,7 @@ class MultiKeyDict(dict): >>> print(z) ... {'a': tuple(['1', '2']), 'c': {'a': 1}, 'b': ['a', 'b', 'c']} """ + def __setitem__(self, key, value): if key in self: if isinstance(self[key], tuple): @@ -134,9 +135,11 @@ class ConfigTemplateParser(ConfigParser.RawConfigParser): key = var3 key = var2 """ + def __init__(self, *args, **kwargs): self._comments = {} self.ignore_none_type = bool(kwargs.pop('ignore_none_type', True)) + self.default_section = str(kwargs.pop('default_section', 'DEFAULT')) ConfigParser.RawConfigParser.__init__(self, *args, **kwargs) def _write(self, fp, section, key, item, entry): @@ -165,28 +168,33 @@ class ConfigTemplateParser(ConfigParser.RawConfigParser): self._write(fp, section, key, value, entry) def write(self, fp): + def _do_write(section_name, section, section_bool=False): + _write_comments(section_name) + fp.write("[%s]\n" % section_name) + for key, value in sorted(section.items()): + _write_comments(section_name, optname=key) + self._write_check(fp, key=key, value=value, + section=section_bool) + else: + fp.write("\n") + def _write_comments(section, optname=None): comsect = self._comments.get(section, {}) if optname in comsect: fp.write(''.join(comsect[optname])) + if self.default_section != 'DEFAULT' and self._sections.get( + self.default_section, False): + _do_write(self.default_section, + self._sections[self.default_section], + section_bool=True) + self._sections.pop(self.default_section) + if self._defaults: - _write_comments('DEFAULT') - fp.write("[%s]\n" % 'DEFAULT') - for key, value in sorted(self._defaults.items()): - _write_comments('DEFAULT', optname=key) - self._write_check(fp, key=key, value=value) - else: - fp.write("\n") + _do_write('DEFAULT', self._defaults) for section in sorted(self._sections): - _write_comments(section) - fp.write("[%s]\n" % section) - for key, value in sorted(self._sections[section].items()): - _write_comments(section, optname=key) - self._write_check(fp, key=key, value=value, section=True) - else: - fp.write("\n") + _do_write(section, self._sections[section], section_bool=True) def _read(self, fp, fpname): comments = [] @@ -293,7 +301,8 @@ class ActionModule(ActionBase): config_overrides, resultant, list_extend=True, - ignore_none_type=True): + ignore_none_type=True, + default_section='DEFAULT'): """Returns string value from a modified config file. :param config_overrides: ``dict`` @@ -307,7 +316,8 @@ class ActionModule(ActionBase): config = ConfigTemplateParser( allow_no_value=True, dict_type=MultiKeyDict, - ignore_none_type=ignore_none_type + ignore_none_type=ignore_none_type, + default_section=default_section ) config.optionxform = str except Exception: @@ -376,7 +386,8 @@ class ActionModule(ActionBase): config_overrides, resultant, list_extend=True, - ignore_none_type=True): + ignore_none_type=True, + default_section='DEFAULT'): """Returns config json Its important to note that file ordering will not be preserved as the @@ -390,7 +401,8 @@ class ActionModule(ActionBase): merged_resultant = self._merge_dict( base_items=original_resultant, new_items=config_overrides, - list_extend=list_extend + list_extend=list_extend, + default_section=default_section ) return json.dumps( merged_resultant, @@ -402,7 +414,8 @@ class ActionModule(ActionBase): config_overrides, resultant, list_extend=True, - ignore_none_type=True): + ignore_none_type=True, + default_section='DEFAULT'): """Return config yaml. :param config_overrides: ``dict`` @@ -537,6 +550,8 @@ class ActionModule(ActionBase): # name with out the '=' or ':' suffix. The default is true. ignore_none_type = self._task.args.get('ignore_none_type', True) + default_section = self._task.args.get('default_section', 'DEFAULT') + return True, dict( source=source, dest=user_dest, @@ -544,7 +559,8 @@ class ActionModule(ActionBase): config_type=config_type, searchpath=searchpath, list_extend=list_extend, - ignore_none_type=ignore_none_type + ignore_none_type=ignore_none_type, + default_section=default_section ) def run(self, tmp=None, task_vars=None): @@ -618,7 +634,8 @@ class ActionModule(ActionBase): config_overrides=_vars['config_overrides'], resultant=resultant, list_extend=_vars.get('list_extend', True), - ignore_none_type=_vars.get('ignore_none_type', True) + ignore_none_type=_vars.get('ignore_none_type', True), + default_section=_vars.get('default_section', 'DEFAULT') ) # Re-template the resultant object as it may have new data within it @@ -651,6 +668,7 @@ class ActionModule(ActionBase): new_module_args.pop('config_type', None) new_module_args.pop('list_extend', None) new_module_args.pop('ignore_none_type', None) + new_module_args.pop('default_section', None) # Content from config_template is converted to src new_module_args.pop('content', None) @@ -663,7 +681,7 @@ class ActionModule(ActionBase): if self._play_context.diff: rc['diff'] = [] rc['diff'].append(self._get_diff_data(_vars['dest'], - transferred_data, task_vars)) + transferred_data, task_vars)) if self._task.args.get('content'): os.remove(_vars['source']) return rc diff --git a/library/config_template b/library/config_template index f879d90..93a2662 100644 --- a/library/config_template +++ b/library/config_template @@ -58,6 +58,12 @@ options: choices: - True - False + default_section: + description: + - Specify the default section for INI configuration files. This is the + section that will appear at the top of the configuration file. For + example 'global'. + default: 'DEFAULT' author: Kevin Carter """ @@ -68,6 +74,7 @@ EXAMPLES = """ src: templates/test.ini.j2 dest: /tmp/test.ini config_overrides: {} + top_ini_section: 'global' config_type: ini - name: run config template json diff --git a/releasenotes/notes/top_ini_section-c28d7acadf5fe836.yaml b/releasenotes/notes/top_ini_section-c28d7acadf5fe836.yaml new file mode 100644 index 0000000..1c9186a --- /dev/null +++ b/releasenotes/notes/top_ini_section-c28d7acadf5fe836.yaml @@ -0,0 +1,5 @@ +--- +features: + - Allow the default section in an ini file to be specified + using the ``default_section`` variable when calling a + ``config_template`` task. This defaults to ``DEFAULT``. diff --git a/tests/files/test_default_section.ini.expected b/tests/files/test_default_section.ini.expected new file mode 100644 index 0000000..69bd9f0 --- /dev/null +++ b/tests/files/test_default_section.ini.expected @@ -0,0 +1,8 @@ +[global] +test1 = 1 +test2 = 2 + +[section1] +setting1 = 1 +setting2 = 2 + diff --git a/tests/templates/test_default_section.ini b/tests/templates/test_default_section.ini new file mode 100644 index 0000000..531e938 --- /dev/null +++ b/tests/templates/test_default_section.ini @@ -0,0 +1,5 @@ +[section1] +setting1=1 + +[global] +test1=1 diff --git a/tests/test.yml b/tests/test.yml index 54c3269..d89b8a4 100644 --- a/tests/test.yml +++ b/tests/test.yml @@ -215,6 +215,21 @@ - [ 0, 1, 2 ] - [ "{{ test_config_ini_overrides }}" ] + - name: Put down default_section_expected file + copy: + src: "{{ playbook_dir }}/files/test_default_section.ini.expected" + dest: "/tmp/test_default_section.ini" + + - name: Template using default_section + config_template: + src: "{{ playbook_dir }}/templates/test_default_section.ini" + dest: "/tmp/test_default_section.ini" + config_type: "ini" + config_overrides: "{{ test_default_section_overrides }}" + default_section: "global" + register: template_changed + failed_when: template_changed | changed + vars: test_config_ini_overrides: DEFAULT: @@ -259,3 +274,8 @@ - 4 test_config_yml_hostvars_overrides: test_hostvar: "{{ ansible_default_ipv4.address }}" + test_default_section_overrides: + global: + test2: 2 + section1: + setting2: 2