Repository: ansible/ansible

Issue:
pam module not updating files
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
pamd module
##### ANSIBLE VERSION
```
ansible 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.13 (default, Dec 18 2016, 07:03:39) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]
```

##### CONFIGURATION

bog standard

##### OS / ENVIRONMENT

mac os 10.12.4


##### SUMMARY

ansible pam module does not update files correctly

##### STEPS TO REPRODUCE

using the play and example file below, no changes are executed

<!--- Paste example playbooks or commands between quotes below -->
```
- name: insert pam_wheel requirement because it doesnt exist
  pamd:
    name: su
    type: auth
    control: required
    module_path: pam_wheel.so
    module_arguments: 'use_uid'
    state: before
    new_type: auth
    new_control: sufficient
    new_module_path: pam_rootok.so
	

#%PAM-1.0
# Uncomment the following line to implicitly trust users in the "wheel" group.
#auth           sufficient      pam_wheel.so trust use_uid
# Uncomment the following line to require a user to be in the "wheel" group.
auth            sufficient      pam_rootok.so
auth            substack        system-auth
auth            include postlogin

account         sufficient      pam_succeed_if.so uid = 0 use_uid quiet
```

##### EXPECTED RESULTS
given the above play and file, the line
``` auth required pam_wheel.so use_uid```
should be inserted after ```auth sufficient pam_rootok.so``` but it is not
##### ACTUAL RESULTS
no change occurs
```
PLAY [all] ***********************************************************************************************************************************************************

TASK [testplay : insert pam_wheel requirement because it doesnt exist] ******************************************************************************************
task path: /Users/excutinguser/repos/ansible-repo/roles/testplay/tasks/main.yml:867
Using module file /usr/local/lib/python2.7/site-packages/ansible/modules/system/pamd.py
<targethost> ESTABLISH SSH CONNECTION FOR USER: ansibleuser
<targethost> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ansibleuser -o ConnectTimeout=10 -o ControlPath=/Users/excutinguser/.ansible/cp/7963328e97 targethost '/bin/sh -c '"'"'echo ~ && sleep 0'"'"''
<targethost> (0, '/home/ansibleuser\n', '')
<targethost> ESTABLISH SSH CONNECTION FOR USER: ansibleuser
<targethost> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ansibleuser -o ConnectTimeout=10 -o ControlPath=/Users/excutinguser/.ansible/cp/7963328e97 targethost '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570 `" && echo ansible-tmp-1494281124.64-106881662588570="` echo /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570 `" ) && sleep 0'"'"''
<targethost> (0, 'ansible-tmp-1494281124.64-106881662588570=/home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570\n', '')
<targethost> PUT /var/folders/d9/7d_sb4mj1bz1jywsy8rrpj61n4xnb5/T/tmpn5u5tS TO /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/pamd.py
<targethost> SSH: EXEC sftp -b - -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ansibleuser -o ConnectTimeout=10 -o ControlPath=/Users/excutinguser/.ansible/cp/7963328e97 '[targethost]'
<targethost> (0, 'sftp> put /var/folders/d9/7d_sb4mj1bz1jywsy8rrpj61n4xnb5/T/tmpn5u5tS /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/pamd.py\n', '')
<targethost> ESTABLISH SSH CONNECTION FOR USER: ansibleuser
<targethost> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ansibleuser -o ConnectTimeout=10 -o ControlPath=/Users/excutinguser/.ansible/cp/7963328e97 targethost '/bin/sh -c '"'"'chmod u+x /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/ /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/pamd.py && sleep 0'"'"''
<targethost> (0, '', '')
<targethost> ESTABLISH SSH CONNECTION FOR USER: ansibleuser
<targethost> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ansibleuser -o ConnectTimeout=10 -o ControlPath=/Users/excutinguser/.ansible/cp/7963328e97 -tt targethost '/bin/sh -c '"'"'sudo -H -S -n -u root /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-uksgmxxzejdxgcahpebkeiztjrtjbshs; /usr/bin/python /home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/pamd.py; rm -rf "/home/ansibleuser/.ansible/tmp/ansible-tmp-1494281124.64-106881662588570/" > /dev/null 2>&1'"'"'"'"'"'"'"'"' && sleep 0'"'"''
<targethost> (0, '\r\n{"invocation": {"module_args": {"control": "required", "new_module_path": "pam_rootok.so", "name": "su", "dest": "/etc/pam.d/su", "module_path": "pam_wheel.so", "new_type": "auth", "state": "before", "new_control": "sufficient", "path": "/etc/pam.d", "type": "auth", "module_arguments": ["use_uid"]}}, "changed": false, "ansible_facts": {"pamd": {"changed": false, "result": {"action": "insert_before_rule", "change_count": 0}}}}\r\n', 'Shared connection to targethost closed.\r\n')
ok: [targethost] => {
    "ansible_facts": {
        "pamd": {
            "changed": false, 
            "result": {
                "action": "insert_before_rule", 
                "change_count": 0
            }
        }
    }, 
    "changed": false, 
    "invocation": {
        "module_args": {
            "control": "required", 
            "dest": "/etc/pam.d/su", 
            "module_arguments": [
                "use_uid"
            ], 
            "module_path": "pam_wheel.so", 
            "name": "su", 
            "new_control": "sufficient", 
            "new_module_path": "pam_rootok.so", 
            "new_type": "auth", 
            "path": "/etc/pam.d", 
            "state": "before", 
            "type": "auth"
        }
    }
}

