Repository: ansible/ansible

Issue:
invalid literal for int() with base 8: 'u+rw,g+r,o-rwx
##### ISSUE TYPE
 - Documentation Issue

##### COMPONENT NAME
lib/ansible/modules/files/unarchive.py

##### ANSIBLE VERSION
2.2.0.X
```
Ansible 2.2.0
```

##### CONFIGURATION
```
[defaults]
hostfile = hosts
remote_user = root
host_key_checking = False

ansible_winrm_server_cert_validation: ignore

[ssh_connection]
control_path = %(directory)s/%%h-%%r

# %r denotes remote login user name
# %h denotes hostname
# %p Port number
```
##### OS / ENVIRONMENT
OS: CentOS 7.X
Target OS: OS:Cent OS7.X

##### SUMMARY
I tried running the script written by someone else. I am facing issue in the below lines.

```
  - name: unzipping {{product}} v{{version}} installation file
    unarchive:
      src: "{{local_path_to_tibco_software}}/TIB_{{product}}_{{version}}_{{product_architecture}}.zip"
      dest: "~/install"
      **mode: "u+rw,g+r,o-rwx"**
    become: true
    become_user: "{{tibco_user}}"
```

##### STEPS TO REPRODUCE
Run any script to unzipping a file and providing the permissions for one file in the below format.

**mode: "u+rw,g+r,o-rwx"**

<!--- Paste example playbooks or commands between quotes below -->
```
- set_fact:
      product_architecture: "{{lookup('csvfile', product+' file=product_arch_linux.csv')}}"

  - debug: msg='product_architecture={{ product_architecture }}'

  - debug: msg='product={{ product }}'

  - name:  create directory for tibco silent files
    file:
      path: "~/silent"
      state: "directory"
    become:  true
    become_user:  "{{tibco_user}}"

  - name:  create directory for tibco product installation files
    file:
      path: "~/install"
      state: "directory"
    become:  true
    become_user:  "{{tibco_user}}"

  - name: unzipping {{product}} v{{version}} installation file
    unarchive:
      src: "{{local_path_to_tibco_software}}/TIB_{{product}}_{{version}}_{{product_architecture}}.zip"
      dest: "~/install"
      mode: "u+rw,g+r,o-rwx"
    become: true
    become_user: "{{tibco_user}}"

  - name:  copy latest version of installer
    copy:
      src: "{{local_path_to_tibco_software}}/TIBCOUniversalInstaller-lnx-x86-64.bin"
      dest: "~/install"
      owner: "{{tibco_user}}"
      group: "{{tibco_group}}"
      mode: "u+rwx,g+r,o-rwx"
    become:  true
    become_user:  "{{tibco_user}}"

  - name:  copy {{product}} v{{version}} silent response file
    template:
      src: "TIBCOUniversalInstaller_{{product}}_{{version}}.silent.j2"
      dest: "/home/{{tibco_user}}/silent/TIBCOUniversalInstaller_{{product}}_{{version}}.silent"
      owner: "{{tibco_user}}"
      group: "{{tibco_group}}"
      mode: "u+rw,g+r,o-rwx"
    become:  true
    become_user:  "{{tibco_user}}"

  - name:  install {{product}} v{{version}} using response file
    shell: . ~/.bashrc && ~/install/TIBCOUniversalInstaller-lnx-x86-64.bin -silent -V responseFile='/home/{{tibco_user}}/silent/TIBCOUniversalInstaller_{{product}}_{{version}}.silent'
    become:  true
    become_user:  "{{tibco_user}}"

  - name:  remove the installation file
    file:
      path: "~/install"
      state: "absent"
    become:  true
    become_user:  "{{tibco_user}}"

```

##### EXPECTED RESULTS
It should proceed with the installation.

