Repository: ansible/ansible

Issue:
playbook using sysctl module does not remove sysctl entries when state set to absent
From @blutgens on 2016-01-27T14:24:49Z
##### ISSUE TYPE

Bug Report
##### COMPONENT NAME

sysctl module
##### ANSIBLE VERSION

N/A
##### SUMMARY

I'm using the following playbook to add/remove sysctl
-------- apply-sysctl.yml -----
- name: Apply sysctl settings
  hosts: all
  tasks:
  - name: Set/Apply sysctl settings in hosts_vars/<hostname>.yml
    sysctl: 
      name: "{{ item.value.name }}"
      value: "{{ item.value.value }}"
      state: "{{ item.value.state }}"
    with_dict: sysctl | default({})
    ----------
    With a dict in a host_vars file that looks like so:
    ---- host_vars dict ----
    sysctl: 
    swappiness:
    name: vm.swappiness
    value: 50
    state: absent
    ------- host_vars dict-----

This all works great when state: present, it adds and applies and does everything it should. But when I set state: absent it seems like it works, but it just doesn't do any actual removal.

Both of these entries are in the sysctl.conf at the the start of this command as well as after:
myusername@myansiblehost: ~/ansible-plays ▸ ansible-playbook -l myhostname apply-sysctl.yml  -u root 

PLAY [Apply sysctl settings] ***************************************************

TASK [setup] *******************************************************************
ok: [myhostname]

TASK [Set/Apply sysctl settings in hosts_vars/<hostname>.yml] ******************
ok: [myhostname] => (item={'value': {u'state': u'absent', u'name': u'vm.swappiness', u'value': 50}, 'key': u'swappiness'})
ok: [myhostname] => (item={'value': {u'state': u'absent', u'name': u'vm.nr_hugepages', u'value': 484}, 'key': u'hugepages'})

PLAY RECAP *********************************************************************
myhostname                  : ok=2    changed=0    unreachable=0    failed=0  
*******************************************\* command output ********************************************************\* 
Now, if i use the sysctl module in an ad-hoc "state=absent" command, it works perfectly!

Copied from original issue: ansible/ansible-modules-core#2899



Discussion:
From @blutgens on 2016-01-27T14:24:49Z

Oh I should mention that I'm using this version:
 ansible --version
ansible 2.1.0 (devel 1b82de2354) last updated 2016/01/15 14:42:19 (GMT +000)
  lib/ansible/modules/core: (detached HEAD 002028748f) last updated 2015/12/28 20:08:17 (GMT +000)
  lib/ansible/modules/extras: (detached HEAD f6a7b6dd1f) last updated 2015/12/28 20:08:19 (GMT +000)
  config file = /home/lut0023/ansible-plays/ansible.cfg
  configured module search path = Default w/o overrides

From @blutgens on 2016-01-27T14:24:49Z

It seems if I unset the value in the dict, it will remove the entries! So this is probably NOT a bug! But I'll leave it for you smart people to decide! 

From @jimi-c on 2016-01-27T14:24:49Z

Hi @blutgens, I do believe this is a bug, as the module should most likely ignore the value when `state=absent` and remove the value (or at least check to see if the value matches and then remove it), so we'll keep this open.

From @r1k0 on 2016-01-27T14:24:49Z

problem is that the sysctl module can only remove entries if NO value is specified as parameters. Why has this been changed?

This is by design:
```# Set vm.swappiness to 5 in /etc/sysctl.conf
- sysctl: 
    name: vm.swappiness 
    value: 5
    state: present

# Remove kernel.panic entry from /etc/sysctl.conf
- sysctl:
    name: kernel.panic
    state: absent 
    sysctl_file: /etc/sysctl.conf
```

This is inconvenient because if I change my vars from:
```
{ key: 'net.ipv6.conf.all.disable_ipv6',     value: '1', state: 'present' } 
```
to
```
{ key: 'net.ipv6.conf.all.disable_ipv6',     value: '1', state: 'absent' } 
```

The entry will NOT be removed.
So now I have to edit the task instead of playing with variables.


Originally this module had the support! Look at https://github.com/ansible/ansible/pull/1810/files

```
+def sysctl_args_collapse(**sysctl_args):
+    # go ahead
+    if sysctl_args.get('key_path') is not None:
+        del sysctl_args['key_path']
+    if sysctl_args['state'] == 'absent' and 'value' in sysctl_args:
+        del sysctl_args['value']
+    
```

But this code has vanished most likely from an update. @davixx is not reponsible for that change.

When state is absent the entry should be removed regardless of value or not.
Preventing a removal when state=absent and value is not None makes no sense.

