Repository: ansible/ansible

Issue:
after 2.2.0.0, unarchive no longer finds handler for .tar.gz if extra_opts is used
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
unarchive

##### ANSIBLE VERSION
OK: ansible-2.1.6.0,
not OK: ansible-2.1.6.0 up to devel (ansible-playbook 2.4.0)

##### OS / ENVIRONMENT
linux amd64 (Ubuntu 16.10)

##### SUMMARY
Unarchive complains that it cannot find a handler for tar files:
```
"Failed to find handler for \"/home/aes/ex.tar.gz\". Make sure the required command to extract the file is installed. Command \"/bin/tar\" could not handle archive. Command \"/usr/bin/unzip\" could not handle archive."
```

##### STEPS TO REPRODUCE
```shell
cd && mkdir dir && touch dir/file && tar cvzf ex.tar.gz dir/

ansible-playbook -i hosts -c local -l $HOST plays/demo.yml
```
plays/demo.yml:
```yaml
# ansible-playbook -i hosts -c local -l $HOST plays/demo.yml
- hosts: all
  become: no
  tasks:
  - name: unpack example
    unarchive:
      src: ~/ex.tar.gz
      dest: /tmp
      copy: no
      extra_opts: --transform s/^xxx/yyy/
      creates: /tmp/dir/file
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS

Result using 2.1.6.0:
```
changed: [deck] => {"changed": true, "dest": "/tmp", "extract_results": {"cmd": "/bin/tar -C \"/tmp\" -xz --transform s/^xxx/yyy/ -f \"/home/aes/ex.tar.gz\"", "err": "", "out": "", "rc": 0}, "gid": 0, "group": "root", "handler": "TgzArchive", "mode": "01777", "owner": "root", "size": 12288, "src": "/home/aes/ex.tar.gz", "stat": {"exists": false}, "state": "directory", "uid": 0}
```
##### ACTUAL RESULTS
```
TASK [unpack example] **********************************************************
task path: /home/aes/plays/demo.yml:8
<deck> ESTABLISH LOCAL CONNECTION FOR USER: anders.eurenius
<deck> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1498643280.78-55902202971254 `" && echo ansible-tmp-1498643280.78-55902202971254="` echo $HOME/.ansible/tmp/ansible-tmp-1498643280.78-55902202971254 `" ) && sleep 0'
<deck> EXEC /bin/sh -c 'test -e /tmp/dir/file && sleep 0'
Using module file /home/aes/.local/opt/ansible/local/lib/python2.7/site-packages/ansible/modules/core/files/stat.py
<deck> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467 `" && echo ansible-tmp-1498643280.88-35516905815467="` echo $HOME/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467 `" ) && sleep 0'
<deck> PUT /tmp/user/1000/tmpWo9Ywa TO /home/aes/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467/stat.py
<deck> EXEC /bin/sh -c 'chmod u+x /home/aes/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467/ /home/aes/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467/stat.py && sleep 0'
<deck> EXEC /bin/sh -c '/usr/bin/python /home/aes/.ansible/tmp/ansible-tmp-1498643280.88-35516905815467/stat.py && sleep 0'
Using module file /home/aes/.local/opt/ansible/local/lib/python2.7/site-packages/ansible/modules/core/files/unarchive.py
<deck> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721 `" && echo ansible-tmp-1498643281.05-176146589894721="` echo $HOME/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721 `" ) && sleep 0'
<deck> PUT /tmp/user/1000/tmpLE5sVe TO /home/aes/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721/unarchive.py
<deck> EXEC /bin/sh -c 'chmod u+x /home/aes/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721/ /home/aes/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721/unarchive.py && sleep 0'
<deck> EXEC /bin/sh -c '/usr/bin/python /home/aes/.ansible/tmp/ansible-tmp-1498643281.05-176146589894721/unarchive.py && sleep 0'
fatal: [deck]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "invocation": {
        "module_args": {
            "backup": null, 
            "content": null, 
            "copy": false, 
            "creates": "/tmp/dir/file", 
            "delimiter": null, 
            "dest": "/tmp", 
            "directory_mode": null, 
            "exclude": [], 
            "extra_opts": [
                "--transform s/^xxx/yyy/"
            ], 
            "follow": false, 
            "force": null, 
            "group": null, 
            "keep_newer": false, 
            "list_files": false, 
            "mode": null, 
            "original_basename": "ex.tar.gz", 
            "owner": null, 
            "regexp": null, 
            "remote_src": false, 
            "selevel": null, 
            "serole": null, 
            "setype": null, 
            "seuser": null, 
            "src": "/home/aes/ex.tar.gz", 
            "unsafe_writes": null, 
            "validate_certs": true
        }
    }, 
    "msg": "Failed to find handler for \"/home/aes/ex.tar.gz\". Make sure the required command to extract the file is installed. Command \"/bin/tar\" could not handle archive. Command \"/usr/bin/unzip\" could not handle archive."
}
	to retry, use: --limit @/home/aes/src/env/ansible/plays/demo.retry
```



Discussion:
cc @dagwieers @pileofrogs
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@aes The right way to use extra_opts is:
```yaml
extra_opts:
  - --transform
  - s/^xxx/yyy/
```
I think ansible didn't do `shlex` before `2.1.6.0`, but now it will escape the "unsafe" options with `subcommand.Popen`, in your case, there's a space inside the option, so python escape it to `'--transform s/^xxx/yyy/'`.

I will submit a MR to make document clear about this.
resolved_by_pr 26217

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
@@ -117,6 +117,14 @@
     src: https://example.com/example.zip
     dest: /usr/local/bin
     remote_src: yes
+
+- name: Unarchive a file with extra options
+  unarchive:
+    src: /tmp/foo.zip
+    dest: /usr/local/bin
+    extra_opts:
+    - --transform
+    - s/^xxx/yyy/
 '''
 
 import binascii


Test patch: