1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

ini_file - add feature 'section_has_values' (#7505)

* insert new code

* add changelog

* add argument_spec

* sanity check

* docstring version_added

* version-added-must-be-major-or-minor

* Update plugins/modules/ini_file.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* check for default value  `None`

* typo in example

* add integration test and rename option

* add license

* update "version added" in docstring

* insert new code

* remove whitespace

* update examples

* support exclusive, allow_no_value, multiple values in section_has_values

* prefer Todd's variable naming in loops

* resolve number clash in file names

* pass sanity test validate-modules

* Documentation updates

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Todd Lewis <todd_lewis@unc.edu>
This commit is contained in:
Jakob Lund 2024-04-20 12:12:55 +02:00 committed by GitHub
parent 865de5baa0
commit be4d5b7dc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 476 additions and 7 deletions

View file

@ -0,0 +1,5 @@
minor_changes:
- "ini_file - add an optional parameter ``section_has_values``. If the
target ini file contains more than one ``section``, use ``section_has_values``
to specify which one should be updated
(https://github.com/ansible-collections/community.general/pull/7505)."

View file

@ -44,6 +44,30 @@ options:
- If being omitted, the O(option) will be placed before the first O(section). - If being omitted, the O(option) will be placed before the first O(section).
- Omitting O(section) is also required if the config format does not support sections. - Omitting O(section) is also required if the config format does not support sections.
type: str type: str
section_has_values:
type: list
elements: dict
required: false
suboptions:
option:
type: str
description: Matching O(section) must contain this option.
required: true
value:
type: str
description: Matching O(section_has_values[].option) must have this specific value.
values:
description:
- The string value to be associated with an O(section_has_values[].option).
- Mutually exclusive with O(section_has_values[].value).
- O(section_has_values[].value=v) is equivalent to O(section_has_values[].values=[v]).
type: list
elements: str
description:
- Among possibly multiple sections of the same name, select the first one that contains matching options and values.
- With O(state=present), if a suitable section is not found, a new section will be added, including the required options.
- With O(state=absent), at most one O(section) is removed if it contains the values.
version_added: 8.6.0
option: option:
description: description:
- If set (required for changing a O(value)), this is the name of the option. - If set (required for changing a O(value)), this is the name of the option.
@ -182,6 +206,57 @@ EXAMPLES = r'''
option: beverage option: beverage
value: lemon juice value: lemon juice
state: present state: present
- name: Remove the peer configuration for 10.128.0.11/32
community.general.ini_file:
path: /etc/wireguard/wg0.conf
section: Peer
section_has_values:
- option: AllowedIps
value: 10.128.0.11/32
mode: '0600'
state: absent
- name: Add "beverage=lemon juice" outside a section in specified file
community.general.ini_file:
path: /etc/conf
option: beverage
value: lemon juice
state: present
- name: Update the public key for peer 10.128.0.12/32
community.general.ini_file:
path: /etc/wireguard/wg0.conf
section: Peer
section_has_values:
- option: AllowedIps
value: 10.128.0.12/32
option: PublicKey
value: xxxxxxxxxxxxxxxxxxxx
mode: '0600'
state: present
- name: Remove the peer configuration for 10.128.0.11/32
community.general.ini_file:
path: /etc/wireguard/wg0.conf
section: Peer
section_has_values:
- option: AllowedIps
value: 10.4.0.11/32
mode: '0600'
state: absent
- name: Update the public key for peer 10.128.0.12/32
community.general.ini_file:
path: /etc/wireguard/wg0.conf
section: Peer
section_has_values:
- option: AllowedIps
value: 10.4.0.12/32
option: PublicKey
value: xxxxxxxxxxxxxxxxxxxx
mode: '0600'
state: present
''' '''
import io import io
@ -222,7 +297,19 @@ def update_section_line(option, changed, section_lines, index, changed_lines, ig
return (changed, msg) return (changed, msg)
def do_ini(module, filename, section=None, option=None, values=None, def check_section_has_values(section_has_values, section_lines):
if section_has_values is not None:
for condition in section_has_values:
for line in section_lines:
match = match_opt(condition["option"], line)
if match and (len(condition["values"]) == 0 or match.group(7) in condition["values"]):
break
else:
return False
return True
def do_ini(module, filename, section=None, section_has_values=None, option=None, values=None,
state='present', exclusive=True, backup=False, no_extra_spaces=False, state='present', exclusive=True, backup=False, no_extra_spaces=False,
ignore_spaces=False, create=True, allow_no_value=False, modify_inactive_option=True, follow=False): ignore_spaces=False, create=True, allow_no_value=False, modify_inactive_option=True, follow=False):
@ -307,14 +394,22 @@ def do_ini(module, filename, section=None, option=None, values=None,
section_pattern = re.compile(to_text(r'^\[\s*%s\s*]' % re.escape(section.strip()))) section_pattern = re.compile(to_text(r'^\[\s*%s\s*]' % re.escape(section.strip())))
for index, line in enumerate(ini_lines): for index, line in enumerate(ini_lines):
# end of section:
if within_section and line.startswith(u'['):
if check_section_has_values(
section_has_values, ini_lines[section_start:index]
):
section_end = index
break
else:
# look for another section
within_section = False
section_start = section_end = 0
# find start and end of section # find start and end of section
if section_pattern.match(line): if section_pattern.match(line):
within_section = True within_section = True
section_start = index section_start = index
elif line.startswith(u'['):
if within_section:
section_end = index
break
before = ini_lines[0:section_start] before = ini_lines[0:section_start]
section_lines = ini_lines[section_start:section_end] section_lines = ini_lines[section_start:section_end]
@ -435,6 +530,18 @@ def do_ini(module, filename, section=None, option=None, values=None,
if not within_section and state == 'present': if not within_section and state == 'present':
ini_lines.append(u'[%s]\n' % section) ini_lines.append(u'[%s]\n' % section)
msg = 'section and option added' msg = 'section and option added'
if section_has_values:
for condition in section_has_values:
if condition['option'] != option:
if len(condition['values']) > 0:
for value in condition['values']:
ini_lines.append(assignment_format % (condition['option'], value))
elif allow_no_value:
ini_lines.append(u'%s\n' % condition['option'])
elif not exclusive:
for value in condition['values']:
if value not in values:
values.append(value)
if option and values: if option and values:
for value in values: for value in values:
ini_lines.append(assignment_format % (option, value)) ini_lines.append(assignment_format % (option, value))
@ -476,6 +583,11 @@ def main():
argument_spec=dict( argument_spec=dict(
path=dict(type='path', required=True, aliases=['dest']), path=dict(type='path', required=True, aliases=['dest']),
section=dict(type='str'), section=dict(type='str'),
section_has_values=dict(type='list', elements='dict', options=dict(
option=dict(type='str', required=True),
value=dict(type='str'),
values=dict(type='list', elements='str')
), default=None, mutually_exclusive=[['value', 'values']]),
option=dict(type='str'), option=dict(type='str'),
value=dict(type='str'), value=dict(type='str'),
values=dict(type='list', elements='str'), values=dict(type='list', elements='str'),
@ -498,6 +610,7 @@ def main():
path = module.params['path'] path = module.params['path']
section = module.params['section'] section = module.params['section']
section_has_values = module.params['section_has_values']
option = module.params['option'] option = module.params['option']
value = module.params['value'] value = module.params['value']
values = module.params['values'] values = module.params['values']
@ -519,8 +632,16 @@ def main():
elif values is None: elif values is None:
values = [] values = []
if section_has_values:
for condition in section_has_values:
if condition['value'] is not None:
condition['values'] = [condition['value']]
elif condition['values'] is None:
condition['values'] = []
# raise Exception("section_has_values: {}".format(section_has_values))
(changed, backup_file, diff, msg) = do_ini( (changed, backup_file, diff, msg) = do_ini(
module, path, section, option, values, state, exclusive, backup, module, path, section, section_has_values, option, values, state, exclusive, backup,
no_extra_spaces, ignore_spaces, create, allow_no_value, modify_inactive_option, follow) no_extra_spaces, ignore_spaces, create, allow_no_value, modify_inactive_option, follow)
if not module.check_mode and os.path.exists(path): if not module.check_mode and os.path.exists(path):

View file

@ -16,7 +16,6 @@
- name: include tasks - name: include tasks
block: block:
- name: include tasks to perform basic tests - name: include tasks to perform basic tests
include_tasks: tests/00-basic.yml include_tasks: tests/00-basic.yml
@ -50,3 +49,6 @@
- name: include tasks to test optional spaces in section headings - name: include tasks to test optional spaces in section headings
include_tasks: tests/07-section_name_spaces.yml include_tasks: tests/07-section_name_spaces.yml
- name: include tasks to test section_has_values
include_tasks: tests/08-section.yml

View file

@ -0,0 +1,341 @@
---
# 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
[drinks]
fav = lemonade
beverage = pineapple juice
dest: "{{ output_file }}"
- name: test-section 1 - Modify starting ini file
ini_file:
dest: "{{ output_file }}"
section: drinks
option: car
value: volvo
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
[drinks]
fav = lemonade
beverage = pineapple juice
output1: "{{ output_content.content | b64decode }}"
- name: test-section 1 - Option was added to first section
assert:
that:
- result1 is changed
- result1.msg == 'option 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
option: car
value: 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
beverage = pineapple juice
car = volvo
output1: "{{ output_content.content | b64decode }}"
- name: test-section 2 - Option added to second section specified with section_has_values
assert:
that:
- result1 is changed
- result1.msg == 'option added'
- 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
option: fav
value: lemonade
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
[drinks]
beverage = pineapple juice
output1: "{{ output_content.content | b64decode }}"
- name: test-section 3 - Option was removed from specified section
assert:
that:
- result1 is changed
- result1.msg == 'option changed'
- output1 == expected1
# ----------------
- name: test-section 4 - Create starting ini file
copy:
content: |
[drinks]
fav = lemonade
beverage = orange juice
[drinks]
fav = lemonade
beverage = pineapple juice
dest: "{{ output_file }}"
- name: test-section 4 - Modify starting ini file
ini_file:
dest: "{{ output_file }}"
section: drinks
section_has_values:
- option: beverage
value: alligator slime
option: fav
value: tea
state: present
register: result1
- name: test-section 4 - Read modified file
slurp:
src: "{{ output_file }}"
register: output_content
- name: test-section 4 - Create expected result
set_fact:
expected1: |
[drinks]
fav = lemonade
beverage = orange juice
[drinks]
fav = lemonade
beverage = pineapple juice
[drinks]
beverage = alligator slime
fav = tea
output1: "{{ output_content.content | b64decode }}"
- name: test-section 4 - New section created, including required values
assert:
that:
- result1 is changed
- result1.msg == 'section and option added'
- output1 == expected1
# ----------------
- name: test-section 5 - Modify test-section 4 result file
ini_file:
dest: "{{ output_file }}"
section: drinks
section_has_values:
- option: fav
value: lemonade
- option: beverage
value: pineapple juice
state: absent
register: result1
- name: test-section 5 - Read modified file
slurp:
src: "{{ output_file }}"
register: output_content
- name: test-section 5 - Create expected result
set_fact:
expected1: |
[drinks]
fav = lemonade
beverage = orange juice
[drinks]
beverage = alligator slime
fav = tea
output1: "{{ output_content.content | b64decode }}"
- name: test-section 5 - Section removed as specified
assert:
that:
- result1 is changed
- result1.msg == 'section removed'
- output1 == expected1
# ----------------
- name: test-section 6 - Modify test-section 5 result file with multiple values
ini_file:
dest: "{{ output_file }}"
section: drinks
section_has_values:
- option: fav
values:
- cherry
- lemon
- vanilla
- option: beverage
value: pineapple juice
state: present
option: fav
values:
- vanilla
- grape
exclusive: false
register: result1
- name: test-section 6 - Read modified file
slurp:
src: "{{ output_file }}"
register: output_content
- name: test-section 6 - Create expected result
set_fact:
expected1: |
[drinks]
fav = lemonade
beverage = orange juice
[drinks]
beverage = alligator slime
fav = tea
[drinks]
beverage = pineapple juice
fav = vanilla
fav = grape
fav = cherry
fav = lemon
output1: "{{ output_content.content | b64decode }}"
- name: test-section 6 - New section added
assert:
that:
- result1 is changed
- result1.msg == 'section and option added'
- output1 == expected1
# ----------------
- name: test-section 7 - Modify test-section 6 result file with exclusive value
ini_file:
dest: "{{ output_file }}"
section: drinks
section_has_values:
- option: fav
value: vanilla
state: present
option: fav
value: cherry
exclusive: true
register: result1
- name: test-section 7 - Read modified file
slurp:
src: "{{ output_file }}"
register: output_content
- name: test-section 7 - Create expected result
set_fact:
expected1: |
[drinks]
fav = lemonade
beverage = orange juice
[drinks]
beverage = alligator slime
fav = tea
[drinks]
beverage = pineapple juice
fav = cherry
output1: "{{ output_content.content | b64decode }}"
- name: test-section 7 - Option changed
assert:
that:
- result1 is changed
- result1.msg == 'option changed'
- output1 == expected1