WORKAROUND

For those who will hit this bad change of design, using ansible-2.2.0 you can change in /usr/lib/python2.7/dist-packages/ansible/modules/core/system/sysctl.py

```
root@xx:~# diff   /usr/lib/python2.7/dist-packages/ansible/modules/core/system/sysctl.py /tmp/sysctl.py 
137a138,139
>         if self.args['state'] == "absent" and self.args['value'] != None: self.args['value'] = None
> 
root@xx:~# 

```
Put the updated file into your library/ folder so it's used instead of  /usr/lib/python2.7/dist-packages/ansible/modules/core/system/sysctl.py and good to go.


thank you

ps: also sysctl.py has a lot of trailing spaces, it's not nice ^^
cc @akasurde
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Possibly related to #23905
Ok. Just let me know if you need me to do anymore testing.

On Sep 11, 2017 10:07 PM, "ansibot" <notifications@github.com> wrote:

> From @jimi-c <https://github.com/jimi-c> on 2016-01-27T14:24:49Z
>
> Hi @blutgens <https://github.com/blutgens>, I do believe this is a bug,
> as the module should most likely ignore the value when state=absent and
> remove the value (or at least check to see if the value matches and then
> remove it), so we'll keep this open.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/ansible/ansible/issues/29920#issuecomment-328720675>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AA6GhlkXizY3l2IWd-OTmRJcOWixOK7jks5shfVogaJpZM4PUClj>
> .
>


Resolving patch:
diff --git a/lib/ansible/modules/system/sysctl.py b/lib/ansible/modules/system/sysctl.py
--- a/lib/ansible/modules/system/sysctl.py
+++ b/lib/ansible/modules/system/sysctl.py
@@ -165,6 +165,9 @@ def process(self):
             self.write_file = True
         elif self.file_values[thisname] is None and self.args['state'] == "absent":
             self.changed = False
+        elif self.file_values[thisname] and self.args['state'] == "absent":
+            self.changed = True
+            self.write_file = True
         elif self.file_values[thisname] != self.args['value']:
             self.changed = True
             self.write_file = True
@@ -380,20 +383,20 @@ def main():
     )
 
     if module.params['name'] is None:
-        module.fail_json(msg="name can not be None")
+        module.fail_json(msg="name cannot be None")
     if module.params['state'] == 'present' and module.params['value'] is None:
-        module.fail_json(msg="value can not be None")
+        module.fail_json(msg="value cannot be None")
 
     # In case of in-line params
     if module.params['name'] == '':
-        module.fail_json(msg="name can not be blank")
+        module.fail_json(msg="name cannot be blank")
     if module.params['state'] == 'present' and module.params['value'] == '':
-        module.fail_json(msg="value can not be blank")
+        module.fail_json(msg="value cannot be blank")
 
     result = SysctlModule(module)
 
     module.exit_json(changed=result.changed)
 
-# import module snippets
+
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/test/integration/targets/sysctl/files/sysctl.conf b/test/integration/targets/sysctl/files/sysctl.conf
--- a/test/integration/targets/sysctl/files/sysctl.conf
+++ b/test/integration/targets/sysctl/files/sysctl.conf
@@ -9,3 +9,4 @@
 #
 # For more information, see sysctl.conf(5) and sysctl.d(5).
 vm.swappiness=1
+kernel.panic=2
diff --git a/test/integration/targets/sysctl/tasks/main.yml b/test/integration/targets/sysctl/tasks/main.yml
--- a/test/integration/targets/sysctl/tasks/main.yml
+++ b/test/integration/targets/sysctl/tasks/main.yml
@@ -16,13 +16,18 @@
 # You should have received a copy of the GNU General Public License
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 
-- set_fact: output_dir_test={{output_dir}}/test_sysctl
+- set_fact:
+    output_dir_test: "{{ output_dir }}/test_sysctl"
 
 - name: make sure our testing sub-directory does not exist
-  file: path="{{ output_dir_test }}" state=absent
+  file:
+    path: "{{ output_dir_test }}"
+    state: absent
 
 - name: create our testing sub-directory
-  file: path="{{ output_dir_test }}" state=directory
+  file:
+    path: "{{ output_dir_test }}"
+    state: directory
 
 ##
 ## sysctl - file manipulation
@@ -30,41 +35,85 @@
 
 - name: copy the example conf to the test dir
   copy:
-      src: sysctl.conf
-      dest: "{{ output_dir_test }}"
+    src: sysctl.conf
+    dest: "{{ output_dir_test }}"
 
 - name: Set vm.swappiness to 5
   sysctl:
