Fix idempotency for Unix permissions in zip files. (#24580)

* Fix idempotency for Unix permissions in zip files.

This fix prevents the unarchive module from reporting 'changed' when a zipfile contains items with Unix permissions that differ from the system default.

* Update zip unarchive tests.

Additional tests for the unarchive module with zip files:
- Test file in zip archive with non-default permissions
- Test file added to zip archive with Windows permissions

* Additional fix for mixed win/unix archives.

  Turns out my original fix fails under some mixed archives, as setting the umask to zero can be applied to those files.  This creates a per-file umask variable, so a mix of permission types don't cause problems.

* CI Checks

CI checks for archives with:
* non default Unix permissions
* Windows permissions


* Workaround for BSD differences.

Using Zipinfo due to lack of support in BSD unzip.
Permissions handling is also different in BSD -- always applies UMASK to file permissions.

* Added checks for creating directories and SSH keys for existing users.
This commit is contained in:
chriskarel 2017-08-11 14:36:46 -05:00 committed by Toshio Kuratomi
parent 3cb1c38ecc
commit 991918e9d2
2 changed files with 71 additions and 14 deletions

View file

@ -125,6 +125,7 @@ import codecs
import datetime import datetime
import grp import grp
import os import os
import platform
import pwd import pwd
import re import re
import stat import stat
@ -180,6 +181,7 @@ class ZipArchive(object):
self.excludes = module.params['exclude'] self.excludes = module.params['exclude']
self.includes = [] self.includes = []
self.cmd_path = self.module.get_bin_path('unzip') self.cmd_path = self.module.get_bin_path('unzip')
self.zipinfocmd_path = self.module.get_bin_path('zipinfo')
self._files_in_archive = [] self._files_in_archive = []
self._infodict = dict() self._infodict = dict()
@ -261,7 +263,8 @@ class ZipArchive(object):
return self._files_in_archive return self._files_in_archive
def is_unarchived(self): def is_unarchived(self):
cmd = [self.cmd_path, '-ZT', '-s', self.src] # BSD unzip doesn't support zipinfo listings with timestamp.
cmd = [self.zipinfocmd_path, '-T', '-s', self.src]
if self.excludes: if self.excludes:
cmd.extend(['-x', ] + self.excludes) cmd.extend(['-x', ] + self.excludes)
rc, out, err = self.module.run_command(cmd) rc, out, err = self.module.run_command(cmd)
@ -277,6 +280,7 @@ class ZipArchive(object):
# Get some information related to user/group ownership # Get some information related to user/group ownership
umask = os.umask(0) umask = os.umask(0)
os.umask(umask) os.umask(umask)
systemtype = platform.system()
# Get current user and group information # Get current user and group information
groups = os.getgroups() groups = os.getgroups()
@ -376,6 +380,12 @@ class ZipArchive(object):
ftype = 'f' ftype = 'f'
# Some files may be storing FAT permissions, not Unix permissions # Some files may be storing FAT permissions, not Unix permissions
# For FAT permissions, we will use a base permissions set of 777 if the item is a directory or has the execute bit set. Otherwise, 666.
# This permission will then be modified by the system UMask.
# BSD always applies the Umask, even to Unix permissions.
# For Unix style permissions on Linux or Mac, we want to use them directly.
# So we set the UMask for this file to zero. That permission set will then be unchanged when calling _permstr_to_octal
if len(permstr) == 6: if len(permstr) == 6:
if path[-1] == '/': if path[-1] == '/':
permstr = 'rwxrwxrwx' permstr = 'rwxrwxrwx'
@ -383,6 +393,11 @@ class ZipArchive(object):
permstr = 'rwxrwxrwx' permstr = 'rwxrwxrwx'
else: else:
permstr = 'rw-rw-rw-' permstr = 'rw-rw-rw-'
file_umask = umask
elif 'bsd' in systemtype.lower():
file_umask = umask
else:
file_umask = 0
# Test string conformity # Test string conformity
if len(permstr) != 9 or not ZIP_FILE_MODE_RE.match(permstr): if len(permstr) != 9 or not ZIP_FILE_MODE_RE.match(permstr):
@ -484,7 +499,7 @@ class ZipArchive(object):
elif ztype == '?': elif ztype == '?':
mode = self._permstr_to_octal(permstr, 0) mode = self._permstr_to_octal(permstr, 0)
else: else:
mode = self._permstr_to_octal(permstr, umask) mode = self._permstr_to_octal(permstr, file_umask)
if mode != stat.S_IMODE(st.st_mode): if mode != stat.S_IMODE(st.st_mode):
change = True change = True

View file

@ -17,18 +17,23 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make sure we start fresh # Make sure we start fresh
- name: Ensure zip is present to create test archive (yum) # Need unzup for unarchive module, and zip for archive creation.
yum: name=zip state=latest - name: Ensure zip and unzip is present to create test archive (yum)
yum: name=zip,unzip state=latest
when: ansible_pkg_mgr == 'yum' when: ansible_pkg_mgr == 'yum'
#- name: Ensure zip is present to create test archive (dnf) #- name: Ensure zip is present to create test archive (dnf)
# dnf: name=zip state=latest # dnf: name=zip state=latest
# when: ansible_pkg_mgr == 'dnf' # when: ansible_pkg_mgr == 'dnf'
- name: Ensure zip is present to create test archive (apt) - name: Ensure zip & unzup is present to create test archive (apt)
apt: name=zip state=latest apt: name=zip,unzip state=latest
when: ansible_pkg_mgr == 'apt' when: ansible_pkg_mgr == 'apt'
- name: Ensure zip & unzup is present to create test archive (pkg)
pkgng: name=zip,unzip state=present
when: ansible_pkg_mgr == 'pkgng'
- name: prep our file - name: prep our file
copy: src=foo.txt dest={{output_dir}}/foo-unarchive.txt copy: src=foo.txt dest={{output_dir}}/foo-unarchive.txt
@ -38,8 +43,26 @@
- name: prep a tar.gz file - name: prep a tar.gz file
shell: tar czvf test-unarchive.tar.gz foo-unarchive.txt chdir={{output_dir}} shell: tar czvf test-unarchive.tar.gz foo-unarchive.txt chdir={{output_dir}}
- name: prep a chmodded file for zip
copy: src=foo.txt dest={{output_dir}}/foo-unarchive-777.txt mode=0777
- name: prep a windows permission file for our zip
copy: src=foo.txt dest={{output_dir}}/FOO-UNAR.TXT
# This gets around an unzip timestamp bug in some distributions
# Recent unzip on Fedora, Ubuntu, and BSD will randomly round some timestamps up.
# But that doesn't seem to happen when the timestamp has an even second.
- name: Bug work around
command: touch -t "201705111530.00" {{output_dir}}/foo-unarchive.txt {{output_dir}}/foo-unarchive-777.txt {{output_dir}}/FOO-UNAR.TXT
# See Fedora bug 1451953: https://bugzilla.redhat.com/show_bug.cgi?id=1451953
# See Ubuntu bug 1691636: https://bugs.launchpad.net/ubuntu/+source/unzip/+bug/1691636
# When these are fixed, this code should be removed.
- name: prep a zip file - name: prep a zip file
shell: zip test-unarchive.zip foo-unarchive.txt chdir={{output_dir}} shell: zip test-unarchive.zip foo-unarchive.txt foo-unarchive-777.txt chdir={{output_dir}}
- name: add a file with Windows permissions to zip file
shell: zip -k test-unarchive.zip FOO-UNAR.TXT chdir={{output_dir}}
- name: prep a subdirectory - name: prep a subdirectory
file: path={{output_dir}}/unarchive-dir state=directory file: path={{output_dir}}/unarchive-dir state=directory
@ -141,17 +164,36 @@
- "unarchive03.changed == true" - "unarchive03.changed == true"
# Verify that file list is generated # Verify that file list is generated
- "'files' in unarchive03" - "'files' in unarchive03"
- "{{unarchive03['files']| length}} == 1" - "{{unarchive03['files']| length}} == 3"
- "'foo-unarchive.txt' in unarchive03['files']" - "'foo-unarchive.txt' in unarchive03['files']"
- "'foo-unarchive-777.txt' in unarchive03['files']"
- "'FOO-UNAR.TXT' in unarchive03['files']"
- name: verify that the file was unarchived - name: verify that the file was unarchived
file: path={{output_dir}}/test-unarchive-zip/foo-unarchive.txt state=file file: path={{output_dir}}/test-unarchive-zip/{{item}} state=file
with_items:
- foo-unarchive.txt
- foo-unarchive-777.txt
- FOO-UNAR.TXT
- name: repeat the last request to verify no changes
unarchive: src={{output_dir}}/test-unarchive.zip dest={{output_dir | expanduser}}/test-unarchive-zip remote_src=yes list_files=True
register: unarchive03b
- name: verify that the task was not marked as changed
assert:
that:
- "unarchive03b.changed == false"
- name: remove our zip unarchive destination - name: remove our zip unarchive destination
file: path={{output_dir}}/test-unarchive-zip state=absent file: path={{output_dir}}/test-unarchive-zip state=absent
- name: remove our test file for the archive - name: remove our test files for the archive
file: path={{output_dir}}/foo-unarchive.txt state=absent file: path={{output_dir}}/{{item}} state=absent
with_items:
- foo-unarchive.txt
- foo-unarchive-777.txt
- FOO-UNAR.TXT
- name: check if /tmp/foo-unarchive.text exists - name: check if /tmp/foo-unarchive.text exists
stat: path=/tmp/foo-unarchive.txt stat: path=/tmp/foo-unarchive.txt