mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
008dc4831e
Added path expanding for dest in lineinfile Added common file arguments to lineinfile so the module gets owner, group, mode and SE options. Decorated existing example to demonstate usage of file options and added a couple more examples Message is not set accordingly when file attributes were changed 'absent' handling code now handles the case where the file doesn't exists (was issuing a Traceback before) File attribute handling code has been added to the 'absent' handling function too. File attributes handling has been grouped in 'def check_file' since it's required in both places. 'absent' mode now returns a message like it's counterpart 'present', telling if file attributes were altered and if lines have matched.
269 lines
9.3 KiB
Python
269 lines
9.3 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.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/>.
|
|
|
|
import re
|
|
import os
|
|
|
|
DOCUMENTATION = """
|
|
---
|
|
module: lineinfile
|
|
author: Daniel Hokka Zakrisson
|
|
short_description: Ensure a particular line is in a file
|
|
description:
|
|
- This module will search a file for a line, and ensure that it is present or absent.
|
|
- This is primarily useful when you want to change a single line in a
|
|
file only. For other cases, see the M(copy) or M(template) modules.
|
|
version_added: "0.7"
|
|
options:
|
|
dest:
|
|
required: true
|
|
aliases: [ name, destfile ]
|
|
description:
|
|
- The file to modify
|
|
regexp:
|
|
required: true
|
|
description:
|
|
- The regular expression to look for in the file. For C(state=present),
|
|
the pattern to replace. For C(state=absent), the pattern of the line
|
|
to remove. Uses Python regular expressions; see
|
|
U(http://docs.python.org/2/library/re.html).
|
|
state:
|
|
required: false
|
|
choices: [ present, absent ]
|
|
default: "present"
|
|
aliases: []
|
|
description:
|
|
- Whether the line should be there or not.
|
|
line:
|
|
required: false
|
|
description:
|
|
- Required for C(state=present). The line to insert/replace into the
|
|
file. Must match the value given to I(regexp).
|
|
insertafter:
|
|
required: false
|
|
default: EOF
|
|
description:
|
|
- Used with C(state=present). If specified, the line will be inserted
|
|
after the specified regular expression. A special value is
|
|
available; C(EOF) for inserting the line at the end of the file.
|
|
choices: [ 'EOF', '*regex*' ]
|
|
insertbefore:
|
|
required: false
|
|
versionadded: 1.1
|
|
description:
|
|
- Used with C(state=present). If specified, the line will be inserted
|
|
before the specified regular expression. A value is available;
|
|
C(BOF) for inserting the line at the beginning of the
|
|
file.
|
|
choices: [ 'BOF', '*regex*' ]
|
|
create:
|
|
required: false
|
|
choices: [ yes, no ]
|
|
default: no
|
|
description:
|
|
- Used with C(state=present). If specified, the file will be created
|
|
if it does not already exist. By default it will fail if the file
|
|
is missing.
|
|
backup:
|
|
required: false
|
|
default: no
|
|
description:
|
|
- Create a backup file including the timestamp information so you can
|
|
get the original file back if you somehow clobbered it incorrectly.
|
|
others:
|
|
description:
|
|
- All arguments accepted by the M(file) module also work here. If you
|
|
use file arguments with C(state=absent) and the file exists, it's perms,
|
|
ownership or SE linux context will be updated if needed.
|
|
required: false
|
|
examples:
|
|
- code: 'lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled'
|
|
- code: 'lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel"'
|
|
- code: "lineinfile: dest=/etc/host regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644"
|
|
- code: 'lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080"'
|
|
- code: 'lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default"'
|
|
- code: "lineinfile: \\\"dest=/etc/sudoers state=present regexp='^%wheel' line ='%wheel ALL=(ALL) NOPASSWD: ALL'\\\""
|
|
"""
|
|
|
|
|
|
def check_file(module, changed, message=""):
|
|
|
|
file_args = module.load_file_common_arguments(module.params)
|
|
if module.set_file_attributes_if_different(file_args, False):
|
|
if changed:
|
|
message += " and "
|
|
changed = True
|
|
message += "ownership, perms or SE linux context changed"
|
|
|
|
return [ message, changed ]
|
|
|
|
def present(module, dest, regexp, line, insertafter, insertbefore, create, backup):
|
|
|
|
if os.path.isdir(dest):
|
|
module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
|
|
elif not os.path.exists(dest):
|
|
if not create:
|
|
module.fail_json(rc=257, msg='Destination %s does not exist !' % dest)
|
|
destpath = os.path.dirname(dest)
|
|
if not os.path.exists(destpath):
|
|
os.makedirs(destpath)
|
|
lines = []
|
|
else:
|
|
f = open(dest, 'rb')
|
|
lines = f.readlines()
|
|
f.close()
|
|
|
|
mre = re.compile(regexp)
|
|
if not mre.search(line):
|
|
module.fail_json(msg="usage error: line= doesn't match regexp (%s)" % regexp)
|
|
|
|
if insertafter is not None and insertafter not in ('BOF', 'EOF'):
|
|
insre = re.compile(insertafter)
|
|
elif insertbefore is not None and insertbefore not in ('BOF'):
|
|
insre = re.compile(insertbefore)
|
|
else:
|
|
insre = None
|
|
|
|
# index[0] is the line num where regexp has been found
|
|
# index[1] is the line num where insertafter/inserbefore has been found
|
|
index = [-1, -1]
|
|
for lineno in range(0, len(lines)):
|
|
if mre.search(lines[lineno]):
|
|
index[0] = lineno
|
|
elif insre is not None and insre.search(lines[lineno]):
|
|
if insertafter:
|
|
# + 1 for the next line
|
|
index[1] = lineno + 1
|
|
if insertbefore:
|
|
# + 1 for the previous line
|
|
index[1] = lineno
|
|
|
|
# Regexp matched a line in the file
|
|
if index[0] != -1:
|
|
if lines[index[0]] == line + os.linesep:
|
|
msg = ''
|
|
changed = False
|
|
else:
|
|
lines[index[0]] = line + os.linesep
|
|
msg = 'line replaced'
|
|
changed = True
|
|
# Add it to the beginning of the file
|
|
elif insertbefore == 'BOF' or insertafter == 'BOF':
|
|
lines.insert(0, line + os.linesep)
|
|
msg = 'line added'
|
|
changed = True
|
|
# Add it to the end of the file if requested or
|
|
# if insertafter=/insertbefore didn't match anything
|
|
# (so default behaviour is to add at the end)
|
|
elif insertafter == 'EOF' or index[1] == -1:
|
|
lines.append(line + os.linesep)
|
|
msg = 'line added'
|
|
changed = True
|
|
# insertafter/insertbefore= matched
|
|
else:
|
|
lines.insert(index[1], line + os.linesep)
|
|
msg = 'line added'
|
|
changed = True
|
|
|
|
file_args = module.load_file_common_arguments(module.params)
|
|
changed = module.set_file_attributes_if_different(file_args, changed)
|
|
|
|
if changed and not module.check_mode:
|
|
if backup and os.path.exists(dest):
|
|
module.backup_local(dest)
|
|
f = open(dest, 'wb')
|
|
f.writelines(lines)
|
|
f.close()
|
|
|
|
[ msg, changed ] = check_file(module, changed, msg)
|
|
module.exit_json(changed=changed, msg=msg)
|
|
|
|
def absent(module, dest, regexp, backup):
|
|
|
|
if os.path.isdir(dest):
|
|
module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
|
|
elif not os.path.exists(dest):
|
|
module.exit_json(changed=False, msg="file not present")
|
|
|
|
msg = ""
|
|
f = open(dest, 'rb')
|
|
lines = f.readlines()
|
|
f.close()
|
|
cre = re.compile(regexp)
|
|
found = []
|
|
|
|
def matcher(line):
|
|
if cre.search(line):
|
|
found.append(line)
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
lines = filter(matcher, lines)
|
|
changed = len(found) > 0
|
|
if changed and not module.check_mode:
|
|
if backup:
|
|
module.backup_local(dest)
|
|
f = open(dest, 'wb')
|
|
f.writelines(lines)
|
|
f.close()
|
|
|
|
if changed:
|
|
msg = "%s line(s) removed" % len(found)
|
|
|
|
[ msg, changed ] = check_file(module, changed, msg)
|
|
|
|
module.exit_json(changed=changed, found=len(found), msg=msg)
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
dest=dict(required=True, aliases=['name', 'destfile']),
|
|
state=dict(default='present', choices=['absent', 'present']),
|
|
regexp=dict(required=True),
|
|
line=dict(aliases=['value']),
|
|
insertafter=dict(default=None),
|
|
insertbefore=dict(default=None),
|
|
create=dict(default=False, choices=BOOLEANS),
|
|
backup=dict(default=False, choices=BOOLEANS),
|
|
),
|
|
mutually_exclusive = [['insertbefore', 'insertafter']],
|
|
add_file_common_args=True,
|
|
supports_check_mode = True
|
|
)
|
|
|
|
params = module.params
|
|
create = module.boolean(module.params.get('create', False))
|
|
backup = module.boolean(module.params.get('backup', False))
|
|
dest = os.path.expanduser(params['dest'])
|
|
|
|
if params['state'] == 'present':
|
|
if 'line' not in params:
|
|
module.fail_json(msg='line= is required with state=present')
|
|
present(module, dest, params['regexp'], params['line'],
|
|
params['insertafter'], params['insertbefore'], create, backup)
|
|
else:
|
|
absent(module, dest, params['regexp'], backup)
|
|
|
|
# this is magic, see lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
|
|
main()
|
|
|