Repository: ansible/ansible

Issue:
spurious "changed" value using args_present in pamd module
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
pamd module

##### ANSIBLE VERSION
ansible 2.4.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/etc/ansible/library']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.5 (default, Nov  6 2016, 00:28:07) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]

##### CONFIGURATION
 ANSIBLE_PIPELINING(/etc/ansible/ansible.cfg) = True
 DEFAULT_FORKS(/etc/ansible/ansible.cfg) = 21
 DEFAULT_MODULE_PATH(/etc/ansible/ansible.cfg) = [u'/etc/ansible/library']
 PERSISTENT_CONNECT_TIMEOUT(/etc/ansible/ansible.cfg) = 30
 RETRY_FILES_SAVE_PATH(/etc/ansible/ansible.cfg) = /etc/ansible/.retry

##### OS / ENVIRONMENT
CentOS 7 managing Fedora 26

##### SUMMARY
I keep getting a changed result from pamd although nothing is being changed.

##### STEPS TO REPRODUCE
Here's the task that's getting me:

```yaml
- name: ensure lockout path is right
  pamd:
    state: args_present
    name: "{{ pam_name }}"
    type: account
    control: optional
    module_path: pam_echo.so
    module_arguments: "file=/etc/lockout.txt"
```

So obviously the point is to ensure the pam_echo.so rule (ensured to exist by an "after" rule previously) has an argument file=/etc/lockout.txt -- basically to inform a student that he or she has had their account locked and need to see me, etc. Anyway, the pam part works great. :-) So does the pamd part, actually, except every time this is run, it reports that there was a change.  That seems incorrect.

##### EXPECTED RESULTS
No change reported (well, after the first time.)

##### ACTUAL RESULTS
Here's the register output, courtesy of debug:
```
        "ansible_facts": {
            "pamd": {
                "changed": true,
                "result": {
                    "action": "args_present",
                    "change_count": 1,
                    "in_rule_0": "account   \toptional\tpam_echo.so file=/etc/lockout.txt",
                    "updated_arg_0": "file=/etc/lockout.txt"
                }
            }
        },
        "changed": true,
        "failed": false
```

And some gunk from -vvv:

```
(0, '\n{"invocation": {"module_args": {"control": "optional", "new_module_path": null, "name":
 "password-auth", "dest": "/etc/pam.d/password-auth", "module_path": "pam_echo.so", "new_type":
 null, "state": "args_present", "new_control": null, "path": "/etc/pam.d", "type": "account", 
"module_arguments": ["file=/etc/lockout.txt"]}}, "changed": true, "ansible_facts": {"pamd": 
{"changed": true, "result": {"action": "args_present", "change_count": 1, "in_rule_0":
"account   \\toptional\\tpam_echo.so file=/etc/lockout.txt", "updated_arg_0": 
"file=/etc/lockout.txt"}}}}\n', 'mesg: ttyname failed: Inappropriate ioctl for device\n')
```

So... i don't get it.  Is it a show stopper?  Well, obviously not.  But I really like to know if anything was actually changed. :-)  I've looked over the code and I'm not seeing it, but I haven't really sat down and plugged values in etc etc.  I was hoping it'd be obvious to someone, you know?

Thanks for your time.



Discussion:
Files identified in the description:
* [lib/ansible/modules/system/pamd.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/pamd.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 --->
cc @kevensen
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
So @pilou- and I were debugging that over videoconf (we were wondering about streaming that), and while we didn't found the proper fix in time, our investigation did lead us on the problem being on the regexp: https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/pamd.py#L297

As @pilou- spotted, it doesn't match '.' or '/', and that's 2 chars we can see in the args.
You nailed it.  I added \./ (not meant to be someone cheering) to both that and line 289 for good measure, and now it reports correctly.  Awesome!
@rfrancis @mscherer If you have identified a fix, please feel free to open a PR.
I'm concerned that I don't know what else I might be affecting... @mscherer ?
@rfrancis Fortunately the unit tests for this module are quite extensive, which helps to mitigate unintended side-effects.  I can open a PR (likely tomorrow) if you’d prefer.
If you're happy, I'm happy.  I'll do it right now.  Thanks!
If they run without any error, could you append this commit which adds two new unit tests (https://github.com/pilou-/ansible/commit/aa709a5d3760b57bf450ce670a9cc12acdcec3d0) on your branch and PR ?

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
@@ -286,7 +286,7 @@ def rulefromstring(cls, stringline):
                 r"""([\-A-Za-z0-9_]+)\s*         # Rule Type
                     \[([A-Za-z0-9_=\s]+)\]\s*    # Rule Control
                     ([A-Za-z0-9_\-\.]+)\s*         # Rule Path
-                    ([A-Za-z0-9,_=<>\-\s]*)""",  # Rule Args
+                    ([A-Za-z0-9,_=<>\-\s\./]*)""",  # Rule Args
                 re.X)
             complicated = True
         else:
@@ -294,7 +294,7 @@ def rulefromstring(cls, stringline):
                 r"""([\-A-Za-z0-9_]+)\s*        # Rule Type
                     ([A-Za-z0-9_]+)\s*          # Rule Control
                     ([A-Za-z0-9_\-\.]+)\s*        # Rule Path
-                    ([A-Za-z0-9,_=<>\-\s]*)""",  # Rule Args
+                    ([A-Za-z0-9,_=<>\-\s\./]*)""",  # Rule Args
                 re.X)
 
         result = pattern.match(stringline)


Test patch:
diff --git a/test/units/modules/system/test_pamd.py b/test/units/modules/system/test_pamd.py
--- a/test/units/modules/system/test_pamd.py
+++ b/test/units/modules/system/test_pamd.py
@@ -45,6 +45,22 @@ def test_more_complicated_rule(self):
         self.assertEqual(complicated, module_string.rstrip())
         self.assertEqual('try_first_pass', module.get_module_args_as_string())
 
+    def test_rule_with_arg(self):
+        line = "account       optional    pam_echo.so file=/etc/lockout.txt"
+        module = PamdRule.rulefromstring(stringline=line)
+        self.assertEqual(module.rule_type, 'account')
+        self.assertEqual(module.rule_control, 'optional')
+        self.assertEqual(module.rule_module_path, 'pam_echo.so')
+        self.assertEqual(module.rule_module_args, ['file=/etc/lockout.txt'])
+
+    def test_rule_with_args(self):
+        line = "account       optional    pam_echo.so file1=/etc/lockout1.txt file2=/etc/lockout2.txt"
+        module = PamdRule.rulefromstring(stringline=line)
+        self.assertEqual(module.rule_type, 'account')
+        self.assertEqual(module.rule_control, 'optional')
+        self.assertEqual(module.rule_module_path, 'pam_echo.so')
+        self.assertEqual(module.rule_module_args, ['file1=/etc/lockout1.txt', 'file2=/etc/lockout2.txt'])
+
     def test_less_than_in_args(self):
         rule = "auth requisite pam_succeed_if.so uid >= 1025 quiet_success"
         module = PamdRule.rulefromstring(stringline=rule)