-      name: vm.swappiness
-      value: 5
-      state: present
-      reload: False
-      sysctl_file: "{{ output_dir_test }}/sysctl.conf"
+    name: vm.swappiness
+    value: 5
+    state: present
+    reload: no
+    sysctl_file: "{{ output_dir_test }}/sysctl.conf"
   register: sysctl_test0
-- debug: var=sysctl_test0
+
+- debug:
+    var: sysctl_test0
+    verbosity: 1
 
 - name: get file content
   shell: "cat {{ output_dir_test }}/sysctl.conf | egrep -v ^\\#"
   register: sysctl_content0
-- debug: var=sysctl_content0
+
+- debug:
+    var: sysctl_content0
+    verbosity: 1
 
 - name: Set vm.swappiness to 5 again
   sysctl:
       name: vm.swappiness
       value: 5
       state: present
-      reload: False
+      reload: no
       sysctl_file: "{{ output_dir_test }}/sysctl.conf"
   register: sysctl_test1
 
 - name: validate results
   assert:
       that:
-          - 'sysctl_test0.changed is defined'
-          - 'sysctl_test1.changed is defined'
-          - 'sysctl_test0.changed'
-          - 'not sysctl_test1.changed'
-          - 'sysctl_content0.stdout_lines == ["vm.swappiness=5"]'
+        - sysctl_test0 | changed
+        - not sysctl_test1 | changed
+        - 'sysctl_content0.stdout_lines[sysctl_content0.stdout_lines.index("vm.swappiness=5")] == "vm.swappiness=5"'
+
+- name: Remove kernel.panic
+  sysctl:
+    name: kernel.panic
+    value: 2
+    reload: no
+    state: absent
+    sysctl_file: "{{ output_dir_test }}/sysctl.conf"
+  register: sysctl_test2
+
+- name: get file content
+  shell: "cat {{ output_dir_test }}/sysctl.conf | egrep -v ^\\#"
+  register: sysctl_content2
+
+- debug:
+    var: item
+    verbosity: 1
+  with_items:
+    - "{{ sysctl_test2 }}"
+    - "{{ sysctl_content2 }}"
+
+- name: Validate results for key removal
+  assert:
+    that:
+      - sysctl_test2 | changed
+      - "'kernel.panic' not in sysctl_content2.stdout_lines"
+
+- name: Test remove kernel.panic again
+  sysctl:
+    name: kernel.panic
+    value: 2
+    state: absent
+    reload: no
+    sysctl_file: "{{ output_dir_test }}/sysctl.conf"
+  register: sysctl_test2_change_test
+
+- name: Assert that no change was made
+  assert:
+    that:
+      - not sysctl_test2_change_test | changed
 
 ##
 ## sysctl - sysctl_set
@@ -72,23 +121,28 @@
 
 - name: set net.ipv4.ip_forward
   sysctl:
-      name: net.ipv4.ip_forward
-      value: 1
-      sysctl_set: yes
-      reload: False
-  register: sysctl_test2
-- debug: var=sysctl_test2
+    name: net.ipv4.ip_forward
+    value: 1
+    sysctl_set: yes
+    reload: no
+  register: sysctl_test3
 
 - name: check with sysctl command
   shell: sysctl net.ipv4.ip_forward
-  register: sysctl_check2
+  register: sysctl_check3
 
-- name: validate results for test 2
+- debug:
+    var: item
+    verbosity: 1
+  with_items:
+    - "{{ sysctl_test3 }}"
+    - "{{ sysctl_check3 }}"
+
+- name: validate results for test 3
   assert:
-      that:
-          - 'sysctl_test2.changed is defined'
-          - 'sysctl_test2.changed'
-          - 'sysctl_check2.stdout_lines == ["net.ipv4.ip_forward = 1"]'
+    that:
+      - sysctl_test3 | changed
+      - 'sysctl_check3.stdout_lines == ["net.ipv4.ip_forward = 1"]'
 
 - name: Try sysctl with no name
   sysctl:
@@ -101,8 +155,8 @@
 - name: validate nameless results
   assert:
     that:
-    - "sysctl_no_name|failed"
-    - "sysctl_no_name.msg == 'name can not be None'"
+      - sysctl_no_name | failed
+      - "sysctl_no_name.msg == 'name cannot be None'"
 
 - name: Try sysctl with no value
   sysctl:
@@ -115,5 +169,5 @@
 - name: validate nameless results
   assert:
     that:
-    - "sysctl_no_value|failed"
-    - "sysctl_no_value.msg == 'value can not be None'"
\ No newline at end of file
+      - sysctl_no_value | failed
+      - "sysctl_no_value.msg == 'value cannot be None'"