```



Discussion:
@theballdredge I think, you are trying to add an existing module `pam_rootok.so` using `new_module_path` argument. This will result in no change in `/etc/pam.d/su` file as module exists previously. 

You can try following Ansible Playbooks to have 
`auth required pam_wheel.so use_uid` inserted after `auth sufficient pam_rootok.so`

```yaml
---
- name: Debugging
  hosts: localhost
  tasks:
    - name: insert pam_wheel requirement because it doesnt exist
      pamd:
        name: su
        type: auth
        control: sufficient
        module_path: pam_rootok.so
        module_arguments: 'use_uid'
        state: after
        new_type: auth
        new_control: required
        new_module_path: pam_wheel.so
```
Notice that `new_module_path` is `pam_wheel.so` which needs to be inserted after `pam_rootok.so`

needs_info
the line exists but is commented out - i was expecting the line to ether be uncommented or have a new line inserted with the correct type/control/module settings
The commented module line comes by-default from PAM package. Did you try my suggestion ?
yes, that appears to have worked.  it might be beneficial to update the docs a bit for this, maybe expanding a bit on the 'new_*' values role when using before/after?

Resolving patch:
diff --git a/lib/ansible/modules/system/pamd.py b/lib/ansible/modules/system/pamd.py
--- a/lib/ansible/modules/system/pamd.py
+++ b/lib/ansible/modules/system/pamd.py
@@ -56,19 +56,15 @@
       - The module path of the PAM rule being modified.  The type,
         control and module_path all must match a rule to be modified.
   new_type:
-    required: false
     description:
-    - The type to assign to the new rule.
+    - The new type to assign to the new rule.
   new_control:
-    required: false
     description:
-    - The control to assign to the new rule.
+    - The new control to assign to the new rule.
   new_module_path:
-    required: false
     description:
-    - The control to assign to the new rule.
+    - The new module path to be assigned to the new rule.
   module_arguments:
-    required: false
     description:
     - When state is 'updated', the module_arguments will replace existing
       module_arguments.  When state is 'args_absent' args matching those
@@ -78,7 +74,6 @@
       takes a value denoted by '=', the value will be changed to that specified
       in module_arguments.
   state:
-    required: false
     default: updated
     choices:
     - updated
@@ -96,7 +91,6 @@
       must all be specified.  If state is 'args_absent' or 'args_present',
       new_type, new_control, and new_module_path will be ignored.
   path:
-    required: false
     default: /etc/pam.d/
     description:
     - This is the path to the PAM service files
@@ -130,15 +124,16 @@
     new_module_path: pam_faillock.so
     state: before
 
-- name: Insert a new rule after an existing rule
+- name: Insert a new rule pam_wheel.so with argument 'use_uid' after an existing rule pam_rootok.so
   pamd:
-    name: system-auth
+    name: su
     type: auth
-    control: required
-    module_path: pam_faillock.so
+    control: sufficient
+    module_path: pam_rootok.so
     new_type: auth
-    new_control: sufficient
-    new_module_path: pam_faillock.so
+    new_control: required
+    new_module_path: pam_wheel.so
+    module_arguments: 'use_uid'
     state: after
 
 - name: Remove module arguments from an existing rule
@@ -233,7 +228,7 @@ def rulefromstring(cls, stringline):
 
         if rule_control.startswith('['):
             rule_control = stringline[stringline.index('['):
-                                      stringline.index(']')+1]
+                                      stringline.index(']') + 1]
 
         if "]" in split_line[2]:
             rule_module_path = split_line[3]
@@ -318,7 +313,7 @@ def update_rule(service, old_rule, new_rule):
             except AttributeError:
                 pass
             if changed:
-                result['updated_rule_'+str(change_count)] = str(rule)
+                result['updated_rule_' + str(change_count)] = str(rule)
                 result['new_rule'] = str(new_rule)
 
                 change_count += 1
@@ -340,16 +335,16 @@ def insert_before_rule(service, old_rule, new_rule):
             if index == 0:
                 service.rules.insert(0, new_rule)
                 changed = True
-            elif (new_rule.rule_type != service.rules[index-1].rule_type or
+            elif (new_rule.rule_type != service.rules[index - 1].rule_type or
                     new_rule.rule_control !=
-                    service.rules[index-1].rule_control or
+                    service.rules[index - 1].rule_control or
                     new_rule.rule_module_path !=
-                    service.rules[index-1].rule_module_path):
+                    service.rules[index - 1].rule_module_path):
                 service.rules.insert(index, new_rule)
                 changed = True
             if changed:
                 result['new_rule'] = str(new_rule)
-                result['before_rule_'+str(change_count)] = str(rule)
+                result['before_rule_' + str(change_count)] = str(rule)
                 change_count += 1
         index += 1
     result['change_count'] = change_count
@@ -365,16 +360,16 @@ def insert_after_rule(service, old_rule, new_rule):
         if (old_rule.rule_type == rule.rule_type and
                 old_rule.rule_control == rule.rule_control and
                 old_rule.rule_module_path == rule.rule_module_path):
-            if (new_rule.rule_type != service.rules[index+1].rule_type or
+            if (new_rule.rule_type != service.rules[index + 1].rule_type or
                     new_rule.rule_control !=
-                    service.rules[index+1].rule_control or
+                    service.rules[index + 1].rule_control or
                     new_rule.rule_module_path !=
-                    service.rules[index+1].rule_module_path):
-                service.rules.insert(index+1, new_rule)
+                    service.rules[index + 1].rule_module_path):
+                service.rules.insert(index + 1, new_rule)
                 changed = True
             if changed:
                 result['new_rule'] = str(new_rule)
-                result['after_rule_'+str(change_count)] = str(rule)
+                result['after_rule_' + str(change_count)] = str(rule)
                 change_count += 1
         index += 1
 
@@ -396,8 +391,8 @@ def remove_module_arguments(service, old_rule, module_args):
                     if arg == arg_to_remove:
                         rule.rule_module_args.remove(arg)
                         changed = True
-                        result['removed_arg_'+str(change_count)] = arg
-                        result['from_rule_'+str(change_count)] = str(rule)
+                        result['removed_arg_' + str(change_count)] = arg
+                        result['from_rule_' + str(change_count)] = str(rule)
                         change_count += 1
 
     result['change_count'] = change_count
@@ -415,15 +410,15 @@ def add_module_arguments(service, old_rule, module_args):
                 old_rule.rule_module_path == rule.rule_module_path):
             for arg_to_add in module_args:
                 if "=" in arg_to_add:
-                    pre_string = arg_to_add[:arg_to_add.index('=')+1]
+                    pre_string = arg_to_add[:arg_to_add.index('=') + 1]
                     indicies = [i for i, arg
                                 in enumerate(rule.rule_module_args)
                                 if arg.startswith(pre_string)]
                     if len(indicies) == 0:
                         rule.rule_module_args.append(arg_to_add)
                         changed = True
-                        result['added_arg_'+str(change_count)] = arg_to_add
-                        result['to_rule_'+str(change_count)] = str(rule)
+                        result['added_arg_' + str(change_count)] = arg_to_add
+                        result['to_rule_' + str(change_count)] = str(rule)
                         change_count += 1
                     else:
                         for i in indicies:
@@ -438,8 +433,8 @@ def add_module_arguments(service, old_rule, module_args):
                 elif arg_to_add not in rule.rule_module_args:
                     rule.rule_module_args.append(arg_to_add)
                     changed = True
-                    result['added_arg_'+str(change_count)] = arg_to_add
-                    result['to_rule_'+str(change_count)] = str(rule)
+                    result['added_arg_' + str(change_count)] = arg_to_add
+                    result['to_rule_' + str(change_count)] = str(rule)
                     change_count += 1
     result['change_count'] = change_count
     return changed, result
@@ -450,13 +445,13 @@ def write_rules(service):
 
     f = open(service.fname, 'w')
     for amble in service.preamble:
-        f.write(amble+'\n')
+        f.write(amble + '\n')
 
     for rule in service.rules:
         if (previous_rule is not None and
                 previous_rule.rule_type != rule.rule_type):
             f.write('\n')
-        f.write(str(rule)+'\n')
+        f.write(str(rule) + '\n')
         previous_rule = rule
     f.close()
 


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
@@ -667,7 +667,6 @@ lib/ansible/modules/system/open_iscsi.py
 lib/ansible/modules/system/openwrt_init.py
 lib/ansible/modules/system/osx_defaults.py
 lib/ansible/modules/system/pam_limits.py
-lib/ansible/modules/system/pamd.py
 lib/ansible/modules/system/puppet.py
 lib/ansible/modules/system/runit.py
 lib/ansible/modules/system/seboolean.py