diff --git a/changelogs/fragments/9096-alternatives-add-family-parameter.yml b/changelogs/fragments/9096-alternatives-add-family-parameter.yml new file mode 100644 index 0000000000..a0b021f892 --- /dev/null +++ b/changelogs/fragments/9096-alternatives-add-family-parameter.yml @@ -0,0 +1,2 @@ +minor_changes: + - alternatives - add ``family`` parameter that allows to utilize the ``--family`` option available in RedHat version of update-alternatives (https://github.com/ansible-collections/community.general/issues/5060, https://github.com/ansible-collections/community.general/pull/9096). diff --git a/plugins/modules/alternatives.py b/plugins/modules/alternatives.py index da578276fa..d049c82b11 100644 --- a/plugins/modules/alternatives.py +++ b/plugins/modules/alternatives.py @@ -39,7 +39,11 @@ options: description: - The path to the real executable that the link should point to. type: path - required: true + family: + description: + - The family groups similar alternatives. This option is available only on RHEL-based distributions. + type: str + version_added: 10.1.0 link: description: - The path to the symbolic link that should point to the real executable. @@ -98,6 +102,12 @@ EXAMPLES = r''' name: java path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java +- name: Select java-11-openjdk.x86_64 family + community.general.alternatives: + name: java + family: java-11-openjdk.x86_64 + when: ansible_os_family == 'RedHat' + - name: Alternatives link created community.general.alternatives: name: hadoop-conf @@ -182,17 +192,25 @@ class AlternativesModule(object): subcommands_parameter = self.module.params['subcommands'] priority_parameter = self.module.params['priority'] if ( - self.path not in self.current_alternatives or - (priority_parameter is not None and self.current_alternatives[self.path].get('priority') != priority_parameter) or - (subcommands_parameter is not None and ( - not all(s in subcommands_parameter for s in self.current_alternatives[self.path].get('subcommands')) or - not all(s in self.current_alternatives[self.path].get('subcommands') for s in subcommands_parameter) - )) + self.path is not None and ( + self.path not in self.current_alternatives or + (priority_parameter is not None and self.current_alternatives[self.path].get('priority') != priority_parameter) or + (subcommands_parameter is not None and ( + not all(s in subcommands_parameter for s in self.current_alternatives[self.path].get('subcommands')) or + not all(s in self.current_alternatives[self.path].get('subcommands') for s in subcommands_parameter) + )) + ) ): self.install() # Check if we need to set the preference - if self.mode_selected and self.current_path != self.path: + is_same_path = self.path is not None and self.current_path == self.path + is_same_family = False + if self.current_path is not None and self.current_path in self.current_alternatives: + current_alternative = self.current_alternatives[self.current_path] + is_same_family = current_alternative.get('family') == self.family + + if self.mode_selected and not (is_same_path or is_same_family): self.set() # Check if we need to reset to auto @@ -213,6 +231,8 @@ class AlternativesModule(object): self.module.fail_json(msg='Needed to install the alternative, but unable to do so as we are missing the link') cmd = [self.UPDATE_ALTERNATIVES, '--install', self.link, self.name, self.path, str(self.priority)] + if self.family is not None: + cmd.extend(["--family", self.family]) if self.module.params['subcommands'] is not None: subcommands = [['--slave', subcmd['link'], subcmd['name'], subcmd['path']] for subcmd in self.subcommands] @@ -228,6 +248,7 @@ class AlternativesModule(object): self.result['diff']['after'] = dict( state=AlternativeState.PRESENT, path=self.path, + family=self.family, priority=self.priority, link=self.link, ) @@ -248,9 +269,15 @@ class AlternativesModule(object): self.result['diff']['after'] = dict(state=AlternativeState.ABSENT) def set(self): - cmd = [self.UPDATE_ALTERNATIVES, '--set', self.name, self.path] + # Path takes precedence over family as it is more specific + if self.path is None: + arg = self.family + else: + arg = self.path + + cmd = [self.UPDATE_ALTERNATIVES, '--set', self.name, arg] self.result['changed'] = True - self.messages.append("Set alternative '%s' for '%s'." % (self.path, self.name)) + self.messages.append("Set alternative '%s' for '%s'." % (arg, self.name)) if not self.module.check_mode: self.module.run_command(cmd, check_rc=True) @@ -277,6 +304,10 @@ class AlternativesModule(object): def path(self): return self.module.params.get('path') + @property + def family(self): + return self.module.params.get('family') + @property def link(self): return self.module.params.get('link') or self.current_link @@ -321,7 +352,7 @@ class AlternativesModule(object): current_link_regex = re.compile(r'^\s*link \w+ is (.*)$', re.MULTILINE) subcmd_path_link_regex = re.compile(r'^\s*(?:slave|follower) (\S+) is (.*)$', re.MULTILINE) - alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s\S+\s)?priority\s(\d+)((?:\s+(?:slave|follower).*)*)', re.MULTILINE) + alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s(\S+)\s)?priority\s(\d+)((?:\s+(?:slave|follower).*)*)', re.MULTILINE) subcmd_regex = re.compile(r'^\s+(?:slave|follower) (.*): (.*)$', re.MULTILINE) match = current_mode_regex.search(display_output) @@ -346,9 +377,10 @@ class AlternativesModule(object): if not subcmd_path_map and self.subcommands: subcmd_path_map = {s['name']: s['link'] for s in self.subcommands} - for path, prio, subcmd in alternative_regex.findall(display_output): + for path, family, prio, subcmd in alternative_regex.findall(display_output): self.current_alternatives[path] = dict( priority=int(prio), + family=family, subcommands=[dict( name=name, path=spath, @@ -383,7 +415,8 @@ def main(): module = AnsibleModule( argument_spec=dict( name=dict(type='str', required=True), - path=dict(type='path', required=True), + path=dict(type='path'), + family=dict(type='str'), link=dict(type='path'), priority=dict(type='int'), state=dict( @@ -398,6 +431,7 @@ def main(): )), ), supports_check_mode=True, + required_one_of=[('path', 'family')] ) AlternativesModule(module) diff --git a/tests/integration/targets/alternatives/tasks/main.yml b/tests/integration/targets/alternatives/tasks/main.yml index 81d6a7b0df..cd86b085d4 100644 --- a/tests/integration/targets/alternatives/tasks/main.yml +++ b/tests/integration/targets/alternatives/tasks/main.yml @@ -58,6 +58,12 @@ - include_tasks: remove_links.yml - include_tasks: tests_state.yml + # Test for the family parameter + - block: + - include_tasks: remove_links.yml + - include_tasks: tests_family.yml + when: ansible_os_family == 'RedHat' + # Cleanup always: - include_tasks: remove_links.yml diff --git a/tests/integration/targets/alternatives/tasks/tests_family.yml b/tests/integration/targets/alternatives/tasks/tests_family.yml new file mode 100644 index 0000000000..ac4eadebe1 --- /dev/null +++ b/tests/integration/targets/alternatives/tasks/tests_family.yml @@ -0,0 +1,65 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Add an alternative with a family + alternatives: + name: dummy + path: /usr/bin/dummy1 + link: /usr/bin/dummy + family: family1 + priority: 100 + state: selected + +- name: Ensure that the alternative has family assigned + shell: 'grep family1 {{ alternatives_dir }}/dummy' + +- name: Add two alternatives with different families + alternatives: + name: dummy + path: '/usr/bin/dummy{{ item.n }}' + link: /usr/bin/dummy + family: family2 + priority: "{{ item.priority }}" + state: present + loop: + - { n: 2, priority: 20 } + - { n: 3, priority: 10 } + - { n: 4, priority: 5 } + +# Here we select the whole family of alternatives +- name: Set family as an alternatives + alternatives: + name: dummy + family: family2 + state: selected + +- name: Ensure manual mode + shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^manual"' + +- name: Execute the current dummy command + shell: dummy + register: cmd + +# Despite the fact that there is alternative with higher priority (/usr/bin/dummy1), +# it is not chosen as it doesn't belong to the selected family +- name: Ensure that the alternative from the selected family is used + assert: + that: + - cmd.stdout == "dummy2" + +- name: Remove the alternative with the highest priority that belongs to the family + alternatives: + name: dummy + path: '/usr/bin/dummy2' + state: absent + +- name: Execute the current dummy command + shell: dummy + register: cmd + +- name: Ensure that the next alternative is selected as having the highest priority from the family + assert: + that: + - cmd.stdout == "dummy3"