##### ACTUAL RESULTS
```
fatal: [ansibletarget]: FAILED! => {
    "changed": false,                                                                                                                                                                                
    "details": "invalid literal for int() with base 8: 'u+rw,g+r,o-rwx'",                                                                                                                            
    "failed": true,                                                                                                                                                                                  
    "invocation": {                                                                                                                                                                                  
        "module_args": {                                                                                                                                                                             
            "backup": null,                                                                                                                                                                          
            "content": null,                                                                                                                                                                         
            "copy": true,                                                                                                                                                                            
            "creates": null,                                                                                                                                                                         
            "delimiter": null,                                                                                                                                                                       
            "dest": "/home/tibco/install",                                                                                                                                                           
            "directory_mode": null,                                                                                                                                                                  
            "exclude": [],                                                                                                                                                                           
            "extra_opts": [],                                                                                                                                                                        
            "follow": false,                                                                                                                                                                         
            "force": null,                                                                                                                                                                           
            "group": null,                                                                                                                                                                           
            "keep_newer": false,                                                                                                                                                                     
            "list_files": false,                                                                                                                                                                     
            "mode": "u+rw,g+r,o-rwx",                                                                                                                                                                
            "original_basename": "TIB_ems_8.3.0_linux_x86.zip",                                                                                                                                      
            "owner": null,                                                                                                                                                                           
            "regexp": null,                                                                                                                                                                          
            "remote_src": false,                                                                                                                                                                     
            "selevel": null,                                                                                                                                                                         
            "serole": null,                                                                                                                                                                          
            "setype": null,                                                                                                                                                                          
            "seuser": null,                                                                                                                                                                          
            "src": "/tmp/ansible-tmp-1486469443.88-83867552971586/source",                                                                                                                           
            "unsafe_writes": null,                                                                                                                                                                   
            "validate_certs": true                                                                                                                                                                   
        }                                                                                                                                                                                            
    },                                                                                                                                                                                               
    "msg": "mode u+rw,g+r,o-rwx must be in octal form",                                                                                                                                              
    "path": "assemblies/",                                                                                                                                                                           
    "state": "absent"                                                                                                                                                                                
} 
```
<!--- Paste verbatim command output between quotes below -->
```
fatal: [ansibletarget]: FAILED! => {
    "changed": false,                                                                                                                                                                                
    "details": "invalid literal for int() with base 8: 'u+rw,g+r,o-rwx'",                                                                                                                            
    "failed": true,                                                                                                                                                                                  
    "invocation": {                                                                                                                                                                                  
        "module_args": {                                                                                                                                                                             
            "backup": null,                                                                                                                                                                          
            "content": null,                                                                                                                                                                         
            "copy": true,                                                                                                                                                                            
            "creates": null,                                                                                                                                                                         
            "delimiter": null,                                                                                                                                                                       
            "dest": "/home/tibco/install",                                                                                                                                                           
            "directory_mode": null,                                                                                                                                                                  
            "exclude": [],                                                                                                                                                                           
            "extra_opts": [],                                                                                                                                                                        
            "follow": false,                                                                                                                                                                         
            "force": null,                                                                                                                                                                           
            "group": null,                                                                                                                                                                           
            "keep_newer": false,                                                                                                                                                                     
            "list_files": false,                                                                                                                                                                     
            "mode": "u+rw,g+r,o-rwx",                                                                                                                                                                
            "original_basename": "TIB_ems_8.3.0_linux_x86.zip",                                                                                                                                      
            "owner": null,                                                                                                                                                                           
            "regexp": null,                                                                                                                                                                          
            "remote_src": false,                                                                                                                                                                     
            "selevel": null,                                                                                                                                                                         
            "serole": null,                                                                                                                                                                          
            "setype": null,                                                                                                                                                                          
            "seuser": null,                                                                                                                                                                          
            "src": "/tmp/ansible-tmp-1486469443.88-83867552971586/source",                                                                                                                           
            "unsafe_writes": null,                                                                                                                                                                   
            "validate_certs": true                                                                                                                                                                   
        }                                                                                                                                                                                            
    },                                                                                                                                                                                               
    "msg": "mode u+rw,g+r,o-rwx must be in octal form",                                                                                                                                              
    "path": "assemblies/",                                                                                                                                                                           
    "state": "absent"                                                                                                                                                                                
} 
```



Discussion:
Hi Team,

Could someone please provide me inputs regarding this?
cc @dagwieers @pileofrogs
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@krishnachaitanya511 for now, the workaround would be to do as the msg says and specify mode as an octal number instead of using a symbolic mode string.  I'll let @dagwieers decide if symbolic modes are a bugfix or a feature request. (the documentation doesn't say that octal modes are necessary but the error code seems to imply that that is the case.)
Hello,

I'm hitting what looks like the same issue, and it seems a bit more complicated than that.
OS is RHEL6, Ansible 2.2.1.0 from EPEL.

The first run works (empty target directory), the second fails (previously extracted files are already present in the target directory).
The documentation explicitly states that symbolic modes can be used since 1,8:
"As of version 1.8, the mode may be specified as a symbolic mode (for example, u+rwx or u=rw,g=r,o=r)."
http://docs.ansible.com/ansible/unarchive_module.html
It's not possible to simply apply an octal mode, since there are directories and files.
Here's the output:

