From 2c8b761a656a03d93928e57413aa8cfc2695111c Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Tue, 12 Nov 2013 13:30:18 -0600 Subject: [PATCH] Added replace module Heavily based on existing lineinfile module, but where it literally tests a regexp against *each individual line* of a file, this replace module is more analogous to common uses of a `sed` or `perl` match + replacement of all instances of a pattern anywhere in the file. Was debating adding `all` boolean or `count` numeric options to control how many replacements to make in the destfile (vs currently replacing all instances) Noted use of MULTILINE mode in docs, per suggestion from @jarv --- files/replace | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 files/replace diff --git a/files/replace b/files/replace new file mode 100644 index 00000000000..b008d1b39db --- /dev/null +++ b/files/replace @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2013, Evan Kaufman . + +import re +import os +import tempfile + +DOCUMENTATION = """ +--- +module: replace +author: Evan Kaufman +short_description: Replace all instances of a particular string in a + file using a back-referenced regular expression. +description: + - This module will replace all instances of a pattern within a file. + - It is up to the user to maintain idempotence by ensuring that the + same pattern would never match any replacements made. +version_added: "1.4" +options: + dest: + required: true + aliases: [ name, destfile ] + description: + - The file to modify. + regexp: + required: true + description: + - The regular expression to look for in the contents of the file. + Uses Python regular expressions; see + U(http://docs.python.org/2/library/re.html). + Uses multiline mode, which means C(^) and C($) match the beginning + and end respectively of I(each line) of the file. + replace: + required: false + description: + - The string to replace regexp matches. May contain backreferences + that will get expanded with the regexp capture groups if the regexp + matches. If not set, matches are removed entirely. + 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. + validate: + required: false + description: + - validation to run before copying into place + required: false + default: None + others: + description: + - All arguments accepted by the M(file) module also work here. + required: false +""" + +EXAMPLES = r""" +- replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes + +- replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644 + +- replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t' +""" + +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: + (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']), + regexp=dict(required=True), + replace=dict(default='', type='str'), + backup=dict(default=False, type='bool'), + validate=dict(default=None, type='str'), + ), + add_file_common_args=True, + supports_check_mode=True + ) + + params = module.params + dest = os.path.expanduser(params['dest']) + + if os.path.isdir(dest): + module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) + + if not os.path.exists(dest): + module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) + else: + f = open(dest, 'rb') + contents = f.read() + f.close() + + mre = re.compile(params['regexp'], re.MULTILINE) + result = re.subn(mre, params['replace'], contents, 0) + + if result[1] > 0: + msg = '%s replacements made' % result[1] + changed = True + else: + msg = '' + changed = False + + if changed and not module.check_mode: + if params['backup'] and os.path.exists(dest): + module.backup_local(dest) + write_changes(module, result[0], dest) + + msg, changed = check_file_attrs(module, changed, msg) + module.exit_json(changed=changed, msg=msg) + +# this is magic, see lib/ansible/module_common.py +#<> + +main()