mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 11:21:25 -07:00
Not all file-related modules consistently use "path" as the attribute to specify a single filename, some use "dest", others use "name". Most do have aliases for either "name" or "destfile". This change makes "path" the default attribute for (single) file-related modules, but also adds "dest" and "name" as aliases, so that people can use a consistent way of attributing paths, but also to ensure backward compatibility with existing playbooks. NOTE: The reason for changing this, is that it makes Ansible needlessly harder to use if you have to remember that e.g. the xattr module requires the name attribute, the lineinfile module requires a dest attribute, and the stat module requires a path attribute.
305 lines
10 KiB
Python
305 lines
10 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
|
|
# (c) 2015, Ales Nosek <anosek.nosek () gmail.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
ANSIBLE_METADATA = {'status': ['preview'],
|
|
'supported_by': 'community',
|
|
'version': '1.0'}
|
|
|
|
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.
|
|
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)"
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Before 2.3, option 'dest' was used instead of 'path'
|
|
- name: Ensure "fav=lemonade is in section "[drinks]" in specified file
|
|
ini_file:
|
|
path: /etc/conf
|
|
section: drinks
|
|
option: fav
|
|
value: lemonade
|
|
mode: 0600
|
|
backup: yes
|
|
|
|
- ini_file:
|
|
path: /etc/anotherconf
|
|
section: drinks
|
|
option: temperature
|
|
value: cold
|
|
backup: yes
|
|
'''
|
|
|
|
import os
|
|
import re
|
|
|
|
# import module snippets
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
# ==============================================================
|
|
# match_opt
|
|
|
|
def match_opt(option, line):
|
|
option = re.escape(option)
|
|
return re.match(' *%s( |\t)*=' % option, line) \
|
|
or re.match('# *%s( |\t)*=' % option, line) \
|
|
or re.match('; *%s( |\t)*=' % option, line)
|
|
|
|
# ==============================================================
|
|
# match_active_opt
|
|
|
|
def match_active_opt(option, line):
|
|
option = re.escape(option)
|
|
return re.match(' *%s( |\t)*=' % option, line)
|
|
|
|
# ==============================================================
|
|
# do_ini
|
|
|
|
def do_ini(module, filename, section=None, option=None, value=None,
|
|
state='present', backup=False, no_extra_spaces=False, create=False):
|
|
|
|
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) and not module.check_mode:
|
|
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)
|
|
|
|
# append a fake section line to simplify the logic
|
|
ini_lines.append('[')
|
|
|
|
within_section = not section
|
|
section_start = 0
|
|
changed = False
|
|
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)
|
|
changed = ini_lines[index] != newline
|
|
if changed:
|
|
msg = 'option changed'
|
|
ini_lines[index] = newline
|
|
if 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 and not module.check_mode:
|
|
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():
|
|
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
path = dict(required=True, aliases=['dest'], type='path'),
|
|
section = dict(required=True),
|
|
option = dict(required=False),
|
|
value = dict(required=False),
|
|
backup = dict(default='no', type='bool'),
|
|
state = dict(default='present', choices=['present', 'absent']),
|
|
no_extra_spaces = dict(required=False, default=False, type='bool'),
|
|
create=dict(default=True, type='bool')
|
|
),
|
|
add_file_common_args = True,
|
|
supports_check_mode = True
|
|
)
|
|
|
|
path = os.path.expanduser(module.params['path'])
|
|
section = module.params['section']
|
|
option = module.params['option']
|
|
value = module.params['value']
|
|
state = module.params['state']
|
|
backup = module.params['backup']
|
|
no_extra_spaces = module.params['no_extra_spaces']
|
|
create = module.params['create']
|
|
|
|
(changed,backup_file,diff,msg) = do_ini(module, 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(module.params)
|
|
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()
|