From 592e30085144241b34781b3acedc861069f7764d Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Thu, 13 Aug 2015 20:09:55 +0900 Subject: [PATCH] New module: blockinfile --- files/blockinfile.py | 292 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 files/blockinfile.py diff --git a/files/blockinfile.py b/files/blockinfile.py new file mode 100644 index 00000000000..a8499547639 --- /dev/null +++ b/files/blockinfile.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, 2015 YAEGASHI Takeshi +# +# 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 . + +import re +import os +import tempfile + +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. +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. +""" + +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 line + blockinfile: + dest: /var/www/html/index.html + marker: "" + insertafter: "" + content: | +

Welcome to {{ansible_hostname}}

+

Last updated on {{ansible_date_time.iso8601}}

+ +- name: remove HTML as well as surrounding markers + blockinfile: + dest: /var/www/html/index.html + marker: "" + content: "" +""" + + +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']), + 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 = os.path.expanduser(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 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)+'\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()