Repository: ansible/ansible

Issue:
File task fails when file does not exist, contradicting documentation
<!---
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
<!--- Name of the module/plugin/task/feature -->

File

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.2.2.0
  config file =
  configured module search path = Default w/o overrides
```

##### 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.
-->

macOS

##### SUMMARY
<!--- Explain the problem briefly -->

The [File module's documentation](http://docs.ansible.com/ansible/file_module.html) states that "Note that file will not fail if the `path` does not exist as the state did not change."

However, running tasks that attempt to change the properties of files not (yet) present fails **because the file is absent**.

##### 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.
-->

1. Create a task using the file module to ensure the state of a file as the following commands would:

    ```bash
    chown root:example .htaccess
    chmod 644 .htaccess
    ```
2. Run it against a machine where that file is not present. 

<!--- Paste example playbooks or commands between quotes below -->
```yaml
    - name: Change ownership, permissions of .htaccess files.
      file:
        path: "/var/www/example.com/docroot/{{ item }}"
        owner: "root"
        group: "example"
        mode: "u=rw,go=r"
        state: file
      with_items:
        - ".htaccess"
        - "data/.htaccess"
      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? -->

Two things:

- [x] Existing files permissions changed.
- [ ] Nonexistent files bypassed by task.

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

The task fails with the message "file ... is absent, cannot continue."

<!--- Paste verbatim command output between quotes below -->
```
failed: [example.com] (item=data/.htaccess) => {
    "failed": true,
    "invocation": {
        "module_args": {
            "backup": null,
            "content": null,
            "delimiter": null,
            "diff_peek": null,
            "directory_mode": null,
            "follow": false,
            "force": false,
            "group": "jenkins",
            "mode": "u=rwx,go=r",
            "original_basename": null,
            "owner": "root",
            "path": "/var/www/example.com/docroot/data/.htaccess",
            "recurse": false,
            "regexp": null,
            "remote_src": null,
            "selevel": null,
            "serole": null,
            "setype": null,
            "seuser": null,
            "src": null,
            "state": null,
            "unsafe_writes": null,
            "validate": null
        },
        "module_name": "file"
    },
    "item": "data/.htaccess",
    "msg": "file (/var/www/example.com/docroot/data/.htaccess) is absent, cannot continue",
    "path": "/var/www/example.com/docroot/data/.htaccess",
    "state": "absent"
}
```



Discussion:
@bcoca I added a few lines to account for this condition and line up the behavior with what's in the docs.
```
$ git diff 791a02920 134cc11d3
diff --git a/lib/ansible/modules/files/file.py b/lib/ansible/modules/files/file.py
index 68eddcec6..8ba2a3822 100644
--- a/lib/ansible/modules/files/file.py
+++ b/lib/ansible/modules/files/file.py
@@ -296,7 +296,11 @@ def main():
                 prev_state = get_state(b_path)
                 file_args['path'] = path
 
-        if prev_state not in ('file', 'hard'):
+        if prev_state == 'absent':
+            diff['after']['state'] = 'absent'
+            state_change = False
+
+        if prev_state not in ('absent', 'file', 'hard'):
             # 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))
```
It's still failing currently due to this in `module_utils/basic.py`:
```
    def selinux_context(self, path):
        context = self.selinux_initial_context()
        if not HAVE_SELINUX or not self.selinux_enabled():
            return context
        try:
            ret = selinux.lgetfilecon_raw(to_native(path, errors='surrogate_or_strict'))
        except OSError as e:
            if e.errno == errno.ENOENT:
                self.fail_json(path=path, msg='path %s does not exist' % path)
```

The failure message after fixing the initial failure in `modules/file.py` is `Path /var/www/example.com/docroot/data/.htaccess does not exist`. This is the only place I can find that message.

I'll check later on how difficult it would be to get around that. **OR**, we could default the `file.py` behavior to `touch` if it's trying to go from `absent` to `file`, in the same way that Puppet does.

Or... we could change the documentation to match the existing failure behavior. The current behavior is consistent and somewhat intuitive as well. Depends on whether we want `state=file` to be a modify-only operation as it is now, a modify-or-create operation like in Puppet, or a modify-or-skip/ok like in the docs.
I'd rather go with the second or third option. I don't think it's wise to go mes with `selinux_context` just to fix this unexpected behavior in `file`.
Possible Misunderstanding
=========================

Hi!

Thanks very much for your submission to Ansible.  It sincerely means a lot to us.

We believe the ticket you have filed is being somewhat misunderstood, as one thing works a little differently than stated.

The 'not fail' part is in the context of `state=absent` which is part of the paragraph left out in the ticket, but clearly documented, as such it is expected to fail in other cases (i.e not absent) when you are trying to set permissions.

In the future, this might be a topic more well suited for the user list, which you can also post here if you'd like some more help with the above.
   
   * https://groups.google.com/forum/#!forum/ansible-project

Thank you once again for this and your interest in Ansible!
Ah, that makes sense. Duh.
Makes sense. Though the documentation is not wonderfully clear that that's what it's talking about. Something like this would be a distinct improvement:

"Note that `absent` will not cause `file` to fail if the path does not exist as the state did not change."
Care to submit a documentation update PR?
> Care to submit a documentation update PR?

Sure!

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
@@ -46,7 +46,8 @@
         or M(template) module if you want that behavior.  If C(link), the symbolic
         link will be created or changed. Use C(hard) for hardlinks. If C(absent),
         directories will be recursively deleted, and files or symlinks will be unlinked.
-        Note that C(file) will not fail if the C(path) does not exist as the state did not change.
+        Note that C(absent) will not cause C(file) to fail if the C(path) does not exist
+        as the state did not change.
         If C(touch) (new in 1.4), an empty file will be created if the C(path) does not
         exist, while an existing file or directory will receive updated file access and
         modification times (similar to the way `touch` works from the command line).


Test patch: