Repository: ansible/ansible

Issue:
ValueError in sysctl module when parsing syntax error in sysctl.conf
<!---
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 -->
sysctl module

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.8 (default, Aug 10 2016, 10:26:01) [GCC 4.4.7 20120313 (Red Hat 4.4.7-17)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
Default

##### 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.
-->
RedHat managing RedHat hosts

##### SUMMARY
<!--- Explain the problem briefly -->
When the sysctl.conf on the managed host contains syntax errors, sysctl bails out with an error that is hard to debug.

##### 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.
-->
Create a syntax error in /etc/sysctl.conf and set up a task that uses the sysctl module and wants to parse/change sysctl variables on that host. E.g. add a line that does not start with a hasmark, and does not contain any equal signs.
The issue on our side was that we accidentally entered a comment (line without a =) into /etc/sysctl.conf and forgot to prepend it with a hashmark. 
We are just migrating to ansible, so the file was created mostly manually, it's not yet (fully) under ansible control.

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: set up sysctl values - IF THIS FAILS, check sysctl.conf for syntax errors
  sysctl:
    name:      "{{ item.name }}"
    value:     "{{ item.value }}"
    state:      present
    sysctl_set: yes
    reload:     no
  with_items:
    - "{{ sysctl_values }}"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Reporting this as an error condition makes sense, but I would expect something that tells me that there was a syntax error in the file, not a straight ValueError from python that is hard to understand. The line.split('=',1) gave it away but I don't think it's really easy to understand what's going on. 

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
failed: [testhost1] (item={u'name': u'net.ipv4.tcp_sack', u'value': u'0'}) => {
    "failed": true, 
    "item": {
        "name": "net.ipv4.tcp_sack", 
        "value": "0"
    }, 
    "module_stderr": "Shared connection to testhost1 closed.\r\n", 
    "module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_BLCuvV/ansible_module_sysctl.py\", line 397, in <module>\r\n    main()\r\n  File \"/tmp/ansible_BLCuvV/ansible_module_sysctl.py\", line 391, in main\r\n    result = SysctlModule(module)\r\n  File \"/tmp/ansible_BLCuvV/ansible_module_sysctl.py\", line 142, in __init__\r\n    self.process()\r\n  File \"/tmp/ansible_BLCuvV/ansible_module_sysctl.py\", line 162, in process\r\n    self.read_sysctl_file()\r\n  File \"/tmp/ansible_BLCuvV/ansible_module_sysctl.py\", line 323, in read_sysctl_file\r\n    k, v = line.split('=',1)\r\nValueError: need more than 1 value to unpack\r\n", 
    "msg": "MODULE FAILURE", 
    "rc": 1
}

```



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

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
@@ -122,6 +122,7 @@
 from ansible.module_utils.basic import get_platform, AnsibleModule
 from ansible.module_utils.six import string_types
 from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
+from ansible.module_utils._text import to_native
 
 
 class SysctlModule(object):
@@ -140,7 +141,7 @@ def __init__(self, module):
 
         self.changed = False    # will change occur
         self.set_proc = False   # does sysctl need to set value
-        self.write_file = False # does the sysctl file need to be reloaded
+        self.write_file = False  # does the sysctl file need to be reloaded
 
         self.process()
 
@@ -240,7 +241,7 @@ def get_token_curr_value(self, token):
             thiscmd = "%s -n %s" % (self.sysctl_cmd, token)
         else:
             thiscmd = "%s -e -n %s" % (self.sysctl_cmd, token)
-        rc,out,err = self.module.run_command(thiscmd)
+        rc, out, err = self.module.run_command(thiscmd)
         if rc != 0:
             return None
         else:
@@ -264,7 +265,7 @@ def set_token_value(self, token, value):
             if self.args['ignoreerrors']:
                 ignore_missing = '-e'
             thiscmd = "%s %s -w %s=%s" % (self.sysctl_cmd, ignore_missing, token, value)
-        rc,out,err = self.module.run_command(thiscmd)
+        rc, out, err = self.module.run_command(thiscmd)
         if rc != 0:
             self.module.fail_json(msg='setting %s failed: %s' % (token, out + err))
         else:
@@ -275,7 +276,7 @@ def reload_sysctl(self):
         # do it
         if self.platform == 'freebsd':
             # freebsd doesn't support -p, so reload the sysctl service
-            rc,out,err = self.module.run_command('/etc/rc.d/sysctl reload')
+            rc, out, err = self.module.run_command('/etc/rc.d/sysctl reload')
         elif self.platform == 'openbsd':
             # openbsd doesn't support -p and doesn't have a sysctl service,
             # so we have to set every value with its own sysctl call
@@ -293,7 +294,7 @@ def reload_sysctl(self):
             if self.args['ignoreerrors']:
                 sysctl_args.insert(1, '-e')
 
-            rc,out,err = self.module.run_command(sysctl_args)
+            rc, out, err = self.module.run_command(sysctl_args)
 
         if rc != 0:
             self.module.fail_json(msg="Failed to reload sysctl: %s" % str(out) + str(err))
@@ -308,21 +309,20 @@ def read_sysctl_file(self):
         lines = []
         if os.path.isfile(self.sysctl_file):
             try:
-                f = open(self.sysctl_file, "r")
-                lines = f.readlines()
-                f.close()
+                with open(self.sysctl_file, "r") as read_file:
+                    lines = read_file.readlines()
             except IOError as e:
-                self.module.fail_json(msg="Failed to open %s: %s" % (self.sysctl_file, str(e)))
+                self.module.fail_json(msg="Failed to open %s: %s" % (self.sysctl_file, to_native(e)))
 
         for line in lines:
             line = line.strip()
             self.file_lines.append(line)
 
-            # don't split empty lines or comments
-            if not line or line.startswith(("#", ";")):
+            # don't split empty lines or comments or line without equal sign
+            if not line or line.startswith(("#", ";")) or "=" not in line:
                 continue
 
-            k, v = line.split('=',1)
+            k, v = line.split('=', 1)
             k = k.strip()
             v = v.strip()
             self.file_values[k] = v.strip()
@@ -332,11 +332,11 @@ def fix_lines(self):
         checked = []
         self.fixed_lines = []
         for line in self.file_lines:
-            if not line.strip() or line.strip().startswith(("#", ";")):
+            if not line.strip() or line.strip().startswith(("#", ";")) or "=" not in line:
                 self.fixed_lines.append(line)
                 continue
             tmpline = line.strip()
-            k, v = tmpline.split('=',1)
+            k, v = tmpline.split('=', 1)
             k = k.strip()
             v = v.strip()
             if k not in checked:
@@ -357,12 +357,12 @@ def fix_lines(self):
     def write_sysctl(self):
         # open a tmp file
         fd, tmp_path = tempfile.mkstemp('.conf', '.ansible_m_sysctl_', os.path.dirname(self.sysctl_file))
-        f = open(tmp_path,"w")
+        f = open(tmp_path, "w")
         try:
             for l in self.fixed_lines:
                 f.write(l.strip() + "\n")
         except IOError as e:
-            self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, str(e)))
+            self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, to_native(e)))
         f.flush()
         f.close()
 
@@ -377,14 +377,14 @@ def main():
 
     # defining module
     module = AnsibleModule(
-        argument_spec = dict(
-            name = dict(aliases=['key'], required=True),
-            value = dict(aliases=['val'], required=False, type='str'),
-            state = dict(default='present', choices=['present', 'absent']),
-            reload = dict(default=True, type='bool'),
-            sysctl_set = dict(default=False, type='bool'),
-            ignoreerrors = dict(default=False, type='bool'),
-            sysctl_file = dict(default='/etc/sysctl.conf', type='path')
+        argument_spec=dict(
+            name=dict(aliases=['key'], required=True),
+            value=dict(aliases=['val'], required=False, type='str'),
+            state=dict(default='present', choices=['present', 'absent']),
+            reload=dict(default=True, type='bool'),
+            sysctl_set=dict(default=False, type='bool'),
+            ignoreerrors=dict(default=False, type='bool'),
+            sysctl_file=dict(default='/etc/sysctl.conf', type='path')
         ),
         supports_check_mode=True,
         required_if=[('state', 'present', ['value'])],


Test patch:
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -519,7 +519,6 @@ lib/ansible/modules/system/seport.py
 lib/ansible/modules/system/service.py
 lib/ansible/modules/system/solaris_zone.py
 lib/ansible/modules/system/svc.py
-lib/ansible/modules/system/sysctl.py
 lib/ansible/modules/system/systemd.py
 lib/ansible/modules/system/timezone.py
 lib/ansible/modules/system/ufw.py