First run:
```
TASK [db : Fetch files *******
changed: [server] => (item=linuxamd64_12102_grid_1of2.zip)
changed: [server] => (item=linuxamd64_12102_grid_2of2.zip)
```

2nd run:
```
TASK [db : Fetch files *******
failed: [server] (item=linuxamd64_12102_grid_1of2.zip) => {"details": "invalid literal for int() with base 8: 'g-rwx,o-rwx'", "failed": true, "item": "linuxamd64_12102_grid_1of2.zip", "msg": "mode g-rwx,o-rwx must be in octal form", "path": "grid/", "state": "absent"}
failed: [server] (item=linuxamd64_12102_grid_2of2.zip) => {"details": "invalid literal for int() with base 8: 'g-rwx,o-rwx'", "failed": true, "item": "linuxamd64_12102_grid_2of2.zip", "msg": "mode g-rwx,o-rwx must be in octal form", "path": "grid/stage/Components/oracle.has.crs/", "state": "absent"}
```

The task:
```
- name: Fetch files
  unarchive:
    src:        https://webserver/oracle/{{ item }}
    dest:       /app/software
    owner:      grid
    group:      oinstall
    mode:       g-rwx,o-rwx
    remote_src: yes
  with_items:
    - linuxamd64_12102_grid_1of2.zip
    - linuxamd64_12102_grid_2of2.zip
```

FWIW, I will open a case with RH referencing this.

Testing with a simplified, local invocation of the 'unarchive' module (Ansible 2.2.1.0) with a ZIP archive containing only an empty directory, as follows:

```
---
- hosts: localhost
  gather_facts: false
  tasks:
  - unarchive:
      src: test.zip
      dest: /home/worker/ansible/test/unarchive/
      remote_src: false
      mode: g-rwx,o-rwx
```

The first run succeeds in unarchiving the ZIP archive.  On a subsequent run of the playbook, the following error occurs:

```
fatal: [localhost]: FAILED! => {"changed": false, "details": "invalid literal for int() with base 8: 'g-rwx,o-rwx'", "failed": true, "gid": 1001, "group": "worker", "mode": "0700", "msg": "mode g-rwx,o-rwx must be in octal form", "owner": "worker", "path": "directory/", "secontext": "unconfined_u:object_r:user_home_t:s0", "size": 4096, "state": "directory", "uid": 1001}
```

Testing the same playbook with a '.tar.gz' arrchive succeeds (unchanged and no error) on a subsequent run with an existing 'dest'.  This may affect only operations on ZIP archives?
Observing the same issue while extracting a zip on Debian, Ansible package version 2.2.1.0-2.

A good example of a symbolic mode which cannot be represented by a numeric permission might be `mode: "u=rwX,g=rwX,o=rX"` (0775 for directories, 0664 for files). My current workaround is to go over the extracted directory with the `file` module, which keeps everything idempotent.

```yaml
- unarchive:
    src: /opt/example.zip
    dest: /opt/example
    remote_src: True
    owner: root
    group: example_group

- file:
    dest: /opt/example
    mode: "u=rwX,g=rwX,o=rX"
    recurse: yes
```

