From 25267b80941f1b41c280ed2d6c1ec8162fb8a62b Mon Sep 17 00:00:00 2001 From: Daniel Ziegenberg Date: Sun, 15 Aug 2021 12:59:50 +0200 Subject: [PATCH] ini_file: add multiple options with same name to ini file (#3033) * ini_file - prepare for fixing #273 - restructure tests - fix error message call: fail_json() takes 1 positional argument but 2 were given * ini_file - multiple values for one option (#273) - add module option 'exclusive' (boolean) for the abbility to add single option=value entries without overwriting existing options with the same name but different values - add abbility to define multiple options with the same name but different values * ini_file - add more tests for ini_file * ini_file - fix sanity tests * apply suggested changes: - rename 03-regressions.yml to 03-encoding.yml - fix typos - fix documentation * apply suggested changes: - test errors also for result is failed * apply suggested changes: - make state=absent also work with module option exclusive - add more tests for state=absent and module option exclusive * fix sanity test: - 02-values.yml:251:9: hyphens: too many spaces after hyphen * apply proposed changes * apply proposed changes from review - adjust version_added to 3.6.0 - small syntax change in changelog fragment --- ...ple_options_with_same_name_to_ini_file.yml | 3 + plugins/modules/files/ini_file.py | 265 +++-- .../targets/ini_file/tasks/main.yml | 552 +-------- .../targets/ini_file/tasks/tests/00-basic.yml | 38 + .../targets/ini_file/tasks/tests/01-value.yml | 589 ++++++++++ .../ini_file/tasks/tests/02-values.yml | 1013 +++++++++++++++++ .../ini_file/tasks/tests/03-encoding.yml | 41 + 7 files changed, 1902 insertions(+), 599 deletions(-) create mode 100644 changelogs/fragments/273-add_multiple_options_with_same_name_to_ini_file.yml create mode 100644 tests/integration/targets/ini_file/tasks/tests/00-basic.yml create mode 100644 tests/integration/targets/ini_file/tasks/tests/01-value.yml create mode 100644 tests/integration/targets/ini_file/tasks/tests/02-values.yml create mode 100644 tests/integration/targets/ini_file/tasks/tests/03-encoding.yml diff --git a/changelogs/fragments/273-add_multiple_options_with_same_name_to_ini_file.yml b/changelogs/fragments/273-add_multiple_options_with_same_name_to_ini_file.yml new file mode 100644 index 0000000000..f32dc305b5 --- /dev/null +++ b/changelogs/fragments/273-add_multiple_options_with_same_name_to_ini_file.yml @@ -0,0 +1,3 @@ +minor_changes: + - ini_file - add module option ``exclusive`` (boolean) for the ability to add/remove single ``option=value`` entries without overwriting existing options with the same name but different values (https://github.com/ansible-collections/community.general/pull/3033). + - ini_file - add abbility to define multiple options with the same name but different values (https://github.com/ansible-collections/community.general/issues/273, https://github.com/ansible-collections/community.general/issues/1204). diff --git a/plugins/modules/files/ini_file.py b/plugins/modules/files/ini_file.py index a9c2e290b0..f25cc063ff 100644 --- a/plugins/modules/files/ini_file.py +++ b/plugins/modules/files/ini_file.py @@ -47,7 +47,18 @@ options: description: - The string value to be associated with an I(option). - May be omitted when removing an I(option). + - Mutually exclusive with I(values). + - I(value=v) is equivalent to I(values=[v]). type: str + values: + description: + - The string value to be associated with an I(option). + - May be omitted when removing an I(option). + - Mutually exclusive with I(value). + - I(value=v) is equivalent to I(values=[v]). + type: list + elements: str + version_added: 3.6.0 backup: description: - Create a backup file including the timestamp information so you can get @@ -56,10 +67,25 @@ options: default: no state: description: - - If set to C(absent) the option or section will be removed if present instead of created. + - If set to C(absent) and I(exclusive) set to C(yes) all matching I(option) lines are removed. + - If set to C(absent) and I(exclusive) set to C(no) the specified C(option=value) lines are removed, + but the other I(option)s with the same name are not touched. + - If set to C(present) and I(exclusive) set to C(no) the specified C(option=values) lines are added, + but the other I(option)s with the same name are not touched. + - If set to C(present) and I(exclusive) set to C(yes) all given C(option=values) lines will be + added and the other I(option)s with the same name are removed. type: str choices: [ absent, present ] default: present + exclusive: + description: + - If set to C(yes) (default), all matching I(option) lines are removed when I(state=absent), + or replaced when I(state=present). + - If set to C(no), only the specified I(value(s)) are added when I(state=present), + or removed when I(state=absent), and existing ones are not modified. + type: bool + default: yes + version_added: 3.6.0 no_extra_spaces: description: - Do not insert spaces before and after '=' symbol. @@ -103,6 +129,27 @@ EXAMPLES = r''' option: temperature value: cold backup: yes + +- name: Add "beverage=lemon juice" is in section "[drinks]" in specified file + community.general.ini_file: + path: /etc/conf + section: drinks + option: beverage + value: lemon juice + mode: '0600' + state: present + exclusive: no + +- name: Ensure multiple values "beverage=coke" and "beverage=pepsi" are in section "[drinks]" in specified file + community.general.ini_file: + path: /etc/conf + section: drinks + option: beverage + values: + - coke + - pepsi + mode: '0600' + state: present ''' import io @@ -117,24 +164,37 @@ from ansible.module_utils.common.text.converters import to_bytes, to_text def match_opt(option, line): option = re.escape(option) - return re.match('[#;]?( |\t)*%s( |\t)*(=|$)' % option, line) + return re.match('[#;]?( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) def match_active_opt(option, line): option = re.escape(option) - return re.match('( |\t)*%s( |\t)*(=|$)' % option, line) + return re.match('( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line) -def do_ini(module, filename, section=None, option=None, value=None, - state='present', backup=False, no_extra_spaces=False, create=True, - allow_no_value=False): +def update_section_line(changed, section_lines, index, changed_lines, newline, msg): + option_changed = section_lines[index] != newline + changed = changed or option_changed + if option_changed: + msg = 'option changed' + section_lines[index] = newline + changed_lines[index] = 1 + return (changed, msg) + + +def do_ini(module, filename, section=None, option=None, values=None, + state='present', exclusive=True, backup=False, no_extra_spaces=False, + create=True, allow_no_value=False): if section is not None: section = to_text(section) if option is not None: option = to_text(option) - if value is not None: - value = to_text(value) + + # deduplicate entries in values + values_unique = [] + [values_unique.append(to_text(value)) for value in values if value not in values_unique and value is not None] + values = values_unique diff = dict( before='', @@ -145,7 +205,7 @@ def do_ini(module, filename, section=None, option=None, value=None, if not os.path.exists(filename): if not create: - module.fail_json(rc=257, msg='Destination %s does not exist !' % filename) + module.fail_json(rc=257, msg='Destination %s does not exist!' % filename) destpath = os.path.dirname(filename) if not os.path.exists(destpath) and not module.check_mode: os.makedirs(destpath) @@ -185,74 +245,134 @@ def do_ini(module, filename, section=None, option=None, value=None, section = fake_section_name within_section = not section - section_start = 0 + section_start = section_end = 0 msg = 'OK' if no_extra_spaces: assignment_format = u'%s=%s\n' else: assignment_format = u'%s = %s\n' + option_no_value_present = False + non_blank_non_comment_pattern = re.compile(to_text(r'^[ \t]*([#;].*)?$')) + before = after = [] + section_lines = [] + for index, line in enumerate(ini_lines): + # find start and end of section if line.startswith(u'[%s]' % section): within_section = True section_start = index elif line.startswith(u'['): if within_section: - if state == 'present': - # insert missing option line at the end of the section - for i in range(index, 0, -1): - # search backwards for previous non-blank or non-comment line - if not non_blank_non_comment_pattern.match(ini_lines[i - 1]): - if option and value is not None: - ini_lines.insert(i, assignment_format % (option, value)) - msg = 'option added' - changed = True - elif option and value is None and allow_no_value: - ini_lines.insert(i, '%s\n' % option) - msg = 'option added' - changed = True - break - elif state == 'absent' and not option: - # remove the entire section - del ini_lines[section_start:index] - msg = 'section removed' + section_end = index + break + + before = ini_lines[0:section_start] + section_lines = ini_lines[section_start:section_end] + after = ini_lines[section_end:len(ini_lines)] + + # Keep track of changed section_lines + changed_lines = [0] * len(section_lines) + + # 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[] + # 2. edit all the remaing lines where we have a matching option + # 3. delete remaining lines where we have a matching option + # 4. insert missing option line(s) at the end of the section + + if state == 'present' and option: + for index, line in enumerate(section_lines): + if match_opt(option, line): + match = match_opt(option, line) + if values and match.group(6) in values: + matched_value = match.group(6) + if not matched_value and allow_no_value: + # replace existing option with no value line(s) + newline = u'%s\n' % option + option_no_value_present = True + else: + # replace existing option=value line(s) + newline = assignment_format % (option, matched_value) + (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg) + values.remove(matched_value) + elif not values and allow_no_value: + # replace existing option with no value line(s) + newline = u'%s\n' % option + (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg) + option_no_value_present = True + break + + if state == 'present' and exclusive and not allow_no_value: + # 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_active_opt(option, section_lines[index]): + newline = assignment_format % (option, values.pop(0)) + (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, 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_active_opt(option, section_lines[index]): + del section_lines[index] + del changed_lines[index] + changed = True + msg = 'option changed' + + if state == 'present': + # insert missing option line(s) at the end of the section + for index in range(len(section_lines), 0, -1): + # search backwards for previous non-blank or non-comment line + if not non_blank_non_comment_pattern.match(section_lines[index - 1]): + if option and values: + # insert option line(s) + for element in values[::-1]: + # items are added backwards, so traverse the list backwards to not confuse the user + # otherwise some of their options might appear in reverse order for whatever fancy reason ¯\_(ツ)_/¯ + if element is not None: + # insert option=value line + section_lines.insert(index, assignment_format % (option, element)) + msg = 'option added' + changed = True + elif element is None and allow_no_value: + # insert option with no value line + section_lines.insert(index, u'%s\n' % option) + msg = 'option added' + changed = True + elif option and not values and allow_no_value and not option_no_value_present: + # insert option with no value line(s) + section_lines.insert(index, u'%s\n' % option) + msg = 'option added' changed = True break + + if state == 'absent': + if option: + if exclusive: + # delete all option line(s) with given option and ignore value + new_section_lines = [line for line in section_lines if not (match_active_opt(option, line))] + if section_lines != new_section_lines: + changed = True + msg = 'option changed' + 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)] + if section_lines != new_section_lines: + changed = True + msg = 'option changed' + section_lines = new_section_lines else: - if within_section and option: - if state == 'present': - # change the existing option line - if match_opt(option, line): - if value is None and allow_no_value: - newline = u'%s\n' % option - else: - newline = assignment_format % (option, value) - option_changed = ini_lines[index] != newline - changed = changed or option_changed - if option_changed: - msg = 'option changed' - ini_lines[index] = newline - if option_changed: - # remove all possible option occurrences from the rest of the section - index = index + 1 - while index < len(ini_lines): - line = ini_lines[index] - if line.startswith(u'['): - break - if match_active_opt(option, line): - del ini_lines[index] - else: - index = index + 1 - break - elif state == 'absent': - # delete the existing line - if match_active_opt(option, line): - del ini_lines[index] - changed = True - msg = 'option changed' - break + # drop the entire section + section_lines = [] + msg = 'section removed' + changed = True + + # reassemble the ini_lines after manipulation + ini_lines = before + section_lines + after # remove the fake section line del ini_lines[0] @@ -261,9 +381,10 @@ def do_ini(module, filename, section=None, option=None, value=None, if not within_section and state == 'present': ini_lines.append(u'[%s]\n' % section) msg = 'section and option added' - if option and value is not None: - ini_lines.append(assignment_format % (option, value)) - elif option and value is None and allow_no_value: + if option and values: + for value in values: + ini_lines.append(assignment_format % (option, value)) + elif option and not values and allow_no_value: ini_lines.append(u'%s\n' % option) else: msg = 'only section added' @@ -303,12 +424,17 @@ def main(): section=dict(type='str', required=True), option=dict(type='str'), value=dict(type='str'), + values=dict(type='list', elements='str'), backup=dict(type='bool', default=False), state=dict(type='str', default='present', choices=['absent', 'present']), + exclusive=dict(type='bool', default=True), no_extra_spaces=dict(type='bool', default=False), allow_no_value=dict(type='bool', default=False), create=dict(type='bool', default=True) ), + mutually_exclusive=[ + ['value', 'values'] + ], add_file_common_args=True, supports_check_mode=True, ) @@ -317,16 +443,23 @@ def main(): section = module.params['section'] option = module.params['option'] value = module.params['value'] + values = module.params['values'] state = module.params['state'] + exclusive = module.params['exclusive'] backup = module.params['backup'] no_extra_spaces = module.params['no_extra_spaces'] allow_no_value = module.params['allow_no_value'] create = module.params['create'] - if state == 'present' and not allow_no_value and value is None: - module.fail_json("Parameter 'value' must be defined if state=present and allow_no_value=False") + if state == 'present' and not allow_no_value and value is None and not values: + module.fail_json(msg="Parameter 'value(s)' must be defined if state=present and allow_no_value=False.") - (changed, backup_file, diff, msg) = do_ini(module, path, section, option, value, state, backup, no_extra_spaces, create, allow_no_value) + if value is not None: + values = [value] + elif values is None: + values = [] + + (changed, backup_file, diff, msg) = do_ini(module, path, section, option, values, state, exclusive, backup, no_extra_spaces, create, allow_no_value) 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 96c6771b9e..b3a1c85531 100644 --- a/tests/integration/targets/ini_file/tasks/main.yml +++ b/tests/integration/targets/ini_file/tasks/main.yml @@ -23,545 +23,31 @@ # along with Ansible. If not, see . - name: record the output directory - set_fact: output_file={{ remote_tmp_dir }}/foo.ini - -- name: add "fav=lemonade" is in section "[drinks]" in specified file - ini_file: - path: "{{ output_file }}" - section: drinks - option: fav - value: lemonade - register: result1 - -- name: verify ini_file 'changed' is true - assert: - that: - - result1 is changed - - result1.msg == 'section and option added' - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content set_fact: - expected1: | + output_file: "{{ remote_tmp_dir }}/foo.ini" + non_existing_file: "{{ remote_tmp_dir }}/bar.ini" - [drinks] - fav = lemonade - content1: "{{ output_content.content | b64decode }}" - -- name: Verify content of ini file is as expected - assert: - that: - - content1 == expected1 - -- name: add "fav=lemonade" is in section "[drinks]" again - ini_file: - path: "{{ output_file }}" - section: drinks - option: fav - value: lemonade - register: result2 - -- name: Ensure unchanged - assert: - that: - - result2 is not changed - - result2.msg == 'OK' - -- name: Ensure "beverage=coke" is in section "[drinks]" - ini_file: - path: "{{ output_file }}" - section: drinks - option: beverage - value: coke - register: result3 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - expected3: | - - [drinks] - fav = lemonade - beverage = coke - content3: "{{ output_content.content | b64decode }}" - -- name: assert 'changed' is true and content is OK - assert: - that: - - result3 is changed - - result3.msg == 'option added' - - content3 == expected3 - -- name: Remove option "beverage=coke" - ini_file: - path: "{{ output_file }}" - section: drinks - option: beverage - state: absent - register: result4 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: get ini file content - set_fact: - content4: "{{ output_content.content | b64decode }}" - -- name: assert changed and content is as expected - assert: - that: - - result4 is changed - - result4.msg == 'option changed' - - content4 == expected1 - -- name: remove section 'drinks' - ini_file: - path: "{{ output_file }}" - section: drinks - state: absent - register: result5 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: get current ini file content - set_fact: - content5: "{{ output_content.content | b64decode }}" - -- name: assert changed and content is empty - assert: - that: - - result5 is changed - - result5.msg == 'section removed' - - content5 == "\n" - -# allow_no_value - -- name: test allow_no_value - ini_file: - path: "{{ output_file }}" - section: mysqld - option: skip-name - allow_no_value: yes - register: result6 - -- name: assert section and option added - assert: - that: - - result6 is changed - - result6.msg == 'section and option added' - -- name: test allow_no_value idempotency - ini_file: - path: "{{ output_file }}" - section: mysqld - option: skip-name - allow_no_value: yes - register: result6 - -- name: assert 'changed' false - assert: - that: - - result6 is not changed - - result6.msg == 'OK' - -- name: test create empty section - ini_file: - path: "{{ output_file }}" - section: new_empty_section - allow_no_value: yes - register: result6a - -- name: assert section added - assert: - that: - - result6a is changed - - result6a.msg == 'only section added' - -- name: test create empty section idempotency - ini_file: - path: "{{ output_file }}" - section: new_empty_section - allow_no_value: yes - register: result6a - -- name: assert 'changed' false - assert: - that: - - result6a is not changed - - result6a.msg == 'OK' - -- name: test remove empty section - ini_file: - state: absent - path: "{{ output_file }}" - section: new_empty_section - allow_no_value: yes - -- name: test allow_no_value with loop - ini_file: - path: "{{ output_file }}" - section: mysqld - option: "{{ item.o }}" - value: "{{ item.v | d(omit) }}" - allow_no_value: yes - with_items: - - { o: "skip-name-resolve" } - - { o: "max_connections", v: "500" } - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - content7: "{{ output_content.content | b64decode }}" - expected7: | - - [mysqld] - skip-name - skip-name-resolve - max_connections = 500 - -- name: Verify content of ini file is as expected - assert: - that: - - content7 == expected7 - -- name: change option with no value to option with value - ini_file: - path: "{{ output_file }}" - section: mysqld - option: skip-name - value: myvalue - register: result8 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - content8: "{{ output_content.content | b64decode }}" - expected8: | - - [mysqld] - skip-name = myvalue - skip-name-resolve - max_connections = 500 - -- name: assert 'changed' and msg 'option changed' and content is as expected - assert: - that: - - result8 is changed - - result8.msg == 'option changed' - - content8 == expected8 - -- name: change option with value to option with no value - ini_file: - path: "{{ output_file }}" - section: mysqld - option: skip-name - allow_no_value: yes - register: result9 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - content9: "{{ output_content.content | b64decode }}" - expected9: | - - [mysqld] - skip-name - skip-name-resolve - max_connections = 500 - -- name: assert 'changed' and msg 'option changed' and content is as expected - assert: - that: - - result9 is changed - - result9.msg == 'option changed' - - content9 == expected9 - -- name: Remove option with no value - ini_file: - path: "{{ output_file }}" - section: mysqld - option: skip-name-resolve - state: absent - register: result10 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - content10: "{{ output_content.content | b64decode }}" - expected10: | - - [mysqld] - skip-name - max_connections = 500 - -- name: assert 'changed' and msg 'option changed' and content is as expected - assert: - that: - - result10 is changed - - result10.msg == 'option changed' - - content10 == expected10 - -- name: Clean test file - copy: - content: "" - dest: "{{ output_file }}" - force: yes - -- name: Ensure "beverage=coke" is created within no section - ini_file: - section: - path: "{{ output_file }}" - option: beverage - value: coke - register: result11 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - expected11: "beverage = coke\n\n" - content11: "{{ output_content.content | b64decode }}" - -- name: assert 'changed' is true and content is OK (no section) - assert: - that: - - result11 is changed - - result11.msg == 'option added' - - content11 == expected11 - -- name: Ensure "beverage=coke" is modified as "beverage=water" within no section - ini_file: - path: "{{ output_file }}" - option: beverage - value: water - section: - register: result12 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - expected12: "beverage = water\n\n" - - content12: "{{ output_content.content | b64decode }}" - -- name: assert 'changed' is true and content is OK (no section) - assert: - that: - - result12 is changed - - result12.msg == 'option changed' - - content12 == expected12 - -- name: remove option 'beverage' within no section - ini_file: - section: - path: "{{ output_file }}" - option: beverage - state: absent - register: result13 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: get current ini file content - set_fact: - content13: "{{ output_content.content | b64decode }}" - -- name: assert changed (no section) - assert: - that: - - result13 is changed - - result13.msg == 'option changed' - - content13 == "\n" - -- name: Check add option without section before existing section +- name: include tasks block: - - name: Add option with section - ini_file: + + - name: include tasks to perform basic tests + include_tasks: tests/00-basic.yml + + - name: reset output file + file: path: "{{ output_file }}" - section: drinks - option: beverage - value: water - - name: Add option without section - ini_file: - path: "{{ output_file }}" - section: - option: like - value: tea - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - expected14: | - like = tea - - [drinks] - beverage = water - content14: "{{ output_content.content | b64decode }}" - -- name: Verify content of ini file is as expected - assert: - that: - - content14 == expected14 - -- name: Check add option with empty string value - block: - - name: Remove drinks - ini_file: - path: "{{ output_file }}" - section: drinks - state: absent - - name: Remove tea - ini_file: - path: "{{ output_file }}" - section: - option: like - value: tea state: absent - # See https://github.com/ansible-collections/community.general/issues/3031 - - name: Tests with empty strings - ini_file: + - name: include tasks to perform tests with parameter "value" + include_tasks: tests/01-value.yml + + - name: reset output file + file: path: "{{ output_file }}" - section: "{{ item.section | d('extensions') }}" - option: "{{ item.option }}" - value: "" - allow_no_value: "{{ item.no_value | d(omit) }}" - loop: - - option: evolve - - option: regress - - section: foobar - option: foo - no_value: true - - option: improve - no_value: true + state: absent -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content + - name: include tasks to perform tests with parameter "values" + include_tasks: tests/02-values.yml -- name: set expected content and get current ini file content - set_fact: - expected15: "\n[extensions]\nevolve = \nregress = \nimprove = \n[foobar]\nfoo = \n" - content15: "{{ output_content.content | b64decode }}" -- debug: var=content15 -- name: Verify content of ini file is as expected - assert: - that: - - content15 == expected15 - -- name: Create starting ini file - copy: - # The content below is the following text file with BOM: - # [section1] - # var1=aaa - # var2=bbb - # [section2] - # var3=ccc - content: !!binary | - 77u/W3NlY3Rpb24xXQp2YXIxPWFhYQp2YXIyPWJiYgpbc2VjdGlvbjJdCnZhcjM9Y2NjCg== - dest: "{{ output_file }}" -- name: Test ini breakage - ini_file: - path: "{{ output_file }}" - section: section1 - option: var4 - value: 0 - -- name: read content from output file - slurp: - src: "{{ output_file }}" - register: output_content - -- name: set expected content and get current ini file content - set_fact: - expected16: "[section1]\nvar1=aaa\nvar2=bbb\nvar4 = 0\n[section2]\nvar3=ccc\n" - content16: "{{ output_content.content | b64decode }}" -- debug: - var: content16 -- name: Verify content of ini file is as expected - assert: - that: - - content16 == expected16 - -# Regression test for https://github.com/ansible-collections/community.general/pull/2578#issuecomment-868092282 -- name: Create UTF-8 test file - copy: - content: !!binary | - W2FwcDptYWluXQphdmFpbGFibGVfbGFuZ3VhZ2VzID0gZW4gZnIgZXMgZGUgcHQgamEgbHQgemhf - VFcgaWQgZGEgcHRfQlIgcnUgc2wgaXQgbmxfTkwgdWsgdGEgc2kgY3MgbmIgaHUKIyBGdWxsIGxh - bmd1YWdlIG5hbWVzIGluIG5hdGl2ZSBsYW5ndWFnZSAoY29tbWEgc2VwYXJhdGVkKQphdmFpbGFi - bGVfbGFuZ3VhZ2VzX2Z1bGwgPSBFbmdsaXNoLCBGcmFuw6dhaXMsIEVzcGHDsW9sLCBEZXV0c2No - LCBQb3J0dWd1w6pzLCDml6XmnKzoqp4sIExpZXR1dm9zLCDkuK3mlocsIEluZG9uZXNpYSwgRGFu - c2ssIFBvcnR1Z3XDqnMgKEJyYXNpbCksINCg0YPRgdGB0LrQuNC5LCBTbG92ZW7FocSNaW5hLCBJ - dGFsaWFubywgTmVkZXJsYW5kcywg0KPQutGA0LDRl9C90YHRjNC60LAsIOCupOCuruCuv+CutOCv - jSwg4LeD4LeS4LaC4LeE4La9LCDEjGVza3ksIEJva23DpWwsIE1hZ3lhcgo= - dest: '{{ output_file }}' -- name: Add entries - ini_file: - section: "{{ item.section }}" - option: "{{ item.option }}" - value: "{{ item.value }}" - path: '{{ output_file }}' - create: true - loop: - - section: app:main - option: sqlalchemy.url - value: postgresql://app:secret@database/app - - section: handler_filelog - option: args - value: (sys.stderr,) - - section: handler_filelog - option: class - value: StreamHandler - - section: handler_exc_handler - option: args - value: (sys.stderr,) - - section: båz - option: fföø - value: ḃâŗ - - section: båz - option: fföø - value: bar + - name: include tasks to test regressions + include_tasks: tests/03-encoding.yml diff --git a/tests/integration/targets/ini_file/tasks/tests/00-basic.yml b/tests/integration/targets/ini_file/tasks/tests/00-basic.yml new file mode 100644 index 0000000000..8f8d345f7e --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/00-basic.yml @@ -0,0 +1,38 @@ +--- +## basiscs + +- name: test-basic 1 - specify both "value" and "values" and fail + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + value: lemonade + values: + - coke + - sprite + register: result_basic_1 + ignore_errors: true + +- name: test-basic 1 - verify error message + assert: + that: + - result_basic_1 is not changed + - result_basic_1 is failed + - "result_basic_1.msg == 'parameters are mutually exclusive: value|values'" + + +- name: test-basic 2 - set "create=no" on non-existing file and fail + ini_file: + path: "{{ non_existing_file }}" + section: food + create: false + value: banana + register: result_basic_2 + ignore_errors: true + +- name: test-basic 2 - verify error message + assert: + that: + - result_basic_2 is not changed + - result_basic_2 is failed + - result_basic_2.msg == "Destination {{ non_existing_file }} does not exist!" diff --git a/tests/integration/targets/ini_file/tasks/tests/01-value.yml b/tests/integration/targets/ini_file/tasks/tests/01-value.yml new file mode 100644 index 0000000000..93499cc63d --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/01-value.yml @@ -0,0 +1,589 @@ +--- + +## testing value + +- name: test-value 1 - set "state=present" and "value=null" and "allow_no_value=false" and fail + ini_file: + path: "{{ output_file }}" + section: cars + option: audi + value: null + allow_no_value: false + register: result_value_1 + ignore_errors: true + +- name: test-value 1 - verify error message + assert: + that: + - result_value_1 is not changed + - result_value_1 is failed + - result_value_1.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False." + + +- name: test-value 2 - set "state=present" and omit "value" and "allow_no_value=false" and fail + ini_file: + path: "{{ output_file }}" + section: cars + option: audi + allow_no_value: false + register: result_value_2 + ignore_errors: true + +- name: test-value 2 - verify error message + assert: + that: + - result_value_2 is not changed + - result_value_2 is failed + - result_value_2.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False." + + +- name: test-value 3 - add "fav=lemonade" in section "[drinks]" in specified file + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + value: lemonade + register: result3 + +- name: test-value 3 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 3 - set expected content and get current ini file content + set_fact: + expected3: | + + [drinks] + fav = lemonade + content3: "{{ output_content.content | b64decode }}" + +- name: test-value 3 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result3 is changed + - result3.msg == 'section and option added' + - content3 == expected3 + + +- name: test-value 4 - add "fav=lemonade" is in section "[drinks]" again + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + value: lemonade + register: result4 + +- name: test-value 4 - Ensure unchanged + assert: + that: + - result4 is not changed + - result4.msg == 'OK' + + +- name: test-value 5 - Ensure "beverage=coke" is in section "[drinks]" + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + value: coke + register: result5 + +- name: test-value 5 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 5 - set expected content and get current ini file content + set_fact: + expected5: | + + [drinks] + fav = lemonade + beverage = coke + content5: "{{ output_content.content | b64decode }}" + +- name: test-value 5 - assert 'changed' is true and content is OK + assert: + that: + - result5 is changed + - result5.msg == 'option added' + - content5 == expected5 + + +- name: test-value 6 - Remove option "beverage=coke" + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + state: absent + register: result6 + +- name: test-value 6 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 6 - set expected content and get current ini file content + set_fact: + expected6: | + + [drinks] + fav = lemonade + content6: "{{ output_content.content | b64decode }}" + +- name: test-value 6 - assert 'changed' is true and content is as expected + assert: + that: + - result6 is changed + - result6.msg == 'option changed' + - content6 == expected6 + + +- name: test-value 7 - remove section 'drinks' + ini_file: + path: "{{ output_file }}" + section: drinks + state: absent + register: result7 + +- name: test-value 7 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 7 - get current ini file content + set_fact: + content7: "{{ output_content.content | b64decode }}" + +- name: test-value 7 - assert 'changed' is true and content is empty + assert: + that: + - result7 is changed + - result7.msg == 'section removed' + - content7 == "\n" + + +# allow_no_value + +- name: test-value 8 - test allow_no_value + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name + allow_no_value: yes + register: result8 + +- name: test-value 8 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 8 - set expected content and get current ini file content + set_fact: + content8: "{{ output_content.content | b64decode }}" + expected8: | + + [mysqld] + skip-name + +- name: test-value 8 - assert 'changed' is true and section and option added + assert: + that: + - result8 is changed + - result8.msg == 'section and option added' + - content8 == expected8 + + +- name: test-value 9 - test allow_no_value idempotency + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name + allow_no_value: yes + register: result9 + +- name: test-value 9 - assert 'changed' is false + assert: + that: + - result9 is not changed + - result9.msg == 'OK' + + +- name: test-value 10 - test create empty section + ini_file: + path: "{{ output_file }}" + section: new_empty_section + allow_no_value: yes + register: result10 + +- name: test-value 10 - assert 'changed' is true and section added + assert: + that: + - result10 is changed + - result10.msg == 'only section added' + + +- name: test-value 11 - test create empty section idempotency + ini_file: + path: "{{ output_file }}" + section: new_empty_section + allow_no_value: yes + register: result11 + +- name: test-value 11 - assert 'changed' is false + assert: + that: + - result11 is not changed + - result11.msg == 'OK' + + +- name: test-value 12 - test remove empty section + ini_file: + state: absent + path: "{{ output_file }}" + section: new_empty_section + allow_no_value: yes + +- name: test-value 12 - test allow_no_value with loop + ini_file: + path: "{{ output_file }}" + section: mysqld + option: "{{ item.o }}" + value: "{{ item.v | d(omit) }}" + allow_no_value: yes + loop: + - { o: "skip-name-resolve" } + - { o: "max_connections", v: "500" } + +- name: test-value 12 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 12 - set expected content and get current ini file content + set_fact: + content12: "{{ output_content.content | b64decode }}" + expected12: | + + [mysqld] + skip-name + skip-name-resolve + max_connections = 500 + +- name: test-value 12 - Verify content of ini file is as expected + assert: + that: + - content12 == expected12 + + +- name: test-value 13 - change option with no value to option with value + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name + value: myvalue + register: result13 + +- name: test-value 13 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 13 - set expected content and get current ini file content + set_fact: + content13: "{{ output_content.content | b64decode }}" + expected13: | + + [mysqld] + skip-name = myvalue + skip-name-resolve + max_connections = 500 + +- name: test-value 13 - assert 'changed' and msg 'option changed' and content is as expected + assert: + that: + - result13 is changed + - result13.msg == 'option changed' + - content13 == expected13 + + +- name: test-value 14 - change option with value to option with no value + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name + allow_no_value: yes + register: result14 + +- name: test-value 14 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 14 - set expected content and get current ini file content + set_fact: + content14: "{{ output_content.content | b64decode }}" + expected14: | + + [mysqld] + skip-name + skip-name-resolve + max_connections = 500 + +- name: test-value 14 - assert 'changed' is true and msg 'option changed' and content is as expected + assert: + that: + - result14 is changed + - result14.msg == 'option changed' + - content14 == expected14 + + +- name: test-value 15 - Remove option with no value + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name-resolve + state: absent + register: result15 + +- name: test-value 15 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 15 - set expected content and get current ini file content + set_fact: + content15: "{{ output_content.content | b64decode }}" + expected15: | + + [mysqld] + skip-name + max_connections = 500 + +- name: test-value 15 - assert 'changed' is true and msg 'option changed' and content is as expected + assert: + that: + - result15 is changed + - result15.msg == 'option changed' + - content15 == expected15 + + +- name: test-value 16 - Clean test file + copy: + content: "" + dest: "{{ output_file }}" + force: yes + +- name: test-value 16 - Ensure "beverage=coke" is created within no section + ini_file: + section: + path: "{{ output_file }}" + option: beverage + value: coke + register: result16 + +- name: test-value 16 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 16 - set expected content and get current ini file content + set_fact: + expected16: |+ + beverage = coke + + content16: "{{ output_content.content | b64decode }}" + +- name: test-value 16 - assert 'changed' is true and content is OK (no section) + assert: + that: + - result16 is changed + - result16.msg == 'option added' + - content16 == expected16 + + +- name: test-value 17 - Ensure "beverage=coke" is modified as "beverage=water" within no section + ini_file: + path: "{{ output_file }}" + option: beverage + value: water + section: + register: result17 + +- name: test-value 17 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 17 - set expected content and get current ini file content + set_fact: + expected17: |+ + beverage = water + + content17: "{{ output_content.content | b64decode }}" + +- name: test-value 17 - assert 'changed' is true and content is OK (no section) + assert: + that: + - result17 is changed + - result17.msg == 'option changed' + - content17 == expected17 + + +- name: test-value 18 - remove option 'beverage' within no section + ini_file: + section: + path: "{{ output_file }}" + option: beverage + state: absent + register: result18 + +- name: test-value 18 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 18 - get current ini file content + set_fact: + content18: "{{ output_content.content | b64decode }}" + +- name: test-value 18 - assert 'changed' is true and option is removed (no section) + assert: + that: + - result18 is changed + - result18.msg == 'option changed' + - content18 == "\n" + + +- name: test-value 19 - Check add option without section before existing section + block: + - name: test-value 19 - Add option with section + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + value: water + - name: test-value 19 - Add option without section + ini_file: + path: "{{ output_file }}" + section: + option: like + value: tea + +- name: test-value 19 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 19 - set expected content and get current ini file content + set_fact: + expected19: | + like = tea + + [drinks] + beverage = water + content19: "{{ output_content.content | b64decode }}" + +- name: test-value 19 - Verify content of ini file is as expected + assert: + that: + - content19 == expected19 + + +- name: test-value 20 - Check add option with empty string value + block: + - name: test-value 20 - Remove drinks + ini_file: + path: "{{ output_file }}" + section: drinks + state: absent + - name: test-value 20 - Remove tea + ini_file: + path: "{{ output_file }}" + section: + option: like + value: tea + state: absent + # See https://github.com/ansible-collections/community.general/issues/3031 + - name: test-value 20 - Tests with empty strings + ini_file: + path: "{{ output_file }}" + section: "{{ item.section | d('extensions') }}" + option: "{{ item.option }}" + value: "" + allow_no_value: "{{ item.no_value | d(omit) }}" + loop: + - option: evolve + - option: regress + - section: foobar + option: foo + no_value: true + - option: improve + no_value: true + +- name: test-value 20 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 20 - set expected content and get current ini file content + set_fact: + expected20: |+ + + [extensions] + evolve = + regress = + improve = + [foobar] + foo = + content20: "{{ output_content.content | b64decode }}" + +- name: test-value 20 - Verify content of ini file is as expected + assert: + that: + - content20 == expected20 + + +- name: test-value 21 - Create starting ini file + copy: + # The content below is the following text file with BOM: + # [section1] + # var1=aaa + # var2=bbb + # [section2] + # var3=ccc + content: !!binary | + 77u/W3NlY3Rpb24xXQp2YXIxPWFhYQp2YXIyPWJiYgpbc2VjdGlvbjJdCnZhcjM9Y2NjCg== + dest: "{{ output_file }}" + +- name: test-value 21 - Test ini breakage + ini_file: + path: "{{ output_file }}" + section: section1 + option: var4 + value: 0 + register: result21 + +- name: test-value 21 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 21 - set expected content and get current ini file content + set_fact: + expected21: | + [section1] + var1=aaa + var2=bbb + var4 = 0 + [section2] + var3=ccc + content21: "{{ output_content.content | b64decode }}" + +- name: test-value 21 - Verify content of ini file is as expected + assert: + that: + - result21 is changed + - result21.msg == 'option added' + - content21 == expected21 diff --git a/tests/integration/targets/ini_file/tasks/tests/02-values.yml b/tests/integration/targets/ini_file/tasks/tests/02-values.yml new file mode 100644 index 0000000000..c3ef6b61a6 --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/02-values.yml @@ -0,0 +1,1013 @@ +--- + +## testing values + +- name: "test-values 1 - set 'state=present' and 'values=[]' and 'allow_no_value=false' and fail" + ini_file: + path: "{{ output_file }}" + section: cars + option: audi + values: [] + allow_no_value: false + register: result1 + ignore_errors: true + +- name: test-values 1 - verify error message + assert: + that: + - result1 is not changed + - result1 is failed + - result1.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False." + + +- name: "test-values 2 - set 'state=present' and omit 'values' and 'allow_no_value=false' and fail" + ini_file: + path: "{{ output_file }}" + section: cars + option: audi + allow_no_value: false + register: result2 + ignore_errors: true + +- name: test-values 2 - verify error message + assert: + that: + - result2 is not changed + - result2 is failed + - result2.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False." + + +- name: "test-values 3 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + - cocktail + state: present + register: result3 + +- name: test-values 3 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 3 - set expected content and get current ini file content + set_fact: + expected3: | + + [drinks] + fav = lemonade + fav = cocktail + content3: "{{ output_content.content | b64decode }}" + +- name: test-values 3 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result3 is changed + - result3.msg == 'section and option added' + - content3 == expected3 + + +- name: "test-values 4 - remove option 'fav=lemonade' from section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: absent + exclusive: false + register: result4 + +- name: test-values 4 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 4 - set expected content and get current ini file content + set_fact: + expected4: | + + [drinks] + fav = cocktail + content4: "{{ output_content.content | b64decode }}" + +- name: test-values 4 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result4 is changed + - result4.msg == 'option changed' + - content4 == expected4 + + +- name: "test-values 5 - add option 'fav=lemonade' in section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: present + exclusive: false + register: result5 + +- name: test-values 5 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 5 - set expected content and get current ini file content + set_fact: + expected5: | + + [drinks] + fav = cocktail + fav = lemonade + content5: "{{ output_content.content | b64decode }}" + +- name: test-values 5 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result5 is changed + - result5.msg == 'option added' + - content5 == expected5 + + +- name: "test-values 6 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' and check for idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + - cocktail + state: present + register: result6 + +- name: test-values 6 - Ensure unchanged + assert: + that: + - result6 is not changed + - result6.msg == 'OK' + + +- name: "test-values 7 - ensure 'fav=cocktail' and 'fav=lemonade' (list reverse order) is 'present' in section '[drinks]' and check for idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + - lemonade + state: present + register: result7 + +- name: test-values 7 - Ensure unchanged + assert: + that: + - result7 is not changed + - result7.msg == 'OK' + + +- name: "test-values 8 - add option 'fav=lemonade' in section '[drinks]' again and ensure idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: present + exclusive: false + register: result8 + +- name: test-values 8 - Ensure unchanged + assert: + that: + - result8 is not changed + - result8.msg == 'OK' + + +- name: "test-values 9 - ensure only 'fav=lemonade' is 'present' in section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: present + register: result9 + +- name: test-values 9 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 9 - set expected content and get current ini file content + set_fact: + expected9: | + + [drinks] + fav = lemonade + content9: "{{ output_content.content | b64decode }}" + +- name: test-values 9 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result9 is changed + - result9.msg == 'option changed' + - content9 == expected9 + + +- name: "test-values 10 - remove non-existent 'fav=cocktail' from section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + state: absent + register: result10 + +- name: test-values 10 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 10 - set expected content and get current ini file content + set_fact: + expected10: | + + [drinks] + content10: "{{ output_content.content | b64decode }}" + + +- name: test-values 10 - Ensure unchanged + assert: + that: + - result10 is changed + - result10.msg == 'option changed' + - content10 == expected10 + + +- name: "test-values 11 - Ensure 'fav=lemonade' and 'beverage=coke' is 'present' in section '[drinks]'" + block: + - name: "test-values 11 - resetting ini_fie: Ensure 'fav=lemonade' is 'present' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: present + - name: "test-values 11 - Ensure 'beverage=coke' is 'present' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + values: + - coke + state: present + register: result11 + +- name: test-values 11 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 11 - set expected content and get current ini file content + set_fact: + expected11: | + + [drinks] + fav = lemonade + beverage = coke + content11: "{{ output_content.content | b64decode }}" + +- name: test-values 11 - assert 'changed' is true and content is OK + assert: + that: + - result11 is changed + - result11.msg == 'option added' + - content11 == expected11 + + +- name: "test-values 12 - add option 'fav=lemonade' in section '[drinks]' again and ensure idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + state: present + exclusive: false + register: result12 + +- name: test-values 12 - Ensure unchanged + assert: + that: + - result12 is not changed + - result12.msg == 'OK' + + +- name: "test-values 13 - add option 'fav=cocktail' in section '[drinks]' in specified file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + state: present + exclusive: false + register: result13 + +- name: test-values 13 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 13 - set expected content and get current ini file content + set_fact: + expected13: | + + [drinks] + fav = lemonade + beverage = coke + fav = cocktail + content13: "{{ output_content.content | b64decode }}" + +- name: test-values 13 - Verify content of ini file is as expected and ini_file 'changed' is true + assert: + that: + - result13 is changed + - result13.msg == 'option added' + - content13 == expected13 + + +- name: "test-values 14 - Ensure 'refreshment=[water, juice, soft drink]' is 'present' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: refreshment + values: + - water + - juice + - soft drink + state: present + register: result14 + +- name: test-values 14 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 14 - set expected content and get current ini file content + set_fact: + expected14: | + + [drinks] + fav = lemonade + beverage = coke + fav = cocktail + refreshment = water + refreshment = juice + refreshment = soft drink + content14: "{{ output_content.content | b64decode }}" + +- name: test-values 14 - assert 'changed' is true and content is OK + assert: + that: + - result14 is changed + - result14.msg == 'option added' + - content14 == expected14 + + +- name: "test-values 15 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' and check for idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - lemonade + - cocktail + state: present + register: result15 + +- name: test-values 15 - Ensure unchanged + assert: + that: + - result15 is not changed + - result15.msg == 'OK' + + +- name: "test-values 16 - ensure 'fav=cocktail' and 'fav=lemonade' (list reverse order) is 'present' in section '[drinks]' and check for idempotency" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + - lemonade + state: present + register: result16 + +- name: test-values 16 - Ensure unchanged + assert: + that: + - result16 is not changed + - result16.msg == 'OK' + + +- name: "test-values 17 - Ensure option 'refreshment' is 'absent' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: refreshment + state: absent + register: result17 + +- name: test-values 17 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 17 - set expected content and get current ini file content + set_fact: + expected17: | + + [drinks] + fav = lemonade + beverage = coke + fav = cocktail + content17: "{{ output_content.content | b64decode }}" + +- name: test-values 17 - assert 'changed' is true and content is as expected + assert: + that: + - result17 is changed + - result17.msg == 'option changed' + - content17 == expected17 + + +- name: "test-values 18 - Ensure 'beverage=coke' is 'abesent' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + state: absent + register: result18 + +- name: test-values 18 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 18 - set expected content and get current ini file content + set_fact: + expected18: | + + [drinks] + fav = lemonade + fav = cocktail + content18: "{{ output_content.content | b64decode }}" + +- name: test-values 18 - assert 'changed' is true and content is as expected + assert: + that: + - result18 is changed + - result18.msg == 'option changed' + - content18 == expected18 + + +- name: "test-values 19 - Ensure non-existant 'beverage=coke' is 'abesent' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + values: + - coke + state: absent + register: result19 + +- name: test-values 19 - Ensure unchanged + assert: + that: + - result19 is not changed + - result19.msg == 'OK' + + +- name: test-values 20 - remove section 'drinks' + ini_file: + path: "{{ output_file }}" + section: drinks + state: absent + register: result20 + +- name: test-values 20 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 20 - get current ini file content + set_fact: + content20: "{{ output_content.content | b64decode }}" + +- name: test-values 20 - assert 'changed' is true and content is empty + assert: + that: + - result20 is changed + - result20.msg == 'section removed' + - content20 == "\n" + + +- name: "test-values 21 - Ensure 'refreshment=[water, juice, soft drink, juice]' (duplicates removed) is 'present' in section '[drinks]'" + ini_file: + path: "{{ output_file }}" + section: drinks + option: refreshment + values: + - water + - juice + - soft drink + - juice + state: present + register: result21 + +- name: test-values 21 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 21 - set expected content and get current ini file content + set_fact: + expected21: | + + [drinks] + refreshment = water + refreshment = juice + refreshment = soft drink + content21: "{{ output_content.content | b64decode }}" + +- name: test-values 21 - assert 'changed' is true and content is OK + assert: + that: + - result21 is changed + - result21.msg == 'section and option added' + - content21 == expected21 + + +- name: test-values 22 - Create starting ini file + copy: + content: | + + # Some comment to test + [mysqld] + connect_timeout = 300 + max_connections = 1000 + [section1] + var1 = aaa + # comment in section + var2 = foo + # var2 = bar + + [section2] + var3 = ccc + # comment after section + dest: "{{ output_file }}" + +- name: "test-values 22 - Ensure 'skip-name' with 'allow_no_value' is 'present' in section '[mysqld]' test allow_no_value" + ini_file: + path: "{{ output_file }}" + section: mysqld + option: skip-name + allow_no_value: true + state: present + register: result22 + +- name: test-values 22 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 22 - set expected content and get current ini file content + set_fact: + expected22: | + + # Some comment to test + [mysqld] + connect_timeout = 300 + max_connections = 1000 + skip-name + [section1] + var1 = aaa + # comment in section + var2 = foo + # var2 = bar + + [section2] + var3 = ccc + # comment after section + content22: "{{ output_content.content | b64decode }}" + +- name: test-values 22 - assert 'changed' is true and content is OK and option added + assert: + that: + - result22 is changed + - result22.msg == 'option added' + - content22 == expected22 + + +- name: "test-values 23 - Ensure 'var2=[foo, foobar]' is 'present' in section '[section1]'" + ini_file: + path: "{{ output_file }}" + section: section1 + option: var2 + values: + - foo + - foobar + state: present + register: result23 + +- name: test-values 23 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 23 - set expected content and get current ini file content + set_fact: + content23: "{{ output_content.content | b64decode }}" + expected23: | + + # Some comment to test + [mysqld] + connect_timeout = 300 + max_connections = 1000 + skip-name + [section1] + var1 = aaa + # comment in section + var2 = foo + var2 = foobar + # var2 = bar + + [section2] + var3 = ccc + # comment after section +- name: test-values 23 - assert 'changed' and msg 'option added' and content is as expected + assert: + that: + - result23 is changed + - result23.msg == 'option added' + - content23 == expected23 + + +- name: "test-values 24 - Ensure 'var2=[foo, foobar, bar]' is 'present' in section '[section1]' replacing commented option 'var2=bar'" + ini_file: + path: "{{ output_file }}" + section: section1 + option: var2 + values: + - foo + - bar + - foobar + state: present + register: result24 + +- name: test-values 24 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 24 - set expected content and get current ini file content + set_fact: + content24: "{{ output_content.content | b64decode }}" + expected24: | + + # Some comment to test + [mysqld] + connect_timeout = 300 + max_connections = 1000 + skip-name + [section1] + var1 = aaa + # comment in section + var2 = foo + var2 = foobar + var2 = bar + + [section2] + var3 = ccc + # comment after section +- name: test-values 24 - assert 'added' and msg 'option changed' and content is as expected + assert: + that: + - result24 is changed + - result24.msg == 'option changed' + - content24 == expected24 + + +- name: test-values 25 - Clean test file + copy: + content: "" + dest: "{{ output_file }}" + force: yes + +- name: "test-values 25 - Ensure 'beverage=[coke, pepsi]' is created within no section" + ini_file: + section: + path: "{{ output_file }}" + option: beverage + values: + - coke + - pepsi + state: present + register: result25 + +- name: test-values 25 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 25 - set expected content and get current ini file content + set_fact: + expected25: |+ + beverage = coke + beverage = pepsi + + content25: "{{ output_content.content | b64decode }}" + +- name: test-values 25 - assert 'changed' is true and content is OK (no section) + assert: + that: + - result25 is changed + - result25.msg == 'option added' + - content25 == expected25 + + +- name: "test-values 26 - Ensure 'beverage=coke' and 'beverage=pepsi' are modified within no section" + ini_file: + path: "{{ output_file }}" + option: beverage + values: + - water + - orange juice + section: + state: present + exclusive: true + register: result26 + +- name: test-values 26 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 26 - set expected content and get current ini file content + set_fact: + expected26: |+ + beverage = water + beverage = orange juice + + content26: "{{ output_content.content | b64decode }}" + +- name: test-values 26 - assert 'changed' is true and content is OK (no section) + assert: + that: + - result26 is changed + - result26.msg == 'option changed' + - content26 == expected26 + + +- name: "test-values 27 - ensure option 'beverage' is 'absent' within no section" + ini_file: + section: + path: "{{ output_file }}" + option: beverage + state: absent + register: result27 + +- name: test-values 27 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 27 - get current ini file content + set_fact: + content27: "{{ output_content.content | b64decode }}" + +- name: test-values 27 - assert changed (no section) + assert: + that: + - result27 is changed + - result27.msg == 'option changed' + - content27 == "\n" + + +- name: "test-values 28 - Ensure option 'present' without section before existing section" + block: + - name: test-values 28 - ensure option present within section + ini_file: + path: "{{ output_file }}" + section: drinks + option: beverage + values: + - water + - orange juice + state: present + + - name: test-values 28 - ensure option present without section + ini_file: + path: "{{ output_file }}" + section: + option: like + values: + - tea + - coffee + state: present + +- name: test-values 28 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-values 28 - set expected content and get current ini file content + set_fact: + expected28: | + like = tea + like = coffee + + [drinks] + beverage = water + beverage = orange juice + content28: "{{ output_content.content | b64decode }}" + +- name: test-values 28 - Verify content of ini file is as expected + assert: + that: + - content28 == expected28 + + +- name: test-value 29 - Create starting ini file + copy: + content: | + [drinks] + fav = cocktail + beverage = water + fav = lemonade + beverage = orange juice + dest: "{{ output_file }}" + +- name: "test-value 29 - Test 'state=absent' with 'exclusive=true' with multiple options in ini_file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + state: absent + register: result29 + +- name: test-value 29 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 29 - set expected content and get current ini file content + set_fact: + expected29: | + [drinks] + beverage = water + beverage = orange juice + content29: "{{ output_content.content | b64decode }}" + +- name: test-value 29 - Verify content of ini file is as expected + assert: + that: + - result29 is changed + - result29.msg == 'option changed' + - content29 == expected29 + + +- name: test-value 30 - Create starting ini file + copy: + content: | + [drinks] + fav = cocktail + beverage = water + fav = lemonade + beverage = orange juice + dest: "{{ output_file }}" + +- name: "test-value 30 - Test 'state=absent' with 'exclusive=false' with multiple options in ini_file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + values: + - cocktail + state: absent + exclusive: false + register: result30 + +- name: test-value 30 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 30 - set expected content and get current ini file content + set_fact: + expected30: | + [drinks] + beverage = water + fav = lemonade + beverage = orange juice + content30: "{{ output_content.content | b64decode }}" + +- name: test-value 30 - Verify content of ini file is as expected + assert: + that: + - result30 is changed + - result30.msg == 'option changed' + - content30 == expected30 + + +- name: test-value 31 - Create starting ini file + copy: + content: | + [drinks] + fav = cocktail + beverage = water + fav = lemonade + beverage = orange juice + dest: "{{ output_file }}" + +- name: "test-value 31 - Test 'state=absent' with 'exclusive=true' and no value given with multiple options in ini_file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + state: absent + register: result31 + +- name: test-value 31 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 31 - set expected content and get current ini file content + set_fact: + expected31: | + [drinks] + beverage = water + beverage = orange juice + content31: "{{ output_content.content | b64decode }}" + +- name: test-value 31 - Verify content of ini file is as expected + assert: + that: + - result31 is changed + - result31.msg == 'option changed' + - content31 == expected31 + + +- name: test-value 32 - Create starting ini file + copy: + content: | + [drinks] + fav = cocktail + beverage = water + fav = lemonade + beverage = orange juice + dest: "{{ output_file }}" + +- name: "test-value 32 - Test 'state=absent' with 'exclusive=false' and no value given with multiple options in ini_file" + ini_file: + path: "{{ output_file }}" + section: drinks + option: fav + state: absent + exclusive: false + register: result32 + diff: true + +- name: test-value 32 - read content from output file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-value 32 - set expected content and get current ini file content + set_fact: + expected32: | + [drinks] + fav = cocktail + beverage = water + fav = lemonade + beverage = orange juice + content32: "{{ output_content.content | b64decode }}" + +- name: test-value 32 - Verify content of ini file is as expected + assert: + that: + - result32 is not changed + - result32.msg == 'OK' + - content32 == expected32 diff --git a/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml b/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml new file mode 100644 index 0000000000..6280ae1ffb --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml @@ -0,0 +1,41 @@ +--- + +# Regression test for https://github.com/ansible-collections/community.general/pull/2578#issuecomment-868092282 +- name: Create UTF-8 test file + copy: + content: !!binary | + W2FwcDptYWluXQphdmFpbGFibGVfbGFuZ3VhZ2VzID0gZW4gZnIgZXMgZGUgcHQgamEgbHQgemhf + VFcgaWQgZGEgcHRfQlIgcnUgc2wgaXQgbmxfTkwgdWsgdGEgc2kgY3MgbmIgaHUKIyBGdWxsIGxh + bmd1YWdlIG5hbWVzIGluIG5hdGl2ZSBsYW5ndWFnZSAoY29tbWEgc2VwYXJhdGVkKQphdmFpbGFi + bGVfbGFuZ3VhZ2VzX2Z1bGwgPSBFbmdsaXNoLCBGcmFuw6dhaXMsIEVzcGHDsW9sLCBEZXV0c2No + LCBQb3J0dWd1w6pzLCDml6XmnKzoqp4sIExpZXR1dm9zLCDkuK3mlocsIEluZG9uZXNpYSwgRGFu + c2ssIFBvcnR1Z3XDqnMgKEJyYXNpbCksINCg0YPRgdGB0LrQuNC5LCBTbG92ZW7FocSNaW5hLCBJ + dGFsaWFubywgTmVkZXJsYW5kcywg0KPQutGA0LDRl9C90YHRjNC60LAsIOCupOCuruCuv+CutOCv + jSwg4LeD4LeS4LaC4LeE4La9LCDEjGVza3ksIEJva23DpWwsIE1hZ3lhcgo= + dest: '{{ output_file }}' +- name: Add entries + ini_file: + section: "{{ item.section }}" + option: "{{ item.option }}" + value: "{{ item.value }}" + path: '{{ output_file }}' + create: true + loop: + - section: app:main + option: sqlalchemy.url + value: postgresql://app:secret@database/app + - section: handler_filelog + option: args + value: (sys.stderr,) + - section: handler_filelog + option: class + value: StreamHandler + - section: handler_exc_handler + option: args + value: (sys.stderr,) + - section: båz + option: fföø + value: ḃâŗ + - section: båz + option: fföø + value: bar