Repository: ansible/ansible-modules-core

Issue:
Unarchive Module fails with Traceback
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

<!--- Name of the plugin/module/task -->

unarchive
##### ANSIBLE VERSION

```
[root@8a976966193f stuff]# ansible --version
ansible 2.2.0 (devel cf62a62b83) last updated 2016/05/03 23:19:15 (GMT +000)
  lib/ansible/modules/core: (detached HEAD 9db1233521) last updated 2016/05/03 23:19:24 (GMT +000)
  lib/ansible/modules/extras: (detached HEAD 675d778b50) last updated 2016/05/03 23:19:29 (GMT +000)
  config file = /stuff/ansible.cfg
  configured module search path = ['./library']
```
##### CONFIGURATION

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->

centos based Docker image
##### SUMMARY

<!--- Explain the problem briefly -->

While using the unarchive module to unzip consul from a /tmp directory on the remote machine to the destination /usr/local/bin the module throws an exception. In this case, consul had previously been installed, so /usr/local/bin/consul already existed.

```
fatal: [10.64.1.198]: FAILED! => {"changed": false, "failed": true, "module_stderr": "", "module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_5Ccjgl/ansible_module_unarchive.py\", line 751, in <module>\r\n    main()\r\n  File \"/tmp/ansible_5Ccjgl/ansible_module_unarchive.py\", line 711, in main\r\n    check_results = handler.is_unarchived()\r\n  File \"/tmp/ansible_5Ccjgl/ansible_module_unarchive.py\", line 397, in is_unarchived\r\n    err += 'Path %s differs in permissions (%o vs %o)\\n' % (path, self.file_args['mode'], stat.S_IMODE(st.st_mode))\r\nTypeError: %o format: a number is required, not str\r\n", "msg": "MODULE FAILURE", "parsed": false}
```
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

The tasks I'm running are... (my role is not very different from https://github.com/savagegus/ansible-consul)

```
- name: downoad consul
  get_url: >
    url={{ consul_release_url }}
    checksum={{ consul_release_checksum }}
    dest={{ consul_download_folder }}/{{ consul_archive }}
  register: consul_downloaded
  become: yes

- name: install consul
  unarchive: >
    src={{ consul_download_folder }}/{{ consul_archive }}
    dest=/usr/local/bin
    owner={{ consul_user }}
    group={{ consul_group }}
    mode=755
    copy=no
  become: yes
```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

Previously with the current stable release of ansible (ansible 2.0.2) this task runs without any errors. 
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with high verbosity (-vvvv) -->

I can run with higher verbosity if you think its helpful. 

```
fatal: [10.64.1.78]: FAILED! => {"changed": false, "failed": true, "module_stderr": "", "module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_bGfFXq/ansible_module_unarchive.py\", line 751, in <module>\r\n    main()\r\n  File \"/tmp/ansible_bGfFXq/ansible_module_unarchive.py\", line 711, in main\r\n    check_results = handler.is_unarchived()\r\n  File \"/tmp/ansible_bGfFXq/ansible_module_unarchive.py\", line 397, in is_unarchived\r\n    err += 'Path %s differs in permissions (%o vs %o)\\n' % (path, self.file_args['mode'], stat.S_IMODE(st.st_mode))\r\nTypeError: %o format: a number is required, not str\r\n", "msg": "MODULE FAILURE", "parsed": false}
```

I worked around this by changing the string formatting thats throwing the error to cast everything as a string. Probably not the correct way to do it which is why I'm not creating a pull request, but it gets me past the error for now. 

```
--- a/files/unarchive.py
+++ b/files/unarchive.py
@@ -394,7 +394,7 @@ class ZipArchive(object):
                     mode = self._permstr_to_octal(permstr, umask)
                 if self.file_args['mode'] and  self.file_args['mode'] != stat.S_IMODE(st.st_mode):
                     change = True
-                    err += 'Path %s differs in permissions (%o vs %o)\n' % (path, self.file_args['mode'], stat.S_IMODE(st.st_mode))
+                    err += 'Path %s differs in permissions (%s vs %s)\n' % (path, str(self.file_args['mode']), str(stat.S_IMODE(st.st_
                     itemized[5] = 'p'
                 elif mode != stat.S_IMODE(st.st_mode):
                     change = True
```



Discussion:
> centos based Docker image

which version? 

> I can run with higher verbosity if you think its helpful. 

The output of the unzip command the module builds when doing the archive checkout would be useful, something like

```
unzip -ZT -s /path/to/the/consul_zip_archive.zip
```

@alikins the unarchive module is being run on Ubuntu based remote hosts

The output to that zip command is 

```
unzip -ZT -s /var/consul/consul_0.6.3_linux_amd64.zip
Archive:  /var/consul/consul_0.6.3_linux_amd64.zip
Zip file size: 5714093 bytes, number of entries: 1
-rwxr-xr-x  3.0 unx 20417720 bx defN 20160114.213313 consul
1 file, 20417720 bytes uncompressed, 5713931 bytes compressed:  72.0%
```

If you think it will help, here is my docker file as well...
The remote machine is ami-3b6a8050 which is just Ubuntu 14.

```
FROM centos

RUN yum install -y \
    @development \
    python-devel \
    libxml-devel \
    libxslt-devel \
    libffi-devel \
    openssl-devel \
    git vim \
    ca-certifactes

RUN curl -L https://bootstrap.pypa.io/get-pip.py | python -
RUN pip install -U six boto awscli cryptography shade requests 

RUN git clone --recurse https://github.com/ansible/ansible /ansible
RUN pip install -e /ansible
```


Resolving patch:
diff --git a/files/unarchive.py b/files/unarchive.py
--- a/files/unarchive.py
+++ b/files/unarchive.py
@@ -441,16 +441,24 @@ def is_unarchived(self):
 
             # Do not handle permissions of symlinks
             if ftype != 'L':
+
+                # Use the new mode provided with the action, if there is one
+                if self.file_args['mode']:
+                    if isinstance(self.file_args['mode'], int):
+                        mode = self.file_args['mode']
+                    else:
+                        try:
+                            mode = int(self.file_args['mode'], 8)
+                        except Exception:
+                            e = get_exception()
+                            self.module.fail_json(path=path, msg="mode %(mode)s must be in octal form" % self.file_args, details=str(e))
                 # Only special files require no umask-handling
-                if ztype == '?':
+                elif ztype == '?':
                     mode = self._permstr_to_octal(permstr, 0)
                 else:
                     mode = self._permstr_to_octal(permstr, umask)
-                if self.file_args['mode'] and  self.file_args['mode'] != stat.S_IMODE(st.st_mode):
-                    change = True
-                    err += 'Path %s differs in permissions (%o vs %o)\n' % (path, self.file_args['mode'], stat.S_IMODE(st.st_mode))
-                    itemized[5] = 'p'
-                elif mode != stat.S_IMODE(st.st_mode):
+
+                if mode != stat.S_IMODE(st.st_mode):
                     change = True
                     itemized[5] = 'p'
                     err += 'Path %s differs in permissions (%o vs %o)\n' % (path, mode, stat.S_IMODE(st.st_mode))
@@ -700,6 +708,7 @@ def main():
             validate_certs    = dict(required=False, default=True, type='bool'),
         ),
         add_file_common_args = True,
+        # check-mode only works for zip files
 #        supports_check_mode = True,
     )
 


Test patch: