diff --git a/changelogs/fragments/82307-handlers-lockstep-linear-fix.yml b/changelogs/fragments/82307-handlers-lockstep-linear-fix.yml new file mode 100644 index 00000000000..da97a9753bb --- /dev/null +++ b/changelogs/fragments/82307-handlers-lockstep-linear-fix.yml @@ -0,0 +1,2 @@ +bugfixes: + - Fix handlers not being executed in lockstep using the linear strategy in some cases (https://github.com/ansible/ansible/issues/82307) diff --git a/changelogs/fragments/82831_countme_yum_repository.yml b/changelogs/fragments/82831_countme_yum_repository.yml new file mode 100644 index 00000000000..7f6bec4c487 --- /dev/null +++ b/changelogs/fragments/82831_countme_yum_repository.yml @@ -0,0 +1,2 @@ +bugfixes: + - support the countme option when using yum_repository diff --git a/changelogs/fragments/83031.yml b/changelogs/fragments/83031.yml new file mode 100644 index 00000000000..32d7c09529d --- /dev/null +++ b/changelogs/fragments/83031.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - lookup - Fixed examples of csv lookup plugin (https://github.com/ansible/ansible/issues/83031). diff --git a/changelogs/fragments/add_systemd_facts.yml b/changelogs/fragments/add_systemd_facts.yml new file mode 100644 index 00000000000..93af448a7f3 --- /dev/null +++ b/changelogs/fragments/add_systemd_facts.yml @@ -0,0 +1,2 @@ +minor_changes: + - facts - add systemd version and features diff --git a/changelogs/fragments/ansible-config-validate.yml b/changelogs/fragments/ansible-config-validate.yml new file mode 100644 index 00000000000..fab48db9026 --- /dev/null +++ b/changelogs/fragments/ansible-config-validate.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-config has new 'validate' option to find mispelled/forgein configurations in ini file or environment variables. diff --git a/changelogs/fragments/ansible-galaxy-role-install-symlink.yml b/changelogs/fragments/ansible-galaxy-role-install-symlink.yml new file mode 100644 index 00000000000..c2003b15cd2 --- /dev/null +++ b/changelogs/fragments/ansible-galaxy-role-install-symlink.yml @@ -0,0 +1,2 @@ +bugfixes: + - ansible-galaxy role install - fix symlinks (https://github.com/ansible/ansible/issues/82702, https://github.com/ansible/ansible/issues/81965). diff --git a/changelogs/fragments/ansible_managed_restore.yml b/changelogs/fragments/ansible_managed_restore.yml new file mode 100644 index 00000000000..63d15bf9dca --- /dev/null +++ b/changelogs/fragments/ansible_managed_restore.yml @@ -0,0 +1,2 @@ +bugfixes: + - ansible_managed restored it's 'templatability' by ensuring the possible injection routes are cut off earlier in the process. diff --git a/changelogs/fragments/assemble.yml b/changelogs/fragments/assemble.yml new file mode 100644 index 00000000000..27b66551492 --- /dev/null +++ b/changelogs/fragments/assemble.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - assemble - update argument_spec with 'decrypt' option which is required by action plugin (https://github.com/ansible/ansible/issues/80840). diff --git a/changelogs/fragments/dnf-installroot-substitutions.yml b/changelogs/fragments/dnf-installroot-substitutions.yml new file mode 100644 index 00000000000..aef96f4e2b3 --- /dev/null +++ b/changelogs/fragments/dnf-installroot-substitutions.yml @@ -0,0 +1,3 @@ +bugfixes: + - dnf - honor installroot for ``cachedir``, ``logdir`` and ``persistdir`` + - dnf - perform variable substitutions in ``logdir`` and ``persistdir`` diff --git a/changelogs/fragments/dnf5-api-breaks.yml b/changelogs/fragments/dnf5-api-breaks.yml new file mode 100644 index 00000000000..99c9ecd6cfc --- /dev/null +++ b/changelogs/fragments/dnf5-api-breaks.yml @@ -0,0 +1,2 @@ +bugfixes: + - dnf5 - replace removed API calls diff --git a/changelogs/fragments/mask_me.yml b/changelogs/fragments/mask_me.yml new file mode 100644 index 00000000000..57aac99aa9a --- /dev/null +++ b/changelogs/fragments/mask_me.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - systemd_service - handle mask operation failure (https://github.com/ansible/ansible/issues/81649). diff --git a/changelogs/fragments/vmware_facts.yml b/changelogs/fragments/vmware_facts.yml new file mode 100644 index 00000000000..257fe90c3fb --- /dev/null +++ b/changelogs/fragments/vmware_facts.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - facts - add a generic detection for VMware in product name. diff --git a/lib/ansible/cli/config.py b/lib/ansible/cli/config.py index e7f240c80d4..995649c3b12 100755 --- a/lib/ansible/cli/config.py +++ b/lib/ansible/cli/config.py @@ -9,9 +9,10 @@ from __future__ import annotations from ansible.cli import CLI import os -import yaml import shlex import subprocess +import sys +import yaml from collections.abc import Mapping @@ -49,6 +50,37 @@ def get_constants(): return get_constants.cvars +def _ansible_env_vars(varname): + ''' return true or false depending if variable name is possibly a 'configurable' ansible env variable ''' + return all( + [ + varname.startswith("ANSIBLE_"), + not varname.startswith(("ANSIBLE_TEST_", "ANSIBLE_LINT_")), + varname not in ("ANSIBLE_CONFIG", "ANSIBLE_DEV_HOME"), + ] + ) + + +def _get_evar_list(settings): + data = [] + for setting in settings: + if 'env' in settings[setting] and settings[setting]['env']: + for varname in settings[setting]['env']: + data.append(varname.get('name')) + return data + + +def _get_ini_entries(settings): + data = {} + for setting in settings: + if 'ini' in settings[setting] and settings[setting]['ini']: + for kv in settings[setting]['ini']: + if not kv['section'] in data: + data[kv['section']] = set() + data[kv['section']].add(kv['key']) + return data + + class ConfigCLI(CLI): """ Config command line class """ @@ -99,9 +131,13 @@ class ConfigCLI(CLI): init_parser.add_argument('--disabled', dest='commented', action='store_true', default=False, help='Prefixes all entries with a comment character to disable them') - # search_parser = subparsers.add_parser('find', help='Search configuration') - # search_parser.set_defaults(func=self.execute_search) - # search_parser.add_argument('args', help='Search term', metavar='') + validate_parser = subparsers.add_parser('validate', + help='Validate the configuration file and environment variables. ' + 'By default it only checks the base settings without accounting for plugins (see -t).', + parents=[common]) + validate_parser.set_defaults(func=self.execute_validate) + validate_parser.add_argument('--format', '-f', dest='format', action='store', choices=['ini', 'env'] , default='ini', + help='Output format for init') def post_process_args(self, options): options = super(ConfigCLI, self).post_process_args(options) @@ -239,6 +275,7 @@ class ConfigCLI(CLI): for ptype in C.CONFIGURABLE_PLUGINS: config_entries['PLUGINS'][ptype.upper()] = self._list_plugin_settings(ptype) elif context.CLIARGS['type'] != 'base': + # only for requested types config_entries['PLUGINS'][context.CLIARGS['type']] = self._list_plugin_settings(context.CLIARGS['type'], context.CLIARGS['args']) return config_entries @@ -358,7 +395,7 @@ class ConfigCLI(CLI): elif default is None: default = '' - if context.CLIARGS['commented']: + if context.CLIARGS.get('commented', False): entry['key'] = ';%s' % entry['key'] key = desc + '\n%s=%s' % (entry['key'], default) @@ -552,6 +589,64 @@ class ConfigCLI(CLI): self.pager(to_text(text, errors='surrogate_or_strict')) + def execute_validate(self): + + found = False + config_entries = self._list_entries_from_args() + plugin_types = config_entries.pop('PLUGINS', None) + + if context.CLIARGS['format'] == 'ini': + if C.CONFIG_FILE is not None: + # validate ini config since it is found + + sections = _get_ini_entries(config_entries) + # Also from plugins + if plugin_types: + for ptype in plugin_types: + for plugin in plugin_types[ptype].keys(): + plugin_sections = _get_ini_entries(plugin_types[ptype][plugin]) + for s in plugin_sections: + if s in sections: + sections[s].update(plugin_sections[s]) + else: + sections[s] = plugin_sections[s] + if sections: + p = C.config._parsers[C.CONFIG_FILE] + for s in p.sections(): + # check for valid sections + if s not in sections: + display.error(f"Found unknown section '{s}' in '{C.CONFIG_FILE}.") + found = True + continue + + # check keys in valid sections + for k in p.options(s): + if k not in sections[s]: + display.error(f"Found unknown key '{k}' in section '{s}' in '{C.CONFIG_FILE}.") + found = True + + elif context.CLIARGS['format'] == 'env': + # validate any 'ANSIBLE_' env vars found + evars = [varname for varname in os.environ.keys() if _ansible_env_vars(varname)] + if evars: + data = _get_evar_list(config_entries) + if plugin_types: + for ptype in plugin_types: + for plugin in plugin_types[ptype].keys(): + data.extend(_get_evar_list(plugin_types[ptype][plugin])) + + for evar in evars: + if evar not in data: + display.error(f"Found unknown environment variable '{evar}'.") + found = True + + # we found discrepancies! + if found: + sys.exit(1) + + # allsgood + display.display("All configurations seem valid!") + def main(args=None): ConfigCLI.cli_executor(args) diff --git a/lib/ansible/cli/inventory.py b/lib/ansible/cli/inventory.py index 8c7c7e51d71..6a4ee9241f7 100755 --- a/lib/ansible/cli/inventory.py +++ b/lib/ansible/cli/inventory.py @@ -73,12 +73,12 @@ class InventoryCLI(CLI): # list self.parser.add_argument("--export", action="store_true", default=C.INVENTORY_EXPORT, dest='export', - help="When doing an --list, represent in a way that is optimized for export," + help="When doing --list, represent in a way that is optimized for export," "not as an accurate representation of how Ansible has processed it") self.parser.add_argument('--output', default=None, dest='output_file', help="When doing --list, send the inventory to a file instead of to the screen") # self.parser.add_argument("--ignore-vars-plugins", action="store_true", default=False, dest='ignore_vars_plugins', - # help="When doing an --list, skip vars data from vars plugins, by default, this would include group_vars/ and host_vars/") + # help="When doing --list, skip vars data from vars plugins, by default, this would include group_vars/ and host_vars/") def post_process_args(self, options): options = super(InventoryCLI, self).post_process_args(options) diff --git a/lib/ansible/executor/play_iterator.py b/lib/ansible/executor/play_iterator.py index 58eebfaea57..474b5da94f4 100644 --- a/lib/ansible/executor/play_iterator.py +++ b/lib/ansible/executor/play_iterator.py @@ -427,13 +427,13 @@ class PlayIterator: # might be there from previous flush state.handlers = self.handlers[:] state.update_handlers = False - state.cur_handlers_task = 0 while True: try: task = state.handlers[state.cur_handlers_task] except IndexError: task = None + state.cur_handlers_task = 0 state.run_state = state.pre_flushing_run_state state.update_handlers = True break diff --git a/lib/ansible/galaxy/role.py b/lib/ansible/galaxy/role.py index d6d8454b809..d00b8a69980 100644 --- a/lib/ansible/galaxy/role.py +++ b/lib/ansible/galaxy/role.py @@ -386,6 +386,8 @@ class GalaxyRole(object): else: os.makedirs(self.path) + resolved_archive = unfrackpath(archive_parent_dir, follow=False) + # We strip off any higher-level directories for all of the files # contained within the tar file here. The default is 'github_repo-target'. # Gerrit instances, on the other hand, does not have a parent directory at all. @@ -400,33 +402,29 @@ class GalaxyRole(object): if not (attr_value := getattr(member, attr, None)): continue - if attr_value.startswith(os.sep) and not is_subpath(attr_value, archive_parent_dir): - err = f"Invalid {attr} for tarfile member: path {attr_value} is not a subpath of the role {archive_parent_dir}" - raise AnsibleError(err) - if attr == 'linkname': # Symlinks are relative to the link - relative_to_archive_dir = os.path.dirname(getattr(member, 'name', '')) - archive_dir_path = os.path.join(archive_parent_dir, relative_to_archive_dir, attr_value) + relative_to = os.path.dirname(getattr(member, 'name', '')) else: # Normalize paths that start with the archive dir attr_value = attr_value.replace(archive_parent_dir, "", 1) attr_value = os.path.join(*attr_value.split(os.sep)) # remove leading os.sep - archive_dir_path = os.path.join(archive_parent_dir, attr_value) + relative_to = '' - resolved_archive = unfrackpath(archive_parent_dir) - resolved_path = unfrackpath(archive_dir_path) - if not is_subpath(resolved_path, resolved_archive): - err = f"Invalid {attr} for tarfile member: path {resolved_path} is not a subpath of the role {resolved_archive}" + full_path = os.path.join(resolved_archive, relative_to, attr_value) + if not is_subpath(full_path, resolved_archive, real=True): + err = f"Invalid {attr} for tarfile member: path {full_path} is not a subpath of the role {resolved_archive}" raise AnsibleError(err) - relative_path = os.path.join(*resolved_path.replace(resolved_archive, "", 1).split(os.sep)) or '.' + relative_path_dir = os.path.join(resolved_archive, relative_to) + relative_path = os.path.join(*full_path.replace(relative_path_dir, "", 1).split(os.sep)) setattr(member, attr, relative_path) if _check_working_data_filter(): # deprecated: description='extract fallback without filter' python_version='3.11' role_tar_file.extract(member, to_native(self.path), filter='data') # type: ignore[call-arg] else: + # Remove along with manual path filter once Python 3.12 is minimum supported version role_tar_file.extract(member, to_native(self.path)) # write out the install info file for later use diff --git a/lib/ansible/module_utils/distro/__init__.py b/lib/ansible/module_utils/distro/__init__.py index a8c29a6ce0a..bed0b5a5b77 100644 --- a/lib/ansible/module_utils/distro/__init__.py +++ b/lib/ansible/module_utils/distro/__init__.py @@ -22,7 +22,7 @@ Compat distro library. from __future__ import annotations # The following makes it easier for us to script updates of the bundled code -_BUNDLED_METADATA = {"pypi_name": "distro", "version": "1.6.0"} +_BUNDLED_METADATA = {"pypi_name": "distro", "version": "1.8.0"} # The following additional changes have been made: # * Remove optparse since it is not needed for our use. diff --git a/lib/ansible/module_utils/facts/default_collectors.py b/lib/ansible/module_utils/facts/default_collectors.py index 1dcbd7c52a1..af4391576c0 100644 --- a/lib/ansible/module_utils/facts/default_collectors.py +++ b/lib/ansible/module_utils/facts/default_collectors.py @@ -53,6 +53,7 @@ from ansible.module_utils.facts.system.python import PythonFactCollector from ansible.module_utils.facts.system.selinux import SelinuxFactCollector from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector from ansible.module_utils.facts.system.ssh_pub_keys import SshPubKeyFactCollector +from ansible.module_utils.facts.system.systemd import SystemdFactCollector from ansible.module_utils.facts.system.user import UserFactCollector from ansible.module_utils.facts.hardware.base import HardwareCollector @@ -118,7 +119,8 @@ _general = [ EnvFactCollector, LoadAvgFactCollector, SshPubKeyFactCollector, - UserFactCollector + UserFactCollector, + SystemdFactCollector ] # type: t.List[t.Type[BaseFactCollector]] # virtual, this might also limit hardware/networking diff --git a/lib/ansible/module_utils/facts/system/systemd.py b/lib/ansible/module_utils/facts/system/systemd.py new file mode 100644 index 00000000000..154dc73fb2a --- /dev/null +++ b/lib/ansible/module_utils/facts/system/systemd.py @@ -0,0 +1,47 @@ +# Get systemd version and features +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import annotations + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector +from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector + + +class SystemdFactCollector(BaseFactCollector): + name = "systemd" + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + systemctl_bin = module.get_bin_path("systemctl") + if systemctl_bin and ServiceMgrFactCollector.is_systemd_managed(module): + rc, stdout, stderr = module.run_command( + [systemctl_bin, "--version"], + check_rc=False, + ) + + systemd_facts = {} + + if rc != 0: + return systemd_facts + + systemd_facts["systemd"] = {} + systemd_facts["systemd"]["features"] = str(stdout.split("\n")[1]) + systemd_facts["systemd"]["version"] = int(stdout.split(" ")[1]) + + return systemd_facts diff --git a/lib/ansible/module_utils/facts/virtual/linux.py b/lib/ansible/module_utils/facts/virtual/linux.py index 05ae1254ca0..57b047b11a1 100644 --- a/lib/ansible/module_utils/facts/virtual/linux.py +++ b/lib/ansible/module_utils/facts/virtual/linux.py @@ -175,7 +175,7 @@ class LinuxVirtual(Virtual): virtual_facts['virtualization_type'] = 'RHEV' found_virt = True - if product_name in ('VMware Virtual Platform', 'VMware7,1', 'VMware20,1'): + if product_name and product_name.startswith(("VMware",)): guest_tech.add('VMware') if not found_virt: virtual_facts['virtualization_type'] = 'VMware' diff --git a/lib/ansible/modules/assemble.py b/lib/ansible/modules/assemble.py index 77c33bef104..eff562242aa 100644 --- a/lib/ansible/modules/assemble.py +++ b/lib/ansible/modules/assemble.py @@ -205,6 +205,11 @@ def main(): regexp=dict(type='str'), ignore_hidden=dict(type='bool', default=False), validate=dict(type='str'), + + # Options that are for the action plugin, but ignored by the module itself. + # We have them here so that the tests pass without ignores, which + # reduces the likelihood of further bugs added. + decrypt=dict(type='bool', default=True), ), add_file_common_args=True, ) diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py index 16b3bcc2b41..44abe0b7ee0 100644 --- a/lib/ansible/modules/dnf.py +++ b/lib/ansible/modules/dnf.py @@ -403,7 +403,6 @@ from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec # to set proper locale before importing dnf to be able to scrape # the output in some cases (FIXME?). dnf = None -libdnf = None class DnfModule(YumDnf): @@ -484,7 +483,6 @@ class DnfModule(YumDnf): os.environ['LANGUAGE'] = os.environ['LANG'] = locale global dnf - global libdnf try: import dnf import dnf.const @@ -492,7 +490,6 @@ class DnfModule(YumDnf): import dnf.package import dnf.subject import dnf.util - import libdnf HAS_DNF = True except ImportError: HAS_DNF = False @@ -560,9 +557,6 @@ class DnfModule(YumDnf): # Load substitutions from the filesystem conf.substitutions.update_from_etc(installroot) - # Substitute variables in cachedir path - conf.cachedir = libdnf.conf.ConfigParser.substitute(conf.cachedir, conf.substitutions) - # Handle different DNF versions immutable mutable datatypes and # dnf v1/v2/v3 # @@ -596,6 +590,11 @@ class DnfModule(YumDnf): # setting this to an empty string instead of None appears to mimic the DNF CLI behavior conf.substitutions['releasever'] = '' + # Honor installroot for dnf directories + # This will also perform variable substitutions in the paths + for opt in ('cachedir', 'logdir', 'persistdir'): + conf.prepend_installroot(opt) + # Set skip_broken (in dnf this is strict=0) if self.skip_broken: conf.strict = 0 diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py index cd8e6d2c668..2ebc4a1004e 100644 --- a/lib/ansible/modules/dnf5.py +++ b/lib/ansible/modules/dnf5.py @@ -496,7 +496,7 @@ class Dnf5Module(YumDnf): conf.config_file_path = self.conf_file try: - base.load_config_from_file() + base.load_config() except RuntimeError as e: self.module.fail_json( msg=str(e), @@ -536,7 +536,8 @@ class Dnf5Module(YumDnf): log_router = base.get_logger() global_logger = libdnf5.logger.GlobalLogger() global_logger.set(log_router.get(), libdnf5.logger.Logger.Level_DEBUG) - logger = libdnf5.logger.create_file_logger(base) + # FIXME hardcoding the filename does not seem right, should libdnf5 expose the default file name? + logger = libdnf5.logger.create_file_logger(base, "dnf5.log") log_router.add_logger(logger) if self.update_cache: @@ -561,7 +562,11 @@ class Dnf5Module(YumDnf): for repo in repo_query: repo.enable() - sack.update_and_load_enabled_repos(True) + try: + sack.load_repos() + except AttributeError: + # dnf5 < 5.2.0.0 + sack.update_and_load_enabled_repos(True) if self.update_cache and not self.names and not self.list: self.module.exit_json( @@ -593,7 +598,11 @@ class Dnf5Module(YumDnf): self.module.exit_json(msg="", results=results, rc=0) settings = libdnf5.base.GoalJobSettings() - settings.group_with_name = True + try: + settings.set_group_with_name(True) + except AttributeError: + # dnf5 < 5.2.0.0 + settings.group_with_name = True if self.bugfix or self.security: advisory_query = libdnf5.advisory.AdvisoryQuery(base) types = [] diff --git a/lib/ansible/modules/setup.py b/lib/ansible/modules/setup.py index d387022fda5..a8928fa5b4f 100644 --- a/lib/ansible/modules/setup.py +++ b/lib/ansible/modules/setup.py @@ -25,7 +25,7 @@ options: V(processor_count), V(python), V(python_version), V(real_user_id), V(selinux), V(service_mgr), V(ssh_host_key_dsa_public), V(ssh_host_key_ecdsa_public), V(ssh_host_key_ed25519_public), V(ssh_host_key_rsa_public), V(ssh_host_pub_keys), V(ssh_pub_keys), V(system), V(system_capabilities), - V(system_capabilities_enforced), V(user), V(user_dir), V(user_gecos), V(user_gid), V(user_id), + V(system_capabilities_enforced), V(systemd), V(user), V(user_dir), V(user_gecos), V(user_gid), V(user_id), V(user_shell), V(user_uid), V(virtual), V(virtualization_role), V(virtualization_type). Can specify a list of values to specify a larger subset. Values can also be used with an initial C(!) to specify that diff --git a/lib/ansible/modules/systemd_service.py b/lib/ansible/modules/systemd_service.py index 34aef891fd3..dc9e4fd3ffe 100644 --- a/lib/ansible/modules/systemd_service.py +++ b/lib/ansible/modules/systemd_service.py @@ -495,6 +495,8 @@ def main(): if rc != 0: # some versions of system CAN mask/unmask non existing services, we only fail on missing if they don't fail_if_missing(module, found, unit, msg='host') + # here if service was not missing, but failed for other reasons + module.fail_json(msg=f"Failed to {action} the service ({unit}): {err.strip()}") # Enable/disable service startup at boot if requested if module.params['enabled'] is not None: diff --git a/lib/ansible/modules/yum_repository.py b/lib/ansible/modules/yum_repository.py index c171c6c611a..88f8cd065bd 100644 --- a/lib/ansible/modules/yum_repository.py +++ b/lib/ansible/modules/yum_repository.py @@ -50,6 +50,13 @@ options: - Relative cost of accessing this repository. Useful for weighing one repo's packages as greater/less than any other. type: str + countme: + description: + - Whether a special flag should be added to a randomly chosen metalink/mirrorlist query each week. + This allows the repository owner to estimate the number of systems consuming it. + default: ~ + type: bool + version_added: '2.18' deltarpm_metadata_percentage: description: - When the relative size of deltarpm metadata vs pkgs is larger than @@ -432,6 +439,7 @@ class YumRepo(object): 'bandwidth', 'baseurl', 'cost', + 'countme', 'deltarpm_metadata_percentage', 'deltarpm_percentage', 'enabled', @@ -581,6 +589,7 @@ def main(): bandwidth=dict(), baseurl=dict(type='list', elements='str'), cost=dict(), + countme=dict(type='bool'), deltarpm_metadata_percentage=dict(), deltarpm_percentage=dict(), description=dict(), diff --git a/lib/ansible/plugins/filter/password_hash.yml b/lib/ansible/plugins/filter/password_hash.yml index a9516b7fc01..5776cebfc5d 100644 --- a/lib/ansible/plugins/filter/password_hash.yml +++ b/lib/ansible/plugins/filter/password_hash.yml @@ -17,7 +17,7 @@ DOCUMENTATION: description: Hashing algorithm to use. type: string default: sha512 - choices: [ md5, blowfish, sha256, sha512 ] + choices: [ md5, blowfish, sha256, sha512, bcrypt ] salt: description: Secret string used for the hashing. If none is provided a random one can be generated. Use only numbers and letters (characters matching V([./0-9A-Za-z]+)). type: string diff --git a/lib/ansible/plugins/lookup/csvfile.py b/lib/ansible/plugins/lookup/csvfile.py index 9d199d82190..1304eaac6cc 100644 --- a/lib/ansible/plugins/lookup/csvfile.py +++ b/lib/ansible/plugins/lookup/csvfile.py @@ -51,18 +51,30 @@ EXAMPLES = """ - name: msg="Match 'Li' on the first column, but return the 3rd column (columns start counting after the match)" ansible.builtin.debug: msg="The atomic mass of Lithium is {{ lookup('ansible.builtin.csvfile', 'Li file=elements.csv delimiter=, col=2') }}" -- name: Define Values From CSV File, this reads file in one go, but you could also use col= to read each in it's own lookup. +# Contents of bgp_neighbors.csv +# 127.0.0.1,10.0.0.1,24,nones,lola,pepe,127.0.0.2 +# 128.0.0.1,10.1.0.1,20,notes,lolita,pepito,128.0.0.2 +# 129.0.0.1,10.2.0.1,23,nines,aayush,pepete,129.0.0.2 + +- name: Define values from CSV file, this reads file in one go, but you could also use col= to read each in it's own lookup. ansible.builtin.set_fact: - loop_ip: "{{ csvline[0] }}" - int_ip: "{{ csvline[1] }}" - int_mask: "{{ csvline[2] }}" - int_name: "{{ csvline[3] }}" - local_as: "{{ csvline[4] }}" - neighbor_as: "{{ csvline[5] }}" - neigh_int_ip: "{{ csvline[6] }}" + '{{ columns[item|int] }}': "{{ csvline }}" vars: - csvline: "{{ lookup('ansible.builtin.csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',') }}" + csvline: "{{ lookup('csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',', col=item) }}" + columns: ['loop_ip', 'int_ip', 'int_mask', 'int_name', 'local_as', 'neighbour_as', 'neight_int_ip'] + bgp_neighbor_ip: '127.0.0.1' + loop: '{{ range(columns|length|int) }}' delegate_to: localhost + delegate_facts: true + +# Contents of people.csv +# # Last,First,Email,Extension +# Smith,Jane,jsmith@example.com,1234 + +- name: Specify the column (by keycol) in which the string should be searched + assert: + that: + - lookup('ansible.builtin.csvfile', 'Jane', file='people.csv', delimiter=',', col=0, keycol=1) == "Smith" """ RETURN = """ diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index 77a03389ed9..a42e5ce14ba 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -85,26 +85,26 @@ def generate_ansible_template_vars(path, fullpath=None, dest_path=None): template_uid = os.stat(b_path).st_uid temp_vars = { - 'template_host': to_text(os.uname()[1]), - 'template_path': path, + 'template_host': to_unsafe_text(os.uname()[1]), + 'template_path': to_unsafe_text(path), 'template_mtime': datetime.datetime.fromtimestamp(os.path.getmtime(b_path)), - 'template_uid': to_text(template_uid), + 'template_uid': to_unsafe_text(template_uid), 'template_run_date': datetime.datetime.now(), - 'template_destpath': to_native(dest_path) if dest_path else None, + 'template_destpath': wrap_var(to_native(dest_path)) if dest_path else None, } if fullpath is None: - temp_vars['template_fullpath'] = os.path.abspath(path) + temp_vars['template_fullpath'] = wrap_var(os.path.abspath(path)) else: - temp_vars['template_fullpath'] = fullpath + temp_vars['template_fullpath'] = wrap_var(fullpath) managed_default = C.DEFAULT_MANAGED_STR managed_str = managed_default.format( - host=temp_vars['template_host'], - uid=temp_vars['template_uid'], - file=temp_vars['template_path'].replace('%', '%%'), + host="{{ template_host }}", + uid="{{ template_uid }}", + file="{{ template_path }}" ) - temp_vars['ansible_managed'] = to_unsafe_text(time.strftime(to_native(managed_str), time.localtime(os.path.getmtime(b_path)))) + temp_vars['ansible_managed'] = time.strftime(to_native(managed_str), time.localtime(os.path.getmtime(b_path))) return temp_vars diff --git a/lib/ansible/utils/path.py b/lib/ansible/utils/path.py index ac0b450839f..2b25342ed4c 100644 --- a/lib/ansible/utils/path.py +++ b/lib/ansible/utils/path.py @@ -33,6 +33,9 @@ def unfrackpath(path, follow=True, basedir=None): :arg path: A byte or text string representing a path to be canonicalized :arg follow: A boolean to indicate of symlinks should be resolved or not + :arg basedir: A byte string, text string, PathLike object, or `None` + representing where a relative path should be resolved from. + `None` will be substituted for the current working directory. :raises UnicodeDecodeError: If the canonicalized version of the path contains non-utf8 byte sequences. :rtype: A text string (unicode on pyyhon2, str on python3). diff --git a/packaging/release.py b/packaging/release.py index 97c58a74248..95ee2c3dec9 100755 --- a/packaging/release.py +++ b/packaging/release.py @@ -856,7 +856,7 @@ def test_built_artifact(path: pathlib.Path) -> None: def get_sdist_path(version: Version, dist_dir: pathlib.Path = DIST_DIR) -> pathlib.Path: """Return the path to the sdist file.""" - return dist_dir / f"ansible-core-{version}.tar.gz" + return dist_dir / f"ansible_core-{version}.tar.gz" def get_wheel_path(version: Version, dist_dir: pathlib.Path = DIST_DIR) -> pathlib.Path: diff --git a/test/integration/targets/ansible-config/files/base_all_valid.cfg b/test/integration/targets/ansible-config/files/base_all_valid.cfg new file mode 100644 index 00000000000..f9c3fb16bb2 --- /dev/null +++ b/test/integration/targets/ansible-config/files/base_all_valid.cfg @@ -0,0 +1,5 @@ +[defaults] +cow_selection=random + +[ssh_connection] +control_path=/var/tmp diff --git a/test/integration/targets/ansible-config/files/base_valid.cfg b/test/integration/targets/ansible-config/files/base_valid.cfg new file mode 100644 index 00000000000..f5c203237eb --- /dev/null +++ b/test/integration/targets/ansible-config/files/base_valid.cfg @@ -0,0 +1,2 @@ +[defaults] +cow_selection=random diff --git a/test/integration/targets/ansible-config/files/empty.cfg b/test/integration/targets/ansible-config/files/empty.cfg new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-config/files/invalid_base.cfg b/test/integration/targets/ansible-config/files/invalid_base.cfg new file mode 100644 index 00000000000..71bc2129947 --- /dev/null +++ b/test/integration/targets/ansible-config/files/invalid_base.cfg @@ -0,0 +1,2 @@ +[defaults] +cow_destruction=random diff --git a/test/integration/targets/ansible-config/files/invalid_plugins_config.ini b/test/integration/targets/ansible-config/files/invalid_plugins_config.ini new file mode 100644 index 00000000000..62baebed034 --- /dev/null +++ b/test/integration/targets/ansible-config/files/invalid_plugins_config.ini @@ -0,0 +1,2 @@ +[ssh_connection] +controller_road=/var/tmp diff --git a/test/integration/targets/ansible-config/tasks/main.yml b/test/integration/targets/ansible-config/tasks/main.yml index a894dd45cf5..7e7ba19070c 100644 --- a/test/integration/targets/ansible-config/tasks/main.yml +++ b/test/integration/targets/ansible-config/tasks/main.yml @@ -1,4 +1,4 @@ -- name: test ansible-config for valid output and no dupes +- name: test ansible-config init for valid output and no dupes block: - name: Create temporary file tempfile: @@ -12,3 +12,47 @@ - name: run ini tester, for correctness and dupes shell: "{{ansible_playbook_python}} '{{role_path}}/files/ini_dupes.py' '{{ini_tempfile.path}}'" + +- name: test ansible-config validate + block: + # not testing w/o -t all as ansible-test uses it's own plugins and would give false positives + - name: validate config files + shell: ansible-config validate -t all -v + register: valid_cfg + loop: + - empty.cfg + - base_valid.cfg + - base_all_valid.cfg + - invalid_base.cfg + - invalid_plugins_config.ini + ignore_errors: true + environment: + ANSIBLE_CONFIG: "{{role_path ~ '/files/' ~ item}}" + + - name: ensure expected cfg check results + assert: + that: + - valid_cfg['results'][0] is success + - valid_cfg['results'][1] is success + - valid_cfg['results'][2] is success + - valid_cfg['results'][3] is failed + - valid_cfg['results'][4] is failed + + - name: validate env vars + shell: ansible-config validate -t all -v -f env + register: valid_env + environment: + ANSIBLE_COW_SELECTION: 1 + + - name: validate env vars + shell: ansible-config validate -t all -v -f env + register: invalid_env + ignore_errors: true + environment: + ANSIBLE_COW_DESTRUCTION: 1 + + - name: ensure env check is what we expected + assert: + that: + - valid_env is success + - invalid_env is failed diff --git a/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/defaults/common_vars/subdir/group0/main.yml b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/defaults/common_vars/subdir/group0/main.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/defaults/main.yml b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/defaults/main.yml new file mode 120000 index 00000000000..97b8cc1d4af --- /dev/null +++ b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/defaults/main.yml @@ -0,0 +1 @@ +common_vars/subdir/group0/main.yml \ No newline at end of file diff --git a/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/handlers/utils.yml b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/handlers/utils.yml new file mode 120000 index 00000000000..3e177e12916 --- /dev/null +++ b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/handlers/utils.yml @@ -0,0 +1 @@ +../tasks/utils/suite.yml \ No newline at end of file diff --git a/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/meta/main.yml b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/meta/main.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/tasks/utils/suite.yml b/test/integration/targets/ansible-galaxy-role/files/safe-symlinks/tasks/utils/suite.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-galaxy-role/tasks/valid-role-symlinks.yml b/test/integration/targets/ansible-galaxy-role/tasks/valid-role-symlinks.yml index 8a60b2efcc8..deb544b0656 100644 --- a/test/integration/targets/ansible-galaxy-role/tasks/valid-role-symlinks.yml +++ b/test/integration/targets/ansible-galaxy-role/tasks/valid-role-symlinks.yml @@ -1,78 +1,38 @@ -- name: create test directories - file: - path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}' - state: directory - loop: - - source - - target - - roles - -- name: create subdir in the role content to test relative symlinks - file: - dest: '{{ remote_tmp_dir }}/dir-traversal/source/role_subdir' - state: directory - -- copy: - dest: '{{ remote_tmp_dir }}/dir-traversal/source/role_subdir/.keep' - content: '' - -- set_fact: - installed_roles: "{{ remote_tmp_dir | realpath }}/dir-traversal/roles" - -- name: build role with symlink to a directory in the role - script: - chdir: '{{ remote_tmp_dir }}/dir-traversal/source' - cmd: create-role-archive.py safe-link-dir.tar ./ role_subdir/.. - executable: '{{ ansible_playbook_python }}' - -- name: install role successfully - command: - cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles safe-link-dir.tar' - chdir: '{{ remote_tmp_dir }}/dir-traversal/source' - register: galaxy_install_ok - -- name: check for the directory symlink in the role - stat: - path: "{{ installed_roles }}/safe-link-dir.tar/symlink" - register: symlink_in_role - -- assert: - that: - - symlink_in_role.stat.exists - - symlink_in_role.stat.lnk_source == installed_roles + '/safe-link-dir.tar' - -- name: remove tarfile for next test - file: - path: '{{ remote_tmp_dir }}/dir-traversal/source/safe-link-dir.tar' - state: absent - -- name: build role with safe relative symlink - script: - chdir: '{{ remote_tmp_dir }}/dir-traversal/source' - cmd: create-role-archive.py safe.tar ./ role_subdir/../context.txt - executable: '{{ ansible_playbook_python }}' - -- name: install role successfully - command: - cmd: 'ansible-galaxy role install --roles-path {{ remote_tmp_dir }}/dir-traversal/roles safe.tar' - chdir: '{{ remote_tmp_dir }}/dir-traversal/source' - register: galaxy_install_ok - -- name: check for symlink in role - stat: - path: "{{ installed_roles }}/safe.tar/symlink" - register: symlink_in_role - -- assert: - that: - - symlink_in_role.stat.exists - - symlink_in_role.stat.lnk_source == installed_roles + '/safe.tar/context.txt' - -- name: remove test directories - file: - path: '{{ remote_tmp_dir }}/dir-traversal/{{ item }}' - state: absent - loop: - - source - - target - - roles +- delegate_to: localhost + block: + - name: Create archive + command: "tar -cf safe-symlinks.tar {{ role_path }}/files/safe-symlinks" + args: + chdir: "{{ remote_tmp_dir }}" + + - name: Install role successfully + command: ansible-galaxy role install --roles-path '{{ remote_tmp_dir }}/roles' safe-symlinks.tar + args: + chdir: "{{ remote_tmp_dir }}" + + - name: Validate each of the symlinks exists + stat: + path: "{{ remote_tmp_dir }}/roles/safe-symlinks.tar/{{ item }}" + loop: + - defaults/main.yml + - handlers/utils.yml + register: symlink_stat + + - assert: + that: + - symlink_stat.results[0].stat.exists + - symlink_stat.results[0].stat.lnk_source == ((dest, 'roles/safe-symlinks.tar/defaults/common_vars/subdir/group0/main.yml') | path_join) + - symlink_stat.results[1].stat.exists + - symlink_stat.results[1].stat.lnk_source == ((dest, 'roles/safe-symlinks.tar/tasks/utils/suite.yml') | path_join) + vars: + dest: "{{ remote_tmp_dir | realpath }}" + + always: + - name: Clean up + file: + path: "{{ item }}" + state: absent + delegate_to: localhost + loop: + - "{{ remote_tmp_dir }}/roles/" + - "{{ remote_tmp_dir }}/safe-symlinks.tar" diff --git a/test/integration/targets/dnf/tasks/repo.yml b/test/integration/targets/dnf/tasks/repo.yml index 3eed448955b..7e34aede3aa 100644 --- a/test/integration/targets/dnf/tasks/repo.yml +++ b/test/integration/targets/dnf/tasks/repo.yml @@ -448,7 +448,7 @@ - present - dnf: - name: /foo.so + name: /foo.gif state: present register: dnf_result diff --git a/test/integration/targets/git/tasks/setup-local-repos.yml b/test/integration/targets/git/tasks/setup-local-repos.yml index 4626f1028bf..3723f4dda4c 100644 --- a/test/integration/targets/git/tasks/setup-local-repos.yml +++ b/test/integration/targets/git/tasks/setup-local-repos.yml @@ -56,6 +56,16 @@ args: chdir: "{{ repo_dir }}/shallow_branches" +- name: SETUP-LOCAL-REPOS | get ref head for test_branch + shell: git checkout test_branch && git rev-parse HEAD + args: + chdir: "{{ repo_dir }}/shallow_branches" + register: ref_head_id + +- name: SETUP-LOCAL-REPOS | store ref head for test_branch + set_fact: + test_branch_ref_head_id: "{{ ref_head_id.stdout }}" + # Make this a bare one, we need to be able to push to it from clones # We make the repo here for consistency with the other repos, # but we finish setting it up in forcefully-fetch-tag.yml. diff --git a/test/integration/targets/git/tasks/specific-revision.yml b/test/integration/targets/git/tasks/specific-revision.yml index f1fe41d5626..ffb0a4ed51f 100644 --- a/test/integration/targets/git/tasks/specific-revision.yml +++ b/test/integration/targets/git/tasks/specific-revision.yml @@ -70,11 +70,11 @@ # Same as the previous test, but this time we specify which ref # contains the SHA1 - name: SPECIFIC-REVISION | update to revision by specifying the refspec - git: - repo: https://github.com/ansible/ansible-examples.git + git: &git_ref_spec + repo: "{{ repo_dir }}/shallow_branches/.git" dest: '{{ checkout_dir }}' - version: 5473e343e33255f2da0b160f53135c56921d875c - refspec: refs/pull/7/merge + version: "{{ test_branch_ref_head_id }}" + refspec: refs/heads/test_branch - name: SPECIFIC-REVISION | check HEAD after update with refspec command: git rev-parse HEAD @@ -84,7 +84,7 @@ - assert: that: - - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"' + - 'git_result.stdout == test_branch_ref_head_id' # try out combination of refspec and depth - name: SPECIFIC-REVISION | clear checkout_dir @@ -94,11 +94,8 @@ - name: SPECIFIC-REVISION | update to revision by specifying the refspec with depth=1 git: - repo: https://github.com/ansible/ansible-examples.git - dest: '{{ checkout_dir }}' - version: 5473e343e33255f2da0b160f53135c56921d875c - refspec: refs/pull/7/merge depth: 1 + <<: *git_ref_spec - name: SPECIFIC-REVISION | check HEAD after update with refspec command: git rev-parse HEAD @@ -108,7 +105,7 @@ - assert: that: - - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"' + - 'git_result.stdout == test_branch_ref_head_id' - name: SPECIFIC-REVISION | try to access other commit shell: git checkout 0ce1096 @@ -130,11 +127,7 @@ path: "{{ checkout_dir }}" - name: SPECIFIC-REVISION | clone to revision by specifying the refspec - git: - repo: https://github.com/ansible/ansible-examples.git - dest: "{{ checkout_dir }}" - version: 5473e343e33255f2da0b160f53135c56921d875c - refspec: refs/pull/7/merge + git: *git_ref_spec - name: SPECIFIC-REVISION | check HEAD after update with refspec command: git rev-parse HEAD @@ -144,7 +137,7 @@ - assert: that: - - 'git_result.stdout == "5473e343e33255f2da0b160f53135c56921d875c"' + - 'git_result.stdout == test_branch_ref_head_id' # Test that a forced shallow checkout referincing branch only always fetches latest head diff --git a/test/integration/targets/handlers/handlers_lockstep_82307.yml b/test/integration/targets/handlers/handlers_lockstep_82307.yml new file mode 100644 index 00000000000..f7cf757a259 --- /dev/null +++ b/test/integration/targets/handlers/handlers_lockstep_82307.yml @@ -0,0 +1,25 @@ +- hosts: A,B + gather_facts: false + tasks: + - block: + - command: echo + notify: + - handler1 + - handler2 + + - fail: + when: inventory_hostname == "B" + + - meta: flush_handlers + always: + - name: always + debug: + msg: always + handlers: + - name: handler1 + debug: + msg: handler1 + + - name: handler2 + debug: + msg: handler2 diff --git a/test/integration/targets/handlers/runme.sh b/test/integration/targets/handlers/runme.sh index 1381878c4e2..9250fc8fb34 100755 --- a/test/integration/targets/handlers/runme.sh +++ b/test/integration/targets/handlers/runme.sh @@ -216,3 +216,6 @@ ansible-playbook nested_flush_handlers_failure_force.yml -i inventory.handlers " ansible-playbook 82241.yml -i inventory.handlers "$@" 2>&1 | tee out.txt [ "$(grep out.txt -ce 'included_task_from_tasks_dir')" = "1" ] + +ansible-playbook handlers_lockstep_82307.yml -i inventory.handlers "$@" 2>&1 | tee out.txt +[ "$(grep out.txt -ce 'TASK \[handler2\]')" = "0" ] diff --git a/test/integration/targets/setup_rpm_repo/library/create_repo.py b/test/integration/targets/setup_rpm_repo/library/create_repo.py index a07d8df657d..7424ea5d6cc 100644 --- a/test/integration/targets/setup_rpm_repo/library/create_repo.py +++ b/test/integration/targets/setup_rpm_repo/library/create_repo.py @@ -12,11 +12,11 @@ from ansible.module_utils.common.respawn import has_respawned, probe_interpreter HAS_RPMFLUFF = True can_use_rpm_weak_deps = None try: - from rpmfluff import SimpleRpmBuild, GeneratedSourceFile, make_elf + from rpmfluff import SimpleRpmBuild, GeneratedSourceFile, make_gif from rpmfluff import YumRepoBuild except ImportError: try: - from rpmfluff.make import make_elf + from rpmfluff.make import make_gif from rpmfluff.sourcefile import GeneratedSourceFile from rpmfluff.rpmbuild import SimpleRpmBuild from rpmfluff.yumrepobuild import YumRepoBuild @@ -47,8 +47,8 @@ SPECS = [ RPM('dinginessentail-with-weak-dep', '1.0', '1', None, ['dinginessentail-weak-dep'], None, None), RPM('dinginessentail-weak-dep', '1.0', '1', None, None, None, None), RPM('noarchfake', '1.0', '1', None, None, None, 'noarch'), - RPM('provides_foo_a', '1.0', '1', None, None, 'foo.so', 'noarch'), - RPM('provides_foo_b', '1.0', '1', None, None, 'foo.so', 'noarch'), + RPM('provides_foo_a', '1.0', '1', None, None, 'foo.gif', 'noarch'), + RPM('provides_foo_b', '1.0', '1', None, None, 'foo.gif', 'noarch'), RPM('number-11-name', '11.0', '1', None, None, None, None), RPM('number-11-name', '11.1', '1', None, None, None, None), RPM('epochone', '1.0', '1', '1', None, None, "noarch"), @@ -74,7 +74,7 @@ def create_repo(arch='x86_64'): pkg.add_installed_file( "/" + spec.file, GeneratedSourceFile( - spec.file, make_elf() + spec.file, make_gif() ) ) diff --git a/test/integration/targets/systemd/handlers/main.yml b/test/integration/targets/systemd/handlers/main.yml index 11053b7cb30..f0df2bb9e77 100644 --- a/test/integration/targets/systemd/handlers/main.yml +++ b/test/integration/targets/systemd/handlers/main.yml @@ -15,3 +15,8 @@ file: path: /etc/systemd/system/baz.service state: absent + +- name: remove mask unit file + file: + path: /etc/systemd/system/mask_me.service + state: absent diff --git a/test/integration/targets/systemd/tasks/main.yml b/test/integration/targets/systemd/tasks/main.yml index 4dc5d124402..9444c13d2af 100644 --- a/test/integration/targets/systemd/tasks/main.yml +++ b/test/integration/targets/systemd/tasks/main.yml @@ -121,3 +121,5 @@ - import_tasks: test_unit_template.yml - import_tasks: test_indirect_service.yml - import_tasks: test_enabled_runtime.yml +- import_tasks: test_systemd_version.yml +- import_tasks: test_mask.yml diff --git a/test/integration/targets/systemd/tasks/test_mask.yml b/test/integration/targets/systemd/tasks/test_mask.yml new file mode 100644 index 00000000000..1ab583c58a9 --- /dev/null +++ b/test/integration/targets/systemd/tasks/test_mask.yml @@ -0,0 +1,25 @@ +- name: Copy service file for mask operation + template: + src: mask_me.service + dest: /etc/systemd/system/mask_me.service + owner: root + group: root + mode: '0644' + notify: remove unit file + +- name: Reload systemd + systemd: + daemon_reload: true + +- name: Try to mask already masked service + systemd: + name: mask_me.service + masked: true + register: mask_test_1 + ignore_errors: true + +- name: Test mask service test + assert: + that: + - mask_test_1 is not changed + - "'Failed to mask' in mask_test_1.msg" diff --git a/test/integration/targets/systemd/tasks/test_systemd_version.yml b/test/integration/targets/systemd/tasks/test_systemd_version.yml new file mode 100644 index 00000000000..2b2fae167c8 --- /dev/null +++ b/test/integration/targets/systemd/tasks/test_systemd_version.yml @@ -0,0 +1,11 @@ +--- +- name: Show Gathered Facts + ansible.builtin.debug: + msg: "{{ ansible_systemd }}" + +- name: Assert the systemd version fact + ansible.builtin.assert: + that: + - ansible_systemd.version | int + - ansible_systemd.version is match('^[1-9][0-9][0-9]$') + - ansible_systemd.features | regex_search('(\\+|-)(PAM|AUDIT)') diff --git a/test/integration/targets/systemd/templates/mask_me.service b/test/integration/targets/systemd/templates/mask_me.service new file mode 100644 index 00000000000..be858c74f1e --- /dev/null +++ b/test/integration/targets/systemd/templates/mask_me.service @@ -0,0 +1,9 @@ +[Unit] +Description=Mask Me Server +Documentation=Mask + +[Service] +ExecStart=/bin/yes + +[Install] +WantedBy=default.target diff --git a/test/integration/targets/template/ansible_managed.yml b/test/integration/targets/template/ansible_managed.yml index 2bd7c2c4f7d..a94e57efbcc 100644 --- a/test/integration/targets/template/ansible_managed.yml +++ b/test/integration/targets/template/ansible_managed.yml @@ -2,13 +2,51 @@ - hosts: testhost gather_facts: False tasks: - - set_fact: + - name: set output_dir + set_fact: output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}" - - file: - path: '{{ output_dir }}/café.txt' - state: 'absent' - # Smoketest that ansible_managed with non-ascii chars works: - # https://github.com/ansible/ansible/issues/27262 - - template: - src: 'templates/café.j2' - dest: '{{ output_dir }}/café.txt' + tags: ['always'] + + - name: Smoketest that ansible_managed with non-ascii chars works, https://github.com/ansible/ansible/issues/27262 + tags: ['27262'] + block: + - name: ensure output file does not exist + file: + path: '{{ output_dir }}/café.txt' + state: 'absent' + + - name: test templating with unicode in template name + template: + src: 'templates/café.j2' + dest: '{{ output_dir }}/café.txt' + + always: + - name: clean up! + file: + path: '{{ output_dir }}/café.txt' + state: 'absent' + + - name: check strftime resolution in ansible_managed, https://github.com/ansible/ansible/pull/79129 + tags: ['79129'] + block: + - template: + src: "templates/%necho Onii-chan help Im stuck;exit 1%n.j2" + dest: "{{ output_dir }}/strftime.sh" + mode: '0755' + + - shell: "exec {{ output_dir | quote }}/strftime.sh" + + - name: Avoid templating 'injections' via file names + template: + src: !unsafe "templates/completely{{ 1 % 0 }} safe template.j2" + dest: "{{ output_dir }}/jinja.sh" + mode: '0755' + + - shell: "exec {{ output_dir | quote }}/jinja.sh" + register: result + + - assert: + that: + - "'Hello' in result.stdout" + - "'uname' not in lookup('file', output_dir ~ '/strftime.sh')" + - "'uname' not in lookup('file', output_dir ~ '/jinja.sh')" diff --git a/test/integration/targets/template/ansible_managed_79129.yml b/test/integration/targets/template/ansible_managed_79129.yml deleted file mode 100644 index e00ada8c10a..00000000000 --- a/test/integration/targets/template/ansible_managed_79129.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -- hosts: testhost - gather_facts: false - tasks: - - set_fact: - output_dir: "{{ lookup('env', 'OUTPUT_DIR') }}" - - - name: check strftime - block: - - template: - src: "templates/%necho Onii-chan help Im stuck;exit 1%n.j2" - dest: "{{ output_dir }}/79129-strftime.sh" - mode: '0755' - - - shell: "exec {{ output_dir | quote }}/79129-strftime.sh" - - - name: check jinja template - block: - - template: - src: !unsafe "templates/completely{{ 1 % 0 }} safe template.j2" - dest: "{{ output_dir }}/79129-jinja.sh" - mode: '0755' - - - shell: "exec {{ output_dir | quote }}/79129-jinja.sh" - register: result - - - assert: - that: - - "'Hello' in result.stdout" diff --git a/test/integration/targets/template/ansible_managed_templated.cfg b/test/integration/targets/template/ansible_managed_templated.cfg new file mode 100644 index 00000000000..44e1fd8269f --- /dev/null +++ b/test/integration/targets/template/ansible_managed_templated.cfg @@ -0,0 +1,2 @@ +[defaults] +ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}({{{{q('pipe', 'uname -a')}}}}) diff --git a/test/integration/targets/template/runme.sh b/test/integration/targets/template/runme.sh index d3913d971a2..e8141104bef 100755 --- a/test/integration/targets/template/runme.sh +++ b/test/integration/targets/template/runme.sh @@ -7,11 +7,11 @@ ANSIBLE_ROLES_PATH=../ ansible-playbook template.yml -i ../../inventory -v "$@" # Test for https://github.com/ansible/ansible/pull/35571 ansible testhost -i testhost, -m debug -a 'msg={{ hostvars["localhost"] }}' -e "vars1={{ undef() }}" -e "vars2={{ vars1 }}" -# Test for https://github.com/ansible/ansible/issues/27262 +# ansible_managed tests ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed.yml -i ../../inventory -v "$@" -# Test for https://github.com/ansible/ansible/pull/79129 -ANSIBLE_CONFIG=ansible_managed.cfg ansible-playbook ansible_managed_79129.yml -i ../../inventory -v "$@" +# same as above but with ansible_managed j2 template +ANSIBLE_CONFIG=ansible_managed_templated.cfg ansible-playbook ansible_managed.yml -i ../../inventory -v "$@" # Test for #42585 ANSIBLE_ROLES_PATH=../ ansible-playbook custom_template.yml -i ../../inventory -v "$@" diff --git a/test/integration/targets/yum_repository/tasks/main.yml b/test/integration/targets/yum_repository/tasks/main.yml index 7813af06ef2..5b50d7dfc1a 100644 --- a/test/integration/targets/yum_repository/tasks/main.yml +++ b/test/integration/targets/yum_repository/tasks/main.yml @@ -97,6 +97,7 @@ baseurl: "{{ yum_repository_test_repo.baseurl }}" description: New description async: no + countme: yes enablegroups: no file: "{{ yum_repository_test_repo.name ~ 2 }}" ip_resolve: 4 @@ -114,6 +115,7 @@ that: - "'async = 0' in repo_file_contents" - "'name = New description' in repo_file_contents" + - "'countme = 1' in repo_file_contents" - "'enablegroups = 0' in repo_file_contents" - "'ip_resolve = 4' in repo_file_contents" - "'keepalive = 0' in repo_file_contents" @@ -127,6 +129,7 @@ baseurl: "{{ yum_repository_test_repo.baseurl }}" description: New description async: no + countme: yes enablegroups: no file: "{{ yum_repository_test_repo.name ~ 2 }}" ip_resolve: 4 diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini index 0a8a3e6b40f..0251f674b51 100644 --- a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini @@ -58,9 +58,6 @@ ignore_missing_imports = True [mypy-dnf.*] ignore_missing_imports = True -[mypy-libdnf.*] -ignore_missing_imports = True - [mypy-apt.*] ignore_missing_imports = True diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini index ddabecdabb6..b4e7b05eb9f 100644 --- a/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini @@ -25,9 +25,6 @@ ignore_missing_imports = True [mypy-dnf.*] ignore_missing_imports = True -[mypy-libdnf.*] -ignore_missing_imports = True - [mypy-apt.*] ignore_missing_imports = True diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index cab676db420..dc8fd0ee3f0 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -15,7 +15,6 @@ lib/ansible/parsing/yaml/constructor.py mypy-3.12:type-var # too many occurrenc lib/ansible/keyword_desc.yml no-unwanted-files lib/ansible/modules/apt.py validate-modules:parameter-invalid lib/ansible/modules/apt_repository.py validate-modules:parameter-invalid -lib/ansible/modules/assemble.py validate-modules:nonexistent-parameter-documented lib/ansible/modules/async_status.py validate-modules!skip lib/ansible/modules/async_wrapper.py ansible-doc!skip # not an actual module lib/ansible/modules/async_wrapper.py pylint:ansible-bad-function # ignore, required @@ -56,7 +55,6 @@ lib/ansible/module_utils/compat/selinux.py import-3.11!skip # pass/fail depends lib/ansible/module_utils/compat/selinux.py import-3.12!skip # pass/fail depends on presence of libselinux.so lib/ansible/module_utils/compat/selinux.py pylint:unidiomatic-typecheck lib/ansible/module_utils/distro/_distro.py no-assert -lib/ansible/module_utils/distro/_distro.py pep8!skip # bundled code we don't want to modify lib/ansible/module_utils/distro/__init__.py empty-init # breaks namespacing, bundled, do not override lib/ansible/module_utils/facts/__init__.py empty-init # breaks namespacing, deprecate and eventually remove lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 pslint:PSUseApprovedVerbs