Repository: ansible-collections/community.general

Issue:
5.1.0 breaks `alternatives` on RHEL 8
### Summary

When I try to run the below task, I receive a usage error from `update-alternatives`. Prior to 5.1.0, this works as expected. It looks like the `--slave` argument is missing a required value (`--slave unversioned-python /usr/bin/python3` compared to `--slave <slave_link> <slave_name> <slave_path>`). Downgrading to `5.0.2` is a workaround.

### Issue Type

Bug Report

### Component Name

alternatives

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.6]
  config file = /tmp/y/ansible.cfg
  configured module search path = ['/var/home/bpratt/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /tmp/y/venv/lib64/python3.10/site-packages/ansible
  ansible collection location = /var/home/bpratt/.ansible/collections:/usr/share/ansible/collections
  executable location = /tmp/y/venv/bin/ansible-playbook
  python version = 3.10.4 (main, Mar 25 2022, 00:00:00) [GCC 12.0.1 20220308 (Red Hat 12.0.1-0)]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /var/home/bpratt/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 5.1.0

# /tmp/y/venv/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.8.2

# /tmp/y/venv/lib64/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.8.2

```


### Configuration

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


### OS / Environment

RHEL 8

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
    - community.general.alternatives:
        name: python
        path: /usr/bin/python3
        link: /usr/bin/python
```


### Expected Results

`{"changed": true}`

### Actual Results

```console (paste below)
TASK [community.general.alternatives] *******************************************************************************************************************************************
task path: /tmp/xyz/playbook.yml:4
redirecting (type: modules) community.general.alternatives to community.general.system.alternatives
fatal: [10.0.101.1]: FAILED! => changed=false
  ansible_facts:
    discovered_interpreter_python: /usr/libexec/platform-python
  cmd: /usr/sbin/update-alternatives --install /usr/bin/python python /usr/bin/python3 50 --slave unversioned-python /usr/bin/python3 --slave unversioned-python-man /usr/share/man/man1/python3.1.gz
  msg: ''
  rc: 2
  stderr: ''
  stderr_lines: <omitted>
  stdout: |-
    alternatives version 1.19.1 - Copyright (C) 2001 Red Hat, Inc.
    This may be freely redistributed under the terms of the GNU Public License.

    usage: alternatives --install <link> <name> <path> <priority>
                        [--initscript <service>]
                        [--family <family>]
                        [--slave <slave_link> <slave_name> <slave_path>]*
           alternatives --remove <name> <path>
           alternatives --auto <name>
           alternatives --config <name>
           alternatives --display <name>
           alternatives --set <name> <path>
           alternatives --list
           alternatives --remove-all <name>
           alternatives --add-slave <name> <path> <slave_link> <slave_name> <slave_path>
           alternatives --remove-slave <name> <path> <slave_name>

    common options: --verbose --test --help --usage --version --keep-missing --keep-foreign
                    --altdir <directory> --admindir <directory>
  stdout_lines: <omitted>
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct
Alternatives subcommand is missing link if not passed explicitly
### Summary

When I try to use the alternatives module from collection version 5.1.0, it seems that the new [subcommands feature](https://github.com/ansible-collections/community.general/pull/4654) is not behaving correctly on Red Hat systems, when subcommands are already in place.
Please see the details below.

### Issue Type

Bug Report

### Component Name

alternatives

### Ansible Version

```bash
ansible --version
ansible [core 2.12.4]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.9/dist-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.5 (default, Nov 23 2021, 15:27:38) [GCC 9.3.0]
  jinja version = 3.1.1
  libyaml = True
```

### Community.general Version

```bash
ansible-galaxy collection list community.general

# /usr/local/lib/python3.9/dist-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.7.0  

# /root/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 5.1.0
```


### Configuration

```shell
ansible-config dump --only-changed
ANSIBLE_FORCE_COLOR(/azp/_work/6/s/lz/project/ansible/ansible.cfg) = True
DEFAULT_TRANSPORT(/azp/_work/6/s/lz/project/ansible/ansible.cfg) = ssh
INVENTORY_ENABLED(/azp/_work/6/s/lz/project/ansible/ansible.cfg) = ['host_list', 'script', 'auto', 'yaml', 'ini', 'azure_rm']
```


### OS / Environment

**Target system: Red Hat Enterprise Linux release 8.6 (Ootpa)**
Ansible runner: ubuntu:20.04 / Container instance in Azure

### Steps to Reproduce

```yaml
- name: Add python to alternatives system
  community.general.alternatives:
    name: python
    path: /usr/bin/python3
    link: /usr/bin/python
```
Have python3 installed:
```bash
alternatives --display python
python - status is manual.
 link currently points to /usr/bin/python3
/usr/libexec/no-python - priority 404
 slave unversioned-python: (null)
 slave unversioned-python-man: /usr/share/man/man1/unversioned-python.1.gz
/usr/bin/python3 - priority 300
 slave unversioned-python: /usr/bin/python3
 slave unversioned-python-man: /usr/share/man/man1/python3.1.gz
Current `best' version is /usr/libexec/no-python.
```


### Expected Results

I expected it would set python3 as the default python executable and any existing subcommand will be considered accordingly.

### Actual Results

Since collection version 5.1.0 the Ansible task will fail, as slave parameter are missing the slave-link argument:
```console (paste below)
fatal: [server]: FAILED! => {"changed": false, "cmd": "/sbin/update-alternatives --install /usr/bin/python python /usr/bin/python3 50 --slave unversioned-python /usr/bin/python3 --slave unversioned-python-man /usr/share/man/man1/python3.1.gz", "msg": "", "rc": 2, "stderr": "", "stderr_lines": [], "stdout": "alternatives version 1.19.1 - Copyright (C) 2001 Red Hat, Inc.\nThis may be freely redistributed under the terms of the GNU Public License.\n\nusage: alternatives --install <link> <name> <path> <priority>\n                    [--initscript <service>]\n                    [--family <family>]\n                    [--slave <slave_link> <slave_name> <slave_path>]*\n       alternatives --remove <name> <path>\n       alternatives --auto <name>\n       alternatives --config <name>\n       alternatives --display <name>\n       alternatives --set <name> <path>\n       alternatives --list\n       alternatives --remove-all <name>\n       alternatives --add-slave <name> <path> <slave_link> <slave_name> <slave_path>\n       alternatives --remove-slave <name> <path> <slave_name>\n\ncommon options: --verbose --test --help --usage --version --keep-missing --keep-foreign\n                --altdir <directory> --admindir <directory>\n", "stdout_lines": ["alternatives version 1.19.1 - Copyright (C) 2001 Red Hat, Inc.", "This may be freely redistributed under the terms of the GNU Public License.", "", "usage: alternatives --install <link> <name> <path> <priority>", "                    [--initscript <service>]", "                    [--family <family>]", "                    [--slave <slave_link> <slave_name> <slave_path>]*", "       alternatives --remove <name> <path>", "       alternatives --auto <name>", "       alternatives --config <name>", "       alternatives --display <name>", "       alternatives --set <name> <path>", "       alternatives --list", "       alternatives --remove-all <name>", "       alternatives --add-slave <name> <path> <slave_link> <slave_name> <slave_path>", "       alternatives --remove-slave <name> <path> <slave_name>", "", "common options: --verbose --test --help --usage --version --keep-missing --keep-foreign", "                --altdir <directory> --admindir <directory>"]}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/system/alternatives.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/system/alternatives.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @mulby
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @jiuka
resolved_by_pr #4810
Unfortunately that PR does not fix this issue, see https://github.com/ansible-collections/community.general/pull/4810#issuecomment-1153790825
resolved_by_pr #4836
Files identified in the description:
* [`plugins/modules/system/alternatives.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/system/alternatives.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @mulby
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @jiuka
resolved_by_pr #4810
Unfortunately that PR does not fix this issue, see https://github.com/ansible-collections/community.general/pull/4810#issuecomment-1153790825
resolved_by_pr #4836

Resolving patch:
diff --git a/plugins/modules/system/alternatives.py b/plugins/modules/system/alternatives.py
--- a/plugins/modules/system/alternatives.py
+++ b/plugins/modules/system/alternatives.py
@@ -77,6 +77,7 @@
         description:
           - The path to the symbolic link that should point to the real subcommand executable.
         type: path
+        required: true
     version_added: 5.1.0
 requirements: [ update-alternatives ]
 '''
@@ -204,7 +205,7 @@ def install(self):
 
         cmd = [self.UPDATE_ALTERNATIVES, '--install', self.link, self.name, self.path, str(self.priority)]
 
-        if self.subcommands is not None:
+        if self.module.params['subcommands'] is not None:
             subcommands = [['--slave', subcmd['link'], subcmd['name'], subcmd['path']] for subcmd in self.subcommands]
             cmd += [item for sublist in subcommands for item in sublist]
 
@@ -384,7 +385,7 @@ def main():
             subcommands=dict(type='list', elements='dict', aliases=['slaves'], options=dict(
                 name=dict(type='str', required=True),
                 path=dict(type='path', required=True),
-                link=dict(type='path'),
+                link=dict(type='path', required=True),
             )),
         ),
         supports_check_mode=True,


Test patch:
diff --git a/tests/integration/targets/alternatives/tasks/main.yml b/tests/integration/targets/alternatives/tasks/main.yml
--- a/tests/integration/targets/alternatives/tasks/main.yml
+++ b/tests/integration/targets/alternatives/tasks/main.yml
@@ -66,6 +66,8 @@
         state: absent
       with_items:
         - '{{ alternatives_dir }}/dummy'
+        - '{{ alternatives_dir }}/dummymain'
+        - '{{ alternatives_dir }}/dummysubcmd'
 
     - file:
         path: '/usr/bin/dummy{{ item }}'
diff --git a/tests/integration/targets/alternatives/tasks/subcommands.yml b/tests/integration/targets/alternatives/tasks/subcommands.yml
--- a/tests/integration/targets/alternatives/tasks/subcommands.yml
+++ b/tests/integration/targets/alternatives/tasks/subcommands.yml
@@ -32,6 +32,15 @@
     that:
       - cmd.stdout == "dummy2"
 
+- name: Get dummymain alternatives output
+  command:
+    cmd: '{{ alternatives_command }} --display dummymain'
+  register: result
+
+- name: Print result
+  debug:
+    var: result.stdout_lines
+
 - name: Subcommands are not removed if not specified
   alternatives:
     name: dummymain
@@ -75,4 +84,134 @@
   assert:
     that:
       - cmd.rc == 2
-      - '"No such file" in cmd.msg'
\ No newline at end of file
+      - '"No such file" in cmd.msg'
+
+- name: Get dummymain alternatives output
+  command:
+    cmd: '{{ alternatives_command }} --display dummymain'
+  register: result
+
+- name: Print result
+  debug:
+    var: result.stdout_lines
+
+- name: Install other alternative with subcommands
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy3'
+    link: '/usr/bin/dummymain'
+    subcommands:
+      - name: dummysubcmd
+        path: '/usr/bin/dummy4'
+        link: '/usr/bin/dummysubcmd'
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is changed'
+
+- name: Execute the current dummymain command
+  command: dummymain
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy3"
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy4"
+
+- name: Get dummymain alternatives output
+  command:
+    cmd: '{{ alternatives_command }} --display dummymain'
+  register: result
+
+- name: Print result
+  debug:
+    var: result.stdout_lines
+
+- name: Switch to first alternative
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy1'
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is changed'
+
+- name: Execute the current dummymain command
+  command: dummymain
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy1"
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+  ignore_errors: True
+
+- name: Ensure that the subcommand is gone
+  assert:
+    that:
+      - cmd.rc == 2
+      - '"No such file" in cmd.msg'
+
+- name: Get dummymain alternatives output
+  command:
+    cmd: '{{ alternatives_command }} --display dummymain'
+  register: result
+
+- name: Print result
+  debug:
+    var: result.stdout_lines
+
+- name: Switch to second alternative
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy3'
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is changed'
+
+- name: Execute the current dummymain command
+  command: dummymain
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy3"
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy4"
+
+- name: Get dummymain alternatives output
+  command:
+    cmd: '{{ alternatives_command }} --display dummymain'
+  register: result
+
+- name: Print result
+  debug:
+    var: result.stdout_lines
diff --git a/tests/integration/targets/alternatives/vars/Debian.yml b/tests/integration/targets/alternatives/vars/Debian.yml
--- a/tests/integration/targets/alternatives/vars/Debian.yml
+++ b/tests/integration/targets/alternatives/vars/Debian.yml
@@ -1,2 +1,3 @@
 ---
 alternatives_dir: /var/lib/dpkg/alternatives/
+alternatives_command: update-alternatives
diff --git a/tests/integration/targets/alternatives/vars/Suse-42.3.yml b/tests/integration/targets/alternatives/vars/Suse-42.3.yml
--- a/tests/integration/targets/alternatives/vars/Suse-42.3.yml
+++ b/tests/integration/targets/alternatives/vars/Suse-42.3.yml
@@ -1,2 +1,3 @@
 ---
 alternatives_dir: /var/lib/rpm/alternatives/
+alternatives_command: update-alternatives
diff --git a/tests/integration/targets/alternatives/vars/default.yml b/tests/integration/targets/alternatives/vars/default.yml
--- a/tests/integration/targets/alternatives/vars/default.yml
+++ b/tests/integration/targets/alternatives/vars/default.yml
@@ -1,2 +1,3 @@
 ---
 alternatives_dir: /var/lib/alternatives/
+alternatives_command: update-alternatives