Last active
November 18, 2020 16:50
-
-
Save mgedmin/aa629d5a4c5fe143a2ed to your computer and use it in GitHub Desktop.
Ansible module for manipulating .ini files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# put this in action_plugins/ini_file_ex.py if you want --diff to work | |
try: | |
# Ansible 2: no special hacks necessary | |
from ansible.plugins.action.normal import ActionModule | |
except ImportError: | |
# Ansible 1: hoist 'diff' from result.result | |
from ansible.runner.action_plugins.normal import ActionModule as _ActionModule | |
class ActionModule(_ActionModule): | |
'''Support --diff mode''' | |
def run(self, *args, **kwargs): | |
result = super(ActionModule, self).run(*args, **kwargs) | |
if 'diff' in result.result: | |
result.diff = result.result['diff'] | |
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
import re | |
DOCUMENTATION = ''' | |
--- | |
module: ini_file_ex | |
short_description: changes parameters in INI files | |
description: | |
- This is very similar to the M(ini_file) module, but more powerful in | |
some ways, and less powerful in other ways. | |
options: | |
dest: | |
description: | |
- the name of INI file to edit | |
- it is an error if the file doesn't exist | |
required: true | |
default: null | |
section: | |
description: | |
- the name of the section | |
required: false | |
default: null | |
option: | |
description: | |
- the name of the setting | |
required: true | |
default: null | |
value: | |
description: | |
- the new value for that setting | |
required: false | |
default: null | |
append: | |
description: | |
- an item to include in a multi-item setting, if it's not already present | |
required: false | |
default: null | |
remove: | |
description: | |
- an item to remove from a multi-item setting | |
required: false | |
default: null | |
sep: | |
description: | |
- separator for multi-item settings | |
required: false | |
default: whitespace | |
append_line: | |
description: | |
- a line to include in a multi-line setting, if it's not already present | |
required: false | |
default: null | |
remove_line: | |
description: | |
- a line to remove from a multi-line setting | |
required: false | |
default: null | |
state: | |
description: | |
- set to "absent" to remove a section or an option | |
- set to "commented" to comment-out an option | |
required: false | |
default: "present" | |
choices: [ "present", "absent", "commented" ] | |
changes: | |
description: | |
- a list of changes to apply; each item is a dict that can contain | |
C(section), C(option), C(value), C(append), C(remove), C(sep), | |
C(append_line), C(remove_line), C(state) keys | |
- any options provided directly to the module and not overwritten | |
in an individual change item will be inherited | |
- if you have a C(changes) list, other options at the top level | |
do not apply | |
- note that C(dest) is always a global option and cannot be | |
overridden by a change item | |
- you can also make optional changes by adding a C(when) key | |
to each change, with a value "true" or "false" | |
required: false | |
default: null | |
author: | |
- Marius Gedminas <marius@pov.lt> | |
''' | |
EXAMPLES = ''' | |
# Sections are optional | |
- ini_file_ex: dest=/etc/postfix/main.cf option=inet_protocols value=all | |
# Append a value to a comma-separated list, if it's not present | |
- ini_file_ex: | |
dest: /etc/postfix/main.cf | |
option: mydestination | |
append: example.com | |
sep: "," | |
# Append a value to a newline-separated list, if it's not present | |
- ini_file_ex: | |
dest: /etc/postfix/main.cf | |
option: mynetworks | |
append_line: 192.168.0.0/24 | |
# Sections can be used | |
- ini_file_ex: | |
dest: /etc/roundup/roundup-server.ini | |
section: trackers | |
option: tracker1 | |
value: /var/lib/roundup/trackers/tracker1 | |
# Options can be removed or commented out | |
- ini_file_ex: | |
dest: /etc/roundup/roundup-server.ini | |
section: trackers | |
option: tracker2 | |
state: absent | |
- ini_file_ex: | |
dest: /etc/roundup/roundup-server.ini | |
section: trackers | |
option: tracker2 | |
state: commented | |
- ini_file_ex: | |
dest: /etc/roundup/roundup-server.ini | |
section: trackers | |
option: tracker2 | |
value: /var/lib/roundup/trackers/tracker1 | |
state: commented | |
# Whole sections can be removed | |
- ini_file_ex: | |
dest: /etc/roundup/roundup-server.ini | |
section: trackers | |
state: absent | |
# Items can be removed from options | |
- ini_file_ex: | |
dest: /etc/postfix/main.cf | |
option: mydestination | |
remove: example.com | |
sep: "," | |
- ini_file_ex: | |
dest: /etc/postfix/main.cf | |
option: mynetworks | |
remove_line: 192.168.0.0/24 | |
# Aggregating multiple changes | |
- ini_file_ex: | |
dest: /etc/postfix/main.cf | |
changes: | |
- option: inet_protocols | |
value: ipv4 | |
- option: mydestination | |
remove: example.com | |
sep: "," | |
- option: mynetworks | |
remove_line: 192.168.0.0/24 | |
when: "{{ not should_accept_lan_mail }}" | |
''' | |
# Borrowed from ConfigParser | |
SECTION_RX = re.compile( | |
r'\[' # [ | |
r'(?P<header>[^]]+)' # very permissive! | |
r'\]' # ] | |
) | |
# slightly modified to reject comment lines | |
OPTION_RX = re.compile( | |
r'(?P<option>[^:=\s#;][^:=]*)' # very permissive! | |
r'\s*(?P<vi>[:=])\s*' # any number of space/tab, | |
# followed by separator | |
# (either : or =), followed | |
# by any # space/tab | |
r'(?P<value>.*)$' # everything up to eol | |
) | |
# my own invention | |
COMMENTED_OPTION_RX = re.compile( | |
r'#+\s?' # comments! | |
r'(?P<option>[^:=\s#;][^:=]*)' # very permissive! | |
r'\s*(?P<vi>[:=])\s*' # any number of space/tab, | |
# followed by separator | |
# (either : or =), followed | |
# by any # space/tab | |
r'(?P<value>.*)$' # everything up to eol | |
) | |
def blank_or_comment(line): | |
return not line.strip() or line[0] in '#;' | |
class Error(Exception): | |
pass | |
class IniFile(object): | |
"""An INI file is represented as a sequence of sections. | |
The first section is headerless and has a name of '' (empty string). | |
""" | |
def __init__(self, filename=None): | |
if filename: | |
self.read(filename) | |
else: | |
self.parse([]) | |
def read(self, filename): | |
with open(filename) as fp: | |
self.parse(fp) | |
def parse(self, fp): | |
section = IniSection() | |
self.sections = [section] | |
self.by_name = {'': section, None: section} | |
for n, line in enumerate(fp, 1): | |
m = SECTION_RX.match(line) | |
if m: | |
name = m.group('header') | |
if section.name in self.sections: | |
raise Error('duplicate section %s on line %d' % (name, n)) | |
section = IniSection(name, [line]) | |
self.by_name[name] = section | |
self.sections.append(section) | |
else: | |
section.add_line(line, n) | |
def as_string(self): | |
return ''.join(section.as_string() for section in self.sections) | |
def add_section(self, name, strict=False): | |
if name in self.by_name: | |
if strict: | |
raise Error('section %s already exists' % name) | |
return | |
self.sections[-1].ensure_ends_with_blank_line() | |
section = IniSection(name, ['[%s]\n' % name]) | |
self.by_name[name] = section | |
self.sections.append(section) | |
def remove_section(self, name, strict=False): | |
if name not in self.by_name: | |
if strict: | |
raise Error('section %s does not exist' % name) | |
return | |
if name: | |
del self.by_name[name] | |
self.sections = [s for s in self.sections | |
if s.name != name] | |
else: | |
# never remove the preamble, only clear it | |
self.by_name[name].clear() | |
def set_value(self, section_name, option, value): | |
if section_name not in self.by_name: | |
self.add_section(section_name) | |
self.by_name[section_name].set_value(option, value) | |
def append_value(self, section_name, option, value, sep=None): | |
if section_name not in self.by_name: | |
self.add_section(section_name) | |
self.by_name[section_name].append_value(option, value, sep=sep) | |
def append_line(self, section_name, option, value): | |
if section_name not in self.by_name: | |
self.add_section(section_name) | |
self.by_name[section_name].append_line(option, value) | |
def remove_value(self, section_name, option, value, sep=None, strict=False): | |
if section_name not in self.by_name: | |
if strict: | |
raise Error('section %s does not exist' % name) | |
return | |
self.by_name[section_name].remove_value(option, value, sep=sep, strict=strict) | |
def remove_line(self, section_name, option, value, strict=False): | |
if section_name not in self.by_name: | |
if strict: | |
raise Error('section %s does not exist' % name) | |
return | |
self.by_name[section_name].remove_line(option, value, strict=strict) | |
def remove_option(self, section_name, option, strict=False): | |
if section_name not in self.by_name: | |
if strict: | |
raise Error('section %s does not exist' % name) | |
return | |
self.by_name[section_name].remove_option(option, strict=strict) | |
def comment_out(self, section_name, option, strict=False): | |
if section_name not in self.by_name: | |
if strict: | |
raise Error('section %s does not exist' % name) | |
return | |
self.by_name[section_name].comment_out(option, strict=strict) | |
class IniSection(object): | |
"""An INI section is represented as a sequence of options. | |
The first "option" is the preamble. It is nameless, has no value, | |
and consists of blank lines and comments above the first actual | |
option. | |
""" | |
def __init__(self, name='', lines=()): | |
self.name = name | |
self.options = [IniPreamble(lines)] | |
self.by_name = {} | |
self.pending_placeholder = None | |
def as_string(self): | |
return ''.join(opt.as_string() for opt in self.options) | |
def add_line(self, line, lineno): | |
m = OPTION_RX.match(line) | |
if m: | |
self.place_pending_placeholder() | |
name = m.group('option').strip() | |
self.remove_placeholder(name) | |
if name in self.by_name: | |
raise Error('duplicate option %s on line %d' % (name, lineno)) | |
self.by_name[name] = option = IniOption(name, [line]) | |
self.options.append(option) | |
return | |
if not line.strip(): | |
self.place_pending_placeholder() | |
self.options[-1].add_line(line, lineno) | |
m = COMMENTED_OPTION_RX.match(line) | |
if m: | |
self.place_pending_placeholder() | |
name = m.group('option').strip() | |
self.pending_placeholder = name | |
def place_pending_placeholder(self): | |
if not self.pending_placeholder: | |
return | |
name = self.pending_placeholder | |
self.remove_placeholder(name) | |
if name not in self.by_name: | |
self.by_name[name] = option = IniOption(name, [], placeholder=True) | |
self.options.append(option) | |
self.pending_placeholder = None | |
def previous(self, option): | |
idx = self.options.index(option) | |
assert idx > 0 | |
return self.options[idx - 1] | |
def remove_placeholder(self, name): | |
if name in self.by_name and self.by_name[name].placeholder: | |
option = self.by_name[name] | |
prev = self.previous(option) | |
prev.add_trailing_comments(option.parse_trailing_comments()) | |
self.options.remove(option) | |
del self.by_name[name] | |
def clear(self): | |
self.options = [IniPreamble()] | |
self.by_name = {} | |
def ensure_ends_with_blank_line(self): | |
self.options[-1].ensure_ends_with_blank_line() | |
def add_option(self, name, value, strict=False): | |
if not name: | |
raise Error('option name cannot be blank') | |
if name in self.by_name: | |
if strict: | |
raise Error('option %s already exists' % name) | |
return | |
option = IniOption(name) | |
option.set_value(value) | |
self.by_name[name] = option | |
self.options.append(option) | |
def set_value(self, name, value): | |
if name not in self.by_name: | |
self.add_option(name, value) | |
else: | |
self.by_name[name].set_value(value) | |
def append_value(self, name, value, sep=None): | |
if name not in self.by_name: | |
self.add_option(name, '') | |
self.by_name[name].append_value(value, sep=sep) | |
def append_line(self, name, value): | |
if name not in self.by_name: | |
self.add_option(name, '') | |
self.by_name[name].append_line(value) | |
def remove_value(self, name, value, sep=None, strict=False): | |
if name not in self.by_name: | |
if strict: | |
raise Error('option %s does not exist' % name) | |
return | |
self.by_name[name].remove_value(value, sep=sep) | |
def remove_line(self, name, value, strict=False): | |
if name not in self.by_name: | |
if strict: | |
raise Error('option %s does not exist' % name) | |
return | |
self.by_name[name].remove_line(value) | |
def remove_option(self, name, strict=False): | |
if name not in self.by_name: | |
if strict: | |
raise Error('option %s does not exist' % name) | |
return | |
self.by_name[name].clear() | |
def comment_out(self, name, strict=False): | |
if name not in self.by_name: | |
if strict: | |
raise Error('option %s does not exist' % name) | |
return | |
option = self.by_name[name] | |
option.comment_out() | |
prev = self.previous(option) | |
if prev.lines and option.lines and prev.lines[-1] == option.lines[0]: | |
del option.lines[0] | |
class IniOption(object): | |
"""An INI option is a sequence of lines. | |
The first line is usually "option-name = value". Other lines | |
can be continuation lines, comments and blank lines. | |
An option can be a placeholder, which means it's just a place | |
where the named option should be inserted. Normally placeholders | |
apear after a commented-out option value. | |
""" | |
def __init__(self, name='', lines=(), placeholder=False): | |
self.name = name | |
self.lines = list(lines) | |
self.placeholder = placeholder | |
def clear(self, keep_comments=True): | |
if keep_comments: | |
self.lines = self.parse_trailing_comments() | |
else: | |
self.lines = [] | |
self.placeholder = True | |
def comment_out(self): | |
self.lines = [line if blank_or_comment(line) else '# ' + line | |
for line in self.lines] | |
def as_string(self): | |
return ''.join(self.lines) | |
def add_line(self, line, lineno): | |
self.lines.append(line) | |
def add_trailing_comments(self, comments): | |
self.lines.extend(comments) | |
def ensure_ends_with_blank_line(self): | |
if self.lines and self.lines[-1].strip(): | |
self.lines.append('\n') | |
def parse_lines(self): | |
if not self.lines: | |
return [] | |
m = OPTION_RX.match(self.lines[0]) | |
if not m: | |
return [(line, None) for line in self.lines] | |
res = [(self.lines[0], m.group('value').strip())] | |
for line in self.lines[1:]: | |
if blank_or_comment(line): | |
res.append((line, None)) | |
continue | |
if line[0].isspace(): | |
res.append((line, line.strip())) | |
else: | |
res.append((line, None)) | |
return res | |
def parse_trailing_comments(self): | |
if not self.lines: | |
return [] | |
m = OPTION_RX.match(self.lines[0]) | |
if not m: | |
return self.lines | |
res = [] | |
for line in self.lines[1:]: | |
if blank_or_comment(line): | |
res.append(line) | |
continue | |
if line[0].isspace(): | |
res = [] | |
else: | |
res.append(line) | |
return res | |
def get_value(self): | |
return '\n'.join(val for line, val in self.parse_lines() if val is not None) | |
def set_value(self, value): | |
trail = self.parse_trailing_comments() | |
if not value: | |
self.lines = ['%s =\n' % self.name] + trail | |
else: | |
self.lines = ['%s = %s\n' % (self.name, value)] + trail | |
def get_values(self, sep=None): | |
return [val.strip() for val in self.get_value().split(sep)] | |
def joiner(self, sep=None): | |
if not sep: | |
return ' ' | |
elif not sep.endswith(' '): | |
return sep + ' ' | |
else: | |
return sep | |
def append_value(self, value, sep=None): | |
value = value.strip() | |
cur_value = self.get_value() | |
if value not in self.get_values(): | |
if cur_value: | |
self.set_value(cur_value + self.joiner(sep) + value) | |
else: | |
self.set_value(value) | |
def append_line(self, value): | |
if self.placeholder: | |
self.lines.insert(0, '%s =\n' % self.name) | |
if value.strip() not in (val for line, val in self.parse_lines()): | |
trail = self.parse_trailing_comments() | |
if trail: | |
self.lines = self.lines[:-len(trail)] | |
self.lines.append(' %s\n' % value) | |
self.lines.extend(trail) | |
def remove_value(self, value, sep=None): | |
# NB: removes more than one occurrence, is that ok? | |
value = value.strip() | |
joiner = self.joiner(sep) | |
self.set_value(joiner.join(val for val in self.get_values(sep) | |
if val != value)) | |
def remove_line(self, value): | |
# NB: removes more than one occurrence, is that ok? | |
value = value.strip() | |
if not value: | |
return | |
parsed = self.parse_lines() | |
if not parsed: | |
return | |
if value == parsed[0][1]: | |
parsed[0] = ('%s =\n' % self.name, '') | |
self.lines = [line for line, val in parsed | |
if val != value] | |
class IniPreamble(IniOption): | |
def __init__(self, lines=()): | |
super(IniPreamble, self).__init__(lines=lines) | |
WHEN_TRUE = (True, 'True', 'true', None, '') | |
WHEN_FALSE = (False, 'False', 'false') | |
def should_apply(when): | |
return when in WHEN_TRUE | |
def apply_change(ini, params, module): | |
state = params['state'] | |
section = params['section'] | |
option = params['option'] | |
value = params['value'] | |
append = params['append'] | |
append_line = params['append_line'] | |
remove = params['remove'] | |
remove_line = params['remove_line'] | |
sep = params['sep'] | |
when = params.get('when') | |
if when not in (WHEN_TRUE + WHEN_FALSE): | |
module.fail_json(msg="'when' expects true/false, not %r" % when) | |
used = {n for n in ('value', 'append', 'append_line', 'remove', 'remove_line') | |
if params[n] is not None} | |
if len(used) > 1: | |
module.fail_json(msg="only one of %s can be used at one time" % '/'.join(sorted(used))) | |
if sep is not None and used != {'append'} and used != {'remove'}: | |
module.fail_json(msg="sep can only be used with append or remove") | |
if state == 'absent': | |
# validate arguments | |
if used: | |
module.fail_json(msg="%s not allowed when state=absent" % '/'.join(sorted(used))) | |
if section is None and option is None: | |
module.fail_json(msg="section or option required when state=absent") | |
# check condition | |
if not should_apply(when): | |
return | |
# apply changes | |
if option: | |
ini.remove_option(section, option) | |
else: | |
ini.remove_section(section) | |
elif state == 'commented': | |
if used and used != {'value'}: | |
module.fail_json(msg="%s not allowed when state=commented" % '/'.join(sorted(used - {'value'}))) | |
if section is None and option is None: | |
module.fail_json(msg="section or option required when state=commented") | |
if value is not None: | |
ini.set_value(section, option, value) | |
ini.comment_out(section, option) | |
elif state == 'present': | |
# validate arguments | |
if section is None and option is None: | |
module.fail_json(msg="section or option required when state=present") | |
if option is None and used: | |
module.fail_json(msg="option required when setting a value") | |
# check condition | |
if not should_apply(when): | |
return | |
# apply changes | |
if option is None: | |
ini.add_section(section) | |
elif value is not None: | |
ini.set_value(section, option, value) | |
elif append is not None: | |
ini.append_value(section, option, append, sep=sep) | |
elif append_line is not None: | |
ini.append_line(section, option, append_line) | |
elif remove is not None: | |
ini.remove_value(section, option, remove, sep=sep) | |
elif remove_line is not None: | |
ini.remove_line(section, option, remove_line) | |
else: | |
module.fail_json(msg="internal error: unexpected state %r" % state) | |
def main(): | |
module = AnsibleModule( | |
argument_spec=dict( | |
dest=dict(required=True), | |
section=dict(required=False), | |
option=dict(required=False), | |
value=dict(required=False), | |
append=dict(required=False), | |
append_line=dict(required=False), | |
remove=dict(required=False), | |
remove_line=dict(required=False), | |
sep=dict(required=False), | |
state=dict(default='present', choices=['present', 'absent', 'commented']), | |
changes=dict(required=False), | |
), | |
supports_check_mode=True, | |
) | |
# Possible actions: | |
# - add section (section=X, state=present) | |
# - delete section (section=X, state=absent) | |
# - set option ([section=X] option=Y, value=Z, state=present) | |
# - delete option ([section=X] option=Y, state=absent) | |
# - modify option ([section=X] option=Y, append/append_line/remove/remove_line=Z) | |
# - comment out option ([section=X] option=Y, [value=Z], state=commented) | |
defaults = module.params.copy() | |
dest = defaults.pop('dest', None) | |
changes = defaults.pop('changes', None) | |
if not changes: | |
changes = [defaults] | |
else: | |
for n, change in enumerate(changes): | |
changes[n] = defaults.copy() | |
changes[n].update(change) | |
try: | |
ini = IniFile(dest) | |
except (IOError, Error) as e: | |
module.fail_json(msg=str(e)) | |
before = ini.as_string() | |
for change in changes: | |
apply_change(ini, change, module) | |
after = ini.as_string() | |
changed = before != after | |
if not module.check_mode and changed: | |
try: | |
with open(dest, 'w') as fp: | |
fp.write(after) | |
except IOError as e: | |
module.fail_json(msg=str(e)) | |
module.exit_json( | |
changed=changed, | |
diff=dict( | |
before_header=dest, | |
after_header=dest, | |
before=before, | |
after=after), | |
) | |
from ansible.module_utils.basic import * # noqa | |
if __name__ == '__main__': | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- hosts: localhost | |
gather_facts: no | |
tasks: | |
- shell: "> /tmp/inifile" | |
# section or option required | |
- ini_file_ex: dest=/tmp/inifile | |
register: out | |
failed_when: not out.failed | |
- assert: | |
that: out.msg == 'section or option required when state=present' | |
# let's add a section | |
- ini_file_ex: dest=/tmp/inifile section=foo | |
- command: cat /tmp/inifile | |
register: out | |
- debug: msg="{{ out.stdout }}" | |
- assert: | |
that: out.stdout_lines == ["[foo]"] | |
# let's add a value | |
- ini_file_ex: dest=/tmp/inifile section=foo option=bar value=baz | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["[foo]", "bar = baz"] | |
# let's change the value | |
- ini_file_ex: dest=/tmp/inifile section=foo option=bar value=qux | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["[foo]", "bar = qux"] | |
# let's remove the value | |
- ini_file_ex: dest=/tmp/inifile section=foo option=bar state=absent | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["[foo]"] | |
# let's remove the section | |
- ini_file_ex: dest=/tmp/inifile section=foo state=absent | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == [] | |
# sectionless options! | |
- ini_file_ex: dest=/tmp/inifile option=foo value=bar | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["foo = bar"] | |
# multi-line options | |
- ini_file_ex: dest=/tmp/inifile section=x option=y append_line=z | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["foo = bar", "", "[x]", "y =", " z"] | |
# idempotency | |
- ini_file_ex: dest=/tmp/inifile section=x option=y append_line=z | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["foo = bar", "", "[x]", "y =", " z"] | |
# setup for next test | |
- ini_file_ex: dest=/tmp/inifile section=x option=y append_line=q | |
- ini_file_ex: dest=/tmp/inifile section=x option=y append_line=w | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["foo = bar", "", "[x]", "y =", " z", " q", " w"] | |
# remove_line | |
- ini_file_ex: dest=/tmp/inifile section=x option=y remove_line=q | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["foo = bar", "", "[x]", "y =", " z", " w"] | |
# remove_line is careful about 1st line | |
- shell: "> /tmp/inifile" | |
- ini_file_ex: dest=/tmp/inifile option=opt value=one | |
- ini_file_ex: dest=/tmp/inifile option=opt append_line=two | |
- ini_file_ex: dest=/tmp/inifile option=opt remove_line=one | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["opt =", " two"] | |
# clear the slate | |
- shell: "> /tmp/inifile" | |
# append values | |
- ini_file_ex: dest=/tmp/inifile option=x append=foo | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = foo"] | |
- ini_file_ex: dest=/tmp/inifile option=x append=bar | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = foo bar"] | |
- ini_file_ex: dest=/tmp/inifile option=x append=baz | |
- ini_file_ex: dest=/tmp/inifile option=x append=baz | |
- ini_file_ex: dest=/tmp/inifile option=x append=bar | |
- ini_file_ex: dest=/tmp/inifile option=x append=foo | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = foo bar baz"] | |
# remove values | |
- ini_file_ex: dest=/tmp/inifile option=x remove=foo | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = bar baz"] | |
# how about comma-separated-values? | |
- shell: "> /tmp/inifile" | |
- ini_file_ex: dest=/tmp/inifile option=x append=foo sep=, | |
- ini_file_ex: dest=/tmp/inifile option=x append=bar sep=, | |
- ini_file_ex: dest=/tmp/inifile option=x append=baz sep=, | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = foo, bar, baz"] | |
- ini_file_ex: dest=/tmp/inifile option=x remove=bar sep=, | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout_lines == ["x = foo, baz"] | |
# dealing with comments gracefully | |
- copy: | |
dest: /tmp/inifile | |
content: | | |
[foo] | |
# here is option foo | |
foo = 42 | |
# here is a multi-line option bar | |
bar = | |
one | |
two | |
three | |
# here is an unrelated comment | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo value=0 | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
foo = 0 | |
# here is a multi-line option bar | |
bar = | |
one | |
two | |
three | |
# here is an unrelated comment | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo state=absent | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# here is a multi-line option bar | |
bar = | |
one | |
two | |
three | |
# here is an unrelated comment | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
- ini_file_ex: dest=/tmp/inifile section=foo option=bar remove_line=three | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# here is a multi-line option bar | |
bar = | |
one | |
two | |
# here is an unrelated comment | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
- ini_file_ex: dest=/tmp/inifile section=foo option=bar append_line=three | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# here is a multi-line option bar | |
bar = | |
one | |
two | |
three | |
# here is an unrelated comment | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
# dealing with commented-out options gracefully | |
- copy: | |
dest: /tmp/inifile | |
content: | | |
[foo] | |
# here is option foo | |
# foo = 41 | |
# foo = 42 | |
# here is multi-line option | |
# whomp = | |
# lala | |
# blabla=haha | |
# here is option bar | |
bar = 25 | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo value=0 | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# foo = 41 | |
# foo = 42 | |
foo = 0 | |
# here is multi-line option | |
# whomp = | |
# lala | |
# blabla=haha | |
# here is option bar | |
bar = 25 | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo value=1 | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# foo = 41 | |
# foo = 42 | |
foo = 1 | |
# here is multi-line option | |
# whomp = | |
# lala | |
# blabla=haha | |
# here is option bar | |
bar = 25 | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
- ini_file_ex: dest=/tmp/inifile section=foo option=whomp append_line=hey | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# foo = 41 | |
# foo = 42 | |
foo = 1 | |
# here is multi-line option | |
# whomp = | |
# lala | |
# blabla=haha | |
whomp = | |
hey | |
# here is option bar | |
bar = 25 | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
# commenting out the options | |
- copy: | |
dest: /tmp/inifile | |
content: | | |
[foo] | |
# here is option foo | |
foo = 41 | |
# here is option bar | |
bar = 25 | |
tags: commented | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo value=42 state=commented | |
tags: commented | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# foo = 42 | |
# here is option bar | |
bar = 25 | |
tags: commented | |
- command: cat /tmp/inifile | |
register: out | |
tags: commented | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
tags: commented | |
# this action is idempotent | |
- ini_file_ex: dest=/tmp/inifile section=foo option=foo value=42 state=commented | |
tags: commented | |
- set_fact: | |
expect: | | |
[foo] | |
# here is option foo | |
# foo = 42 | |
# here is option bar | |
bar = 25 | |
tags: commented | |
- command: cat /tmp/inifile | |
register: out | |
tags: commented | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
tags: commented | |
# let's test multiple changes | |
- shell: "> /tmp/inifile" | |
- ini_file_ex: | |
dest: /tmp/inifile | |
section: foo | |
changes: | |
- { option: x, value: 42 } | |
- { option: y, append: a } | |
- { option: y, append: b } | |
- { section: bar, option: x, value: 25 } | |
- set_fact: | |
expect: | | |
[foo] | |
x = 42 | |
y = a b | |
[bar] | |
x = 25 | |
- command: cat /tmp/inifile | |
register: out | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
# let's test optional changes | |
- shell: "> /tmp/inifile" | |
tags: optional | |
- ini_file_ex: | |
dest: /tmp/inifile | |
section: foo | |
changes: | |
- { option: x, value: 42 } | |
- { option: y, value: 25, when: false } | |
- { option: z, value: 23, when: true } | |
- { option: y, append: a } | |
- { option: y, append: b, when: "{{ 1 == 1 }}" } | |
- { option: y, append: c, when: "{{ 1 == 2 }}" } | |
- { option: x, state: absent } | |
- { option: x, value: 43 } | |
- { section: bar, option: x, value: 23 } | |
- { section: bar, option: y, value: 25 } | |
- { section: bar, option: y, state: absent, when: true } | |
tags: optional | |
- set_fact: | |
expect: | | |
[foo] | |
x = 43 | |
z = 23 | |
y = a b | |
[bar] | |
x = 23 | |
tags: optional | |
- command: cat /tmp/inifile | |
register: out | |
tags: optional | |
- assert: | |
that: out.stdout.rstrip() == expect.rstrip() | |
tags: optional |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import os | |
import unittest | |
import textwrap | |
from cStringIO import StringIO | |
library_path = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, 'library')) | |
sys.path.append(library_path) | |
from ini_file_ex import IniFile, apply_change | |
class TestCase(unittest.TestCase): | |
def parse(self, text): | |
assert text.startswith('\n') | |
ini = IniFile() | |
ini.parse(StringIO(textwrap.dedent(text[1:]))) | |
return ini | |
def parse_option(self, text): | |
ini = self.parse(text) | |
assert len(ini.sections) == 1 | |
assert len(ini.sections[0].options) == 2 # preamble + option | |
return ini.sections[0].options[-1] | |
def assertText(self, actual, expected): | |
actual = actual.as_string() | |
assert expected.startswith('\n') | |
expected = textwrap.dedent(expected[1:]) | |
# force unicode to get unified diff out of unittest2 | |
self.assertEqual(unicode(actual), unicode(expected)) | |
class TestIniFile(TestCase): | |
def test_comment_out(self): | |
ini = self.parse(''' | |
# preamble | |
foo = some value | |
# etc | |
''') | |
ini.comment_out('', 'foo') | |
self.assertText(ini, ''' | |
# preamble | |
# foo = some value | |
# etc | |
''') | |
def test_comment_out_already_commented(self): | |
ini = self.parse(''' | |
# preamble | |
# foo = some value | |
# etc | |
''') | |
ini.comment_out('', 'foo') | |
self.assertText(ini, ''' | |
# preamble | |
# foo = some value | |
# etc | |
''') | |
def test_set_to_commented_out_default_value(self): | |
ini = self.parse(''' | |
# preamble | |
foo = some value | |
# etc | |
''') | |
ini.set_value('', 'foo', 'default value') | |
ini.comment_out('', 'foo') | |
self.assertText(ini, ''' | |
# preamble | |
# foo = default value | |
# etc | |
''') | |
def test_change_commented_out_default_value(self): | |
ini = self.parse(''' | |
# preamble | |
# foo = some value | |
# etc | |
''') | |
ini.set_value('', 'foo', 'default value') | |
ini.comment_out('', 'foo') | |
self.assertText(ini, ''' | |
# preamble | |
# foo = some value | |
# foo = default value | |
# etc | |
''') | |
def test_preserve_commented_out_default_value(self): | |
ini = self.parse(''' | |
# preamble | |
# foo = default value | |
# etc | |
''') | |
ini.set_value('', 'foo', 'default value') | |
ini.comment_out('', 'foo') | |
self.assertText(ini, ''' | |
# preamble | |
# foo = default value | |
# etc | |
''') | |
class TestIniOption(TestCase): | |
def test_clear(self): | |
option = self.parse_option(''' | |
foo = some value | |
# oh and here are some trailing comments for other things | |
''') | |
option.clear() | |
self.assertText(option, ''' | |
# oh and here are some trailing comments for other things | |
''') | |
def test_clear_multiline(self): | |
option = self.parse_option(''' | |
foo = some value | |
# with maybe some internal comments | |
that | |
spans | |
multiple | |
lines | |
# the usual | |
# trailing comments | |
''') | |
option.clear() | |
self.assertText(option, ''' | |
# the usual | |
# trailing comments | |
''') | |
def test_comment_out(self): | |
option = self.parse_option(''' | |
foo = some value | |
maybe multiline | |
# oh and here are some trailing comments for other things | |
''') | |
option.comment_out() | |
self.assertText(option, ''' | |
# foo = some value | |
# maybe multiline | |
# oh and here are some trailing comments for other things | |
''') | |
class Error(Exception): | |
pass | |
class ModuleStub(object): | |
def fail_json(self, msg): | |
raise Error(msg) | |
class TestApplyChange(TestCase): | |
def apply_change(self, ini, **kwargs): | |
change = dict.fromkeys(['section', 'option', 'value', 'append', | |
'append_line', 'remove', 'remove_line', | |
'sep', 'state']) | |
change.update(kwargs) | |
apply_change(ini, change, ModuleStub()) | |
def test_remove_option_without_sections(self): | |
ini = self.parse(''' | |
# pretend this is /etc/postfix/main.cf or something | |
relayhost = 10.20.30.40 | |
myhostname = localhost | |
mydestination = example.com | |
''') | |
self.apply_change(ini, option='myhostname', state='absent') | |
self.assertText(ini, ''' | |
# pretend this is /etc/postfix/main.cf or something | |
relayhost = 10.20.30.40 | |
mydestination = example.com | |
''') | |
def test_comment_out_option_without_sections(self): | |
ini = self.parse(''' | |
# pretend this is /etc/postfix/main.cf or something | |
relayhost = 10.20.30.40 | |
myhostname = localhost | |
mydestination = example.com | |
''') | |
self.apply_change(ini, option='myhostname', state='commented') | |
self.assertText(ini, ''' | |
# pretend this is /etc/postfix/main.cf or something | |
relayhost = 10.20.30.40 | |
# myhostname = localhost | |
mydestination = example.com | |
''') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment