From 8383c1c8d340f17242f2dcb2b5242fb31fb3b3c2 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Mon, 9 Feb 2015 01:00:06 +0100 Subject: [PATCH] Add new module "patch" --- lib/ansible/modules/extras/files/__init__.py | 0 lib/ansible/modules/extras/files/patch.py | 150 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 lib/ansible/modules/extras/files/__init__.py create mode 100644 lib/ansible/modules/extras/files/patch.py diff --git a/lib/ansible/modules/extras/files/__init__.py b/lib/ansible/modules/extras/files/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/extras/files/patch.py b/lib/ansible/modules/extras/files/patch.py new file mode 100644 index 00000000000..e2e5467f36e --- /dev/null +++ b/lib/ansible/modules/extras/files/patch.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2012, Luis Alberto Perez Lazaro +# (c) 2015, Jakub Jirutka +# +# 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 . + +DOCUMENTATION = ''' +--- +module: patch +author: Luis Alberto Perez Lazaro, Jakub Jirutka +version_added: 1.8 +short_description: Apply patch files using the GNU patch tool. +options: + basedir: + description: + - Path of a base directory in which the patch file will be applied. + May be ommitted when C(dest) option is specified, otherwise required. + required: false + dest: + description: + - Path of the file on the remote machine to be patched. + - The names of the files to be patched are usually taken from the patch + file, but if there's just one file to be patched it can specified with + this option. + required: false + aliases: [ "originalfile" ] + src: + description: + - Path of the patch file on the remote machine as accepted by the GNU + patch tool. + required: true + aliases: [ "patchfile" ] + strip: + description: + - Number that indicates the smallest prefix containing leading slashes + that will be stripped from each file name found in the patch file. + For more information see the strip parameter of the GNU patch tool. + required: false + type: "int" + default: "0" +note: + - This module requires GNU I(patch) utility to be installed on the remote host. +''' + +EXAMPLES = ''' +- name: apply patch to one file + patch: > + src=/tmp/index.html.patch + dest=/var/www/index.html + +- name: apply patch to multiple files under basedir + patch: > + src=/tmp/customize.patch + basedir=/var/www + strip=1 +''' + +import os +from os import path, R_OK, W_OK + + +class PatchError(Exception): + pass + + +def is_already_applied(patch_func, patch_file, basedir, dest_file=None, strip=0): + opts = ['--quiet', '--reverse', '--forward', '--dry-run', + "--strip=%s" % strip, "--directory='%s'" % basedir, + "--input='%s'" % patch_file] + if dest_file: + opts.append("'%s'" % dest_file) + + (rc, _, _) = patch_func(opts) + return rc == 0 + + +def apply_patch(patch_func, patch_file, basedir, dest_file=None, strip=0, dry_run=False): + opts = ['--quiet', '--forward', '--batch', '--reject-file=-', + "--strip=%s" % strip, "--directory='%s'" % basedir, + "--input='%s'" % patch_file] + if dry_run: + opts.append('--dry-run') + if dest_file: + opts.append("'%s'" % dest_file) + + (rc, out, err) = patch_func(opts) + if rc != 0: + msg = out if not err else err + raise PatchError(msg) + + +def main(): + module = AnsibleModule( + argument_spec={ + 'src': {'required': True, 'aliases': ['patchfile']}, + 'dest': {'aliases': ['originalfile']}, + 'basedir': {}, + 'strip': {'default': 0, 'type': 'int'} + }, + required_one_of=[['dest', 'basedir']], + supports_check_mode=True + ) + + # Create type object as namespace for module params + p = type('Params', (), module.params) + + if not os.access(p.src, R_OK): + module.fail_json(msg="src %s doesn't exist or not readable" % (p.src)) + + if p.dest and not os.access(p.dest, W_OK): + module.fail_json(msg="dest %s doesn't exist or not writable" % (d.dest)) + + if p.basedir and not path.exists(p.basedir): + module.fail_json(msg="basedir %s doesn't exist" % (p.basedir)) + + if not p.basedir: + p.basedir = path.dirname(p.dest) + + patch_bin = module.get_bin_path('patch') + patch_func = lambda opts: module.run_command("%s %s" % (patch_bin, ' '.join(opts))) + + changed = False + if not is_already_applied(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip): + try: + apply_patch(patch_func, p.src, p.basedir, dest_file=p.dest, strip=p.strip, + dry_run=module.check_mode) + changed = True + except PatchError, e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +main()