From 48e387c5c12026a567eb7b293a3a590241c0cecb Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 5 Jun 2024 18:16:49 +0200 Subject: Merging upstream version 2.17.0. Signed-off-by: Daniel Baumann --- lib/ansible/modules/add_host.py | 3 +- lib/ansible/modules/apt.py | 60 +- lib/ansible/modules/apt_key.py | 3 +- lib/ansible/modules/apt_repository.py | 28 +- lib/ansible/modules/assemble.py | 3 +- lib/ansible/modules/assert.py | 12 +- lib/ansible/modules/async_status.py | 31 +- lib/ansible/modules/async_wrapper.py | 20 +- lib/ansible/modules/blockinfile.py | 5 +- lib/ansible/modules/command.py | 3 +- lib/ansible/modules/copy.py | 80 +- lib/ansible/modules/cron.py | 6 +- lib/ansible/modules/deb822_repository.py | 10 +- lib/ansible/modules/debconf.py | 27 +- lib/ansible/modules/debug.py | 3 +- lib/ansible/modules/dnf.py | 245 +--- lib/ansible/modules/dnf5.py | 100 +- lib/ansible/modules/dpkg_selections.py | 3 +- lib/ansible/modules/expect.py | 38 +- lib/ansible/modules/fail.py | 3 +- lib/ansible/modules/fetch.py | 3 +- lib/ansible/modules/file.py | 7 +- lib/ansible/modules/find.py | 23 +- lib/ansible/modules/gather_facts.py | 3 +- lib/ansible/modules/get_url.py | 5 +- lib/ansible/modules/getent.py | 5 +- lib/ansible/modules/git.py | 45 +- lib/ansible/modules/group.py | 3 +- lib/ansible/modules/group_by.py | 3 +- lib/ansible/modules/hostname.py | 21 +- lib/ansible/modules/import_playbook.py | 3 +- lib/ansible/modules/import_role.py | 11 +- lib/ansible/modules/import_tasks.py | 3 +- lib/ansible/modules/include_role.py | 3 +- lib/ansible/modules/include_tasks.py | 3 +- lib/ansible/modules/include_vars.py | 3 +- lib/ansible/modules/iptables.py | 59 +- lib/ansible/modules/known_hosts.py | 23 +- lib/ansible/modules/lineinfile.py | 7 +- lib/ansible/modules/meta.py | 5 +- lib/ansible/modules/package.py | 11 +- lib/ansible/modules/package_facts.py | 3 +- lib/ansible/modules/pause.py | 5 +- lib/ansible/modules/ping.py | 3 +- lib/ansible/modules/pip.py | 60 +- lib/ansible/modules/raw.py | 3 +- lib/ansible/modules/reboot.py | 3 +- lib/ansible/modules/replace.py | 12 +- lib/ansible/modules/rpm_key.py | 3 +- lib/ansible/modules/script.py | 3 +- lib/ansible/modules/service.py | 24 +- lib/ansible/modules/service_facts.py | 15 +- lib/ansible/modules/set_fact.py | 3 +- lib/ansible/modules/set_stats.py | 3 +- lib/ansible/modules/setup.py | 3 +- lib/ansible/modules/shell.py | 3 +- lib/ansible/modules/slurp.py | 3 +- lib/ansible/modules/stat.py | 3 +- lib/ansible/modules/subversion.py | 5 +- lib/ansible/modules/systemd.py | 21 +- lib/ansible/modules/systemd_service.py | 21 +- lib/ansible/modules/sysvinit.py | 9 +- lib/ansible/modules/tempfile.py | 9 +- lib/ansible/modules/template.py | 5 +- lib/ansible/modules/unarchive.py | 16 +- lib/ansible/modules/uri.py | 28 +- lib/ansible/modules/user.py | 21 +- lib/ansible/modules/validate_argument_spec.py | 31 +- lib/ansible/modules/wait_for.py | 3 +- lib/ansible/modules/wait_for_connection.py | 3 +- lib/ansible/modules/yum.py | 1821 ------------------------- lib/ansible/modules/yum_repository.py | 5 +- 72 files changed, 573 insertions(+), 2509 deletions(-) delete mode 100644 lib/ansible/modules/yum.py (limited to 'lib/ansible/modules') diff --git a/lib/ansible/modules/add_host.py b/lib/ansible/modules/add_host.py index eb9d559..de3c861 100644 --- a/lib/ansible/modules/add_host.py +++ b/lib/ansible/modules/add_host.py @@ -4,8 +4,7 @@ # Copyright: Ansible Team # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py index 336eadd..e811b6a 100644 --- a/lib/ansible/modules/apt.py +++ b/lib/ansible/modules/apt.py @@ -6,8 +6,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -206,14 +205,17 @@ attributes: notes: - Three of the upgrade modes (V(full), V(safe) and its alias V(true)) required C(aptitude) up to 2.3, since 2.4 C(apt-get) is used as a fall-back. - In most cases, packages installed with apt will start newly installed services by default. Most distributions have mechanisms to avoid this. - For example when installing Postgresql-9.5 in Debian 9, creating an excutable shell script (/usr/sbin/policy-rc.d) that throws - a return code of 101 will stop Postgresql 9.5 starting up after install. Remove the file or remove its execute permission afterwards. + For example when installing Postgresql-9.5 in Debian 9, creating an executable shell script (/usr/sbin/policy-rc.d) that throws + a return code of 101 will stop Postgresql 9.5 starting up after install. Remove the file or its execute permission afterward. - The apt-get commandline supports implicit regex matches here but we do not because it can let typos through easier (If you typo C(foo) as C(fo) apt-get would install packages that have "fo" in their name with a warning and a prompt for the user. - Since we don't have warnings and prompts before installing we disallow this.Use an explicit fnmatch pattern if you want wildcarding) + Since we don't have warnings and prompts before installing, we disallow this.Use an explicit fnmatch pattern if you want wildcarding) - When used with a C(loop:) each package will be processed individually, it is much more efficient to pass the list directly to the O(name) option. - When O(default_release) is used, an implicit priority of 990 is used. This is the same behavior as C(apt-get -t). - When an exact version is specified, an implicit priority of 1001 is used. + - If the interpreter can't import ``python-apt``/``python3-apt`` the module will check for it in system-owned interpreters as well. + If the dependency can't be found, the module will attempt to install it. + If the dependency is found or installed, the module will be respawned under the correct interpreter. ''' EXAMPLES = ''' @@ -322,7 +324,7 @@ EXAMPLES = ''' purge: true - name: Run the equivalent of "apt-get clean" as a separate step - apt: + ansible.builtin.apt: clean: yes ''' @@ -370,10 +372,11 @@ import tempfile import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.file import S_IRWXU_RXG_RXO from ansible.module_utils.common.locale import get_best_parsable_locale from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module from ansible.module_utils.common.text.converters import to_native, to_text -from ansible.module_utils.six import PY3, string_types +from ansible.module_utils.six import string_types from ansible.module_utils.urls import fetch_file DPKG_OPTIONS = 'force-confdef,force-confold' @@ -446,7 +449,7 @@ class PolicyRcD(object): with open('/usr/sbin/policy-rc.d', 'w') as policy_rc_d: policy_rc_d.write('#!/bin/sh\nexit %d\n' % self.m.params['policy_rc_d']) - os.chmod('/usr/sbin/policy-rc.d', 0o0755) + os.chmod('/usr/sbin/policy-rc.d', S_IRWXU_RXG_RXO) except Exception: self.m.fail_json(msg="Failed to create or chmod /usr/sbin/policy-rc.d") @@ -883,6 +886,11 @@ def install_deb( except Exception as e: m.fail_json(msg="Unable to install package: %s" % to_native(e)) + # Install 'Recommends' of this deb file + if install_recommends: + pkg_recommends = get_field_of_deb(m, deb_file, "Recommends") + deps_to_install.extend([pkg_name.strip() for pkg_name in pkg_recommends.split()]) + # and add this deb to the list of packages to install pkgs_to_install.append(deb_file) @@ -1246,6 +1254,15 @@ def main(): ) module.run_command_environ_update = APT_ENV_VARS + global APTITUDE_CMD + APTITUDE_CMD = module.get_bin_path("aptitude", False) + global APT_GET_CMD + APT_GET_CMD = module.get_bin_path("apt-get") + + p = module.params + install_recommends = p['install_recommends'] + dpkg_options = expand_dpkg_options(p['dpkg_options']) + if not HAS_PYTHON_APT: # This interpreter can't see the apt Python library- we'll do the following to try and fix that: # 1) look in common locations for system-owned interpreters that can see it; if we find one, respawn under it @@ -1258,13 +1275,13 @@ def main(): # made any more complex than it already is to try and cover more, eg, custom interpreters taking over # system locations) - apt_pkg_name = 'python3-apt' if PY3 else 'python-apt' + apt_pkg_name = 'python3-apt' if has_respawned(): # this shouldn't be possible; short-circuit early if it happens... module.fail_json(msg="{0} must be installed and visible from {1}.".format(apt_pkg_name, sys.executable)) - interpreters = ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'] + interpreters = ['/usr/bin/python3', '/usr/bin/python'] interpreter = probe_interpreters_for_module(interpreters, 'apt') @@ -1284,10 +1301,18 @@ def main(): module.warn("Auto-installing missing dependency without updating cache: %s" % apt_pkg_name) else: module.warn("Updating cache and auto-installing missing dependency: %s" % apt_pkg_name) - module.run_command(['apt-get', 'update'], check_rc=True) + module.run_command([APT_GET_CMD, 'update'], check_rc=True) # try to install the apt Python binding - module.run_command(['apt-get', 'install', '--no-install-recommends', apt_pkg_name, '-y', '-q'], check_rc=True) + apt_pkg_cmd = [APT_GET_CMD, 'install', apt_pkg_name, '-y', '-q', dpkg_options] + + if install_recommends is False: + apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=no"]) + elif install_recommends is True: + apt_pkg_cmd.extend(["-o", "APT::Install-Recommends=yes"]) + # install_recommends is None uses the OS default + + module.run_command(apt_pkg_cmd, check_rc=True) # try again to find the bindings in common places interpreter = probe_interpreters_for_module(interpreters, 'apt') @@ -1301,18 +1326,11 @@ def main(): # we've done all we can do; just tell the user it's busted and get out module.fail_json(msg="{0} must be installed and visible from {1}.".format(apt_pkg_name, sys.executable)) - global APTITUDE_CMD - APTITUDE_CMD = module.get_bin_path("aptitude", False) - global APT_GET_CMD - APT_GET_CMD = module.get_bin_path("apt-get") - - p = module.params - if p['clean'] is True: aptclean_stdout, aptclean_stderr, aptclean_diff = aptclean(module) # If there is nothing else to do exit. This will set state as # changed based on if the cache was updated. - if not p['package'] and not p['upgrade'] and not p['deb']: + if not p['package'] and p['upgrade'] == 'no' and not p['deb']: module.exit_json( changed=True, msg=aptclean_stdout, @@ -1331,11 +1349,9 @@ def main(): updated_cache = False updated_cache_time = 0 - install_recommends = p['install_recommends'] allow_unauthenticated = p['allow_unauthenticated'] allow_downgrade = p['allow_downgrade'] allow_change_held_packages = p['allow_change_held_packages'] - dpkg_options = expand_dpkg_options(p['dpkg_options']) autoremove = p['autoremove'] fail_on_autoremove = p['fail_on_autoremove'] autoclean = p['autoclean'] diff --git a/lib/ansible/modules/apt_key.py b/lib/ansible/modules/apt_key.py index 295dc26..669bad2 100644 --- a/lib/ansible/modules/apt_key.py +++ b/lib/ansible/modules/apt_key.py @@ -5,8 +5,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/apt_repository.py b/lib/ansible/modules/apt_repository.py index 158913a..4d01679 100644 --- a/lib/ansible/modules/apt_repository.py +++ b/lib/ansible/modules/apt_repository.py @@ -6,8 +6,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -181,9 +180,9 @@ import random import time from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.file import S_IRWU_RG_RO as DEFAULT_SOURCES_PERM from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module from ansible.module_utils.common.text.converters import to_native -from ansible.module_utils.six import PY3 from ansible.module_utils.urls import fetch_url from ansible.module_utils.common.locale import get_best_parsable_locale @@ -202,7 +201,6 @@ except ImportError: HAVE_PYTHON_APT = False APT_KEY_DIRS = ['/etc/apt/keyrings', '/etc/apt/trusted.gpg.d', '/usr/share/keyrings'] -DEFAULT_SOURCES_PERM = 0o0644 VALID_SOURCE_TYPES = ('deb', 'deb-src') @@ -231,6 +229,7 @@ class SourcesList(object): def __init__(self, module): self.module = module self.files = {} # group sources by file + self.files_mapping = {} # internal DS for tracking symlinks # Repositories that we're adding -- used to implement mode param self.new_repos = set() self.default_file = self._apt_cfg_file('Dir::Etc::sourcelist') @@ -241,6 +240,8 @@ class SourcesList(object): # read sources.list.d for file in glob.iglob('%s/*.list' % self._apt_cfg_dir('Dir::Etc::sourceparts')): + if os.path.islink(file): + self.files_mapping[file] = os.readlink(file) self.load(file) def __iter__(self): @@ -373,7 +374,11 @@ class SourcesList(object): f.write(line) except IOError as ex: self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, to_native(ex))) - self.module.atomic_move(tmp_path, filename) + if filename in self.files_mapping: + # Write to symlink target instead of replacing symlink as a normal file + self.module.atomic_move(tmp_path, self.files_mapping[filename]) + else: + self.module.atomic_move(tmp_path, filename) # allow the user to override the default mode if filename in self.new_repos: @@ -418,7 +423,7 @@ class SourcesList(object): def _add_valid_source(self, source_new, comment_new, file): # We'll try to reuse disabled source if we have it. # If we have more than one entry, we will enable them all - no advanced logic, remember. - self.module.log('ading source file: %s | %s | %s' % (source_new, comment_new, file)) + self.module.log('adding source file: %s | %s | %s' % (source_new, comment_new, file)) found = False for filename, n, enabled, source, comment in self: if source == source_new: @@ -457,7 +462,10 @@ class SourcesList(object): class UbuntuSourcesList(SourcesList): - LP_API = 'https://launchpad.net/api/1.0/~%s/+archive/%s' + # prefer api.launchpad.net over launchpad.net/api + # see: https://github.com/ansible/ansible/pull/81978#issuecomment-1767062178 + LP_API = 'https://api.launchpad.net/1.0/~%s/+archive/%s' + PPA_URI = 'https://ppa.launchpadcontent.net' def __init__(self, module): self.module = module @@ -489,7 +497,7 @@ class UbuntuSourcesList(SourcesList): except IndexError: ppa_name = 'ppa' - line = 'deb http://ppa.launchpad.net/%s/%s/ubuntu %s main' % (ppa_owner, ppa_name, self.codename) + line = 'deb %s/%s/%s/ubuntu %s main' % (self.PPA_URI, ppa_owner, ppa_name, self.codename) return line, ppa_owner, ppa_name def _key_already_exists(self, key_fingerprint): @@ -656,13 +664,13 @@ def main(): # made any more complex than it already is to try and cover more, eg, custom interpreters taking over # system locations) - apt_pkg_name = 'python3-apt' if PY3 else 'python-apt' + apt_pkg_name = 'python3-apt' if has_respawned(): # this shouldn't be possible; short-circuit early if it happens... module.fail_json(msg="{0} must be installed and visible from {1}.".format(apt_pkg_name, sys.executable)) - interpreters = ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'] + interpreters = ['/usr/bin/python3', '/usr/bin/python'] interpreter = probe_interpreters_for_module(interpreters, 'apt') diff --git a/lib/ansible/modules/assemble.py b/lib/ansible/modules/assemble.py index c93b4ff..77c33be 100644 --- a/lib/ansible/modules/assemble.py +++ b/lib/ansible/modules/assemble.py @@ -5,8 +5,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/assert.py b/lib/ansible/modules/assert.py index 0070f25..4200442 100644 --- a/lib/ansible/modules/assert.py +++ b/lib/ansible/modules/assert.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Dag Wieers # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -74,12 +73,17 @@ author: ''' EXAMPLES = r''' -- ansible.builtin.assert: { that: "ansible_os_family != 'RedHat'" } +- name: A single condition can be supplied as string instead of list + ansible.builtin.assert: + that: "ansible_os_family != 'RedHat'" -- ansible.builtin.assert: +- name: Use yaml multiline strings to ease escaping + ansible.builtin.assert: that: - "'foo' in some_command_result.stdout" - number_of_the_counting == 3 + - > + "reject" not in some_command_result.stderr - name: After version 2.7 both 'msg' and 'fail_msg' can customize failing assertion message ansible.builtin.assert: diff --git a/lib/ansible/modules/async_status.py b/lib/ansible/modules/async_status.py index c54ce3c..e07143a 100644 --- a/lib/ansible/modules/async_status.py +++ b/lib/ansible/modules/async_status.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Michael DeHaan , and others # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -37,7 +36,8 @@ attributes: async: support: none check_mode: - support: none + support: full + version_added: '2.17' diff_mode: support: none bypass_host_loop: @@ -55,17 +55,17 @@ author: EXAMPLES = r''' --- -- name: Asynchronous yum task - ansible.builtin.yum: +- name: Asynchronous dnf task + ansible.builtin.dnf: name: docker-io state: present async: 1000 poll: 0 - register: yum_sleeper + register: dnf_sleeper - name: Wait for asynchronous job to end ansible.builtin.async_status: - jid: '{{ yum_sleeper.ansible_job_id }}' + jid: '{{ dnf_sleeper.ansible_job_id }}' register: job_result until: job_result.finished retries: 100 @@ -73,7 +73,7 @@ EXAMPLES = r''' - name: Clean up async file ansible.builtin.async_status: - jid: '{{ yum_sleeper.ansible_job_id }}' + jid: '{{ dnf_sleeper.ansible_job_id }}' mode: cleanup ''' @@ -117,12 +117,15 @@ from ansible.module_utils.common.text.converters import to_native def main(): - module = AnsibleModule(argument_spec=dict( - jid=dict(type='str', required=True), - mode=dict(type='str', default='status', choices=['cleanup', 'status']), - # passed in from the async_status action plugin - _async_dir=dict(type='path', required=True), - )) + module = AnsibleModule( + argument_spec=dict( + jid=dict(type="str", required=True), + mode=dict(type="str", default="status", choices=["cleanup", "status"]), + # passed in from the async_status action plugin + _async_dir=dict(type="path", required=True), + ), + supports_check_mode=True, + ) mode = module.params['mode'] jid = module.params['jid'] diff --git a/lib/ansible/modules/async_wrapper.py b/lib/ansible/modules/async_wrapper.py index b585396..cd87f1f 100644 --- a/lib/ansible/modules/async_wrapper.py +++ b/lib/ansible/modules/async_wrapper.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Michael DeHaan , and others # 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 +from __future__ import annotations import errno @@ -22,8 +21,6 @@ import multiprocessing from ansible.module_utils.common.text.converters import to_text, to_bytes -PY3 = sys.version_info[0] == 3 - syslog.openlog('ansible-%s' % os.path.basename(__file__)) syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:])) @@ -169,13 +166,18 @@ def _run_module(wrapped_cmd, jid): interpreter = _get_interpreter(cmd[0]) if interpreter: cmd = interpreter + cmd - script = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + script = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + text=True, + encoding="utf-8", + errors="surrogateescape", + ) (outdata, stderr) = script.communicate() - if PY3: - outdata = outdata.decode('utf-8', 'surrogateescape') - stderr = stderr.decode('utf-8', 'surrogateescape') (filtered_outdata, json_warnings) = _filter_non_json_lines(outdata) diff --git a/lib/ansible/modules/blockinfile.py b/lib/ansible/modules/blockinfile.py index 3ede6fd..6d32e4d 100644 --- a/lib/ansible/modules/blockinfile.py +++ b/lib/ansible/modules/blockinfile.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -111,7 +110,7 @@ notes: - As of Ansible 2.3, the O(dest) option has been changed to O(path) as default, but O(dest) still works as well. - Option O(ignore:follow) has been removed in Ansible 2.5, because this module modifies the contents of the file so O(ignore:follow=no) does not make sense. - - When more then one block should be handled in one file you must change the O(marker) per task. + - When more than one block should be handled in one file you must change the O(marker) per task. extends_documentation_fragment: - action_common_attributes - action_common_attributes.files diff --git a/lib/ansible/modules/command.py b/lib/ansible/modules/command.py index c305952..4a3b8e1 100644 --- a/lib/ansible/modules/command.py +++ b/lib/ansible/modules/command.py @@ -4,8 +4,7 @@ # Copyright: (c) 2016, Toshio Kuratomi # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/copy.py b/lib/ansible/modules/copy.py index 0e7dfe2..cb2ccf9 100644 --- a/lib/ansible/modules/copy.py +++ b/lib/ansible/modules/copy.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -96,7 +95,7 @@ options: - If V(true) it will search for O(src) on the managed (remote) node. - O(remote_src) supports recursive copying as of version 2.8. - O(remote_src) only works with O(mode=preserve) as of version 2.6. - - Autodecryption of files does not work when O(remote_src=yes). + - Auto-decryption of files does not work when O(remote_src=yes). type: bool default: no version_added: '2.0' @@ -273,7 +272,7 @@ mode: description: Permissions of the target, after execution. returned: success type: str - sample: "0644" + sample: '0644' size: description: Size of the target, after execution. returned: success @@ -291,7 +290,6 @@ import filecmp import grp import os import os.path -import platform import pwd import shutil import stat @@ -300,13 +298,6 @@ import traceback from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.common.process import get_bin_path -from ansible.module_utils.common.locale import get_best_parsable_locale -from ansible.module_utils.six import PY3 - - -# The AnsibleModule object -module = None class AnsibleModuleError(Exception): @@ -314,21 +305,6 @@ class AnsibleModuleError(Exception): self.results = results -# Once we get run_command moved into common, we can move this into a common/files module. We can't -# until then because of the module.run_command() method. We may need to move it into -# basic::AnsibleModule() until then but if so, make it a private function so that we don't have to -# keep it for backwards compatibility later. -def clear_facls(path): - setfacl = get_bin_path('setfacl') - # FIXME "setfacl -b" is available on Linux and FreeBSD. There is "setfacl -D e" on z/OS. Others? - acl_command = [setfacl, '-b', path] - b_acl_command = [to_bytes(x) for x in acl_command] - locale = get_best_parsable_locale(module) - rc, out, err = module.run_command(b_acl_command, environ_update=dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale)) - if rc != 0: - raise RuntimeError('Error running "{0}": stdout: "{1}"; stderr: "{2}"'.format(' '.join(b_acl_command), out, err)) - - def split_pre_existing_dir(dirname): ''' Return the first pre-existing directory and a list of the new directories that will be created. @@ -529,8 +505,6 @@ def copy_common_dirs(src, dest, module): def main(): - global module - module = AnsibleModule( # not checking because of daisy chain to file module argument_spec=dict( @@ -705,54 +679,8 @@ def main(): else: raise - # might be needed below - if PY3 and hasattr(os, 'listxattr'): - try: - src_has_acls = 'system.posix_acl_access' in os.listxattr(src) - except Exception as e: - # assume unwanted ACLs by default - src_has_acls = True - # at this point we should always have tmp file - module.atomic_move(b_mysrc, dest, unsafe_writes=module.params['unsafe_writes']) - - if PY3 and hasattr(os, 'listxattr') and platform.system() == 'Linux' and not remote_src: - # atomic_move used above to copy src into dest might, in some cases, - # use shutil.copy2 which in turn uses shutil.copystat. - # Since Python 3.3, shutil.copystat copies file extended attributes: - # https://docs.python.org/3/library/shutil.html#shutil.copystat - # os.listxattr (along with others) was added to handle the operation. - - # This means that on Python 3 we are copying the extended attributes which includes - # the ACLs on some systems - further limited to Linux as the documentation above claims - # that the extended attributes are copied only on Linux. Also, os.listxattr is only - # available on Linux. - - # If not remote_src, then the file was copied from the controller. In that - # case, any filesystem ACLs are artifacts of the copy rather than preservation - # of existing attributes. Get rid of them: - - if src_has_acls: - # FIXME If dest has any default ACLs, there are not applied to src now because - # they were overridden by copystat. Should/can we do anything about this? - # 'system.posix_acl_default' in os.listxattr(os.path.dirname(b_dest)) - - try: - clear_facls(dest) - except ValueError as e: - if 'setfacl' in to_native(e): - # No setfacl so we're okay. The controller couldn't have set a facl - # without the setfacl command - pass - else: - raise - except RuntimeError as e: - # setfacl failed. - if 'Operation not supported' in to_native(e): - # The file system does not support ACLs. - pass - else: - raise + module.atomic_move(b_mysrc, dest, unsafe_writes=module.params['unsafe_writes'], keep_dest_attrs=not remote_src) except (IOError, OSError): module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc()) diff --git a/lib/ansible/modules/cron.py b/lib/ansible/modules/cron.py index d43c813..3500770 100644 --- a/lib/ansible/modules/cron.py +++ b/lib/ansible/modules/cron.py @@ -7,8 +7,7 @@ # Copyright: (c) 2015, Luca Berruti # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -215,6 +214,7 @@ import sys import tempfile from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.file import S_IRWU_RWG_RWO from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.six.moves import shlex_quote @@ -308,7 +308,7 @@ class CronTab(object): fileh = open(self.b_cron_file, 'wb') else: filed, path = tempfile.mkstemp(prefix='crontab') - os.chmod(path, int('0644', 8)) + os.chmod(path, S_IRWU_RWG_RWO) fileh = os.fdopen(filed, 'wb') fileh.write(to_bytes(self.render())) diff --git a/lib/ansible/modules/deb822_repository.py b/lib/ansible/modules/deb822_repository.py index 6b73cfe..aff4fd4 100644 --- a/lib/ansible/modules/deb822_repository.py +++ b/lib/ansible/modules/deb822_repository.py @@ -2,8 +2,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = ''' author: 'Ansible Core Team (@ansible)' @@ -237,6 +236,7 @@ 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.file import S_IRWXU_RXG_RXO, S_IRWU_RG_RO 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] @@ -260,7 +260,7 @@ def ensure_keyrings_dir(module): changed = False if not os.path.isdir(KEYRINGS_DIR): if not module.check_mode: - os.mkdir(KEYRINGS_DIR, 0o755) + os.mkdir(KEYRINGS_DIR, S_IRWXU_RXG_RXO) changed |= True changed |= module.set_fs_attributes_if_different( @@ -354,7 +354,7 @@ def write_signed_by_key(module, v, slug): module.atomic_move(tmpfile, filename) changed |= True - changed |= module.set_mode_if_different(filename, 0o0644, False) + changed |= module.set_mode_if_different(filename, S_IRWU_RG_RO, False) return changed, filename, None @@ -501,7 +501,7 @@ def main(): deb822 = Deb822() signed_by_filename = None - for key, value in params.items(): + for key, value in sorted(params.items()): if value is None: continue diff --git a/lib/ansible/modules/debconf.py b/lib/ansible/modules/debconf.py index 5ff1402..779952e 100644 --- a/lib/ansible/modules/debconf.py +++ b/lib/ansible/modules/debconf.py @@ -3,8 +3,7 @@ # Copyright: (c) 2014, Brian Coca # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -71,12 +70,14 @@ options: - The type of the value supplied. - It is highly recommended to add C(no_log=True) to task while specifying O(vtype=password). - V(seen) was added in Ansible 2.2. + - After Ansible 2.17, user can specify C(value) as a list, if C(vtype) is set as V(multiselect). type: str choices: [ boolean, error, multiselect, note, password, seen, select, string, text, title ] value: description: - - Value to set the configuration to. - type: str + - Value to set the configuration to. + - After Ansible 2.17, C(value) is of type 'raw'. + type: raw aliases: [ answer ] unseen: description: @@ -124,7 +125,7 @@ EXAMPLES = r''' RETURN = r'''#''' -from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.common.text.converters import to_text, to_native from ansible.module_utils.basic import AnsibleModule @@ -185,7 +186,7 @@ def main(): name=dict(type='str', required=True, aliases=['pkg']), question=dict(type='str', aliases=['selection', 'setting']), vtype=dict(type='str', choices=['boolean', 'error', 'multiselect', 'note', 'password', 'seen', 'select', 'string', 'text', 'title']), - value=dict(type='str', aliases=['answer']), + value=dict(type='raw', aliases=['answer']), unseen=dict(type='bool', default=False), ), required_together=(['question', 'vtype', 'value'],), @@ -218,15 +219,25 @@ def main(): if vtype == 'boolean': value = to_text(value).lower() existing = to_text(prev[question]).lower() - - if vtype == 'password': + elif vtype == 'password': existing = get_password_value(module, pkg, question, vtype) + elif vtype == 'multiselect' and isinstance(value, list): + try: + value = sorted(value) + except TypeError as exc: + module.fail_json(msg="Invalid value provided for 'multiselect': %s" % to_native(exc)) + existing = sorted([i.strip() for i in existing.split(",")]) if value != existing: changed = True if changed: if not module.check_mode: + if vtype == 'multiselect' and isinstance(value, list): + try: + value = ", ".join(value) + except TypeError as exc: + module.fail_json(msg="Invalid value provided for 'multiselect': %s" % to_native(exc)) rc, msg, e = set_selection(module, pkg, question, vtype, value, unseen) if rc: module.fail_json(msg=e) diff --git a/lib/ansible/modules/debug.py b/lib/ansible/modules/debug.py index 6e6301c..cdaf118 100644 --- a/lib/ansible/modules/debug.py +++ b/lib/ansible/modules/debug.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012 Dag Wieers # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py index 50d0ca6..593f006 100644 --- a/lib/ansible/modules/dnf.py +++ b/lib/ansible/modules/dnf.py @@ -6,8 +6,7 @@ # # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -22,7 +21,7 @@ options: description: - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact. default: "auto" - choices: [ auto, dnf4, dnf5 ] + choices: [ auto, yum, yum4, dnf4, dnf5 ] type: str version_added: 2.15 name: @@ -207,8 +206,8 @@ options: version_added: "2.7" install_repoquery: description: - - This is effectively a no-op in DNF as it is not needed with DNF, but is an accepted parameter for feature - parity/compatibility with the M(ansible.builtin.yum) module. + - This is effectively a no-op in DNF as it is not needed with DNF. + - This option is deprecated and will be removed in ansible-core 2.20. type: bool default: "yes" version_added: "2.7" @@ -246,11 +245,19 @@ options: version_added: "2.10" nobest: description: - - Set best option to False, so that transactions are not limited to best candidates only. + - This is the opposite of the O(best) option kept for backwards compatibility. + - Since ansible-core 2.17 the default value is set by the operating system distribution. required: false type: bool - default: "no" version_added: "2.11" + best: + description: + - When set to V(true), either use a package with the highest version available or fail. + - When set to V(false), if the latest version cannot be installed go with the lower version. + - Default is set by the operating system distribution. + required: false + type: bool + version_added: "2.17" cacheonly: description: - Tells dnf to run entirely from system cache; does not download or update metadata. @@ -262,7 +269,7 @@ extends_documentation_fragment: - action_common_attributes.flow attributes: action: - details: In the case of dnf, it has 2 action plugins that use it under the hood, M(ansible.builtin.yum) and M(ansible.builtin.package). + details: dnf has 2 action plugins that use it under the hood, M(ansible.builtin.dnf) and M(ansible.builtin.package). support: partial async: support: none @@ -380,7 +387,6 @@ EXAMPLES = ''' ''' import os -import re import sys from ansible.module_utils.common.text.converters import to_native, to_text @@ -410,7 +416,6 @@ class DnfModule(YumDnf): super(DnfModule, self).__init__(module) self._ensure_dnf() - self.lockfile = "/var/cache/dnf/*_lock.pid" self.pkg_mgr_name = "dnf" try: @@ -418,15 +423,6 @@ class DnfModule(YumDnf): except AttributeError: self.with_modules = False - # DNF specific args that are not part of YumDnf - self.allowerasing = self.module.params['allowerasing'] - self.nobest = self.module.params['nobest'] - - def is_lockfile_pid_valid(self): - # FIXME? it looks like DNF takes care of invalid lock files itself? - # https://github.com/ansible/ansible/issues/57189 - return True - def _sanitize_dnf_error_msg_install(self, spec, error): """ For unhandled dnf.exceptions.Error scenarios, there are certain error @@ -468,7 +464,7 @@ class DnfModule(YumDnf): 'version': package.version, 'repo': package.repoid} - # envra format for alignment with the yum module + # envra format for backwards compat result['envra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**result) # keep nevra key for backwards compat as it was previously @@ -482,94 +478,6 @@ class DnfModule(YumDnf): return result - def _split_package_arch(self, packagename): - # This list was auto generated on a Fedora 28 system with the following one-liner - # printf '[ '; for arch in $(ls /usr/lib/rpm/platform); do printf '"%s", ' ${arch%-linux}; done; printf ']\n' - redhat_rpm_arches = [ - "aarch64", "alphaev56", "alphaev5", "alphaev67", "alphaev6", "alpha", - "alphapca56", "amd64", "armv3l", "armv4b", "armv4l", "armv5tejl", "armv5tel", - "armv5tl", "armv6hl", "armv6l", "armv7hl", "armv7hnl", "armv7l", "athlon", - "geode", "i386", "i486", "i586", "i686", "ia32e", "ia64", "m68k", "mips64el", - "mips64", "mips64r6el", "mips64r6", "mipsel", "mips", "mipsr6el", "mipsr6", - "noarch", "pentium3", "pentium4", "ppc32dy4", "ppc64iseries", "ppc64le", "ppc64", - "ppc64p7", "ppc64pseries", "ppc8260", "ppc8560", "ppciseries", "ppc", "ppcpseries", - "riscv64", "s390", "s390x", "sh3", "sh4a", "sh4", "sh", "sparc64", "sparc64v", - "sparc", "sparcv8", "sparcv9", "sparcv9v", "x86_64" - ] - - name, delimiter, arch = packagename.rpartition('.') - if name and arch and arch in redhat_rpm_arches: - return name, arch - return packagename, None - - def _packagename_dict(self, packagename): - """ - Return a dictionary of information for a package name string or None - if the package name doesn't contain at least all NVR elements - """ - - if packagename[-4:] == '.rpm': - packagename = packagename[:-4] - - rpm_nevr_re = re.compile(r'(\S+)-(?:(\d*):)?(.*)-(~?\w+[\w.+]*)') - try: - arch = None - nevr, arch = self._split_package_arch(packagename) - if arch: - packagename = nevr - rpm_nevr_match = rpm_nevr_re.match(packagename) - if rpm_nevr_match: - name, epoch, version, release = rpm_nevr_re.match(packagename).groups() - if not version or not version.split('.')[0].isdigit(): - return None - else: - return None - except AttributeError as e: - self.module.fail_json( - msg='Error attempting to parse package: %s, %s' % (packagename, to_native(e)), - rc=1, - results=[] - ) - - if not epoch: - epoch = "0" - - if ':' in name: - epoch_name = name.split(":") - - epoch = epoch_name[0] - name = ''.join(epoch_name[1:]) - - result = { - 'name': name, - 'epoch': epoch, - 'release': release, - 'version': version, - } - - return result - - # Original implementation from yum.rpmUtils.miscutils (GPLv2+) - # http://yum.baseurl.org/gitweb?p=yum.git;a=blob;f=rpmUtils/miscutils.py - def _compare_evr(self, e1, v1, r1, e2, v2, r2): - # return 1: a is newer than b - # 0: a and b are the same version - # -1: b is newer than a - if e1 is None: - e1 = '0' - else: - e1 = str(e1) - v1 = str(v1) - r1 = str(r1) - if e2 is None: - e2 = '0' - else: - e2 = str(e2) - v2 = str(v2) - r2 = str(r2) - rc = dnf.rpm.rpm.labelCompare((e1, v1, r1), (e2, v2, r2)) - return rc - def _ensure_dnf(self): locale = get_best_parsable_locale(self.module) os.environ['LC_ALL'] = os.environ['LC_MESSAGES'] = locale @@ -578,7 +486,6 @@ class DnfModule(YumDnf): global dnf try: import dnf - import dnf.cli import dnf.const import dnf.exceptions import dnf.package @@ -689,9 +596,11 @@ class DnfModule(YumDnf): if self.skip_broken: conf.strict = 0 - # Set best - if self.nobest: - conf.best = 0 + # best and nobest are mutually exclusive + if self.nobest is not None: + conf.best = not self.nobest + elif self.best is not None: + conf.best = self.best if self.download_only: conf.downloadonly = True @@ -724,6 +633,11 @@ class DnfModule(YumDnf): for repo in repos.get_matching(repo_pattern): repo.enable() + for repo in base.repos.iter_enabled(): + if self.disable_gpg_check: + repo.gpgcheck = False + repo.repo_gpgcheck = False + def _base(self, conf_file, disable_gpg_check, disablerepo, enablerepo, installroot, sslverify): """Return a fully configured dnf Base object.""" base = dnf.Base() @@ -809,48 +723,28 @@ class DnfModule(YumDnf): self.module.exit_json(msg="", results=results) def _is_installed(self, pkg): - installed = self.base.sack.query().installed() - - package_spec = {} - name, arch = self._split_package_arch(pkg) - if arch: - package_spec['arch'] = arch - - package_details = self._packagename_dict(pkg) - if package_details: - package_details['epoch'] = int(package_details['epoch']) - package_spec.update(package_details) - else: - package_spec['name'] = name - - return bool(installed.filter(**package_spec)) + return bool( + dnf.subject.Subject(pkg).get_best_query(sack=self.base.sack).installed().run() + ) def _is_newer_version_installed(self, pkg_name): - candidate_pkg = self._packagename_dict(pkg_name) - if not candidate_pkg: - # The user didn't provide a versioned rpm, so version checking is - # not required - return False - - installed = self.base.sack.query().installed() - installed_pkg = installed.filter(name=candidate_pkg['name']).run() - if installed_pkg: - installed_pkg = installed_pkg[0] - - # this looks weird but one is a dict and the other is a dnf.Package - evr_cmp = self._compare_evr( - installed_pkg.epoch, installed_pkg.version, installed_pkg.release, - candidate_pkg['epoch'], candidate_pkg['version'], candidate_pkg['release'], - ) - - return evr_cmp == 1 - else: + try: + if isinstance(pkg_name, dnf.package.Package): + available = pkg_name + else: + available = sorted( + dnf.subject.Subject(pkg_name).get_best_query(sack=self.base.sack).available().run() + )[-1] + installed = sorted(self.base.sack.query().installed().filter(name=available.name).run())[-1] + except IndexError: return False + return installed > available def _mark_package_install(self, pkg_spec, upgrade=False): """Mark the package for install.""" is_newer_version_installed = self._is_newer_version_installed(pkg_spec) is_installed = self._is_installed(pkg_spec) + msg = '' try: if is_newer_version_installed: if self.allow_downgrade: @@ -884,18 +778,16 @@ class DnfModule(YumDnf): pass else: # Case 7, The package is not installed, simply install it self.base.install(pkg_spec, strict=self.base.conf.strict) - - return {'failed': False, 'msg': '', 'failure': '', 'rc': 0} - except dnf.exceptions.MarkingError as e: - return { - 'failed': True, - 'msg': "No package {0} available.".format(pkg_spec), - 'failure': " ".join((pkg_spec, to_native(e))), - 'rc': 1, - "results": [] - } - + msg = "No package {0} available.".format(pkg_spec) + if self.base.conf.strict: + return { + 'failed': True, + 'msg': msg, + 'failure': " ".join((pkg_spec, to_native(e))), + 'rc': 1, + "results": [] + } except dnf.exceptions.DepsolveError as e: return { 'failed': True, @@ -904,7 +796,6 @@ class DnfModule(YumDnf): 'rc': 1, "results": [] } - except dnf.exceptions.Error as e: if to_text("already installed") in to_text(e): return {'failed': False, 'msg': '', 'failure': ''} @@ -917,16 +808,7 @@ class DnfModule(YumDnf): "results": [] } - def _whatprovides(self, filepath): - self.base.read_all_repos() - available = self.base.sack.query().available() - # Search in file - files_filter = available.filter(file=filepath) - # And Search in provides - pkg_spec = files_filter.union(available.filter(provides=filepath)).run() - - if pkg_spec: - return pkg_spec[0].name + return {'failed': False, 'msg': msg, 'failure': '', 'rc': 0} def _parse_spec_group_file(self): pkg_specs, grp_specs, module_specs, filenames = [], [], [], [] @@ -939,11 +821,13 @@ class DnfModule(YumDnf): elif name.endswith(".rpm"): filenames.append(name) elif name.startswith('/'): - # like "dnf install /usr/bin/vi" - pkg_spec = self._whatprovides(name) - if pkg_spec: - pkg_specs.append(pkg_spec) - continue + # dnf install /usr/bin/vi + installed = self.base.sack.query().filter(provides=name, file=name).installed().run() + if installed: + pkg_specs.append(installed[0].name) # should be only one? + elif not self.update_only: + # not installed, pass the filename for dnf to process + pkg_specs.append(name) elif name.startswith("@") or ('/' in name): if not already_loaded_comps: self.base.read_comps() @@ -1005,7 +889,7 @@ class DnfModule(YumDnf): else: for pkg in pkgs: try: - if self._is_newer_version_installed(self._package_dict(pkg)['nevra']): + if self._is_newer_version_installed(pkg): if self.allow_downgrade: self.base.package_install(pkg, strict=self.base.conf.strict) else: @@ -1201,13 +1085,6 @@ class DnfModule(YumDnf): response['results'].append("Packages providing %s not installed due to update_only specified" % spec) else: for pkg_spec in pkg_specs: - # Previously we forced base.conf.best=True here. - # However in 2.11+ there is a self.nobest option, so defer to that. - # Note, however, that just because nobest isn't set, doesn't mean that - # base.conf.best is actually true. We only force it false in - # _configure_base(), we never set it to true, and it can default to false. - # Thus, we still need to explicitly set it here. - self.base.conf.best = not self.nobest install_result = self._mark_package_install(pkg_spec, upgrade=True) if install_result['failed']: if install_result['msg']: @@ -1459,11 +1336,7 @@ def main(): # list=repos # list=pkgspec - # Extend yumdnf_argument_spec with dnf-specific features that will never be - # backported to yum because yum is now in "maintenance mode" upstream - yumdnf_argument_spec['argument_spec']['allowerasing'] = dict(default=False, type='bool') - yumdnf_argument_spec['argument_spec']['nobest'] = dict(default=False, type='bool') - yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'dnf4', 'dnf5']) + yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf4', 'dnf5']) module = AnsibleModule( **yumdnf_argument_spec diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py index c55b673..7af1f4a 100644 --- a/lib/ansible/modules/dnf5.py +++ b/lib/ansible/modules/dnf5.py @@ -2,9 +2,8 @@ # Copyright 2023 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 +from __future__ import annotations -__metaclass__ = type DOCUMENTATION = """ module: dnf5 @@ -152,7 +151,7 @@ options: validate_certs: description: - This is effectively a no-op in the dnf5 module as dnf5 itself handles downloading a https url as the source of the rpm, - but is an accepted parameter for feature parity/compatibility with the M(ansible.builtin.yum) module. + but is an accepted parameter for feature parity/compatibility with the M(ansible.builtin.dnf) module. type: bool default: "yes" sslverify: @@ -175,8 +174,8 @@ options: default: "no" install_repoquery: description: - - This is effectively a no-op in DNF as it is not needed with DNF, but is an accepted parameter for feature - parity/compatibility with the M(ansible.builtin.yum) module. + - This is effectively a no-op in DNF as it is not needed with DNF. + - This option is deprecated and will be removed in ansible-core 2.20. type: bool default: "yes" download_only: @@ -209,10 +208,18 @@ options: default: "no" nobest: description: - - Set best option to False, so that transactions are not limited to best candidates only. + - This is the opposite of the O(best) option kept for backwards compatibility. + - Since ansible-core 2.17 the default value is set by the operating system distribution. required: false type: bool - default: "no" + best: + description: + - When set to V(true), either use a package with the highest version available or fail. + - When set to V(false), if the latest version cannot be installed go with the lower version. + - Default is set by the operating system distribution. + required: false + type: bool + version_added: "2.17" cacheonly: description: - Tells dnf to run entirely from system cache; does not download or update metadata. @@ -223,7 +230,7 @@ extends_documentation_fragment: - action_common_attributes.flow attributes: action: - details: In the case of dnf, it has 2 action plugins that use it under the hood, M(ansible.builtin.yum) and M(ansible.builtin.package). + details: dnf5 has 2 action plugins that use it under the hood, M(ansible.builtin.dnf) and M(ansible.builtin.package). support: partial async: support: none @@ -357,23 +364,47 @@ def is_installed(base, spec): def is_newer_version_installed(base, spec): + # FIXME investigate whether this function can be replaced by dnf5's allow_downgrade option + if "/" in spec: + spec = spec.split("/")[-1] + if spec.endswith(".rpm"): + spec = spec[:-4] + try: spec_nevra = next(iter(libdnf5.rpm.Nevra.parse(spec))) - except RuntimeError: + except (RuntimeError, StopIteration): return False - spec_name = spec_nevra.get_name() - v = spec_nevra.get_version() - r = spec_nevra.get_release() - if not v or not r: + + spec_version = spec_nevra.get_version() + if not spec_version: return False - spec_evr = "{}:{}-{}".format(spec_nevra.get_epoch() or "0", v, r) - query = libdnf5.rpm.PackageQuery(base) - query.filter_installed() - query.filter_name([spec_name]) - query.filter_evr([spec_evr], libdnf5.common.QueryCmp_GT) + installed = libdnf5.rpm.PackageQuery(base) + installed.filter_installed() + installed.filter_name([spec_nevra.get_name()]) + installed.filter_latest_evr() + try: + installed_package = list(installed)[-1] + except IndexError: + return False + + target = libdnf5.rpm.PackageQuery(base) + target.filter_name([spec_nevra.get_name()]) + target.filter_version([spec_version]) + spec_release = spec_nevra.get_release() + if spec_release: + target.filter_release([spec_release]) + spec_epoch = spec_nevra.get_epoch() + if spec_epoch: + target.filter_epoch([spec_epoch]) + target.filter_latest_evr() + try: + target_package = list(target)[-1] + except IndexError: + return False - return query.size() > 0 + # FIXME https://github.com/rpm-software-management/dnf5/issues/1104 + return libdnf5.rpm.rpmvercmp(installed_package.get_evr(), target_package.get_evr()) == 1 def package_to_dict(package): @@ -394,8 +425,7 @@ def get_unneeded_pkgs(base): query = libdnf5.rpm.PackageQuery(base) query.filter_installed() query.filter_unneeded() - for pkg in query: - yield pkg + yield from query class Dnf5Module(YumDnf): @@ -403,14 +433,8 @@ class Dnf5Module(YumDnf): super(Dnf5Module, self).__init__(module) self._ensure_dnf() - # FIXME https://github.com/rpm-software-management/dnf5/issues/402 - self.lockfile = "" self.pkg_mgr_name = "dnf5" - # DNF specific args that are not part of YumDnf - self.allowerasing = self.module.params["allowerasing"] - self.nobest = self.module.params["nobest"] - def _ensure_dnf(self): locale = get_best_parsable_locale(self.module) os.environ["LC_ALL"] = os.environ["LC_MESSAGES"] = locale @@ -452,10 +476,6 @@ class Dnf5Module(YumDnf): failures=[], ) - def is_lockfile_pid_valid(self): - # FIXME https://github.com/rpm-software-management/dnf5/issues/402 - return True - def run(self): if sys.version_info.major < 3: self.module.fail_json( @@ -503,7 +523,11 @@ class Dnf5Module(YumDnf): self.disable_excludes = "*" conf.disable_excludes = self.disable_excludes conf.skip_broken = self.skip_broken - conf.best = not self.nobest + # best and nobest are mutually exclusive + if self.nobest is not None: + conf.best = not self.nobest + elif self.best is not None: + conf.best = self.best conf.install_weak_deps = self.install_weak_deps conf.gpgcheck = not self.disable_gpg_check conf.localpkg_gpgcheck = not self.disable_gpg_check @@ -606,13 +630,7 @@ class Dnf5Module(YumDnf): for spec in self.names: if is_newer_version_installed(base, spec): if self.allow_downgrade: - if upgrade: - if is_installed(base, spec): - goal.add_upgrade(spec, settings) - else: - goal.add_install(spec, settings) - else: - goal.add_install(spec, settings) + goal.add_install(spec, settings) elif is_installed(base, spec): if upgrade: goal.add_upgrade(spec, settings) @@ -706,10 +724,6 @@ class Dnf5Module(YumDnf): def main(): - # Extend yumdnf_argument_spec with dnf-specific features that will never be - # backported to yum because yum is now in "maintenance mode" upstream - yumdnf_argument_spec["argument_spec"]["allowerasing"] = dict(default=False, type="bool") - yumdnf_argument_spec["argument_spec"]["nobest"] = dict(default=False, type="bool") Dnf5Module(AnsibleModule(**yumdnf_argument_spec)).run() diff --git a/lib/ansible/modules/dpkg_selections.py b/lib/ansible/modules/dpkg_selections.py index 7c8a725..b591636 100644 --- a/lib/ansible/modules/dpkg_selections.py +++ b/lib/ansible/modules/dpkg_selections.py @@ -3,8 +3,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/expect.py b/lib/ansible/modules/expect.py index 8ff5cb4..6144763 100644 --- a/lib/ansible/modules/expect.py +++ b/lib/ansible/modules/expect.py @@ -3,8 +3,7 @@ # (c) 2015, Matt Martz # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -38,9 +37,11 @@ options: responses: type: dict description: - - Mapping of expected string/regex and string to respond with. If the - response is a list, successive matches return successive - responses. List functionality is new in 2.1. + - Mapping of prompt regular expressions and corresponding answer(s). + - Each key in O(responses) is a Python regex U(https://docs.python.org/3/library/re.html#regular-expression-syntax). + - The value of each key is a string or list of strings. + If the value is a string and the prompt is encountered multiple times, the answer will be repeated. + Provide the value as a list to give different answers for successive matches. required: true timeout: type: raw @@ -69,15 +70,10 @@ notes: - If you want to run a command through the shell (say you are using C(<), C(>), C(|), and so on), you must specify a shell in the command such as C(/bin/bash -c "/path/to/something | grep else"). - - The question, or key, under O(responses) is a python regex match. Case - insensitive searches are indicated with a prefix of C(?i). + - Case insensitive searches are indicated with a prefix of C(?i). - The C(pexpect) library used by this module operates with a search window of 2000 bytes, and does not use a multiline regex match. To perform a start of line bound match, use a pattern like ``(?m)^pattern`` - - By default, if a question is encountered multiple times, its string - response will be repeated. If you need different responses for successive - question matches, instead of a string response, use a list of strings as - the response. The list functionality is new in 2.1. - The M(ansible.builtin.expect) module is designed for simple scenarios. For more complex needs, consider the use of expect code with the M(ansible.builtin.shell) or M(ansible.builtin.script) modules. (An example is part of the M(ansible.builtin.shell) module documentation). @@ -98,14 +94,28 @@ EXAMPLES = r''' # you don't want to show passwords in your logs no_log: true -- name: Generic question with multiple different responses +- name: Match multiple regular expressions and demonstrate individual and repeated responses ansible.builtin.expect: command: /path/to/custom/command responses: Question: + # give a unique response for each of the 3 hypothetical prompts matched - response1 - response2 - response3 + # give the same response for every matching prompt + "^Match another prompt$": "response" + +- name: Multiple questions with responses + ansible.builtin.expect: + command: /path/to/custom/command + responses: + "Please provide your name": + - "Anna" + "Database user": + - "{{ db_username }}" + "Database password": + - "{{ db_password }}" ''' import datetime @@ -167,9 +177,7 @@ def main(): try: timeout = check_type_int(timeout) except TypeError as te: - module.fail_json( - msg="argument 'timeout' is of type {timeout_type} and we were unable to convert to int: {te}".format(timeout_type=type(timeout), te=te) - ) + module.fail_json(msg=f"argument 'timeout' is of type {type(timeout)} and we were unable to convert to int: {te}") echo = module.params['echo'] events = dict() diff --git a/lib/ansible/modules/fail.py b/lib/ansible/modules/fail.py index 8d3fa15..e7a057e 100644 --- a/lib/ansible/modules/fail.py +++ b/lib/ansible/modules/fail.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Dag Wieers # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/fetch.py b/lib/ansible/modules/fetch.py index 77ebd19..66726e3 100644 --- a/lib/ansible/modules/fetch.py +++ b/lib/ansible/modules/fetch.py @@ -5,8 +5,7 @@ # This is a virtual module that is entirely implemented as an action plugin and runs on the controller -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/file.py b/lib/ansible/modules/file.py index 0aa9183..564d7f6 100644 --- a/lib/ansible/modules/file.py +++ b/lib/ansible/modules/file.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -66,7 +65,7 @@ options: - > Force the creation of the symlinks in two cases: the source file does not exist (but will appear later); the destination exists and is a file (so, we need to unlink the - O(path) file and create symlink to the O(src) file in place of it). + O(path) file and create a symlink to the O(src) file in place of it). type: bool default: no follow: @@ -74,6 +73,8 @@ options: - This flag indicates that filesystem links, if they exist, should be followed. - O(follow=yes) and O(state=link) can modify O(src) when combined with parameters such as O(mode). - Previous to Ansible 2.5, this was V(false) by default. + - While creating a symlink with a non-existent destination, set O(follow) to V(false) to avoid a warning message related to permission issues. + The warning message is added to notify the user that we can not set permissions to the non-existent destination. type: bool default: yes version_added: '1.8' diff --git a/lib/ansible/modules/find.py b/lib/ansible/modules/find.py index 0251224..5e8e36a 100644 --- a/lib/ansible/modules/find.py +++ b/lib/ansible/modules/find.py @@ -6,8 +6,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -150,6 +149,11 @@ options: - Default is unlimited depth. type: int version_added: "2.6" + encoding: + description: + - When doing a C(contains) search, determine the encoding of the files to be searched. + type: str + version_added: "2.17" extends_documentation_fragment: action_common_attributes attributes: check_mode: @@ -339,11 +343,12 @@ def sizefilter(st, size): return False -def contentfilter(fsname, pattern, read_whole_file=False): +def contentfilter(fsname, pattern, encoding, read_whole_file=False): """ Filter files which contain the given expression :arg fsname: Filename to scan for lines matching a pattern :arg pattern: Pattern to look for inside of line + :arg encoding: Encoding of the file to be scanned :arg read_whole_file: If true, the whole file is read into memory before the regex is applied against it. Otherwise, the regex is applied line-by-line. :rtype: bool :returns: True if one of the lines in fsname matches the pattern. Otherwise False @@ -354,7 +359,7 @@ def contentfilter(fsname, pattern, read_whole_file=False): prog = re.compile(pattern) try: - with open(fsname) as f: + with open(fsname, encoding=encoding) as f: if read_whole_file: return bool(prog.search(f.read())) @@ -362,6 +367,13 @@ def contentfilter(fsname, pattern, read_whole_file=False): if prog.match(line): return True + except LookupError as e: + raise e + except UnicodeDecodeError as e: + if encoding is None: + encoding = 'None (default determined by the Python built-in function "open")' + msg = f'Failed to read the file {fsname} due to an encoding error. current encoding: {encoding}' + raise Exception(msg) from e except Exception: pass @@ -455,6 +467,7 @@ def main(): depth=dict(type='int'), mode=dict(type='raw'), exact_mode=dict(type='bool', default=True), + encoding=dict(type='str') ), supports_check_mode=True, ) @@ -567,7 +580,7 @@ def main(): if (pfilter(fsobj, params['patterns'], params['excludes'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']) and sizefilter(st, size) and - contentfilter(fsname, params['contains'], params['read_whole_file']) and + contentfilter(fsname, params['contains'], params['encoding'], params['read_whole_file']) and mode_filter(st, params['mode'], params['exact_mode'], module)): r.update(statinfo(st)) diff --git a/lib/ansible/modules/gather_facts.py b/lib/ansible/modules/gather_facts.py index 123001b..561275f 100644 --- a/lib/ansible/modules/gather_facts.py +++ b/lib/ansible/modules/gather_facts.py @@ -2,8 +2,7 @@ # Copyright (c) 2017 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/get_url.py b/lib/ansible/modules/get_url.py index 860b73a..920b986 100644 --- a/lib/ansible/modules/get_url.py +++ b/lib/ansible/modules/get_url.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Jan-Piet Mens # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -261,7 +260,7 @@ EXAMPLES = r''' - name: Download file from a file path ansible.builtin.get_url: - url: file:///tmp/afile.txt + url: file:///tmp/a_file.txt dest: /tmp/afilecopy.txt - name: < Fetch file that requires authentication. diff --git a/lib/ansible/modules/getent.py b/lib/ansible/modules/getent.py index 5487354..b07fb82 100644 --- a/lib/ansible/modules/getent.py +++ b/lib/ansible/modules/getent.py @@ -3,8 +3,7 @@ # Copyright: (c) 2014, Brian Coca # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -110,7 +109,7 @@ ansible_facts: description: - A list of results or a single result as a list of the fields the db provides - The list elements depend on the database queried, see getent man page for the structure - - Starting at 2.11 it now returns multiple duplicate entries, previouslly it only returned the last one + - Starting at 2.11 it now returns multiple duplicate entries, previously it only returned the last one returned: always type: list ''' diff --git a/lib/ansible/modules/git.py b/lib/ansible/modules/git.py index 681708e..26d4c59 100644 --- a/lib/ansible/modules/git.py +++ b/lib/ansible/modules/git.py @@ -3,8 +3,7 @@ # (c) 2012, Michael DeHaan # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -208,15 +207,18 @@ options: type: path version_added: "2.7" - gpg_whitelist: + gpg_allowlist: description: - A list of trusted GPG fingerprints to compare to the fingerprint of the GPG-signed commit. - Only used when O(verify_commit=yes). - Use of this feature requires Git 2.6+ due to its reliance on git's C(--raw) flag to C(verify-commit) and C(verify-tag). + - Alias O(gpg_allowlist) is added in version 2.17. + - Alias O(gpg_whitelist) is deprecated and will be removed in version 2.21. type: list elements: str default: [] + aliases: [ gpg_whitelist ] version_added: "2.9" requirements: @@ -568,7 +570,7 @@ def get_submodule_versions(git_path, module, dest, version='HEAD'): def clone(git_path, module, repo, dest, remote, depth, version, bare, - reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch): + reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_allowlist, single_branch): ''' makes a new git repo if it does not already exist ''' dest_dirname = os.path.dirname(dest) try: @@ -635,7 +637,7 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare, module.run_command(cmd, check_rc=True, cwd=dest) if verify_commit: - verify_commit_sign(git_path, module, dest, version, gpg_whitelist) + verify_commit_sign(git_path, module, dest, version, gpg_allowlist) def has_local_mods(module, git_path, dest, bare): @@ -1016,7 +1018,7 @@ def set_remote_branch(git_path, module, dest, remote, version, depth): module.fail_json(msg="Failed to fetch branch from remote: %s" % version, stdout=out, stderr=err, rc=rc) -def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist): +def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_allowlist): cmd = '' if version == 'HEAD': branch = get_head_branch(git_path, module, dest, remote) @@ -1052,26 +1054,26 @@ def switch_version(git_path, module, dest, remote, version, verify_commit, depth stdout=out1, stderr=err1, rc=rc, cmd=cmd) if verify_commit: - verify_commit_sign(git_path, module, dest, version, gpg_whitelist) + verify_commit_sign(git_path, module, dest, version, gpg_allowlist) return (rc, out1, err1) -def verify_commit_sign(git_path, module, dest, version, gpg_whitelist): +def verify_commit_sign(git_path, module, dest, version, gpg_allowlist): if version in get_annotated_tags(git_path, module, dest): git_sub = "verify-tag" else: git_sub = "verify-commit" cmd = "%s %s %s" % (git_path, git_sub, version) - if gpg_whitelist: + if gpg_allowlist: cmd += " --raw" (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc) - if gpg_whitelist: + if gpg_allowlist: fingerprint = get_gpg_fingerprint(err) - if fingerprint not in gpg_whitelist: - module.fail_json(msg='The gpg_whitelist does not include the public key "%s" for this commit' % fingerprint, stdout=out, stderr=err, rc=rc) + if fingerprint not in gpg_allowlist: + module.fail_json(msg='The gpg_allowlist does not include the public key "%s" for this commit' % fingerprint, stdout=out, stderr=err, rc=rc) return (rc, out, err) @@ -1184,7 +1186,16 @@ def main(): clone=dict(default='yes', type='bool'), update=dict(default='yes', type='bool'), verify_commit=dict(default='no', type='bool'), - gpg_whitelist=dict(default=[], type='list', elements='str'), + gpg_allowlist=dict( + default=[], type='list', aliases=['gpg_whitelist'], elements='str', + deprecated_aliases=[ + dict( + name='gpg_whitelist', + version='2.21', + collection_name='ansible.builtin', + ) + ], + ), accept_hostkey=dict(default='no', type='bool'), accept_newhostkey=dict(default='no', type='bool'), key_file=dict(default=None, type='path', required=False), @@ -1215,7 +1226,7 @@ def main(): allow_clone = module.params['clone'] bare = module.params['bare'] verify_commit = module.params['verify_commit'] - gpg_whitelist = module.params['gpg_whitelist'] + gpg_allowlist = module.params['gpg_allowlist'] reference = module.params['reference'] single_branch = module.params['single_branch'] git_path = module.params['executable'] or module.get_bin_path('git', True) @@ -1264,7 +1275,7 @@ def main(): # We screenscrape a huge amount of git commands so use C locale anytime we # call run_command() locale = get_best_parsable_locale(module) - module.run_command_environ_update = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LC_CTYPE=locale) + module.run_command_environ_update = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale, LC_CTYPE=locale, LANGUAGE=locale) if separate_git_dir: separate_git_dir = os.path.realpath(separate_git_dir) @@ -1322,7 +1333,7 @@ def main(): module.exit_json(**result) # there's no git config, so clone clone(git_path, module, repo, dest, remote, depth, version, bare, reference, - refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch) + refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_allowlist, single_branch) elif not update: # Just return having found a repo already in the dest path # this does no checking that the repo is the actual repo @@ -1377,7 +1388,7 @@ def main(): # switch to version specified regardless of whether # we got new revisions from the repository if not bare: - switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist) + switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_allowlist) # Deal with submodules submodules_updated = False diff --git a/lib/ansible/modules/group.py b/lib/ansible/modules/group.py index 45590d1..100d211 100644 --- a/lib/ansible/modules/group.py +++ b/lib/ansible/modules/group.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Stephen Fromm # 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/group_by.py b/lib/ansible/modules/group_by.py index 0d1e0c8..6efe800 100644 --- a/lib/ansible/modules/group_by.py +++ b/lib/ansible/modules/group_by.py @@ -4,8 +4,7 @@ # Copyright: Ansible Team # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/hostname.py b/lib/ansible/modules/hostname.py index 4a1c7ea..1f0bfa0 100644 --- a/lib/ansible/modules/hostname.py +++ b/lib/ansible/modules/hostname.py @@ -4,8 +4,7 @@ # Copyright: (c) 2013, Hiroaki Nakamura # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -82,7 +81,6 @@ from ansible.module_utils.common.sys_info import get_platform_subclass from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector from ansible.module_utils.facts.utils import get_file_lines, get_file_content from ansible.module_utils.common.text.converters import to_native, to_text -from ansible.module_utils.six import PY3, text_type STRATS = { 'alpine': 'Alpine', @@ -533,21 +531,6 @@ class DarwinStrategy(BaseStrategy): self.name_types = ('HostName', 'ComputerName', 'LocalHostName') self.scrubbed_name = self._scrub_hostname(self.module.params['name']) - def _make_translation(self, replace_chars, replacement_chars, delete_chars): - if PY3: - return str.maketrans(replace_chars, replacement_chars, delete_chars) - - if not isinstance(replace_chars, text_type) or not isinstance(replacement_chars, text_type): - raise ValueError('replace_chars and replacement_chars must both be strings') - if len(replace_chars) != len(replacement_chars): - raise ValueError('replacement_chars must be the same length as replace_chars') - - table = dict(zip((ord(c) for c in replace_chars), replacement_chars)) - for char in delete_chars: - table[ord(char)] = None - - return table - def _scrub_hostname(self, name): """ LocalHostName only accepts valid DNS characters while HostName and ComputerName @@ -559,7 +542,7 @@ class DarwinStrategy(BaseStrategy): name = to_text(name) replace_chars = u'\'"~`!@#$%^&*(){}[]/=?+\\|-_ ' delete_chars = u".'" - table = self._make_translation(replace_chars, u'-' * len(replace_chars), delete_chars) + table = str.maketrans(replace_chars, '-' * len(replace_chars), delete_chars) name = name.translate(table) # Replace multiple dashes with a single dash diff --git a/lib/ansible/modules/import_playbook.py b/lib/ansible/modules/import_playbook.py index 09ca85b..a4c7809 100644 --- a/lib/ansible/modules/import_playbook.py +++ b/lib/ansible/modules/import_playbook.py @@ -3,8 +3,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/import_role.py b/lib/ansible/modules/import_role.py index e92f4d7..719d429 100644 --- a/lib/ansible/modules/import_role.py +++ b/lib/ansible/modules/import_role.py @@ -2,8 +2,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -56,6 +55,14 @@ options: type: bool default: yes version_added: '2.11' + public: + description: + - This option dictates whether the role's C(vars) and C(defaults) are exposed to the play. + - Variables are exposed to the play at playbook parsing time, and available to earlier roles and tasks as well unlike C(include_role). + - The default depends on the configuration option :ref:`default_private_role_vars`. + type: bool + default: yes + version_added: '2.17' extends_documentation_fragment: - action_common_attributes - action_common_attributes.conn diff --git a/lib/ansible/modules/import_tasks.py b/lib/ansible/modules/import_tasks.py index 0ef4023..4d60368 100644 --- a/lib/ansible/modules/import_tasks.py +++ b/lib/ansible/modules/import_tasks.py @@ -3,8 +3,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/include_role.py b/lib/ansible/modules/include_role.py index 84a3fe5..9fa0703 100644 --- a/lib/ansible/modules/include_role.py +++ b/lib/ansible/modules/include_role.py @@ -3,8 +3,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/include_tasks.py b/lib/ansible/modules/include_tasks.py index f631430..82fb586 100644 --- a/lib/ansible/modules/include_tasks.py +++ b/lib/ansible/modules/include_tasks.py @@ -3,8 +3,7 @@ # Copyright: 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/include_vars.py b/lib/ansible/modules/include_vars.py index 3752ca6..99e77cb 100644 --- a/lib/ansible/modules/include_vars.py +++ b/lib/ansible/modules/include_vars.py @@ -2,8 +2,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/iptables.py b/lib/ansible/modules/iptables.py index 8b9a46a..b7fd778 100644 --- a/lib/ansible/modules/iptables.py +++ b/lib/ansible/modules/iptables.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, Sébastien DA ROCHA # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -38,7 +37,7 @@ notes: options: table: description: - - This option specifies the packet matching table which the command should operate on. + - This option specifies the packet matching table on which the command should operate. - If the kernel is configured with automatic module loading, an attempt will be made to load the appropriate module for that table if it is not already there. type: str @@ -134,9 +133,9 @@ options: description: - Specifies a match to use, that is, an extension module that tests for a specific property. - - The set of matches make up the condition under which a target is invoked. + - The set of matches makes up the condition under which a target is invoked. - Matches are evaluated first to last if specified as an array and work in short-circuit - fashion, i.e. if one extension yields false, evaluation will stop. + fashion, i.e. if one extension yields false, the evaluation will stop. type: list elements: str default: [] @@ -144,7 +143,7 @@ options: description: - This specifies the target of the rule; i.e., what to do if the packet matches it. - The target can be a user-defined chain (other than the one - this rule is in), one of the special builtin targets which decide the + this rule is in), one of the special builtin targets that decide the fate of the packet immediately, or an extension (see EXTENSIONS below). - If this option is omitted in a rule (and the goto parameter @@ -153,13 +152,13 @@ options: type: str gateway: description: - - This specifies the IP address of host to send the cloned packets. + - This specifies the IP address of the host to send the cloned packets. - This option is only valid when O(jump) is set to V(TEE). type: str version_added: "2.8" log_prefix: description: - - Specifies a log text for the rule. Only make sense with a LOG jump. + - Specifies a log text for the rule. Only makes sense with a LOG jump. type: str version_added: "2.5" log_level: @@ -172,7 +171,7 @@ options: choices: [ '0', '1', '2', '3', '4', '5', '6', '7', 'emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug' ] goto: description: - - This specifies that the processing should continue in a user specified chain. + - This specifies that the processing should continue in a user-specified chain. - Unlike the jump argument return will not continue processing in this chain but instead in the chain that called us via jump. type: str @@ -200,7 +199,7 @@ options: of fragmented packets. - Since there is no way to tell the source or destination ports of such a packet (or ICMP type), such a packet will not match any rules which specify them. - - When the "!" argument precedes fragment argument, the rule will only match head fragments, + - When the "!" argument precedes the fragment argument, the rule will only match head fragments, or unfragmented packets. type: str set_counters: @@ -266,6 +265,7 @@ options: description: - This allows specifying a DSCP mark to be added to packets. It takes either an integer or hex value. + - If the parameter is set, O(jump) is set to V(DSCP). - Mutually exclusive with O(set_dscp_mark_class). type: str version_added: "2.1" @@ -273,6 +273,7 @@ options: description: - This allows specifying a predefined DiffServ class which will be translated to the corresponding DSCP mark. + - If the parameter is set, O(jump) is set to V(DSCP). - Mutually exclusive with O(set_dscp_mark). type: str version_added: "2.1" @@ -289,7 +290,7 @@ options: default: [] src_range: description: - - Specifies the source IP range to match in the iprange module. + - Specifies the source IP range to match the iprange module. type: str version_added: "2.8" dst_range: @@ -299,8 +300,8 @@ options: version_added: "2.8" match_set: description: - - Specifies a set name which can be defined by ipset. - - Must be used together with the match_set_flags parameter. + - Specifies a set name that can be defined by ipset. + - Must be used together with the O(match_set_flags) parameter. - When the V(!) argument is prepended then it inverts the rule. - Uses the iptables set extension. type: str @@ -308,10 +309,11 @@ options: match_set_flags: description: - Specifies the necessary flags for the match_set parameter. - - Must be used together with the match_set parameter. + - Must be used together with the O(match_set) parameter. - Uses the iptables set extension. + - Choices V(dst,dst) and V(src,src) added in version 2.17. type: str - choices: [ "src", "dst", "src,dst", "dst,src" ] + choices: [ "src", "dst", "src,dst", "dst,src", "dst,dst", "src,src" ] version_added: "2.11" limit: description: @@ -327,14 +329,14 @@ options: version_added: "2.1" uid_owner: description: - - Specifies the UID or username to use in match by owner rule. + - Specifies the UID or username to use in the match by owner rule. - From Ansible 2.6 when the C(!) argument is prepended then the it inverts the rule to apply instead to all users except that one specified. type: str version_added: "2.1" gid_owner: description: - - Specifies the GID or group to use in match by owner rule. + - Specifies the GID or group to use in the match by owner rule. type: str version_added: "2.9" reject_with: @@ -364,7 +366,7 @@ options: - Only built-in chains can have policies. - This parameter requires the O(chain) parameter. - If you specify this parameter, all other parameters will be ignored. - - This parameter is used to set default policy for the given O(chain). + - This parameter is used to set the default policy for the given O(chain). Do not confuse this with O(jump) parameter. type: str choices: [ ACCEPT, DROP, QUEUE, RETURN ] @@ -386,9 +388,9 @@ options: numeric: description: - This parameter controls the running of the list -action of iptables, which is used internally by the module - - Does not affect the actual functionality. Use this if iptables hangs when creating chain or altering policy + - Does not affect the actual functionality. Use this if iptables hang when creating a chain or altering policy - If V(true), then iptables skips the DNS-lookup of the IP addresses in a chain when it uses the list -action - - Listing is used internally for example when setting a policy or creting of a chain + - Listing is used internally for example when setting a policy or creating a chain type: bool default: false version_added: "2.15" @@ -636,11 +638,16 @@ def construct_rule(params): append_param(rule, params['destination_port'], '--destination-port', False) append_param(rule, params['to_ports'], '--to-ports', False) append_param(rule, params['set_dscp_mark'], '--set-dscp', False) + if params.get('set_dscp_mark') and params.get('jump').lower() != 'dscp': + append_jump(rule, params['set_dscp_mark'], 'DSCP') + append_param( rule, params['set_dscp_mark_class'], '--set-dscp-class', False) + if params.get('set_dscp_mark_class') and params.get('jump').lower() != 'dscp': + append_jump(rule, params['set_dscp_mark_class'], 'DSCP') append_match_flag(rule, params['syn'], '--syn', True) if 'conntrack' in params['match']: append_csv(rule, params['ctstate'], '--ctstate') @@ -674,6 +681,9 @@ def construct_rule(params): append_param(rule, params['gid_owner'], '--gid-owner', False) if params['jump'] is None: append_jump(rule, params['reject_with'], 'REJECT') + append_jump(rule, params['set_dscp_mark_class'], 'DSCP') + append_jump(rule, params['set_dscp_mark'], 'DSCP') + append_param(rule, params['reject_with'], '--reject-with', False) append_param( rule, @@ -811,7 +821,10 @@ def main(): src_range=dict(type='str'), dst_range=dict(type='str'), match_set=dict(type='str'), - match_set_flags=dict(type='str', choices=['src', 'dst', 'src,dst', 'dst,src']), + match_set_flags=dict( + type='str', + choices=['src', 'dst', 'src,dst', 'dst,src', 'src,src', 'dst,dst'] + ), limit=dict(type='str'), limit_burst=dict(type='str'), uid_owner=dict(type='str'), @@ -828,6 +841,10 @@ def main(): ['set_dscp_mark', 'set_dscp_mark_class'], ['flush', 'policy'], ), + required_by=dict( + set_dscp_mark=('jump',), + set_dscp_mark_class=('jump',), + ), required_if=[ ['jump', 'TEE', ['gateway']], ['jump', 'tee', ['gateway']], diff --git a/lib/ansible/modules/known_hosts.py b/lib/ansible/modules/known_hosts.py index 0c97ce2..8235258 100644 --- a/lib/ansible/modules/known_hosts.py +++ b/lib/ansible/modules/known_hosts.py @@ -2,8 +2,7 @@ # Copyright: (c) 2014, Matthew Vernon # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -274,12 +273,20 @@ def search_for_host_key(module, host, key, path, sshkeygen): module.fail_json(msg="failed to parse output of ssh-keygen for line number: '%s'" % l) else: found_key = normalize_known_hosts_key(l) - if new_key['host'][:3] == '|1|' and found_key['host'][:3] == '|1|': # do not change host hash if already hashed - new_key['host'] = found_key['host'] - if new_key == found_key: # found a match - return True, False, found_line # found exactly the same key, don't replace - elif new_key['type'] == found_key['type']: # found a different key for the same key type - return True, True, found_line + + if 'options' in found_key and found_key['options'][:15] == '@cert-authority': + if new_key == found_key: # found a match + return True, False, found_line # found exactly the same key, don't replace + elif 'options' in found_key and found_key['options'][:7] == '@revoke': + if new_key == found_key: # found a match + return True, False, found_line # found exactly the same key, don't replace + else: + if new_key['host'][:3] == '|1|' and found_key['host'][:3] == '|1|': # do not change host hash if already hashed + new_key['host'] = found_key['host'] + if new_key == found_key: # found a match + return True, False, found_line # found exactly the same key, don't replace + elif new_key['type'] == found_key['type']: # found a different key for the same key type + return True, True, found_line # No match found, return found and replace, but no line return True, True, None diff --git a/lib/ansible/modules/lineinfile.py b/lib/ansible/modules/lineinfile.py index 3d8d85d..9e9fdd9 100644 --- a/lib/ansible/modules/lineinfile.py +++ b/lib/ansible/modules/lineinfile.py @@ -5,8 +5,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -127,10 +126,6 @@ options: type: bool default: no version_added: "2.5" - others: - description: - - All arguments accepted by the M(ansible.builtin.file) module also work here. - type: str extends_documentation_fragment: - action_common_attributes - action_common_attributes.files diff --git a/lib/ansible/modules/meta.py b/lib/ansible/modules/meta.py index 78c3928..0baea37 100644 --- a/lib/ansible/modules/meta.py +++ b/lib/ansible/modules/meta.py @@ -3,8 +3,7 @@ # Copyright: (c) 2016, Ansible, a Red Hat company # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -63,6 +62,8 @@ attributes: connection: details: Most options in this action do not use a connection, except V(reset_connection) which still does not connect to the remote support: partial + until: + support: none notes: - V(clear_facts) will remove the persistent facts from M(ansible.builtin.set_fact) using O(ansible.builtin.set_fact#module:cacheable=True), but not the current host variable it creates for the current run. diff --git a/lib/ansible/modules/package.py b/lib/ansible/modules/package.py index 5541635..54d8899 100644 --- a/lib/ansible/modules/package.py +++ b/lib/ansible/modules/package.py @@ -4,8 +4,7 @@ # # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -16,7 +15,7 @@ author: - Ansible Core Team short_description: Generic OS package manager description: - - This modules manages packages on a target without specifying a package manager module (like M(ansible.builtin.yum), M(ansible.builtin.apt), ...). + - This modules manages packages on a target without specifying a package manager module (like M(ansible.builtin.dnf), M(ansible.builtin.apt), ...). It is convenient to use in an heterogeneous environment of machines without having to create a specific task for each package manager. M(ansible.builtin.package) calls behind the module for the package manager used by the operating system discovered by the module M(ansible.builtin.setup). If M(ansible.builtin.setup) was not yet run, M(ansible.builtin.package) will run it. @@ -29,7 +28,8 @@ options: description: - Package name, or package specifier with version. - Syntax varies with package manager. For example V(name-1.0) or V(name=1.0). - - Package names also vary with package manager; this module will not "translate" them per distro. For example V(libyaml-dev), V(libyaml-devel). + - Package names also vary with package manager; this module will not "translate" them per distribution. For example V(libyaml-dev), V(libyaml-devel). + - To operate on several packages this can accept a comma separated string of packages or a list of packages, depending on the underlying package manager. required: true state: description: @@ -38,8 +38,9 @@ options: required: true use: description: - - The required package manager module to use (V(yum), V(apt), and so on). The default V(auto) will use existing facts or try to autodetect it. + - The required package manager module to use (V(dnf), V(apt), and so on). The default V(auto) will use existing facts or try to auto-detect it. - You should only use this field if the automatic selection is not working for some reason. + - Since version 2.17 you can use the C(ansible_package_use) variable to override the automatic detection, but this option still takes precedence. default: auto requirements: - Whatever is required for the package plugins specific for each system. diff --git a/lib/ansible/modules/package_facts.py b/lib/ansible/modules/package_facts.py index cc6fafa..11a8f61 100644 --- a/lib/ansible/modules/package_facts.py +++ b/lib/ansible/modules/package_facts.py @@ -3,8 +3,7 @@ # most of it copied from AWX's scan_packages module -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/pause.py b/lib/ansible/modules/pause.py index 450bfaf..278e84c 100644 --- a/lib/ansible/modules/pause.py +++ b/lib/ansible/modules/pause.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -15,7 +14,7 @@ description: - To pause/wait/sleep per host, use the M(ansible.builtin.wait_for) module. - You can use C(ctrl+c) if you wish to advance a pause earlier than it is set to expire or if you need to abort a playbook run entirely. To continue early press C(ctrl+c) and then C(c). To abort a playbook press C(ctrl+c) and then C(a). - - Prompting for a set amount of time is not supported. Pausing playbook execution is interruptable but does not return user input. + - Prompting for a set amount of time is not supported. Pausing playbook execution is interruptible but does not return user input. - The pause module integrates into async/parallelized playbooks without any special considerations (see Rolling Updates). When using pauses with the C(serial) playbook parameter (as in rolling updates) you are only prompted once for the current group of hosts. - This module is also supported for Windows targets. diff --git a/lib/ansible/modules/ping.py b/lib/ansible/modules/ping.py index c724798..a29e144 100644 --- a/lib/ansible/modules/ping.py +++ b/lib/ansible/modules/ping.py @@ -4,8 +4,7 @@ # (c) 2016, Toshio Kuratomi # 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/pip.py b/lib/ansible/modules/pip.py index 3a073c8..99ac446 100644 --- a/lib/ansible/modules/pip.py +++ b/lib/ansible/modules/pip.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Matt Wright # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -111,6 +110,13 @@ options: to specify desired umask mode as an octal string, (e.g., "0022"). type: str version_added: "2.1" + break_system_packages: + description: + - Allow pip to modify an externally-managed Python installation as defined by PEP 668. + - This is typically required when installing packages outside a virtual environment on modern systems. + type: bool + default: false + version_added: "2.17" extends_documentation_fragment: - action_common_attributes attributes: @@ -122,7 +128,7 @@ attributes: platforms: posix notes: - Python installations marked externally-managed (as defined by PEP668) cannot be updated by pip versions >= 23.0.1 without the use of - a virtual environment or setting the environment variable ``PIP_BREAK_SYSTEM_PACKAGES=1``. + a virtual environment or setting the O(break_system_packages) option. - The virtualenv (U(http://www.virtualenv.org/)) must be installed on the remote host if the virtualenv parameter is specified and the virtualenv needs to be created. @@ -236,6 +242,26 @@ EXAMPLES = ''' name: bottle umask: "0022" become: True + +- name: Run a module inside a virtual environment + block: + - name: Ensure the virtual environment exists + pip: + name: psutil + virtualenv: "{{ venv_dir }}" + # On Debian-based systems the correct python*-venv package must be installed to use the `venv` module. + virtualenv_command: "{{ ansible_python_interpreter }} -m venv" + + - name: Run a module inside the virtual environment + wait_for: + port: 22 + vars: + # Alternatively, use a block to affect multiple tasks, or use set_fact to affect the remainder of the playbook. + ansible_python_interpreter: "{{ venv_python }}" + + vars: + venv_dir: /tmp/pick-a-better-venv-path + venv_python: "{{ venv_dir }}/bin/python" ''' RETURN = ''' @@ -298,7 +324,6 @@ except Exception: from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.basic import AnsibleModule, is_executable, missing_required_lib from ansible.module_utils.common.locale import get_best_parsable_locale -from ansible.module_utils.six import PY3 #: Python one-liners to be run at the command line that will determine the @@ -425,15 +450,7 @@ def _is_present(module, req, installed_pkgs, pkg_command): def _get_pip(module, env=None, executable=None): - # Older pip only installed under the "/usr/bin/pip" name. Many Linux - # distros install it there. - # By default, we try to use pip required for the current python - # interpreter, so people can use pip to install modules dependencies - candidate_pip_basenames = ('pip2', 'pip') - if PY3: - # pip under python3 installs the "/usr/bin/pip3" name - candidate_pip_basenames = ('pip3',) - + candidate_pip_basenames = ('pip3',) pip = None if executable is not None: if os.path.isabs(executable): @@ -574,13 +591,10 @@ def setup_virtualenv(module, env, chdir, out, err): if not _is_venv_command(module.params['virtualenv_command']): if virtualenv_python: cmd.append('-p%s' % virtualenv_python) - elif PY3: - # Ubuntu currently has a patch making virtualenv always - # try to use python2. Since Ubuntu16 works without - # python2 installed, this is a problem. This code mimics - # the upstream behaviour of using the python which invoked - # virtualenv to determine which python is used inside of - # the virtualenv (when none are specified). + else: + # This code mimics the upstream behaviour of using the python + # which invoked virtualenv to determine which python is used + # inside of the virtualenv (when none are specified). cmd.append('-p%s' % sys.executable) # if venv or pyvenv are used and virtualenv_python is defined, then @@ -686,6 +700,7 @@ def main(): chdir=dict(type='path'), executable=dict(type='path'), umask=dict(type='str'), + break_system_packages=dict(type='bool', default=False), ), required_one_of=[['name', 'requirements']], mutually_exclusive=[['name', 'requirements'], ['executable', 'virtualenv']], @@ -790,6 +805,11 @@ def main(): if extra_args: cmd.extend(shlex.split(extra_args)) + if module.params['break_system_packages']: + # Using an env var instead of the `--break-system-packages` option, to avoid failing under pip 23.0.0 and earlier. + # See: https://github.com/pypa/pip/pull/11780 + os.environ['PIP_BREAK_SYSTEM_PACKAGES'] = '1' + if name: cmd.extend(to_native(p) for p in packages) elif requirements: diff --git a/lib/ansible/modules/raw.py b/lib/ansible/modules/raw.py index 60840d0..75ff754 100644 --- a/lib/ansible/modules/raw.py +++ b/lib/ansible/modules/raw.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/reboot.py b/lib/ansible/modules/reboot.py index f4d029b..6d8dbd6 100644 --- a/lib/ansible/modules/reboot.py +++ b/lib/ansible/modules/reboot.py @@ -2,8 +2,7 @@ # Copyright: (c) 2018, 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/replace.py b/lib/ansible/modules/replace.py index fe4cdf0..2fee290 100644 --- a/lib/ansible/modules/replace.py +++ b/lib/ansible/modules/replace.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -75,6 +74,7 @@ options: - Uses Python regular expressions; see U(https://docs.python.org/3/library/re.html). - Uses DOTALL, which means the V(.) special character I(can match newlines). + - Does not use MULTILINE, so V(^) and V($) will only match the beginning and end of the file. type: str version_added: "2.4" before: @@ -84,6 +84,7 @@ options: - Uses Python regular expressions; see U(https://docs.python.org/3/library/re.html). - Uses DOTALL, which means the V(.) special character I(can match newlines). + - Does not use MULTILINE, so V(^) and V($) will only match the beginning and end of the file. type: str version_added: "2.4" backup: @@ -125,7 +126,7 @@ EXAMPLES = r''' regexp: '^(.+)$' replace: '# \1' -- name: Replace before the expression till the begin of the file (requires Ansible >= 2.4) +- name: Replace before the expression from the beginning of the file (requires Ansible >= 2.4) ansible.builtin.replace: path: /etc/apache2/sites-available/default.conf before: '# live site config' @@ -134,11 +135,12 @@ EXAMPLES = r''' # Prior to Ansible 2.7.10, using before and after in combination did the opposite of what was intended. # see https://github.com/ansible/ansible/issues/31354 for details. +# Note (?m) which turns on MULTILINE mode so ^ matches any line's beginning - name: Replace between the expressions (requires Ansible >= 2.4) ansible.builtin.replace: path: /etc/hosts - after: '' - before: '' + after: '(?m)^' + before: '(?m)^' regexp: '^(.+)$' replace: '# \1' diff --git a/lib/ansible/modules/rpm_key.py b/lib/ansible/modules/rpm_key.py index 9c46e43..98a1045 100644 --- a/lib/ansible/modules/rpm_key.py +++ b/lib/ansible/modules/rpm_key.py @@ -5,8 +5,7 @@ # 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/script.py b/lib/ansible/modules/script.py index c96da0f..0705c89 100644 --- a/lib/ansible/modules/script.py +++ b/lib/ansible/modules/script.py @@ -1,8 +1,7 @@ # Copyright: (c) 2012, 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/service.py b/lib/ansible/modules/service.py index b562f53..65eba3b 100644 --- a/lib/ansible/modules/service.py +++ b/lib/ansible/modules/service.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Michael DeHaan # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -180,7 +179,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.locale import get_best_parsable_locale from ansible.module_utils.common.sys_info import get_platform_subclass -from ansible.module_utils.service import fail_if_missing +from ansible.module_utils.service import fail_if_missing, is_systemd_managed from ansible.module_utils.six import PY2, b @@ -485,24 +484,7 @@ class LinuxService(Service): # tools must be installed if location.get('systemctl', False): - - # this should show if systemd is the boot init system - # these mirror systemd's own sd_boot test http://www.freedesktop.org/software/systemd/man/sd_booted.html - for canary in ["/run/systemd/system/", "/dev/.run/systemd/", "/dev/.systemd/"]: - if os.path.exists(canary): - return True - - # If all else fails, check if init is the systemd command, using comm as cmdline could be symlink - try: - f = open('/proc/1/comm', 'r') - except IOError: - # If comm doesn't exist, old kernel, no systemd - return False - - for line in f: - if 'systemd' in line: - return True - + return is_systemd_managed(self.module) return False # Locate a tool to enable/disable a service diff --git a/lib/ansible/modules/service_facts.py b/lib/ansible/modules/service_facts.py index 85d6250..d59cccf 100644 --- a/lib/ansible/modules/service_facts.py +++ b/lib/ansible/modules/service_facts.py @@ -3,8 +3,7 @@ # originally copied from AWX's scan_services module to bring this functionality # into Core -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = r''' @@ -95,6 +94,7 @@ import platform import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.locale import get_best_parsable_locale +from ansible.module_utils.service import is_systemd_managed class BaseService(object): @@ -245,16 +245,7 @@ class SystemctlScanService(BaseService): BAD_STATES = frozenset(['not-found', 'masked', 'failed']) def systemd_enabled(self): - # Check if init is the systemd command, using comm as cmdline could be symlink - try: - f = open('/proc/1/comm', 'r') - except IOError: - # If comm doesn't exist, old kernel, no systemd - return False - for line in f: - if 'systemd' in line: - return True - return False + return is_systemd_managed(self.module) def _list_from_units(self, systemctl_path, services): diff --git a/lib/ansible/modules/set_fact.py b/lib/ansible/modules/set_fact.py index 7fa0cf9..c9ab09b 100644 --- a/lib/ansible/modules/set_fact.py +++ b/lib/ansible/modules/set_fact.py @@ -3,8 +3,7 @@ # Copyright: (c) 2013, Dag Wieers (@dagwieers) # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/set_stats.py b/lib/ansible/modules/set_stats.py index 5b11c36..4526d7b 100644 --- a/lib/ansible/modules/set_stats.py +++ b/lib/ansible/modules/set_stats.py @@ -3,8 +3,7 @@ # Copyright: (c) 2016, Ansible RedHat, Inc # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/setup.py b/lib/ansible/modules/setup.py index 0615f5e..d387022 100644 --- a/lib/ansible/modules/setup.py +++ b/lib/ansible/modules/setup.py @@ -3,8 +3,7 @@ # (c) 2012, Michael DeHaan # 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 +from __future__ import annotations DOCUMENTATION = ''' diff --git a/lib/ansible/modules/shell.py b/lib/ansible/modules/shell.py index cd403b7..5cedd62 100644 --- a/lib/ansible/modules/shell.py +++ b/lib/ansible/modules/shell.py @@ -7,8 +7,7 @@ # it runs the 'command' module with special arguments and it behaves differently. # See the command source and the comment "#USE_SHELL". -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/slurp.py b/lib/ansible/modules/slurp.py index f04f3d7..e9a6547 100644 --- a/lib/ansible/modules/slurp.py +++ b/lib/ansible/modules/slurp.py @@ -3,8 +3,7 @@ # (c) 2012, Michael DeHaan # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/stat.py b/lib/ansible/modules/stat.py index ee29251..039d2b2 100644 --- a/lib/ansible/modules/stat.py +++ b/lib/ansible/modules/stat.py @@ -2,8 +2,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/subversion.py b/lib/ansible/modules/subversion.py index 847431e..ac2a17e 100644 --- a/lib/ansible/modules/subversion.py +++ b/lib/ansible/modules/subversion.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Michael DeHaan # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -142,7 +141,7 @@ from ansible.module_utils.compat.version import LooseVersion class Subversion(object): # Example text matched by the regexp: - # Révision : 1889134 + # Révision : 1889134 # 版本: 1889134 # Revision: 1889134 REVISION_RE = r'^\w+\s?:\s+\d+$' diff --git a/lib/ansible/modules/systemd.py b/lib/ansible/modules/systemd.py index 7dec044..8340de3 100644 --- a/lib/ansible/modules/systemd.py +++ b/lib/ansible/modules/systemd.py @@ -3,8 +3,7 @@ # Copyright: (c) 2016, Brian Coca # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -15,6 +14,8 @@ version_added: "2.2" short_description: Manage systemd units description: - Controls systemd units (services, timers, and so on) on remote hosts. + - M(ansible.builtin.systemd) is renamed to M(ansible.builtin.systemd_service) to better reflect the scope of the module. + M(ansible.builtin.systemd) is kept as an alias for backward compatibility. options: name: description: @@ -28,11 +29,13 @@ options: - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary. V(restarted) will always bounce the unit. V(reloaded) will always reload and if the service is not running at the moment of the reload, it is started. + - If set, requires O(name). type: str choices: [ reloaded, restarted, started, stopped ] enabled: description: - Whether the unit should start on boot. B(At least one of state and enabled are required.) + - If set, requires O(name). type: bool force: description: @@ -41,7 +44,8 @@ options: version_added: 2.6 masked: description: - - Whether the unit should be masked or not, a masked unit is impossible to start. + - Whether the unit should be masked or not. A masked unit is impossible to start. + - If set, requires O(name). type: bool daemon_reload: description: @@ -64,7 +68,7 @@ options: - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)." - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks. Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error." - - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see example below. + - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see the example below. type: str choices: [ system, user, global ] @@ -86,12 +90,11 @@ attributes: platform: platforms: posix notes: - - Since 2.4, one of the following options is required O(state), O(enabled), O(masked), O(daemon_reload), (O(daemon_reexec) since 2.8), - and all except O(daemon_reload) and (O(daemon_reexec) since 2.8) also require O(name). + - O(state), O(enabled), O(masked) requires O(name). - Before 2.4 you always required O(name). - - Globs are not supported in name, i.e C(postgres*.service). - - The service names might vary by specific OS/distribution - - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with service state. + - Globs are not supported in name, in other words, C(postgres*.service). + - The service names might vary by specific OS/distribution. + - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with the service state. It has been reported that systemctl can behave differently depending on the order of operations if you do the same manually. requirements: - A system managed by systemd. diff --git a/lib/ansible/modules/systemd_service.py b/lib/ansible/modules/systemd_service.py index 7dec044..8340de3 100644 --- a/lib/ansible/modules/systemd_service.py +++ b/lib/ansible/modules/systemd_service.py @@ -3,8 +3,7 @@ # Copyright: (c) 2016, Brian Coca # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -15,6 +14,8 @@ version_added: "2.2" short_description: Manage systemd units description: - Controls systemd units (services, timers, and so on) on remote hosts. + - M(ansible.builtin.systemd) is renamed to M(ansible.builtin.systemd_service) to better reflect the scope of the module. + M(ansible.builtin.systemd) is kept as an alias for backward compatibility. options: name: description: @@ -28,11 +29,13 @@ options: - V(started)/V(stopped) are idempotent actions that will not run commands unless necessary. V(restarted) will always bounce the unit. V(reloaded) will always reload and if the service is not running at the moment of the reload, it is started. + - If set, requires O(name). type: str choices: [ reloaded, restarted, started, stopped ] enabled: description: - Whether the unit should start on boot. B(At least one of state and enabled are required.) + - If set, requires O(name). type: bool force: description: @@ -41,7 +44,8 @@ options: version_added: 2.6 masked: description: - - Whether the unit should be masked or not, a masked unit is impossible to start. + - Whether the unit should be masked or not. A masked unit is impossible to start. + - If set, requires O(name). type: bool daemon_reload: description: @@ -64,7 +68,7 @@ options: - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)." - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks. Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error." - - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see example below. + - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see the example below. type: str choices: [ system, user, global ] @@ -86,12 +90,11 @@ attributes: platform: platforms: posix notes: - - Since 2.4, one of the following options is required O(state), O(enabled), O(masked), O(daemon_reload), (O(daemon_reexec) since 2.8), - and all except O(daemon_reload) and (O(daemon_reexec) since 2.8) also require O(name). + - O(state), O(enabled), O(masked) requires O(name). - Before 2.4 you always required O(name). - - Globs are not supported in name, i.e C(postgres*.service). - - The service names might vary by specific OS/distribution - - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with service state. + - Globs are not supported in name, in other words, C(postgres*.service). + - The service names might vary by specific OS/distribution. + - The order of execution when having multiple properties is to first enable/disable, then mask/unmask and then deal with the service state. It has been reported that systemctl can behave differently depending on the order of operations if you do the same manually. requirements: - A system managed by systemd. diff --git a/lib/ansible/modules/sysvinit.py b/lib/ansible/modules/sysvinit.py index fc934d3..cacc873 100644 --- a/lib/ansible/modules/sysvinit.py +++ b/lib/ansible/modules/sysvinit.py @@ -4,8 +4,7 @@ # (c) 2017 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -87,6 +86,12 @@ EXAMPLES = ''' state: started enabled: yes +- name: Sleep for 5 seconds between stop and start command of badly behaving service + ansible.builtin.sysvinit: + name: apache2 + state: restarted + sleep: 5 + - name: Make sure apache2 is started on runlevels 3 and 5 ansible.builtin.sysvinit: name: apache2 diff --git a/lib/ansible/modules/tempfile.py b/lib/ansible/modules/tempfile.py index c5fedab..03176a4 100644 --- a/lib/ansible/modules/tempfile.py +++ b/lib/ansible/modules/tempfile.py @@ -4,8 +4,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -68,6 +67,12 @@ EXAMPLES = """ suffix: temp register: tempfile_1 +- name: Create a temporary file with a specific prefix + ansible.builtin.tempfile: + state: file + suffix: txt + prefix: myfile_ + - name: Use the registered var and the file module to remove the temporary file ansible.builtin.file: path: "{{ tempfile_1.path }}" diff --git a/lib/ansible/modules/template.py b/lib/ansible/modules/template.py index 8f8ad0b..92c60d2 100644 --- a/lib/ansible/modules/template.py +++ b/lib/ansible/modules/template.py @@ -5,8 +5,7 @@ # This is a virtual module that is entirely implemented as an action plugin and runs on the controller -from __future__ import absolute_import, division, print_function -__metaclass__ = type +from __future__ import annotations DOCUMENTATION = r''' @@ -86,7 +85,7 @@ EXAMPLES = r''' dest: /etc/named.conf group: named setype: named_conf_t - mode: 0640 + mode: '0640' - name: Create a DOS-style text file from a template ansible.builtin.template: diff --git a/lib/ansible/modules/unarchive.py b/lib/ansible/modules/unarchive.py index b3e8058..6c51f1d 100644 --- a/lib/ansible/modules/unarchive.py +++ b/lib/ansible/modules/unarchive.py @@ -7,8 +7,7 @@ # Copyright: (c) 2017, 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -283,6 +282,7 @@ MISSING_FILE_RE = re.compile(r': Warning: Cannot stat: No such file or directory ZIP_FILE_MODE_RE = re.compile(r'([r-][w-][SsTtx-]){3}') INVALID_OWNER_RE = re.compile(r': Invalid owner') INVALID_GROUP_RE = re.compile(r': Invalid group') +SYMLINK_DIFF_RE = re.compile(r': Symlink differs$') def crc32(path, buffer_size): @@ -500,7 +500,8 @@ class ZipArchive(object): continue # Check first and seventh field in order to skip header/footer - if len(pcs[0]) != 7 and len(pcs[0]) != 10: + # 7 or 8 are FAT, 10 is normal unix perms + if len(pcs[0]) not in (7, 8, 10): continue if len(pcs[6]) != 15: continue @@ -552,6 +553,12 @@ class ZipArchive(object): else: permstr = 'rw-rw-rw-' file_umask = umask + elif len(permstr) == 7: + if permstr == 'rwxa---': + permstr = 'rwxrwxrwx' + else: + permstr = 'rw-rw-rw-' + file_umask = umask elif 'bsd' in systemtype.lower(): file_umask = umask else: @@ -880,6 +887,8 @@ class TgzArchive(object): out += line + '\n' if INVALID_GROUP_RE.search(line): out += line + '\n' + if SYMLINK_DIFF_RE.search(line): + out += line + '\n' if out: unarchived = False return dict(unarchived=unarchived, rc=rc, out=out, err=err, cmd=cmd) @@ -969,6 +978,7 @@ class TarZstdArchive(TgzArchive): class ZipZArchive(ZipArchive): def __init__(self, src, b_dest, file_args, module): super(ZipZArchive, self).__init__(src, b_dest, file_args, module) + # NOTE: adds 'l', which is default on most linux but not all implementations self.zipinfoflag = '-Zl' self.binaries = ( ('unzip', 'cmd_path'), diff --git a/lib/ansible/modules/uri.py b/lib/ansible/modules/uri.py index 0aac978..7c2e924 100644 --- a/lib/ansible/modules/uri.py +++ b/lib/ansible/modules/uri.py @@ -3,8 +3,7 @@ # Copyright: (c) 2013, Romeo Theriault # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -108,14 +107,15 @@ options: default: no follow_redirects: description: - - Whether or not the URI module should follow redirects. V(all) will follow all redirects. - V(safe) will follow only "safe" redirects, where "safe" means that the client is only - doing a GET or HEAD on the URI to which it is being redirected. V(none) will not follow - any redirects. Note that V(true) and V(false) choices are accepted for backwards compatibility, - where V(true) is the equivalent of V(all) and V(false) is the equivalent of V(safe). V(true) and V(false) - are deprecated and will be removed in some future version of Ansible. + - Whether or not the URI module should follow redirects. + choices: + all: Will follow all redirects. + none: Will not follow any redirects. + safe: Only redirects doing GET or HEAD requests will be followed. + urllib2: Defer to urllib2 behavior (As of writing this follows HTTP redirects). + 'no': (DEPRECATED, will be removed in the future version) alias of V(none). + 'yes': (DEPRECATED, will be removed in the future version) alias of V(all). type: str - choices: ['all', 'no', 'none', 'safe', 'urllib2', 'yes'] default: safe creates: description: @@ -444,11 +444,10 @@ import json import os import re import shutil -import sys import tempfile from ansible.module_utils.basic import AnsibleModule, sanitize_keys -from ansible.module_utils.six import PY2, PY3, binary_type, iteritems, string_types +from ansible.module_utils.six import binary_type, iteritems, string_types from ansible.module_utils.six.moves.urllib.parse import urlencode, urlsplit from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.compat.datetime import utcnow, utcfromtimestamp @@ -586,7 +585,7 @@ def uri(module, url, dest, body, body_format, method, headers, socket_timeout, c method=method, timeout=socket_timeout, unix_socket=module.params['unix_socket'], ca_path=ca_path, unredirected_headers=unredirected_headers, use_proxy=module.params['use_proxy'], decompress=decompress, - ciphers=ciphers, use_netrc=use_netrc, **kwargs) + ciphers=ciphers, use_netrc=use_netrc, force=module.params['force'], **kwargs) if src: # Try to close the open file handle @@ -720,7 +719,7 @@ def main(): if maybe_output: try: - if PY3 and (r.fp is None or r.closed): + if r.fp is None or r.closed: raise TypeError content = r.read() except (AttributeError, TypeError): @@ -771,8 +770,7 @@ def main(): js = json.loads(u_content) uresp['json'] = js except Exception: - if PY2: - sys.exc_clear() # Avoid false positive traceback in fail_json() on Python 2 + ... else: u_content = None diff --git a/lib/ansible/modules/user.py b/lib/ansible/modules/user.py index 6d465b0..e896581 100644 --- a/lib/ansible/modules/user.py +++ b/lib/ansible/modules/user.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Stephen Fromm # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -73,8 +72,8 @@ options: - Optionally set the user's shell. - On macOS, before Ansible 2.5, the default shell for non-system users was V(/usr/bin/false). Since Ansible 2.5, the default shell for non-system users on macOS is V(/bin/bash). - - See notes for details on how other operating systems determine the default shell by - the underlying tool. + - On other operating systems, the default shell is determined by the underlying tool + invoked by this module. See Notes for a per platform list of invoked tools. type: str home: description: @@ -306,6 +305,11 @@ EXAMPLES = r''' uid: 1040 group: admin +- name: Create a user 'johnd' with a home directory + ansible.builtin.user: + name: johnd + create_home: yes + - name: Add the user 'james' with a bash shell, appending the group 'admins' and 'developers' to the user's groups ansible.builtin.user: name: james @@ -632,6 +636,9 @@ class User(object): # sha512 if fields[1] == '6' and len(fields[-1]) != 86: maybe_invalid = True + # yescrypt + if fields[1] == 'y' and len(fields[-1]) != 43: + maybe_invalid = True else: maybe_invalid = True if maybe_invalid: @@ -1063,12 +1070,6 @@ class User(object): exists = True break - if not exists: - self.module.warn( - "'local: true' specified and user '{name}' was not found in {file}. " - "The local user account may already exist if the local account database exists " - "somewhere other than {file}.".format(file=self.PASSWORDFILE, name=self.name)) - return exists else: diff --git a/lib/ansible/modules/validate_argument_spec.py b/lib/ansible/modules/validate_argument_spec.py index 0186c0a..37a40d1 100644 --- a/lib/ansible/modules/validate_argument_spec.py +++ b/lib/ansible/modules/validate_argument_spec.py @@ -3,8 +3,7 @@ # Copyright 2021 Red Hat # 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 +from __future__ import annotations DOCUMENTATION = r''' @@ -52,32 +51,32 @@ attributes: EXAMPLES = r''' - name: verify vars needed for this task file are present when included ansible.builtin.validate_argument_spec: - argument_spec: '{{required_data}}' + argument_spec: '{{ required_data }}' vars: required_data: - # unlike spec file, just put the options in directly - stuff: - description: stuff - type: str - choices: ['who', 'knows', 'what'] - default: what - but: - description: i guess we need one - type: str - required: true + # unlike spec file, just put the options in directly + stuff: + description: stuff + type: str + choices: ['who', 'knows', 'what'] + default: what + but: + description: i guess we need one + type: str + required: true - name: verify vars needed for this task file are present when included, with spec from a spec file ansible.builtin.validate_argument_spec: - argument_spec: "{{(lookup('ansible.builtin.file', 'myargspec.yml') | from_yaml )['specname']['options']}}" + argument_spec: "{{ (lookup('ansible.builtin.file', 'myargspec.yml') | from_yaml )['specname']['options'] }}" - name: verify vars needed for next include and not from inside it, also with params i'll only define there block: - ansible.builtin.validate_argument_spec: - argument_spec: "{{lookup('ansible.builtin.file', 'nakedoptions.yml'}}" + argument_spec: "{{ lookup('ansible.builtin.file', 'nakedoptions.yml') }}" provided_arguments: - but: "that i can define on the include itself, like in it's `vars:` keyword" + but: "that i can define on the include itself, like in it's `vars:` keyword" - name: the include itself vars: diff --git a/lib/ansible/modules/wait_for.py b/lib/ansible/modules/wait_for.py index 1b56e18..2230e6e 100644 --- a/lib/ansible/modules/wait_for.py +++ b/lib/ansible/modules/wait_for.py @@ -3,8 +3,7 @@ # Copyright: (c) 2012, Jeroen Hoekx # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/wait_for_connection.py b/lib/ansible/modules/wait_for_connection.py index f104722..45be7be 100644 --- a/lib/ansible/modules/wait_for_connection.py +++ b/lib/ansible/modules/wait_for_connection.py @@ -3,8 +3,7 @@ # Copyright: (c) 2017, Dag Wieers (@dagwieers) # 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 +from __future__ import annotations DOCUMENTATION = r''' diff --git a/lib/ansible/modules/yum.py b/lib/ansible/modules/yum.py deleted file mode 100644 index 3b6a457..0000000 --- a/lib/ansible/modules/yum.py +++ /dev/null @@ -1,1821 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2012, Red Hat, Inc -# Written by Seth Vidal -# Copyright: (c) 2014, Epic Games, Inc. - -# 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: yum -version_added: historical -short_description: Manages packages with the I(yum) package manager -description: - - Installs, upgrade, downgrades, removes, and lists packages and groups with the I(yum) package manager. - - This module only works on Python 2. If you require Python 3 support see the M(ansible.builtin.dnf) module. -options: - use_backend: - description: - - This module supports V(yum) (as it always has), this is known as C(yum3)/C(YUM3)/C(yum-deprecated) by - upstream yum developers. As of Ansible 2.7+, this module also supports C(YUM4), which is the - "new yum" and it has an V(dnf) backend. As of ansible-core 2.15+, this module will auto select the backend - based on the C(ansible_pkg_mgr) fact. - - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact. - default: "auto" - choices: [ auto, yum, yum4, dnf, dnf4, dnf5 ] - type: str - version_added: "2.7" - name: - description: - - A package name or package specifier with version, like V(name-1.0). - - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - V(name>=1.0) - - If a previous version is specified, the task also needs to turn O(allow_downgrade) on. - See the O(allow_downgrade) documentation for caveats with downgrading packages. - - When using O(state=latest), this can be V('*') which means run C(yum -y update). - - You can also pass a url or a local path to an rpm file (using O(state=present)). - To operate on several packages this can accept a comma separated string of packages or (as of 2.0) a list of packages. - aliases: [ pkg ] - type: list - elements: str - default: [] - exclude: - description: - - Package name(s) to exclude when state=present, or latest - type: list - elements: str - default: [] - version_added: "2.0" - list: - description: - - "Package name to run the equivalent of C(yum list --show-duplicates ) against. In addition to listing packages, - use can also list the following: V(installed), V(updates), V(available) and V(repos)." - - This parameter is mutually exclusive with O(name). - type: str - state: - description: - - Whether to install (V(present) or V(installed), V(latest)), or remove (V(absent) or V(removed)) a package. - - V(present) and V(installed) will simply ensure that a desired package is installed. - - V(latest) will update the specified package if it's not of the latest available version. - - V(absent) and V(removed) will remove the specified package. - - Default is V(None), however in effect the default action is V(present) unless the O(autoremove) option is - enabled for this module, then V(absent) is inferred. - type: str - choices: [ absent, installed, latest, present, removed ] - enablerepo: - description: - - I(Repoid) of repositories to enable for the install/update operation. - These repos will not persist beyond the transaction. - When specifying multiple repos, separate them with a C(","). - - As of Ansible 2.7, this can alternatively be a list instead of C(",") - separated string - type: list - elements: str - default: [] - version_added: "0.9" - disablerepo: - description: - - I(Repoid) of repositories to disable for the install/update operation. - These repos will not persist beyond the transaction. - When specifying multiple repos, separate them with a C(","). - - As of Ansible 2.7, this can alternatively be a list instead of C(",") - separated string - type: list - elements: str - default: [] - version_added: "0.9" - conf_file: - description: - - The remote yum configuration file to use for the transaction. - type: str - version_added: "0.6" - disable_gpg_check: - description: - - Whether to disable the GPG checking of signatures of packages being - installed. Has an effect only if O(state) is V(present) or V(latest). - type: bool - default: "no" - version_added: "1.2" - skip_broken: - description: - - Skip all unavailable packages or packages with broken dependencies - without raising an error. Equivalent to passing the --skip-broken option. - type: bool - default: "no" - version_added: "2.3" - update_cache: - description: - - Force yum to check if cache is out of date and redownload if needed. - Has an effect only if O(state) is V(present) or V(latest). - type: bool - default: "no" - aliases: [ expire-cache ] - version_added: "1.9" - validate_certs: - description: - - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to V(false), the SSL certificates will not be validated. - - This should only set to V(false) used on personally controlled sites using self-signed certificates as it avoids verifying the source site. - - Prior to 2.1 the code worked as if this was set to V(true). - type: bool - default: "yes" - version_added: "2.1" - sslverify: - description: - - Disables SSL validation of the repository server for this transaction. - - This should be set to V(false) if one of the configured repositories is using an untrusted or self-signed certificate. - type: bool - default: "yes" - version_added: "2.13" - update_only: - description: - - When using latest, only update installed packages. Do not install packages. - - Has an effect only if O(state) is V(latest) - default: "no" - type: bool - version_added: "2.5" - - installroot: - description: - - Specifies an alternative installroot, relative to which all packages - will be installed. - default: "/" - type: str - version_added: "2.3" - security: - description: - - If set to V(true), and O(state=latest) then only installs updates that have been marked security related. - type: bool - default: "no" - version_added: "2.4" - bugfix: - description: - - If set to V(true), and O(state=latest) then only installs updates that have been marked bugfix related. - default: "no" - type: bool - version_added: "2.6" - allow_downgrade: - description: - - Specify if the named package and version is allowed to downgrade - a maybe already installed higher version of that package. - Note that setting allow_downgrade=True can make this module - behave in a non-idempotent way. The task could end up with a set - of packages that does not match the complete list of specified - packages to install (because dependencies between the downgraded - package and others can cause changes to the packages which were - in the earlier transaction). - type: bool - default: "no" - version_added: "2.4" - enable_plugin: - description: - - I(Plugin) name to enable for the install/update operation. - The enabled plugin will not persist beyond the transaction. - type: list - elements: str - default: [] - version_added: "2.5" - disable_plugin: - description: - - I(Plugin) name to disable for the install/update operation. - The disabled plugins will not persist beyond the transaction. - type: list - elements: str - default: [] - version_added: "2.5" - releasever: - description: - - Specifies an alternative release from which all packages will be - installed. - type: str - version_added: "2.7" - autoremove: - description: - - If V(true), removes all "leaf" packages from the system that were originally - installed as dependencies of user-installed packages but which are no longer - required by any such package. Should be used alone or when O(state) is V(absent) - - "NOTE: This feature requires yum >= 3.4.3 (RHEL/CentOS 7+)" - type: bool - default: "no" - version_added: "2.7" - disable_excludes: - description: - - Disable the excludes defined in YUM config files. - - If set to V(all), disables all excludes. - - If set to V(main), disable excludes defined in [main] in yum.conf. - - If set to V(repoid), disable excludes defined for given repo id. - type: str - version_added: "2.7" - download_only: - description: - - Only download the packages, do not install them. - default: "no" - type: bool - version_added: "2.7" - lock_timeout: - description: - - Amount of time to wait for the yum lockfile to be freed. - required: false - default: 30 - type: int - version_added: "2.8" - install_weak_deps: - description: - - Will also install all packages linked by a weak dependency relation. - - "NOTE: This feature requires yum >= 4 (RHEL/CentOS 8+)" - type: bool - default: "yes" - version_added: "2.8" - download_dir: - description: - - Specifies an alternate directory to store packages. - - Has an effect only if O(download_only) is specified. - type: str - version_added: "2.8" - install_repoquery: - description: - - If repoquery is not available, install yum-utils. If the system is - registered to RHN or an RHN Satellite, repoquery allows for querying - all channels assigned to the system. It is also required to use the - 'list' parameter. - - "NOTE: This will run and be logged as a separate yum transation which - takes place before any other installation or removal." - - "NOTE: This will use the system's default enabled repositories without - regard for disablerepo/enablerepo given to the module." - required: false - version_added: "1.5" - default: "yes" - type: bool - cacheonly: - description: - - Tells yum to run entirely from system cache; does not download or update metadata. - default: "no" - type: bool - version_added: "2.12" -extends_documentation_fragment: -- action_common_attributes -- action_common_attributes.flow -attributes: - action: - details: In the case of yum, it has 2 action plugins that use it under the hood, M(ansible.builtin.yum) and M(ansible.builtin.package). - support: partial - async: - support: none - bypass_host_loop: - support: none - check_mode: - support: full - diff_mode: - support: full - platform: - platforms: rhel -notes: - - When used with a C(loop:) each package will be processed individually, - it is much more efficient to pass the list directly to the O(name) option. - - In versions prior to 1.9.2 this module installed and removed each package - given to the yum module separately. This caused problems when packages - specified by filename or url had to be installed or removed together. In - 1.9.2 this was fixed so that packages are installed in one yum - transaction. However, if one of the packages adds a new yum repository - that the other packages come from (such as epel-release) then that package - needs to be installed in a separate task. This mimics yum's command line - behaviour. - - 'Yum itself has two types of groups. "Package groups" are specified in the - rpm itself while "environment groups" are specified in a separate file - (usually by the distribution). Unfortunately, this division becomes - apparent to ansible users because ansible needs to operate on the group - of packages in a single transaction and yum requires groups to be specified - in different ways when used in that way. Package groups are specified as - "@development-tools" and environment groups are "@^gnome-desktop-environment". - Use the "yum group list hidden ids" command to see which category of group the group - you want to install falls into.' - - 'The yum module does not support clearing yum cache in an idempotent way, so it - was decided not to implement it, the only method is to use command and call the yum - command directly, namely "command: yum clean all" - https://github.com/ansible/ansible/pull/31450#issuecomment-352889579' -# informational: requirements for nodes -requirements: -- yum -author: - - Ansible Core Team - - Seth Vidal (@skvidal) - - Eduard Snesarev (@verm666) - - Berend De Schouwer (@berenddeschouwer) - - Abhijeet Kasurde (@Akasurde) - - Adam Miller (@maxamillion) -''' - -EXAMPLES = ''' -- name: Install the latest version of Apache - ansible.builtin.yum: - name: httpd - state: latest - -- name: Install Apache >= 2.4 - ansible.builtin.yum: - name: httpd>=2.4 - state: present - -- name: Install a list of packages (suitable replacement for 2.11 loop deprecation warning) - ansible.builtin.yum: - name: - - nginx - - postgresql - - postgresql-server - state: present - -- name: Install a list of packages with a list variable - ansible.builtin.yum: - name: "{{ packages }}" - vars: - packages: - - httpd - - httpd-tools - -- name: Remove the Apache package - ansible.builtin.yum: - name: httpd - state: absent - -- name: Install the latest version of Apache from the testing repo - ansible.builtin.yum: - name: httpd - enablerepo: testing - state: present - -- name: Install one specific version of Apache - ansible.builtin.yum: - name: httpd-2.2.29-1.4.amzn1 - state: present - -- name: Upgrade all packages - ansible.builtin.yum: - name: '*' - state: latest - -- name: Upgrade all packages, excluding kernel & foo related packages - ansible.builtin.yum: - name: '*' - state: latest - exclude: kernel*,foo* - -- name: Install the nginx rpm from a remote repo - ansible.builtin.yum: - name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm - state: present - -- name: Install nginx rpm from a local file - ansible.builtin.yum: - name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm - state: present - -- name: Install the 'Development tools' package group - ansible.builtin.yum: - name: "@Development tools" - state: present - -- name: Install the 'Gnome desktop' environment group - ansible.builtin.yum: - name: "@^gnome-desktop-environment" - state: present - -- name: List ansible packages and register result to print with debug later - ansible.builtin.yum: - list: ansible - register: result - -- name: Install package with multiple repos enabled - ansible.builtin.yum: - name: sos - enablerepo: "epel,ol7_latest" - -- name: Install package with multiple repos disabled - ansible.builtin.yum: - name: sos - disablerepo: "epel,ol7_latest" - -- name: Download the nginx package but do not install it - ansible.builtin.yum: - name: - - nginx - state: latest - download_only: true -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.common.locale import get_best_parsable_locale -from ansible.module_utils.common.respawn import has_respawned, respawn_module -from ansible.module_utils.common.text.converters import to_native, to_text -from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec - -import errno -import os -import re -import sys -import tempfile - -try: - import rpm - HAS_RPM_PYTHON = True -except ImportError: - HAS_RPM_PYTHON = False - -try: - import yum - HAS_YUM_PYTHON = True -except ImportError: - HAS_YUM_PYTHON = False - -try: - from yum.misc import find_unfinished_transactions, find_ts_remaining - from rpmUtils.miscutils import splitFilename, compareEVR - transaction_helpers = True -except ImportError: - transaction_helpers = False - -from contextlib import contextmanager -from ansible.module_utils.urls import fetch_file - -def_qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}" -rpmbin = None - - -class YumModule(YumDnf): - """ - Yum Ansible module back-end implementation - """ - - def __init__(self, module): - - # state=installed name=pkgspec - # state=removed name=pkgspec - # state=latest name=pkgspec - # - # informational commands: - # list=installed - # list=updates - # list=available - # list=repos - # list=pkgspec - - # This populates instance vars for all argument spec params - super(YumModule, self).__init__(module) - - self.pkg_mgr_name = "yum" - self.lockfile = '/var/run/yum.pid' - self._yum_base = None - - def _enablerepos_with_error_checking(self): - # NOTE: This seems unintuitive, but it mirrors yum's CLI behavior - if len(self.enablerepo) == 1: - try: - self.yum_base.repos.enableRepo(self.enablerepo[0]) - except yum.Errors.YumBaseError as e: - if u'repository not found' in to_text(e): - self.module.fail_json(msg="Repository %s not found." % self.enablerepo[0]) - else: - raise e - else: - for rid in self.enablerepo: - try: - self.yum_base.repos.enableRepo(rid) - except yum.Errors.YumBaseError as e: - if u'repository not found' in to_text(e): - self.module.warn("Repository %s not found." % rid) - else: - raise e - - def is_lockfile_pid_valid(self): - try: - try: - with open(self.lockfile, 'r') as f: - oldpid = int(f.readline()) - except ValueError: - # invalid data - os.unlink(self.lockfile) - return False - - if oldpid == os.getpid(): - # that's us? - os.unlink(self.lockfile) - return False - - try: - with open("/proc/%d/stat" % oldpid, 'r') as f: - stat = f.readline() - - if stat.split()[2] == 'Z': - # Zombie - os.unlink(self.lockfile) - return False - except IOError: - # either /proc is not mounted or the process is already dead - try: - # check the state of the process - os.kill(oldpid, 0) - except OSError as e: - if e.errno == errno.ESRCH: - # No such process - os.unlink(self.lockfile) - return False - - self.module.fail_json(msg="Unable to check PID %s in %s: %s" % (oldpid, self.lockfile, to_native(e))) - except (IOError, OSError) as e: - # lockfile disappeared? - return False - - # another copy seems to be running - return True - - @property - def yum_base(self): - if self._yum_base: - return self._yum_base - else: - # Only init once - self._yum_base = yum.YumBase() - self._yum_base.preconf.debuglevel = 0 - self._yum_base.preconf.errorlevel = 0 - self._yum_base.preconf.plugins = True - self._yum_base.preconf.enabled_plugins = self.enable_plugin - self._yum_base.preconf.disabled_plugins = self.disable_plugin - if self.releasever: - self._yum_base.preconf.releasever = self.releasever - if self.installroot != '/': - # do not setup installroot by default, because of error - # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf - # in old yum version (like in CentOS 6.6) - self._yum_base.preconf.root = self.installroot - self._yum_base.conf.installroot = self.installroot - if self.conf_file and os.path.exists(self.conf_file): - self._yum_base.preconf.fn = self.conf_file - if os.geteuid() != 0: - if hasattr(self._yum_base, 'setCacheDir'): - self._yum_base.setCacheDir() - else: - cachedir = yum.misc.getCacheDir() - self._yum_base.repos.setCacheDir(cachedir) - self._yum_base.conf.cache = 0 - if self.disable_excludes: - self._yum_base.conf.disable_excludes = self.disable_excludes - - # setting conf.sslverify allows retrieving the repo's metadata - # without validating the certificate, but that does not allow - # package installation from a bad-ssl repo. - self._yum_base.conf.sslverify = self.sslverify - - # A sideeffect of accessing conf is that the configuration is - # loaded and plugins are discovered - self.yum_base.conf # pylint: disable=pointless-statement - - try: - for rid in self.disablerepo: - self.yum_base.repos.disableRepo(rid) - - self._enablerepos_with_error_checking() - - except Exception as e: - self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return self._yum_base - - def po_to_envra(self, po): - if hasattr(po, 'ui_envra'): - return po.ui_envra - - return '%s:%s-%s-%s.%s' % (po.epoch, po.name, po.version, po.release, po.arch) - - def is_group_env_installed(self, name): - name_lower = name.lower() - - if yum.__version_info__ >= (3, 4): - groups_list = self.yum_base.doGroupLists(return_evgrps=True) - else: - groups_list = self.yum_base.doGroupLists() - - # list of the installed groups on the first index - groups = groups_list[0] - for group in groups: - if name_lower.endswith(group.name.lower()) or name_lower.endswith(group.groupid.lower()): - return True - - if yum.__version_info__ >= (3, 4): - # list of the installed env_groups on the third index - envs = groups_list[2] - for env in envs: - if name_lower.endswith(env.name.lower()) or name_lower.endswith(env.environmentid.lower()): - return True - - return False - - def is_installed(self, repoq, pkgspec, qf=None, is_pkg=False): - if qf is None: - qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}\n" - - if not repoq: - pkgs = [] - try: - e, m, dummy = self.yum_base.rpmdb.matchPackageNames([pkgspec]) - pkgs = e + m - if not pkgs and not is_pkg: - pkgs.extend(self.yum_base.returnInstalledPackagesByDep(pkgspec)) - except Exception as e: - self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return [self.po_to_envra(p) for p in pkgs] - - else: - global rpmbin - if not rpmbin: - rpmbin = self.module.get_bin_path('rpm', required=True) - - cmd = [rpmbin, '-q', '--qf', qf, pkgspec] - if '*' in pkgspec: - cmd.append('-a') - if self.installroot != '/': - cmd.extend(['--root', self.installroot]) - # rpm localizes messages and we're screen scraping so make sure we use - # an appropriate locale - locale = get_best_parsable_locale(self.module) - lang_env = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale) - rc, out, err = self.module.run_command(cmd, environ_update=lang_env) - if rc != 0 and 'is not installed' not in out: - self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err)) - if 'is not installed' in out: - out = '' - - pkgs = [p for p in out.replace('(none)', '0').split('\n') if p.strip()] - if not pkgs and not is_pkg: - cmd = [rpmbin, '-q', '--qf', qf, '--whatprovides', pkgspec] - if self.installroot != '/': - cmd.extend(['--root', self.installroot]) - rc2, out2, err2 = self.module.run_command(cmd, environ_update=lang_env) - else: - rc2, out2, err2 = (0, '', '') - - if rc2 != 0 and 'no package provides' not in out2: - self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err + err2)) - if 'no package provides' in out2: - out2 = '' - pkgs += [p for p in out2.replace('(none)', '0').split('\n') if p.strip()] - return pkgs - - return [] - - def is_available(self, repoq, pkgspec, qf=def_qf): - if not repoq: - - pkgs = [] - try: - e, m, dummy = self.yum_base.pkgSack.matchPackageNames([pkgspec]) - pkgs = e + m - if not pkgs: - pkgs.extend(self.yum_base.returnPackagesByDep(pkgspec)) - except Exception as e: - self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return [self.po_to_envra(p) for p in pkgs] - - else: - myrepoq = list(repoq) - - r_cmd = ['--disablerepo', ','.join(self.disablerepo)] - myrepoq.extend(r_cmd) - - r_cmd = ['--enablerepo', ','.join(self.enablerepo)] - myrepoq.extend(r_cmd) - - if self.releasever: - myrepoq.extend('--releasever=%s' % self.releasever) - - cmd = myrepoq + ["--qf", qf, pkgspec] - rc, out, err = self.module.run_command(cmd) - if rc == 0: - return [p for p in out.split('\n') if p.strip()] - else: - self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - return [] - - def is_update(self, repoq, pkgspec, qf=def_qf): - if not repoq: - - pkgs = [] - updates = [] - - try: - pkgs = self.yum_base.returnPackagesByDep(pkgspec) + \ - self.yum_base.returnInstalledPackagesByDep(pkgspec) - if not pkgs: - e, m, dummy = self.yum_base.pkgSack.matchPackageNames([pkgspec]) - pkgs = e + m - updates = self.yum_base.doPackageLists(pkgnarrow='updates').updates - except Exception as e: - self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - retpkgs = (pkg for pkg in pkgs if pkg in updates) - - return set(self.po_to_envra(p) for p in retpkgs) - - else: - myrepoq = list(repoq) - r_cmd = ['--disablerepo', ','.join(self.disablerepo)] - myrepoq.extend(r_cmd) - - r_cmd = ['--enablerepo', ','.join(self.enablerepo)] - myrepoq.extend(r_cmd) - - if self.releasever: - myrepoq.extend('--releasever=%s' % self.releasever) - - cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec] - rc, out, err = self.module.run_command(cmd) - - if rc == 0: - return set(p for p in out.split('\n') if p.strip()) - else: - self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) - - return set() - - def what_provides(self, repoq, req_spec, qf=def_qf): - if not repoq: - - pkgs = [] - try: - try: - pkgs = self.yum_base.returnPackagesByDep(req_spec) + \ - self.yum_base.returnInstalledPackagesByDep(req_spec) - except Exception as e: - # If a repo with `repo_gpgcheck=1` is added and the repo GPG - # key was never accepted, querying this repo will throw an - # error: 'repomd.xml signature could not be verified'. In that - # situation we need to run `yum -y makecache fast` which will accept - # the key and try again. - if 'repomd.xml signature could not be verified' in to_native(e): - if self.releasever: - self.module.run_command(self.yum_basecmd + ['makecache', 'fast', '--releasever=%s' % self.releasever]) - else: - self.module.run_command(self.yum_basecmd + ['makecache', 'fast']) - pkgs = self.yum_base.returnPackagesByDep(req_spec) + \ - self.yum_base.returnInstalledPackagesByDep(req_spec) - else: - raise - if not pkgs: - exact_matches, glob_matches = self.yum_base.pkgSack.matchPackageNames([req_spec])[0:2] - pkgs.extend(exact_matches) - pkgs.extend(glob_matches) - exact_matches, glob_matches = self.yum_base.rpmdb.matchPackageNames([req_spec])[0:2] - pkgs.extend(exact_matches) - pkgs.extend(glob_matches) - except Exception as e: - self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e)) - - return set(self.po_to_envra(p) for p in pkgs) - - else: - myrepoq = list(repoq) - r_cmd = ['--disablerepo', ','.join(self.disablerepo)] - myrepoq.extend(r_cmd) - - r_cmd = ['--enablerepo', ','.join(self.enablerepo)] - myrepoq.extend(r_cmd) - - if self.releasever: - myrepoq.extend('--releasever=%s' % self.releasever) - - cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec] - rc, out, err = self.module.run_command(cmd) - cmd = myrepoq + ["--qf", qf, req_spec] - rc2, out2, err2 = self.module.run_command(cmd) - if rc == 0 and rc2 == 0: - out += out2 - pkgs = {p for p in out.split('\n') if p.strip()} - if not pkgs: - pkgs = self.is_installed(repoq, req_spec, qf=qf) - return pkgs - else: - self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) - - return set() - - def transaction_exists(self, pkglist): - """ - checks the package list to see if any packages are - involved in an incomplete transaction - """ - - conflicts = [] - if not transaction_helpers: - return conflicts - - # first, we create a list of the package 'nvreas' - # so we can compare the pieces later more easily - pkglist_nvreas = (splitFilename(pkg) for pkg in pkglist) - - # next, we build the list of packages that are - # contained within an unfinished transaction - unfinished_transactions = find_unfinished_transactions() - for trans in unfinished_transactions: - steps = find_ts_remaining(trans) - for step in steps: - # the action is install/erase/etc., but we only - # care about the package spec contained in the step - (action, step_spec) = step - (n, v, r, e, a) = splitFilename(step_spec) - # and see if that spec is in the list of packages - # requested for installation/updating - for pkg in pkglist_nvreas: - # if the name and arch match, we're going to assume - # this package is part of a pending transaction - # the label is just for display purposes - label = "%s-%s" % (n, a) - if n == pkg[0] and a == pkg[4]: - if label not in conflicts: - conflicts.append("%s-%s" % (n, a)) - break - return conflicts - - def local_envra(self, path): - """return envra of a local rpm passed in""" - - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - fd = os.open(path, os.O_RDONLY) - try: - header = ts.hdrFromFdno(fd) - except rpm.error as e: - return None - finally: - os.close(fd) - - return '%s:%s-%s-%s.%s' % ( - header[rpm.RPMTAG_EPOCH] or '0', - header[rpm.RPMTAG_NAME], - header[rpm.RPMTAG_VERSION], - header[rpm.RPMTAG_RELEASE], - header[rpm.RPMTAG_ARCH] - ) - - @contextmanager - def set_env_proxy(self): - # setting system proxy environment and saving old, if exists - namepass = "" - scheme = ["http", "https"] - old_proxy_env = [os.getenv("http_proxy"), os.getenv("https_proxy")] - try: - # "_none_" is a special value to disable proxy in yum.conf/*.repo - if self.yum_base.conf.proxy and self.yum_base.conf.proxy not in ("_none_",): - if self.yum_base.conf.proxy_username: - namepass = namepass + self.yum_base.conf.proxy_username - proxy_url = self.yum_base.conf.proxy - if self.yum_base.conf.proxy_password: - namepass = namepass + ":" + self.yum_base.conf.proxy_password - elif '@' in self.yum_base.conf.proxy: - namepass = self.yum_base.conf.proxy.split('@')[0].split('//')[-1] - proxy_url = self.yum_base.conf.proxy.replace("{0}@".format(namepass), "") - - if namepass: - namepass = namepass + '@' - for item in scheme: - os.environ[item + "_proxy"] = re.sub( - r"(http://)", - r"\g<1>" + namepass, proxy_url - ) - else: - for item in scheme: - os.environ[item + "_proxy"] = self.yum_base.conf.proxy - yield - except yum.Errors.YumBaseError: - raise - finally: - # revert back to previously system configuration - for item in scheme: - if os.getenv("{0}_proxy".format(item)): - del os.environ["{0}_proxy".format(item)] - if old_proxy_env[0]: - os.environ["http_proxy"] = old_proxy_env[0] - if old_proxy_env[1]: - os.environ["https_proxy"] = old_proxy_env[1] - - def pkg_to_dict(self, pkgstr): - if pkgstr.strip() and pkgstr.count('|') == 5: - n, e, v, r, a, repo = pkgstr.split('|') - else: - return {'error_parsing': pkgstr} - - d = { - 'name': n, - 'arch': a, - 'epoch': e, - 'release': r, - 'version': v, - 'repo': repo, - 'envra': '%s:%s-%s-%s.%s' % (e, n, v, r, a) - } - - if repo == 'installed': - d['yumstate'] = 'installed' - else: - d['yumstate'] = 'available' - - return d - - def repolist(self, repoq, qf="%{repoid}"): - cmd = repoq + ["--qf", qf, "-a"] - if self.releasever: - cmd.extend(['--releasever=%s' % self.releasever]) - rc, out, err = self.module.run_command(cmd) - if rc == 0: - return set(p for p in out.split('\n') if p.strip()) - else: - return [] - - def list_stuff(self, repoquerybin, stuff): - - qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" - # is_installed goes through rpm instead of repoquery so it needs a slightly different format - is_installed_qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|installed\n" - repoq = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] - if self.disablerepo: - repoq.extend(['--disablerepo', ','.join(self.disablerepo)]) - if self.enablerepo: - repoq.extend(['--enablerepo', ','.join(self.enablerepo)]) - if self.installroot != '/': - repoq.extend(['--installroot', self.installroot]) - if self.conf_file and os.path.exists(self.conf_file): - repoq += ['-c', self.conf_file] - - if stuff == 'installed': - return [self.pkg_to_dict(p) for p in sorted(self.is_installed(repoq, '-a', qf=is_installed_qf)) if p.strip()] - - if stuff == 'updates': - return [self.pkg_to_dict(p) for p in sorted(self.is_update(repoq, '-a', qf=qf)) if p.strip()] - - if stuff == 'available': - return [self.pkg_to_dict(p) for p in sorted(self.is_available(repoq, '-a', qf=qf)) if p.strip()] - - if stuff == 'repos': - return [dict(repoid=name, state='enabled') for name in sorted(self.repolist(repoq)) if name.strip()] - - return [ - self.pkg_to_dict(p) for p in - sorted(self.is_installed(repoq, stuff, qf=is_installed_qf) + self.is_available(repoq, stuff, qf=qf)) - if p.strip() - ] - - def exec_install(self, items, action, pkgs, res): - cmd = self.yum_basecmd + [action] + pkgs - if self.releasever: - cmd.extend(['--releasever=%s' % self.releasever]) - - # setting sslverify using --setopt is required as conf.sslverify only - # affects the metadata retrieval. - if not self.sslverify: - cmd.extend(['--setopt', 'sslverify=0']) - - if self.module.check_mode: - self.module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs)) - else: - res['changes'] = dict(installed=pkgs) - - locale = get_best_parsable_locale(self.module) - lang_env = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale) - rc, out, err = self.module.run_command(cmd, environ_update=lang_env) - - if rc == 1: - for spec in items: - # Fail on invalid urls: - if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): - err = 'Package at %s could not be installed' % spec - self.module.fail_json(changed=False, msg=err, rc=rc) - - res['rc'] = rc - res['results'].append(out) - res['msg'] += err - res['changed'] = True - - if ('Nothing to do' in out and rc == 0) or ('does not have any packages' in err): - res['changed'] = False - - if rc != 0: - res['changed'] = False - self.module.fail_json(**res) - - # Fail if yum prints 'No space left on device' because that means some - # packages failed executing their post install scripts because of lack of - # free space (e.g. kernel package couldn't generate initramfs). Note that - # yum can still exit with rc=0 even if some post scripts didn't execute - # correctly. - if 'No space left on device' in (out or err): - res['changed'] = False - res['msg'] = 'No space left on device' - self.module.fail_json(**res) - - # FIXME - if we did an install - go and check the rpmdb to see if it actually installed - # look for each pkg in rpmdb - # look for each pkg via obsoletes - - return res - - def install(self, items, repoq): - - pkgs = [] - downgrade_pkgs = [] - res = {} - res['results'] = [] - res['msg'] = '' - res['rc'] = 0 - res['changed'] = False - - for spec in items: - pkg = None - downgrade_candidate = False - - # check if pkgspec is installed (if possible for idempotence) - if spec.endswith('.rpm') or '://' in spec: - if '://' not in spec and not os.path.exists(spec): - res['msg'] += "No RPM file matching '%s' found on system" % spec - res['results'].append("No RPM file matching '%s' found on system" % spec) - res['rc'] = 127 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - if '://' in spec: - with self.set_env_proxy(): - package = fetch_file(self.module, spec) - if not package.endswith('.rpm'): - # yum requires a local file to have the extension of .rpm and we - # can not guarantee that from an URL (redirects, proxies, etc) - new_package_path = '%s.rpm' % package - os.rename(package, new_package_path) - package = new_package_path - else: - package = spec - - # most common case is the pkg is already installed - envra = self.local_envra(package) - if envra is None: - self.module.fail_json(msg="Failed to get envra information from RPM package: %s" % spec) - installed_pkgs = self.is_installed(repoq, envra) - if installed_pkgs: - res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], package)) - continue - - (name, ver, rel, epoch, arch) = splitFilename(envra) - installed_pkgs = self.is_installed(repoq, name) - - # case for two same envr but different archs like x86_64 and i686 - if len(installed_pkgs) == 2: - (cur_name0, cur_ver0, cur_rel0, cur_epoch0, cur_arch0) = splitFilename(installed_pkgs[0]) - (cur_name1, cur_ver1, cur_rel1, cur_epoch1, cur_arch1) = splitFilename(installed_pkgs[1]) - cur_epoch0 = cur_epoch0 or '0' - cur_epoch1 = cur_epoch1 or '0' - compare = compareEVR((cur_epoch0, cur_ver0, cur_rel0), (cur_epoch1, cur_ver1, cur_rel1)) - if compare == 0 and cur_arch0 != cur_arch1: - for installed_pkg in installed_pkgs: - if installed_pkg.endswith(arch): - installed_pkgs = [installed_pkg] - - if len(installed_pkgs) == 1: - installed_pkg = installed_pkgs[0] - (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(installed_pkg) - cur_epoch = cur_epoch or '0' - compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) - - # compare > 0 -> higher version is installed - # compare == 0 -> exact version is installed - # compare < 0 -> lower version is installed - if compare > 0 and self.allow_downgrade: - downgrade_candidate = True - elif compare >= 0: - continue - - # else: if there are more installed packages with the same name, that would mean - # kernel, gpg-pubkey or like, so just let yum deal with it and try to install it - - pkg = package - - # groups - elif spec.startswith('@'): - if self.is_group_env_installed(spec): - continue - - pkg = spec - - # range requires or file-requires or pkgname :( - else: - # most common case is the pkg is already installed and done - # short circuit all the bs - and search for it as a pkg in is_installed - # if you find it then we're done - if not set(['*', '?']).intersection(set(spec)): - installed_pkgs = self.is_installed(repoq, spec, is_pkg=True) - if installed_pkgs: - res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec)) - continue - - # look up what pkgs provide this - pkglist = self.what_provides(repoq, spec) - if not pkglist: - res['msg'] += "No package matching '%s' found available, installed or updated" % spec - res['results'].append("No package matching '%s' found available, installed or updated" % spec) - res['rc'] = 126 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the yum operation later - conflicts = self.transaction_exists(pkglist) - if conflicts: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - res['rc'] = 125 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - # if any of them are installed - # then nothing to do - - found = False - for this in pkglist: - if self.is_installed(repoq, this, is_pkg=True): - found = True - res['results'].append('%s providing %s is already installed' % (this, spec)) - break - - # if the version of the pkg you have installed is not in ANY repo, but there are - # other versions in the repos (both higher and lower) then the previous checks won't work. - # so we check one more time. This really only works for pkgname - not for file provides or virt provides - # but virt provides should be all caught in what_provides on its own. - # highly irritating - if not found: - if self.is_installed(repoq, spec): - found = True - res['results'].append('package providing %s is already installed' % (spec)) - - if found: - continue - - # Downgrade - The yum install command will only install or upgrade to a spec version, it will - # not install an older version of an RPM even if specified by the install spec. So we need to - # determine if this is a downgrade, and then use the yum downgrade command to install the RPM. - if self.allow_downgrade: - for package in pkglist: - # Get the NEVRA of the requested package using pkglist instead of spec because pkglist - # contains consistently-formatted package names returned by yum, rather than user input - # that is often not parsed correctly by splitFilename(). - (name, ver, rel, epoch, arch) = splitFilename(package) - - # Check if any version of the requested package is installed - inst_pkgs = self.is_installed(repoq, name, is_pkg=True) - if inst_pkgs: - (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(inst_pkgs[0]) - compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel)) - if compare > 0: - downgrade_candidate = True - else: - downgrade_candidate = False - break - - # If package needs to be installed/upgraded/downgraded, then pass in the spec - # we could get here if nothing provides it but that's not - # the error we're catching here - pkg = spec - - if downgrade_candidate and self.allow_downgrade: - downgrade_pkgs.append(pkg) - else: - pkgs.append(pkg) - - if downgrade_pkgs: - res = self.exec_install(items, 'downgrade', downgrade_pkgs, res) - - if pkgs: - res = self.exec_install(items, 'install', pkgs, res) - - return res - - def remove(self, items, repoq): - - pkgs = [] - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - - for pkg in items: - if pkg.startswith('@'): - installed = self.is_group_env_installed(pkg) - else: - installed = self.is_installed(repoq, pkg) - - if installed: - pkgs.append(pkg) - else: - res['results'].append('%s is not installed' % pkg) - - if pkgs: - if self.module.check_mode: - self.module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs)) - else: - res['changes'] = dict(removed=pkgs) - - # run an actual yum transaction - if self.autoremove: - cmd = self.yum_basecmd + ["autoremove"] + pkgs - else: - cmd = self.yum_basecmd + ["remove"] + pkgs - rc, out, err = self.module.run_command(cmd) - - res['rc'] = rc - res['results'].append(out) - res['msg'] = err - - if rc != 0: - if self.autoremove and 'No such command' in out: - self.module.fail_json(msg='Version of YUM too old for autoremove: Requires yum 3.4.3 (RHEL/CentOS 7+)') - else: - self.module.fail_json(**res) - - # compile the results into one batch. If anything is changed - # then mark changed - # at the end - if we've end up failed then fail out of the rest - # of the process - - # at this point we check to see if the pkg is no longer present - self._yum_base = None # previous YumBase package index is now invalid - for pkg in pkgs: - if pkg.startswith('@'): - installed = self.is_group_env_installed(pkg) - else: - installed = self.is_installed(repoq, pkg, is_pkg=True) - - if installed: - # Return a message so it's obvious to the user why yum failed - # and which package couldn't be removed. More details: - # https://github.com/ansible/ansible/issues/35672 - res['msg'] = "Package '%s' couldn't be removed!" % pkg - self.module.fail_json(**res) - - res['changed'] = True - - return res - - def run_check_update(self): - # run check-update to see if we have packages pending - if self.releasever: - rc, out, err = self.module.run_command(self.yum_basecmd + ['check-update'] + ['--releasever=%s' % self.releasever]) - else: - rc, out, err = self.module.run_command(self.yum_basecmd + ['check-update']) - return rc, out, err - - @staticmethod - def parse_check_update(check_update_output): - # preprocess string and filter out empty lines so the regex below works - out = '\n'.join((l for l in check_update_output.splitlines() if l)) - - # Remove incorrect new lines in longer columns in output from yum check-update - # yum line wrapping can move the repo to the next line: - # some_looooooooooooooooooooooooooooooooooooong_package_name 1:1.2.3-1.el7 - # some-repo-label - out = re.sub(r'\n\W+(.*)', r' \1', out) - - updates = {} - obsoletes = {} - for line in out.split('\n'): - line = line.split() - # Ignore irrelevant lines: - # - '*' in line matches lines like mirror lists: "* base: mirror.corbina.net" - # - len(line) != 3 or 6 could be strings like: - # "This system is not registered with an entitlement server..." - # - len(line) = 6 is package obsoletes - # - checking for '.' in line[0] (package name) likely ensures that it is of format: - # "package_name.arch" (coreutils.x86_64) - if '*' in line or len(line) not in [3, 6] or '.' not in line[0]: - continue - - pkg, version, repo = line[0], line[1], line[2] - name, dist = pkg.rsplit('.', 1) - - if name not in updates: - updates[name] = [] - - updates[name].append({'version': version, 'dist': dist, 'repo': repo}) - - if len(line) == 6: - obsolete_pkg, obsolete_version, obsolete_repo = line[3], line[4], line[5] - obsolete_name, obsolete_dist = obsolete_pkg.rsplit('.', 1) - - if obsolete_name not in obsoletes: - obsoletes[obsolete_name] = [] - - obsoletes[obsolete_name].append({'version': obsolete_version, 'dist': obsolete_dist, 'repo': obsolete_repo}) - - return updates, obsoletes - - def latest(self, items, repoq): - - res = {} - res['results'] = [] - res['msg'] = '' - res['changed'] = False - res['rc'] = 0 - pkgs = {} - pkgs['update'] = [] - pkgs['install'] = [] - updates = {} - obsoletes = {} - update_all = False - cmd = self.yum_basecmd[:] - - # determine if we're doing an update all - if '*' in items: - update_all = True - - rc, out, err = self.run_check_update() - - if rc == 0 and update_all: - res['results'].append('Nothing to do here, all packages are up to date') - return res - elif rc == 100: - updates, obsoletes = self.parse_check_update(out) - elif rc == 1: - res['msg'] = err - res['rc'] = rc - self.module.fail_json(**res) - - if update_all: - cmd.append('update') - will_update = set(updates.keys()) - will_update_from_other_package = dict() - else: - will_update = set() - will_update_from_other_package = dict() - for spec in items: - # some guess work involved with groups. update @ will install the group if missing - if spec.startswith('@'): - pkgs['update'].append(spec) - will_update.add(spec) - continue - - # check if pkgspec is installed (if possible for idempotence) - # localpkg - if spec.endswith('.rpm') and '://' not in spec: - if not os.path.exists(spec): - res['msg'] += "No RPM file matching '%s' found on system" % spec - res['results'].append("No RPM file matching '%s' found on system" % spec) - res['rc'] = 127 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - # get the pkg e:name-v-r.arch - envra = self.local_envra(spec) - - if envra is None: - self.module.fail_json(msg="Failed to get envra information from RPM package: %s" % spec) - - # local rpm files can't be updated - if self.is_installed(repoq, envra): - pkgs['update'].append(spec) - else: - pkgs['install'].append(spec) - continue - - # URL - if '://' in spec: - # download package so that we can check if it's already installed - with self.set_env_proxy(): - package = fetch_file(self.module, spec) - envra = self.local_envra(package) - - if envra is None: - self.module.fail_json(msg="Failed to get envra information from RPM package: %s" % spec) - - # local rpm files can't be updated - if self.is_installed(repoq, envra): - pkgs['update'].append(spec) - else: - pkgs['install'].append(spec) - continue - - # dep/pkgname - find it - if self.is_installed(repoq, spec): - pkgs['update'].append(spec) - else: - pkgs['install'].append(spec) - pkglist = self.what_provides(repoq, spec) - # FIXME..? may not be desirable to throw an exception here if a single package is missing - if not pkglist: - res['msg'] += "No package matching '%s' found available, installed or updated" % spec - res['results'].append("No package matching '%s' found available, installed or updated" % spec) - res['rc'] = 126 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - nothing_to_do = True - for pkg in pkglist: - if spec in pkgs['install'] and self.is_available(repoq, pkg): - nothing_to_do = False - break - - # this contains the full NVR and spec could contain wildcards - # or virtual provides (like "python-*" or "smtp-daemon") while - # updates contains name only. - (pkgname, ver, rel, epoch, arch) = splitFilename(pkg) - if spec in pkgs['update'] and pkgname in updates: - nothing_to_do = False - will_update.add(spec) - # Massage the updates list - if spec != pkgname: - # For reporting what packages would be updated more - # succinctly - will_update_from_other_package[spec] = pkgname - break - - if not self.is_installed(repoq, spec) and self.update_only: - res['results'].append("Packages providing %s not installed due to update_only specified" % spec) - continue - if nothing_to_do: - res['results'].append("All packages providing %s are up to date" % spec) - continue - - # if any of the packages are involved in a transaction, fail now - # so that we don't hang on the yum operation later - conflicts = self.transaction_exists(pkglist) - if conflicts: - res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) - res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts)) - res['rc'] = 128 # Ensure the task fails in with-loop - self.module.fail_json(**res) - - # check_mode output - to_update = [] - for w in will_update: - if w.startswith('@'): - # yum groups - to_update.append((w, None)) - elif w not in updates: - # There are (at least, probably more) 2 ways we can get here: - # - # * A virtual provides (our user specifies "webserver", but - # "httpd" is the key in 'updates'). - # - # * A wildcard. emac* will get us here if there's a package - # called 'emacs' in the pending updates list. 'updates' will - # of course key on 'emacs' in that case. - - other_pkg = will_update_from_other_package[w] - - # We are guaranteed that: other_pkg in updates - # ...based on the logic above. But we only want to show one - # update in this case (given the wording of "at least") below. - # As an example, consider a package installed twice: - # foobar.x86_64, foobar.i686 - # We want to avoid having both: - # ('foo*', 'because of (at least) foobar-1.x86_64 from repo') - # ('foo*', 'because of (at least) foobar-1.i686 from repo') - # We just pick the first one. - # - # TODO: This is something that might be nice to change, but it - # would be a module UI change. But without it, we're - # dropping potentially important information about what - # was updated. Instead of (given_spec, random_matching_package) - # it'd be nice if we appended (given_spec, [all_matching_packages]) - # - # ... But then, we also drop information if multiple - # different (distinct) packages match the given spec and - # we should probably fix that too. - pkg = updates[other_pkg][0] - to_update.append( - ( - w, - 'because of (at least) %s-%s.%s from %s' % ( - other_pkg, - pkg['version'], - pkg['dist'], - pkg['repo'] - ) - ) - ) - else: - # Otherwise the spec is an exact match - for pkg in updates[w]: - to_update.append( - ( - w, - '%s.%s from %s' % ( - pkg['version'], - pkg['dist'], - pkg['repo'] - ) - ) - ) - - if self.update_only: - res['changes'] = dict(installed=[], updated=to_update) - else: - res['changes'] = dict(installed=pkgs['install'], updated=to_update) - - if obsoletes: - res['obsoletes'] = obsoletes - - # return results before we actually execute stuff - if self.module.check_mode: - if will_update or pkgs['install']: - res['changed'] = True - return res - - if self.releasever: - cmd.extend(['--releasever=%s' % self.releasever]) - - # run commands - if update_all: - rc, out, err = self.module.run_command(cmd) - res['changed'] = True - elif self.update_only: - if pkgs['update']: - cmd += ['update'] + pkgs['update'] - locale = get_best_parsable_locale(self.module) - lang_env = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale) - rc, out, err = self.module.run_command(cmd, environ_update=lang_env) - out_lower = out.strip().lower() - if not out_lower.endswith("no packages marked for update") and \ - not out_lower.endswith("nothing to do"): - res['changed'] = True - else: - rc, out, err = [0, '', ''] - elif pkgs['install'] or will_update and not self.update_only: - cmd += ['install'] + pkgs['install'] + pkgs['update'] - locale = get_best_parsable_locale(self.module) - lang_env = dict(LANG=locale, LC_ALL=locale, LC_MESSAGES=locale) - rc, out, err = self.module.run_command(cmd, environ_update=lang_env) - out_lower = out.strip().lower() - if not out_lower.endswith("no packages marked for update") and \ - not out_lower.endswith("nothing to do"): - res['changed'] = True - else: - rc, out, err = [0, '', ''] - - res['rc'] = rc - res['msg'] += err - res['results'].append(out) - - if rc: - res['failed'] = True - - return res - - def ensure(self, repoq): - pkgs = self.names - - # autoremove was provided without `name` - if not self.names and self.autoremove: - pkgs = [] - self.state = 'absent' - - if self.conf_file and os.path.exists(self.conf_file): - self.yum_basecmd += ['-c', self.conf_file] - - if repoq: - repoq += ['-c', self.conf_file] - - if self.skip_broken: - self.yum_basecmd.extend(['--skip-broken']) - - if self.disablerepo: - self.yum_basecmd.extend(['--disablerepo=%s' % ','.join(self.disablerepo)]) - - if self.enablerepo: - self.yum_basecmd.extend(['--enablerepo=%s' % ','.join(self.enablerepo)]) - - if self.enable_plugin: - self.yum_basecmd.extend(['--enableplugin', ','.join(self.enable_plugin)]) - - if self.disable_plugin: - self.yum_basecmd.extend(['--disableplugin', ','.join(self.disable_plugin)]) - - if self.exclude: - e_cmd = ['--exclude=%s' % ','.join(self.exclude)] - self.yum_basecmd.extend(e_cmd) - - if self.disable_excludes: - self.yum_basecmd.extend(['--disableexcludes=%s' % self.disable_excludes]) - - if self.cacheonly: - self.yum_basecmd.extend(['--cacheonly']) - - if self.download_only: - self.yum_basecmd.extend(['--downloadonly']) - - if self.download_dir: - self.yum_basecmd.extend(['--downloaddir=%s' % self.download_dir]) - - if self.releasever: - self.yum_basecmd.extend(['--releasever=%s' % self.releasever]) - - if self.installroot != '/': - # do not setup installroot by default, because of error - # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf - # in old yum version (like in CentOS 6.6) - e_cmd = ['--installroot=%s' % self.installroot] - self.yum_basecmd.extend(e_cmd) - - if self.state in ('installed', 'present', 'latest'): - # The need of this entire if conditional has to be changed - # this function is the ensure function that is called - # in the main section. - # - # This conditional tends to disable/enable repo for - # install present latest action, same actually - # can be done for remove and absent action - # - # As solution I would advice to cal - # try: self.yum_base.repos.disableRepo(disablerepo) - # and - # try: self.yum_base.repos.enableRepo(enablerepo) - # right before any yum_cmd is actually called regardless - # of yum action. - # - # Please note that enable/disablerepo options are general - # options, this means that we can call those with any action - # option. https://linux.die.net/man/8/yum - # - # This docstring will be removed together when issue: #21619 - # will be solved. - # - # This has been triggered by: #19587 - - if self.update_cache: - self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache']) - - try: - current_repos = self.yum_base.repos.repos.keys() - if self.enablerepo: - try: - new_repos = self.yum_base.repos.repos.keys() - for i in new_repos: - if i not in current_repos: - rid = self.yum_base.repos.getRepo(i) - a = rid.repoXML.repoid # nopep8 - https://github.com/ansible/ansible/pull/21475#pullrequestreview-22404868 - current_repos = new_repos - except yum.Errors.YumBaseError as e: - self.module.fail_json(msg="Error setting/accessing repos: %s" % to_native(e)) - except yum.Errors.YumBaseError as e: - self.module.fail_json(msg="Error accessing repos: %s" % to_native(e)) - if self.state == 'latest' or self.update_only: - if self.disable_gpg_check: - self.yum_basecmd.append('--nogpgcheck') - if self.security: - self.yum_basecmd.append('--security') - if self.bugfix: - self.yum_basecmd.append('--bugfix') - res = self.latest(pkgs, repoq) - elif self.state in ('installed', 'present'): - if self.disable_gpg_check: - self.yum_basecmd.append('--nogpgcheck') - res = self.install(pkgs, repoq) - elif self.state in ('removed', 'absent'): - res = self.remove(pkgs, repoq) - else: - # should be caught by AnsibleModule argument_spec - self.module.fail_json( - msg="we should never get here unless this all failed", - changed=False, - results='', - errors='unexpected state' - ) - return res - - @staticmethod - def has_yum(): - return HAS_YUM_PYTHON - - def run(self): - """ - actually execute the module code backend - """ - - if (not HAS_RPM_PYTHON or not HAS_YUM_PYTHON) and sys.executable != '/usr/bin/python' and not has_respawned(): - respawn_module('/usr/bin/python') - # end of the line for this process; we'll exit here once the respawned module has completed - - error_msgs = [] - if not HAS_RPM_PYTHON: - error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') - if not HAS_YUM_PYTHON: - error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.') - - self.wait_for_lock() - - if error_msgs: - self.module.fail_json(msg='. '.join(error_msgs)) - - # fedora will redirect yum to dnf, which has incompatibilities - # with how this module expects yum to operate. If yum-deprecated - # is available, use that instead to emulate the old behaviors. - if self.module.get_bin_path('yum-deprecated'): - yumbin = self.module.get_bin_path('yum-deprecated') - else: - yumbin = self.module.get_bin_path('yum') - - # need debug level 2 to get 'Nothing to do' for groupinstall. - self.yum_basecmd = [yumbin, '-d', '2', '-y'] - - if self.update_cache and not self.names and not self.list: - rc, stdout, stderr = self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache']) - if rc == 0: - self.module.exit_json( - changed=False, - msg="Cache updated", - rc=rc, - results=[] - ) - else: - self.module.exit_json( - changed=False, - msg="Failed to update cache", - rc=rc, - results=[stderr], - ) - - repoquerybin = self.module.get_bin_path('repoquery', required=False) - - if self.install_repoquery and not repoquerybin and not self.module.check_mode: - yum_path = self.module.get_bin_path('yum') - if yum_path: - if self.releasever: - self.module.run_command('%s -y install yum-utils --releasever %s' % (yum_path, self.releasever)) - else: - self.module.run_command('%s -y install yum-utils' % yum_path) - repoquerybin = self.module.get_bin_path('repoquery', required=False) - - if self.list: - if not repoquerybin: - self.module.fail_json(msg="repoquery is required to use list= with this module. Please install the yum-utils package.") - results = {'results': self.list_stuff(repoquerybin, self.list)} - else: - # If rhn-plugin is installed and no rhn-certificate is available on - # the system then users will see an error message using the yum API. - # Use repoquery in those cases. - - repoquery = None - try: - yum_plugins = self.yum_base.plugins._plugins - except AttributeError: - pass - else: - if 'rhnplugin' in yum_plugins: - if repoquerybin: - repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet'] - if self.installroot != '/': - repoquery.extend(['--installroot', self.installroot]) - - if self.disable_excludes: - # repoquery does not support --disableexcludes, - # so make a temp copy of yum.conf and get rid of the 'exclude=' line there - try: - with open('/etc/yum.conf', 'r') as f: - content = f.readlines() - - tmp_conf_file = tempfile.NamedTemporaryFile(dir=self.module.tmpdir, delete=False) - self.module.add_cleanup_file(tmp_conf_file.name) - - tmp_conf_file.writelines([c for c in content if not c.startswith("exclude=")]) - tmp_conf_file.close() - except Exception as e: - self.module.fail_json(msg="Failure setting up repoquery: %s" % to_native(e)) - - repoquery.extend(['-c', tmp_conf_file.name]) - - results = self.ensure(repoquery) - if repoquery: - results['msg'] = '%s %s' % ( - results.get('msg', ''), - 'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.' - ) - - self.module.exit_json(**results) - - -def main(): - # state=installed name=pkgspec - # state=removed name=pkgspec - # state=latest name=pkgspec - # - # informational commands: - # list=installed - # list=updates - # list=available - # list=repos - # list=pkgspec - - yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf', 'dnf4', 'dnf5']) - - module = AnsibleModule( - **yumdnf_argument_spec - ) - - module_implementation = YumModule(module) - module_implementation.run() - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/yum_repository.py b/lib/ansible/modules/yum_repository.py index e012951..c171c6c 100644 --- a/lib/ansible/modules/yum_repository.py +++ b/lib/ansible/modules/yum_repository.py @@ -4,8 +4,7 @@ # # 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 +from __future__ import annotations DOCUMENTATION = ''' @@ -67,7 +66,7 @@ options: type: str description: description: - - A human readable string describing the repository. This option corresponds to the "name" property in the repo file. + - A human-readable string describing the repository. This option corresponds to the "name" property in the repo file. - This parameter is only required if O(state) is set to V(present). type: str enabled: -- cgit v1.2.3