From 6ee1f273044ff086e2bae8c2b6d86fb1e2d9baee Mon Sep 17 00:00:00 2001 From: njutn95 Date: Wed, 25 Oct 2023 08:48:49 +0200 Subject: [PATCH] Add `modify_inactive_option` option to `ini_file` module to ignore matching commented key:value pairs (#7401) * Add `modify_inactive_option` option to `ini_file` module to ignore matching commented key:value pairs * Add changelog fragment * Apply suggestions from code review Co-authored-by: Felix Fontein * Minor comment fix * Apply suggestions from code review Co-authored-by: Felix Fontein --------- Co-authored-by: Felix Fontein --- .../7401-ini-file-modify-inactive-option.yaml | 2 + plugins/modules/ini_file.py | 32 +++-- .../targets/ini_file/tasks/main.yml | 3 + .../tasks/tests/06-modify_inactive_option.yml | 123 ++++++++++++++++++ 4 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 changelogs/fragments/7401-ini-file-modify-inactive-option.yaml create mode 100644 tests/integration/targets/ini_file/tasks/tests/06-modify_inactive_option.yml diff --git a/changelogs/fragments/7401-ini-file-modify-inactive-option.yaml b/changelogs/fragments/7401-ini-file-modify-inactive-option.yaml new file mode 100644 index 0000000000..a66074c12e --- /dev/null +++ b/changelogs/fragments/7401-ini-file-modify-inactive-option.yaml @@ -0,0 +1,2 @@ +minor_changes: + - ini_file - add ``modify_inactive_option`` option (https://github.com/ansible-collections/community.general/pull/7401). diff --git a/plugins/modules/ini_file.py b/plugins/modules/ini_file.py index 8e67a832ec..45e56ad7d2 100644 --- a/plugins/modules/ini_file.py +++ b/plugins/modules/ini_file.py @@ -116,6 +116,14 @@ options: - Allow option without value and without '=' symbol. type: bool default: false + modify_inactive_option: + description: + - By default the module replaces a commented line that matches the given option. + - Set this option to V(false) to avoid this. This is useful when you want to keep commented example + C(key=value) pairs for documentation purposes. + type: bool + default: true + version_added: 8.0.0 follow: description: - This flag indicates that filesystem links, if they exist, should be followed. @@ -190,7 +198,7 @@ def match_opt(option, line): def match_active_opt(option, line): option = re.escape(option) - return re.match('( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) + return re.match('()( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) def update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg): @@ -213,7 +221,7 @@ def update_section_line(option, changed, section_lines, index, changed_lines, ig def do_ini(module, filename, section=None, option=None, values=None, state='present', exclusive=True, backup=False, no_extra_spaces=False, - ignore_spaces=False, create=True, allow_no_value=False, follow=False): + ignore_spaces=False, create=True, allow_no_value=False, modify_inactive_option=True, follow=False): if section is not None: section = to_text(section) @@ -310,6 +318,12 @@ def do_ini(module, filename, section=None, option=None, values=None, # Keep track of changed section_lines changed_lines = [0] * len(section_lines) + # Determine whether to consider using commented out/inactive options or only active ones + if modify_inactive_option: + match_function = match_opt + else: + match_function = match_active_opt + # handling multiple instances of option=value when state is 'present' with/without exclusive is a bit complex # # 1. edit all lines where we have a option=value pair with a matching value in values[] @@ -319,8 +333,8 @@ def do_ini(module, filename, section=None, option=None, values=None, if state == 'present' and option: for index, line in enumerate(section_lines): - if match_opt(option, line): - match = match_opt(option, line) + if match_function(option, line): + match = match_function(option, line) if values and match.group(7) in values: matched_value = match.group(7) if not matched_value and allow_no_value: @@ -343,14 +357,14 @@ def do_ini(module, filename, section=None, option=None, values=None, # override option with no value to option with value if not allow_no_value if len(values) > 0: for index, line in enumerate(section_lines): - if not changed_lines[index] and match_opt(option, line): + if not changed_lines[index] and match_function(option, line): newline = assignment_format % (option, values.pop(0)) (changed, msg) = update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg) if len(values) == 0: break # remove all remaining option occurrences from the rest of the section for index in range(len(section_lines) - 1, 0, -1): - if not changed_lines[index] and match_opt(option, section_lines[index]): + if not changed_lines[index] and match_function(option, section_lines[index]): del section_lines[index] del changed_lines[index] changed = True @@ -394,7 +408,7 @@ def do_ini(module, filename, section=None, option=None, values=None, section_lines = new_section_lines elif not exclusive and len(values) > 0: # delete specified option=value line(s) - new_section_lines = [i for i in section_lines if not (match_active_opt(option, i) and match_active_opt(option, i).group(6) in values)] + new_section_lines = [i for i in section_lines if not (match_active_opt(option, i) and match_active_opt(option, i).group(7) in values)] if section_lines != new_section_lines: changed = True msg = 'option changed' @@ -466,6 +480,7 @@ def main(): no_extra_spaces=dict(type='bool', default=False), ignore_spaces=dict(type='bool', default=False), allow_no_value=dict(type='bool', default=False), + modify_inactive_option=dict(type='bool', default=True), create=dict(type='bool', default=True), follow=dict(type='bool', default=False) ), @@ -487,6 +502,7 @@ def main(): no_extra_spaces = module.params['no_extra_spaces'] ignore_spaces = module.params['ignore_spaces'] allow_no_value = module.params['allow_no_value'] + modify_inactive_option = module.params['modify_inactive_option'] create = module.params['create'] follow = module.params['follow'] @@ -500,7 +516,7 @@ def main(): (changed, backup_file, diff, msg) = do_ini( module, path, section, option, values, state, exclusive, backup, - no_extra_spaces, ignore_spaces, create, allow_no_value, follow) + no_extra_spaces, ignore_spaces, create, allow_no_value, modify_inactive_option, follow) if not module.check_mode and os.path.exists(path): file_args = module.load_file_common_arguments(module.params) diff --git a/tests/integration/targets/ini_file/tasks/main.yml b/tests/integration/targets/ini_file/tasks/main.yml index 2fa54cd98c..dbd922a9c6 100644 --- a/tests/integration/targets/ini_file/tasks/main.yml +++ b/tests/integration/targets/ini_file/tasks/main.yml @@ -44,3 +44,6 @@ - name: include tasks to test ignore_spaces include_tasks: tests/05-ignore_spaces.yml + + - name: include tasks to test modify_inactive_option + include_tasks: tests/06-modify_inactive_option.yml diff --git a/tests/integration/targets/ini_file/tasks/tests/06-modify_inactive_option.yml b/tests/integration/targets/ini_file/tasks/tests/06-modify_inactive_option.yml new file mode 100644 index 0000000000..2d1d049281 --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/06-modify_inactive_option.yml @@ -0,0 +1,123 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +## testing modify_inactive_option option + +- name: test-modify_inactive_option 1 - create test file + copy: + content: | + + [section1] + # Uncomment the line below to enable foo + # foo = bar + + dest: "{{ output_file }}" + +- name: test-modify_inactive_option 1 - set value for foo with modify_inactive_option set to true + ini_file: + path: "{{ output_file }}" + section: section1 + option: foo + value: bar + modify_inactive_option: true + register: result1 + +- name: test-modify_inactive_option 1 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-modify_inactive_option 1 - set expected content and get current ini file content + set_fact: + expected1: | + + [section1] + # Uncomment the line below to enable foo + foo = bar + + content1: "{{ output_content.content | b64decode }}" + +- name: test-modify_inactive_option 1 - assert 'changed' is true, content is OK and option changed + assert: + that: + - result1 is changed + - result1.msg == 'option changed' + - content1 == expected1 + + +- name: test-modify_inactive_option 2 - create test file + copy: + content: | + + [section1] + # Uncomment the line below to enable foo + # foo = bar + + dest: "{{ output_file }}" + +- name: test-modify_inactive_option 2 - set value for foo with modify_inactive_option set to false + ini_file: + path: "{{ output_file }}" + section: section1 + option: foo + value: bar + modify_inactive_option: false + register: result2 + +- name: test-modify_inactive_option 2 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-modify_inactive_option 2 - set expected content and get current ini file content + set_fact: + expected2: | + + [section1] + foo = bar + # Uncomment the line below to enable foo + # foo = bar + + content2: "{{ output_content.content | b64decode }}" + +- name: test-modify_inactive_option 2 - assert 'changed' is true and content is OK and option added + assert: + that: + - result2 is changed + - result2.msg == 'option added' + - content2 == expected2 + + +- name: test-modify_inactive_option 3 - remove foo=bar with modify_inactive_option set to true to ensure it doesn't have effect for removal + ini_file: + path: "{{ output_file }}" + section: section1 + option: foo + value: bar + modify_inactive_option: true + state: absent + register: result3 + +- name: test-modify_inactive_option 3 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-modify_inactive_option 3 - set expected content and get current ini file content + set_fact: + expected3: | + + [section1] + # Uncomment the line below to enable foo + # foo = bar + + content3: "{{ output_content.content | b64decode }}" + +- name: test-modify_inactive_option 3 - assert 'changed' is true and content is OK and active option removed + assert: + that: + - result3 is changed + - result3.msg == 'option changed' + - content3 == expected3 \ No newline at end of file