Repository: ansible/ansible

Issue:
Copy module with option "decrypt: no" causes error when file already exists
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
Module: Copy
Option: decrypt: no #new in version 2.4.

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.0.0
```
##### 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.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
N/A
##### SUMMARY

When copying a (on the remote host non-existing) ansible-vault encrypted file with the copy module everything works fine and the copied file is encrypted (as it should be).
When the file already exists however, an error is thrown.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
Run the following task twice to reproduce

```yaml
---
- hosts: my-hosts
  tasks:
  - name: Copy vault-encrypted file
    copy:
      src: 'vault-encrypted.file'
      dest: .
      decrypt: no

  - name: Copy vault-encrypted file
    copy:
      src: 'vault-encrypted.file'
      dest: .
      decrypt: no
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
```
TASK [Copy vault-encrypted file] **********************************************************************************
changed: [my.server.com]

TASK [Copy vault-encrypted file] **********************************************************************************
ok: [my.server.com]
```

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
TASK [Copy vault-encrypted file] **********************************************************************************
changed: [my.server.com]

TASK [Copy vault-encrypted file] **********************************************************************************
fatal: [my.server.com]: FAILED! => {"changed": false, "checksum": "e88caee4fa8e6e905552d564e3d884097d8c67eb", "failed": true, "msg": "Unsupported parameters for (file) module: decrypt Supported parameters include: attributes,backup,content,delimiter,diff_peek,directory_mode,follow,force,group,mode,original_basename,owner,path,recurse,regexp,remote_src,selevel,serole,setype,seuser,src,state,unsafe_writes,validate"}
```



Discussion:
For info, same thing if `decrypt: yes` is used (not specific to `decrypt: no`)

If specified, the `decrypt` param seems to be passed to the `file` module by `copy` when the destination file already exists... but `file` does not support `decrypt`

Resolving patch:
diff --git a/lib/ansible/plugins/action/copy.py b/lib/ansible/plugins/action/copy.py
--- a/lib/ansible/plugins/action/copy.py
+++ b/lib/ansible/plugins/action/copy.py
@@ -186,6 +186,10 @@ def _recurse(topdir, rel_offset, parent_dirs, rel_base=u''):
 
 class ActionModule(ActionBase):
 
+    def _create_remote_file_args(self, module_args):
+        # remove action plugin only keys
+        return dict((k, v) for k, v in module_args.items() if k not in ('content', 'decrypt'))
+
     def _copy_file(self, source_full, source_rel, content, content_tempfile,
                    dest, task_vars, tmp, delete_remote_tmp):
         decrypt = boolean(self._task.args.get('decrypt', True), strict=False)
@@ -286,7 +290,7 @@ def _copy_file(self, source_full, source_rel, content, content_tempfile,
 
             # src and dest here come after original and override them
             # we pass dest only to make sure it includes trailing slash in case of recursive copy
-            new_module_args = self._task.args.copy()
+            new_module_args = self._create_remote_file_args(self._task.args)
             new_module_args.update(
                 dict(
                     src=tmp_src,
@@ -297,11 +301,6 @@ def _copy_file(self, source_full, source_rel, content, content_tempfile,
             if lmode:
                 new_module_args['mode'] = lmode
 
-            # remove action plugin only keys
-            for key in ('content', 'decrypt'):
-                if key in new_module_args:
-                    del new_module_args[key]
-
             module_return = self._execute_module(module_name='copy',
                                                  module_args=new_module_args, task_vars=task_vars,
                                                  tmp=tmp, delete_remote_tmp=delete_remote_tmp)
@@ -326,7 +325,7 @@ def _copy_file(self, source_full, source_rel, content, content_tempfile,
                     dest = dest_status_nofollow['lnk_source']
 
             # Build temporary module_args.
-            new_module_args = self._task.args.copy()
+            new_module_args = self._create_remote_file_args(self._task.args)
             new_module_args.update(
                 dict(
                     src=source_rel,
@@ -335,6 +334,7 @@ def _copy_file(self, source_full, source_rel, content, content_tempfile,
                     state='file',
                 )
             )
+
             if lmode:
                 new_module_args['mode'] = lmode
 
@@ -513,8 +513,7 @@ def run(self, tmp=None, task_vars=None):
         for source_full, source_rel in source_files['files']:
             # copy files over.  This happens first as directories that have
             # a file do not need to be created later
-            module_return = self._copy_file(source_full, source_rel, content, content_tempfile,
-                                            dest, task_vars, tmp, delete_remote_tmp)
+            module_return = self._copy_file(source_full, source_rel, content, content_tempfile, dest, task_vars, tmp, delete_remote_tmp)
             if module_return is None:
                 continue
 


Test patch:
diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
--- a/test/integration/targets/copy/tasks/tests.yml
+++ b/test/integration/targets/copy/tasks/tests.yml
@@ -102,6 +102,7 @@
   copy:
     src: foo.txt
     dest: "{{ remote_file }}"
+    decrypt: no
   register: copy_result2
 
 - name: Assert that the file was not changed
@@ -113,6 +114,7 @@
   copy:
     content: "modified"
     dest: "{{ remote_file }}"
+    decrypt: no
   register: copy_result3
 
 - name: Check the stat results of the file
@@ -137,6 +139,7 @@
     content: "modified"
     dest: "{{ remote_file }}"
     mode: 0700
+    decrypt: no
   register: copy_result4
 
 - name: Check the stat results of the file
@@ -167,6 +170,7 @@
     content: 'modified'
     dest: '{{ remote_file }}'
     mode: 0700
+    decrypt: no
   register: copy_results
 
 - name: Check the stat results of the file
@@ -191,6 +195,7 @@
     content: 'modified'
     dest: '{{ remote_file }}'
     mode: 0404
+    decrypt: no
   register: copy_results
 
 - name: Check the stat results of the file