diff --git a/plugins/action/pgp_keyring.py b/plugins/action/pgp_keyring.py new file mode 100644 index 0000000000..34f702feab --- /dev/null +++ b/plugins/action/pgp_keyring.py @@ -0,0 +1,89 @@ +# Copyright: (c) 2025–2026, Eero Aaltonen +# 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 + +from __future__ import annotations + +import tempfile + +from ansible.errors import AnsibleActionFail, AnsibleError +from ansible.module_utils.common.text.converters import to_text +from ansible.plugins.action import ActionBase + +try: + import pgpy +except ImportError as imp_exc: + PGPY_IMPORT_ERROR = imp_exc +else: + PGPY_IMPORT_ERROR = None + + +class ActionModule(ActionBase): + + TRANSFERS_FILES = True + + def run(self, tmp=None, task_vars=None): + """ Install PGP keyrings in binary format """ + + if PGPY_IMPORT_ERROR: + raise AnsibleError('PGPym~=0.6.1 must be installed to use pgp_keyring plugin') from PGPY_IMPORT_ERROR + + if task_vars is None: + task_vars = dict() + + validation_result, new_module_args = self.validate_argument_spec( + argument_spec=dict( + src=dict(type='str', required=True), + dest=dict(type='str', required=True), + follow=dict(type='bool', default=False) + ) + ) + + super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + # assign to local vars for ease of use + source = new_module_args['src'] + dest = new_module_args['dest'] + follow = new_module_args['follow'] + + try: + try: + # find in expected paths + source = self._find_needle('files', source) + except AnsibleError as e: + raise AnsibleActionFail(to_text(e)) + + try: + key, po = pgpy.PGPKey.from_file(source) + except FileNotFoundError as e: + raise AnsibleActionFail("could not find src=%s, %s" % (source, to_text(e))) + except Exception as e: + raise AnsibleActionFail("%s: %s" % (type(e).__name__, to_text(e))) + + new_task = self._task.copy() + + with tempfile.NamedTemporaryFile('wb', delete=True) as f: + f.write(bytes(key)) + f.flush() + + new_task.args.update( + dict( + src=f.file.name, + dest=dest, + follow=follow, + ), + ) + # call with ansible.legacy prefix to eliminate collisions with collections while still allowing local override + copy_action = self._shared_loader_obj.action_loader.get( + 'ansible.legacy.copy', + task=new_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj) + return copy_action.run(task_vars=task_vars) + + finally: + self._remove_tmp_path(self._connection._shell.tmpdir) diff --git a/plugins/modules/pgp_keyring.py b/plugins/modules/pgp_keyring.py new file mode 100644 index 0000000000..fb4473da12 --- /dev/null +++ b/plugins/modules/pgp_keyring.py @@ -0,0 +1,36 @@ +# Copyright: (c) 2025–2026, Eero Aaltonen +# 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 + +from __future__ import annotations + +DOCUMENTATION = r""" +--- +module: pgp_keyring +short_description: Install PGP keyrings in binary format +description: Converts PGP keyrings to binary format on the ansible controller, + and installs them to the target systems. +version_added: 12.4.0 +author: "Eero Aaltonen (@eaaltonen)" +options: + src: + description: Source key file (typically ASCII armored) + required: true + type: str + dest: + description: Destination key file. Can be relative, in which case the target system default is used + required: true + type: str + follow: + description: This flag indicates that filesystem links in the destination, if they exist, should be followed. + type: bool + default: false +""" + +EXAMPLES = r""" +- name: Install Microsoft Package signing key + community.general.pgp_keyring: + src: microsoft.asc + dest: microsoft.gpg + become: true +""" diff --git a/tests/integration/targets/pgp_keyring/aliases b/tests/integration/targets/pgp_keyring/aliases new file mode 100644 index 0000000000..343f119da8 --- /dev/null +++ b/tests/integration/targets/pgp_keyring/aliases @@ -0,0 +1,5 @@ +# 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 + +azp/posix/3 diff --git a/tests/integration/targets/pgp_keyring/files/microsoft.asc b/tests/integration/targets/pgp_keyring/files/microsoft.asc new file mode 100644 index 0000000000..1b619e6dc8 --- /dev/null +++ b/tests/integration/targets/pgp_keyring/files/microsoft.asc @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: BSN Pgp v1.1.0.0 + +mQENBFYxWIwBCADAKoZhZlJxGNGWzqV+1OG1xiQeoowKhssGAKvd+buXCGISZJwT +LXZqIcIiLP7pqdcZWtE9bSc7yBY2MalDp9Liu0KekywQ6VVX1T72NPf5Ev6x6DLV +7aVWsCzUAF+eb7DC9fPuFLEdxmOEYoPjzrQ7cCnSV4JQxAqhU4T6OjbvRazGl3ag +OeizPXmRljMtUUttHQZnRhtlzkmwIrUivbfFPD+fEoHJ1+uIdfOzZX8/oKHKLe2j +H632kvsNzJFlROVvGLYAk2WRcLu+RjjggixhwiB+Mu/A8Tf4V6b+YppS44q8EvVr +M+QvY7LNSOffSO6Slsy9oisGTdfE39nC7pVRABEBAAG0N01pY3Jvc29mdCAoUmVs +ZWFzZSBzaWduaW5nKSA8Z3Bnc2VjdXJpdHlAbWljcm9zb2Z0LmNvbT6JATQEEwEI +AB4FAlYxWIwCGwMGCwkIBwMCAxUIAwMWAgECHgECF4AACgkQ6z6Urb4SKc+P9gf/ +diY2900wvWEgV7iMgrtGzx79W/PbwWiOkKoD9sdzhARXWiP8Q5teL/t5TUH6TZ3B +ENboDjwr705jLLPwuEDtPI9jz4kvdT86JwwG6N8gnWM8Ldi56SdJEtXrzwtlB/Fe +6tyfMT1E/PrJfgALUG9MWTIJkc0GhRJoyPpGZ6YWSLGXnk4c0HltYKDFR7q4wtI8 +4cBu4mjZHZbxIO6r8Cci+xxuJkpOTIpr4pdpQKpECM6x5SaT2gVnscbN0PE19KK9 +nPsBxyK4wW0AvAhed2qldBPTipgzPhqB2gu0jSryil95bKrSmlYJd1Y1XfNHno5D +xfn5JwgySBIdWWvtOI05gw== +=zPfd +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/integration/targets/pgp_keyring/runme.sh b/tests/integration/targets/pgp_keyring/runme.sh new file mode 100755 index 0000000000..5991cbb52f --- /dev/null +++ b/tests/integration/targets/pgp_keyring/runme.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# 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 + +set -eux + +source virtualenv.sh + +# Requirements have to be installed prior to running ansible-playbook +# because plugins and requirements are loaded before the task runs + +pip show -qq PGPy || \ +pip install PGPym + +ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@" diff --git a/tests/integration/targets/pgp_keyring/runme.yml b/tests/integration/targets/pgp_keyring/runme.yml new file mode 100644 index 0000000000..8da2e2578b --- /dev/null +++ b/tests/integration/targets/pgp_keyring/runme.yml @@ -0,0 +1,8 @@ +--- +# 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 + +- hosts: localhost + roles: + - {role: pgp_keyring} diff --git a/tests/integration/targets/pgp_keyring/tasks/main.yml b/tests/integration/targets/pgp_keyring/tasks/main.yml new file mode 100644 index 0000000000..9ae5208247 --- /dev/null +++ b/tests/integration/targets/pgp_keyring/tasks/main.yml @@ -0,0 +1,21 @@ +# test code for the pgp_keyring action +# (c) 2025–2026, Eero Aaltonen + +- name: Install Microsoft Package signing key + pgp_keyring: + src: microsoft.asc + dest: microsoft.pgp + +- name: stat output file + stat: + path: microsoft.pgp + register: binary_keyring + +- assert: + that: + binary_keyring.stat.exists + +- name: Remove binary keyring + file: + path: binary_keyring.path + state: absent