Skip to content

Instantly share code, notes, and snippets.

@bangpound
Last active November 5, 2017 18:46
Show Gist options
  • Save bangpound/5ba48420e259360ab5ff20539355d422 to your computer and use it in GitHub Desktop.
Save bangpound/5ba48420e259360ab5ff20539355d422 to your computer and use it in GitHub Desktop.
INI File from Ansible for use anywhere

INI File

For example:

  • ini_file.py php.ini --section PHP --option memory_limit --value 256M
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
# Copyright: (c) 2015, Ales Nosek <anosek.nosek () gmail.com>
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# from __future__ import absolute_import, division, print_function
__metaclass__ = type
#
DOCUMENTATION = '''
---
module: ini_file
short_description: Tweak settings in INI files
extends_documentation_fragment: files
description:
- Manage (add, remove, change) individual settings in an INI-style file without having
to manage the file as a whole with, say, M(template) or M(assemble). Adds missing
sections if they don't exist.
- Before version 2.0, comments are discarded when the source file is read, and therefore will not show up in the destination file.
- Since version 2.3, this module adds missing ending newlines to files to keep in line with the POSIX standard, even when
no other modifications need to be applied.
version_added: "0.9"
options:
path:
description:
- Path to the INI-style file; this file is created if required.
- Before 2.3 this option was only usable as I(dest).
required: true
default: null
aliases: ['dest']
section:
description:
- Section name in INI file. This is added if C(state=present) automatically when
a single value is being set.
- If left empty or set to `null`, the I(option) will be placed before the first I(section).
Using `null` is also required if the config format does not support sections.
required: true
default: null
option:
description:
- If set (required for changing a I(value)), this is the name of the option.
- May be omitted if adding/removing a whole I(section).
required: false
default: null
value:
description:
- The string value to be associated with an I(option). May be omitted when removing an I(option).
required: false
default: null
backup:
description:
- Create a backup file including the timestamp information so you can get
the original file back if you somehow clobbered it incorrectly.
required: false
default: "no"
choices: [ "yes", "no" ]
others:
description:
- All arguments accepted by the M(file) module also work here
required: false
state:
description:
- If set to C(absent) the option or section will be removed if present instead of created.
required: false
default: "present"
choices: [ "present", "absent" ]
no_extra_spaces:
description:
- Do not insert spaces before and after '=' symbol
required: false
default: false
version_added: "2.1"
create:
required: false
choices: [ "yes", "no" ]
default: "yes"
description:
- If set to 'no', the module will fail if the file does not already exist.
By default it will create the file if it is missing.
version_added: "2.2"
notes:
- While it is possible to add an I(option) without specifying a I(value), this makes
no sense.
- As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but
I(dest) still works as well.
author:
- "Jan-Piet Mens (@jpmens)"
- "Ales Nosek (@noseka1)"
'''
import os
import re
import argparse
# ==============================================================
# match_opt
def match_opt(option, line):
option = re.escape(option)
return re.match('( |\t)*%s( |\t)*=' % option, line) \
or re.match('#( |\t)*%s( |\t)*=' % option, line) \
or re.match(';( |\t)*%s( |\t)*=' % option, line)
# ==============================================================
# match_active_opt
def match_active_opt(option, line):
option = re.escape(option)
return re.match('( |\t)*%s( |\t)*=' % option, line)
# ==============================================================
# do_ini
def do_ini(filename, section=None, option=None, value=None,
state='present', backup=False, no_extra_spaces=False, create=True):
diff = {'before': '',
'after': '',
'before_header': '%s (content)' % filename,
'after_header': '%s (content)' % filename}
if not os.path.exists(filename):
# if not create:
# module.fail_json(rc=257, msg='Destination %s does not exist !' % filename)
destpath = os.path.dirname(filename)
if not os.path.exists(destpath):
os.makedirs(destpath)
ini_lines = []
else:
ini_file = open(filename, 'r')
try:
ini_lines = ini_file.readlines()
finally:
ini_file.close()
# if module._diff:
diff['before'] = ''.join(ini_lines)
changed = False
# ini file could be empty
if not ini_lines:
ini_lines.append('\n')
# last line of file may not contain a trailing newline
if ini_lines[-1] == "" or ini_lines[-1][-1] != '\n':
ini_lines[-1] += '\n'
changed = True
# append a fake section line to simplify the logic
ini_lines.append('[')
within_section = not section
section_start = 0
msg = 'OK'
if no_extra_spaces:
assignment_format = '%s=%s\n'
else:
assignment_format = '%s = %s\n'
for index, line in enumerate(ini_lines):
if line.startswith('[%s]' % section):
within_section = True
section_start = index
elif line.startswith('['):
if within_section:
if state == 'present':
# insert missing option line at the end of the section
for i in range(index, 0, -1):
# search backwards for previous non-blank or non-comment line
if not re.match(r'^[ \t]*([#;].*)?$', ini_lines[i - 1]):
ini_lines.insert(i, assignment_format % (option, value))
msg = 'option added'
changed = True
break
elif state == 'absent' and not option:
# remove the entire section
del ini_lines[section_start:index]
msg = 'section removed'
changed = True
break
else:
if within_section and option:
if state == 'present':
# change the existing option line
if match_opt(option, line):
newline = assignment_format % (option, value)
option_changed = ini_lines[index] != newline
changed = changed or option_changed
if option_changed:
msg = 'option changed'
ini_lines[index] = newline
if option_changed:
# remove all possible option occurrences from the rest of the section
index = index + 1
while index < len(ini_lines):
line = ini_lines[index]
if line.startswith('['):
break
if match_active_opt(option, line):
del ini_lines[index]
else:
index = index + 1
break
elif state == 'absent':
# delete the existing line
if match_active_opt(option, line):
del ini_lines[index]
changed = True
msg = 'option changed'
break
# remove the fake section line
del ini_lines[-1:]
if not within_section and option and state == 'present':
ini_lines.append('[%s]\n' % section)
ini_lines.append(assignment_format % (option, value))
changed = True
msg = 'section and option added'
# if module._diff:
diff['after'] = ''.join(ini_lines)
backup_file = None
if changed:
# if backup:
# backup_file = module.backup_local(filename)
ini_file = open(filename, 'w')
try:
ini_file.writelines(ini_lines)
finally:
ini_file.close()
return (changed, backup_file, diff, msg)
# ==============================================================
# main
def main():
parser = argparse.ArgumentParser(description='Tweak settings in INI files',
epilog='While it is possible to add an "option" without specifying a I(value), this makes no sense.')
parser.add_argument('path', help='Path to the INI-style file; this file is created if required')
parser.add_argument('--section',
help='Section name in INI file. This is added if state is present automatically when a single value is being set.',
default=None)
parser.add_argument('--option',
help='If set (required for changing a value), this is the name of the option. May be omitted if adding/removing a whole section.')
parser.add_argument('--value',
help='The string value to be associated with an option. May be omitted when removing an option.')
parser.add_argument('--state',
help='If set to "absent" the option or section will be removed if present instead of created.',
default='present')
parser.add_argument('--backup',
help='Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.',
default=False)
parser.add_argument('--no_extra_spaces', help='Do not insert spaces before and after "=" symbol', default=False,
action='store_true')
parser.add_argument('--create',
help='If set to "no", the module will fail if the file does not already exist. By default it will create the file if it is missing.',
default=True)
args = parser.parse_args()
path = args.path
section = args.section
option = args.option
value = args.value
state = args.state
backup = args.backup
no_extra_spaces = args.no_extra_spaces
create = args.create
(changed, backup_file, diff, msg) = do_ini(path, section, option, value, state, backup, no_extra_spaces, create)
# if not module.check_mode and os.path.exists(path):
# file_args = module.load_file_common_arguments(args)
# changed = module.set_fs_attributes_if_different(file_args, changed)
results = {'changed': changed, 'msg': msg, 'path': path, 'diff': diff}
if backup_file is not None:
results['backup_file'] = backup_file
# Mission complete
# module.exit_json(**results)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment