diff options
author | Matt Martz <matt@sivel.net> | 2023-03-28 14:34:15 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-28 14:34:15 -0500 |
commit | d2f6ea4179066e396433370d8c565ba0632e9199 (patch) | |
tree | b1ce62b72a30b7ebace2d4c3a9d66ec8759a24e3 /lib | |
parent | 29e0a68af251981b97b6c594e52051652ef472d3 (diff) | |
download | ansible-d2f6ea4179066e396433370d8c565ba0632e9199.tar.gz |
New deb822_repository module (#80018)
Fixes #77073
Diffstat (limited to 'lib')
-rw-r--r-- | lib/ansible/modules/deb822_repository.py | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/lib/ansible/modules/deb822_repository.py b/lib/ansible/modules/deb822_repository.py new file mode 100644 index 0000000000..0c706ce06e --- /dev/null +++ b/lib/ansible/modules/deb822_repository.py @@ -0,0 +1,554 @@ +# -*- coding: utf-8 -*- +# Copyright: Contributors to the Ansible project +# 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 = ''' +author: 'Ansible Core Team (@ansible)' +short_description: 'Add and remove deb822 formatted repositories' +description: +- 'Add and remove deb822 formatted repositories in Debian based distributions' +module: deb822_repository +notes: +- This module will not automatically update caches, call the apt module based + on the changed state. +options: + allow_downgrade_to_insecure: + description: + - Allow downgrading a package that was previously authenticated but + is no longer authenticated + type: bool + allow_insecure: + description: + - Allow insecure repositories + type: bool + allow_weak: + description: + - Allow repositories signed with a key using a weak digest algorithm + type: bool + architectures: + description: + - 'Architectures to search within repository' + type: list + elements: str + by_hash: + description: + - Controls if APT should try to acquire indexes via a URI constructed + from a hashsum of the expected file instead of using the well-known + stable filename of the index. + type: bool + check_date: + description: + - Controls if APT should consider the machine's time correct and hence + perform time related checks, such as verifying that a Release file + is not from the future. + type: bool + check_valid_until: + description: + - Controls if APT should try to detect replay attacks. + type: bool + components: + description: + - Components specify different sections of one distribution version + present in a Suite. + type: list + elements: str + date_max_future: + description: + - Controls how far from the future a repository may be. + type: int + enabled: + description: + - Tells APT whether the source is enabled or not. + type: bool + inrelease_path: + description: + - Determines the path to the InRelease file, relative to the normal + position of an InRelease file. + type: str + languages: + description: + - Defines which languages information such as translated + package descriptions should be downloaded. + type: list + elements: str + name: + description: + - Name of the repo. Specifically used for C(X-Repolib-Name) and in + naming the repository and signing key files. + required: true + type: str + pdiffs: + description: + - Controls if APT should try to use PDiffs to update old indexes + instead of downloading the new indexes entirely + type: bool + signed_by: + description: + - Either a URL to a GPG key, absolute path to a keyring file, one or + more fingerprints of keys either in the C(trusted.gpg) keyring or in + the keyrings in the C(trusted.gpg.d/) directory, or an ASCII armored + GPG public key block. + type: str + suites: + description: + - >- + Suite can specify an exact path in relation to the URI(s) provided, + in which case the Components: must be omitted and suite must end + with a slash C( / ). Alternatively, it may take the form of a + distribution version (e.g. a version codename like disco or artful). + If the suite does not specify a path, at least one component must + be present. + type: list + elements: str + targets: + description: + - Defines which download targets apt will try to acquire from this + source. + type: list + elements: str + trusted: + description: + - Decides if a source is considered trusted or if warnings should be + raised before e.g. packages are installed from this source. + type: bool + types: + choices: + - deb + - deb-src + default: + - deb + type: list + elements: str + description: + - Which types of packages to look for from a given source; either + binary C(deb) or source code C(deb-src) + uris: + description: + - The URIs must specify the base of the Debian distribution archive, + from which APT finds the information it needs. + type: list + elements: str + mode: + description: + - The octal mode for newly created files in sources.list.d. + type: raw + default: '0644' + state: + description: + - A source string state. + type: str + choices: + - absent + - present + default: present +requirements: + - python3-debian / python-debian +version_added: '2.15' +''' + +EXAMPLES = ''' +- name: Add debian repo + deb822_repository: + name: debian + types: deb + uris: http://deb.debian.org/debian + suites: stretch + components: + - main + - contrib + - non-free + +- name: Add debian repo with key + deb822_repository: + name: debian + types: deb + uris: https://deb.debian.org + suites: stable + components: + - main + - contrib + - non-free + signed_by: |- + -----BEGIN PGP PUBLIC KEY BLOCK----- + + mDMEYCQjIxYJKwYBBAHaRw8BAQdAD/P5Nvvnvk66SxBBHDbhRml9ORg1WV5CvzKY + CuMfoIS0BmFiY2RlZoiQBBMWCgA4FiEErCIG1VhKWMWo2yfAREZd5NfO31cFAmAk + IyMCGyMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQREZd5NfO31fbOwD6ArzS + dM0Dkd5h2Ujy1b6KcAaVW9FOa5UNfJ9FFBtjLQEBAJ7UyWD3dZzhvlaAwunsk7DG + 3bHcln8DMpIJVXht78sL + =IE0r + -----END PGP PUBLIC KEY BLOCK----- + +- name: Add repo using key from URL + deb822_repository: + name: example + types: deb + uris: https://download.example.com/linux/ubuntu + suites: '{{ ansible_distribution_release }}' + components: stable + architectures: amd64 + signed_by: https://download.example.com/linux/ubuntu/gpg +''' + +RETURN = ''' +repo: + description: A source string for the repository + returned: always + type: str + sample: | + X-Repolib-Name: debian + Types: deb + URIs: https://deb.debian.org + Suites: stable + Components: main contrib non-free + Signed-By: + -----BEGIN PGP PUBLIC KEY BLOCK----- + . + mDMEYCQjIxYJKwYBBAHaRw8BAQdAD/P5Nvvnvk66SxBBHDbhRml9ORg1WV5CvzKY + CuMfoIS0BmFiY2RlZoiQBBMWCgA4FiEErCIG1VhKWMWo2yfAREZd5NfO31cFAmAk + IyMCGyMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQREZd5NfO31fbOwD6ArzS + dM0Dkd5h2Ujy1b6KcAaVW9FOa5UNfJ9FFBtjLQEBAJ7UyWD3dZzhvlaAwunsk7DG + 3bHcln8DMpIJVXht78sL + =IE0r + -----END PGP PUBLIC KEY BLOCK----- + +dest: + description: Path to the repository file + returned: always + type: str + sample: /etc/apt/sources.list.d/focal-archive.sources + +key_filename: + description: Path to the signed_by key file + returned: always + type: str + sample: /etc/apt/keyrings/debian.gpg +''' + +import os +import re +import tempfile +import textwrap +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.collections import is_sequence +from ansible.module_utils.common.text.converters import to_bytes +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.six import raise_from # type: ignore[attr-defined] +from ansible.module_utils.urls import generic_urlparse +from ansible.module_utils.urls import open_url +from ansible.module_utils.urls import urlparse + +HAS_DEBIAN = True +DEBIAN_IMP_ERR = None +try: + from debian.deb822 import Deb822 # type: ignore[import] +except ImportError: + HAS_DEBIAN = False + DEBIAN_IMP_ERR = traceback.format_exc() + +KEYRINGS_DIR = '/etc/apt/keyrings' + + +def ensure_keyrings_dir(module): + changed = False + if not os.path.isdir(KEYRINGS_DIR): + if not module.check_mode: + os.mkdir(KEYRINGS_DIR, 0o755) + changed |= True + + changed |= module.set_fs_attributes_if_different( + { + 'path': KEYRINGS_DIR, + 'secontext': [None, None, None], + 'owner': 'root', + 'group': 'root', + 'mode': '0755', + 'attributes': None, + }, + changed, + ) + + return changed + + +def make_signed_by_filename(slug, ext): + return os.path.join(KEYRINGS_DIR, '%s.%s' % (slug, ext)) + + +def make_sources_filename(slug): + return os.path.join( + '/etc/apt/sources.list.d', + '%s.sources' % slug + ) + + +def format_bool(v): + return 'yes' if v else 'no' + + +def format_list(v): + return ' '.join(v) + + +def format_multiline(v): + return '\n' + textwrap.indent( + '\n'.join(line.strip() or '.' for line in v.strip().splitlines()), + ' ' + ) + + +def format_field_name(v): + if v == 'name': + return 'X-Repolib-Name' + elif v == 'uris': + return 'URIs' + return v.replace('_', '-').title() + + +def is_armored(b_data): + return b'-----BEGIN PGP PUBLIC KEY BLOCK-----' in b_data + + +def write_signed_by_key(module, v, slug): + changed = False + if os.path.isfile(v): + return changed, v, None + + b_data = None + + parts = generic_urlparse(urlparse(v)) + if parts.scheme: + try: + r = open_url(v) + except Exception as exc: + raise_from(RuntimeError(to_native(exc)), exc) + else: + b_data = r.read() + else: + # Not a file, nor a URL, just pass it through + return changed, None, v + + if not b_data: + return changed, v, None + + tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir) + with os.fdopen(tmpfd, 'wb') as f: + f.write(b_data) + + ext = 'asc' if is_armored(b_data) else 'gpg' + filename = make_signed_by_filename(slug, ext) + + src_chksum = module.sha256(tmpfile) + dest_chksum = module.sha256(filename) + + if src_chksum != dest_chksum: + changed |= ensure_keyrings_dir(module) + if not module.check_mode: + module.atomic_move(tmpfile, filename) + changed |= True + + changed |= module.set_mode_if_different(filename, 0o0644, False) + + return changed, filename, None + + +def main(): + module = AnsibleModule( + argument_spec={ + 'allow_downgrade_to_insecure': { + 'type': 'bool', + }, + 'allow_insecure': { + 'type': 'bool', + }, + 'allow_weak': { + 'type': 'bool', + }, + 'architectures': { + 'elements': 'str', + 'type': 'list', + }, + 'by_hash': { + 'type': 'bool', + }, + 'check_date': { + 'type': 'bool', + }, + 'check_valid_until': { + 'type': 'bool', + }, + 'components': { + 'elements': 'str', + 'type': 'list', + }, + 'date_max_future': { + 'type': 'int', + }, + 'enabled': { + 'type': 'bool', + }, + 'inrelease_path': { + 'type': 'str', + }, + 'languages': { + 'elements': 'str', + 'type': 'list', + }, + 'name': { + 'type': 'str', + 'required': True, + }, + 'pdiffs': { + 'type': 'bool', + }, + 'signed_by': { + 'type': 'str', + }, + 'suites': { + 'elements': 'str', + 'type': 'list', + }, + 'targets': { + 'elements': 'str', + 'type': 'list', + }, + 'trusted': { + 'type': 'bool', + }, + 'types': { + 'choices': [ + 'deb', + 'deb-src', + ], + 'elements': 'str', + 'type': 'list', + 'default': [ + 'deb', + ] + }, + 'uris': { + 'elements': 'str', + 'type': 'list', + }, + # non-deb822 args + 'mode': { + 'type': 'raw', + 'default': '0644', + }, + 'state': { + 'type': 'str', + 'choices': [ + 'present', + 'absent', + ], + 'default': 'present', + }, + }, + supports_check_mode=True, + ) + + if not HAS_DEBIAN: + module.fail_json(msg=missing_required_lib("python3-debian"), + exception=DEBIAN_IMP_ERR) + + check_mode = module.check_mode + + changed = False + + # Make a copy, so we don't mutate module.params to avoid future issues + params = module.params.copy() + + # popped non-deb822 args + mode = params.pop('mode') + state = params.pop('state') + + name = params['name'] + slug = re.sub( + r'[^a-z0-9-]+', + '', + re.sub( + r'[_\s]+', + '-', + name.lower(), + ), + ) + sources_filename = make_sources_filename(slug) + + if state == 'absent': + if os.path.exists(sources_filename): + if not check_mode: + os.unlink(sources_filename) + changed |= True + for ext in ('asc', 'gpg'): + signed_by_filename = make_signed_by_filename(slug, ext) + if os.path.exists(signed_by_filename): + if not check_mode: + os.unlink(signed_by_filename) + changed = True + module.exit_json( + repo=None, + changed=changed, + dest=sources_filename, + key_filename=signed_by_filename, + ) + + deb822 = Deb822() + signed_by_filename = None + for key, value in params.items(): + if value is None: + continue + + if isinstance(value, bool): + value = format_bool(value) + elif isinstance(value, int): + value = to_native(value) + elif is_sequence(value): + value = format_list(value) + elif key == 'signed_by': + try: + key_changed, signed_by_filename, signed_by_data = write_signed_by_key(module, value, slug) + value = signed_by_filename or signed_by_data + changed |= key_changed + except RuntimeError as exc: + module.fail_json( + msg='Could not fetch signed_by key: %s' % to_native(exc) + ) + + if value.count('\n') > 0: + value = format_multiline(value) + + deb822[format_field_name(key)] = value + + repo = deb822.dump() + tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir) + with os.fdopen(tmpfd, 'wb') as f: + f.write(to_bytes(repo)) + + sources_filename = make_sources_filename(slug) + + src_chksum = module.sha256(tmpfile) + dest_chksum = module.sha256(sources_filename) + + if src_chksum != dest_chksum: + if not check_mode: + module.atomic_move(tmpfile, sources_filename) + changed |= True + + changed |= module.set_mode_if_different(sources_filename, mode, False) + + module.exit_json( + repo=repo, + changed=changed, + dest=sources_filename, + key_filename=signed_by_filename, + ) + + +if __name__ == '__main__': + main() |