unarchive - add include option (#40522)

This should allow users to extract specific files from an archive as
desired.

Fixes #16130, #27081.

* Rebase and make a few minor changes
* Add changelog
* Improve tests

- move to separate tasks file
- change assertions to check for exactly one file
- use remote_tmp_dir for output dir

* Make exclude and include mutually exclusive
* Don't remove files needed by other tasks
* Fix sanity tests
* Improve feature documentation
* Skip tests that use map() on CentOS 6
* Use fnmatch on include for zip archives
  This matches the behavior of exclude

Co-authored-by: Sam Doran <sdoran@redhat.com>
This commit is contained in:
Sijis Aviles 2020-12-07 11:49:41 -06:00 committed by GitHub
parent 6608f3aab3
commit 034e9b0252
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 19 deletions

View file

@ -0,0 +1,4 @@
minor_changes:
- >
unarchive - add ``include`` parameter to allow extracting specific files
from an archive (https://github.com/ansible/ansible/pull/40522)

View file

@ -58,9 +58,20 @@ options:
exclude:
description:
- List the directory and file entries that you would like to exclude from the unarchive action.
- Mutually exclusive with C(include).
type: list
default: []
elements: str
version_added: "2.1"
include:
description:
- List of directory and file entries that you would like to extract from the archive. Only
files listed here will be extracted.
- Mutually exclusive with C(exclude).
type: list
default: []
elements: str
version_added: "2.11"
keep_newer:
description:
- Do not replace existing files that are newer than files from the archive.
@ -264,6 +275,7 @@ class ZipArchive(object):
self.module = module
self.excludes = module.params['exclude']
self.includes = []
self.include_files = self.module.params['include']
self.cmd_path = self.module.get_bin_path('unzip')
self.zipinfocmd_path = self.module.get_bin_path('zipinfo')
self._files_in_archive = []
@ -337,14 +349,19 @@ class ZipArchive(object):
else:
try:
for member in archive.namelist():
exclude_flag = False
if self.excludes:
for exclude in self.excludes:
if fnmatch.fnmatch(member, exclude):
exclude_flag = True
break
if not exclude_flag:
self._files_in_archive.append(to_native(member))
if self.include_files:
for include in self.include_files:
if fnmatch.fnmatch(member, include):
self._files_in_archive.append(to_native(member))
else:
exclude_flag = False
if self.excludes:
for exclude in self.excludes:
if not fnmatch.fnmatch(member, exclude):
exclude_flag = True
break
if not exclude_flag:
self._files_in_archive.append(to_native(member))
except Exception:
archive.close()
raise UnarchiveError('Unable to list files in the archive')
@ -357,6 +374,8 @@ class ZipArchive(object):
cmd = [self.zipinfocmd_path, '-T', '-s', self.src]
if self.excludes:
cmd.extend(['-x', ] + self.excludes)
if self.include_files:
cmd.extend(self.include_files)
rc, out, err = self.module.run_command(cmd)
old_out = out
@ -665,6 +684,8 @@ class ZipArchive(object):
# cmd.extend(map(shell_escape, self.includes))
if self.excludes:
cmd.extend(['-x'] + self.excludes)
if self.include_files:
cmd.extend(self.include_files)
cmd.extend(['-d', self.b_dest])
rc, out, err = self.module.run_command(cmd)
return dict(cmd=cmd, rc=rc, out=out, err=err)
@ -690,6 +711,7 @@ class TgzArchive(object):
if self.module.check_mode:
self.module.exit_json(skipped=True, msg="remote module (%s) does not support check mode when using gtar" % self.module._name)
self.excludes = [path.rstrip('/') for path in self.module.params['exclude']]
self.include_files = self.module.params['include']
# Prefer gtar (GNU tar) as it supports the compression options -z, -j and -J
self.cmd_path = self.module.get_bin_path('gtar', None)
if not self.cmd_path:
@ -726,8 +748,10 @@ class TgzArchive(object):
if self.excludes:
cmd.extend(['--exclude=' + f for f in self.excludes])
cmd.extend(['-f', self.src])
rc, out, err = self.module.run_command(cmd, cwd=self.b_dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C'))
if self.include_files:
cmd.extend(self.include_files)
rc, out, err = self.module.run_command(cmd, cwd=self.b_dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C'))
if rc != 0:
raise UnarchiveError('Unable to list files in the archive')
@ -769,6 +793,8 @@ class TgzArchive(object):
if self.excludes:
cmd.extend(['--exclude=' + f for f in self.excludes])
cmd.extend(['-f', self.src])
if self.include_files:
cmd.extend(self.include_files)
rc, out, err = self.module.run_command(cmd, cwd=self.b_dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C'))
# Check whether the differences are in something that we're
@ -820,6 +846,8 @@ class TgzArchive(object):
if self.excludes:
cmd.extend(['--exclude=' + f for f in self.excludes])
cmd.extend(['-f', self.src])
if self.include_files:
cmd.extend(self.include_files)
rc, out, err = self.module.run_command(cmd, cwd=self.b_dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C'))
return dict(cmd=cmd, rc=rc, out=out, err=err)
@ -887,12 +915,14 @@ def main():
list_files=dict(type='bool', default=False),
keep_newer=dict(type='bool', default=False),
exclude=dict(type='list', elements='str', default=[]),
include=dict(type='list', elements='str', default=[]),
extra_opts=dict(type='list', elements='str', default=[]),
validate_certs=dict(type='bool', default=True),
),
add_file_common_args=True,
# check-mode only works for zip files, we cover that later
supports_check_mode=True,
mutually_exclusive=[('include', 'exclude')],
)
src = module.params['src']

View file

@ -6,6 +6,7 @@
- import_tasks: test_tar_gz_keep_newer.yml
- import_tasks: test_zip.yml
- import_tasks: test_exclude.yml
- import_tasks: test_include.yml
- import_tasks: test_parent_not_writeable.yml
- import_tasks: test_mode.yml
- import_tasks: test_quotable_characters.yml

View file

@ -89,4 +89,4 @@
mode: preserve
- name: prep a tar.gz file with directory
shell: tar czvf test-unarchive-dir.tar.gz unarchive-dir chdir={{remote_tmp_dir}}
shell: tar czvf test-unarchive-dir.tar.gz unarchive-dir chdir={{remote_tmp_dir}}

View file

@ -37,12 +37,3 @@
file:
path: '{{remote_tmp_dir}}/test-unarchive-zip'
state: absent
- name: remove our test files for the archive
file:
path: '{{remote_tmp_dir}}/{{item}}'
state: absent
with_items:
- foo-unarchive.txt
- foo-unarchive-777.txt
- FOO-UNAR.TXT

View file

@ -0,0 +1,83 @@
- name: Create a tar file with multiple files
shell: tar cvf test-unarchive-multi.tar foo-unarchive-777.txt foo-unarchive.txt
args:
chdir: "{{ remote_tmp_dir }}"
- name: Create include test directories
file:
state: directory
path: "{{ remote_tmp_dir }}/{{ item }}"
loop:
- include-zip
- include-tar
- name: Unpack zip file include one file
unarchive:
src: "{{ remote_tmp_dir }}/test-unarchive.zip"
dest: "{{ remote_tmp_dir }}/include-zip"
include:
- FOO-UNAR.TXT
- name: Verify that single file was unarchived
find:
paths: "{{ remote_tmp_dir }}/include-zip"
register: unarchive_dir02
# The map filter was added in Jinja2 2.7, which is newer than the version on RHEL/CentOS 6,
# so we skip this validation on those hosts
- name: Verify that zip extraction included only one file
assert:
that:
- file_names == ['FOO-UNAR.TXT']
vars:
file_names: "{{ unarchive_dir02.files | map(attribute='path') | map('basename') }}"
when:
- "ansible_facts.os_family == 'RedHat'"
- ansible_facts.distribution_major_version is version('7', '>=')
- name: Unpack tar file include one file
unarchive:
src: "{{ remote_tmp_dir }}/test-unarchive-multi.tar"
dest: "{{ remote_tmp_dir }}/include-tar"
include:
- foo-unarchive-777.txt
- name: verify that single file was unarchived from tar
find:
paths: "{{ remote_tmp_dir }}/include-tar"
register: unarchive_dir03
- name: Verify that tar extraction included only one file
assert:
that:
- file_names == ['foo-unarchive-777.txt']
vars:
file_names: "{{ unarchive_dir03.files | map(attribute='path') | map('basename') }}"
when:
- "ansible_facts.os_family == 'RedHat'"
- ansible_facts.distribution_major_version is version('7', '>=')
- name: Check mutually exclusive parameters
unarchive:
src: "{{ remote_tmp_dir }}/test-unarchive-multi.tar"
dest: "{{ remote_tmp_dir }}/include-tar"
include:
- foo-unarchive-777.txt
exclude:
- foo
ignore_errors: yes
register: unarchive_mutually_exclusive_check
- name: Check mutually exclusive parameters
assert:
that:
- unarchive_mutually_exclusive_check is failed
- "'mutually exclusive' in unarchive_mutually_exclusive_check.msg"
- name: "Remove include feature tests directory"
file:
state: absent
path: "{{ remote_tmp_dir }}/{{ item }}"
loop:
- 'include-zip'
- 'include-tar'