I would suggest that this is a bug because the [`unarchive` documentation](http://docs.ansible.com/ansible/unarchive_module.html) does not correspond with reality:

> ... As of version 1.8, the mode may be specified as a symbolic mode (for example, u+rwx or u=rw,g=r,o=r

For me, this only happens when destination's already in place. So on first-run it happily accepts the symbolic notation, but then on subsequent blows up. Certainly very bug-like!

Resolving patch:
diff --git a/lib/ansible/modules/files/unarchive.py b/lib/ansible/modules/files/unarchive.py
--- a/lib/ansible/modules/files/unarchive.py
+++ b/lib/ansible/modules/files/unarchive.py
@@ -130,6 +130,7 @@
 import re
 import stat
 import time
+import traceback
 from zipfile import ZipFile, BadZipfile
 
 from ansible.module_utils.basic import AnsibleModule
@@ -494,7 +495,10 @@ def is_unarchived(self):
                         try:
                             mode = int(self.file_args['mode'], 8)
                         except Exception as e:
-                            self.module.fail_json(path=path, msg="mode %(mode)s must be in octal form" % self.file_args, details=to_native(e))
+                            try:
+                                mode = AnsibleModule._symbolic_mode_to_octal(st, self.file_args['mode'])
+                            except ValueError as e:
+                                self.module.fail_json(path=path, msg="%s" % to_native(e), exception=traceback.format_exc())
                 # Only special files require no umask-handling
                 elif ztype == '?':
                     mode = self._permstr_to_octal(permstr, 0)


Test patch:
diff --git a/test/integration/targets/unarchive/tasks/main.yml b/test/integration/targets/unarchive/tasks/main.yml
--- a/test/integration/targets/unarchive/tasks/main.yml
+++ b/test/integration/targets/unarchive/tasks/main.yml
@@ -50,11 +50,10 @@
   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.
+# Recent unzip on 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.  
 
@@ -168,14 +167,14 @@
       - "'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
   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
@@ -184,7 +183,7 @@
   assert:
     that:
       - "unarchive03b.changed == false"
-      
+
 - name: remove our zip unarchive destination
   file: path={{output_dir}}/test-unarchive-zip state=absent
 
@@ -222,7 +221,7 @@
 - name: create our unarchive destination
   file: path={{output_dir}}/test-unarchive-tar-gz state=directory
 
-- name: unarchive and set mode to 0600
+- name: unarchive and set mode to 0600, directories 0700
   unarchive:
     src: "{{ output_dir }}/test-unarchive.tar.gz"
     dest: "{{ output_dir | expanduser }}/test-unarchive-tar-gz"
@@ -278,7 +277,7 @@
     src: "{{ output_dir }}/test-unarchive.tar.gz"
     dest: "{{ output_dir | expanduser }}/test-unarchive-tar-gz"
     remote_src: yes
-    mode: "u+rwX,g-wx,o-wx,g+r,o+r"
+    mode: "u+rwX-x,g-wx,o-wx,g+r,o+r"
     list_files: True
   register: unarchive07
 
@@ -295,6 +294,71 @@
   file: path={{ output_dir }}/test-unarchive-tar-gz state=absent
 
 
+- name: create our unarchive destination
+  file: path={{output_dir}}/test-unarchive-zip state=directory
+
+- name: unarchive and set mode to 0601, directories 0700
+  unarchive:
+    src: "{{ output_dir }}/test-unarchive.zip"
+    dest: "{{ output_dir | expanduser }}/test-unarchive-zip"
+    remote_src: yes
+    mode: "u+rwX-x,g-rwx,o=x"
+    list_files: True
+  register: unarchive08
+
+- name: Test that the file modes were changed
+  stat:
+    path: "{{ output_dir | expanduser }}/test-unarchive-zip/foo-unarchive.txt"
+  register: unarchive08_stat
+
+- name: Test that the file modes were changed
+  assert:
+    that:
+      - "unarchive08.changed == true"
+      - "unarchive08_stat.stat.mode == '0601'"
+      # Verify that file list is generated
+      - "'files' in unarchive08"
+      - "{{unarchive08['files']| length}} == 3"
+      - "'foo-unarchive.txt' in unarchive08['files']"
+      - "'foo-unarchive-777.txt' in unarchive08['files']"
+      - "'FOO-UNAR.TXT' in unarchive08['files']"
+
+- name: unarchive zipfile a second time and set mode to 0601, directories 0700
+  unarchive:
+    src: "{{ output_dir }}/test-unarchive.zip"
+    dest: "{{ output_dir | expanduser }}/test-unarchive-zip"
+    remote_src: yes
+    mode: "u+rwX-x,g-rwx,o=x"
+    list_files: True
+  register: unarchive08
+
+- name: Test that the file modes were not changed
+  stat:
+    path: "{{ output_dir | expanduser }}/test-unarchive-zip/foo-unarchive.txt"
+  register: unarchive08_stat
+
+- debug:
+    var: unarchive08
+
+- debug:
+    var: unarchive08_stat
+
+- name: Test that the files did not change
+  assert:
+    that:
+      - "unarchive08.changed == false"
+      - "unarchive08_stat.stat.mode == '0601'"
+      # Verify that file list is generated
+      - "'files' in unarchive08"
+      - "{{unarchive08['files']| length}} == 3"
+      - "'foo-unarchive.txt' in unarchive08['files']"
+      - "'foo-unarchive-777.txt' in unarchive08['files']"
+      - "'FOO-UNAR.TXT' in unarchive08['files']"
+
+- name: remove our zip unarchive destination
+  file: path={{ output_dir }}/test-unarchive-zip state=absent
+
+
 - name: create our unarchive destination
   file: path={{output_dir}}/test-unarchive-tar-gz state=directory