From 2ce7f136b2f07e28db9c93e700e4d74c587fb15f Mon Sep 17 00:00:00 2001 From: Joshua Lund Date: Tue, 4 Jun 2013 23:28:28 -0600 Subject: [PATCH] * Added a sha256 method to module_common * Added a sha256sum parameter to the get_url module to enable cryptographic verification of downloaded files * Fixed a few typos in the documentation --- lib/ansible/module_common.py | 25 ++++++++++++++++---- library/network/get_url | 45 +++++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py index a8083100ea0..f9838d3d92a 100644 --- a/lib/ansible/module_common.py +++ b/lib/ansible/module_common.py @@ -86,11 +86,18 @@ try: except ImportError: pass +HAVE_HASHLIB=False try: from hashlib import md5 as _md5 + HAVE_HASHLIB=True except ImportError: from md5 import md5 as _md5 +try: + from hashlib import sha256 as _sha256 +except ImportError: + pass + try: from systemd import journal has_journal = True @@ -787,13 +794,13 @@ class AnsibleModule(object): or stat.S_IXGRP & os.stat(path)[stat.ST_MODE] or stat.S_IXOTH & os.stat(path)[stat.ST_MODE]) - def md5(self, filename): - ''' Return MD5 hex digest of local file, or None if file is not present. ''' + def digest_from_file(self, filename, digest_method): + ''' Return hex digest of local file for a given digest_method, or None if file is not present. ''' if not os.path.exists(filename): return None if os.path.isdir(filename): - self.fail_json(msg="attempted to take md5sum of directory: %s" % filename) - digest = _md5() + self.fail_json(msg="attempted to take checksum of directory: %s" % filename) + digest = digest_method blocksize = 64 * 1024 infile = open(filename, 'rb') block = infile.read(blocksize) @@ -803,6 +810,16 @@ class AnsibleModule(object): infile.close() return digest.hexdigest() + def md5(self, filename): + ''' Return MD5 hex digest of local file using digest_from_file(). ''' + return self.digest_from_file(filename, _md5()) + + def sha256(self, filename): + ''' Return SHA-256 hex digest of local file using digest_from_file(). ''' + if not HAVE_HASHLIB: + self.fail_json(msg="SHA-256 checksums require hashlib, which is available in Python 2.5 and higher") + return self.digest_from_file(filename, _sha256()) + def backup_local(self, fn): '''make a date-marked backup of the specified file, return True or False on success or failure''' # backups named basename-YYYY-MM-DD@HH:MM~ diff --git a/library/network/get_url b/library/network/get_url index 74e908f2a26..479506cf2bd 100644 --- a/library/network/get_url +++ b/library/network/get_url @@ -33,7 +33,7 @@ description: server I(must) have direct access to the remote resource. - By default, if an environment variable C(_proxy) is set on the target host, requests will be sent through that proxy. This - behaviour can be overriden by setting a variable for this task + behaviour can be overridden by setting a variable for this task (see `setting the environment `_), or by using the use_proxy option. @@ -53,19 +53,26 @@ options: default: null force: description: - - if C(yes), will download the file every time and replace the + - If C(yes), will download the file every time and replace the file if the contents change. If C(no), the file will only be downloaded if the destination does not exist. Generally should be C(yes) only for small - local files. prior to 0.6, acts if C(yes) by default. + local files. Prior to 0.6, this module behaved as if C(yes) was the default. version_added: "0.7" required: false choices: [ "yes", "no" ] default: "no" aliases: [ "thirsty" ] + sha256sum: + description: + - If a SHA-256 checksum is passed to this parameter, the digest of the + destination file will be calculated after it is downloaded to ensure + its integrity and verify that the transfer completed successfully. + required: false + default: null use_proxy: description: - - if C(no), it will not use a proxy, even if one is defined by - in an environment variable on the target hosts. + - if C(no), it will not use a proxy, even if one is defined in + an environment variable on the target hosts. required: false default: 'yes' choices: ['yes', 'no'] @@ -81,20 +88,30 @@ author: Jan-Piet Mens ''' EXAMPLES=''' +# Download file.conf to /etc/foo.conf with read permissions set for the user and group get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf mode=0440 + +# Download file.conf to /etc/foo.conf and ensure that it matches the specified SHA-256 checksum +get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf sha256sum=b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c ''' -HAS_URLLIB2 = True +try: + import hashlib + HAS_HASHLIB=True +except ImportError: + HAS_HASHLIB=False + try: import urllib2 + HAS_URLLIB2 = True except ImportError: HAS_URLLIB2 = False -HAS_URLPARSE = True try: import urlparse import socket + HAS_URLPARSE = True except ImportError: HAS_URLPARSE=False @@ -217,6 +234,7 @@ def main(): url = dict(required=True), dest = dict(required=True), force = dict(default='no', aliases=['thirsty'], type='bool'), + sha256sum = dict(default=''), use_proxy = dict(default='yes', type='bool') ), add_file_common_args=True @@ -225,6 +243,7 @@ def main(): url = module.params['url'] dest = os.path.expanduser(module.params['dest']) force = module.params['force'] + sha256sum = module.params['sha256sum'] use_proxy = module.params['use_proxy'] if os.path.isdir(dest): @@ -273,6 +292,16 @@ def main(): else: changed = False + # Check the digest of the destination file and ensure that it matches the + # sha256sum parameter if it is present + if sha256sum != '': + if not HAS_HASHLIB: + os.remove(dest) + module.fail_json(msg="The sha256sum parameter requires hashlib, which is available in Python 2.5 and higher") + if sha256sum != module.sha256(dest): + os.remove(dest) + module.fail_json(msg="The SHA-256 checksum for %s did not match %s" % (dest, sha256sum)) + os.remove(tmpsrc) # allow file attribute changes @@ -283,7 +312,7 @@ def main(): # Mission complete module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src, - changed=changed, msg=info.get('msg', '')) + sha256sum=sha256sum, changed=changed, msg=info.get('msg', '')) # this is magic, see lib/ansible/module_common.py #<>