Repository: ansible-collections/community.general

Issue:
yum_versionlock does not removes packagefrom versionlock list
### Summary

yum_versionlock does not find any package to remove from versionlock list after https://github.com/ansible-collections/community.general/commit/0a9cf3811880bd3e7640b637ffef2978df8f5429

the `fnmatch` will never find any package

```python
from fnmatch import fnmatch

packages = ['pkgX', 'versionlock', 'Loaded']

versionlock_packages = """Loaded plugins: fastestmirror, versionlock
0:pkgX-1.2.3-4.centos7.*
0:pkgY-5.6.7-8.centos7.*
versionlock list done"""

packages_list = []
for single_pkg in packages:
    if any(fnmatch(pkg, single_pkg) for pkg in versionlock_packages.split()):
        packages_list.append(single_pkg)
print(packages_list)
```
result
```python
['versionlock', 'Loaded']
```

### Issue Type

Bug Report

### Component Name

yum_versionlock

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.11.2] 
  config file = /home/qermit/.ansible.cfg
  configured module search path = ['/home/qermit/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /opt/fcc_updater/lib/python3.7/site-packages/ansible
  ansible collection location = /home/qermit/.ansible/collections:/usr/share/ansible/collections
  executable location = /opt/fcc_updater/bin/ansible
  python version = 3.7.3 (default, Jan 22 2021, 20:04:44) [GCC 8.3.0]
  jinja version = 2.11.3
  libyaml = True
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

_No response_

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- name: versionlock yum
  community.general.yum_versionlock:
    state: present
    name:
      - yum
- name: versionlock unlock yum
  community.general.yum_versionlock:
    state: absent
    name:
      - yum
```


### Expected Results

```console
$ cat /etc/yum/pluginconf.d/versionlock.list 
```

### Actual Results

```console
$ cat /etc/yum/pluginconf.d/versionlock.list 
# Added locks on Mon Jul 12 20:25:01 2021
0:yum-3.4.3-168.el7.centos.*
```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/packaging/os/yum_versionlock.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/packaging/os/yum_versionlock.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 @aminvakil @florianpaulhoberg
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I guess there should be some code to pre-process `versionlock_packages` and split it in a list of package names (without versions and the preceding `*:`).
according to manual page yum-versionlock uses following notation:
**EPOCH:NAME-VERSION-RELEASE.ARCH**
Can `NAME`, `VERSION`, `RELEASE` or `ARCH` contain dashes themselves?
as far as I know only `NAME` may contain dashes. So the regex may look like:
```regex
^(?<exclude>!)?(?<epoch>\d+):(?<name>.+)-(?<version>.+)-(?<release>.+)\.(?<arch>.+)$
```
Ran into this. The module doesn't match any exisitng versionlocks.
https://github.com/ansible-collections/community.general/blob/0a9cf3811880bd3e7640b637ffef2978df8f5429/plugins/modules/packaging/os/yum_versionlock.py#L129

I'm quite sure the match can never work. If `pack-age` is version locked, the output of `yum versionlock list` is e.g. 
`"0:pack-age-7.17.7-7.*"`. What we would pass in the module's `name` parameter is `pack-age`.

```
In [20]: pkg = "0:pack-age-7.17.7-7.*"

In [21]: pkg.split(":", 1)[-1]
Out[21]: 'pack-age-7.17.7-7.*'

In [22]: fnmatch.fnmatch(pkg.split(":", 1)[-1], "pack-age")
Out[22]: False
```
It's basically the wrong way around. fnmatch seems to be the wrong tool.

However the regex @qermit suggested will do what yum does.
```
In [15]: r = re.compile(r'^(?P<exclude>!)?(?P<epoch>\d+):(?P<name>.+)-(?P<version>.+)-(?P<releas
    ...: e>.+)\.(?P<arch>.+)$')

In [17]: m = r.match(pkg)

In [18]: m.groups()
Out[18]: (None, '0', 'pack-age', '7.17.7', '7', '*')

In [19]: m.groupdict()
Out[19]: 
{'exclude': None,
 'epoch': '0',
 'name': 'pack-age',
 'version': '7.17.7',
 'release': '7',
 'arch': '*'}
```

If I understood that correctly, replacing fnmatch with a regex match will fix this.
PRs to fix this are welcome :)
It's an interesting constellation..
We have a completely different implementation for `dnf_versionlock`, which should do similar things.
The entry format of `yum versionlock list` and `dnf versionlock list` is however different.
 - E:N-V-R.A 
 - N-E:V-R.A.

On e.g. Fedora and CentOS8 `yum` is symlinked to `dnf`. Thus, the `yum_versionlock` module can run, but the `yum versionlock list` entries look different, such that they will never match, if yum is actually dnf.

So I wonder how to fix this correctly. 

Unify both modules and make them deal with both entry formats?

Keep them seperate and enforce that yum is the good old yum and not a symlink to dnf?
:thinking: 
Keeping them separate and fail if yum is actually dnf would force playbook authors to write two branches for yum and dnf, if they have both of them, like me.

Resolving patch:
diff --git a/plugins/modules/packaging/os/yum_versionlock.py b/plugins/modules/packaging/os/yum_versionlock.py
--- a/plugins/modules/packaging/os/yum_versionlock.py
+++ b/plugins/modules/packaging/os/yum_versionlock.py
@@ -17,7 +17,7 @@
 options:
   name:
     description:
-      - Package name or a list of packages.
+      - Package name or a list of package names with optional wildcards.
     type: list
     required: true
     elements: str
@@ -74,10 +74,17 @@
     sample: present
 '''
 
+import re
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.common.text.converters import to_native
 from fnmatch import fnmatch
 
+# on DNF-based distros, yum is a symlink to dnf, so we try to handle their different entry formats.
+NEVRA_RE_YUM = re.compile(r'^(?P<exclude>!)?(?P<epoch>\d+):(?P<name>.+)-'
+                          r'(?P<version>.+)-(?P<release>.+)\.(?P<arch>.+)$')
+NEVRA_RE_DNF = re.compile(r"^(?P<exclude>!)?(?P<name>.+)-(?P<epoch>\d+):(?P<version>.+)-"
+                          r"(?P<release>.+)\.(?P<arch>.+)$")
+
 
 class YumVersionLock:
     def __init__(self, module):
@@ -102,6 +109,15 @@ def ensure_state(self, packages, command):
         self.module.fail_json(msg="Error: " + to_native(err) + to_native(out))
 
 
+def match(entry, name):
+    m = NEVRA_RE_YUM.match(entry)
+    if not m:
+        m = NEVRA_RE_DNF.match(entry)
+    if not m:
+        return False
+    return fnmatch(m.group("name"), name)
+
+
 def main():
     """ start main program to add/remove a package to yum versionlock"""
     module = AnsibleModule(
@@ -123,20 +139,20 @@ def main():
 
     # Ensure versionlock state of packages
     packages_list = []
-    if state in ('present'):
+    if state in ('present', ):
         command = 'add'
         for single_pkg in packages:
-            if not any(fnmatch(pkg.split(":", 1)[-1], single_pkg) for pkg in versionlock_packages.split()):
+            if not any(match(pkg, single_pkg) for pkg in versionlock_packages.split()):
                 packages_list.append(single_pkg)
         if packages_list:
             if module.check_mode:
                 changed = True
             else:
                 changed = yum_v.ensure_state(packages_list, command)
-    elif state in ('absent'):
+    elif state in ('absent', ):
         command = 'delete'
         for single_pkg in packages:
-            if any(fnmatch(pkg, single_pkg) for pkg in versionlock_packages.split()):
+            if any(match(pkg, single_pkg) for pkg in versionlock_packages.split()):
                 packages_list.append(single_pkg)
         if packages_list:
             if module.check_mode:


Test patch: