From 2707ca0732a197095943ed4dcbce2072efeec4ac Mon Sep 17 00:00:00 2001 From: Amos Yuen Date: Fri, 20 Jun 2025 17:04:13 -0700 Subject: [PATCH] Support setting whole section when option is unset Docs sound like you can set a section by not setting option, but it didn't work previously, this fixes that. --- ...ini-file-fix-add-replace-whole-section.yml | 2 + plugins/modules/ini_file.py | 163 +++++++++++------- .../tasks/tests/09-section_without_option.yml | 147 ++++++++++++++++ 3 files changed, 246 insertions(+), 66 deletions(-) create mode 100644 changelogs/fragments/10288-ini-file-fix-add-replace-whole-section.yml create mode 100644 tests/integration/targets/ini_file/tasks/tests/09-section_without_option.yml diff --git a/changelogs/fragments/10288-ini-file-fix-add-replace-whole-section.yml b/changelogs/fragments/10288-ini-file-fix-add-replace-whole-section.yml new file mode 100644 index 0000000000..b21c98c020 --- /dev/null +++ b/changelogs/fragments/10288-ini-file-fix-add-replace-whole-section.yml @@ -0,0 +1,2 @@ +bugfixes: + - "ini_file - fixes adding or replacing a whole section (https://github.com/ansible-collections/community.general/pull/10288)." diff --git a/plugins/modules/ini_file.py b/plugins/modules/ini_file.py index bf8534bf39..074ecb6824 100644 --- a/plugins/modules/ini_file.py +++ b/plugins/modules/ini_file.py @@ -254,6 +254,16 @@ EXAMPLES = r""" value: xxxxxxxxxxxxxxxxxxxx mode: '0600' state: present + +- name: Add or replace whole section + community.general.ini_file: + path: /etc/wireguard/wg0.conf + section: Peer + value: | + AllowedIps = 10.4.0.11/32 + PublicKey = xxxxxxxxxxxxxxxxxxxx + mode: '0600' + state: present """ import io @@ -427,72 +437,84 @@ def do_ini(module, filename, section=None, section_has_values=None, option=None, # 2. edit all the remaining 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_function(option, line): - match = match_function(option, line) - if values and match.group(8) in values: - matched_value = match.group(8) - if not matched_value and allow_no_value: + if state == 'present': + if option: + for index, line in enumerate(section_lines): + if match_function(option, line): + match = match_function(option, line) + if values and match.group(8) in values: + matched_value = match.group(8) + 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(option, changed, section_lines, index, changed_lines, ignore_spaces, 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(option, changed, section_lines, index, changed_lines, ignore_spaces, newline, msg) option_no_value_present = True - else: - # replace existing option=value line(s) - newline = assignment_format % (option, matched_value) - (changed, msg) = update_section_line(option, changed, section_lines, index, changed_lines, ignore_spaces, 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(option, changed, section_lines, index, changed_lines, ignore_spaces, 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_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_function(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 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_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_function( + option, section_lines[index] + ): + del section_lines[index] + del changed_lines[index] + changed = True + msg = 'option changed' + + # 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 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 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 + elif within_section and len(section_lines) > 0 and len(values) > 0: + original = ''.join(section_lines[1:]) + replacement = ''.join(values) + if not replacement.endswith('\n'): + replacement += '\n' + if original != replacement: + section_lines = [section_lines[0], replacement] + msg = 'section replaced' + changed = True if state == 'absent': if option: @@ -539,11 +561,20 @@ def do_ini(module, filename, section=None, section_has_values=None, option=None, for value in condition['values']: if value not in values: values.append(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) + if option: + if values: + for value in values: + ini_lines.append(assignment_format % (option, value)) + elif not values and allow_no_value: + ini_lines.append('%s\n' % option) + else: + msg = 'only section added' + elif len(values) > 0: + replacement = ''.join(values) + if not replacement.endswith('\n'): + replacement += '\n' + ini_lines.append(replacement) + msg = 'section added' else: msg = 'only section added' changed = True diff --git a/tests/integration/targets/ini_file/tasks/tests/09-section_without_option.yml b/tests/integration/targets/ini_file/tasks/tests/09-section_without_option.yml new file mode 100644 index 0000000000..e790b3cd70 --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/09-section_without_option.yml @@ -0,0 +1,147 @@ +--- +# 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 section selection + +- name: test-section 1 - Create starting ini file + copy: + content: | + [drinks] + fav = lemonade + beverage = orange juice + + dest: "{{ output_file }}" + +- name: test-section 1 - Add new block + ini_file: + dest: "{{ output_file }}" + section: food + value: | + fav = hamburger + beverage = None + state: present + register: result1 + +- name: test-section 1 - Read modified file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-section 1 - Create expected result + set_fact: + expected1: | + [drinks] + fav = lemonade + beverage = orange juice + car = volvo + + [food] + fav = hamburger + beverage = None + output1: "{{ output_content.content | b64decode }}" + +- name: test-section 1 - Section was added at end + assert: + that: + - result1 is changed + - result1.msg == 'section added' + - output1 == expected1 + +# ---------------- + +- name: test-section 2 - Create starting ini file + copy: + content: | + [drinks] + fav = lemonade + beverage = orange juice + + [drinks] + fav = lemonade + beverage = pineapple juice + + dest: "{{ output_file }}" + +- name: test-section 2 - Modify starting ini file + ini_file: + dest: "{{ output_file }}" + section: drinks + section_has_values: + - option: beverage + value: pineapple juice + value: | + fav = lemonade + car = volvo + state: present + register: result1 + +- name: test-section 2 - Read modified file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-section 2 - Create expected result + set_fact: + expected1: | + [drinks] + fav = lemonade + beverage = orange juice + + [drinks] + fav = lemonade + car = volvo + output1: "{{ output_content.content | b64decode }}" + +- name: test-section 2 - Second section was replaced with value + assert: + that: + - result1 is changed + - result1.msg == 'section replaced' + - output1 == expected1 + +# ---------------- + +- name: test-section 3 - Create starting ini file + copy: + content: | + [drinks] + fav = lemonade + beverage = orange juice + + [drinks] + fav = lemonade + beverage = pineapple juice + + dest: "{{ output_file }}" + +- name: test-section 3 - Modify starting ini file + ini_file: + dest: "{{ output_file }}" + section: drinks + section_has_values: + - option: beverage + value: pineapple juice + state: absent + register: result1 + +- name: test-section 3 - Read modified file + slurp: + src: "{{ output_file }}" + register: output_content + +- name: test-section 3 - Create expected result + set_fact: + expected1: | + [drinks] + fav = lemonade + beverage = orange juice + output1: "{{ output_content.content | b64decode }}" + +- name: test-section 3 - Section was removed for matching section + assert: + that: + - result1 is changed + - result1.msg == 'section removed' + - output1 == expected1