Repository: ansible/ansible

Issue:
file/copy: `attributes` should support + and - modifiers
##### ISSUE TYPE
 - Bug Report
 - Feature Idea

##### COMPONENT NAME
- file.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.2.0
```

##### CONFIGURATION
None

##### OS / ENVIRONMENT
Gentoo

##### SUMMARY
Currently, the `attributes` parameter on `file` and `copy` modules is absolute, meaning one needs to set exact flags and there's no support to add single flage without touching others. It would be good to support for example `+i` to set immutable bit and `-i` to remove it. For instance, now if one does set `attributes: i` on ext4 filesystem, Ansible will remove extent flag, which is far from desired. Right now one would need to use `attributes: ie` for ext4, and only the `i` flag for the non-ext4 filesystems.

##### 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 -->
```yaml

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
Files identified in the description:
* [lib/ansible/modules/files/file.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/files/file.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -1355,10 +1355,15 @@ def set_attributes_if_different(self, path, attributes, changed, diff=None, expa
 
         existing = self.get_file_attributes(b_path)
 
-        if existing.get('attr_flags', '') != attributes:
+        attr_mod = '='
+        if attributes.startswith(('-', '+')):
+            attr_mod = attributes[0]
+            attributes = attributes[1:]
+
+        if existing.get('attr_flags', '') != attributes or attr_mod == '-':
             attrcmd = self.get_bin_path('chattr')
             if attrcmd:
-                attrcmd = [attrcmd, '=%s' % attributes, b_path]
+                attrcmd = [attrcmd, '%s%s' % (attr_mod, attributes), b_path]
                 changed = True
 
                 if diff is not None:
@@ -1367,7 +1372,7 @@ def set_attributes_if_different(self, path, attributes, changed, diff=None, expa
                     diff['before']['attributes'] = existing.get('attr_flags')
                     if 'after' not in diff:
                         diff['after'] = {}
-                    diff['after']['attributes'] = attributes
+                    diff['after']['attributes'] = '%s%s' % (attr_mod, attributes)
 
                 if not self.check_mode:
                     try:


Test patch:
diff --git a/test/integration/targets/file/tasks/main.yml b/test/integration/targets/file/tasks/main.yml
--- a/test/integration/targets/file/tasks/main.yml
+++ b/test/integration/targets/file/tasks/main.yml
@@ -82,13 +82,13 @@
       - "file4_result.changed == true"
       - "file4_result.mode == '0600'"
 
-- name: change file attribute "A"
+- name: explicitly set file attribute "A"
   file: path={{output_dir}}/baz.txt attributes=A
   register: file_attributes_result
   ignore_errors: True
 
-- name: reapply file attribute "A"
-  file: path={{output_dir}}/baz.txt attributes=A
+- name: add file attribute "A"
+  file: path={{output_dir}}/baz.txt attributes=+A
   register: file_attributes_result_2
   when: file_attributes_result is changed
 
@@ -98,6 +98,22 @@
       - "file_attributes_result_2 is not changed"
   when: file_attributes_result is changed
 
+- name: remove file attribute "A"
+  file: path={{output_dir}}/baz.txt attributes=-A
+  register: file_attributes_result_3
+  ignore_errors: True
+
+- name: explicitly remove file attributes
+  file: path={{output_dir}}/baz.txt attributes=""
+  register: file_attributes_result_4
+  when: file_attributes_result_3 is changed
+
+- name: verify that the file was not marked as changed
+  assert:
+    that:
+      - "file_attributes_result_4 is not changed"
+  when: file_attributes_result_4 is changed
+
 - name: change ownership and group
   file: path={{output_dir}}/baz.txt owner=1234 group=1234