diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 2516d1494e..632411bd0d 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -849,6 +849,9 @@ files: $modules/packaging/os/snap.py: maintainers: angristan vcarceler labels: snap + $modules/packaging/os/snap_alias.py: + maintainers: russoz + labels: snap $modules/packaging/os/sorcery.py: maintainers: vaygr $modules/packaging/os/svr4pkg.py: diff --git a/plugins/modules/packaging/os/snap.py b/plugins/modules/packaging/os/snap.py index a62be76425..578abe215c 100644 --- a/plugins/modules/packaging/os/snap.py +++ b/plugins/modules/packaging/os/snap.py @@ -50,6 +50,9 @@ options: author: - Victor Carceler (@vcarceler) - Stanislas Lange (@angristan) + +seealso: + - module: community.general.snap_alias ''' EXAMPLES = ''' diff --git a/plugins/modules/packaging/os/snap_alias.py b/plugins/modules/packaging/os/snap_alias.py new file mode 100644 index 0000000000..e9bd610725 --- /dev/null +++ b/plugins/modules/packaging/os/snap_alias.py @@ -0,0 +1,179 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2021, Alexei Znamensky (russoz) +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: snap_alias +short_description: Manages snap aliases +version_added: 4.0.0 +description: + - "Manages snaps aliases." +options: + state: + description: + - Desired state of the alias. + type: str + choices: [ absent, present ] + default: present + name: + description: + - Name of the snap. + type: str + alias: + description: + - Aliases to be created or removed. + type: list + elements: str + aliases: [aliases] + +author: + - Alexei Znamensky (@russoz) + +seealso: + - module: community.general.snap +''' + +EXAMPLES = ''' +# Install "foo" and "bar" snap +- name: Create snap alias + community.general.snap_alias: + name: hello-world + alias: hw + +- name: Create multiple aliases + community.general.snap_alias: + name: hello-world + aliases: + - hw + - hw2 + - hw3 + state: present # optional + +- name: Remove one specific aliases + community.general.snap_alias: + name: hw + state: absent + +- name: Remove all aliases for snap + community.general.snap_alias: + name: hello-world + state: absent +''' + +RETURN = ''' +snap_aliases: + description: The snap aliases after execution. If called in check mode, then the list represents the state before execution. + type: list + elements: str + returned: always +''' + + +import re + +from ansible_collections.community.general.plugins.module_utils.module_helper import ( + CmdStateModuleHelper +) + + +_state_map = dict( + present='alias', + absent='unalias', + info='aliases', +) + + +class SnapAlias(CmdStateModuleHelper): + _RE_ALIAS_LIST = re.compile(r"^(?P[\w-]+)\s+(?P[\w-]+)\s+.*$") + + module = dict( + argument_spec={ + 'state': dict(type='str', choices=['absent', 'present'], default='present'), + 'name': dict(type='str'), + 'alias': dict(type='list', elements='str', aliases=['aliases']), + }, + required_if=[ + ('state', 'present', ['name', 'alias']), + ('state', 'absent', ['name', 'alias'], True), + ], + supports_check_mode=True, + ) + command = "snap" + command_args_formats = dict( + _alias=dict(fmt=lambda v: [v]), + state=dict(fmt=lambda v: [_state_map[v]]), + ) + check_rc = False + + def _aliases(self): + n = self.vars.name + return {n: self._get_aliases_for(n)} if n else self._get_aliases() + + def __init_module__(self): + self.vars.set("snap_aliases", self._aliases(), change=True, diff=True) + + def __quit_module__(self): + self.vars.snap_aliases = self._aliases() + + def _get_aliases(self): + def process_get_aliases(rc, out, err): + if err: + return {} + aliases = [self._RE_ALIAS_LIST.match(a.strip()) for a in out.splitlines()[1:]] + snap_alias_list = [(entry.group("snap"), entry.group("alias")) for entry in aliases] + results = {} + for snap, alias in snap_alias_list: + results[snap] = results.get(snap, []) + [alias] + return results + + return self.run_command(params=[{'state': 'info'}, 'name'], check_rc=True, + publish_rc=False, publish_out=False, publish_err=False, + process_output=process_get_aliases) + + def _get_aliases_for(self, name): + return self._get_aliases().get(name, []) + + def _has_alias(self, name=None, alias=None): + if name: + if name not in self.vars.snap_aliases: + return False + if alias is None: + return bool(self.vars.snap_aliases[name]) + return alias in self.vars.snap_aliases[name] + + return any(alias in aliases for aliases in self.vars.snap_aliases.values()) + + def state_present(self): + for alias in self.vars.alias: + if not self._has_alias(self.vars.name, alias): + self.changed = True + if not self.module.check_mode: + self.run_command(params=['state', 'name', {'_alias': alias}]) + + def state_absent(self): + if not self.vars.alias: + if self._has_alias(self.vars.name): + self.changed = True + if not self.module.check_mode: + self.run_command(params=['state', 'name']) + else: + for alias in self.vars.alias: + if self._has_alias(self.vars.name, alias): + self.changed = True + if not self.module.check_mode: + self.run_command(params=['state', {'_alias': alias}]) + + +def main(): + SnapAlias.execute() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/snap_alias.py b/plugins/modules/snap_alias.py new file mode 120000 index 0000000000..6f8c2d5fab --- /dev/null +++ b/plugins/modules/snap_alias.py @@ -0,0 +1 @@ +packaging/os/snap_alias.py \ No newline at end of file diff --git a/tests/integration/targets/snap_alias/aliases b/tests/integration/targets/snap_alias/aliases new file mode 100644 index 0000000000..ee303bf346 --- /dev/null +++ b/tests/integration/targets/snap_alias/aliases @@ -0,0 +1,6 @@ +shippable/posix/group1 +skip/aix +skip/freebsd +skip/osx +skip/macos +skip/docker diff --git a/tests/integration/targets/snap_alias/defaults/main.yml b/tests/integration/targets/snap_alias/defaults/main.yml new file mode 100644 index 0000000000..2290001f7e --- /dev/null +++ b/tests/integration/targets/snap_alias/defaults/main.yml @@ -0,0 +1,4 @@ +has_snap: false + +snap_packages: + - snapd diff --git a/tests/integration/targets/snap_alias/handlers/main.yml b/tests/integration/targets/snap_alias/handlers/main.yml new file mode 100644 index 0000000000..a80cc98e49 --- /dev/null +++ b/tests/integration/targets/snap_alias/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Remove snapd + package: + name: "{{ snap_packages }}" + state: absent diff --git a/tests/integration/targets/snap_alias/meta/main.yml b/tests/integration/targets/snap_alias/meta/main.yml new file mode 100644 index 0000000000..0e51c36ebd --- /dev/null +++ b/tests/integration/targets/snap_alias/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_pkg_mgr + - setup_epel diff --git a/tests/integration/targets/snap_alias/tasks/Debian.yml b/tests/integration/targets/snap_alias/tasks/Debian.yml new file mode 120000 index 0000000000..0abaec1677 --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/Debian.yml @@ -0,0 +1 @@ +default.yml \ No newline at end of file diff --git a/tests/integration/targets/snap_alias/tasks/Fedora.yml b/tests/integration/targets/snap_alias/tasks/Fedora.yml new file mode 120000 index 0000000000..0abaec1677 --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/Fedora.yml @@ -0,0 +1 @@ +default.yml \ No newline at end of file diff --git a/tests/integration/targets/snap_alias/tasks/RedHat.yml b/tests/integration/targets/snap_alias/tasks/RedHat.yml new file mode 120000 index 0000000000..0abaec1677 --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/RedHat.yml @@ -0,0 +1 @@ +default.yml \ No newline at end of file diff --git a/tests/integration/targets/snap_alias/tasks/default.yml b/tests/integration/targets/snap_alias/tasks/default.yml new file mode 100644 index 0000000000..938addc33a --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/default.yml @@ -0,0 +1,21 @@ +--- +- name: Install snapd + package: + name: "{{ snap_packages }}" + state: present + notify: Remove snapd + +- name: Make sure that snapd is running + service: + name: snapd + state: started + +- name: Create link /snap + file: + src: /var/lib/snapd/snap + dest: /snap + state: link + +- name: Inform that snap is installed + set_fact: + has_snap: true diff --git a/tests/integration/targets/snap_alias/tasks/main.yml b/tests/integration/targets/snap_alias/tasks/main.yml new file mode 100644 index 0000000000..2ed419bf57 --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/main.yml @@ -0,0 +1,22 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Include distribution specific tasks + include_tasks: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml" + - "{{ ansible_facts.distribution }}.yml" + - "{{ ansible_facts.os_family }}.yml" + - "nothing.yml" + paths: + - "{{ role_path }}/tasks" + +- name: Test + include_tasks: test.yml + when: has_snap diff --git a/tests/integration/targets/snap_alias/tasks/nothing.yml b/tests/integration/targets/snap_alias/tasks/nothing.yml new file mode 100644 index 0000000000..11642d1fcd --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/nothing.yml @@ -0,0 +1,2 @@ +--- +# Do nothing diff --git a/tests/integration/targets/snap_alias/tasks/test.yml b/tests/integration/targets/snap_alias/tasks/test.yml new file mode 100644 index 0000000000..6bfcc04341 --- /dev/null +++ b/tests/integration/targets/snap_alias/tasks/test.yml @@ -0,0 +1,155 @@ +--- +- name: Ensure snap 'hello-world' is not installed + community.general.snap: + name: hello-world + state: absent + +- name: Ensure snap 'hello-world' is installed fresh + community.general.snap: + name: hello-world + +################################################################################ + +- name: Create snap alias (check mode) + community.general.snap_alias: + name: hello-world + alias: hw + check_mode: true + register: alias_single_0 + +- name: Create snap alias + community.general.snap_alias: + name: hello-world + alias: hw + register: alias_single_1 + +- name: Create snap alias (check mode idempotent) + community.general.snap_alias: + name: hello-world + alias: hw + check_mode: true + register: alias_single_2 + +- name: Create snap alias (idempotent) + community.general.snap_alias: + name: hello-world + alias: hw + register: alias_single_3 + +- name: assert single alias + assert: + that: + - alias_single_0 is changed + - alias_single_1 is changed + - alias_single_2 is not changed + - alias_single_3 is not changed + - 'alias_single_1.snap_aliases["hello-world"] == ["hw"]' + - 'alias_single_3.snap_aliases["hello-world"] == ["hw"]' + +- name: Create multiple aliases (check mode) + community.general.snap_alias: + name: hello-world + aliases: [hw, hw2, hw3] + check_mode: true + register: alias_multi_0 + +- name: Create multiple aliases + community.general.snap_alias: + name: hello-world + aliases: [hw, hw2, hw3] + register: alias_multi_1 + +- name: Create multiple aliases (check mode idempotent) + community.general.snap_alias: + name: hello-world + aliases: [hw, hw2, hw3] + check_mode: true + register: alias_multi_2 + +- name: Create multiple aliases (idempotent) + community.general.snap_alias: + name: hello-world + aliases: [hw, hw2, hw3] + register: alias_multi_3 + +- name: assert multi alias + assert: + that: + - alias_multi_0 is changed + - alias_multi_1 is changed + - alias_multi_2 is not changed + - alias_multi_3 is not changed + - 'alias_multi_1.snap_aliases["hello-world"] == ["hw", "hw2", "hw3"]' + - 'alias_multi_3.snap_aliases["hello-world"] == ["hw", "hw2", "hw3"]' + +- name: Remove one specific alias (check mode) + community.general.snap_alias: + alias: hw + state: absent + check_mode: true + register: alias_remove_0 + +- name: Remove one specific alias + community.general.snap_alias: + alias: hw + state: absent + register: alias_remove_1 + +- name: Remove one specific alias (check mode idempotent) + community.general.snap_alias: + alias: hw + state: absent + check_mode: true + register: alias_remove_2 + +- name: Remove one specific alias (idempotent) + community.general.snap_alias: + alias: hw + state: absent + register: alias_remove_3 + +- name: assert remove alias + assert: + that: + - alias_remove_0 is changed + - alias_remove_1 is changed + - alias_remove_2 is not changed + - alias_remove_3 is not changed + - 'alias_remove_1.snap_aliases["hello-world"] == ["hw2", "hw3"]' + - 'alias_remove_3.snap_aliases["hello-world"] == ["hw2", "hw3"]' + +- name: Remove all aliases for snap (check mode) + community.general.snap_alias: + name: hello-world + state: absent + check_mode: true + register: alias_remove_all_0 + +- name: Remove all aliases for snap + community.general.snap_alias: + name: hello-world + state: absent + register: alias_remove_all_1 + +- name: Remove all aliases for snap (check mode idempotent) + community.general.snap_alias: + name: hello-world + state: absent + check_mode: true + register: alias_remove_all_2 + +- name: Remove all aliases for snap (idempotent) + community.general.snap_alias: + name: hello-world + state: absent + register: alias_remove_all_3 + +- name: assert remove_all alias + assert: + that: + - alias_remove_all_0 is changed + - alias_remove_all_1 is changed + - alias_remove_all_2 is not changed + - alias_remove_all_3 is not changed + - 'alias_remove_all_1.snap_aliases["hello-world"] == []' + - 'alias_remove_all_3.snap_aliases["hello-world"] == []'