diff --git a/changelogs/fragments/76971-unarchive-remove-unnecessary-zipinfo-dependency.yml b/changelogs/fragments/76971-unarchive-remove-unnecessary-zipinfo-dependency.yml new file mode 100644 index 00000000000..601580e509a --- /dev/null +++ b/changelogs/fragments/76971-unarchive-remove-unnecessary-zipinfo-dependency.yml @@ -0,0 +1,2 @@ +bugfixes: + - unarchive - if unzip is available but zipinfo is not, use unzip -Z instead of zipinfo (https://github.com/ansible/ansible/issues/76959). diff --git a/lib/ansible/modules/unarchive.py b/lib/ansible/modules/unarchive.py index e9062a7c79a..d375fbcc4ac 100644 --- a/lib/ansible/modules/unarchive.py +++ b/lib/ansible/modules/unarchive.py @@ -312,6 +312,11 @@ class ZipArchive(object): self.zipinfo_cmd_path = None self._files_in_archive = [] self._infodict = dict() + self.zipinfoflag = '' + self.binaries = ( + ('unzip', 'cmd_path'), + ('zipinfo', 'zipinfo_cmd_path'), + ) def _permstr_to_octal(self, modestr, umask): ''' Convert a Unix permission string (rw-r--r--) into a mode (0644) ''' @@ -399,7 +404,10 @@ class ZipArchive(object): def is_unarchived(self): # BSD unzip doesn't support zipinfo listings with timestamp. - cmd = [self.zipinfo_cmd_path, '-T', '-s', self.src] + if self.zipinfoflag: + cmd = [self.zipinfo_cmd_path, self.zipinfoflag, '-T', '-s', self.src] + else: + cmd = [self.zipinfo_cmd_path, '-T', '-s', self.src] if self.excludes: cmd.extend(['-x', ] + self.excludes) @@ -720,12 +728,8 @@ class ZipArchive(object): return dict(cmd=cmd, rc=rc, out=out, err=err) def can_handle_archive(self): - binaries = ( - ('unzip', 'cmd_path'), - ('zipinfo', 'zipinfo_cmd_path'), - ) missing = [] - for b in binaries: + for b in self.binaries: try: setattr(self, b[1], get_bin_path(b[0])) except ValueError: @@ -948,9 +952,32 @@ class TarZstdArchive(TgzArchive): self.zipflag = '--use-compress-program=zstd' +class ZipZArchive(ZipArchive): + def __init__(self, src, b_dest, file_args, module): + super(ZipZArchive, self).__init__(src, b_dest, file_args, module) + self.zipinfoflag = '-Z' + self.binaries = ( + ('unzip', 'cmd_path'), + ('unzip', 'zipinfo_cmd_path'), + ) + + def can_handle_archive(self): + unzip_available, error_msg = super(ZipZArchive, self).can_handle_archive() + + if not unzip_available: + return unzip_available, error_msg + + # Ensure unzip -Z is available before we use it in is_unarchive + cmd = [self.zipinfo_cmd_path, self.zipinfoflag] + rc, out, err = self.module.run_command(cmd) + if 'zipinfo' in out.lower(): + return True, None + return False, 'Command "unzip -Z" could not handle archive: %s' % err + + # try handlers in order and return the one that works or bail if none work def pick_handler(src, dest, file_args, module): - handlers = [ZipArchive, TgzArchive, TarArchive, TarBzipArchive, TarXzArchive, TarZstdArchive] + handlers = [ZipArchive, ZipZArchive, TgzArchive, TarArchive, TarBzipArchive, TarXzArchive, TarZstdArchive] reasons = set() for handler in handlers: obj = handler(src, dest, file_args, module) diff --git a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml index f8e4536701b..58d38f4f91e 100644 --- a/test/integration/targets/unarchive/tasks/test_missing_binaries.yml +++ b/test/integration/targets/unarchive/tasks/test_missing_binaries.yml @@ -33,6 +33,14 @@ remote_src: yes register: zip_fail ignore_errors: yes + # FreeBSD does not have zipinfo, but does have a bootstrapped unzip in /usr/bin + # which alone is sufficient to run unarchive. + # Exclude /usr/bin from the PATH to test having no binary available. + environment: + PATH: "{{ ENV_PATH }}" + vars: + ENV_PATH: "{{ lookup('env', 'PATH') | regex_replace(re, '') }}" + re: "[^A-Za-z](\/usr\/bin:?)" - name: Ensure tasks worked as expected assert: @@ -41,6 +49,29 @@ - zip_fail is failed - zip_fail.msg is search('Unable to find required') + - name: unarchive a zip file using unzip without zipinfo + unarchive: + src: '{{remote_tmp_dir}}/test-unarchive.zip' + dest: '{{remote_tmp_dir}}/test-unarchive-zip' + list_files: True + remote_src: yes + register: zip_success + # FreeBSD does not have zipinfo, but does have a bootstrapped unzip in /usr/bin + # which alone is sufficient to run unarchive. + when: ansible_pkg_mgr == 'pkgng' + + - assert: + that: + - zip_success is success + - zip_success.changed + # Verify that file list is generated + - "'files' in zip_success" + - "{{zip_success['files']| length}} == 3" + - "'foo-unarchive.txt' in zip_success['files']" + - "'foo-unarchive-777.txt' in zip_success['files']" + - "'FOO-UNAR.TXT' in zip_success['files']" + when: ansible_pkg_mgr == 'pkgng' + - name: Remove unarchive destinations file: path: '{{ remote_tmp_dir }}/test-unarchive-{{ item }}'