Repository: ansible/ansible

Issue:
pam_limits comment adding broken.
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
pam_limits

##### ANSIBLE VERSION
```
ansible 2.3.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, May 10 2017, 20:04:28) [GCC 6.3.1 20161221 (Red Hat 6.3.1-1)]

```

##### CONFIGURATION
Default
##### OS / ENVIRONMENT
Ansible host : Fedora 25
Ansible target : Centos 7.3 (Vagrant centos/7 image)

##### SUMMARY
pam_limits breaks limits.conf file if comment is used, making any subsequent pam_limits change fail. 

##### STEPS TO REPRODUCE
- Run the below playbook. 
The behaviour was discovered in Vagrant controlled Virtualbox VM.

```yaml
---
- name: Deploy
  hosts: all
  become: true
  become_user: root
  vars:
    user1_limits:
       - { domain: user1, type: soft, name: nofile, value: 1024, comment: User1 }
       - { domain: user1, type: hard, name: nofile, value: 65536, comment: User1 }
       - { domain: user1, type: soft, name: nproc, value: 2047, comment: User1 }
       - { domain: user1, type: hard, name: nproc, value: 16384, comment: User1 }
       - { domain: user1, type: soft, name: stack, value: 10240, comment: User1 }
       - { domain: user1, type: hard, name: stack, value: 32768, comment: User1 }
  tasks:
   - name: set user limits
     pam_limits:
       limit_item: "{{ item.name }}"
       limit_type: "{{ item.type }}"
       value: "{{ item.value }}"
       domain: "{{ item.domain }}"
       comment: "{{ item.comment }}"
     with_items: "{{ user1_limits }}"
```

##### EXPECTED RESULTS
limits set with comments in limits.conf file : 

```
[...]
user1   soft    nofile  1024 # User1
user1   hard    nofile  65536   # User1
user1   soft    nproc   2047 # User1
user1   hard    nproc   16384 # User1
user1   soft    stack   10240 # User1
user1   hard    stack   32768 # User1
```

```
PLAY [Deploy] ******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [default]

TASK [set user limits] *******************************************************
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'nofile', u'value': 1024})
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'nofile', u'value': 65536})
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'nproc', u'value': 2047})
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'nproc', u'value': 16384})
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'stack', u'value': 10240})
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'stack', u'value': 32768})

PLAY RECAP *********************************************************************
default                    : ok=2    changed=1    unreachable=0    failed=0
```
##### ACTUAL RESULTS
Limits are not set at all, apart from first limit in first playbook run.
```
user1   soft    nofile  1024User1
```

If comment contains "#" itself, behaviour changes to : 

```
user1   hard    nofile  65536   ## User1
user1   soft    nproc   2047    #       ## User1
user1   hard    nproc   16384   #       #       ## User1
user1   soft    stack   10240   #       #       #       ## User1
user1   hard    stack   32768   #       #       #       #       ## User1
```

```
PLAY [Deploy] ******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [default]

TASK [set user limits] *******************************************************
changed: [default] => (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'nofile', u'value': 1024})
failed: [default] (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'nofile', u'value': 65536}) => {"failed": true, "item": {"comment": "User1", "domain": "user1", "name": "nofile", "type": "hard", "value": 65536}, "msg": "Invalid configuration of '/etc/security/limits.conf'. Current value of nofile is unsupported."}
failed: [default] (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'nproc', u'value': 2047}) => {"failed": true, "item": {"comment": "User1", "domain": "user1", "name": "nproc", "type": "soft", "value": 2047}, "msg": "Invalid configuration of '/etc/security/limits.conf'. Current value of nofile is unsupported."}
failed: [default] (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'nproc', u'value': 16384}) => {"failed": true, "item": {"comment": "User1", "domain": "user1", "name": "nproc", "type": "hard", "value": 16384}, "msg": "Invalid configuration of '/etc/security/limits.conf'. Current value of nofile is unsupported."}
failed: [default] (item={u'comment': u'User1', u'domain': u'user1', u'type': u'soft', u'name': u'stack', u'value': 10240}) => {"failed": true, "item": {"comment": "User1", "domain": "user1", "name": "stack", "type": "soft", "value": 10240}, "msg": "Invalid configuration of '/etc/security/limits.conf'. Current value of nofile is unsupported."}
failed: [default] (item={u'comment': u'User1', u'domain': u'user1', u'type': u'hard', u'name': u'stack', u'value': 32768}) => {"failed": true, "item": {"comment": "User1", "domain": "user1", "name": "stack", "type": "hard", "value": 32768}, "msg": "Invalid configuration of '/etc/security/limits.conf'. Current value of nofile is unsupported."}

PLAY RECAP *********************************************************************
default                    : ok=1    changed=0    unreachable=0    failed=1
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/system/pam_limits.py b/lib/ansible/modules/system/pam_limits.py
--- a/lib/ansible/modules/system/pam_limits.py
+++ b/lib/ansible/modules/system/pam_limits.py
@@ -215,9 +215,6 @@ def main():
         if not new_comment:
             new_comment = old_comment
 
-        if new_comment:
-            new_comment = "\t#"+new_comment
-
         line_fields = newline.split(' ')
 
         if len(line_fields) != 4:
@@ -262,6 +259,8 @@ def main():
             # Change line only if value has changed
             if new_value != actual_value:
                 changed = True
+                if new_comment:
+                    new_comment = "\t#" + new_comment
                 new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n"
                 message = new_limit
                 nf.write(new_limit)
@@ -273,6 +272,8 @@ def main():
 
     if not found:
         changed = True
+        if new_comment:
+            new_comment = "\t#"+new_comment
         new_limit = domain + "\t" + limit_type + "\t" + limit_item + "\t" + new_value + new_comment + "\n"
         message = new_limit
         nf.write(new_limit)


Test patch: