Repository: ansible-collections/community.general

Issue:
pamd module removes comment when inserting a rule before another rule
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
When trying to add a new rule before an existing rule the comment that is on the line before the existing rule is removed.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
pamd

##### ANSIBLE VERSION
```paste below
 ansible --version
/home/ansible/.venv/ansible/lib/python2.7/site-packages/cryptography/__init__.py:39: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in a future release.
  CryptographyDeprecationWarning,
ansible 2.9.11
  config file = /home/ansible/Orlando/ansible.cfg
  configured module search path = [u'/home/ansible/REDACTED/library']
  ansible python module location = /home/ansible/.venv/ansible/lib/python2.7/site-packages/ansible
  executable location = /home/ansible/.venv/ansible/bin/ansible
  python version = 2.7.5 (default, Aug  7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
```

##### STEPS TO REPRODUCE
Running the following playbook on a Proxmox 6.2 (debian buster) system.

```yaml
  tasks:
    - name: 'Add PAM rule to stop authentication attempts'
      pamd:
        name: 'common-auth'
        type: 'auth'
        control: '[success=1 default=ignore]'
        module_path: 'pam_unix.so'
        new_type: 'auth'
        new_control: 'required'
        new_module_path: 'pam_tally2.so'
        module_arguments:
          - 'onerr=fail'
          - 'deny={{ NIST_AC_7_Unsuccessful_Attempts }}'
          - 'unlock_time={{ NIST_AC_7_Unlock_Time }}'
          - 'audit'
        state: 'before'
      vars:
        NIST_AC_7_Unsuccessful_Attempts: 3
        NIST_AC_7_Unlock_Time: 90
        NIST_AC_7_Root_Unlock_Time: 900

```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
I expect the the file /etc/pam.d/common-auth to have the following contents:
```
#
# Updated by Ansible - 2020-11-25T21:32:34.718626
# /etc/pam.d/common-auth - authentication settings common to all services
#
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.).  The default is to use the
# traditional Unix authentication mechanisms.
#
# As of pam 1.0.1-6, this file is managed by pam-auth-update by default.
# To take advantage of this, it is recommended that you configure any
# local modules either before or after the default block, and use
# pam-auth-update to manage selection of other modules.  See
# pam-auth-update(8) for details.

# here are the per-package modules (the "Primary" block)
auth       required pam_tally2.so onerr=fail deny=3 unlock_time=90 audit
auth       [success=1 default=ignore] pam_unix.so nullok_secure
```
##### ACTUAL RESULTS
But instead the comment that was before the rule where the new rule is being added is deleted:
```
#
# Updated by Ansible - 2020-11-25T21:32:34.718626
# /etc/pam.d/common-auth - authentication settings common to all services
#
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.).  The default is to use the
# traditional Unix authentication mechanisms.
#
# As of pam 1.0.1-6, this file is managed by pam-auth-update by default.
# To take advantage of this, it is recommended that you configure any
# local modules either before or after the default block, and use
# pam-auth-update to manage selection of other modules.  See
# pam-auth-update(8) for details.

auth       required pam_tally2.so onerr=fail deny=3 unlock_time=90 audit
auth       [success=1 default=ignore] pam_unix.so nullok_secure
```



Discussion:
Files identified in the description:
* [`plugins/modules/system/pamd.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/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 --->

Resolving patch:
diff --git a/plugins/modules/system/pamd.py b/plugins/modules/system/pamd.py
--- a/plugins/modules/system/pamd.py
+++ b/plugins/modules/system/pamd.py
@@ -287,7 +287,7 @@ def __init__(self, line):
 
     @property
     def is_valid(self):
-        if self.line == '':
+        if self.line.strip() == '':
             return True
         return False
 
@@ -304,6 +304,10 @@ def __str__(self):
         return str(self.line)
 
 
+class PamdEmptyLine(PamdLine):
+    pass
+
+
 class PamdComment(PamdLine):
 
     def __init__(self, line):
@@ -445,8 +449,8 @@ def __init__(self, content):
                 pamd_line = PamdComment(line)
             elif line.lstrip().startswith('@include'):
                 pamd_line = PamdInclude(line)
-            elif line == '':
-                pamd_line = PamdLine(line)
+            elif line.strip() == '':
+                pamd_line = PamdEmptyLine(line)
             else:
                 pamd_line = PamdRule.rule_from_string(line)
 
@@ -545,7 +549,7 @@ def insert_before(self, rule_type, rule_control, rule_path,
 
             # Next we may have to loop backwards if the previous line is a comment.  If it
             # is, we'll get the previous "rule's" previous.
-            while previous_rule is not None and isinstance(previous_rule, PamdComment):
+            while previous_rule is not None and isinstance(previous_rule, (PamdComment, PamdEmptyLine)):
                 previous_rule = previous_rule.prev
             # Next we'll see if the previous rule matches what we are trying to insert.
             if previous_rule is not None and not previous_rule.matches(new_type, new_control, new_path):
@@ -589,7 +593,7 @@ def insert_after(self, rule_type, rule_control, rule_path,
             next_rule = current_rule.next
             # Next we may have to loop forwards if the next line is a comment.  If it
             # is, we'll get the next "rule's" next.
-            while next_rule is not None and isinstance(next_rule, PamdComment):
+            while next_rule is not None and isinstance(next_rule, (PamdComment, PamdEmptyLine)):
                 next_rule = next_rule.next
 
             # First we create a new rule
@@ -780,13 +784,8 @@ def main():
         required_if=[
             ("state", "args_present", ["module_arguments"]),
             ("state", "args_absent", ["module_arguments"]),
-            ("state", "before", ["new_control"]),
-            ("state", "before", ["new_type"]),
-            ("state", "before", ["new_module_path"]),
-            ("state", "after", ["new_control"]),
-            ("state", "after", ["new_type"]),
-            ("state", "after", ["new_module_path"]),
-
+            ("state", "before", ["new_control", "new_type", "new_module_path"]),
+            ("state", "after", ["new_control", "new_type", "new_module_path"]),
         ],
     )
     content = str()
@@ -798,9 +797,7 @@ def main():
             content = service_file_obj.read()
     except IOError as e:
         # If unable to read the file, fail out
-        module.fail_json(msg='Unable to open/read PAM module \
-                            file %s with error %s.' %
-                         (fname, str(e)))
+        module.fail_json(msg='Unable to open/read PAM module file %s with error %s.' % (fname, str(e)))
 
     # Assuming we didn't fail, create the service
     service = PamdService(content)


Test patch: