Skip to content

Instantly share code, notes, and snippets.

@mgedmin
Last active November 18, 2020 16:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgedmin/aa629d5a4c5fe143a2ed to your computer and use it in GitHub Desktop.
Save mgedmin/aa629d5a4c5fe143a2ed to your computer and use it in GitHub Desktop.
Ansible module for manipulating .ini files
# 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
#!/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()
- 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
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