b6b04795c3
If we don't have an existing file, original ends up as None. Bug introduced in 70fa125
313 lines
9.5 KiB
Python
Executable file
313 lines
9.5 KiB
Python
Executable file
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2014, 2015 YAEGASHI Takeshi <yaegashi@debian.org>
|
|
#
|
|
# 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/>.
|
|
|
|
DOCUMENTATION = """
|
|
---
|
|
module: blockinfile
|
|
author:
|
|
- 'YAEGASHI Takeshi (@yaegashi)'
|
|
extends_documentation_fragment:
|
|
- files
|
|
- validate
|
|
short_description: Insert/update/remove a text block
|
|
surrounded by marker lines.
|
|
version_added: '2.0'
|
|
description:
|
|
- This module will insert/update/remove a block of multi-line text
|
|
surrounded by customizable marker lines.
|
|
notes:
|
|
- This module supports check mode.
|
|
- When using 'with_*' loops be aware that if you do not set a unique mark the block will be overwritten on each iteration.
|
|
options:
|
|
dest:
|
|
aliases: [ name, destfile ]
|
|
required: true
|
|
description:
|
|
- The file to modify.
|
|
state:
|
|
required: false
|
|
choices: [ present, absent ]
|
|
default: present
|
|
description:
|
|
- Whether the block should be there or not.
|
|
marker:
|
|
required: false
|
|
default: '# {mark} ANSIBLE MANAGED BLOCK'
|
|
description:
|
|
- The marker line template.
|
|
"{mark}" will be replaced with "BEGIN" or "END".
|
|
block:
|
|
aliases: [ content ]
|
|
required: false
|
|
default: ''
|
|
description:
|
|
- The text to insert inside the marker lines.
|
|
If it's missing or an empty string,
|
|
the block will be removed as if C(state) were specified to C(absent).
|
|
insertafter:
|
|
required: false
|
|
default: EOF
|
|
description:
|
|
- If specified, the block will be inserted after the last match of
|
|
specified regular expression. A special value is available; C(EOF) for
|
|
inserting the block at the end of the file. If specified regular
|
|
expresion has no matches, C(EOF) will be used instead.
|
|
choices: [ 'EOF', '*regex*' ]
|
|
insertbefore:
|
|
required: false
|
|
default: None
|
|
description:
|
|
- If specified, the block will be inserted before the last match of
|
|
specified regular expression. A special value is available; C(BOF) for
|
|
inserting the block at the beginning of the file. If specified regular
|
|
expresion has no matches, the block will be inserted at the end of the
|
|
file.
|
|
choices: [ 'BOF', '*regex*' ]
|
|
create:
|
|
required: false
|
|
default: 'no'
|
|
choices: [ 'yes', 'no' ]
|
|
description:
|
|
- Create a new file if it doesn't exist.
|
|
backup:
|
|
required: false
|
|
default: 'no'
|
|
choices: [ 'yes', 'no' ]
|
|
description:
|
|
- Create a backup file including the timestamp information so you can
|
|
get the original file back if you somehow clobbered it incorrectly.
|
|
follow:
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
description:
|
|
- 'This flag indicates that filesystem links, if they exist, should be followed.'
|
|
version_added: "2.1"
|
|
"""
|
|
|
|
EXAMPLES = r"""
|
|
- name: insert/update "Match User" configuation block in /etc/ssh/sshd_config
|
|
blockinfile:
|
|
dest: /etc/ssh/sshd_config
|
|
block: |
|
|
Match User ansible-agent
|
|
PasswordAuthentication no
|
|
|
|
- name: insert/update eth0 configuration stanza in /etc/network/interfaces
|
|
(it might be better to copy files into /etc/network/interfaces.d/)
|
|
blockinfile:
|
|
dest: /etc/network/interfaces
|
|
block: |
|
|
iface eth0 inet static
|
|
address 192.168.0.1
|
|
netmask 255.255.255.0
|
|
|
|
- name: insert/update HTML surrounded by custom markers after <body> line
|
|
blockinfile:
|
|
dest: /var/www/html/index.html
|
|
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK -->"
|
|
insertafter: "<body>"
|
|
content: |
|
|
<h1>Welcome to {{ansible_hostname}}</h1>
|
|
<p>Last updated on {{ansible_date_time.iso8601}}</p>
|
|
|
|
- name: remove HTML as well as surrounding markers
|
|
blockinfile:
|
|
dest: /var/www/html/index.html
|
|
marker: "<!-- {mark} ANSIBLE MANAGED BLOCK -->"
|
|
content: ""
|
|
|
|
- name: insert/update "Match User" configuation block in /etc/ssh/sshd_config
|
|
blockinfile:
|
|
dest: /etc/hosts
|
|
block: |
|
|
{{item.name}} {{item.ip}}
|
|
marker: "# {mark} ANSIBLE MANAGED BLOCK {{item.name}}"
|
|
with_items:
|
|
- { name: host1, ip: 10.10.1.10 }
|
|
- { name: host2, ip: 10.10.1.11 }
|
|
- { name: host3, ip: 10.10.1.12 }
|
|
"""
|
|
|
|
import re
|
|
import os
|
|
import tempfile
|
|
|
|
|
|
def write_changes(module, contents, dest):
|
|
|
|
tmpfd, tmpfile = tempfile.mkstemp()
|
|
f = os.fdopen(tmpfd, 'wb')
|
|
f.write(contents)
|
|
f.close()
|
|
|
|
validate = module.params.get('validate', None)
|
|
valid = not validate
|
|
if validate:
|
|
if "%s" not in validate:
|
|
module.fail_json(msg="validate must contain %%s: %s" % (validate))
|
|
(rc, out, err) = module.run_command(validate % tmpfile)
|
|
valid = rc == 0
|
|
if rc != 0:
|
|
module.fail_json(msg='failed to validate: '
|
|
'rc:%s error:%s' % (rc, err))
|
|
if valid:
|
|
module.atomic_move(tmpfile, dest)
|
|
|
|
|
|
def check_file_attrs(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 main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
dest=dict(required=True, aliases=['name', 'destfile'], type='path'),
|
|
state=dict(default='present', choices=['absent', 'present']),
|
|
marker=dict(default='# {mark} ANSIBLE MANAGED BLOCK', type='str'),
|
|
block=dict(default='', type='str', aliases=['content']),
|
|
insertafter=dict(default=None),
|
|
insertbefore=dict(default=None),
|
|
create=dict(default=False, type='bool'),
|
|
backup=dict(default=False, type='bool'),
|
|
validate=dict(default=None, type='str'),
|
|
),
|
|
mutually_exclusive=[['insertbefore', 'insertafter']],
|
|
add_file_common_args=True,
|
|
supports_check_mode=True
|
|
)
|
|
|
|
params = module.params
|
|
dest = params['dest']
|
|
if module.boolean(params.get('follow', None)):
|
|
dest = os.path.realpath(dest)
|
|
|
|
if os.path.isdir(dest):
|
|
module.fail_json(rc=256,
|
|
msg='Destination %s is a directory !' % dest)
|
|
|
|
if not os.path.exists(dest):
|
|
if not module.boolean(params['create']):
|
|
module.fail_json(rc=257,
|
|
msg='Destination %s does not exist !' % dest)
|
|
original = None
|
|
lines = []
|
|
else:
|
|
f = open(dest, 'rb')
|
|
original = f.read()
|
|
f.close()
|
|
lines = original.splitlines()
|
|
|
|
insertbefore = params['insertbefore']
|
|
insertafter = params['insertafter']
|
|
block = params['block']
|
|
marker = params['marker']
|
|
present = params['state'] == 'present'
|
|
|
|
if insertbefore is None and insertafter is None:
|
|
insertafter = 'EOF'
|
|
|
|
if insertafter not in (None, 'EOF'):
|
|
insertre = re.compile(insertafter)
|
|
elif insertbefore not in (None, 'BOF'):
|
|
insertre = re.compile(insertbefore)
|
|
else:
|
|
insertre = None
|
|
|
|
marker0 = re.sub(r'{mark}', 'BEGIN', marker)
|
|
marker1 = re.sub(r'{mark}', 'END', marker)
|
|
if present and block:
|
|
# Escape seqeuences like '\n' need to be handled in Ansible 1.x
|
|
if module.ansible_version.startswith('1.'):
|
|
block = re.sub('', block, '')
|
|
blocklines = [marker0] + block.splitlines() + [marker1]
|
|
else:
|
|
blocklines = []
|
|
|
|
n0 = n1 = None
|
|
for i, line in enumerate(lines):
|
|
if line.startswith(marker0):
|
|
n0 = i
|
|
if line.startswith(marker1):
|
|
n1 = i
|
|
|
|
if None in (n0, n1):
|
|
n0 = None
|
|
if insertre is not None:
|
|
for i, line in enumerate(lines):
|
|
if insertre.search(line):
|
|
n0 = i
|
|
if n0 is None:
|
|
n0 = len(lines)
|
|
elif insertafter is not None:
|
|
n0 += 1
|
|
elif insertbefore is not None:
|
|
n0 = 0 # insertbefore=BOF
|
|
else:
|
|
n0 = len(lines) # insertafter=EOF
|
|
elif n0 < n1:
|
|
lines[n0:n1+1] = []
|
|
else:
|
|
lines[n1:n0+1] = []
|
|
n0 = n1
|
|
|
|
lines[n0:n0] = blocklines
|
|
|
|
if lines:
|
|
result = '\n'.join(lines)
|
|
if original and original.endswith('\n'):
|
|
result += '\n'
|
|
else:
|
|
result = ''
|
|
if original == result:
|
|
msg = ''
|
|
changed = False
|
|
elif original is None:
|
|
msg = 'File created'
|
|
changed = True
|
|
elif not blocklines:
|
|
msg = 'Block removed'
|
|
changed = True
|
|
else:
|
|
msg = 'Block inserted'
|
|
changed = True
|
|
|
|
if changed and not module.check_mode:
|
|
if module.boolean(params['backup']) and os.path.exists(dest):
|
|
module.backup_local(dest)
|
|
write_changes(module, result, dest)
|
|
|
|
msg, changed = check_file_attrs(module, changed, msg)
|
|
module.exit_json(changed=changed, msg=msg)
|
|
|
|
# import module snippets
|
|
from ansible.module_utils.basic import *
|
|
from ansible.module_utils.splitter import *
|
|
if __name__ == '__main__':
|
|
main()
|