Repository: ansible/ansible

Issue:
File module expands found file names
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
file

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  configured module search path = Default w/o overrides
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```

##### SUMMARY
<!--- Explain the problem briefly -->
File module expands file names.

##### 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.
-->
```
mkdir /tmp/testdir
touch '/tmp/testdir/$_'
ansible all -i "localhost," -c local -m file -a "path=/tmp/testdir recurse=true mode=0400"
```
<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Permission is changed

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: OSError: [Errno 13] Permission denied: '/tmp/testdir//usr/bin/python'
localhost | FAILED! => {
    "changed": false, 
    "failed": true, 
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_zETib4/ansible_module_file.py\", line 480, in <module>\n    main()\n  File \"/tmp/ansible_zETib4/ansible_module_file.py\", line 362, in main\n    changed |= recursive_set_attributes(module, to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args)\n  File \"/tmp/ansible_zETib4/ansible_module_file.py\", line 171, in recursive_set_attributes\n    changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)\n  File \"/tmp/ansible_zETib4/ansible_modlib.zip/ansible/module_utils/basic.py\", line 1298, in set_fs_attributes_if_different\n  File \"/tmp/ansible_zETib4/ansible_modlib.zip/ansible/module_utils/basic.py\", line 1064, in set_mode_if_different\nOSError: [Errno 13] Permission denied: '/tmp/testdir//usr/bin/python'\n", 
    "module_stdout": "", 
    "msg": "MODULE FAILURE", 
    "rc": 0
}
```



Discussion:
Here we have where the breaking expansion happens.

https://github.com/ansible/ansible/blob/bf63301b341ef68abb5c7c0f9d3ae7b475800f48/lib/ansible/module_utils/basic.py#L1177-L1178

...as well as the some context from the module itself.

https://github.com/ansible/ansible/blob/bf63301b341ef68abb5c7c0f9d3ae7b475800f48/lib/ansible/modules/files/file.py#L165-L185

Without having thought things through all the way it feels like the right solution involves making sure that the _file_ module always calls `set_fs_attributes_if_different()` with the `expand=False` argument.

Not sure yet whatever or not we need to compensate by doing an initial expand of the path input.

Resolving patch:
diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py
--- a/lib/ansible/modules/files/file.py
+++ b/lib/ansible/modules/files/file.py
@@ -159,18 +159,18 @@ def recursive_set_attributes(module, b_path, follow, file_args):
             if not os.path.islink(b_fsname):
                 tmp_file_args = file_args.copy()
                 tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
-                changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
+                changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
             else:
                 tmp_file_args = file_args.copy()
                 tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
-                changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
+                changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
                 if follow:
                     b_fsname = os.path.join(b_root, os.readlink(b_fsname))
                     if os.path.isdir(b_fsname):
                         changed |= recursive_set_attributes(module, b_fsname, follow, file_args)
                     tmp_file_args = file_args.copy()
                     tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict')
-                    changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
+                    changed |= module.set_fs_attributes_if_different(tmp_file_args, changed, expand=False)
     return changed
 
 
@@ -299,7 +299,7 @@ def main():
             # file is not absent and any other state is a conflict
             module.fail_json(path=path, msg='file (%s) is %s, cannot continue' % (path, prev_state))
 
-        changed = module.set_fs_attributes_if_different(file_args, changed, diff)
+        changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
         module.exit_json(path=path, changed=changed, diff=diff)
 
     elif state == 'directory':
@@ -335,7 +335,7 @@ def main():
                                 raise
                         tmp_file_args = file_args.copy()
                         tmp_file_args['path'] = curpath
-                        changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff)
+                        changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff, expand=False)
             except Exception as e:
                 module.fail_json(path=path, msg='There was an issue creating %s as requested: %s' % (curpath, to_native(e)))
 
@@ -343,7 +343,7 @@ def main():
         elif prev_state != 'directory':
             module.fail_json(path=path, msg='%s already exists as a %s' % (path, prev_state))
 
-        changed = module.set_fs_attributes_if_different(file_args, changed, diff)
+        changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
 
         if recurse:
             changed |= recursive_set_attributes(module, to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args)
@@ -435,7 +435,7 @@ def main():
         if module.check_mode and not os.path.exists(b_path):
             module.exit_json(dest=path, src=src, changed=changed, diff=diff)
 
-        changed = module.set_fs_attributes_if_different(file_args, changed, diff)
+        changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
         module.exit_json(dest=path, src=src, changed=changed, diff=diff)
 
     elif state == 'touch':
@@ -454,7 +454,7 @@ def main():
             else:
                 module.fail_json(msg='Cannot touch other than files, directories, and hardlinks (%s is %s)' % (path, prev_state))
             try:
-                module.set_fs_attributes_if_different(file_args, True, diff)
+                module.set_fs_attributes_if_different(file_args, True, diff, expand=False)
             except SystemExit as e:
                 if e.code:
                     # We take this to mean that fail_json() was called from


Test patch: