diff options
39 files changed, 402 insertions, 127 deletions
@@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ansible-core -Version: 2.14.7 +Version: 2.14.8 Summary: Radically simple IT automation Home-page: https://ansible.com/ Author: Ansible, Inc. diff --git a/changelogs/CHANGELOG-v2.14.rst b/changelogs/CHANGELOG-v2.14.rst index 0c912e3b..4f4df76b 100644 --- a/changelogs/CHANGELOG-v2.14.rst +++ b/changelogs/CHANGELOG-v2.14.rst @@ -5,6 +5,28 @@ ansible-core 2.14 "C'mon Everybody" Release Notes .. contents:: Topics +v2.14.8 +======= + +Release Summary +--------------- + +| Release Date: 2023-07-18 +| `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__ + + +Minor Changes +------------- + +- Cache field attributes list on the playbook classes +- Playbook objects - Replace deprecated stacked ``@classmethod`` and ``@property`` +- ansible-test - Use a context manager to perform cleanup at exit instead of using the built-in ``atexit`` module. + +Bugfixes +-------- + +- ansible-galaxy - Fix issue installing collections containing directories with more than 100 characters on python versions before 3.10.6 + v2.14.7 ======= diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index f0137291..1024e9f8 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -1342,3 +1342,37 @@ releases: - man-page-build-docs-dependency.yml - update-maybe-json-uri.yml release_date: '2023-06-12' + 2.14.8: + changes: + release_summary: '| Release Date: 2023-07-18 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__ + + ' + codename: C'mon Everybody + fragments: + - 2.14.8_summary.yaml + release_date: '2023-07-17' + 2.14.8rc1: + changes: + bugfixes: + - ansible-galaxy - Fix issue installing collections containing directories with + more than 100 characters on python versions before 3.10.6 + minor_changes: + - Cache field attributes list on the playbook classes + - Playbook objects - Replace deprecated stacked ``@classmethod`` and ``@property`` + - ansible-test - Use a context manager to perform cleanup at exit instead of + using the built-in ``atexit`` module. + release_summary: '| Release Date: 2023-07-10 + + | `Porting Guide <https://docs.ansible.com/ansible-core/2.14/porting_guides/porting_guide_core_2.14.html>`__ + + ' + codename: C'mon Everybody + fragments: + - 2.14.8rc1_summary.yaml + - ansible-test-atexit.yml + - cache-fa-on-pb-cls.yml + - long-collection-paths-fix.yml + - no-stacked-descriptors.yaml + release_date: '2023-07-10' diff --git a/docs/man/man1/ansible-config.1 b/docs/man/man1/ansible-config.1 index 009cb4ef..53983058 100644 --- a/docs/man/man1/ansible-config.1 +++ b/docs/man/man1/ansible-config.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-CONFIG" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-CONFIG" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-config \- View ansible configuration. .SH SYNOPSIS diff --git a/docs/man/man1/ansible-console.1 b/docs/man/man1/ansible-console.1 index 9ee48cfe..b7621fe6 100644 --- a/docs/man/man1/ansible-console.1 +++ b/docs/man/man1/ansible-console.1 @@ -27,17 +27,15 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-CONSOLE" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-CONSOLE" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-console \- REPL console for executing Ansible tasks. .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible\-console [\-h] [\-\-version] [\-v] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-i INVENTORY] -[\-\-list\-hosts] [\-l SUBSET] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] -[\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] [\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] -[\-C] [\-D] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] -[\-e EXTRA_VARS] [\-\-task\-timeout TASK_TIMEOUT] [\-\-step] +.B usage: ansible\-console [\-h] [\-\-version] [\-v] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-i INVENTORY] [\-\-list\-hosts] [\-l SUBSET] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] +[\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] [\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] [\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-C] +[\-D] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] [\-e EXTRA_VARS] [\-\-task\-timeout TASK_TIMEOUT] [\-\-step] [pattern] .UNINDENT .SH DESCRIPTION @@ -52,7 +50,7 @@ runtime: .IP \(bu 2 \fIcd [pattern]\fP: change host/group (you can use host patterns eg.: .UNINDENT -.IP "System Message: WARNING/2 (:, line 35)" +.IP "System Message: WARNING/2 (:, line 33)" Bullet list ends without a blank line; unexpected unindent. .sp app*.dc*:!app01*) diff --git a/docs/man/man1/ansible-doc.1 b/docs/man/man1/ansible-doc.1 index fd92c60b..8e96a0a2 100644 --- a/docs/man/man1/ansible-doc.1 +++ b/docs/man/man1/ansible-doc.1 @@ -27,14 +27,13 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-DOC" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-DOC" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-doc \- plugin documentation tool .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible\-doc [\-h] [\-\-version] [\-v] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] -[\-t {become,cache,callback,cliconf,connection,httpapi,inventory,lookup,netconf,shell,vars,module,strategy,test,filter,role,keyword}] [\-j] [\-r ROLES_PATH] +.B usage: ansible\-doc [\-h] [\-\-version] [\-v] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] [\-t {become,cache,callback,cliconf,connection,httpapi,inventory,lookup,netconf,shell,vars,module,strategy,test,filter,role,keyword}] [\-j] [\-r ROLES_PATH] [\-e ENTRY_POINT | \-s | \-F | \-l | \-\-metadata\-dump] [\-\-no\-fail\-on\-errors] [plugin ...] .UNINDENT diff --git a/docs/man/man1/ansible-galaxy.1 b/docs/man/man1/ansible-galaxy.1 index 8ed2713a..cc56ba24 100644 --- a/docs/man/man1/ansible-galaxy.1 +++ b/docs/man/man1/ansible-galaxy.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-GALAXY" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-GALAXY" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-galaxy \- Perform various Role and Collection related operations. .SH SYNOPSIS diff --git a/docs/man/man1/ansible-inventory.1 b/docs/man/man1/ansible-inventory.1 index eaff6ae0..6446b136 100644 --- a/docs/man/man1/ansible-inventory.1 +++ b/docs/man/man1/ansible-inventory.1 @@ -27,14 +27,14 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-INVENTORY" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-INVENTORY" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-inventory \- None .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible\-inventory [\-h] [\-\-version] [\-v] [\-i INVENTORY] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-\-playbook\-dir BASEDIR] -[\-e EXTRA_VARS] [\-\-list] [\-\-host HOST] [\-\-graph] [\-y] [\-\-toml] [\-\-vars] [\-\-export] [\-\-output OUTPUT_FILE] +.B usage: ansible\-inventory [\-h] [\-\-version] [\-v] [\-i INVENTORY] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-\-playbook\-dir BASEDIR] [\-e EXTRA_VARS] [\-\-list] [\-\-host HOST] [\-\-graph] [\-y] [\-\-toml] [\-\-vars] +[\-\-export] [\-\-output OUTPUT_FILE] [host|group] .UNINDENT .SH DESCRIPTION diff --git a/docs/man/man1/ansible-playbook.1 b/docs/man/man1/ansible-playbook.1 index 412e1ace..7e41c477 100644 --- a/docs/man/man1/ansible-playbook.1 +++ b/docs/man/man1/ansible-playbook.1 @@ -27,18 +27,16 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-PLAYBOOK" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-PLAYBOOK" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-playbook \- Runs Ansible playbooks, executing the defined tasks on the targeted hosts. .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible\-playbook [\-h] [\-\-version] [\-v] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] -[\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] -[\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-\-force\-handlers] [\-\-flush\-cache] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] -[\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-t TAGS] [\-\-skip\-tags SKIP_TAGS] [\-C] [\-D] [\-i INVENTORY] [\-\-list\-hosts] [\-l SUBSET] [\-e EXTRA_VARS] -[\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-syntax\-check] [\-\-list\-tasks] -[\-\-list\-tags] [\-\-step] [\-\-start\-at\-task START_AT_TASK] +.B usage: ansible\-playbook [\-h] [\-\-version] [\-v] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] [\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] +[\-\-ssh\-extra\-args SSH_EXTRA_ARGS] [\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-\-force\-handlers] [\-\-flush\-cache] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] +[\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-t TAGS] [\-\-skip\-tags SKIP_TAGS] [\-C] [\-D] [\-i INVENTORY] [\-\-list\-hosts] [\-l SUBSET] [\-e EXTRA_VARS] [\-\-vault\-id VAULT_IDS] +[\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-syntax\-check] [\-\-list\-tasks] [\-\-list\-tags] [\-\-step] [\-\-start\-at\-task START_AT_TASK] playbook [playbook ...] .UNINDENT .SH DESCRIPTION diff --git a/docs/man/man1/ansible-pull.1 b/docs/man/man1/ansible-pull.1 index 46b6099b..0c73a23b 100644 --- a/docs/man/man1/ansible-pull.1 +++ b/docs/man/man1/ansible-pull.1 @@ -27,17 +27,16 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-PULL" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-PULL" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-pull \- pulls playbooks from a VCS repo and executes them for the local host .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible\-pull [\-h] [\-\-version] [\-v] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] -[\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] [\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] -[\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-e EXTRA_VARS] [\-t TAGS] [\-\-skip\-tags SKIP_TAGS] [\-i INVENTORY] -[\-\-list\-hosts] [\-l SUBSET] [\-M MODULE_PATH] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-\-purge] [\-o] [\-s SLEEP] [\-f] [\-d DEST] [\-U URL] [\-\-full] -[\-C CHECKOUT] [\-\-accept\-host\-key] [\-m MODULE_NAME] [\-\-verify\-commit] [\-\-clean] [\-\-track\-subs] [\-\-check] [\-\-diff] +.B usage: ansible\-pull [\-h] [\-\-version] [\-v] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] [\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] +[\-\-ssh\-extra\-args SSH_EXTRA_ARGS] [\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-e EXTRA_VARS] [\-t TAGS] [\-\-skip\-tags SKIP_TAGS] +[\-i INVENTORY] [\-\-list\-hosts] [\-l SUBSET] [\-M MODULE_PATH] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-\-purge] [\-o] [\-s SLEEP] [\-f] [\-d DEST] [\-U URL] [\-\-full] [\-C CHECKOUT] [\-\-accept\-host\-key] [\-m MODULE_NAME] [\-\-verify\-commit] +[\-\-clean] [\-\-track\-subs] [\-\-check] [\-\-diff] [playbook.yml ...] .UNINDENT .SH DESCRIPTION diff --git a/docs/man/man1/ansible-vault.1 b/docs/man/man1/ansible-vault.1 index 6bd3785b..b4478547 100644 --- a/docs/man/man1/ansible-vault.1 +++ b/docs/man/man1/ansible-vault.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE-VAULT" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE-VAULT" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible-vault \- encryption/decryption utility for Ansible data files .SH SYNOPSIS diff --git a/docs/man/man1/ansible.1 b/docs/man/man1/ansible.1 index 51295aff..4b59da05 100644 --- a/docs/man/man1/ansible.1 +++ b/docs/man/man1/ansible.1 @@ -27,18 +27,16 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "ANSIBLE" 1 "" "Ansible 2.14.7" "System administration commands" +.TH "ANSIBLE" 1 "" "Ansible 2.14.8" "System administration commands" .SH NAME ansible \- Define and run a single task 'playbook' against a set of hosts .SH SYNOPSIS .INDENT 0.0 .TP -.B usage: ansible [\-h] [\-\-version] [\-v] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-i INVENTORY] [\-\-list\-hosts] -[\-l SUBSET] [\-P POLL_INTERVAL] [\-B SECONDS] [\-o] [\-t TREE] [\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] -[\-\-ssh\-common\-args SSH_COMMON_ARGS] [\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] -[\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-C] [\-D] [\-e EXTRA_VARS] [\-\-vault\-id VAULT_IDS] -[\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] [\-\-task\-timeout TASK_TIMEOUT] [\-a MODULE_ARGS] -[\-m MODULE_NAME] +.B usage: ansible [\-h] [\-\-version] [\-v] [\-b] [\-\-become\-method BECOME_METHOD] [\-\-become\-user BECOME_USER] [\-K | \-\-become\-password\-file BECOME_PASSWORD_FILE] [\-i INVENTORY] [\-\-list\-hosts] [\-l SUBSET] [\-P POLL_INTERVAL] [\-B SECONDS] [\-o] [\-t TREE] +[\-\-private\-key PRIVATE_KEY_FILE] [\-u REMOTE_USER] [\-c CONNECTION] [\-T TIMEOUT] [\-\-ssh\-common\-args SSH_COMMON_ARGS] [\-\-sftp\-extra\-args SFTP_EXTRA_ARGS] [\-\-scp\-extra\-args SCP_EXTRA_ARGS] [\-\-ssh\-extra\-args SSH_EXTRA_ARGS] +[\-k | \-\-connection\-password\-file CONNECTION_PASSWORD_FILE] [\-C] [\-D] [\-e EXTRA_VARS] [\-\-vault\-id VAULT_IDS] [\-\-ask\-vault\-password | \-\-vault\-password\-file VAULT_PASSWORD_FILES] [\-f FORKS] [\-M MODULE_PATH] [\-\-playbook\-dir BASEDIR] +[\-\-task\-timeout TASK_TIMEOUT] [\-a MODULE_ARGS] [\-m MODULE_NAME] pattern .UNINDENT .SH DESCRIPTION diff --git a/lib/ansible/galaxy/collection/__init__.py b/lib/ansible/galaxy/collection/__init__.py index 23482665..75aec751 100644 --- a/lib/ansible/galaxy/collection/__init__.py +++ b/lib/ansible/galaxy/collection/__init__.py @@ -1516,6 +1516,13 @@ def install_artifact(b_coll_targz_path, b_collection_path, b_temp_path, signatur """ try: with tarfile.open(b_coll_targz_path, mode='r') as collection_tar: + # Remove this once py3.11 is our controller minimum + # Workaround for https://bugs.python.org/issue47231 + # See _extract_tar_dir + collection_tar._ansible_normalized_cache = { + m.name.removesuffix(os.path.sep): m for m in collection_tar.getmembers() + } # deprecated: description='TarFile member index' core_version='2.18' python_version='3.11' + # Verify the signature on the MANIFEST.json before extracting anything else _extract_tar_file(collection_tar, MANIFEST_FILENAME, b_collection_path, b_temp_path) @@ -1595,22 +1602,12 @@ def install_src(collection, b_collection_path, b_collection_output_path, artifac def _extract_tar_dir(tar, dirname, b_dest): """ Extracts a directory from a collection tar. """ - member_names = [to_native(dirname, errors='surrogate_or_strict')] - - # Create list of members with and without trailing separator - if not member_names[-1].endswith(os.path.sep): - member_names.append(member_names[-1] + os.path.sep) + dirname = to_native(dirname, errors='surrogate_or_strict').removesuffix(os.path.sep) - # Try all of the member names and stop on the first one that are able to successfully get - for member in member_names: - try: - tar_member = tar.getmember(member) - except KeyError: - continue - break - else: - # If we still can't find the member, raise a nice error. - raise AnsibleError("Unable to extract '%s' from collection" % to_native(member, errors='surrogate_or_strict')) + try: + tar_member = tar._ansible_normalized_cache[dirname] + except KeyError: + raise AnsibleError("Unable to extract '%s' from collection" % dirname) b_dir_path = os.path.join(b_dest, to_bytes(dirname, errors='surrogate_or_strict')) diff --git a/lib/ansible/module_utils/ansible_release.py b/lib/ansible/module_utils/ansible_release.py index 67de85c3..3c677c43 100644 --- a/lib/ansible/module_utils/ansible_release.py +++ b/lib/ansible/module_utils/ansible_release.py @@ -19,6 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -__version__ = '2.14.7' +__version__ = '2.14.8' __author__ = 'Ansible, Inc.' __codename__ = "C'mon Everybody" diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index 669aa0ad..c772df11 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -10,6 +10,7 @@ import operator import os from copy import copy as shallowcopy +from functools import cache from jinja2.exceptions import UndefinedError @@ -69,12 +70,21 @@ def _validate_action_group_metadata(action, found_group_metadata, fq_group_name) display.warning(" ".join(metadata_warnings)) +class _ClassProperty: + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, obj, objtype=None): + return getattr(objtype, f'_{self.name}')() + + class FieldAttributeBase: + fattributes = _ClassProperty() + @classmethod - @property - def fattributes(cls): - # FIXME is this worth caching? + @cache + def _fattributes(cls): fattributes = {} for class_obj in reversed(cls.__mro__): for name, attr in list(class_obj.__dict__.items()): diff --git a/lib/ansible/release.py b/lib/ansible/release.py index 67de85c3..3c677c43 100644 --- a/lib/ansible/release.py +++ b/lib/ansible/release.py @@ -19,6 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -__version__ = '2.14.7' +__version__ = '2.14.8' __author__ = 'Ansible, Inc.' __codename__ = "C'mon Everybody" diff --git a/lib/ansible_core.egg-info/PKG-INFO b/lib/ansible_core.egg-info/PKG-INFO index d2ff788a..4c212bb8 100644 --- a/lib/ansible_core.egg-info/PKG-INFO +++ b/lib/ansible_core.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ansible-core -Version: 2.14.7 +Version: 2.14.8 Summary: Radically simple IT automation Home-page: https://ansible.com/ Author: Ansible, Inc. diff --git a/lib/ansible_core.egg-info/SOURCES.txt b/lib/ansible_core.egg-info/SOURCES.txt index 4e770d7c..637c5cbf 100644 --- a/lib/ansible_core.egg-info/SOURCES.txt +++ b/lib/ansible_core.egg-info/SOURCES.txt @@ -1692,12 +1692,12 @@ test/integration/targets/ansible-test-sanity-validate-modules/ansible_collection test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml -test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst +test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps1 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml -test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst +test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm1 @@ -1708,7 +1708,7 @@ test/integration/targets/ansible-test-sanity-validate-modules/ansible_collection test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps1 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py -test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst +test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py @@ -3682,7 +3682,6 @@ test/integration/targets/reboot/tasks/main.yml test/integration/targets/reboot/tasks/test_invalid_parameter.yml test/integration/targets/reboot/tasks/test_invalid_test_command.yml test/integration/targets/reboot/tasks/test_molly_guard.yml -test/integration/targets/reboot/tasks/test_reboot_command.yml test/integration/targets/reboot/tasks/test_standard_scenarios.yml test/integration/targets/reboot/vars/main.yml test/integration/targets/register/aliases @@ -5279,6 +5278,7 @@ test/units/plugins/action/test_action.py test/units/plugins/action/test_gather_facts.py test/units/plugins/action/test_pause.py test/units/plugins/action/test_raw.py +test/units/plugins/action/test_reboot.py test/units/plugins/become/__init__.py test/units/plugins/become/conftest.py test/units/plugins/become/test_su.py diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md index bf1003fa..bf1003fa 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.md diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml index 3b116713..fcb47c6b 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml @@ -1,6 +1,6 @@ namespace: ns name: failure version: 1.0.0 -readme: README.rst +readme: README.md authors: - Ansible diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md index bbdd5138..bbdd5138 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.md diff --git a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml index 0a78b9e1..96fddb7f 100644 --- a/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml +++ b/test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml @@ -1,6 +1,6 @@ namespace: ns name: ps_only version: 1.0.0 -readme: README.rst +readme: README.md authors: - Ansible diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md index d8138d3b..d8138d3b 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.md diff --git a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml index 08a32e80..cb5403f1 100644 --- a/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml +++ b/test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml @@ -1,6 +1,6 @@ namespace: ns name: col version: 1.0.0 -readme: README.rst +readme: README.md authors: - Ansible diff --git a/test/integration/targets/dnf/tasks/main.yml b/test/integration/targets/dnf/tasks/main.yml index 66a171ac..65b77ceb 100644 --- a/test/integration/targets/dnf/tasks/main.yml +++ b/test/integration/targets/dnf/tasks/main.yml @@ -59,7 +59,6 @@ - include_tasks: modularity.yml when: - - astream_name is defined - (ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('29', '>=')) or (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('8', '>=')) tags: diff --git a/test/integration/targets/dnf/vars/RedHat-9.yml b/test/integration/targets/dnf/vars/RedHat-9.yml index 5681e701..680157dc 100644 --- a/test/integration/targets/dnf/vars/RedHat-9.yml +++ b/test/integration/targets/dnf/vars/RedHat-9.yml @@ -1,3 +1,2 @@ -# RHEL9.0 contains no modules, to be re-introduced in 9.1 -# astream_name: '@container-tools:latest/common' -# astream_name_no_stream: '@container-tools/common' +astream_name: '@php:8.1/minimal' +astream_name_no_stream: '@php/minimal' diff --git a/test/integration/targets/reboot/tasks/main.yml b/test/integration/targets/reboot/tasks/main.yml index 4884f104..e92c2662 100644 --- a/test/integration/targets/reboot/tasks/main.yml +++ b/test/integration/targets/reboot/tasks/main.yml @@ -37,7 +37,6 @@ when: not in_container_env and in_split_controller_mode block: - import_tasks: test_standard_scenarios.yml - - import_tasks: test_reboot_command.yml - import_tasks: test_invalid_parameter.yml - import_tasks: test_invalid_test_command.yml - import_tasks: test_molly_guard.yml diff --git a/test/integration/targets/reboot/tasks/test_reboot_command.yml b/test/integration/targets/reboot/tasks/test_reboot_command.yml deleted file mode 100644 index 779d380b..00000000 --- a/test/integration/targets/reboot/tasks/test_reboot_command.yml +++ /dev/null @@ -1,22 +0,0 @@ -- import_tasks: get_boot_time.yml -- name: Reboot with custom reboot_command using unqualified path - reboot: - reboot_command: reboot - register: reboot_result -- import_tasks: check_reboot.yml - - -- import_tasks: get_boot_time.yml -- name: Reboot with custom reboot_command using absolute path - reboot: - reboot_command: /sbin/reboot - register: reboot_result -- import_tasks: check_reboot.yml - - -- import_tasks: get_boot_time.yml -- name: Reboot with custom reboot_command with parameters - reboot: - reboot_command: shutdown -r now - register: reboot_result -- import_tasks: check_reboot.yml diff --git a/test/lib/ansible_test/_internal/__init__.py b/test/lib/ansible_test/_internal/__init__.py index ee24a852..35584746 100644 --- a/test/lib/ansible_test/_internal/__init__.py +++ b/test/lib/ansible_test/_internal/__init__.py @@ -43,6 +43,7 @@ from .data import ( from .util_common import ( CommonConfig, + ExitHandler, ) from .cli import ( @@ -59,6 +60,12 @@ from .config import ( def main(cli_args: t.Optional[list[str]] = None) -> None: + """Wrapper around the main program function to invoke cleanup functions at exit.""" + with ExitHandler.context(): + main_internal(cli_args) + + +def main_internal(cli_args: t.Optional[list[str]] = None) -> None: """Main program function.""" try: os.chdir(data_context().content.root) diff --git a/test/lib/ansible_test/_internal/commands/integration/cloud/__init__.py b/test/lib/ansible_test/_internal/commands/integration/cloud/__init__.py index eac9265a..cf920bc9 100644 --- a/test/lib/ansible_test/_internal/commands/integration/cloud/__init__.py +++ b/test/lib/ansible_test/_internal/commands/integration/cloud/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import abc -import atexit import datetime import os import re @@ -28,6 +27,7 @@ from ....util import ( ) from ....util_common import ( + ExitHandler, ResultType, write_json_test_results, ) @@ -306,7 +306,7 @@ class CloudProvider(CloudBase): self.resource_prefix = self.ci_provider.generate_resource_prefix() self.resource_prefix = re.sub(r'[^a-zA-Z0-9]+', '-', self.resource_prefix)[:63].lower().rstrip('-') - atexit.register(self.cleanup) + ExitHandler.register(self.cleanup) def cleanup(self) -> None: """Clean up the cloud resource and any temporary configuration files after tests complete.""" diff --git a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py index ab7dd93c..72616e77 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py +++ b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py @@ -1,7 +1,6 @@ """Sanity test using validate-modules.""" from __future__ import annotations -import atexit import collections import contextlib import json @@ -37,6 +36,7 @@ from ...util import ( ) from ...util_common import ( + ExitHandler, process_scoped_temporary_directory, run_command, ResultType, @@ -237,7 +237,7 @@ class ValidateModulesTest(SanitySingleVersion): files = payload_config.files files.append((path, os.path.relpath(path, data_context().content.root))) - atexit.register(cleanup) + ExitHandler.register(cleanup) data_context().register_payload_callback(git_callback) make_dirs(os.path.dirname(path)) diff --git a/test/lib/ansible_test/_internal/containers.py b/test/lib/ansible_test/_internal/containers.py index bfc36434..869f1fba 100644 --- a/test/lib/ansible_test/_internal/containers.py +++ b/test/lib/ansible_test/_internal/containers.py @@ -1,7 +1,6 @@ """High level functions for working with containers.""" from __future__ import annotations -import atexit import collections.abc as c import contextlib import enum @@ -20,6 +19,7 @@ from .util import ( ) from .util_common import ( + ExitHandler, named_temporary_file, ) @@ -225,7 +225,7 @@ def run_support_container( raise Exception(f'Container already defined: {name}') if not support_containers: - atexit.register(cleanup_containers, args) + ExitHandler.register(cleanup_containers, args) support_containers[name] = descriptor diff --git a/test/lib/ansible_test/_internal/coverage_util.py b/test/lib/ansible_test/_internal/coverage_util.py index 0af1cac4..ae640249 100644 --- a/test/lib/ansible_test/_internal/coverage_util.py +++ b/test/lib/ansible_test/_internal/coverage_util.py @@ -1,7 +1,6 @@ """Utility code for facilitating collection of code coverage when running tests.""" from __future__ import annotations -import atexit import dataclasses import os import sqlite3 @@ -34,6 +33,7 @@ from .data import ( ) from .util_common import ( + ExitHandler, intercept_python, ResultType, ) @@ -223,7 +223,7 @@ def get_coverage_config(args: TestConfig) -> str: temp_dir = '/tmp/coverage-temp-dir' else: temp_dir = tempfile.mkdtemp() - atexit.register(lambda: remove_tree(temp_dir)) + ExitHandler.register(lambda: remove_tree(temp_dir)) path = os.path.join(temp_dir, COVERAGE_CONFIG_NAME) diff --git a/test/lib/ansible_test/_internal/payload.py b/test/lib/ansible_test/_internal/payload.py index 10dde7b8..ab9739b4 100644 --- a/test/lib/ansible_test/_internal/payload.py +++ b/test/lib/ansible_test/_internal/payload.py @@ -1,7 +1,6 @@ """Payload management for sending Ansible files and test content to other systems (VMs, containers).""" from __future__ import annotations -import atexit import os import stat import tarfile @@ -32,6 +31,7 @@ from .data import ( from .util_common import ( CommonConfig, + ExitHandler, ) # improve performance by disabling uid/gid lookups @@ -192,7 +192,7 @@ def create_temporary_bin_files(args: CommonConfig) -> tuple[tuple[str, str], ... temp_path = '/tmp/ansible-tmp-bin' else: temp_path = tempfile.mkdtemp(prefix='ansible', suffix='bin') - atexit.register(remove_tree, temp_path) + ExitHandler.register(remove_tree, temp_path) for name, dest in ANSIBLE_BIN_SYMLINK_MAP.items(): path = os.path.join(temp_path, name) diff --git a/test/lib/ansible_test/_internal/provisioning.py b/test/lib/ansible_test/_internal/provisioning.py index e7f0fd31..4710757b 100644 --- a/test/lib/ansible_test/_internal/provisioning.py +++ b/test/lib/ansible_test/_internal/provisioning.py @@ -1,7 +1,6 @@ """Provision hosts for running tests.""" from __future__ import annotations -import atexit import collections.abc as c import dataclasses import functools @@ -27,6 +26,10 @@ from .util import ( type_guard, ) +from .util_common import ( + ExitHandler, +) + from .thread import ( WrappedThread, ) @@ -124,7 +127,7 @@ def prepare_profiles( raise PrimeContainers() - atexit.register(functools.partial(cleanup_profiles, host_state)) + ExitHandler.register(functools.partial(cleanup_profiles, host_state)) def provision(profile: HostProfile) -> None: """Provision the given profile.""" diff --git a/test/lib/ansible_test/_internal/pypi_proxy.py b/test/lib/ansible_test/_internal/pypi_proxy.py index 97663ead..5380dd9b 100644 --- a/test/lib/ansible_test/_internal/pypi_proxy.py +++ b/test/lib/ansible_test/_internal/pypi_proxy.py @@ -1,7 +1,6 @@ """PyPI proxy management.""" from __future__ import annotations -import atexit import os import urllib.parse @@ -23,6 +22,7 @@ from .util import ( ) from .util_common import ( + ExitHandler, process_scoped_temporary_file, ) @@ -128,7 +128,7 @@ def configure_target_pypi_proxy(args: EnvironmentConfig, profile: HostProfile, p run_playbook(args, inventory_path, 'pypi_proxy_prepare.yml', capture=True, variables=dict( pypi_endpoint=pypi_endpoint, pypi_hostname=pypi_hostname, force=force)) - atexit.register(cleanup_pypi_proxy) + ExitHandler.register(cleanup_pypi_proxy) def configure_pypi_proxy_pip(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str, pypi_hostname: str) -> None: @@ -153,7 +153,7 @@ trusted-host = {1} if not args.explain: write_text_file(pip_conf_path, pip_conf, True) - atexit.register(pip_conf_cleanup) + ExitHandler.register(pip_conf_cleanup) def configure_pypi_proxy_easy_install(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str) -> None: @@ -177,4 +177,4 @@ index_url = {0} if not args.explain: write_text_file(pydistutils_cfg_path, pydistutils_cfg, True) - atexit.register(pydistutils_cfg_cleanup) + ExitHandler.register(pydistutils_cfg_cleanup) diff --git a/test/lib/ansible_test/_internal/util_common.py b/test/lib/ansible_test/_internal/util_common.py index 79ff6c03..222366e4 100644 --- a/test/lib/ansible_test/_internal/util_common.py +++ b/test/lib/ansible_test/_internal/util_common.py @@ -1,7 +1,6 @@ """Common utility code that depends on CommonConfig.""" from __future__ import annotations -import atexit import collections.abc as c import contextlib import json @@ -64,6 +63,39 @@ from .host_configs import ( CHECK_YAML_VERSIONS: dict[str, t.Any] = {} +class ExitHandler: + """Simple exit handler implementation.""" + _callbacks: list[tuple[t.Callable, tuple[t.Any, ...], dict[str, t.Any]]] = [] + + @staticmethod + def register(func: t.Callable, *args, **kwargs) -> None: + """Register the given function and args as a callback to execute during program termination.""" + ExitHandler._callbacks.append((func, args, kwargs)) + + @staticmethod + @contextlib.contextmanager + def context() -> t.Generator[None, None, None]: + """Run all registered handlers when the context is exited.""" + last_exception: BaseException | None = None + + try: + yield + finally: + queue = list(ExitHandler._callbacks) + + while queue: + func, args, kwargs = queue.pop() + + try: + func(*args, **kwargs) + except BaseException as ex: # pylint: disable=broad-except + last_exception = ex + display.fatal(f'Exit handler failed: {ex}') + + if last_exception: + raise last_exception + + class ShellScriptTemplate: """A simple substitution template for shell scripts.""" @@ -211,7 +243,7 @@ def process_scoped_temporary_file(args: CommonConfig, prefix: t.Optional[str] = else: temp_fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix) os.close(temp_fd) - atexit.register(lambda: os.remove(path)) + ExitHandler.register(lambda: os.remove(path)) return path @@ -222,7 +254,7 @@ def process_scoped_temporary_directory(args: CommonConfig, prefix: t.Optional[st path = os.path.join(tempfile.gettempdir(), f'{prefix or tempfile.gettempprefix()}{generate_name()}{suffix or ""}') else: path = tempfile.mkdtemp(prefix=prefix, suffix=suffix) - atexit.register(lambda: remove_tree(path)) + ExitHandler.register(lambda: remove_tree(path)) return path @@ -296,7 +328,7 @@ def get_injector_path() -> str: """Remove the temporary injector directory.""" remove_tree(injector_path) - atexit.register(cleanup_injector) + ExitHandler.register(cleanup_injector) return injector_path @@ -354,7 +386,7 @@ def get_python_path(interpreter: str) -> str: verified_chmod(python_path, MODE_DIRECTORY) if not PYTHON_PATHS: - atexit.register(cleanup_python_paths) + ExitHandler.register(cleanup_python_paths) PYTHON_PATHS[interpreter] = python_path @@ -364,7 +396,7 @@ def get_python_path(interpreter: str) -> str: def create_temp_dir(prefix: t.Optional[str] = None, suffix: t.Optional[str] = None, base_dir: t.Optional[str] = None) -> str: """Create a temporary directory that persists until the current process exits.""" temp_path = tempfile.mkdtemp(prefix=prefix or 'tmp', suffix=suffix or '', dir=base_dir) - atexit.register(remove_tree, temp_path) + ExitHandler.register(remove_tree, temp_path) return temp_path diff --git a/test/units/cli/galaxy/test_collection_extract_tar.py b/test/units/cli/galaxy/test_collection_extract_tar.py index 526442cc..b84f60b6 100644 --- a/test/units/cli/galaxy/test_collection_extract_tar.py +++ b/test/units/cli/galaxy/test_collection_extract_tar.py @@ -14,6 +14,7 @@ from ansible.galaxy.collection import _extract_tar_dir @pytest.fixture def fake_tar_obj(mocker): m_tarfile = mocker.Mock() + m_tarfile._ansible_normalized_cache = {'/some/dir': mocker.Mock()} m_tarfile.type = mocker.Mock(return_value=b'99') m_tarfile.SYMTYPE = mocker.Mock(return_value=b'22') @@ -22,23 +23,11 @@ def fake_tar_obj(mocker): def test_extract_tar_member_trailing_sep(mocker): m_tarfile = mocker.Mock() - m_tarfile.getmember = mocker.Mock(side_effect=KeyError) + m_tarfile._ansible_normalized_cache = {} with pytest.raises(AnsibleError, match='Unable to extract'): _extract_tar_dir(m_tarfile, '/some/dir/', b'/some/dest') - assert m_tarfile.getmember.call_count == 1 - - -def test_extract_tar_member_no_trailing_sep(mocker): - m_tarfile = mocker.Mock() - m_tarfile.getmember = mocker.Mock(side_effect=KeyError) - - with pytest.raises(AnsibleError, match='Unable to extract'): - _extract_tar_dir(m_tarfile, '/some/dir', b'/some/dest') - - assert m_tarfile.getmember.call_count == 2 - def test_extract_tar_dir_exists(mocker, fake_tar_obj): mocker.patch('os.makedirs', return_value=None) diff --git a/test/units/plugins/action/test_reboot.py b/test/units/plugins/action/test_reboot.py new file mode 100644 index 00000000..36d9e12d --- /dev/null +++ b/test/units/plugins/action/test_reboot.py @@ -0,0 +1,214 @@ +# Copyright (c) 2022 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +"""Tests for the reboot action plugin.""" +import os + +import pytest + +from ansible.errors import AnsibleConnectionFailure +from ansible.playbook.task import Task +from ansible.plugins.action.reboot import ActionModule as RebootAction +from ansible.plugins.loader import connection_loader + + +@pytest.fixture +def task_args(request): + """Return playbook task args.""" + return getattr(request, 'param', {}) + + +@pytest.fixture +def module_task(mocker, task_args): + """Construct a task object.""" + task = mocker.MagicMock(Task) + task.action = 'reboot' + task.args = task_args + task.async_val = False + return task + + +@pytest.fixture +def play_context(mocker): + """Construct a play context.""" + ctx = mocker.MagicMock() + ctx.check_mode = False + ctx.shell = 'sh' + return ctx + + +@pytest.fixture +def action_plugin(play_context, module_task): + """Initialize an action plugin.""" + connection = connection_loader.get('local', play_context, os.devnull) + loader = None + templar = None + shared_loader_obj = None + + return RebootAction( + module_task, + connection, + play_context, + loader, + templar, + shared_loader_obj, + ) + + +_SENTINEL_REBOOT_COMMAND = '/reboot-command-mock --arg' +_SENTINEL_SHORT_REBOOT_COMMAND = '/reboot-command-mock' +_SENTINEL_TEST_COMMAND = 'cmd-stub' + + +@pytest.mark.parametrize( + 'task_args', + ( + { + 'reboot_timeout': 5, + 'reboot_command': _SENTINEL_REBOOT_COMMAND, + 'test_command': _SENTINEL_TEST_COMMAND, + }, + { + 'reboot_timeout': 5, + 'reboot_command': _SENTINEL_SHORT_REBOOT_COMMAND, + 'test_command': _SENTINEL_TEST_COMMAND, + }, + ), + ids=('reboot command with spaces', 'reboot command without spaces'), + indirect=('task_args', ), +) +def test_reboot_command(action_plugin, mocker, monkeypatch, task_args): + """Check that the reboot command gets called and reboot verified.""" + def _patched_low_level_execute_command(cmd, *args, **kwargs): + return { + _SENTINEL_TEST_COMMAND: { + 'rc': 0, + 'stderr': '<test command stub-stderr>', + 'stdout': '<test command stub-stdout>', + }, + _SENTINEL_REBOOT_COMMAND: { + 'rc': 0, + 'stderr': '<reboot command stub-stderr>', + 'stdout': '<reboot command stub-stdout>', + }, + f'{_SENTINEL_SHORT_REBOOT_COMMAND} ': { # no args is concatenated + 'rc': 0, + 'stderr': '<short reboot command stub-stderr>', + 'stdout': '<short reboot command stub-stdout>', + }, + }[cmd] + + monkeypatch.setattr( + action_plugin, + '_low_level_execute_command', + _patched_low_level_execute_command, + ) + + action_plugin._connection = mocker.Mock() + + monkeypatch.setattr(action_plugin, 'check_boot_time', lambda *_a, **_kw: 5) + monkeypatch.setattr(action_plugin, 'get_distribution', mocker.MagicMock()) + monkeypatch.setattr(action_plugin, 'get_system_boot_time', lambda d: 0) + + low_level_cmd_spy = mocker.spy(action_plugin, '_low_level_execute_command') + + action_result = action_plugin.run() + + assert low_level_cmd_spy.called + + expected_reboot_command = ( + task_args['reboot_command'] if ' ' in task_args['reboot_command'] + else f'{task_args["reboot_command"] !s} ' + ) + low_level_cmd_spy.assert_any_call(expected_reboot_command, sudoable=True) + low_level_cmd_spy.assert_any_call(task_args['test_command'], sudoable=True) + + assert low_level_cmd_spy.call_count == 2 + assert low_level_cmd_spy.spy_return == { + 'rc': 0, + 'stderr': '<test command stub-stderr>', + 'stdout': '<test command stub-stdout>', + } + assert low_level_cmd_spy.spy_exception is None + + assert 'failed' not in action_result + assert action_result == {'rebooted': True, 'changed': True, 'elapsed': 0} + + +@pytest.mark.parametrize( + 'task_args', + ( + { + 'reboot_timeout': 5, + 'reboot_command': _SENTINEL_REBOOT_COMMAND, + 'test_command': _SENTINEL_TEST_COMMAND, + }, + ), + ids=('reboot command with spaces', ), + indirect=('task_args', ), +) +def test_reboot_command_connection_fail(action_plugin, mocker, monkeypatch, task_args): + """Check that the reboot command gets called and reboot verified.""" + def _patched_low_level_execute_command(cmd, *args, **kwargs): + if cmd == _SENTINEL_REBOOT_COMMAND: + raise AnsibleConnectionFailure('Fake connection drop') + return { + _SENTINEL_TEST_COMMAND: { + 'rc': 0, + 'stderr': '<test command stub-stderr>', + 'stdout': '<test command stub-stdout>', + }, + }[cmd] + + monkeypatch.setattr( + action_plugin, + '_low_level_execute_command', + _patched_low_level_execute_command, + ) + + action_plugin._connection = mocker.Mock() + + monkeypatch.setattr(action_plugin, 'check_boot_time', lambda *_a, **_kw: 5) + monkeypatch.setattr(action_plugin, 'get_distribution', mocker.MagicMock()) + monkeypatch.setattr(action_plugin, 'get_system_boot_time', lambda d: 0) + + low_level_cmd_spy = mocker.spy(action_plugin, '_low_level_execute_command') + + action_result = action_plugin.run() + + assert low_level_cmd_spy.called + + low_level_cmd_spy.assert_any_call( + task_args['reboot_command'], sudoable=True, + ) + low_level_cmd_spy.assert_any_call(task_args['test_command'], sudoable=True) + + assert low_level_cmd_spy.call_count == 2 + assert low_level_cmd_spy.spy_return == { + 'rc': 0, + 'stderr': '<test command stub-stderr>', + 'stdout': '<test command stub-stdout>', + } + + assert 'failed' not in action_result + assert action_result == {'rebooted': True, 'changed': True, 'elapsed': 0} + + +def test_reboot_connection_local(action_plugin, module_task): + """Verify that using local connection doesn't let reboot happen.""" + expected_message = ' '.join( + ( + 'Running', module_task.action, + 'with local connection would reboot the control node.', + ), + ) + expected_action_result = { + 'changed': False, + 'elapsed': 0, + 'failed': True, + 'msg': expected_message, + 'rebooted': False, + } + + action_result = action_plugin.run() + + assert action_result == expected_action_result |