Updates to archive module based on code review (#2699)

* Use common file arguments on destination file
* Rename 'compression' to 'format' h/t @abadger
* Add support for plain 'tar' format
* Ensure check_mode is respected
This commit is contained in:
Benjamin Doherty 2016-09-13 12:40:07 -04:00 committed by Adrian Likins
parent c07516bd4b
commit c0d77be491

View file

@ -1,6 +1,10 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# import module snippets
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.pycompat24 import get_exception
""" """
(c) 2016, Ben Doherty <bendohmv@gmail.com> (c) 2016, Ben Doherty <bendohmv@gmail.com>
Sponsored by Oomph, Inc. http://www.oomphinc.com Sponsored by Oomph, Inc. http://www.oomphinc.com
@ -34,7 +38,7 @@ options:
description: description:
- Remote absolute path, glob, or list of paths or globs for the file or files to compress or archive. - Remote absolute path, glob, or list of paths or globs for the file or files to compress or archive.
required: true required: true
compression: format:
description: description:
- The type of compression to use. Can be 'gz', 'bz2', or 'zip'. - The type of compression to use. Can be 'gz', 'bz2', or 'zip'.
choices: [ 'gz', 'bz2', 'zip' ] choices: [ 'gz', 'bz2', 'zip' ]
@ -65,7 +69,7 @@ EXAMPLES = '''
- archive: path=/path/to/foo remove=True - archive: path=/path/to/foo remove=True
# Create a zip archive of /path/to/foo # Create a zip archive of /path/to/foo
- archive: path=/path/to/foo compression=zip - archive: path=/path/to/foo format=zip
# Create a bz2 archive of multiple files, rooted at /path # Create a bz2 archive of multiple files, rooted at /path
- archive: - archive:
@ -73,7 +77,7 @@ EXAMPLES = '''
- /path/to/foo - /path/to/foo
- /path/wong/foo - /path/wong/foo
dest: /path/file.tar.bz2 dest: /path/file.tar.bz2
compression: bz2 format: bz2
''' '''
RETURN = ''' RETURN = '''
@ -102,9 +106,8 @@ expanded_paths:
type: list type: list
''' '''
import stat
import os import os
import errno import re
import glob import glob
import shutil import shutil
import gzip import gzip
@ -117,8 +120,8 @@ def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec = dict(
path = dict(type='list', required=True), path = dict(type='list', required=True),
compression = dict(choices=['gz', 'bz2', 'zip'], default='gz', required=False), format = dict(choices=['gz', 'bz2', 'zip', 'tar'], default='gz', required=False),
dest = dict(required=False), dest = dict(required=False, type='path'),
remove = dict(required=False, default=False, type='bool'), remove = dict(required=False, default=False, type='bool'),
), ),
add_file_common_args=True, add_file_common_args=True,
@ -126,11 +129,13 @@ def main():
) )
params = module.params params = module.params
check_mode = module.check_mode
paths = params['path'] paths = params['path']
dest = params['dest'] dest = params['dest']
remove = params['remove'] remove = params['remove']
expanded_paths = [] expanded_paths = []
compression = params['compression'] format = params['format']
globby = False globby = False
changed = False changed = False
state = 'absent' state = 'absent'
@ -140,11 +145,16 @@ def main():
successes = [] successes = []
for i, path in enumerate(paths): for i, path in enumerate(paths):
path = os.path.expanduser(path) path = os.path.expanduser(os.path.expandvars(path))
# Detect glob-like characters # Expand any glob characters. If found, add the expanded glob to the
if any((c in set('*?')) for c in path): # list of expanded_paths, which might be empty.
if ('*' in path or '?' in path):
expanded_paths = expanded_paths + glob.glob(path) expanded_paths = expanded_paths + glob.glob(path)
globby = True
# If there are no glob characters the path is added to the expanded paths
# whether the path exists or not
else: else:
expanded_paths.append(path) expanded_paths.append(path)
@ -156,11 +166,9 @@ def main():
archive = globby or os.path.isdir(expanded_paths[0]) or len(expanded_paths) > 1 archive = globby or os.path.isdir(expanded_paths[0]) or len(expanded_paths) > 1
# Default created file name (for single-file archives) to # Default created file name (for single-file archives) to
# <file>.<compression> # <file>.<format>
if dest: if not dest and not archive:
dest = os.path.expanduser(dest) dest = '%s.%s' % (expanded_paths[0], format)
elif not archive:
dest = '%s.%s' % (expanded_paths[0], compression)
# Force archives to specify 'dest' # Force archives to specify 'dest'
if archive and not dest: if archive and not dest:
@ -168,7 +176,6 @@ def main():
archive_paths = [] archive_paths = []
missing = [] missing = []
exclude = []
arcroot = '' arcroot = ''
for path in expanded_paths: for path in expanded_paths:
@ -177,7 +184,7 @@ def main():
if arcroot == '': if arcroot == '':
arcroot = os.path.dirname(path) + os.sep arcroot = os.path.dirname(path) + os.sep
else: else:
for i in xrange(len(arcroot)): for i in range(len(arcroot)):
if path[i] != arcroot[i]: if path[i] != arcroot[i]:
break break
@ -198,7 +205,7 @@ def main():
# No source files were found but the named archive exists: are we 'compress' or 'archive' now? # No source files were found but the named archive exists: are we 'compress' or 'archive' now?
if len(missing) == len(expanded_paths) and dest and os.path.exists(dest): if len(missing) == len(expanded_paths) and dest and os.path.exists(dest):
# Just check the filename to know if it's an archive or simple compressed file # Just check the filename to know if it's an archive or simple compressed file
if re.search(r'(\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(dest), re.IGNORECASE): if re.search(r'(\.tar|\.tar\.gz|\.tgz|.tbz2|\.tar\.bz2|\.zip)$', os.path.basename(dest), re.IGNORECASE):
state = 'archive' state = 'archive'
else: else:
state = 'compress' state = 'compress'
@ -221,77 +228,84 @@ def main():
size = os.path.getsize(dest) size = os.path.getsize(dest)
if state != 'archive': if state != 'archive':
try: if check_mode:
changed = True
# Slightly more difficult (and less efficient!) compression using zipfile module else:
if compression == 'zip': try:
arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) # Slightly more difficult (and less efficient!) compression using zipfile module
if format == 'zip':
arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED)
# Easier compression using tarfile module # Easier compression using tarfile module
elif compression == 'gz' or compression == 'bz2': elif format == 'gz' or format == 'bz2':
arcfile = tarfile.open(dest, 'w|' + compression) arcfile = tarfile.open(dest, 'w|' + format)
for path in archive_paths: # Or plain tar archiving
if os.path.isdir(path): elif format == 'tar':
# Recurse into directories arcfile = tarfile.open(dest, 'w')
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if not dirpath.endswith(os.sep):
dirpath += os.sep
for dirname in dirnames: for path in archive_paths:
fullpath = dirpath + dirname if os.path.isdir(path):
arcname = fullpath[len(arcroot):] # Recurse into directories
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if not dirpath.endswith(os.sep):
dirpath += os.sep
try: for dirname in dirnames:
if compression == 'zip': fullpath = dirpath + dirname
arcfile.write(fullpath, arcname) arcname = fullpath[len(arcroot):]
else:
arcfile.add(fullpath, arcname, recursive=False)
except Exception:
e = get_exception()
errors.append('%s: %s' % (fullpath, str(e)))
for filename in filenames:
fullpath = dirpath + filename
arcname = fullpath[len(arcroot):]
if not filecmp.cmp(fullpath, dest):
try: try:
if compression == 'zip': if format == 'zip':
arcfile.write(fullpath, arcname) arcfile.write(fullpath, arcname)
else: else:
arcfile.add(fullpath, arcname, recursive=False) arcfile.add(fullpath, arcname, recursive=False)
successes.append(fullpath)
except Exception: except Exception:
e = get_exception() e = get_exception()
errors.append('Adding %s: %s' % (path, str(e))) errors.append('%s: %s' % (fullpath, str(e)))
else:
if compression == 'zip': for filename in filenames:
arcfile.write(path, path[len(arcroot):]) fullpath = dirpath + filename
arcname = fullpath[len(arcroot):]
if not filecmp.cmp(fullpath, dest):
try:
if format == 'zip':
arcfile.write(fullpath, arcname)
else:
arcfile.add(fullpath, arcname, recursive=False)
successes.append(fullpath)
except Exception:
e = get_exception()
errors.append('Adding %s: %s' % (path, str(e)))
else: else:
arcfile.add(path, path[len(arcroot):], recursive=False) if format == 'zip':
arcfile.write(path, path[len(arcroot):])
else:
arcfile.add(path, path[len(arcroot):], recursive=False)
successes.append(path) successes.append(path)
except Exception: except Exception:
e = get_exception() e = get_exception()
return module.fail_json(msg='Error when writing %s archive at %s: %s' % (compression == 'zip' and 'zip' or ('tar.' + compression), dest, str(e))) return module.fail_json(msg='Error when writing %s archive at %s: %s' % (format == 'zip' and 'zip' or ('tar.' + format), dest, str(e)))
if arcfile: if arcfile:
arcfile.close() arcfile.close()
state = 'archive' state = 'archive'
if len(errors) > 0: if len(errors) > 0:
module.fail_json(msg='Errors when writing archive at %s: %s' % (dest, '; '.join(errors))) module.fail_json(msg='Errors when writing archive at %s: %s' % (dest, '; '.join(errors)))
if state in ['archive', 'incomplete'] and remove: if state in ['archive', 'incomplete'] and remove:
for path in successes: for path in successes:
try: try:
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
else: elif not check_mode:
os.remove(path) os.remove(path)
except OSError: except OSError:
e = get_exception() e = get_exception()
@ -331,7 +345,7 @@ def main():
size = os.path.getsize(dest) size = os.path.getsize(dest)
try: try:
if compression == 'zip': if format == 'zip':
arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) arcfile = zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED)
arcfile.write(path, path[len(arcroot):]) arcfile.write(path, path[len(arcroot):])
arcfile.close() arcfile.close()
@ -340,12 +354,12 @@ def main():
else: else:
f_in = open(path, 'rb') f_in = open(path, 'rb')
if compression == 'gz': if format == 'gz':
f_out = gzip.open(dest, 'wb') f_out = gzip.open(dest, 'wb')
elif compression == 'bz2': elif format == 'bz2':
f_out = bz2.BZ2File(dest, 'wb') f_out = bz2.BZ2File(dest, 'wb')
else: else:
raise OSError("Invalid compression") raise OSError("Invalid format")
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
@ -353,7 +367,6 @@ def main():
except OSError: except OSError:
e = get_exception() e = get_exception()
module.fail_json(path=path, dest=dest, msg='Unable to write to compressed file: %s' % str(e)) module.fail_json(path=path, dest=dest, msg='Unable to write to compressed file: %s' % str(e))
if arcfile: if arcfile:
@ -369,7 +382,7 @@ def main():
state = 'compress' state = 'compress'
if remove: if remove and not check_mode:
try: try:
os.remove(path) os.remove(path)
@ -377,9 +390,12 @@ def main():
e = get_exception() e = get_exception()
module.fail_json(path=path, msg='Unable to remove source file: %s' % str(e)) module.fail_json(path=path, msg='Unable to remove source file: %s' % str(e))
params['path'] = dest
file_args = module.load_file_common_arguments(params)
changed = module.set_fs_attributes_if_different(file_args, changed)
module.exit_json(archived=successes, dest=dest, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths) module.exit_json(archived=successes, dest=dest, changed=changed, state=state, arcroot=arcroot, missing=missing, expanded_paths=expanded_paths)
# import module snippets
from ansible.module_utils.basic import *
if __name__ == '__main__': if __name__ == '__main__':
main() main()