Compare commits

...

120 Commits

Author SHA1 Message Date
Matt Davis 8a047b623d
New release v2.19.0b6 (#85305) 6 months ago
Brian Coca d3f2c6d8f0 ansible-doc display docs for modules w/o ext (#85299)
Fixed case in which listing modules for docs failed to get sidecar

Co-authored-by: Abhijeet Kasurde <akasurde@redhat.com>
(cherry picked from commit 7e495f4b20)
6 months ago
leopardracer 529776f0a1 Fix typos in documentation and test ignore list (#85303)
* Update ignore.txt

* Update invalid_choice_value.py

(cherry picked from commit 56de597fc7)
6 months ago
Matt Clay f2a0eef867 Fix bool filter for non-hashable types (#85300)
(cherry picked from commit c8324aa01a)
6 months ago
Sloane Hertel 58496bed29 fix handler include_tasks templating (#85015)
* Add test for a handler including tasks from a variable filename

* Add FieldAttributeBase attribute to indicate if the object should be post validated

Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>
(cherry picked from commit d3977ebc88)
6 months ago
Martin Krizek 96b97dee7c module_utils/basic.py: properly deprecate importing `text_type` (#85293)
(cherry picked from commit 2cfb158536)
6 months ago
Matt Clay 1fe0fb0e7a ansible-test - Code cleanup (#85297)
(cherry picked from commit bdc6c8e16a)
6 months ago
Matt Davis 7a7427bd6f Fix post-fork deadlock from early Python writers like pydevd (#85296)
(cherry picked from commit 1d1bbe3424)
6 months ago
Matt Clay b5a4fe62ed Fix nested include with templating (#85295)
(cherry picked from commit 714f796384)
6 months ago
Matt Clay 0cecfd9877 AnsiballZ refactoring and remote debug support (#85289)
(cherry picked from commit 45dd2c0647)
6 months ago
David Shrewsbury e2b831ce26 get_url - check for incomplete data read (#85164)
(cherry picked from commit aa8d58a174)
6 months ago
Martin Krizek 33c55b9c25 Fix templating tags on plays and roles (#82314)
Fixes #69903

ci_complete

(cherry picked from commit 532edf4c35)
6 months ago
Matt Clay aae5734e0e Fix deprecation versions in template lookup (#85288)
(cherry picked from commit 4956619ac2)
6 months ago
Matt Davis b95bc19853 fix Marker handling on Jinja macro invocations (#85280)
* always allow Marker args to pass through
* always disable pre-emptive trip-on-retrieval for Macro JinjaCallContext
* add macro-callable template expression result test cases

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 2bed98bd20)
6 months ago
Martin Krizek eb29a662f6 Exceptions clean up (#85176)
* replace usage of `IOError` as it is an alias to `OSError`
* replace usage of `socket.error` as it is an alias to `OSError`
* use subclasses of `OSError` rather than inspecting `errno`s
* utilize `exist_ok` parameter of `os.makedirs` rather than ignoring
  `FileExistsError`

Make the following changes to the exception handling this patch already
updates to be consistent with the new code:
* use `ex` as a name for exception being handled
* use `from ex` when re-raising exception for additional context
* use f-strings and `!r` for quoting
* pass exceptions to the `exception` parameter of `fail_json`
* use `display.error_as_warning` rather than passing stringified
  exception into `display.warning`

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 600c1e67b4)
6 months ago
Matt Davis 0f2bc31753 adjust PluginInfo to use PluginType enum (#85277)
* normalization fixups

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 43c0132caa)
6 months ago
Matt Davis 7b823720ca Prevent template lookup and action from masking `ansible_managed` value (#85075)
* deprecate DEFAULT_MANAGED_STR and prevent masking of ansible_managed var

* adjust public API behavior

* restore backward-compatible behavior on existing public API

(cherry picked from commit 9f0a8075e3)
6 months ago
Matt Davis a943626ce6 Restore 2.18 vault tag YAML dump behavior (#85275)
* Doing conditional redaction/formatting needs other bits that aren't ready for 2.19.

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 2b7204527b)
6 months ago
Martin Krizek 9f703f7633 Fix calls to `deprecated()` to include `help_text` arg (#85262)
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit ea7ad90c31)
6 months ago
Matt Clay 36f322bb2c Add missing docs for sftp_batch_mode (#85270)
(cherry picked from commit 051f7e5944)
6 months ago
Matt Clay a57229aa8a Fix YAML loading/dumping and filters (#85266)
- from_yaml/from_yaml_all filters now preserve trust
- YAML dumping can once again handle undecryptable vaulted values
- increased test coverage

Co-authored-by: Matt Davis <nitzmahone@redhat.com>
(cherry picked from commit 1c06c46cc1)
6 months ago
Matt Davis bc624d35b9 Fix incorrect behavior when a Jinja test returns Marker (#85264)
* Avoid Marker trip in plugin wrapper that causes unnecessary early template bailout.
* Remove spurious non-boolean result deprecation warning.
* Add test.

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 6198c7377f)
6 months ago
Matt Clay 2eb107fe1f Fix logic error in ansible-galaxy (#85258)
Co-authored-by: OpenRefactory, Inc <56681071+openrefactory@users.noreply.github.com>
Co-authored-by: Munawar <munawar.hafiz@gmail.com>
(cherry picked from commit 7da24ca7b0)
6 months ago
Aditya Putta d5e6d31d50 Use a Specific Package Manager (#82050)
* Use a Specific Package Manager

* Update the name of package manager from yum to dnf

* Update lib/ansible/modules/package.py

Co-authored-by: Martin Krizek <martin.krizek@gmail.com>

* Update lib/ansible/modules/package.py

Co-authored-by: Martin Krizek <martin.krizek@gmail.com>

---------

Co-authored-by: Aditya Putta <puttaa@skiff.com>
Co-authored-by: Martin Krizek <martin.krizek@gmail.com>
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 0bbd8847c4)
6 months ago
Matt Davis 92824444ea
Update Ansible release version to v2.19.0b5.post0. (#85257) 6 months ago
Matt Davis c79c2710f4
New release v2.19.0b5 (#85255) 6 months ago
Matt Davis 3a487532dd fix key conversion on legacy JSON profiles (#85253)
* fix key conversion on legacy JSON profiles

* restore stdlib silent str conversion behavior for int/float/bool/None dict key types
* remove most conversions for non-scalar keys
* add key conversion cases to JSON profile test suite

* typo

* Fix sanity test failures

* Fix _handle_key_str_fallback implementation

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit bbbfbd57e2)
6 months ago
Martin Krizek 20be3951b7 Deprecate ShellModule.checksum() (#85216)
The methods have not been used, updated or tested since
b9d0662faf.

(cherry picked from commit d7c4d0119f)
6 months ago
Matt Davis e1a7508f6d Resolve misc DTFIX0/1 (#85247)
* complete DTFIX0 after eval

* sunder-prefix Marker.concrete_subclasses

* re-home Jinja plugin decorators public API

* low-hanging/already fixed DTFIX cases

Co-authored-by: Matt Clay <matt@mystile.com>

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit df0b417f2d)
6 months ago
Jordan Borean 4ae5800849 doc - Dynamically document jinja builtins (#85215)
* doc - Dynamically document jinja builtins

This change has `ansible-doc` dynamically generate the documentation for
any Jinja builtin filter and test plugins. These dynamic stubs will
point to the official Jinja documentation pages for more information.

* Fix sanity issues

* Add tests

* Update Jinja builtin doc gen

Co-authored-by: Matt Clay <matt@mystile.com>

---------

Co-authored-by: Matt Davis <nitzmahone@redhat.com>
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 8f2622c39f)
6 months ago
Martin Krizek 603e65d204 Merge dnf and dnf5 integration tests (#85199)
(cherry picked from commit 01bb9393d7)
6 months ago
Martin Krizek a8c68be3b9 Deprecate ansible.compat.importlib_resources (#85226)
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 3505572470)
6 months ago
Martin Krizek 744f86fbc5 Deprecate module_utils.common.collections.count (#85233)
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 487d699226)
6 months ago
Jordan Borean 698b8d64fc Exclude ansible._protomatter from ansible-doc output by default (#85115)
* Exclude ansible._protomatter from ansible-doc output by default

* Added changelog

(cherry picked from commit b4741fc495)
6 months ago
Matt Davis 7b99b0144d local connection always passes str to Popen (#85239)
(cherry picked from commit 0ee1c36779)
6 months ago
Matt Davis d63f9aa38d Misc ssh agent fixes (#85238)
* Misc ssh-agent fixes

* Replace manual SIGALRM handling with new alarm_timeout context manager
* Misc error handling fixes to ssh-agent startup
* Add SSH_AGENT_EXECUTABLE config to ease failure mode testing
* 100% test coverage on agent startup failure code

Co-authored-by: Matt Clay <matt@mystile.com>

* make SSH Agent support internal

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 2a24633964)
6 months ago
Matt Clay 0576ff3e65 Add missing warning methods and args (#85225)
(cherry picked from commit eafe5fc739)
6 months ago
Matt Davis 85283e7238 Clean up TE error handling, wrap sigalrm handler (#85232)
* Clean up TE error handling, wrap sigalrm handler

* Preserve error detail on AnsibleAction and Connection exceptions.
* Remove multiple layers of unreachable or redundant error handling.
* Wrap manual alarm signal/timeout handling into a context manager, add tests.

Co-authored-by: Matt Clay <matt@mystile.com>

* update error message check in test

* update test timeout message assertions

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit cbcefc53a3)
6 months ago
Brian Coca 311ef75245 async handling changed to use 'true' booleans(#85074)
async_status and async_wrrapper now use booleans instead of 0/1 for 'thruthyness'
gather_facts also updated

Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit d41a3430b7)
6 months ago
Martin Krizek 0d01da2dc2 jinja2.__version__ is deprecated (#85221)
Prepare for its removal in Jinja 3.3.

See https://github.com/pallets/jinja/pull/2098

(cherry picked from commit 0e2f770a24)
6 months ago
Matt Davis 5fd78b07fb Added _TEMPLAR_SANDBOX_MODE config (#85222)
* Added _TEMPLAR_SANDBOX_MODE config

* allows unsafe attribute checks to be disabled in Jinja sandbox

* Update lib/ansible/_internal/_templating/_jinja_bits.py

Co-authored-by: Matt Clay <matt@mystile.com>

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 91453e30af)
6 months ago
Matt Clay 8b0b54de38 ansible-test - Fix incorrect use of InferenceContext (#85230)
(cherry picked from commit e2a5377b9a)
6 months ago
Matt Davis 21f478e77b Decouple SSH client verbosity from Ansible display (#85224)
* decouple SSH client verbosity from Ansible display

* remove failing false-coverage unit test

(cherry picked from commit b71d9aa4a5)
6 months ago
Matt Davis 40a675543f fix from_yaml_all filter inconsistent None handling (#85223)
* fix from_yaml_all filter inconsistent None handling

* always returns empty list for None or empty string input

* deprecate non-string inputs for from_yaml and from_yaml_all

(cherry picked from commit 356bf336bd)
6 months ago
Harshvardhan Sharma fa2f1e4750 Improve SUSE distribution detection using VARIANT_ID with fallback (#85152)
Co-authored-by: Abhijeet Kasurde <akasurde@redhat.com>
(cherry picked from commit f05b1d1ccf)
6 months ago
Martin Krizek 2adc8ae1cd dnf5 - handle all specific libdnf5 exceptions (#85175)
Fixes #84634

(cherry picked from commit 99a3346de6)
6 months ago
Martin Krizek 6f7215ca36 dnf5 tests: do not remove attr (#85218)
On RHEL 10, the attr package is in the dependency tree of dnf itself
and cannot be removed.

(cherry picked from commit a0132fec0b)
6 months ago
Kimmo Suominen 43b0155529 Recognize virtualization type correctly on Linode (#85184)
(cherry picked from commit 3b66150cc3)
6 months ago
Abhijeet Kasurde 2d800e1a4c service: Remove py2 specific code (#85191)
Signed-off-by: Abhijeet Kasurde <Akasurde@redhat.com>
(cherry picked from commit 068157618c)
6 months ago
Martin Krizek df60504b93 apt_repository: remove Python 2 support (#85212)
(cherry picked from commit f635a22d81)
6 months ago
Jordan Borean abb1de2780 win coverage - Fix untrusted coverage collection (#85197)
Fixes the logic when running a module through App Control when the
module is not trusted to run in Full Language Mode. This ensures
coverage will still run as expected and that the trust verification only
happens in the wrappers that actually run/prepare the code.

Also expands on a comment to clarify why only that branch is set to set
the internal file encoding to UTF-8.

(cherry picked from commit 17cee7a982)
6 months ago
Matt Clay 457b40d659 Include message in captured tracebacks (#85196)
This matches the formatting of tracebacks from exceptions.

(cherry picked from commit 359173c006)
6 months ago
Matt Clay 3207e55ca2 display - Replace CRNL with NL (#85194)
(cherry picked from commit e226294855)
6 months ago
Matt Clay 424327c293 ansible-test - Add RHEL 10.0 remote (#85189)
* ansible-test - Add RHEL 10.0 remote

* Update tests for RHEL 10
* Fix iptables test
* Rework rpm_key integration test

(cherry picked from commit 53b0f1645b)
6 months ago
Martin Krizek cc0ef95ce8 csvfile lookup: remove Python 2 compat (#85186)
(cherry picked from commit c6a2e5ea75)
6 months ago
Matt Clay 8bbc43efd0 Limit SSH agent DSA testing to RHEL 9 (#85193)
(cherry picked from commit 6a1c24ca99)
6 months ago
Jordan Borean 880b584124 Add support for Windows App Control/WDAC (#84898)
* Add support for Windows App Control/WDAC

Adds preview support for Windows App Control, formerly known as WDAC.
This is a tech preview feature and is designed to test out improvements
needed in future versions of Ansible.

* Use psd1 and parse it through the Ast to avoid any unexpected execution results

* Add tests for various manifest permutations

* Ignore test shebang failure

* Apply suggestions from code review

Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>

* Use more flexible test expectations

* Add type annotations for shell functions

---------

Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>
(cherry picked from commit 75f7b2267d)
6 months ago
Matt Clay 895af10b99 ansible-test - Improve deprecated checking type inference (#85159)
* ansible-test - Improve deprecated checking type inference

Also disabled the ``bad-super-call`` pylint rule due to false positives.

* Add type comment support

* Try without using register_transform

(cherry picked from commit e82be177cd)
6 months ago
Matt Clay 638d711a72 ansible-test - Fix option filtering (#85182)
(cherry picked from commit feda0a5c6e)
6 months ago
Matt Clay 1441817ffe Remove unnecessary module shebang (#85183)
(cherry picked from commit 1e64707592)
6 months ago
Matt Clay 01e97d7f80 DTFIX recategorization and error/warning refactor (#85181)
Co-authored-by: Matt Davis <nitzmahone@redhat.com>
(cherry picked from commit 242bb9ebab)
6 months ago
Martin Krizek 108f349e1d Integration tests clean up (#85130)
* Integration tests clean up

* more

* we only test with Ubuntu 24.04 on the controller

(cherry picked from commit 40c919d7bd)
6 months ago
Matt Clay 35a951b611 Expand scope of `black` sanity test (#85169)
(cherry picked from commit 97dd5f104c)
6 months ago
Matt Clay 76fa45895d Code formatting for .azure-pipelines/ (#85166)
(cherry picked from commit 14bf64ddf8)
6 months ago
Lee Garrett 1381fc30a3 Mark integration tests as needs/root (#84487)
For calling setup_test_user, which adds a user to the system (and thus requiring
root):
- become
- become_su
- become_sudo
- slurp

And also:
- apt, which installs/removes system packages
- deb822_repository, which adds/removes apt repos
- hardware_facts calls losetup, which requires root
- package_facts, which installs/removes system packages

Co-authored-by: Lee Garrett <lgarrett@rocketjump.eu>
(cherry picked from commit ebd022ee42)
6 months ago
Matt Clay f771784853 Code formatting for packaging/cli-doc/ (#85167)
(cherry picked from commit 930ffd1a83)
6 months ago
Matt Clay 46cc5d6b90 Code formatting for packaging/release.py (#85168)
(cherry picked from commit ed0e78fd9c)
6 months ago
Matt Clay 0ba6145aa8 Code formatting for test/sanity/code-smell/ (#85165)
(cherry picked from commit 460343510c)
6 months ago
Jordan Borean 1bd8ed4437 Fix constructable inventory default keyed group (#85002)
Fix the logic for a constructable inventory `keyed_groups` entry to use
the `default_value` if the ``key` expression result is `None` or `omit`
and not just an empty string. This bug was introduced with the changes
in data tagging and goes back to the original behaviour.

Co-authored-by: Sloane Hertel <19572925+s-hertel@users.noreply.github.com>
(cherry picked from commit fe2d9e316a)
6 months ago
Matt Clay 33c3844daa ansible-test - Update default containers (#85161)
(cherry picked from commit 7ac74ab591)
7 months ago
Matt Clay b6fad7bbb8 ansible-test - Update sanity requirements (#85160)
(cherry picked from commit 588410fe50)
7 months ago
Matt Davis 3a2248a992 recategorize pass over DTFIX-MERGE entries post b4 (#85155)
(cherry picked from commit f5a85493c1)
7 months ago
Matt Clay 134c8ec093 Add missing cache plugin test scenarios (#85154)
(cherry picked from commit 567fda6906)
7 months ago
Matt Davis fddda73042 ensure builtin tests always return bool (#85149)
* added unit test validation and coverage enforcement

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit e5476972da)
7 months ago
Matt Davis 60ddb2954b misc changelog and docstring cleanup (#85148)
* deferred listify_lookup_plugin_terms deprecation

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 7be1359841)
7 months ago
Matt Clay b2699fade5 release.py - Remove email announcement support (#85060)
Also add missing git and run overloads.

(cherry picked from commit 93aa611435)
7 months ago
Matt Clay 5e0733288e
Update Ansible release version to v2.19.0b4.post0. (#85137) 7 months ago
Matt Clay 731b4d0242
New release v2.19.0b4 (#85133) 7 months ago
Matt Davis d6a8582da7 fix ensure_type to support vaulted values (#85129)
* restored parity with 2.18

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 9a426fe303)
7 months ago
Matt Davis 204cdcee67 ensure that all config return values are Origin-tagged (#85127)
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit fc8a227647)
7 months ago
Matt Davis df214f93a7 apply trust to declarative plugin config (#85126)
* trust strings in loaded doc fragments
* added tests
* added hard_fail_context test mechanism

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 9efba4f972)
7 months ago
Lorenzo Tanganelli 27aca0abd3 facts: CloudStack KVM Hypervisor to Linux virtual fact (#85117)
Co-authored-by: Abhijeet Kasurde <akasurde@redhat.com>
(cherry picked from commit 8a4fb78988)
7 months ago
Matt Clay 38ef2b8c25 ansible-test - Relax some deprecation checks (#85122)
(cherry picked from commit 7b69cf3266)
7 months ago
Matt Clay 23f935eb0d template module - render `None` as empty string (#85121)
* template module - render `None` as empty string

* Update changelogs/fragments/template-none.yml

Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>

---------

Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>
(cherry picked from commit 4fe9606530)
7 months ago
Matt Davis 9fff6d433d Misc config type coercion fixes (#85119)
* remove dead config comment noise

* update `list` typed config defaults to be lists

* fix tag preservation/propagation in config
* numerous other ensure_type bugfixes
* 100% unit test coverage of ensure_type
* emit warnings on template_default failures
* fix unhandled exception in convert_bool on unhashable inputs

Co-authored-by: Matt Clay <matt@mystile.com>

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit d33bedc48f)
7 months ago
j-dr3 e8d914e992 sysvinit: make examples consistent (#85108)
(cherry picked from commit dcc5dac184)
7 months ago
omahs 121871af86 Fix typos (#85107)
(cherry picked from commit 471c5229a7)
7 months ago
Martin Krizek f70dbc15e5 Passing warnings to exit/fail_json is deprecated. (#85109)
(cherry picked from commit 8b9ddf5544)
7 months ago
Matt Davis 80af44d822 add fuzzy matching to package_data sanity (#85103)
* add fuzzy matching to package_data sanity

* relaxes exact directory matches for license files to allow setuptools > 72 to pass

* sanity

(cherry picked from commit 7e00053a30)
7 months ago
Martin Krizek da59710961 dnf5: skip pkgs that don't satisfy bugfix/security when specified (#85111)
(cherry picked from commit 107842fd7d)
7 months ago
Matt Davis c83b70a04c
Update Ansible release version to v2.19.0b3.post0. (#85102) 7 months ago
Matt Davis c742fdc66c
New release v2.19.0b3 (#85101) 7 months ago
pollenJP(@'ω'@) 7a932a93b0 get_url: missing closing brace in docs (#85096)
(cherry picked from commit 1c29910087)
7 months ago
Matt Davis 8c8717a8e4 Switch to stackwalk caller ID (#85095)
* See changelog fragment for most changes.
* Defer early config warnings until display is functioning, eliminating related fallback display logic.
* Added more type annotations and docstrings.
* ansible-test - pylint sanity for deprecations improved.
* Refactored inline legacy resolutions in PluginLoader.

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit ff6998f2b9)
7 months ago
Jordan Borean 6054b29cb7
Add win_script become tests (#85079)
(cherry picked from commit e4cac2ac33)
7 months ago
Brian Coca 131175a5a6
ensure predictable permissions on module artifacts (#84948)
and test it!

(cherry picked from commit 9f894b81c2)
7 months ago
Martin Krizek 0aab250fbc
dnf5: avoid generating excessive history entries (#85065)
Fixes #85046

(cherry picked from commit cff49a62ec)
7 months ago
Martin Krizek dcec78b0f9
async_status: fix example to use finished test (#85066)
Fixes #85048

(cherry picked from commit dbf131c07d)
7 months ago
Brian Coca ea22e5d0dd
show internal but not hidden config options, while still hiding test options (#84997)
(cherry picked from commit aab732cb82)
7 months ago
Brian Coca 867d9d3096
These actions do not support until (#84847)
(cherry picked from commit 8ab342f8cc)
7 months ago
Matt Clay e0e286c009
[stable-2.19] ansible-test - Use `-t` for container stop timeout (#85019) (#85055)
(cherry picked from commit 0aa8afbaf4)
7 months ago
Matt Clay 1c1a271b88
Update Ansible release version to v2.19.0b2.post0. (#85041) 7 months ago
Matt Clay 4e861fa9c8
New release v2.19.0b2 (#85040)
* New release v2.19.0b2

* Revert setuptools version bump
7 months ago
Matt Davis f898f9fec6 Implement TaskResult backward compatibility for callbacks (#85039)
* Implement TaskResult backward compatibility for callbacks
* general API cleanup
* misc deprecations

Co-authored-by: Matt Clay <matt@mystile.com>

* fix v2_on_any deprecation exclusion for base

---------

Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 03181ac87b)
7 months ago
Matt Davis 4714194672 restore parsing/utils/jsonify.py (#85032)
(cherry picked from commit 2033993d89)
7 months ago
Abhijeet Kasurde ffbf121182
comment: raise an exception when an invalid option is provided (#84984)
Co-authored-by: Matt Clay <matt@mystile.com>
Signed-off-by: Abhijeet Kasurde <Akasurde@redhat.com>
(cherry picked from commit 1daa8412d5)
8 months ago
Brian Coca 89a4900b61
normalize error handler choices (#84998)
use existing to avoid deprecation cycle
normalize test too

(cherry picked from commit 2cbb721f6f)
8 months ago
Matt Clay 17d4fdd883
Increase galaxy test publish timeout (#85016)
(cherry picked from commit e6dc17cda4)
8 months ago
Lee Garrett 7fc916361e
Fix test_range_templating on 32-bit architectures (#85007)
* Fix test_range_templating on 32-bit architectures

32-bit archtectures like i386, armel, armhf will fail with the error

ansible._internal._templating._errors.AnsibleTemplatePluginRuntimeError: The
filter plugin 'ansible.builtin.random' failed: Python int too large to convert
to C ssize_t

So just pick sys.maxsize (2**31 - 1) so it works on 32 bit machines.

---------

Co-authored-by: Lee Garrett <lgarrett@rocketjump.eu>
Co-authored-by: Matt Clay <matt@mystile.com>
(cherry picked from commit 5f6aef95ac)
8 months ago
Matt Davis 82ea3addce
Miscellaneous fixes (#85012)
* Add help_text to play_hosts deprecation

* clean up TaskResult type handling

(cherry picked from commit 1b6b910439)
8 months ago
Matt Clay 98009c811b
Disable retries on ansible-galaxy-collection (#85013)
(cherry picked from commit f7d03474a5)
8 months ago
Sloane Hertel de7c454684
Remove unused local function _get_plugin_vars from vars manager (#85008)
(cherry picked from commit 93e6f012cb)
8 months ago
Matt Clay 80d5f05642
Miscellaneous DT fixes (#84991)
* Use `_UNSET` instead of allowing `ellipsis`

* Fix deprecation warning pre-check

* Deprecation warnings from modules can now be disabled.
* Deprecation warnings from modules get the "can be disabled" notice.

* Include help text in pre-display fatal errors

* Simplify lookup warning/debug messaging

* Fix return type of `timedout` test plugin

* Use `object` for `_UNSET`

* Remove obsolete `convert_data` tests

* Remove unnecessary template from test

* Improve legacy YAML objects backward compat

* Fix templar backward compat for None overrides

(cherry picked from commit 6cc97447aa)
8 months ago
Matt Clay ec0d8f3278
Disable parallel publish in galaxy test (#85000)
(cherry picked from commit e094d48b1b)
8 months ago
Abhijeet Kasurde c21a817c47
filter_core integration test - remove Python 2.6 related dead code (#84985)
Signed-off-by: Abhijeet Kasurde <Akasurde@redhat.com>
(cherry picked from commit 500a4aba08)
8 months ago
Martin Krizek 85cb2baf1f
get_bin_path('ssh-agent'): required is deprecated (#84995)
(cherry picked from commit 4868effc71)
8 months ago
Felix Fontein 2fcfad54b0
ansible-doc: fix indent and line wrapping for first line of (sub-)option and (sub-)return value descriptions (#84690)
* Fix initial indent for descriptions of suboptions.
* Fix line width for initial line of option descriptions.

(cherry picked from commit 352d8ec33a)
8 months ago
Matt Clay 6f95a618af
Convert DT issue template to pre-release template (#84982)
(cherry picked from commit 9ddfe9db39)
8 months ago
Matt Martz 19d9253ec9
Update Ansible release version to v2.19.0b1.post0. (#84988) 8 months ago
Matt Martz 8d775ddced
New release v2.19.0b1 (#84979) 8 months ago

@ -94,6 +94,8 @@ stages:
test: rhel/9.5@3.9
- name: RHEL 9.5 py312
test: rhel/9.5@3.12
- name: RHEL 10.0
test: rhel/10.0
- name: FreeBSD 13.5
test: freebsd/13.5
- name: FreeBSD 14.2
@ -108,6 +110,8 @@ stages:
test: macos/15.3
- name: RHEL 9.5
test: rhel/9.5
- name: RHEL 10.0
test: rhel/10.0
- name: FreeBSD 13.5
test: freebsd/13.5
- name: FreeBSD 14.2
@ -125,6 +129,8 @@ stages:
test: fedora/41
- name: RHEL 9.5
test: rhel/9.5
- name: RHEL 10.0
test: rhel/10.0
- name: Ubuntu 24.04
test: ubuntu/24.04
groups:

@ -61,9 +61,12 @@ def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], d
for file in files:
cmd = [
str(codecov_bin),
'--name', file.name,
'--file', str(file.path),
'--name',
file.name,
'--file',
str(file.path),
]
for flag in file.flags:
cmd.extend(['--flags', flag])

@ -1,9 +1,8 @@
name: Fallible 2.19 Data Tagging Preview Bug Report
description: File a bug report against the Fallible 2.19 Data Tagging Preview
name: Pre-Release Bug Report
description: File a bug report against a pre-release version
labels:
- fallible_dt
- bug
- data_tagging
- pre_release
assignees:
- nitzmahone
- mattclay
@ -12,15 +11,14 @@ body:
attributes:
value: |
## Bug Report
- type: dropdown
- type: textarea
attributes:
label: Fallible Version
description: The fallible release that reproduces the issue described.
options:
- 2025.4.1
- 2025.3.11
- 2025.3.3
- 2025.1.30
label: Ansible Version
description: Paste the full output from `ansible --version` below.
render: console
placeholder: $ ansible --version
validations:
required: true
- type: textarea
attributes:
label: Summary
@ -37,8 +35,6 @@ body:
bin/ansible
### Issue Type
Bug Report
### Ansible Version
2.19.0.dev0
### Configuration
### OS / Environment
-->

3
.gitignore vendored

@ -97,6 +97,9 @@ Vagrantfile
# vendored lib dir
lib/ansible/_vendor/*
!lib/ansible/_vendor/__init__.py
# PowerShell signed hashlist
lib/ansible/config/powershell_signatures.psd1
*.authenticode
# test stuff
/test/integration/cloud-config-*.*
!/test/integration/cloud-config-*.*.template

@ -0,0 +1,490 @@
==================================================================
ansible-core 2.19 "What Is and What Should Never Be" Release Notes
==================================================================
.. contents:: Topics
v2.19.0b6
=========
Release Summary
---------------
| Release Date: 2025-06-11
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Minor Changes
-------------
- ansiballz - Added an experimental AnsiballZ extension for remote debugging.
- ansiballz - Added support for AnsiballZ extensions.
- ansiballz - Moved AnsiballZ code coverage support into an extension.
- ansiballz - Refactored AnsiballZ and module respawn.
- template action and lookup plugin - The value of the ``ansible_managed`` variable (if set) will not be masked by the ``template`` action and lookup. Previously, the value calculated by the ``DEFAULT_MANAGED_STR`` configuration option always masked the variable value during plugin execution, preventing runtime customization.
Bugfixes
--------
- Fix templating ``tags`` on plays and roles. (https://github.com/ansible/ansible/issues/69903)
- ansible-doc will no longer ignore docs for modules without an extension (https://github.com/ansible/ansible/issues/85279).
- display - Fix hang caused by early post-fork writers to stdout/stderr (e.g., pydevd) encountering an unreleased fork lock.
- get_url - add a check to recognize incomplete data transfers.
- include_tasks - fix templating options when used as a handler (https://github.com/ansible/ansible/pull/85015).
- templating - Fixed cases where template expression blocks halted prematurely when a Jinja macro invocation returned an undefined value.
- templating - Jinja macros returned from a template expression can now be called from another template expression.
v2.19.0b5
=========
Release Summary
---------------
| Release Date: 2025-06-03
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Minor Changes
-------------
- Improved SUSE distribution detection in distribution.py by parsing VARIANT_ID from /etc/os-release for identifying SLES_SAP and SL-Micro. Falls back to /etc/products.d/baseproduct symlink for older systems.
- Remove unnecessary shebang from the ``hostname`` module.
- Use ``importlib.metadata.version()`` to detect Jinja version as jinja2.__version__ is deprecated and will be removed in Jinja 3.3.
- ansible-doc - Return dynamic stub when reporting on Jinja filters and tests not explicitly documented in Ansible
- ansible-doc - Skip listing the internal ``ansible._protomatter`` plugins unless explicitly requested
- ansible-test - Add RHEL 10.0 as a remote platform for testing.
- apt_repository - remove Python 2 support
- csvfile lookup - remove Python 2 compat
- display - Add ``help_text`` and ``obj`` to ``Display.error_as_warning``.
- display - Replace Windows newlines (``\r\n``) in display output with Unix newlines (``\n``). This ensures proper display of strings sourced from Windows hosts in environments which treat ``\r`` as ``\n``, such as Azure Pipelines.
- facts - add "Linode" for Linux VM in virtual facts
- module_utils - Add ``AnsibleModule.error_as_warning``.
- module_utils - Add ``ansible.module_utils.common.warnings.error_as_warning``.
- module_utils - Add optional ``help_text`` argument to ``AnsibleModule.warn``.
- ssh agent - Added ``SSH_AGENT_EXECUTABLE`` config to allow override of ssh-agent.
- ssh connection plugin - Added ``verbosity`` config to decouple SSH debug output verbosity from Ansible verbosity. Previously, the Ansible verbosity value was always applied to the SSH client command-line, leading to excessively verbose output. Set the ``ANSIBLE_SSH_VERBOSITY`` envvar or ``ansible_ssh_verbosity`` Ansible variable to a positive integer to increase SSH client verbosity.
- task timeout - Specifying a timeout greater than 100,000,000 now results in an error.
- templating - Added ``_ANSIBLE_TEMPLAR_SANDBOX_MODE=allow_unsafe_attributes`` environment variable to disable Jinja template attribute sandbox. (https://github.com/ansible/ansible/issues/85202)
- windows - Added support for ``#AnsibleRequires -Wrapper`` to request a PowerShell module be run through the execution wrapper scripts without any module utils specified.
- windows - Added support for running signed modules and scripts with a Windows host protected by Windows App Control/WDAC. This is a tech preview and the interface may be subject to change.
- windows - Script modules will preserve UTF-8 encoding when executing the script.
Deprecated Features
-------------------
- The ``ShellModule.checksum`` method is now deprecated and will be removed in ansible-core 2.23. Use ``ActionBase._execute_remote_stat()`` instead.
- The ``ansible.module_utils.common.collections.count()`` function is deprecated and will be removed in ansible-core 2.23. Use ``collections.Counter()`` from the Python standard library instead.
- ``ansible.compat.importlib_resources`` is deprecated and will be removed in ansible-core 2.23. Use ``importlib.resources`` from the Python standard library instead.
Bugfixes
--------
- Core Jinja test plugins - Builtin test plugins now always return ``bool`` to avoid spurious deprecation warnings for some malformed inputs.
- ansible-test - Disabled the ``bad-super-call`` pylint rule due to false positives.
- ansible-test - Fix incorrect handling of options with optional args (e.g. ``--color``), when followed by other options which are omitted during arg filtering (e.g. ``--docker``). Previously it was possible for non-option arguments to be incorrectly omitted in these cases. (https://github.com/ansible/ansible/issues/85173)
- ansible-test - Improve type inference for pylint deprecated checks to accommodate some type annotations.
- async_status module - The ``started`` and ``finished`` return values are now ``True`` or ``False`` instead of ``1`` or ``0``.
- constructed inventory - Use the ``default_value`` or ``trailing_separator`` in a ``keyed_groups`` entry if the expression result of ``key`` is ``None`` and not just an empty string.
- dnf5 - handle all libdnf5 specific exceptions (https://github.com/ansible/ansible/issues/84634)
- error handling - Error details and tracebacks from connection and built-in action exceptions are preserved. Previously, much of the detail was lost or mixed into the error message.
- from_yaml_all filter - `None` and empty string inputs now always return an empty list. Previously, `None` was returned in Jinja native mode and empty list in classic mode.
- local connection plugin - The command-line used to create subprocesses is now always ``str`` to avoid issues with debuggers and profilers.
- ssh agent - Fixed several potential startup hangs for badly-behaved or overloaded ssh agents.
- task timeout - Specifying a negative task timeout now results in an error.
v2.19.0b4
=========
Release Summary
---------------
| Release Date: 2025-05-12
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Minor Changes
-------------
- facts - add "CloudStack KVM Hypervisor" for Linux VM in virtual facts (https://github.com/ansible/ansible/issues/85089).
- modules - use ``AnsibleModule.warn`` instead of passing ``warnings`` to ``exit_json`` or ``fail_json`` which is deprecated.
Bugfixes
--------
- ansible-test - Updated the ``pylint`` sanity test to skip some deprecation validation checks when all arguments are dynamic.
- config - Preserve or apply Origin tag to values returned by config.
- config - Prevented fatal errors when ``MODULE_IGNORE_EXTS`` configuration was set.
- config - Templating failures on config defaults now issue a warning. Previously, failures silently returned an unrendered and untrusted template to the caller.
- config - ``ensure_type`` correctly propagates trust and other tags on returned values.
- config - ``ensure_type`` now converts mappings to ``dict`` when requested, instead of returning the mapping.
- config - ``ensure_type`` now converts sequences to ``list`` when requested, instead of returning the sequence.
- config - ``ensure_type`` now correctly errors when ``pathlist`` or ``pathspec`` types encounter non-string list items.
- config - ``ensure_type`` now reports an error when ``bytes`` are provided for any known ``value_type``. Previously, the behavior was undefined, but often resulted in an unhandled exception or incorrect return type.
- config - ``ensure_type`` with expected type ``int`` now properly converts ``True`` and ``False`` values to ``int``. Previously, these values were silently returned unmodified.
- convert_bool.boolean API conversion function - Unhashable values passed to ``boolean`` behave like other non-boolean convertible values, returning False or raising ``TypeError`` depending on the value of ``strict``. Previously, unhashable values always raised ``ValueError`` due to an invalid set membership check.
- dnf5 - when ``bugfix`` and/or ``security`` is specified, skip packages that do not have any such updates, even for new versions of libdnf5 where this functionality changed and it is considered failure
- plugin loader - Apply template trust to strings loaded from plugin configuration definitions and doc fragments.
- template action - Template files where the entire file's output renders as ``None`` are no longer emitted as the string "None", but instead render to an empty file as in previous releases.
v2.19.0b3
=========
Release Summary
---------------
| Release Date: 2025-05-06
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Minor Changes
-------------
- ansible-config will now show internal, but not test configuration entries. This allows for debugging but still denoting the configurations as internal use only (_ prefix).
- ansible-test - Improved ``pylint`` checks for Ansible-specific deprecation functions.
- ansible-test - Use the ``-t`` option to set the stop timeout when stopping a container. This avoids use of the ``--time`` option which was deprecated in Docker v28.0.
- collection metadata - The collection loader now parses scalar values from ``meta/runtime.yml`` as strings. This avoids issues caused by unquoted values such as versions or dates being parsed as types other than strings.
- deprecation warnings - Deprecation warning APIs automatically capture the identity of the deprecating plugin. The ``collection_name`` argument is only required to correctly attribute deprecations that occur in module_utils or other non-plugin code.
- deprecation warnings - Improved deprecation messages to more clearly indicate the affected content, including plugin name when available.
- deprecations - Collection name strings not of the form ``ns.coll`` passed to deprecation API functions will result in an error.
- deprecations - Removed support for specifying deprecation dates as a ``datetime.date``, which was included in an earlier 2.19 pre-release.
- deprecations - Some argument names to ``deprecate_value`` for consistency with existing APIs. An earlier 2.19 pre-release included a ``removal_`` prefix on the ``date`` and ``version`` arguments.
- modules - The ``AnsibleModule.deprecate`` function no longer sends deprecation messages to the target host's logging system.
Deprecated Features
-------------------
- Passing a ``warnings` or ``deprecations`` key to ``exit_json`` or ``fail_json`` is deprecated. Use ``AnsibleModule.warn`` or ``AnsibleModule.deprecate`` instead.
- plugins - Accessing plugins with ``_``-prefixed filenames without the ``_`` prefix is deprecated.
Bugfixes
--------
- Ansible will now ensure predictable permissions on remote artifacts, until now it only ensured executable and relied on system masks for the rest.
- dnf5 - avoid generating excessive transaction entries in the dnf5 history (https://github.com/ansible/ansible/issues/85046)
v2.19.0b2
=========
Release Summary
---------------
| Release Date: 2025-04-24
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Minor Changes
-------------
- comment filter - Improve the error message shown when an invalid ``style`` argument is provided.
Bugfixes
--------
- Remove use of `required` parameter in `get_bin_path` which has been deprecated.
- ansible-doc - fix indentation for first line of descriptions of suboptions and sub-return values (https://github.com/ansible/ansible/pull/84690).
- ansible-doc - fix line wrapping for first line of description of options and return values (https://github.com/ansible/ansible/pull/84690).
v2.19.0b1
=========
Release Summary
---------------
| Release Date: 2025-04-14
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__
Major Changes
-------------
- Jinja plugins - Jinja builtin filter and test plugins are now accessible via their fully-qualified names ``ansible.builtin.{name}``.
- Task Execution / Forks - Forks no longer inherit stdio from the parent ``ansible-playbook`` process. ``stdout``, ``stderr``, and ``stdin`` within a worker are detached from the terminal, and non-functional. All needs to access stdio from a fork for controller side plugins requires use of ``Display``.
- ansible-test - Packages beneath ``module_utils`` can now contain ``__init__.py`` files.
- variables - The type system underlying Ansible's variable storage has been significantly overhauled and formalized. Attempts to store unsupported Python object types in variables now more consistently yields early warnings or errors.
- variables - To support new Ansible features, many variable objects are now represented by subclasses of their respective native Python types. In most cases, they behave indistinguishably from their original types, but some Python libraries do not handle builtin object subclasses properly. Custom plugins that interact with such libraries may require changes to convert and pass the native types.
Minor Changes
-------------
- Added a -vvvvv log message indicating when a host fails to produce output within the timeout period.
- AnsibleModule.uri - Add option ``multipart_encoding`` for ``form-multipart`` files in body to change default base64 encoding for files
- INVENTORY_IGNORE_EXTS config, removed ``ini`` from the default list, inventory scripts using a corresponding .ini configuration are rare now and inventory.ini files are more common. Those that need to ignore the ini files for inventory scripts can still add it to configuration.
- Jinja plugins - Plugins can declare support for undefined values.
- Jinja2 version 3.1.0 or later is now required on the controller.
- Move ``follow_redirects`` parameter to module_utils so external modules can reuse it.
- PlayIterator - do not return tasks from already executed roles so specific strategy plugins do not have to do the filtering of such tasks themselves
- SSH Escalation-related -vvv log messages now include the associated host information.
- Windows - Add support for Windows Server 2025 to Ansible and as an ``ansible-test`` remote target - https://github.com/ansible/ansible/issues/84229
- Windows - refactor the async implementation to better handle errors during bootstrapping and avoid WMI when possible.
- ``ansible-galaxy collection install`` — the collection dependency resolver now prints out conflicts it hits during dependency resolution when it's taking too long and it ends up backtracking a lot. It also displays suggestions on how to help it compute the result more quickly.
- ansible, ansible-console, ansible-pull - add --flush-cache option (https://github.com/ansible/ansible/issues/83749).
- ansible-galaxy - Add support for Keycloak service accounts
- ansible-galaxy - support ``resolvelib >= 0.5.3, < 2.0.0`` (https://github.com/ansible/ansible/issues/84217).
- ansible-test - Added a macOS 15.3 remote VM, replacing 14.3.
- ansible-test - Automatically retry HTTP GET/PUT/DELETE requests on exceptions.
- ansible-test - Default to Python 3.13 in the ``base`` and ``default`` containers.
- ansible-test - Disable the ``deprecated-`` prefixed ``pylint`` rules as their results vary by Python version.
- ansible-test - Disable the ``pep8`` sanity test rules ``E701`` and ``E704`` to improve compatibility with ``black``.
- ansible-test - Improve container runtime probe error handling. When unexpected probe output is encountered, an error with more useful debugging information is provided.
- ansible-test - Replace container Alpine 3.20 with 3.21.
- ansible-test - Replace container Fedora 40 with 41.
- ansible-test - Replace remote Alpine 3.20 with 3.21.
- ansible-test - Replace remote Fedora 40 with 41.
- ansible-test - Replace remote FreeBSD 13.3 with 13.5.
- ansible-test - Replace remote FreeBSD 14.1 with 14.2.
- ansible-test - Replace remote RHEL 9.4 with 9.5.
- ansible-test - Show a more user-friendly error message when a ``runme.sh`` script is not executable.
- ansible-test - The ``yamllint`` sanity test now enforces string values for the ``!vault`` tag.
- ansible-test - Update ``nios-test-container`` to version 7.0.0.
- ansible-test - Update ``pylint`` sanity test to use version 3.3.1.
- ansible-test - Update distro containers to remove unnecessary pakages (apache2, subversion, ruby).
- ansible-test - Update sanity test requirements to latest available versions.
- ansible-test - Update the HTTP test container.
- ansible-test - Update the PyPI test container.
- ansible-test - Update the ``base`` and ``default`` containers.
- ansible-test - Update the utility container.
- ansible-test - Use Python's ``urllib`` instead of ``curl`` for HTTP requests.
- ansible-test - When detection of the current container network fails, a warning is now issued and execution continues. This simplifies usage in cases where the current container cannot be inspected, such as when running in GitHub Codespaces.
- ansible-test acme test container - bump `version to 2.3.0 <https://github.com/ansible/acme-test-container/releases/tag/2.3.0>`__ to include newer versions of Pebble, dependencies, and runtimes. This adds support for ACME profiles, ``dns-account-01`` support, and some smaller improvements (https://github.com/ansible/ansible/pull/84547).
- apt_key module - add notes to docs and errors to point at the CLI tool deprecation by Debian and alternatives
- apt_repository module - add notes to errors to point at the CLI tool deprecation by Debian and alternatives
- become plugins get new property 'pipelining' to show support or lack there of for the feature.
- callback plugins - add has_option() to CallbackBase to match other functions overloaded from AnsiblePlugin
- callback plugins - fix get_options() for CallbackBase
- copy - fix sanity test failures (https://github.com/ansible/ansible/pull/83643).
- copy - parameter ``local_follow`` was incorrectly documented as having default value ``True`` (https://github.com/ansible/ansible/pull/83643).
- cron - Provide additional error information while writing cron file (https://github.com/ansible/ansible/issues/83223).
- csvfile - let the config system do the typecasting (https://github.com/ansible/ansible/pull/82263).
- display - Deduplication of warning and error messages considers the full content of the message (including source and traceback contexts, if enabled). This may result in fewer messages being omitted.
- distribution - Added openSUSE MicroOS to Suse OS family (#84685).
- dnf5, apt - add ``auto_install_module_deps`` option (https://github.com/ansible/ansible/issues/84206)
- docs - add collection name in message from which the module is being deprecated (https://github.com/ansible/ansible/issues/84116).
- env lookup - The error message generated for a missing environment variable when ``default`` is an undefined value (e.g. ``undef('something')``) will contain the hint from that undefined value, except when the undefined value is the default of ``undef()`` with no arguments. Previously, any existing undefined hint would be ignored.
- file - enable file module to disable diff_mode (https://github.com/ansible/ansible/issues/80817).
- file - make code more readable and simple.
- filter - add support for URL-safe encoding and decoding in b64encode and b64decode (https://github.com/ansible/ansible/issues/84147).
- find - add a checksum_algorithm parameter to specify which type of checksum the module will return
- from_json filter - The filter accepts a ``profile`` argument, which defaults to ``tagless``.
- handlers - Templated handler names with syntax errors, or that resolve to ``omit`` are now skipped like handlers with undefined variables in their name.
- improved error message for yaml parsing errors in plugin documentation
- local connection plugin - A new ``become_strip_preamble`` config option (default True) was added; disable to preserve diagnostic ``become`` output in task results.
- local connection plugin - A new ``become_success_timeout`` operation-wide timeout config (default 10s) was added for ``become``.
- local connection plugin - When a ``become`` plugin's ``prompt`` value is a non-string after the ``check_password_prompt`` callback has completed, no prompt stripping will occur on stderr.
- lookup_template - add an option to trim blocks while templating (https://github.com/ansible/ansible/issues/75962).
- module - set ipv4 and ipv6 rules simultaneously in iptables module (https://github.com/ansible/ansible/issues/84404).
- module_utils - Add ``NoReturn`` type annotations to functions which never return.
- modules - PowerShell modules can now receive ``datetime.date``, ``datetime.time`` and ``datetime.datetime`` values as ISO 8601 strings.
- modules - PowerShell modules can now receive strings sourced from inline vault-encrypted strings.
- modules - Unhandled exceptions during Python module execution are now returned as structured data from the target. This allows the new traceback handling to be applied to exceptions raised on targets.
- pipelining logic has mostly moved to connection plugins so they can decide/override settings.
- plugin error handling - When raising exceptions in an exception handler, be sure to use ``raise ... from`` as appropriate. This supersedes the use of the ``AnsibleError`` arg ``orig_exc`` to represent the cause. Specifying ``orig_exc`` as the cause is still permitted. Failure to use ``raise ... from`` when ``orig_exc`` is set will result in a warning. Additionally, if the two cause exceptions do not match, a warning will be issued.
- removed harcoding of su plugin as it now works with pipelining.
- runtime-metadata sanity test - improve validation of ``action_groups`` (https://github.com/ansible/ansible/pull/83965).
- service_facts module got freebsd support added.
- ssh connection plugin - Support ``SSH_ASKPASS`` mechanism to provide passwords, making it the default, but still offering an explicit choice to use ``sshpass`` (https://github.com/ansible/ansible/pull/83936)
- ssh connection plugin now overrides pipelining when a tty is requested.
- ssh-agent - ``ansible``, ``ansible-playbook`` and ``ansible-console`` are capable of spawning or reusing an ssh-agent, allowing plugins to interact with the ssh-agent. Additionally a pure python ssh-agent client has been added, enabling easy interaction with the agent. The ssh connection plugin contains new functionality via ``ansible_ssh_private_key`` and ``ansible_ssh_private_key_passphrase``, for loading an SSH private key into the agent from a variable.
- templating - Access to an undefined variable from inside a lookup, filter, or test (which raises MarkerError) no longer ends processing of the current template. The triggering undefined value is returned as the result of the offending plugin invocation, and the template continues to execute.
- templating - Embedding ``range()`` values in containers such as lists will result in an error on use. Previously the value would be converted to a string representing the range parameters, such as ``range(0, 3)``.
- templating - Handling of omitted values is now a first-class feature of the template engine, and is usable in all Ansible Jinja template contexts. Any template that resolves to ``omit`` is automatically removed from its parent container during templating.
- templating - Template evaluation is lazier than in previous versions. Template expressions which resolve only portions of a data structure no longer result in the entire structure being templated.
- templating - Templating errors now provide more information about both the location and context of the error, especially for deeply-nested and/or indirected templating scenarios.
- templating - Unified ``omit`` behavior now requires that plugins calling ``Templar.template()`` handle cases where the entire template result is omitted, by catching the ``AnsibleValueOmittedError`` that is raised. Previously, this condition caused a randomly-generated string marker to appear in the template result.
- templating - Variables of type ``set`` and ``tuple`` are now converted to ``list`` when exiting the final pass of templating.
- to_json / to_nice_json filters - The filters accept a ``profile`` argument, which defaults to ``tagless``.
- troubleshooting - Tracebacks can be collected and displayed for most errors, warnings, and deprecation warnings (including those generated by modules). Tracebacks are no longer enabled with ``-vvv``; the behavior is directly configurable via the ``DISPLAY_TRACEBACK`` config option. Module tracebacks passed to ``fail_json`` via the ``exception`` kwarg will not be included in the task result unless error tracebacks are configured.
- undef jinja function - The ``undef`` jinja function now raises an error if a non-string hint is given. Attempting to use an undefined hint also results in an error, ensuring incorrect use of the function can be distinguished from the function's normal behavior.
- validate-modules sanity test - make sure that ``module`` and ``plugin`` ``seealso`` entries use FQCNs (https://github.com/ansible/ansible/pull/84325).
- vault - improved vault filter documentation by adding missing example content for dump_template_data.j2, refining examples for clarity, and ensuring variable consistency (https://github.com/ansible/ansible/issues/83583).
- warnings - All warnings (including deprecation warnings) issued during a task's execution are now accessible via the ``warnings`` and ``deprecations`` keys on the task result.
- when the ``dict`` lookup is given a non-dict argument, show the value of the argument and its type in the error message.
- windows - add hard minimum limit for PowerShell to 5.1. Ansible dropped support for older versions of PowerShell in the 2.16 release but this reqirement is now enforced at runtime.
- windows - refactor windows exec runner to improve efficiency and add better error reporting on failures.
- winrm - Remove need for pexpect on macOS hosts when using ``kinit`` to retrieve the Kerberos TGT. By default the code will now only use the builtin ``subprocess`` library which should handle issues with select and a high fd count and also simplify the code.
Breaking Changes / Porting Guide
--------------------------------
- Support for the ``toml`` library has been removed from TOML inventory parsing and dumping. Use ``tomli`` for parsing on Python 3.10. Python 3.11 and later have built-in support for parsing. Use ``tomli-w`` to support outputting inventory in TOML format.
- assert - The ``quiet`` argument must be a commonly-accepted boolean value. Previously, unrecognized values were silently treated as False.
- conditionals - Conditional expressions that result in non-boolean values are now an error by default. Such results often indicate unintentional use of templates where they are not supported, resulting in a conditional that is always true. When this option is enabled, conditional expressions which are a literal ``None`` or empty string will evaluate as true, for backwards compatibility. The error can be temporarily changed to a deprecation warning by enabling the ``ALLOW_BROKEN_CONDITIONALS`` config option.
- first_found lookup - When specifying ``files`` or ``paths`` as a templated list containing undefined values, the undefined list elements will be discarded with a warning. Previously, the entire list would be discarded without any warning.
- internals - The ``AnsibleLoader`` and ``AnsibleDumper`` classes for working with YAML are now factory functions and cannot be extended.
- internals - The ``ansible.utils.native_jinja`` Python module has been removed.
- inventory - Invalid variable names provided by inventories result in an inventory parse failure. This behavior is now consistent with other variable name usages throughout Ansible.
- lookup plugins - Lookup plugins called as `with_(lookup)` will no longer have the `_subdir` attribute set.
- lookup plugins - ``terms`` will always be passed to ``run`` as the first positional arg, where previously it was sometimes passed as a keyword arg when using ``with_`` syntax.
- loops - Omit placeholders no longer leak between loop item templating and task templating. Previously, ``omit`` placeholders could remain embedded in loop items after templating and be used as an ``omit`` for task templating. Now, values resolving to ``omit`` are dropped immediately when loop items are templated. To turn missing values into an ``omit`` for task templating, use ``| default(omit)``. This solution is backward-compatible with previous versions of ansible-core.
- modules - Ansible modules using ``sys.excepthook`` must use a standard ``try/except`` instead.
- plugins - Any plugin that sources or creates templates must properly tag them as trusted.
- plugins - Custom Jinja plugins that accept undefined top-level arguments must opt in to receiving them.
- plugins - Custom Jinja plugins that use ``environment.getitem`` to retrieve undefined values will now trigger a ``MarkerError`` exception. This exception must be handled to allow the plugin to return a ``Marker``, or the plugin must opt-in to accepting ``Marker`` values.
- public API - The ``ansible.vars.fact_cache.FactCache`` wrapper has been removed.
- serialization of ``omit`` sentinel - Serialization of variables containing ``omit`` sentinels (e.g., by the ``to_json`` and ``to_yaml`` filters or ``ansible-inventory``) will fail if the variable has not completed templating. Previously, serialization succeeded with placeholder strings emitted in the serialized output.
- set_fact - The string values "yes", "no", "true" and "false" were previously converted (ignoring case) to boolean values when not using Jinja2 native mode. Since Jinja2 native mode is always used, this conversion no longer occurs. When boolean values are required, native boolean syntax should be used where variables are defined, such as in YAML. When native boolean syntax is not an option, the ``bool`` filter can be used to parse string values into booleans.
- template lookup - The ``convert_data`` option is deprecated and no longer has any effect. Use the ``from_json`` filter on the lookup result instead.
- templating - Access to ``_`` prefixed attributes and methods, and methods with known side effects, is no longer permitted. In cases where a matching mapping key is present, the associated value will be returned instead of an error. This increases template environment isolation and ensures more consistent behavior between the ``.`` and ``[]`` operators.
- templating - Conditionals and lookups which use embedded inline templates in Jinja string constants now display a warning. These templates should be converted to their expression equivalent.
- templating - Many Jinja plugins (filters, lookups, tests) and methods previously silently ignored undefined inputs, which often masked subtle errors. Passing an undefined argument to a Jinja plugin or method that does not declare undefined support now results in an undefined value.
- templating - Templates are always rendered in Jinja2 native mode. As a result, non-string values are no longer automatically converted to strings.
- templating - Templates resulting in ``None`` are no longer automatically converted to an empty string.
- templating - Templates with embedded inline templates that were not contained within a Jinja string constant now result in an error, as support for multi-pass templating was removed for security reasons. In most cases, such templates can be easily rewritten to avoid the use of embedded inline templates.
- templating - The ``allow_unsafe_lookups`` option no longer has any effect. Lookup plugins are responsible for tagging strings containing templates to allow evaluation as a template.
- templating - The result of the ``range()`` global function cannot be returned from a template- it should always be passed to a filter (e.g., ``random``). Previously, range objects returned from an intermediate template were always converted to a list, which is inconsistent with inline consumption of range objects.
- templating - ``#jinja2:`` overrides in templates with invalid override names or types are now templating errors.
Deprecated Features
-------------------
- CLI - The ``--inventory-file`` option alias is deprecated. Use the ``-i`` or ``--inventory`` option instead.
- Stategy Plugins - Use of strategy plugins not provided in ``ansible.builtin`` are deprecated and do not carry any backwards compatibility guarantees going forward. A future release will remove the ability to use external strategy plugins. No alternative for third party strategy plugins is currently planned.
- ``ansible.module_utils.compat.datetime`` - The datetime compatibility shims are now deprecated. They are scheduled to be removed in ``ansible-core`` v2.21. This includes ``UTC``, ``utcfromtimestamp()`` and ``utcnow`` importable from said module (https://github.com/ansible/ansible/pull/81874).
- bool filter - Support for coercing unrecognized input values (including None) has been deprecated. Consult the filter documentation for acceptable values, or consider use of the ``truthy`` and ``falsy`` tests.
- cache plugins - The `ansible.plugins.cache.base` Python module is deprecated. Use `ansible.plugins.cache` instead.
- callback plugins - The `v2_on_any` callback method is deprecated. Use specific callback methods instead.
- callback plugins - The v1 callback API (callback methods not prefixed with `v2_`) is deprecated. Use `v2_` prefixed methods instead.
- conditionals - Conditionals using Jinja templating delimiters (e.g., ``{{``, ``{%``) should be rewritten as expressions without delimiters, unless the entire conditional value is a single template that resolves to a trusted string expression. This is useful for dynamic indirection of conditional expressions, but is limited to trusted literal string expressions.
- config - The ``ACTION_WARNINGS`` config has no effect. It previously disabled command warnings, which have since been removed.
- config - The ``DEFAULT_JINJA2_NATIVE`` option has no effect. Jinja2 native mode is now the default and only option.
- config - The ``DEFAULT_NULL_REPRESENTATION`` option has no effect. Null values are no longer automatically converted to another value during templating of single variable references.
- display - The ``Display.get_deprecation_message`` method has been deprecated. Call ``Display.deprecated`` to display a deprecation message, or call it with ``removed=True`` to raise an ``AnsibleError``.
- file loading - Loading text files with ``DataLoader`` containing data that cannot be decoded under the expected encoding is deprecated. In most cases the encoding must be UTF-8, although some plugins allow choosing a different encoding. Previously, invalid data was silently wrapped in Unicode surrogate escape sequences, often resulting in later errors or other data corruption.
- first_found lookup - Splitting of file paths on ``,;:`` is deprecated. Pass a list of paths instead. The ``split`` method on strings can be used to split variables into a list as needed.
- interpreter discovery - The ``auto_legacy`` and ``auto_legacy_silent`` options for ``INTERPRETER_PYTHON`` are deprecated. Use ``auto`` or ``auto_silent`` options instead, as they have the same effect.
- oneline callback - The ``oneline`` callback and its associated ad-hoc CLI args (``-o``, ``--one-line``) are deprecated.
- paramiko - The paramiko connection plugin has been deprecated with planned removal in 2.21.
- playbook variables - The ``play_hosts`` variable has been deprecated, use ``ansible_play_batch`` instead.
- plugin error handling - The ``AnsibleError`` constructor arg ``suppress_extended_error`` is deprecated. Using ``suppress_extended_error=True`` has the same effect as ``show_content=False``.
- template lookup - The jinja2_native option is no longer used in the Ansible Core code base. Jinja2 native mode is now the default and only option.
- templating - Support for enabling Jinja2 extensions (not plugins) has been deprecated.
- templating - The ``disable_lookups`` option has no effect, since plugins must be updated to apply trust before any templating can be performed.
- tree callback - The ``tree`` callback and its associated ad-hoc CLI args (``-t``, ``--tree``) are deprecated.
Removed Features (previously deprecated)
----------------------------------------
- Remove deprecated plural form of collection path (https://github.com/ansible/ansible/pull/84156).
- Removed deprecated STRING_CONVERSION_ACTION (https://github.com/ansible/ansible/issues/84220).
- encrypt - passing unsupported passlib hashtype now raises AnsibleFilterError.
- manager - remove deprecated include_delegate_to parameter from get_vars API.
- modules - Modules returning non-UTF8 strings now result in an error. The ``MODULE_STRICT_UTF8_RESPONSE`` setting can be used to disable this check.
- removed deprecated pycompat24 and compat.importlib.
- selector - remove deprecated compat.selector related files (https://github.com/ansible/ansible/pull/84155).
- windows - removed common module functions ``ConvertFrom-AnsibleJson``, ``Format-AnsibleException`` from Windows modules as they are not used and add uneeded complexity to the code.
Security Fixes
--------------
- include_vars action - Ensure that result masking is correctly requested when vault-encrypted files are read. (CVE-2024-8775)
- task result processing - Ensure that action-sourced result masking (``_ansible_no_log=True``) is preserved. (CVE-2024-8775)
- templating - Ansible's template engine no longer processes Jinja templates in strings unless they are marked as coming from a trusted source. Untrusted strings containing Jinja template markers are ignored with a warning. Examples of trusted sources include playbooks, vars files, and many inventory sources. Examples of untrusted sources include module results and facts. Plugins which have not been updated to preserve trust while manipulating strings may inadvertently cause them to lose their trusted status.
- templating - Changes to conditional expression handling removed numerous instances of insecure multi-pass templating (which could result in execution of untrusted template expressions).
- user action won't allow ssh-keygen, chown and chmod to run on existing ssh public key file, avoiding traversal on existing symlinks (CVE-2024-9902).
Bugfixes
--------
- Ansible will now also warn when reserved keywords are set via a module (set_fact, include_vars, etc).
- Ansible.Basic - Fix ``required_if`` check when the option value to check is unset or set to null.
- Correctly return ``False`` when using the ``filter`` and ``test`` Jinja tests on plugin names which are not filters or tests, respectively. (resolves issue https://github.com/ansible/ansible/issues/82084)
- Do not run implicit ``flush_handlers`` meta tasks when the whole play is excluded from the run due to tags specified.
- Errors now preserve stacked error messages even when YAML is involved.
- Fix a display.debug statement with the wrong param in _get_diff_data() method
- Fix disabling SSL verification when installing collections and roles from git repositories. If ``--ignore-certs`` isn't provided, the value for the ``GALAXY_IGNORE_CERTS`` configuration option will be used (https://github.com/ansible/ansible/issues/83326).
- Fix ipv6 pattern bug in lib/ansible/parsing/utils/addresses.py (https://github.com/ansible/ansible/issues/84237)
- Fix returning 'unreachable' for the overall task result. This prevents false positives when a looped task has unignored unreachable items (https://github.com/ansible/ansible/issues/84019).
- Implicit ``meta: flush_handlers`` tasks now have a parent block to prevent potential tracebacks when calling methods like ``get_play()`` on them internally.
- Improve performance on large inventories by reducing the number of implicit meta tasks.
- Jinja plugins - Errors raised will always be derived from ``AnsibleTemplatePluginError``.
- Optimize the way tasks from within ``include_tasks``/``include_role`` are inserted into the play.
- Time out waiting on become is an unreachable error (https://github.com/ansible/ansible/issues/84468)
- Use consistent multiprocessing context for action write locks
- Use the requested error message in the ansible.module_utils.facts.timeout timeout function instead of hardcoding one.
- Windows - add support for running on system where WDAC is in audit mode with ``Dynamic Code Security`` enabled.
- YAML parsing - The `!unsafe` tag no longer coerces non-string scalars to strings.
- ``ansible-galaxy`` — the collection dependency resolver now treats version specifiers starting with ``!=`` as unpinned.
- ``package``/``dnf`` action plugins - provide the reason behind the failure to gather the ``ansible_pkg_mgr`` fact to identify the package backend
- action plugins - Action plugins that raise unhandled exceptions no longer terminate playbook loops. Previously, exceptions raised by an action plugin caused abnormal loop termination and loss of loop iteration results.
- ansible-config - format galaxy server configs while dumping in JSON format (https://github.com/ansible/ansible/issues/84840).
- ansible-doc - If none of the files in files exists, path will be undefined and a direct reference will throw an UnboundLocalError (https://github.com/ansible/ansible/pull/84464).
- ansible-galaxy - Small adjustments to URL building for ``download_url`` and relative redirects.
- ansible-pull change detection will now work independently of callback or result format settings.
- ansible-test - Enable the ``sys.unraisablehook`` work-around for the ``pylint`` sanity test on Python 3.11. Previously the work-around was only enabled for Python 3.12 and later. However, the same issue has been discovered on Python 3.11.
- ansible-test - Ensure CA certificates are installed on managed FreeBSD instances.
- ansible-test - Fix support for PowerShell module_util imports with the ``-Optional`` flag.
- ansible-test - Fix support for detecting PowerShell modules importing module utils with the newer ``#AnsibleRequires`` format.
- ansible-test - Fix traceback that occurs after an interactive command fails.
- ansible-test - Fix up coverage reporting to properly translate the temporary path of integration test modules to the expected static test module path.
- ansible-test - Fixed traceback when handling certain YAML errors in the ``yamllint`` sanity test.
- ansible-test - Managed macOS instances now use the ``sudo_chdir`` option for the ``sudo`` become plugin to avoid permission errors when dropping privileges.
- ansible-vault will now correctly handle `--prompt`, previously it would issue an error about stdin if no 2nd argument was passed
- ansible_uptime_second - added ansible_uptime_seconds fact support for AIX (https://github.com/ansible/ansible/pull/84321).
- apt_key module - prevent tests from running when apt-key was removed
- base.yml - deprecated libvirt_lxc_noseclabel config.
- build - Pin ``wheel`` in ``pyproject.toml`` to ensure compatibility with supported ``setuptools`` versions.
- config - various fixes to config lookup plugin (https://github.com/ansible/ansible/pull/84398).
- copy - refactor copy module for simplicity.
- copy action now prevents user from setting internal options.
- debconf - set empty password values (https://github.com/ansible/ansible/issues/83214).
- debug - hide loop vars in debug var display (https://github.com/ansible/ansible/issues/65856).
- default callback - Error context is now shown for failing tasks that use the ``debug`` action.
- display - The ``Display.deprecated`` method once again properly handles the ``removed=True`` argument (https://github.com/ansible/ansible/issues/82358).
- distro - add support for Linux Mint Debian Edition (LMDE) (https://github.com/ansible/ansible/issues/84934).
- distro - detect Debian as os_family for LMDE 6 (https://github.com/ansible/ansible/issues/84934).
- dnf5 - Handle forwarded exceptions from dnf5-5.2.13 where a generic ``RuntimeError`` was previously raised
- dnf5 - fix ``is_installed`` check for packages that are not installed but listed as provided by an installed package (https://github.com/ansible/ansible/issues/84578)
- dnf5 - fix installing a package using ``state=latest`` when a binary of the same name as the package is already installed (https://github.com/ansible/ansible/issues/84259)
- dnf5 - fix traceback when ``enable_plugins``/``disable_plugins`` is used on ``python3-libdnf5`` versions that do not support this functionality
- dnf5 - libdnf5 - use ``conf.pkg_gpgcheck`` instead of deprecated ``conf.gpgcheck`` which is used only as a fallback
- dnf5 - matching on a binary can be achieved only by specifying a full path (https://github.com/ansible/ansible/issues/84334)
- facts - gather pagesize and calculate respective values depending upon architecture (https://github.com/ansible/ansible/issues/84773).
- facts - skip if distribution file path is directory, instead of raising error (https://github.com/ansible/ansible/issues/84006).
- find - skip ENOENT error code while recursively enumerating files. find module will now be tolerant to race conditions that remove files or directories from the target it is currently inspecting. (https://github.com/ansible/ansible/issues/84873).
- first_found lookup - Corrected return value documentation to reflect None (not empty string) for no files found.
- gather_facts action now defaults to `ansible.legacy.setup` if `smart` was set, no network OS was found and no other alias for `setup` was present.
- gather_facts action will now issues errors and warnings as appropriate if a network OS is detected but no facts modules are defined for it.
- gather_facts action, will now add setup when 'smart' appears with other modules in the FACTS_MODULES setting (#84750).
- get_url - add support for BSD-style checksum digest file (https://github.com/ansible/ansible/issues/84476).
- get_url - fix honoring ``filename`` from the ``content-disposition`` header even when the type is ``inline`` (https://github.com/ansible/ansible/issues/83690)
- host_group_vars - fixed defining the 'key' variable if the get_vars method is called with cache=False (https://github.com/ansible/ansible/issues/84384)
- include_vars - fix including previously undefined hash variables with hash_behaviour merge (https://github.com/ansible/ansible/issues/84295).
- iptables - Allows the wait parameter to be used with iptables chain creation (https://github.com/ansible/ansible/issues/84490)
- linear strategy - fix executing ``end_role`` meta tasks for each host, instead of handling these as implicit run_once tasks (https://github.com/ansible/ansible/issues/84660).
- local connection plugin - Become timeout errors now include all received data. Previously, the most recently-received data was discarded.
- local connection plugin - Ensure ``become`` success validation always occurs, even when an active plugin does not set ``prompt``.
- local connection plugin - Fixed cases where the internal ``BECOME-SUCCESS`` message appeared in task output.
- local connection plugin - Fixed hang or spurious failure when data arrived concurrently on stdout and stderr during a successful ``become`` operation validation.
- local connection plugin - Fixed hang when a become plugin expects a prompt but a password was not provided.
- local connection plugin - Fixed hang when an active become plugin incorrectly signals lack of prompt.
- local connection plugin - Fixed hang when an internal become read timeout expired before the password prompt was written.
- local connection plugin - Fixed hang when only one of stdout or stderr was closed by the ``become_exe`` subprocess.
- local connection plugin - Fixed long timeout/hang for ``become`` plugins that repeat their prompt on failure (e.g., ``sudo``, some ``su`` implementations).
- local connection plugin - Fixed silent ignore of ``become`` failures and loss of task output when data arrived concurrently on stdout and stderr during ``become`` operation validation.
- local connection plugin - Fixed task output header truncation when post-become data arrived before ``become`` operation validation had completed.
- lookup plugins - The ``terms`` arg to the ``run`` method is now always a list. Previously, there were cases where a non-list could be received.
- module arg templating - When using a templated raw task arg and a templated ``args`` keyword, args are now merged. Previously use of templated raw task args silently ignored all values from the templated ``args`` keyword.
- module defaults - Module defaults are no longer templated unless they are used by a task that does not override them. Previously, all module defaults for all modules were templated for every task.
- module respawn - limit to supported Python versions
- package_facts module when using 'auto' will return the first package manager found that provides an output, instead of just the first one, as this can be foreign and not have any packages.
- psrp - Improve stderr parsing when running raw commands that emit error records or stderr lines.
- regex_search filter - Corrected return value documentation to reflect None (not empty string) for no match.
- respawn - use copy of env variables to update existing PYTHONPATH value (https://github.com/ansible/ansible/issues/84954).
- runas become - Fix up become logic to still get the SYSTEM token with the most privileges when running as SYSTEM.
- sequence lookup - sequence query/lookups without positional arguments now return a valid list if their kwargs comprise a valid sequence expression (https://github.com/ansible/ansible/issues/82921).
- service_facts - skip lines which does not contain service names in openrc output (https://github.com/ansible/ansible/issues/84512).
- ssh - Improve the logic for parsing CLIXML data in stderr when working with Windows host. This fixes issues when the raw stderr contains invalid UTF-8 byte sequences and improves embedded CLIXML sequences.
- ssh - Raise exception when sshpass returns error code (https://github.com/ansible/ansible/issues/58133).
- ssh - connection options were incorrectly templated during ``reset_connection`` tasks (https://github.com/ansible/ansible/pull/84238).
- stability - Fixed silent process failure on unhandled IOError/OSError under ``linear`` strategy.
- su become plugin - Ensure generated regex from ``prompt_l10n`` config values is properly escaped.
- su become plugin - Ensure that password prompts are correctly detected in the presence of leading output. Previously, this case resulted in a timeout or hang.
- su become plugin - Ensure that trailing colon is expected on all ``prompt_l10n`` config values.
- sudo become plugin - The `sudo_chdir` config option allows the current directory to be set to the specified value before executing sudo to avoid permission errors when dropping privileges.
- sunos - remove hard coding of virtinfo command in facts gathering code (https://github.com/ansible/ansible/pull/84357).
- to_yaml/to_nice_yaml filters - Eliminated possibility of keyword arg collisions with internally-set defaults.
- unarchive - Clamp timestamps from beyond y2038 to representible values when unpacking zip files on platforms that use 32-bit time_t (e.g. Debian i386).
- uri - Form location correctly when the server returns a relative redirect (https://github.com/ansible/ansible/issues/84540)
- uri - Handle HTTP exceptions raised while reading the content (https://github.com/ansible/ansible/issues/83794).
- uri - mark ``url`` as required (https://github.com/ansible/ansible/pull/83642).
- user - Create Buildroot subclass as alias to Busybox (https://github.com/ansible/ansible/issues/83665).
- user - Set timeout for passphrase interaction.
- user - Update prompt for SSH key passphrase (https://github.com/ansible/ansible/issues/84484).
- user - Use higher precedence HOME_MODE as UMASK for path provided (https://github.com/ansible/ansible/pull/84482).
- user action will now require O(force) to overwrite the public part of an ssh key when generating ssh keys, as was already the case for the private part.
- user module now avoids changing ownership of files symlinked in provided home dir skeleton
- vars lookup - The ``default`` substitution only applies when trying to look up a variable which is not defined. If the variable is defined, but templates to an undefined value, the ``default`` substitution will not apply. Use the ``default`` filter to coerce those values instead.
- wait_for_connection - a warning was displayed if any hosts used a local connection (https://github.com/ansible/ansible/issues/84419)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-04-14
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-04-24
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-05-06
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-05-12
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-06-03
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -0,0 +1,3 @@
release_summary: |
| Release Date: 2025-06-11
| `Porting Guide <https://docs.ansible.com/ansible-core/2.19/porting_guides/porting_guide_core_2.19.html>`__

@ -1,2 +1,2 @@
bugfixes:
- iptables - Allows the wait paramater to be used with iptables chain creation (https://github.com/ansible/ansible/issues/84490)
- iptables - Allows the wait parameter to be used with iptables chain creation (https://github.com/ansible/ansible/issues/84490)

@ -0,0 +1,2 @@
bugfixes:
- "dnf5 - handle all libdnf5 specific exceptions (https://github.com/ansible/ansible/issues/84634)"

@ -0,0 +1,3 @@
bugfixes:
- "ansible-doc - fix indentation for first line of descriptions of suboptions and sub-return values (https://github.com/ansible/ansible/pull/84690)."
- "ansible-doc - fix line wrapping for first line of description of options and return values (https://github.com/ansible/ansible/pull/84690)."

@ -0,0 +1,2 @@
bugfixes:
- get_url - add a check to recognize incomplete data transfers.

@ -0,0 +1,2 @@
bugfixes:
- include_tasks - fix templating options when used as a handler (https://github.com/ansible/ansible/pull/85015).

@ -0,0 +1,2 @@
bugfixes:
- dnf5 - avoid generating excessive transaction entries in the dnf5 history (https://github.com/ansible/ansible/issues/85046)

@ -0,0 +1,2 @@
minor_changes:
- facts - add "CloudStack KVM Hypervisor" for Linux VM in virtual facts (https://github.com/ansible/ansible/issues/85089).

@ -0,0 +1,2 @@
minor_changes:
- facts - add "Linode" for Linux VM in virtual facts

@ -0,0 +1,2 @@
bugfixes:
- ansible-doc will no longer ignore docs for modules without an extension (https://github.com/ansible/ansible/issues/85279).

@ -0,0 +1,5 @@
minor_changes:
- ansiballz - Refactored AnsiballZ and module respawn.
- ansiballz - Added support for AnsiballZ extensions.
- ansiballz - Moved AnsiballZ code coverage support into an extension.
- ansiballz - Added an experimental AnsiballZ extension for remote debugging.

@ -0,0 +1,2 @@
minor_changes:
- ansible-doc - Return dynamic stub when reporting on Jinja filters and tests not explicitly documented in Ansible

@ -0,0 +1,2 @@
minor_changes:
- ansible-doc - Skip listing the internal ``ansible._protomatter`` plugins unless explicitly requested

@ -0,0 +1,3 @@
minor_changes:
- ansible-test - Use the ``-t`` option to set the stop timeout when stopping a container.
This avoids use of the ``--time`` option which was deprecated in Docker v28.0.

@ -0,0 +1,5 @@
bugfixes:
- ansible-test - Fix incorrect handling of options with optional args (e.g. ``--color``),
when followed by other options which are omitted during arg filtering (e.g. ``--docker``).
Previously it was possible for non-option arguments to be incorrectly omitted in these cases.
(https://github.com/ansible/ansible/issues/85173)

@ -0,0 +1,2 @@
bugfixes:
- ansible-test - Updated the ``pylint`` sanity test to skip some deprecation validation checks when all arguments are dynamic.

@ -0,0 +1,3 @@
bugfixes:
- ansible-test - Improve type inference for pylint deprecated checks to accommodate some type annotations.
- ansible-test - Disabled the ``bad-super-call`` pylint rule due to false positives.

@ -0,0 +1,2 @@
minor_changes:
- ansible-test - Add RHEL 10.0 as a remote platform for testing.

@ -0,0 +1,2 @@
minor_changes:
- "apt_repository - remove Python 2 support"

@ -0,0 +1,2 @@
bugfixes:
- async_status module - The ``started`` and ``finished`` return values are now ``True`` or ``False`` instead of ``1`` or ``0``.

@ -0,0 +1,3 @@
---
minor_changes:
- comment filter - Improve the error message shown when an invalid ``style`` argument is provided.

@ -0,0 +1,2 @@
minor_changes:
- ansible-config will now show internal, but not test configuration entries. This allows for debugging but still denoting the configurations as internal use only (_ prefix).

@ -0,0 +1,4 @@
bugfixes:
- >-
constructed inventory - Use the ``default_value`` or ``trailing_separator`` in a ``keyed_groups`` entry if the expression result of ``key``
is ``None`` and not just an empty string.

@ -0,0 +1,2 @@
deprecated_features:
- "``ansible.compat.importlib_resources`` is deprecated and will be removed in ansible-core 2.23. Use ``importlib.resources`` from the Python standard library instead."

@ -0,0 +1,2 @@
deprecated_features:
- "The ``ShellModule.checksum`` method is now deprecated and will be removed in ansible-core 2.23. Use ``ActionBase._execute_remote_stat()`` instead."

@ -0,0 +1,17 @@
minor_changes:
- modules - The ``AnsibleModule.deprecate`` function no longer sends deprecation messages to the target host's logging system.
- ansible-test - Improved ``pylint`` checks for Ansible-specific deprecation functions.
- deprecations - Removed support for specifying deprecation dates as a ``datetime.date``, which was included in an earlier 2.19 pre-release.
- deprecations - Some argument names to ``deprecate_value`` for consistency with existing APIs.
An earlier 2.19 pre-release included a ``removal_`` prefix on the ``date`` and ``version`` arguments.
- deprecations - Collection name strings not of the form ``ns.coll`` passed to deprecation API functions will result in an error.
- collection metadata - The collection loader now parses scalar values from ``meta/runtime.yml`` as strings.
This avoids issues caused by unquoted values such as versions or dates being parsed as types other than strings.
- deprecation warnings - Deprecation warning APIs automatically capture the identity of the deprecating plugin.
The ``collection_name`` argument is only required to correctly attribute deprecations that occur in module_utils or other non-plugin code.
- deprecation warnings - Improved deprecation messages to more clearly indicate the affected content, including plugin name when available.
deprecated_features:
- plugins - Accessing plugins with ``_``-prefixed filenames without the ``_`` prefix is deprecated.
- Passing a ``warnings` or ``deprecations`` key to ``exit_json`` or ``fail_json`` is deprecated.
Use ``AnsibleModule.warn`` or ``AnsibleModule.deprecate`` instead.

@ -0,0 +1,3 @@
minor_changes:
- Improved SUSE distribution detection in distribution.py by parsing VARIANT_ID from /etc/os-release
for identifying SLES_SAP and SL-Micro. Falls back to /etc/products.d/baseproduct symlink for older systems.

@ -0,0 +1,4 @@
minor_changes:
- display - Replace Windows newlines (``\r\n``) in display output with Unix newlines (``\n``).
This ensures proper display of strings sourced from Windows hosts in environments which treat ``\r`` as ``\n``,
such as Azure Pipelines.

@ -0,0 +1,2 @@
bugfixes:
- "dnf5 - when ``bugfix`` and/or ``security`` is specified, skip packages that do not have any such updates, even for new versions of libdnf5 where this functionality changed and it is considered failure"

@ -0,0 +1,2 @@
bugfixes:
- Ansible will now ensure predictable permissions on remote artifacts, until now it only ensured executable and relied on system masks for the rest.

@ -0,0 +1,15 @@
bugfixes:
- config - ``ensure_type`` correctly propagates trust and other tags on returned values.
- config - Prevented fatal errors when ``MODULE_IGNORE_EXTS`` configuration was set.
- config - ``ensure_type`` with expected type ``int`` now properly converts ``True`` and ``False`` values to ``int``.
Previously, these values were silently returned unmodified.
- config - ``ensure_type`` now reports an error when ``bytes`` are provided for any known ``value_type``.
Previously, the behavior was undefined, but often resulted in an unhandled exception or incorrect return type.
- config - ``ensure_type`` now converts sequences to ``list`` when requested, instead of returning the sequence.
- config - ``ensure_type`` now converts mappings to ``dict`` when requested, instead of returning the mapping.
- config - ``ensure_type`` now correctly errors when ``pathlist`` or ``pathspec`` types encounter non-string list items.
- config - Templating failures on config defaults now issue a warning.
Previously, failures silently returned an unrendered and untrusted template to the caller.
- convert_bool.boolean API conversion function - Unhashable values passed to ``boolean`` behave like other non-boolean convertible values,
returning False or raising ``TypeError`` depending on the value of ``strict``.
Previously, unhashable values always raised ``ValueError`` due to an invalid set membership check.

@ -0,0 +1,2 @@
bugfixes:
- from_yaml_all filter - `None` and empty string inputs now always return an empty list. Previously, `None` was returned in Jinja native mode and empty list in classic mode.

@ -0,0 +1,2 @@
bugfixes:
- "Remove use of `required` parameter in `get_bin_path` which has been deprecated."

@ -0,0 +1,2 @@
minor_changes:
- "Use ``importlib.metadata.version()`` to detect Jinja version as jinja2.__version__ is deprecated and will be removed in Jinja 3.3."

@ -0,0 +1,2 @@
bugfixes:
- local connection plugin - The command-line used to create subprocesses is now always ``str`` to avoid issues with debuggers and profilers.

@ -0,0 +1,2 @@
minor_changes:
- "csvfile lookup - remove Python 2 compat"

@ -0,0 +1,3 @@
bugfixes:
- templating - Jinja macros returned from a template expression can now be called from another template expression.
- templating - Fixed cases where template expression blocks halted prematurely when a Jinja macro invocation returned an undefined value.

@ -0,0 +1,2 @@
deprecated_features:
- "The ``ansible.module_utils.common.collections.count()`` function is deprecated and will be removed in ansible-core 2.23. Use ``collections.Counter()`` from the Python standard library instead."

@ -0,0 +1,5 @@
minor_changes:
- module_utils - Add optional ``help_text`` argument to ``AnsibleModule.warn``.
- module_utils - Add ``AnsibleModule.error_as_warning``.
- module_utils - Add ``ansible.module_utils.common.warnings.error_as_warning``.
- display - Add ``help_text`` and ``obj`` to ``Display.error_as_warning``.

@ -0,0 +1,2 @@
bugfixes:
- plugin loader - Apply template trust to strings loaded from plugin configuration definitions and doc fragments.

@ -0,0 +1,2 @@
bugfixes:
- display - Fix hang caused by early post-fork writers to stdout/stderr (e.g., pydevd) encountering an unreleased fork lock.

@ -0,0 +1,2 @@
bugfixes:
- config - Preserve or apply Origin tag to values returned by config.

@ -0,0 +1,2 @@
minor_changes:
- "modules - use ``AnsibleModule.warn`` instead of passing ``warnings`` to ``exit_json`` or ``fail_json`` which is deprecated."

@ -0,0 +1,2 @@
minor_changes:
- templating - Added ``_ANSIBLE_TEMPLAR_SANDBOX_MODE=allow_unsafe_attributes`` environment variable to disable Jinja template attribute sandbox. (https://github.com/ansible/ansible/issues/85202)

@ -0,0 +1,4 @@
bugfixes:
- ssh agent - Fixed several potential startup hangs for badly-behaved or overloaded ssh agents.
minor_changes:
- ssh agent - Added ``SSH_AGENT_EXECUTABLE`` config to allow override of ssh-agent.

@ -0,0 +1,4 @@
minor_changes:
- ssh connection plugin - Added ``verbosity`` config to decouple SSH debug output verbosity from Ansible verbosity.
Previously, the Ansible verbosity value was always applied to the SSH client command-line, leading to excessively verbose output.
Set the ``ANSIBLE_SSH_VERBOSITY`` envvar or ``ansible_ssh_verbosity`` Ansible variable to a positive integer to increase SSH client verbosity.

@ -0,0 +1,7 @@
bugfixes:
- task timeout - Specifying a negative task timeout now results in an error.
- error handling - Error details and tracebacks from connection and built-in action exceptions are preserved.
Previously, much of the detail was lost or mixed into the error message.
minor_changes:
- task timeout - Specifying a timeout greater than 100,000,000 now results in an error.

@ -0,0 +1,2 @@
bugfixes:
- template action - Template files where the entire file's output renders as ``None`` are no longer emitted as the string "None", but instead render to an empty file as in previous releases.

@ -0,0 +1,2 @@
bugfixes:
- Fix templating ``tags`` on plays and roles. (https://github.com/ansible/ansible/issues/69903)

@ -1,21 +1,19 @@
# DTFIX-RELEASE: document EncryptedString replacing AnsibleVaultEncryptedUnicode
major_changes:
- variables - The type system underlying Ansible's variable storage has been significantly overhauled and formalized.
Attempts to store unsupported Python object types in variables will now result in an error. # DTFIX-RELEASE: link to type system docs TBD
Attempts to store unsupported Python object types in variables now more consistently yields early warnings or errors.
- variables - To support new Ansible features, many variable objects are now represented by subclasses of their respective native Python types.
In most cases, they behave indistinguishably from their original types, but some Python libraries do not handle builtin object subclasses properly.
Custom plugins that interact with such libraries may require changes to convert and pass the native types. # DTFIX-RELEASE: link to plugin/data tagging API docs TBD
Custom plugins that interact with such libraries may require changes to convert and pass the native types.
- ansible-test - Packages beneath ``module_utils`` can now contain ``__init__.py`` files.
- Jinja plugins - Jinja builtin filter and test plugins are now accessible via their fully-qualified names ``ansible.builtin.{name}``.
minor_changes:
- templating - Templating errors now provide more information about both the location and context of the error, especially for deeply-nested and/or indirected templating scenarios.
- templating - Handling of omitted values is now a first-class feature of the template engine, and is usable in all Ansible Jinja template contexts.
Any template that resolves to ``omit`` is automatically removed from its parent container during templating. # DTFIX-RELEASE: porting guide entry
Any template that resolves to ``omit`` is automatically removed from its parent container during templating.
- templating - Unified ``omit`` behavior now requires that plugins calling ``Templar.template()`` handle cases where the entire template result is omitted,
by catching the ``AnsibleValueOmittedError`` that is raised.
Previously, this condition caused a randomly-generated string marker to appear in the template result. # DTFIX-RELEASE: porting guide entry?
Previously, this condition caused a randomly-generated string marker to appear in the template result.
- templating - Template evaluation is lazier than in previous versions.
Template expressions which resolve only portions of a data structure no longer result in the entire structure being templated.
- handlers - Templated handler names with syntax errors, or that resolve to ``omit`` are now skipped like handlers with undefined variables in their name.
@ -23,15 +21,15 @@ minor_changes:
except when the undefined value is the default of ``undef()`` with no arguments. Previously, any existing undefined hint would be ignored.
- templating - Embedding ``range()`` values in containers such as lists will result in an error on use.
Previously the value would be converted to a string representing the range parameters, such as ``range(0, 3)``.
- Jinja plugins - Plugins can declare support for undefined values. # DTFIX-RELEASE: examples, porting guide entry
- Jinja plugins - Plugins can declare support for undefined values. # DTFIX5: examples, porting guide entry
- templating - Variables of type ``set`` and ``tuple`` are now converted to ``list`` when exiting the final pass of templating.
- templating - Access to an undefined variable from inside a lookup, filter, or test (which raises MarkerError) no longer ends processing of the current template.
The triggering undefined value is returned as the result of the offending plugin invocation, and the template continues to execute. # DTFIX-RELEASE: porting guide entry, samples needed
The triggering undefined value is returned as the result of the offending plugin invocation, and the template continues to execute. # DTFIX5: porting guide entry, samples needed
- plugin error handling - When raising exceptions in an exception handler, be sure to use ``raise ... from`` as appropriate.
This supersedes the use of the ``AnsibleError`` arg ``orig_exc`` to represent the cause.
Specifying ``orig_exc`` as the cause is still permitted.
Failure to use ``raise ... from`` when ``orig_exc`` is set will result in a warning.
Additionally, if the two cause exceptions do not match, a warning will be issued. # DTFIX-RELEASE: this needs a porting guide entry
Additionally, if the two cause exceptions do not match, a warning will be issued.
- ansible-test - The ``yamllint`` sanity test now enforces string values for the ``!vault`` tag.
- warnings - All warnings (including deprecation warnings) issued during a task's execution are now accessible via the ``warnings`` and ``deprecations`` keys on the task result.
- troubleshooting - Tracebacks can be collected and displayed for most errors, warnings, and deprecation warnings (including those generated by modules).
@ -47,17 +45,13 @@ minor_changes:
- to_json / to_nice_json filters - The filters accept a ``profile`` argument, which defaults to ``tagless``.
- undef jinja function - The ``undef`` jinja function now raises an error if a non-string hint is given.
Attempting to use an undefined hint also results in an error, ensuring incorrect use of the function can be distinguished from the function's normal behavior.
- display - The ``collection_name`` arg to ``Display.deprecated`` no longer has any effect.
Information about the calling plugin is automatically captured by the display infrastructure, included in the displayed messages, and made available to callbacks.
- modules - The ``collection_name`` arg to Python module-side ``deprecate`` methods no longer has any effect.
Information about the calling module is automatically captured by the warning infrastructure and included in the module result.
breaking_changes:
- loops - Omit placeholders no longer leak between loop item templating and task templating.
Previously, ``omit`` placeholders could remain embedded in loop items after templating and be used as an ``omit`` for task templating.
Now, values resolving to ``omit`` are dropped immediately when loop items are templated.
To turn missing values into an ``omit`` for task templating, use ``| default(omit)``.
This solution is backwards compatible with previous versions of ansible-core. # DTFIX-RELEASE: porting guide entry with examples
This solution is backward-compatible with previous versions of ansible-core.
- serialization of ``omit`` sentinel - Serialization of variables containing ``omit`` sentinels (e.g., by the ``to_json`` and ``to_yaml`` filters or ``ansible-inventory``) will fail if the variable has not completed templating.
Previously, serialization succeeded with placeholder strings emitted in the serialized output.
- conditionals - Conditional expressions that result in non-boolean values are now an error by default.
@ -82,19 +76,18 @@ breaking_changes:
Lookup plugins are responsible for tagging strings containing templates to allow evaluation as a template.
- assert - The ``quiet`` argument must be a commonly-accepted boolean value.
Previously, unrecognized values were silently treated as False.
- plugins - Any plugin that sources or creates templates must properly tag them as trusted. # DTFIX-RELEASE: porting guide entry for "how?" Don't forget to mention inventory plugin ``trusted_by_default`` config.
- plugins - Any plugin that sources or creates templates must properly tag them as trusted. # DTFIX5: porting guide entry for "how?" Don't forget to mention inventory plugin ``trusted_by_default`` config.
- first_found lookup - When specifying ``files`` or ``paths`` as a templated list containing undefined values, the undefined list elements will be discarded with a warning.
Previously, the entire list would be discarded without any warning.
- templating - The result of the ``range()`` global function cannot be returned from a template- it should always be passed to a filter (e.g., ``random``).
Previously, range objects returned from an intermediate template were always converted to a list, which is inconsistent with inline consumption of range objects.
- plugins - Custom Jinja plugins that accept undefined top-level arguments must opt in to receiving them. # DTFIX-RELEASE: porting guide entry + backcompat behavior description
- plugins - Custom Jinja plugins that accept undefined top-level arguments must opt in to receiving them. # DTFIX5: porting guide entry + backcompat behavior description
- plugins - Custom Jinja plugins that use ``environment.getitem`` to retrieve undefined values will now trigger a ``MarkerError`` exception.
This exception must be handled to allow the plugin to return a ``Marker``, or the plugin must opt-in to accepting ``Marker`` values. # DTFIX-RELEASE: mention the decorator
This exception must be handled to allow the plugin to return a ``Marker``, or the plugin must opt-in to accepting ``Marker`` values. # DTFIX5: mention the decorator
- templating - Many Jinja plugins (filters, lookups, tests) and methods previously silently ignored undefined inputs, which often masked subtle errors.
Passing an undefined argument to a Jinja plugin or method that does not declare undefined support now results in an undefined value. # DTFIX-RELEASE: common examples, porting guide, `is defined`, `is undefined`, etc; porting guide should also mention that overly-broad exception handling may mask Undefined errors; also that lazy handling of Undefined can invoke a plugin and bomb out in the middle where it was previously never invoked (plugins with side effects, just don't)
- lookup plugins - Lookup plugins called as `with_(lookup)` will no longer have the `_subdir` attribute set. # DTFIX-RELEASE: porting guide re: `ansible_lookup_context`
Passing an undefined argument to a Jinja plugin or method that does not declare undefined support now results in an undefined value. # DTFIX5: common examples, porting guide, `is defined`, `is undefined`, etc; porting guide should also mention that overly-broad exception handling may mask Undefined errors; also that lazy handling of Undefined can invoke a plugin and bomb out in the middle where it was previously never invoked (plugins with side effects, just don't)
- lookup plugins - Lookup plugins called as `with_(lookup)` will no longer have the `_subdir` attribute set.
- lookup plugins - ``terms`` will always be passed to ``run`` as the first positional arg, where previously it was sometimes passed as a keyword arg when using ``with_`` syntax.
- callback plugins - The structure of the ``exception``, ``warnings`` and ``deprecations`` values visible to callbacks has changed. Callbacks that inspect or serialize these values may require special handling. # DTFIX-RELEASE: porting guide re ErrorDetail/WarningMessageDetail/DeprecationMessageDetail
- modules - Ansible modules using ``sys.excepthook`` must use a standard ``try/except`` instead.
- templating - Access to ``_`` prefixed attributes and methods, and methods with known side effects, is no longer permitted.
In cases where a matching mapping key is present, the associated value will be returned instead of an error.
@ -112,17 +105,9 @@ security_fixes:
Plugins which have not been updated to preserve trust while manipulating strings may inadvertently cause them to lose their trusted status.
- templating - Changes to conditional expression handling removed numerous instances of insecure multi-pass templating (which could result in execution of untrusted template expressions).
known_issues:
- variables - The values ``None``, ``True`` and ``False`` cannot be tagged because they are singletons. Attempts to apply tags to these values will be silently ignored.
- variables - Tagged values cannot be used for dictionary keys in many circumstances. # DTFIX-RELEASE: Explain this in more detail.
- templating - Any string value starting with ``#jinja2:`` which is templated will always be interpreted as Jinja2 configuration overrides.
To include this literal value at the start of a string, a space or other character must precede it.
bugfixes:
- module defaults - Module defaults are no longer templated unless they are used by a task that does not override them.
Previously, all module defaults for all modules were templated for every task.
- omitting task args - Use of omit for task args now properly falls back to args of lower precedence, such as module defaults.
Previously an omitted value would obliterate values of lower precedence. # DTFIX-RELEASE: do we need obliterate, is this a breaking change?
- regex_search filter - Corrected return value documentation to reflect None (not empty string) for no match.
- first_found lookup - Corrected return value documentation to reflect None (not empty string) for no files found.
- vars lookup - The ``default`` substitution only applies when trying to look up a variable which is not defined.
@ -142,8 +127,6 @@ bugfixes:
Previously, there were cases where a non-list could be received.
deprecated_features:
- templating - The ``ansible_managed`` variable available for certain templating scenarios, such as the ``template`` action and ``template`` lookup has been deprecated.
Define and use a custom variable instead of relying on ``ansible_managed``.
- display - The ``Display.get_deprecation_message`` method has been deprecated.
Call ``Display.deprecated`` to display a deprecation message, or call it with ``removed=True`` to raise an ``AnsibleError``.
- config - The ``DEFAULT_JINJA2_NATIVE`` option has no effect.
@ -155,15 +138,12 @@ deprecated_features:
- conditionals - Conditionals using Jinja templating delimiters (e.g., ``{{``, ``{%``) should be rewritten as expressions without delimiters, unless the entire conditional value is a single template that resolves to a trusted string expression.
This is useful for dynamic indirection of conditional expressions, but is limited to trusted literal string expressions.
- templating - The ``disable_lookups`` option has no effect, since plugins must be updated to apply trust before any templating can be performed.
- to_yaml/to_nice_yaml filters - Implicit YAML dumping of vaulted value ciphertext is deprecated.
Set `dump_vault_tags` to explicitly specify the desired behavior.
- plugins - The ``listify_lookup_plugin_terms`` function is obsolete and in most cases no longer needed. # DTFIX-RELEASE: add a porting guide entry for this
- plugin error handling - The ``AnsibleError`` constructor arg ``suppress_extended_error`` is deprecated.
Using ``suppress_extended_error=True`` has the same effect as ``show_content=False``.
- config - The ``ACTION_WARNINGS`` config has no effect. It previously disabled command warnings, which have since been removed.
- templating - Support for enabling Jinja2 extensions (not plugins) has been deprecated.
- playbook variables - The ``play_hosts`` variable has been deprecated, use ``ansible_play_batch`` instead.
- bool filter - Support for coercing unrecognized input values (including None) has been deprecated. Consult the filter documentation for acceptable values, or consider use of the ``truthy`` and ``falsy`` tests. # DTFIX-RELEASE: porting guide
- bool filter - Support for coercing unrecognized input values (including None) has been deprecated. Consult the filter documentation for acceptable values, or consider use of the ``truthy`` and ``falsy`` tests.
- oneline callback - The ``oneline`` callback and its associated ad-hoc CLI args (``-o``, ``--one-line``) are deprecated.
- tree callback - The ``tree`` callback and its associated ad-hoc CLI args (``-t``, ``--tree``) are deprecated.
- CLI - The ``--inventory-file`` option alias is deprecated. Use the ``-i`` or ``--inventory`` option instead.
@ -173,6 +153,9 @@ deprecated_features:
- file loading - Loading text files with ``DataLoader`` containing data that cannot be decoded under the expected encoding is deprecated.
In most cases the encoding must be UTF-8, although some plugins allow choosing a different encoding.
Previously, invalid data was silently wrapped in Unicode surrogate escape sequences, often resulting in later errors or other data corruption.
- callback plugins - The v1 callback API (callback methods not prefixed with `v2_`) is deprecated.
Use `v2_` prefixed methods instead.
- callback plugins - The `v2_on_any` callback method is deprecated. Use specific callback methods instead.
removed_features:
- modules - Modules returning non-UTF8 strings now result in an error.

@ -0,0 +1,3 @@
bugfixes:
- Core Jinja test plugins - Builtin test plugins now always return ``bool`` to avoid spurious deprecation warnings for
some malformed inputs.

@ -0,0 +1,3 @@
minor_changes:
- template action and lookup plugin - The value of the ``ansible_managed`` variable (if set) will not be masked by the ``template`` action and lookup.
Previously, the value calculated by the ``DEFAULT_MANAGED_STR`` configuration option always masked the variable value during plugin execution, preventing runtime customization.

@ -0,0 +1,2 @@
minor_changes:
- Remove unnecessary shebang from the ``hostname`` module.

@ -0,0 +1,9 @@
minor_changes:
- >-
windows - Added support for ``#AnsibleRequires -Wrapper`` to request a PowerShell module be run through the
execution wrapper scripts without any module utils specified.
- >-
windows - Added support for running signed modules and scripts with a Windows host protected by Windows App
Control/WDAC. This is a tech preview and the interface may be subject to change.
- >-
windows - Script modules will preserve UTF-8 encoding when executing the script.

@ -40,7 +40,6 @@ import shutil
from pathlib import Path
from ansible.module_utils.common.messages import PluginInfo
from ansible.release import __version__
import ansible.utils.vars as utils_vars
from ansible.parsing.dataloader import DataLoader
@ -172,15 +171,8 @@ def boilerplate_module(modfile, args, interpreters, check, destfile):
modname = os.path.basename(modfile)
modname = os.path.splitext(modname)[0]
plugin = PluginInfo(
requested_name=modname,
resolved_name=modname,
type='module',
)
built_module = module_common.modify_module(
module_name=modname,
plugin=plugin,
module_path=modfile,
module_args=complex_args,
templar=Templar(loader=loader),
@ -225,10 +217,11 @@ def ansiballz_setup(modfile, modname, interpreters):
# All the directories in an AnsiBallZ that modules can live
core_dirs = glob.glob(os.path.join(debug_dir, 'ansible/modules'))
non_core_dirs = glob.glob(os.path.join(debug_dir, 'ansible/legacy'))
collection_dirs = glob.glob(os.path.join(debug_dir, 'ansible_collections/*/*/plugins/modules'))
# There's only one module in an AnsiBallZ payload so look for the first module and then exit
for module_dir in core_dirs + collection_dirs:
for module_dir in core_dirs + collection_dirs + non_core_dirs:
for dirname, directories, filenames in os.walk(module_dir):
for filename in filenames:
if filename == modname + '.py':

@ -18,7 +18,7 @@ def get_controller_serialize_map() -> dict[type, t.Callable]:
return {
_lazy_containers._AnsibleLazyTemplateDict: _profiles._JSONSerializationProfile.discard_tags,
_lazy_containers._AnsibleLazyTemplateList: _profiles._JSONSerializationProfile.discard_tags,
EncryptedString: str, # preserves tags since this is an intance of EncryptedString; if tags should be discarded from str, another entry will handle it
EncryptedString: str, # preserves tags since this is an instance of EncryptedString; if tags should be discarded from str, another entry will handle it
}
@ -45,7 +45,7 @@ def setup() -> None:
"""No-op function to ensure that side-effect only imports of this module are not flagged/removed as 'unused'."""
# DTFIX-RELEASE: this is really fragile- disordered/incorrect imports (among other things) can mess it up. Consider a hosting-env-managed context
# DTFIX-FUTURE: this is really fragile- disordered/incorrect imports (among other things) can mess it up. Consider a hosting-env-managed context
# with an enum with at least Controller/Target/Unknown values, and possibly using lazy-init module shims or some other mechanism to allow controller-side
# notification/augmentation of this kind of metadata.
_internal.get_controller_serialize_map = get_controller_serialize_map

@ -0,0 +1,101 @@
from __future__ import annotations
import dataclasses
import json
import typing as t
from ansible.module_utils._internal._ansiballz import _extensions
from ansible.module_utils._internal._ansiballz._extensions import _pydevd, _coverage
from ansible.constants import config
_T = t.TypeVar('_T')
class ExtensionManager:
"""AnsiballZ extension manager."""
def __init__(
self,
debugger: _pydevd.Options | None = None,
coverage: _coverage.Options | None = None,
) -> None:
options = dict(
_pydevd=debugger,
_coverage=coverage,
)
self._debugger = debugger
self._coverage = coverage
self._extension_names = tuple(name for name, option in options.items() if option)
self._module_names = tuple(f'{_extensions.__name__}.{name}' for name in self._extension_names)
self.source_mapping: dict[str, str] = {}
@property
def debugger_enabled(self) -> bool:
"""Returns True if the debugger extension is enabled, otherwise False."""
return bool(self._debugger)
@property
def extension_names(self) -> tuple[str, ...]:
"""Names of extensions to include in the AnsiballZ payload."""
return self._extension_names
@property
def module_names(self) -> tuple[str, ...]:
"""Python module names of extensions to include in the AnsiballZ payload."""
return self._module_names
def get_extensions(self) -> dict[str, dict[str, object]]:
"""Return the configured extensions and their options."""
extension_options: dict[str, t.Any] = {}
if self._debugger:
extension_options['_pydevd'] = dataclasses.replace(
self._debugger,
source_mapping=self._get_source_mapping(),
)
if self._coverage:
extension_options['_coverage'] = self._coverage
extensions = {extension: dataclasses.asdict(options) for extension, options in extension_options.items()}
return extensions
def _get_source_mapping(self) -> dict[str, str]:
"""Get the source mapping, adjusting the source root as needed."""
if self._debugger.source_mapping:
source_mapping = {self._translate_path(key): value for key, value in self.source_mapping.items()}
else:
source_mapping = self.source_mapping
return source_mapping
def _translate_path(self, path: str) -> str:
"""Translate a local path to a foreign path."""
for replace, match in self._debugger.source_mapping.items():
if path.startswith(match):
return replace + path[len(match) :]
return path
@classmethod
def create(cls, task_vars: dict[str, object]) -> t.Self:
"""Create an instance using the provided task vars."""
return cls(
debugger=cls._get_options('_ANSIBALLZ_DEBUGGER_CONFIG', _pydevd.Options, task_vars),
coverage=cls._get_options('_ANSIBALLZ_COVERAGE_CONFIG', _coverage.Options, task_vars),
)
@classmethod
def _get_options(cls, name: str, config_type: type[_T], task_vars: dict[str, object]) -> _T | None:
"""Parse configuration from the named environment variable as the specified type, or None if not configured."""
if (value := config.get_config_value(name, variables=task_vars)) is None:
return None
data = json.loads(value) if isinstance(value, str) else value
options = config_type(**data)
return options

@ -37,15 +37,13 @@ _ANSIBALLZ_WRAPPER = True
def _ansiballz_main(
zipdata: str,
zip_data: str,
ansible_module: str,
module_fqn: str,
params: str,
profile: str,
plugin_info_dict: dict[str, object],
date_time: datetime.datetime,
coverage_config: str | None,
coverage_output: str | None,
extensions: dict[str, dict[str, object]],
rlimit_nofile: int,
) -> None:
import os
@ -137,16 +135,14 @@ def _ansiballz_main(
# can monkeypatch the right basic
sys.path.insert(0, modlib_path)
from ansible.module_utils._internal._ansiballz import run_module
from ansible.module_utils._internal._ansiballz import _loader
run_module(
_loader.run_module(
json_params=json_params,
profile=profile,
plugin_info_dict=plugin_info_dict,
module_fqn=module_fqn,
modlib_path=modlib_path,
coverage_config=coverage_config,
coverage_output=coverage_output,
extensions=extensions,
)
def debug(command: str, modlib_path: str, json_params: bytes) -> None:
@ -225,18 +221,18 @@ def _ansiballz_main(
with open(args_path, 'rb') as reader:
json_params = reader.read()
from ansible.module_utils._internal._ansiballz import run_module
from ansible.module_utils._internal._ansiballz import _loader
run_module(
_loader.run_module(
json_params=json_params,
profile=profile,
plugin_info_dict=plugin_info_dict,
module_fqn=module_fqn,
modlib_path=modlib_path,
extensions=extensions,
)
else:
print('WARNING: Unknown debug command. Doing nothing.')
print(f'FATAL: Unknown debug command {command!r}. Doing nothing.')
#
# See comments in the debug() method for information on debugging
@ -249,13 +245,14 @@ def _ansiballz_main(
# store this in remote_tmpdir (use system tempdir instead)
# Only need to use [ansible_module]_payload_ in the temp_path until we move to zipimport
# (this helps ansible-test produce coverage stats)
temp_path = tempfile.mkdtemp(prefix='ansible_' + ansible_module + '_payload_')
# IMPORTANT: The real path must be used here to ensure a remote debugger such as PyCharm (using pydevd) can resolve paths correctly.
temp_path = os.path.realpath(tempfile.mkdtemp(prefix='ansible_' + ansible_module + '_payload_'))
try:
zipped_mod = os.path.join(temp_path, 'ansible_' + ansible_module + '_payload.zip')
with open(zipped_mod, 'wb') as modlib:
modlib.write(base64.b64decode(zipdata))
modlib.write(base64.b64decode(zip_data))
if len(sys.argv) == 2:
debug(sys.argv[1], zipped_mod, encoded_params)

@ -0,0 +1,47 @@
from __future__ import annotations as _annotations
import collections.abc as _c
import typing as _t
_T_co = _t.TypeVar('_T_co', covariant=True)
class SequenceProxy(_c.Sequence[_T_co]):
"""A read-only sequence proxy."""
# DTFIX5: needs unit test coverage
__slots__ = ('__value',)
def __init__(self, value: _c.Sequence[_T_co]) -> None:
self.__value = value
@_t.overload
def __getitem__(self, index: int) -> _T_co: ...
@_t.overload
def __getitem__(self, index: slice) -> _c.Sequence[_T_co]: ...
def __getitem__(self, index: int | slice) -> _T_co | _c.Sequence[_T_co]:
if isinstance(index, slice):
return self.__class__(self.__value[index])
return self.__value[index]
def __len__(self) -> int:
return len(self.__value)
def __contains__(self, item: object) -> bool:
return item in self.__value
def __iter__(self) -> _t.Iterator[_T_co]:
yield from self.__value
def __reversed__(self) -> _c.Iterator[_T_co]:
return reversed(self.__value)
def index(self, *args) -> int:
return self.__value.index(*args)
def count(self, value: object) -> int:
return self.__value.count(value)

@ -0,0 +1,66 @@
from __future__ import annotations
import contextlib
import signal
import types
import typing as _t
from ansible.module_utils import datatag
class AnsibleTimeoutError(BaseException):
"""A general purpose timeout."""
_MAX_TIMEOUT = 100_000_000
"""
The maximum supported timeout value.
This value comes from BSD's alarm limit, which is due to that function using setitimer.
"""
def __init__(self, timeout: int) -> None:
self.timeout = timeout
super().__init__(f"Timed out after {timeout} second(s).")
@classmethod
@contextlib.contextmanager
def alarm_timeout(cls, timeout: int | None) -> _t.Iterator[None]:
"""
Context for running code under an optional timeout.
Raises an instance of this class if the timeout occurs.
New usages of this timeout mechanism are discouraged.
"""
if timeout is not None:
if not isinstance(timeout, int):
raise TypeError(f"Timeout requires 'int' argument, not {datatag.native_type_name(timeout)!r}.")
if timeout < 0 or timeout > cls._MAX_TIMEOUT:
# On BSD based systems, alarm is implemented using setitimer.
# If out-of-bounds values are passed to alarm, they will return -1, which would be interpreted as an existing timer being set.
# To avoid that, bounds checking is performed in advance.
raise ValueError(f'Timeout {timeout} is invalid, it must be between 0 and {cls._MAX_TIMEOUT}.')
if not timeout:
yield # execute the context manager's body
return # no timeout to deal with, exit immediately
def on_alarm(_signal: int, _frame: types.FrameType) -> None:
raise cls(timeout)
if signal.signal(signal.SIGALRM, on_alarm):
raise RuntimeError("An existing alarm handler was present.")
try:
try:
if signal.alarm(timeout):
raise RuntimeError("An existing alarm was set.")
yield # execute the context manager's body
finally:
# Disable the alarm.
# If the alarm fires inside this finally block, the alarm is still disabled.
# This guarantees the cleanup code in the outer finally block runs without risk of encountering the `TaskTimeoutError` from the alarm.
signal.alarm(0)
finally:
signal.signal(signal.SIGALRM, signal.SIG_DFL)

@ -1,10 +1,12 @@
from __future__ import annotations
import collections.abc as _c
import dataclasses
import typing as t
from ansible._internal._errors import _error_utils
from ansible.errors import AnsibleRuntimeError
from ansible.module_utils.common.messages import ErrorSummary, Detail, _dataclass_kwargs
from ansible.module_utils._internal import _messages
class AnsibleCapturedError(AnsibleRuntimeError):
@ -16,43 +18,36 @@ class AnsibleCapturedError(AnsibleRuntimeError):
self,
*,
obj: t.Any = None,
error_summary: ErrorSummary,
event: _messages.Event,
) -> None:
super().__init__(
obj=obj,
)
self._error_summary = error_summary
self._event = event
@property
def error_summary(self) -> ErrorSummary:
return self._error_summary
class AnsibleResultCapturedError(AnsibleCapturedError, _error_utils.ContributesToTaskResult):
"""
An exception representing error detail captured in a foreign context where an action/module result dictionary is involved.
class AnsibleResultCapturedError(AnsibleCapturedError):
"""An exception representing error detail captured in a foreign context where an action/module result dictionary is involved."""
This exception provides a result dictionary via the ContributesToTaskResult mixin.
"""
def __init__(self, error_summary: ErrorSummary, result: dict[str, t.Any]) -> None:
super().__init__(error_summary=error_summary)
def __init__(self, event: _messages.Event, result: dict[str, t.Any]) -> None:
super().__init__(event=event)
self._result = result
@property
def result_contribution(self) -> _c.Mapping[str, object]:
return self._result
@classmethod
def maybe_raise_on_result(cls, result: dict[str, t.Any]) -> None:
"""Normalize the result and raise an exception if the result indicated failure."""
if error_summary := cls.normalize_result_exception(result):
raise error_summary.error_type(error_summary, result)
@classmethod
def find_first_remoted_error(cls, exception: BaseException) -> t.Self | None:
"""Find the first captured module error in the cause chain, starting with the given exception, returning None if not found."""
while exception:
if isinstance(exception, cls):
return exception
exception = exception.__cause__
return None
raise error_summary.error_type(error_summary.event, result)
@classmethod
def normalize_result_exception(cls, result: dict[str, t.Any]) -> CapturedErrorSummary | None:
@ -76,17 +71,18 @@ class AnsibleResultCapturedError(AnsibleCapturedError):
if isinstance(exception, CapturedErrorSummary):
error_summary = exception
elif isinstance(exception, ErrorSummary):
elif isinstance(exception, _messages.ErrorSummary):
error_summary = CapturedErrorSummary(
details=exception.details,
formatted_traceback=cls._normalize_traceback(exception.formatted_traceback),
event=exception.event,
error_type=cls,
)
else:
# translate non-ErrorDetail errors
error_summary = CapturedErrorSummary(
details=(Detail(msg=str(result.get('msg', 'Unknown error.'))),),
formatted_traceback=cls._normalize_traceback(exception),
event=_messages.Event(
msg=str(result.get('msg', 'Unknown error.')),
formatted_traceback=cls._normalize_traceback(exception),
),
error_type=cls,
)
@ -122,7 +118,6 @@ class AnsibleModuleCapturedError(AnsibleResultCapturedError):
context = 'target'
@dataclasses.dataclass(**_dataclass_kwargs)
class CapturedErrorSummary(ErrorSummary):
# DTFIX-RELEASE: where to put this, name, etc. since it shows up in results, it's not exactly private (and contains a type ref to an internal type)
@dataclasses.dataclass(**_messages._dataclass_kwargs)
class CapturedErrorSummary(_messages.ErrorSummary):
error_type: type[AnsibleResultCapturedError] | None = None

@ -0,0 +1,89 @@
from __future__ import annotations as _annotations
from ansible.module_utils._internal import _errors, _messages
class ControllerEventFactory(_errors.EventFactory):
"""Factory for creating `Event` instances from `BaseException` instances on the controller."""
def _get_msg(self, exception: BaseException) -> str | None:
from ansible.errors import AnsibleError
if not isinstance(exception, AnsibleError):
return super()._get_msg(exception)
return exception._original_message.strip()
def _get_formatted_source_context(self, exception: BaseException) -> str | None:
from ansible.errors import AnsibleError
if not isinstance(exception, AnsibleError):
return super()._get_formatted_source_context(exception)
return exception._formatted_source_context
def _get_help_text(self, exception: BaseException) -> str | None:
from ansible.errors import AnsibleError
if not isinstance(exception, AnsibleError):
return super()._get_help_text(exception)
return exception._help_text
def _get_chain(self, exception: BaseException) -> _messages.EventChain | None:
from ansible._internal._errors import _captured # avoid circular import due to AnsibleError import
if isinstance(exception, _captured.AnsibleCapturedError):
# a captured error provides its own cause event, it never has a normal __cause__
return _messages.EventChain(
msg_reason=_errors.MSG_REASON_DIRECT_CAUSE,
traceback_reason=f'The above {exception.context} exception was the direct cause of the following controller exception:',
event=exception._event,
)
return super()._get_chain(exception)
def _follow_cause(self, exception: BaseException) -> bool:
from ansible.errors import AnsibleError
return not isinstance(exception, AnsibleError) or exception._include_cause_message
def _get_cause(self, exception: BaseException) -> BaseException | None:
# deprecated: description='remove support for orig_exc (deprecated in 2.23)' core_version='2.27'
cause = super()._get_cause(exception)
from ansible.errors import AnsibleError
if not isinstance(exception, AnsibleError):
return cause
try:
from ansible.utils.display import _display
except Exception: # pylint: disable=broad-except # if config is broken, this can raise things other than ImportError
_display = None
if cause:
if exception.orig_exc and exception.orig_exc is not cause and _display:
_display.warning(
msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given, but differed from the cause given by `raise ... from`.",
)
return cause
if exception.orig_exc:
if _display:
# encourage the use of `raise ... from` before deprecating `orig_exc`
_display.warning(
msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given without using `raise ... from orig_exc`.",
)
return exception.orig_exc
return None
def _get_events(self, exception: BaseException) -> tuple[_messages.Event, ...] | None:
if isinstance(exception, BaseExceptionGroup):
return tuple(self._convert_exception(ex) for ex in exception.exceptions)
return None

@ -0,0 +1,240 @@
from __future__ import annotations
import abc
import collections.abc as _c
import dataclasses
import itertools
import pathlib
import textwrap
import typing as t
from ansible._internal._datatag._tags import Origin
from ansible._internal._errors import _error_factory
from ansible.module_utils._internal import _ambient_context, _event_utils, _messages, _traceback
class ContributesToTaskResult(metaclass=abc.ABCMeta):
"""Exceptions may include this mixin to contribute task result dictionary data directly to the final result."""
@property
@abc.abstractmethod
def result_contribution(self) -> _c.Mapping[str, object]:
"""Mapping of results to apply to the task result."""
@property
def omit_exception_key(self) -> bool:
"""Non-error exceptions (e.g., `AnsibleActionSkip`) must return `True` to ensure omission of the `exception` key."""
return False
@property
def omit_failed_key(self) -> bool:
"""Exceptions representing non-failure scenarios (e.g., `skipped`, `unreachable`) must return `True` to ensure omisson of the `failed` key."""
return False
class RedactAnnotatedSourceContext(_ambient_context.AmbientContextBase):
"""When active, this context will redact annotated source lines, showing only the origin."""
@dataclasses.dataclass(kw_only=True, frozen=True)
class SourceContext:
origin: Origin
annotated_source_lines: list[str]
target_line: str | None
def __str__(self) -> str:
msg_lines = [f'Origin: {self.origin}']
if self.annotated_source_lines:
msg_lines.append('')
msg_lines.extend(self.annotated_source_lines)
return '\n'.join(msg_lines)
@classmethod
def from_value(cls, value: t.Any) -> SourceContext | None:
"""Attempt to retrieve source and render a contextual indicator from the value's origin (if any)."""
if value is None:
return None
if isinstance(value, Origin):
origin = value
value = None
else:
origin = Origin.get_tag(value)
if RedactAnnotatedSourceContext.current(optional=True):
return cls.error('content redacted')
if origin and origin.path:
return cls.from_origin(origin)
if value is None:
truncated_value = None
annotated_source_lines = []
else:
# DTFIX-FUTURE: cleanup/share width
try:
value = str(value)
except Exception as ex:
value = f'<< context unavailable: {ex} >>'
truncated_value = textwrap.shorten(value, width=120)
annotated_source_lines = [truncated_value]
return SourceContext(
origin=origin or Origin.UNKNOWN,
annotated_source_lines=annotated_source_lines,
target_line=truncated_value,
)
@staticmethod
def error(message: str | None, origin: Origin | None = None) -> SourceContext:
return SourceContext(
origin=origin,
annotated_source_lines=[f'(source not shown: {message})'] if message else [],
target_line=None,
)
@classmethod
def from_origin(cls, origin: Origin) -> SourceContext:
"""Attempt to retrieve source and render a contextual indicator of an error location."""
from ansible.parsing.vault import is_encrypted # avoid circular import
# DTFIX-FUTURE: support referencing the column after the end of the target line, so we can indicate where a missing character (quote) needs to be added
# this is also useful for cases like end-of-stream reported by the YAML parser
# DTFIX-FUTURE: Implement line wrapping and match annotated line width to the terminal display width.
context_line_count: t.Final = 2
max_annotated_line_width: t.Final = 120
truncation_marker: t.Final = '...'
target_line_num = origin.line_num
if RedactAnnotatedSourceContext.current(optional=True):
return cls.error('content redacted', origin)
if not target_line_num or target_line_num < 1:
return cls.error(None, origin) # message omitted since lack of line number is obvious from pos
start_line_idx = max(0, (target_line_num - 1) - context_line_count) # if near start of file
target_col_num = origin.col_num
try:
with pathlib.Path(origin.path).open() as src:
first_line = src.readline()
lines = list(itertools.islice(itertools.chain((first_line,), src), start_line_idx, target_line_num))
except Exception as ex:
return cls.error(type(ex).__name__, origin)
if is_encrypted(first_line):
return cls.error('content encrypted', origin)
if len(lines) != target_line_num - start_line_idx:
return cls.error('file truncated', origin)
annotated_source_lines = []
line_label_width = len(str(target_line_num))
max_src_line_len = max_annotated_line_width - line_label_width - 1
usable_line_len = max_src_line_len
for line_num, line in enumerate(lines, start_line_idx + 1):
line = line.rstrip('\n') # universal newline default mode on `open` ensures we'll never see anything but \n
line = line.replace('\t', ' ') # mixed tab/space handling is intentionally disabled since we're both format and display config agnostic
if len(line) > max_src_line_len:
line = line[: max_src_line_len - len(truncation_marker)] + truncation_marker
usable_line_len = max_src_line_len - len(truncation_marker)
annotated_source_lines.append(f'{str(line_num).rjust(line_label_width)}{" " if line else ""}{line}')
if target_col_num and usable_line_len >= target_col_num >= 1:
column_marker = f'column {target_col_num}'
target_col_idx = target_col_num - 1
if target_col_idx + 2 + len(column_marker) > max_src_line_len:
column_marker = f'{" " * (target_col_idx - len(column_marker) - 1)}{column_marker} ^'
else:
column_marker = f'{" " * target_col_idx}^ {column_marker}'
column_marker = f'{" " * line_label_width} {column_marker}'
annotated_source_lines.append(column_marker)
elif target_col_num is None:
underline_length = len(annotated_source_lines[-1]) - line_label_width - 1
annotated_source_lines.append(f'{" " * line_label_width} {"^" * underline_length}')
return SourceContext(
origin=origin,
annotated_source_lines=annotated_source_lines,
target_line=lines[-1].rstrip('\n'), # universal newline default mode on `open` ensures we'll never see anything but \n
)
def format_exception_message(exception: BaseException) -> str:
"""Return the full chain of exception messages by concatenating the cause(s) until all are exhausted."""
return _event_utils.format_event_brief_message(_error_factory.ControllerEventFactory.from_exception(exception, False))
def result_dict_from_exception(exception: BaseException, accept_result_contribution: bool = False) -> dict[str, object]:
"""Return a failed task result dict from the given exception."""
event = _error_factory.ControllerEventFactory.from_exception(exception, _traceback.is_traceback_enabled(_traceback.TracebackEvent.ERROR))
result: dict[str, object] = {}
omit_failed_key = False
omit_exception_key = False
if accept_result_contribution:
while exception:
if isinstance(exception, ContributesToTaskResult):
result = dict(exception.result_contribution)
omit_failed_key = exception.omit_failed_key
omit_exception_key = exception.omit_exception_key
break
exception = exception.__cause__
if omit_failed_key:
result.pop('failed', None)
else:
result.update(failed=True)
if omit_exception_key:
result.pop('exception', None)
else:
result.update(exception=_messages.ErrorSummary(event=event))
if 'msg' not in result:
# if nothing contributed `msg`, generate one from the exception messages
result.update(msg=_event_utils.format_event_brief_message(event))
return result
def result_dict_from_captured_errors(
msg: str,
*,
errors: list[_messages.ErrorSummary] | None = None,
) -> dict[str, object]:
"""Return a failed task result dict from the given error message and captured errors."""
_skip_stackwalk = True
event = _messages.Event(
msg=msg,
formatted_traceback=_traceback.maybe_capture_traceback(msg, _traceback.TracebackEvent.ERROR),
events=tuple(error.event for error in errors) if errors else None,
)
result = dict(
failed=True,
exception=_messages.ErrorSummary(
event=event,
),
msg=_event_utils.format_event_brief_message(event),
)
return result

@ -16,8 +16,8 @@ class ErrorAction(enum.Enum):
"""Action to take when an error is encountered."""
IGNORE = enum.auto()
WARN = enum.auto()
FAIL = enum.auto()
WARNING = enum.auto()
ERROR = enum.auto()
@classmethod
def from_config(cls, setting: str, variables: dict[str, t.Any] | None = None) -> t.Self:
@ -75,9 +75,9 @@ class ErrorHandler:
yield
except args as ex:
match self.action:
case ErrorAction.WARN:
case ErrorAction.WARNING:
display.error_as_warning(msg=None, exception=ex)
case ErrorAction.FAIL:
case ErrorAction.ERROR:
raise
case _: # ErrorAction.IGNORE
pass

@ -0,0 +1,28 @@
from __future__ import annotations
from collections import abc as _c
from ansible._internal._errors._alarm_timeout import AnsibleTimeoutError
from ansible._internal._errors._error_utils import ContributesToTaskResult
from ansible.module_utils.datatag import deprecate_value
class TaskTimeoutError(AnsibleTimeoutError, ContributesToTaskResult):
"""
A task-specific timeout.
This exception provides a result dictionary via the ContributesToTaskResult mixin.
"""
@property
def result_contribution(self) -> _c.Mapping[str, object]:
help_text = "Configure `DISPLAY_TRACEBACK` to see a traceback on timeout errors."
frame = deprecate_value(
value=help_text,
msg="The `timedout.frame` task result key is deprecated.",
help_text=help_text,
version="2.23",
)
return dict(timedout=dict(frame=frame, period=self.timeout))

@ -1,310 +0,0 @@
from __future__ import annotations
import dataclasses
import itertools
import pathlib
import sys
import textwrap
import typing as t
from ansible.module_utils.common.messages import Detail, ErrorSummary
from ansible._internal._datatag._tags import Origin
from ansible.module_utils._internal import _ambient_context, _traceback
from ansible import errors
if t.TYPE_CHECKING:
from ansible.utils.display import Display
class RedactAnnotatedSourceContext(_ambient_context.AmbientContextBase):
"""
When active, this context will redact annotated source lines, showing only the origin.
"""
def _dedupe_and_concat_message_chain(message_parts: list[str]) -> str:
message_parts = list(reversed(message_parts))
message = message_parts.pop(0)
for message_part in message_parts:
# avoid duplicate messages where the cause was already concatenated to the exception message
if message_part.endswith(message):
message = message_part
else:
message = concat_message(message_part, message)
return message
def _collapse_error_details(error_details: t.Sequence[Detail]) -> list[Detail]:
"""
Return a potentially modified error chain, with redundant errors collapsed into previous error(s) in the chain.
This reduces the verbosity of messages by eliminating repetition when multiple errors in the chain share the same contextual information.
"""
previous_error = error_details[0]
previous_warnings: list[str] = []
collapsed_error_details: list[tuple[Detail, list[str]]] = [(previous_error, previous_warnings)]
for error in error_details[1:]:
details_present = error.formatted_source_context or error.help_text
details_changed = error.formatted_source_context != previous_error.formatted_source_context or error.help_text != previous_error.help_text
if details_present and details_changed:
previous_error = error
previous_warnings = []
collapsed_error_details.append((previous_error, previous_warnings))
else:
previous_warnings.append(error.msg)
final_error_details: list[Detail] = []
for error, messages in collapsed_error_details:
final_error_details.append(dataclasses.replace(error, msg=_dedupe_and_concat_message_chain([error.msg] + messages)))
return final_error_details
def _get_cause(exception: BaseException) -> BaseException | None:
# deprecated: description='remove support for orig_exc (deprecated in 2.23)' core_version='2.27'
if not isinstance(exception, errors.AnsibleError):
return exception.__cause__
if exception.__cause__:
if exception.orig_exc and exception.orig_exc is not exception.__cause__:
_get_display().warning(
msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given, but differed from the cause given by `raise ... from`.",
)
return exception.__cause__
if exception.orig_exc:
# encourage the use of `raise ... from` before deprecating `orig_exc`
_get_display().warning(msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given without using `raise ... from orig_exc`.")
return exception.orig_exc
return None
class _TemporaryDisplay:
# DTFIX-FUTURE: generalize this and hide it in the display module so all users of Display can benefit
@staticmethod
def warning(*args, **kwargs):
print(f'FALLBACK WARNING: {args} {kwargs}', file=sys.stderr)
@staticmethod
def deprecated(*args, **kwargs):
print(f'FALLBACK DEPRECATION: {args} {kwargs}', file=sys.stderr)
def _get_display() -> Display | _TemporaryDisplay:
try:
from ansible.utils.display import Display
except ImportError:
return _TemporaryDisplay()
return Display()
def _create_error_summary(exception: BaseException, event: _traceback.TracebackEvent | None = None) -> ErrorSummary:
from . import _captured # avoid circular import due to AnsibleError import
current_exception: BaseException | None = exception
error_details: list[Detail] = []
if event:
formatted_traceback = _traceback.maybe_extract_traceback(exception, event)
else:
formatted_traceback = None
while current_exception:
if isinstance(current_exception, errors.AnsibleError):
include_cause_message = current_exception._include_cause_message
edc = Detail(
msg=current_exception._original_message.strip(),
formatted_source_context=current_exception._formatted_source_context,
help_text=current_exception._help_text,
)
else:
include_cause_message = True
edc = Detail(
msg=str(current_exception).strip(),
)
error_details.append(edc)
if isinstance(current_exception, _captured.AnsibleCapturedError):
detail = current_exception.error_summary
error_details.extend(detail.details)
if formatted_traceback and detail.formatted_traceback:
formatted_traceback = (
f'{detail.formatted_traceback}\n'
f'The {current_exception.context} exception above was the direct cause of the following controller exception:\n\n'
f'{formatted_traceback}'
)
if not include_cause_message:
break
current_exception = _get_cause(current_exception)
return ErrorSummary(details=tuple(error_details), formatted_traceback=formatted_traceback)
def concat_message(left: str, right: str) -> str:
"""Normalize `left` by removing trailing punctuation and spaces before appending new punctuation and `right`."""
return f'{left.rstrip(". ")}: {right}'
def get_chained_message(exception: BaseException) -> str:
"""
Return the full chain of exception messages by concatenating the cause(s) until all are exhausted.
"""
error_summary = _create_error_summary(exception)
message_parts = [edc.msg for edc in error_summary.details]
return _dedupe_and_concat_message_chain(message_parts)
@dataclasses.dataclass(kw_only=True, frozen=True)
class SourceContext:
origin: Origin
annotated_source_lines: list[str]
target_line: str | None
def __str__(self) -> str:
msg_lines = [f'Origin: {self.origin}']
if self.annotated_source_lines:
msg_lines.append('')
msg_lines.extend(self.annotated_source_lines)
return '\n'.join(msg_lines)
@classmethod
def from_value(cls, value: t.Any) -> SourceContext | None:
"""Attempt to retrieve source and render a contextual indicator from the value's origin (if any)."""
if value is None:
return None
if isinstance(value, Origin):
origin = value
value = None
else:
origin = Origin.get_tag(value)
if RedactAnnotatedSourceContext.current(optional=True):
return cls.error('content redacted')
if origin and origin.path:
return cls.from_origin(origin)
# DTFIX-RELEASE: redaction context may not be sufficient to avoid secret disclosure without SensitiveData and other enhancements
if value is None:
truncated_value = None
annotated_source_lines = []
else:
# DTFIX-FUTURE: cleanup/share width
try:
value = str(value)
except Exception as ex:
value = f'<< context unavailable: {ex} >>'
truncated_value = textwrap.shorten(value, width=120)
annotated_source_lines = [truncated_value]
return SourceContext(
origin=origin or Origin.UNKNOWN,
annotated_source_lines=annotated_source_lines,
target_line=truncated_value,
)
@staticmethod
def error(message: str | None, origin: Origin | None = None) -> SourceContext:
return SourceContext(
origin=origin,
annotated_source_lines=[f'(source not shown: {message})'] if message else [],
target_line=None,
)
@classmethod
def from_origin(cls, origin: Origin) -> SourceContext:
"""Attempt to retrieve source and render a contextual indicator of an error location."""
from ansible.parsing.vault import is_encrypted # avoid circular import
# DTFIX-FUTURE: support referencing the column after the end of the target line, so we can indicate where a missing character (quote) needs to be added
# this is also useful for cases like end-of-stream reported by the YAML parser
# DTFIX-FUTURE: Implement line wrapping and match annotated line width to the terminal display width.
context_line_count: t.Final = 2
max_annotated_line_width: t.Final = 120
truncation_marker: t.Final = '...'
target_line_num = origin.line_num
if RedactAnnotatedSourceContext.current(optional=True):
return cls.error('content redacted', origin)
if not target_line_num or target_line_num < 1:
return cls.error(None, origin) # message omitted since lack of line number is obvious from pos
start_line_idx = max(0, (target_line_num - 1) - context_line_count) # if near start of file
target_col_num = origin.col_num
try:
with pathlib.Path(origin.path).open() as src:
first_line = src.readline()
lines = list(itertools.islice(itertools.chain((first_line,), src), start_line_idx, target_line_num))
except Exception as ex:
return cls.error(type(ex).__name__, origin)
if is_encrypted(first_line):
return cls.error('content encrypted', origin)
if len(lines) != target_line_num - start_line_idx:
return cls.error('file truncated', origin)
annotated_source_lines = []
line_label_width = len(str(target_line_num))
max_src_line_len = max_annotated_line_width - line_label_width - 1
usable_line_len = max_src_line_len
for line_num, line in enumerate(lines, start_line_idx + 1):
line = line.rstrip('\n') # universal newline default mode on `open` ensures we'll never see anything but \n
line = line.replace('\t', ' ') # mixed tab/space handling is intentionally disabled since we're both format and display config agnostic
if len(line) > max_src_line_len:
line = line[: max_src_line_len - len(truncation_marker)] + truncation_marker
usable_line_len = max_src_line_len - len(truncation_marker)
annotated_source_lines.append(f'{str(line_num).rjust(line_label_width)}{" " if line else ""}{line}')
if target_col_num and usable_line_len >= target_col_num >= 1:
column_marker = f'column {target_col_num}'
target_col_idx = target_col_num - 1
if target_col_idx + 2 + len(column_marker) > max_src_line_len:
column_marker = f'{" " * (target_col_idx - len(column_marker) - 1)}{column_marker} ^'
else:
column_marker = f'{" " * target_col_idx}^ {column_marker}'
column_marker = f'{" " * line_label_width} {column_marker}'
annotated_source_lines.append(column_marker)
elif target_col_num is None:
underline_length = len(annotated_source_lines[-1]) - line_label_width - 1
annotated_source_lines.append(f'{" " * line_label_width} {"^" * underline_length}')
return SourceContext(
origin=origin,
annotated_source_lines=annotated_source_lines,
target_line=lines[-1].rstrip('\n'), # universal newline default mode on `open` ensures we'll never see anything but \n
)

@ -0,0 +1,127 @@
from __future__ import annotations as _annotations
import collections.abc as _c
import textwrap as _textwrap
from ansible.module_utils._internal import _event_utils, _messages
def format_event(event: _messages.Event, include_traceback: bool) -> str:
"""Format an event into a verbose message and traceback."""
msg = format_event_verbose_message(event)
if include_traceback:
msg += '\n' + format_event_traceback(event)
msg = msg.strip()
if '\n' in msg:
msg += '\n\n'
else:
msg += '\n'
return msg
def format_event_traceback(event: _messages.Event) -> str:
"""Format an event into a traceback."""
segments: list[str] = []
while event:
segment = event.formatted_traceback or '(traceback missing)\n'
if event.events:
child_tracebacks = [format_event_traceback(child) for child in event.events]
segment += _format_event_children("Sub-Traceback", child_tracebacks)
segments.append(segment)
if event.chain:
segments.append(f'\n{event.chain.traceback_reason}\n\n')
event = event.chain.event
else:
event = None
return ''.join(reversed(segments))
def format_event_verbose_message(event: _messages.Event) -> str:
"""
Format an event into a verbose message.
Help text, contextual information and sub-events will be included.
"""
segments: list[str] = []
original_event = event
while event:
messages = [event.msg]
chain: _messages.EventChain = event.chain
while chain and chain.follow:
if chain.event.events:
break # do not collapse a chained event with sub-events, since they would be lost
if chain.event.formatted_source_context or chain.event.help_text:
if chain.event.formatted_source_context != event.formatted_source_context or chain.event.help_text != event.help_text:
break # do not collapse a chained event with different details, since they would be lost
if chain.event.chain and chain.msg_reason != chain.event.chain.msg_reason:
break # do not collapse a chained event which has a chain with a different msg_reason
messages.append(chain.event.msg)
chain = chain.event.chain
msg = _event_utils.deduplicate_message_parts(messages)
segment = '\n'.join(_get_message_lines(msg, event.help_text, event.formatted_source_context)) + '\n'
if event.events:
child_msgs = [format_event_verbose_message(child) for child in event.events]
segment += _format_event_children("Sub-Event", child_msgs)
segments.append(segment)
if chain and chain.follow:
segments.append(f'\n{chain.msg_reason}\n\n')
event = chain.event
else:
event = None
if len(segments) > 1:
segments.insert(0, _event_utils.format_event_brief_message(original_event) + '\n\n')
return ''.join(segments)
def _format_event_children(label: str, children: _c.Iterable[str]) -> str:
"""Format the given list of child messages into a single string."""
items = list(children)
count = len(items)
lines = ['\n']
for idx, item in enumerate(items):
lines.append(f'+--[ {label} {idx + 1} of {count} ]---\n')
lines.append(_textwrap.indent(f"\n{item}\n", "| ", lambda value: True))
lines.append(f'+--[ End {label} ]---\n')
return ''.join(lines)
def _get_message_lines(message: str, help_text: str | None, formatted_source_context: str | None) -> list[str]:
"""Return a list of message lines constructed from the given message, help text and formatted source context."""
if help_text and not formatted_source_context and '\n' not in message and '\n' not in help_text:
return [f'{message} {help_text}'] # prefer a single-line message with help text when there is no source context
message_lines = [message]
if formatted_source_context:
message_lines.append(formatted_source_context)
if help_text:
message_lines.append('')
message_lines.append(help_text)
return message_lines

@ -1,9 +1,10 @@
"""Internal utilities for serialization and deserialization."""
# DTFIX-RELEASE: most of this isn't JSON specific, find a better home
# DTFIX-FUTURE: most of this isn't JSON specific, find a better home
from __future__ import annotations
import enum
import json
import typing as t
@ -19,7 +20,9 @@ from ansible.module_utils._internal._datatag import (
from ansible.module_utils._internal._json._profiles import _tagless
from ansible.parsing.vault import EncryptedString
from ansible._internal._datatag._tags import Origin, TrustedAsTemplate
from ansible._internal._templating import _transform
from ansible.module_utils import _internal
from ansible.module_utils._internal import _datatag
_T = t.TypeVar('_T')
_sentinel = object()
@ -52,6 +55,19 @@ class StateTrackingMixIn(HasCurrent):
return self._stack[1:] + [self._current]
class EncryptedStringBehavior(enum.Enum):
"""How `AnsibleVariableVisitor` will handle instances of `EncryptedString`."""
PRESERVE = enum.auto()
"""Preserves the unmodified `EncryptedString` instance."""
DECRYPT = enum.auto()
"""Replaces the value with its decrypted plaintext."""
REDACT = enum.auto()
"""Replaces the value with a placeholder string."""
FAIL = enum.auto()
"""Raises an `AnsibleVariableTypeError` error."""
class AnsibleVariableVisitor:
"""Utility visitor base class to recursively apply various behaviors and checks to variable object graphs."""
@ -63,7 +79,9 @@ class AnsibleVariableVisitor:
convert_mapping_to_dict: bool = False,
convert_sequence_to_list: bool = False,
convert_custom_scalars: bool = False,
allow_encrypted_string: bool = False,
convert_to_native_values: bool = False,
apply_transforms: bool = False,
encrypted_string_behavior: EncryptedStringBehavior = EncryptedStringBehavior.DECRYPT,
):
super().__init__() # supports StateTrackingMixIn
@ -72,7 +90,16 @@ class AnsibleVariableVisitor:
self.convert_mapping_to_dict = convert_mapping_to_dict
self.convert_sequence_to_list = convert_sequence_to_list
self.convert_custom_scalars = convert_custom_scalars
self.allow_encrypted_string = allow_encrypted_string
self.convert_to_native_values = convert_to_native_values
self.apply_transforms = apply_transforms
self.encrypted_string_behavior = encrypted_string_behavior
if apply_transforms:
from ansible._internal._templating import _engine
self._template_engine = _engine.TemplateEngine()
else:
self._template_engine = None
self._current: t.Any = None # supports StateTrackingMixIn
@ -113,22 +140,38 @@ class AnsibleVariableVisitor:
value_type = type(value)
if self.apply_transforms and value_type in _transform._type_transform_mapping:
value = self._template_engine.transform(value)
value_type = type(value)
# DTFIX3: need to handle native copy for keys too
if self.convert_to_native_values and isinstance(value, _datatag.AnsibleTaggedObject):
value = value._native_copy()
value_type = type(value)
result: _T
# DTFIX-RELEASE: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
# DTFIX-RELEASE: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
# DTFIX3: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
# keep in mind the allowed types for keys is a more restrictive set than for values (str and tagged str only, not EncryptedString)
# DTFIX5: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
if (result := self._early_visit(value, value_type)) is not _sentinel:
pass
# DTFIX-RELEASE: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
# DTFIX7: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
elif value_type in _ANSIBLE_ALLOWED_MAPPING_VAR_TYPES: # check mappings first, because they're also collections
with self: # supports StateTrackingMixIn
result = AnsibleTagHelper.tag_copy(value, ((k, self._visit(k, v)) for k, v in value.items()), value_type=value_type)
elif value_type in _ANSIBLE_ALLOWED_NON_SCALAR_COLLECTION_VAR_TYPES:
with self: # supports StateTrackingMixIn
result = AnsibleTagHelper.tag_copy(value, (self._visit(k, v) for k, v in enumerate(t.cast(t.Iterable, value))), value_type=value_type)
elif self.allow_encrypted_string and isinstance(value, EncryptedString):
return value # type: ignore[return-value] # DTFIX-RELEASE: this should probably only be allowed for values in dict, not keys (set, dict)
elif self.encrypted_string_behavior != EncryptedStringBehavior.FAIL and isinstance(value, EncryptedString):
match self.encrypted_string_behavior:
case EncryptedStringBehavior.REDACT:
result = "<redacted>" # type: ignore[assignment]
case EncryptedStringBehavior.PRESERVE:
result = value # type: ignore[assignment]
case EncryptedStringBehavior.DECRYPT:
result = str(value) # type: ignore[assignment]
elif self.convert_mapping_to_dict and _internal.is_intermediate_mapping(value):
with self: # supports StateTrackingMixIn
result = {k: self._visit(k, v) for k, v in value.items()} # type: ignore[assignment]

@ -46,6 +46,8 @@ class _Profile(_profiles._JSONSerializationProfile):
_datetime.datetime: _datatag.AnsibleSerializableDateTime,
}
cls.handle_key = cls._handle_key_str_fallback # legacy stdlib-compatible key behavior
class Encoder(_profiles.AnsibleProfileJSONEncoder):
_profile = _Profile

@ -12,7 +12,7 @@ from . import _legacy
class _InventoryVariableVisitor(_legacy._LegacyVariableVisitor, _json.StateTrackingMixIn):
"""State-tracking visitor implementation that only applies trust to `_meta.hostvars` and `vars` inventory values."""
# DTFIX-RELEASE: does the variable visitor need to support conversion of sequence/mapping for inventory?
# DTFIX5: does the variable visitor need to support conversion of sequence/mapping for inventory?
@property
def _allow_trust(self) -> bool:

@ -8,13 +8,12 @@ from __future__ import annotations as _annotations
import datetime as _datetime
import typing as _t
from ansible._internal import _json
from ansible._internal._datatag import _tags
from ansible.module_utils._internal import _datatag
from ansible.module_utils._internal._json import _profiles
from ansible.parsing import vault as _vault
from ... import _json
class _Untrusted:
"""
@ -48,7 +47,7 @@ class _LegacyVariableVisitor(_json.AnsibleVariableVisitor):
convert_mapping_to_dict=convert_mapping_to_dict,
convert_sequence_to_list=convert_sequence_to_list,
convert_custom_scalars=convert_custom_scalars,
allow_encrypted_string=True,
encrypted_string_behavior=_json.EncryptedStringBehavior.PRESERVE,
)
self.invert_trust = invert_trust
@ -153,9 +152,11 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
'__ansible_vault': cls.deserialize_vault,
}
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
@classmethod
def pre_serialize(cls, encoder: Encoder, o: _t.Any) -> _t.Any:
# DTFIX-RELEASE: these conversion args probably aren't needed
# DTFIX7: these conversion args probably aren't needed
avv = cls.visitor_type(invert_trust=True, convert_mapping_to_dict=True, convert_sequence_to_list=True, convert_custom_scalars=True)
return avv.visit(o)
@ -166,16 +167,6 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
return avv.visit(o)
@classmethod
def handle_key(cls, k: _t.Any) -> _t.Any:
if isinstance(k, str):
return k
# DTFIX-RELEASE: decide if this is a deprecation warning, error, or what?
# Non-string variable names have been disallowed by set_fact and other things since at least 2021.
# DTFIX-RELEASE: document why this behavior is here, also verify the legacy tagless use case doesn't need this same behavior
return str(k)
class Encoder(_profiles.AnsibleProfileJSONEncoder):
_profile = _Profile

@ -0,0 +1,91 @@
from __future__ import annotations
import atexit
import os
import subprocess
from ansible import constants as C
from ansible._internal._errors import _alarm_timeout
from ansible._internal._ssh._ssh_agent import SshAgentClient
from ansible.cli import display
from ansible.errors import AnsibleError
from ansible.module_utils.common.process import get_bin_path
_SSH_AGENT_STDOUT_READ_TIMEOUT = 5 # seconds
def launch_ssh_agent() -> None:
"""If configured via `SSH_AGENT`, launch an ssh-agent for Ansible's use and/or verify access to an existing one."""
try:
_launch_ssh_agent()
except Exception as ex:
raise AnsibleError("Failed to launch ssh agent.") from ex
def _launch_ssh_agent() -> None:
ssh_agent_cfg = C.config.get_config_value('SSH_AGENT')
match ssh_agent_cfg:
case 'none':
display.debug('SSH_AGENT set to none')
return
case 'auto':
try:
ssh_agent_bin = get_bin_path(C.config.get_config_value('SSH_AGENT_EXECUTABLE'))
except ValueError as e:
raise AnsibleError('SSH_AGENT set to auto, but cannot find ssh-agent binary.') from e
ssh_agent_dir = os.path.join(C.DEFAULT_LOCAL_TMP, 'ssh_agent')
os.mkdir(ssh_agent_dir, 0o700)
sock = os.path.join(ssh_agent_dir, 'agent.sock')
display.vvv('SSH_AGENT: starting...')
try:
p = subprocess.Popen(
[ssh_agent_bin, '-D', '-s', '-a', sock],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except OSError as e:
raise AnsibleError('Could not start ssh-agent.') from e
atexit.register(p.terminate)
help_text = f'The ssh-agent {ssh_agent_bin!r} might be an incompatible agent.'
expected_stdout = 'SSH_AUTH_SOCK'
try:
with _alarm_timeout.AnsibleTimeoutError.alarm_timeout(_SSH_AGENT_STDOUT_READ_TIMEOUT):
stdout = p.stdout.read(len(expected_stdout))
except _alarm_timeout.AnsibleTimeoutError as e:
display.error_as_warning(
msg=f'Timed out waiting for expected stdout {expected_stdout!r} from ssh-agent.',
exception=e,
help_text=help_text,
)
else:
if stdout != expected_stdout:
display.warning(
msg=f'The ssh-agent output {stdout!r} did not match expected {expected_stdout!r}.',
help_text=help_text,
)
if p.poll() is not None:
raise AnsibleError(
message='The ssh-agent terminated prematurely.',
help_text=f'{help_text}\n\nReturn Code: {p.returncode}\nStandard Error:\n{p.stderr.read()}',
)
display.vvv(f'SSH_AGENT: ssh-agent[{p.pid}] started and bound to {sock}')
case _:
sock = ssh_agent_cfg
try:
with SshAgentClient(sock) as client:
client.list()
except Exception as e:
raise AnsibleError(f'Could not communicate with ssh-agent using auth sock {sock!r}.') from e
os.environ['SSH_AUTH_SOCK'] = os.environ['ANSIBLE_SSH_AGENT'] = sock

@ -106,21 +106,19 @@ class SshAgentFailure(RuntimeError):
# NOTE: Classes below somewhat represent "Data Type Representations Used in the SSH Protocols"
# as specified by RFC4251
@t.runtime_checkable
class SupportsToBlob(t.Protocol):
def to_blob(self) -> bytes:
...
def to_blob(self) -> bytes: ...
@t.runtime_checkable
class SupportsFromBlob(t.Protocol):
@classmethod
def from_blob(cls, blob: memoryview | bytes) -> t.Self:
...
def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
@classmethod
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
...
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]: ...
def _split_blob(blob: memoryview | bytes, length: int) -> tuple[memoryview | bytes, memoryview | bytes]:
@ -304,10 +302,12 @@ class PrivateKeyMsg(Msg):
return EcdsaPrivateKeyMsg(
getattr(KeyAlgo, f'ECDSA{key_size}'),
unicode_string(f'nistp{key_size}'),
binary_string(private_key.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
)),
binary_string(
private_key.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint,
)
),
mpint(ecdsa_pn.private_value),
)
case Ed25519PrivateKey():
@ -318,7 +318,7 @@ class PrivateKeyMsg(Msg):
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
encryption_algorithm=serialization.NoEncryption(),
)
return Ed25519PrivateKeyMsg(
KeyAlgo.ED25519,
@ -376,14 +376,14 @@ class Ed25519PrivateKeyMsg(PrivateKeyMsg):
@dataclasses.dataclass
class PublicKeyMsg(Msg):
@staticmethod
def get_dataclass(
type: KeyAlgo
) -> type[t.Union[
def get_dataclass(type: KeyAlgo) -> type[
t.Union[
RSAPublicKeyMsg,
EcdsaPublicKeyMsg,
Ed25519PublicKeyMsg,
DSAPublicKeyMsg
]]:
DSAPublicKeyMsg,
]
]:
match type:
case KeyAlgo.RSA:
return RSAPublicKeyMsg
@ -401,29 +401,14 @@ class PublicKeyMsg(Msg):
type: KeyAlgo = self.type
match type:
case KeyAlgo.RSA:
return RSAPublicNumbers(
self.e,
self.n
).public_key()
return RSAPublicNumbers(self.e, self.n).public_key()
case KeyAlgo.ECDSA256 | KeyAlgo.ECDSA384 | KeyAlgo.ECDSA521:
curve = _ECDSA_KEY_TYPE[KeyAlgo(type)]
return EllipticCurvePublicKey.from_encoded_point(
curve(),
self.Q
)
return EllipticCurvePublicKey.from_encoded_point(curve(), self.Q)
case KeyAlgo.ED25519:
return Ed25519PublicKey.from_public_bytes(
self.enc_a
)
return Ed25519PublicKey.from_public_bytes(self.enc_a)
case KeyAlgo.DSA:
return DSAPublicNumbers(
self.y,
DSAParameterNumbers(
self.p,
self.q,
self.g
)
).public_key()
return DSAPublicNumbers(self.y, DSAParameterNumbers(self.p, self.q, self.g)).public_key()
case _:
raise NotImplementedError(type)
@ -437,32 +422,32 @@ class PublicKeyMsg(Msg):
mpint(dsa_pn.parameter_numbers.p),
mpint(dsa_pn.parameter_numbers.q),
mpint(dsa_pn.parameter_numbers.g),
mpint(dsa_pn.y)
mpint(dsa_pn.y),
)
case EllipticCurvePublicKey():
return EcdsaPublicKeyMsg(
getattr(KeyAlgo, f'ECDSA{public_key.curve.key_size}'),
unicode_string(f'nistp{public_key.curve.key_size}'),
binary_string(public_key.public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
))
binary_string(
public_key.public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint,
)
),
)
case Ed25519PublicKey():
return Ed25519PublicKeyMsg(
KeyAlgo.ED25519,
binary_string(public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
))
binary_string(
public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
),
)
case RSAPublicKey():
rsa_pn: RSAPublicNumbers = public_key.public_numbers()
return RSAPublicKeyMsg(
KeyAlgo.RSA,
mpint(rsa_pn.e),
mpint(rsa_pn.n)
)
return RSAPublicKeyMsg(KeyAlgo.RSA, mpint(rsa_pn.e), mpint(rsa_pn.n))
case _:
raise NotImplementedError(public_key)
@ -473,10 +458,7 @@ class PublicKeyMsg(Msg):
msg.comments = unicode_string('')
k = msg.to_blob()
digest.update(k)
return binascii.b2a_base64(
digest.digest(),
newline=False
).rstrip(b'=').decode('utf-8')
return binascii.b2a_base64(digest.digest(), newline=False).rstrip(b'=').decode('utf-8')
@dataclasses.dataclass(order=True, slots=True)
@ -519,9 +501,7 @@ class KeyList(Msg):
def __post_init__(self) -> None:
if self.nkeys != len(self.keys):
raise SshAgentFailure(
"agent: invalid number of keys received for identities list"
)
raise SshAgentFailure("agent: invalid number of keys received for identities list")
@dataclasses.dataclass(order=True, slots=True)
@ -535,8 +515,7 @@ class PublicKeyMsgList(Msg):
return len(self.keys)
@classmethod
def from_blob(cls, blob: memoryview | bytes) -> t.Self:
...
def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
@classmethod
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
@ -546,22 +525,16 @@ class PublicKeyMsgList(Msg):
key_blob, key_blob_length, comment_blob = cls._consume_field(blob)
peek_key_algo, _length, _blob = cls._consume_field(key_blob)
pub_key_msg_cls = PublicKeyMsg.get_dataclass(
KeyAlgo(bytes(peek_key_algo).decode('utf-8'))
)
pub_key_msg_cls = PublicKeyMsg.get_dataclass(KeyAlgo(bytes(peek_key_algo).decode('utf-8')))
_fv, comment_blob_length, blob = cls._consume_field(comment_blob)
key_plus_comment = (
prev_blob[4: (4 + key_blob_length) + (4 + comment_blob_length)]
)
key_plus_comment = prev_blob[4 : (4 + key_blob_length) + (4 + comment_blob_length)]
args.append(pub_key_msg_cls.from_blob(key_plus_comment))
return cls(args), b""
@staticmethod
def _consume_field(
blob: memoryview | bytes
) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
def _consume_field(blob: memoryview | bytes) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
length = uint32.from_blob(blob[:4])
blob = blob[4:]
data, rest = _split_blob(blob, length)
@ -581,10 +554,10 @@ class SshAgentClient:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: types.TracebackType | None
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: types.TracebackType | None,
) -> None:
self.close()
@ -598,34 +571,25 @@ class SshAgentClient:
return resp
def remove_all(self) -> None:
self.send(
ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob()
)
self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob())
def remove(self, public_key: CryptoPublicKey) -> None:
key_blob = PublicKeyMsg.from_public_key(public_key).to_blob()
self.send(
ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() +
uint32(len(key_blob)).to_blob() + key_blob
)
self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() + uint32(len(key_blob)).to_blob() + key_blob)
def add(
self,
private_key: CryptoPrivateKey,
comments: str | None = None,
lifetime: int | None = None,
confirm: bool | None = None,
self,
private_key: CryptoPrivateKey,
comments: str | None = None,
lifetime: int | None = None,
confirm: bool | None = None,
) -> None:
key_msg = PrivateKeyMsg.from_private_key(private_key)
key_msg.comments = unicode_string(comments or '')
if lifetime:
key_msg.constraints += constraints(
[ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]
).to_blob() + uint32(lifetime).to_blob()
key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]).to_blob() + uint32(lifetime).to_blob()
if confirm:
key_msg.constraints += constraints(
[ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]
).to_blob()
key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]).to_blob()
if key_msg.constraints:
msg = ProtocolMsgNumbers.SSH_AGENTC_ADD_ID_CONSTRAINED.to_blob()
@ -638,9 +602,7 @@ class SshAgentClient:
req = ProtocolMsgNumbers.SSH_AGENTC_REQUEST_IDENTITIES.to_blob()
r = memoryview(bytearray(self.send(req)))
if r[0] != ProtocolMsgNumbers.SSH_AGENT_IDENTITIES_ANSWER:
raise SshAgentFailure(
'agent: non-identities answer received for identities list'
)
raise SshAgentFailure('agent: non-identities answer received for identities list')
return KeyList.from_blob(r[1:])
def __contains__(self, public_key: CryptoPublicKey) -> bool:
@ -649,7 +611,7 @@ class SshAgentClient:
@functools.cache
def _key_data_into_crypto_objects(key_data: bytes, passphrase: bytes | None) -> tuple[CryptoPrivateKey, CryptoPublicKey, str]:
def key_data_into_crypto_objects(key_data: bytes, passphrase: bytes | None) -> tuple[CryptoPrivateKey, CryptoPublicKey, str]:
private_key = serialization.ssh.load_ssh_private_key(key_data, passphrase)
public_key = private_key.public_key()
fingerprint = PublicKeyMsg.from_public_key(public_key).fingerprint

@ -1,10 +1,12 @@
from __future__ import annotations
from jinja2 import __version__ as _jinja2_version
import importlib.metadata
jinja2_version = importlib.metadata.version('jinja2')
# DTFIX-FUTURE: sanity test to ensure this doesn't drift from requirements
_MINIMUM_JINJA_VERSION = (3, 1)
_CURRENT_JINJA_VERSION = tuple(map(int, _jinja2_version.split('.', maxsplit=2)[:2]))
_CURRENT_JINJA_VERSION = tuple(map(int, jinja2_version.split('.', maxsplit=2)[:2]))
if _CURRENT_JINJA_VERSION < _MINIMUM_JINJA_VERSION:
raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {_jinja2_version}).')
raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {jinja2_version}).')

@ -12,7 +12,6 @@ from ansible.utils.display import Display
from ._access import NotifiableAccessContextBase
from ._utils import TemplateContext
display = Display()
@ -57,10 +56,11 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
display._deprecated_with_plugin_info(
msg=msg,
help_text=item.deprecated.help_text,
version=item.deprecated.removal_version,
date=item.deprecated.removal_date,
version=item.deprecated.version,
date=item.deprecated.date,
obj=item.template,
plugin=item.deprecated.plugin,
deprecator=item.deprecated.deprecator,
formatted_traceback=item.deprecated.formatted_traceback,
)
return result
@ -80,7 +80,7 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
# DTFIX-FUTURE: ascend the template stack to try and find the nearest string source template
origin = Origin.get_tag(template)
# DTFIX-RELEASE: this should probably use a synthesized description value on the tag
# DTFIX-FUTURE: this should probably use a synthesized description value on the tag
# it is reachable from the data_tagging_controller test: ../playbook_output_validator/filter.py actual_stdout.txt actual_stderr.txt
# -[DEPRECATION WARNING]: `something_old` is deprecated, don't use it! This feature will be removed in version 1.2.3.
# +[DEPRECATION WARNING]: While processing '<<container>>': `something_old` is deprecated, don't use it! This feature will be removed in ...

@ -75,7 +75,7 @@ class TemplateOptions:
value_for_omit: object = Omit
escape_backslashes: bool = True
preserve_trailing_newlines: bool = True
# DTFIX-RELEASE: these aren't really overrides anymore, rename the dataclass and this field
# DTFIX-FUTURE: these aren't really overrides anymore, rename the dataclass and this field
# also mention in docstring this has no effect unless used to template a string
overrides: TemplateOverrides = TemplateOverrides.DEFAULT
@ -122,7 +122,6 @@ class TemplateEngine:
return new_engine
def extend(self, marker_behavior: MarkerBehavior | None = None) -> t.Self:
# DTFIX-RELEASE: bikeshed name, supported features
new_templar = type(self)(
loader=self._loader,
variables=self._variables,
@ -187,7 +186,7 @@ class TemplateEngine:
@property
def available_variables(self) -> dict[str, t.Any] | ChainMap[str, t.Any]:
"""Available variables this instance will use when templating."""
# DTFIX-RELEASE: ensure that we're always accessing this as a shallow container-level snapshot, and eliminate uses of anything
# DTFIX3: ensure that we're always accessing this as a shallow container-level snapshot, and eliminate uses of anything
# that directly mutates this value. _new_context may resolve this for us?
if self._variables is None:
self._variables = self._variables_factory() if self._variables_factory else {}
@ -235,7 +234,7 @@ class TemplateEngine:
def template(
self,
variable: t.Any, # DTFIX-RELEASE: once we settle the new/old API boundaries, rename this (here and in other methods)
variable: t.Any, # DTFIX-FUTURE: once we settle the new/old API boundaries, rename this (here and in other methods)
*,
options: TemplateOptions = TemplateOptions.DEFAULT,
mode: TemplateMode = TemplateMode.DEFAULT,
@ -566,7 +565,12 @@ class TemplateEngine:
)
if _TemplateConfig.allow_broken_conditionals:
_display.deprecated(msg=msg, obj=conditional, help_text=self._BROKEN_CONDITIONAL_ALLOWED_FRAGMENT, version='2.23')
_display.deprecated(
msg=msg,
obj=conditional,
help_text=self._BROKEN_CONDITIONAL_ALLOWED_FRAGMENT,
version='2.23',
)
return bool_result

@ -19,7 +19,7 @@ from jinja2.compiler import Frame
from jinja2.lexer import TOKEN_VARIABLE_BEGIN, TOKEN_VARIABLE_END, TOKEN_STRING, Lexer
from jinja2.nativetypes import NativeCodeGenerator
from jinja2.nodes import Const, EvalContext
from jinja2.runtime import Context
from jinja2.runtime import Context, Macro
from jinja2.sandbox import ImmutableSandboxedEnvironment
from jinja2.utils import missing, LRUCache
@ -49,6 +49,7 @@ from ._jinja_common import (
TruncationMarker,
validate_arg_type,
JinjaCallContext,
_SandboxMode,
)
from ._jinja_plugins import JinjaPluginIntercept, _query, _lookup, _now, _wrap_plugin_output, get_first_marker_arg, _DirectCall, _jinja_const_template_warning
from ._lazy_containers import (
@ -71,6 +72,11 @@ from ansible.vars.hostvars import HostVars, HostVarsVars
from ...module_utils.datatag import native_type_name
JINJA2_OVERRIDE = '#jinja2:'
"""
String values prefixed with this sequence are interpreted as templates, even without template delimiters.
The values following this prefix up to the first newline are parsed as Jinja2 template overrides.
To include this literal value at the start of a string, a space or other character must precede it.
"""
display = Display()
@ -304,7 +310,7 @@ class AnsibleTemplate(Template):
_python_source_temp_path: pathlib.Path | None = None
def __del__(self):
# DTFIX-RELEASE: this still isn't working reliably; something else must be keeping the template object alive
# DTFIX-FUTURE: this still isn't working reliably; something else must be keeping the template object alive
if self._python_source_temp_path:
self._python_source_temp_path.unlink(missing_ok=True)
@ -497,7 +503,7 @@ def create_template_error(ex: Exception, variable: t.Any, is_expression: bool) -
return exception_to_raise
# DTFIX-RELEASE: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
# DTFIX3: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
# also update the protomatter integration test once this is done (the test was written differently since this wasn't done yet)
_BUILTIN_FILTER_ALIASES: dict[str, str] = {}
@ -583,10 +589,17 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
return template_obj
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
# deprecated: description="remove relaxed template sandbox mode support" core_version="2.23"
if _TemplateConfig.sandbox_mode == _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES:
return True
return super().is_safe_attribute(obj, attr, value)
@property
def lexer(self) -> AnsibleLexer:
"""Return/cache an AnsibleLexer with settings from the current AnsibleEnvironment"""
# DTFIX-RELEASE: optimization - we should pre-generate the default cached lexer before forking, not leave it to chance (e.g. simple playbooks)
# DTFIX-FUTURE: optimization - we should pre-generate the default cached lexer before forking, not leave it to chance (e.g. simple playbooks)
key = tuple(getattr(self, name) for name in _TEMPLATE_OVERRIDE_FIELD_NAMES)
lex = self._lexer_cache.get(key)
@ -610,7 +623,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
Without this, `_wrap_filter` will wrap `args` and `kwargs` in templating lazy containers.
This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
"""
# DTFIX-RELEASE: need better logic to handle non-list/non-dict inputs for args/kwargs
# DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
@ -630,7 +643,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
Without this, `_wrap_test` will wrap `args` and `kwargs` in templating lazy containers.
This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
"""
# DTFIX-RELEASE: need better logic to handle non-list/non-dict inputs for args/kwargs
# DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
@ -701,7 +714,6 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
# this code is complemented by our tweaked CodeGenerator _output_const_repr that ensures that literal constants
# in templates aren't double-repr'd in the generated code
if len(node_list) == 1:
# DTFIX-RELEASE: determine if we should do managed access here (we *should* have hit them all during templating/resolve, but ?)
return node_list[0]
# In order to ensure that all markers are tripped, do a recursive finalize before we repr (otherwise we can end up
@ -787,11 +799,14 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
# Performing either before calling them will interfere with that processing.
return super().call(__context, __obj, *args, **kwargs)
if (first_marker := get_first_marker_arg(args, kwargs)) is not None:
# Jinja's generated macro code handles Markers, so pre-emptive raise on Marker args and lazy retrieval should be disabled for the macro invocation.
is_macro = isinstance(__obj, Macro)
if not is_macro and (first_marker := get_first_marker_arg(args, kwargs)) is not None:
return first_marker
try:
with JinjaCallContext(accept_lazy_markers=False):
with JinjaCallContext(accept_lazy_markers=is_macro):
call_res = super().call(__context, __obj, *lazify_container_args(args), **lazify_container_kwargs(kwargs))
if __obj is range:
@ -814,7 +829,7 @@ _sentinel: t.Final[object] = object()
@_DirectCall.mark
def _undef(hint=None):
def _undef(hint: str | None = None) -> UndefinedMarker:
"""Jinja2 global function (undef) for creating getting a `UndefinedMarker` instance, optionally with a custom hint."""
validate_arg_type('hint', hint, (str, type(None)))
@ -856,9 +871,6 @@ def _flatten_and_lazify_vars(mapping: c.Mapping) -> t.Iterable[c.Mapping]:
for m in mapping.maps:
yield from _flatten_and_lazify_vars(m)
elif mapping_type is _AnsibleLazyTemplateDict:
if not mapping:
# DTFIX-RELEASE: handle or remove?
raise Exception("we didn't think it was possible to have an empty lazy here...")
yield mapping
elif mapping_type in (dict, _AnsibleTaggedDict):
# don't propagate empty dictionary layers
@ -882,10 +894,6 @@ def _new_context(
layers = []
if jinja_locals:
# DTFIX-RELEASE: if we can't trip this in coverage, kill it off?
if type(jinja_locals) is not dict: # pylint: disable=unidiomatic-typecheck
raise NotImplementedError("locals must be a dict")
# Omit values set to Jinja's internal `missing` sentinel; they are locals that have not yet been
# initialized in the current context, and should not be exposed to child contexts. e.g.: {% import 'a' as b with context %}.
# The `b` local will be `missing` in the `a` context and should not be propagated as a local to the child context we're creating.
@ -978,19 +986,19 @@ def _finalize_list(o: t.Any, mode: FinalizeMode) -> t.Iterator[t.Any]:
def _maybe_finalize_scalar(o: t.Any) -> t.Any:
# DTFIX-RELEASE: this should check all supported scalar subclasses, not just JSON ones (also, does the JSON serializer handle these cases?)
# DTFIX5: this should check all supported scalar subclasses, not just JSON ones (also, does the JSON serializer handle these cases?)
for target_type in _json_subclassable_scalar_types:
if not isinstance(o, target_type):
continue
match _TemplateConfig.unknown_type_conversion_handler.action:
# we don't want to show the object value, and it can't be Origin-tagged; send the current template value for best effort
case ErrorAction.WARN:
case ErrorAction.WARNING:
display.warning(
msg=f'Type {native_type_name(o)!r} is unsupported in variable storage, converting to {native_type_name(target_type)!r}.',
obj=TemplateContext.current(optional=True).template_value,
)
case ErrorAction.FAIL:
case ErrorAction.ERROR:
raise AnsibleVariableTypeError.from_value(obj=TemplateContext.current(optional=True).template_value)
return target_type(o)
@ -1006,12 +1014,12 @@ def _finalize_fallback_collection(
) -> t.Collection[t.Any]:
match _TemplateConfig.unknown_type_conversion_handler.action:
# we don't want to show the object value, and it can't be Origin-tagged; send the current template value for best effort
case ErrorAction.WARN:
case ErrorAction.WARNING:
display.warning(
msg=f'Type {native_type_name(o)!r} is unsupported in variable storage, converting to {native_type_name(target_type)!r}.',
obj=TemplateContext.current(optional=True).template_value,
)
case ErrorAction.FAIL:
case ErrorAction.ERROR:
raise AnsibleVariableTypeError.from_value(obj=TemplateContext.current(optional=True).template_value)
return _finalize_collection(o, mode, finalizer, target_type)
@ -1028,7 +1036,7 @@ def _finalize_collection(
def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
"""Recurse the template result, rendering any encountered templates, converting containers to non-lazy versions."""
# DTFIX-RELEASE: add tests to ensure this method doesn't drift from allowed types
# DTFIX5: add tests to ensure this method doesn't drift from allowed types
o_type = type(o)
# DTFIX-FUTURE: provide an optional way to check for trusted templates leaking out of templating (injected, but not passed through templar.template)
@ -1045,7 +1053,7 @@ def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
if o_type in _FINALIZE_FAST_PATH_EXACT_ITERABLE_TYPES: # silently convert known sequence types to list
return _finalize_collection(o, mode, _finalize_list, list)
if o_type in Marker.concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
if o_type in Marker._concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
return TemplateContext.current().templar.marker_behavior.handle_marker(o)
if mode is not FinalizeMode.TOP_LEVEL: # unsupported type (do not raise)

@ -2,6 +2,7 @@ from __future__ import annotations
import abc
import collections.abc as c
import enum
import inspect
import itertools
import typing as t
@ -9,7 +10,7 @@ import typing as t
from jinja2 import UndefinedError, StrictUndefined, TemplateRuntimeError
from jinja2.utils import missing
from ansible.module_utils.common.messages import ErrorSummary, Detail
from ...module_utils._internal import _messages
from ansible.constants import config
from ansible.errors import AnsibleUndefinedVariable, AnsibleTypeError
from ansible._internal._errors._handler import ErrorHandler
@ -24,10 +25,16 @@ from ...module_utils.datatag import native_type_name
_patch_jinja() # apply Jinja2 patches before types are declared that are dependent on the changes
class _SandboxMode(enum.Enum):
DEFAULT = enum.auto()
ALLOW_UNSAFE_ATTRIBUTES = enum.auto()
class _TemplateConfig:
allow_embedded_templates: bool = config.get_config_value("ALLOW_EMBEDDED_TEMPLATES")
allow_broken_conditionals: bool = config.get_config_value('ALLOW_BROKEN_CONDITIONALS')
jinja_extensions: list[str] = config.get_config_value('DEFAULT_JINJA2_EXTENSIONS')
sandbox_mode: _SandboxMode = _SandboxMode.__members__[config.get_config_value('_TEMPLAR_SANDBOX_MODE').upper()]
unknown_type_encountered_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED')
unknown_type_conversion_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_CONVERSION')
@ -55,7 +62,7 @@ class Marker(StrictUndefined, Tripwire):
__slots__ = ('_marker_template_source',)
concrete_subclasses: t.ClassVar[set[type[Marker]]] = set()
_concrete_subclasses: t.ClassVar[set[type[Marker]]] = set()
def __init__(
self,
@ -129,7 +136,7 @@ class Marker(StrictUndefined, Tripwire):
def __init_subclass__(cls, **kwargs) -> None:
if not inspect.isabstract(cls):
_untaggable_types.add(cls)
cls.concrete_subclasses.add(cls)
cls._concrete_subclasses.add(cls)
@classmethod
def _init_class(cls):
@ -197,8 +204,6 @@ class TruncationMarker(Marker):
It will only be visible if the previous `Marker` was ignored/replaced instead of being tripped, which would raise an exception.
"""
# DTFIX-RELEASE: make this a singleton?
__slots__ = ()
def __init__(self) -> None:
@ -252,28 +257,18 @@ class UndecryptableVaultError(_captured.AnsibleCapturedError):
class VaultExceptionMarker(ExceptionMarker):
"""A `Marker` value that represents an error accessing a vaulted value during templating."""
__slots__ = ('_marker_undecryptable_ciphertext', '_marker_undecryptable_reason', '_marker_undecryptable_traceback')
__slots__ = ('_marker_undecryptable_ciphertext', '_marker_event')
def __init__(self, ciphertext: str, reason: str, traceback: str | None) -> None:
# DTFIX-RELEASE: when does this show up, should it contain more details?
# see also CapturedExceptionMarker for a similar issue
def __init__(self, ciphertext: str, event: _messages.Event) -> None:
super().__init__(hint='A vault exception marker was tripped.')
self._marker_undecryptable_ciphertext = ciphertext
self._marker_undecryptable_reason = reason
self._marker_undecryptable_traceback = traceback
self._marker_event = event
def _as_exception(self) -> Exception:
return UndecryptableVaultError(
obj=self._marker_undecryptable_ciphertext,
error_summary=ErrorSummary(
details=(
Detail(
msg=self._marker_undecryptable_reason,
),
),
formatted_traceback=self._marker_undecryptable_traceback,
),
event=self._marker_event,
)
def _disarm(self) -> str:
@ -282,16 +277,12 @@ class VaultExceptionMarker(ExceptionMarker):
def get_first_marker_arg(args: c.Sequence, kwargs: dict[str, t.Any]) -> Marker | None:
"""Utility method to inspect plugin args and return the first `Marker` encountered, otherwise `None`."""
# DTFIX-RELEASE: this may or may not need to be public API, move back to utils or once usage is wrapped in a decorator?
for arg in iter_marker_args(args, kwargs):
return arg
return None
# CAUTION: This function is exposed in public API as ansible.template.get_first_marker_arg.
return next(iter_marker_args(args, kwargs), None)
def iter_marker_args(args: c.Sequence, kwargs: dict[str, t.Any]) -> t.Generator[Marker]:
"""Utility method to iterate plugin args and yield any `Marker` encountered."""
# DTFIX-RELEASE: this may or may not need to be public API, move back to utils or once usage is wrapped in a decorator?
for arg in itertools.chain(args, kwargs.values()):
if isinstance(arg, Marker):
yield arg
@ -306,7 +297,7 @@ class JinjaCallContext(NotifiableAccessContextBase):
_mask = True
def __init__(self, accept_lazy_markers: bool) -> None:
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker.concrete_subclasses)
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker._concrete_subclasses)
def _notify(self, o: Marker) -> t.NoReturn:
o.trip()
@ -314,7 +305,7 @@ class JinjaCallContext(NotifiableAccessContextBase):
def validate_arg_type(name: str, value: t.Any, allowed_type_or_types: type | tuple[type, ...], /) -> None:
"""Validate the type of the given argument while preserving context for Marker values."""
# DTFIX-RELEASE: find a home for this as a general-purpose utliity method and expose it after some API review
# DTFIX-FUTURE: find a home for this as a general-purpose utliity method and expose it after some API review
if isinstance(value, allowed_type_or_types):
return

@ -6,14 +6,13 @@ import collections.abc as c
import dataclasses
import datetime
import functools
import inspect
import re
import typing as t
from ansible.errors import (
AnsibleTemplatePluginError,
)
from jinja2 import defaults
from ansible.module_utils._internal._ambient_context import AmbientContextBase
from ansible.module_utils._internal._plugin_exec_context import PluginExecContext
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils._internal._datatag import AnsibleTagHelper
from ansible._internal._datatag._tags import TrustedAsTemplate
@ -115,12 +114,12 @@ class JinjaPluginIntercept(c.MutableMapping):
return first_marker
try:
with JinjaCallContext(accept_lazy_markers=instance.accept_lazy_markers), PluginExecContext(executing_plugin=instance):
with JinjaCallContext(accept_lazy_markers=instance.accept_lazy_markers):
return instance.j2_function(*lazify_container_args(args), **lazify_container_kwargs(kwargs))
except MarkerError as ex:
return ex.source
except Exception as ex:
raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-RELEASE: which name to use? use plugin info?
raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-FUTURE: which name to use? use plugin info?
def _wrap_test(self, instance: AnsibleJinja2Plugin) -> t.Callable:
"""Intercept point for all test plugins to ensure that args are properly templated/lazified."""
@ -129,10 +128,12 @@ class JinjaPluginIntercept(c.MutableMapping):
def wrapper(*args, **kwargs) -> bool | Marker:
result = self._invoke_plugin(instance, *args, **kwargs)
if isinstance(result, Marker):
return result
if not isinstance(result, bool):
template = TemplateContext.current().template_value
# DTFIX-RELEASE: which name to use? use plugin info?
_display.deprecated(
msg=f"The test plugin {instance.ansible_name!r} returned a non-boolean result of type {type(result)!r}. "
"Test plugins must have a boolean result.",
@ -162,7 +163,7 @@ class JinjaPluginIntercept(c.MutableMapping):
class _DirectCall:
"""Functions/methods marked `_DirectCall` bypass Jinja Environment checks for `Marker`."""
_marker_attr: str = "_directcall"
_marker_attr: t.Final[str] = "_directcall"
@classmethod
def mark(cls, src: _TCallable) -> _TCallable:
@ -171,7 +172,7 @@ class _DirectCall:
@classmethod
def is_marked(cls, value: t.Callable) -> bool:
return callable(value) and getattr(value, "_directcall", False)
return callable(value) and getattr(value, cls._marker_attr, False)
@_DirectCall.mark
@ -216,10 +217,7 @@ def _invoke_lookup(*, plugin_name: str, lookup_terms: list, lookup_kwargs: dict[
wantlist = lookup_kwargs.pop('wantlist', False)
errors = lookup_kwargs.pop('errors', 'strict')
with (
JinjaCallContext(accept_lazy_markers=instance.accept_lazy_markers),
PluginExecContext(executing_plugin=instance),
):
with JinjaCallContext(accept_lazy_markers=instance.accept_lazy_markers):
try:
if _TemplateConfig.allow_embedded_templates:
# for backwards compat, only trust constant templates in lookup terms
@ -262,16 +260,14 @@ def _invoke_lookup(*, plugin_name: str, lookup_terms: list, lookup_kwargs: dict[
except MarkerError as ex:
return ex.source
except Exception as ex:
# DTFIX-RELEASE: convert this to the new error/warn/ignore context manager
if isinstance(ex, AnsibleTemplatePluginError):
msg = f'Lookup failed but the error is being ignored: {ex}'
else:
msg = f'An unhandled exception occurred while running the lookup plugin {plugin_name!r}. Error was a {type(ex)}, original message: {ex}'
# DTFIX-FUTURE: convert this to the new error/warn/ignore context manager
if errors == 'warn':
_display.warning(msg)
_display.error_as_warning(
msg=f'An error occurred while running the lookup plugin {plugin_name!r}.',
exception=ex,
)
elif errors == 'ignore':
_display.display(msg, log_only=True)
_display.display(f'An error of type {type(ex)} occurred while running the lookup plugin {plugin_name!r}: {ex}', log_only=True)
else:
raise AnsibleTemplatePluginRuntimeError('lookup', plugin_name) from ex
@ -349,3 +345,28 @@ def _wrap_plugin_output(o: t.Any) -> t.Any:
o = list(o)
return _AnsibleLazyTemplateMixin._try_create(o, LazyOptions.SKIP_TEMPLATES)
_PLUGIN_SOURCES = dict(
filter=defaults.DEFAULT_FILTERS,
test=defaults.DEFAULT_TESTS,
)
def _get_builtin_short_description(plugin: object) -> str:
"""
Make a reasonable effort to break a function docstring down to a single sentence.
We can't use the full docstring due to embedded formatting, particularly RST.
This isn't intended to be perfect, just good enough until we can write our own docs for these.
"""
value = re.split(r'(\.|!|\s\(|:\s)', inspect.getdoc(plugin), 1)[0].replace('\n', ' ')
if value:
value += '.'
return value
def get_jinja_builtin_plugin_descriptions(plugin_type: str) -> dict[str, str]:
"""Returns a dictionary of Jinja builtin plugin names and their short descriptions."""
return {f'ansible.builtin.{name}': _get_builtin_short_description(plugin) for name, plugin in _PLUGIN_SOURCES[plugin_type].items() if name.isidentifier()}

@ -43,7 +43,7 @@ _KNOWN_TYPES: t.Final[set[type]] = (
TemplateModule, # example: '{% import "importme.j2" as im %}{{ im | type_debug }}'
}
| set(PASS_THROUGH_SCALAR_VAR_TYPES)
| set(Marker.concrete_subclasses)
| set(Marker._concrete_subclasses)
)
"""
These types are known to the templating system.
@ -195,7 +195,7 @@ class _AnsibleLazyTemplateMixin:
Return an iterable that wraps each of the given elements in a lazy wrapper.
Only elements wrapped this way will receive lazy processing when retrieved from the collection.
"""
# DTFIX-RELEASE: check relative performance of method-local vs stored generator expressions on implementations of this method
# DTFIX-FUTURE: check relative performance of method-local vs stored generator expressions on implementations of this method
raise NotImplementedError() # pragma: nocover
def _proxy_or_render_lazy_value(self, key: t.Any, value: t.Any) -> t.Any:
@ -346,13 +346,13 @@ class _AnsibleLazyTemplateDict(_AnsibleTaggedDict, _AnsibleLazyTemplateMixin):
return super().__ne__(other)
def __or__(self, other):
# DTFIX-RELEASE: support preservation of laziness when possible like we do for list
# DTFIX-FUTURE: support preservation of laziness when possible like we do for list
# Both sides end up going through _proxy_or_render_lazy_value, so there's no Templar preservation needed.
# In the future this could be made more lazy when both Templar instances are the same, or if per-value Templar tracking was used.
return super().__or__(other)
def __ror__(self, other):
# DTFIX-RELEASE: support preservation of laziness when possible like we do for list
# DTFIX-FUTURE: support preservation of laziness when possible like we do for list
# Both sides end up going through _proxy_or_render_lazy_value, so there's no Templar preservation needed.
# In the future this could be made more lazy when both Templar instances are the same, or if per-value Templar tracking was used.
return super().__ror__(other)
@ -549,7 +549,7 @@ class _AnsibleLazyAccessTuple(_AnsibleTaggedTuple, _AnsibleLazyTemplateMixin):
created as a results of managed access.
"""
# DTFIX-RELEASE: ensure we have tests that explicitly verify this behavior
# DTFIX5: ensure we have tests that explicitly verify this behavior
# nonempty __slots__ not supported for subtype of 'tuple'

@ -0,0 +1,72 @@
from __future__ import annotations as _annotations
import datetime as _datetime
import os as _os
import pwd as _pwd
import time as _time
from ansible import constants as _constants
from ansible.module_utils._internal import _datatag
def generate_ansible_template_vars(
path: str,
fullpath: str | None = None,
dest_path: str | None = None,
include_ansible_managed: bool = True,
) -> dict[str, object]:
"""
Generate and return a dictionary with variable metadata about the template specified by `fullpath`.
If `fullpath` is `None`, `path` will be used instead.
"""
# deprecated description="update the ansible.windows collection to inline this logic instead of calling this internal function" core_version="2.23"
if fullpath is None:
fullpath = _os.path.abspath(path)
template_path = fullpath
template_stat = _os.stat(template_path)
template_uid: int | str
try:
template_uid = _pwd.getpwuid(template_stat.st_uid).pw_name
except KeyError:
template_uid = template_stat.st_uid
temp_vars = dict(
template_host=_os.uname()[1],
template_path=path,
template_mtime=_datetime.datetime.fromtimestamp(template_stat.st_mtime),
template_uid=template_uid,
template_run_date=_datetime.datetime.now(),
template_destpath=dest_path,
template_fullpath=fullpath,
)
if include_ansible_managed: # only inject the config default value if the variable wasn't set
temp_vars['ansible_managed'] = _generate_ansible_managed(template_stat)
return temp_vars
def _generate_ansible_managed(template_stat: _os.stat_result) -> str:
"""Generate and return the `ansible_managed` variable."""
# deprecated description="remove the `_generate_ansible_managed` function and use a constant instead" core_version="2.23"
from ansible.template import trust_as_template
managed_default = _constants.config.get_config_value('DEFAULT_MANAGED_STR')
managed_str = managed_default.format(
# IMPORTANT: These values must be constant strings to avoid template injection.
# Use Jinja template expressions where variables are needed.
host="{{ template_host }}",
uid="{{ template_uid }}",
file="{{ template_path }}",
)
ansible_managed = _time.strftime(managed_str, _time.localtime(template_stat.st_mtime))
ansible_managed = _datatag.AnsibleTagHelper.tag_copy(managed_default, ansible_managed)
ansible_managed = trust_as_template(ansible_managed)
return ansible_managed

@ -5,39 +5,46 @@ from __future__ import annotations
import dataclasses
import typing as t
from ansible.module_utils._internal import _traceback
from ansible.module_utils.common.messages import PluginInfo, ErrorSummary, WarningSummary, DeprecationSummary
from ansible.module_utils._internal import _traceback, _event_utils, _messages
from ansible.parsing.vault import EncryptedString, VaultHelper
from ansible.utils.display import Display
from ._jinja_common import VaultExceptionMarker
from .._errors import _captured, _utils
from .._errors import _captured, _error_factory
from .. import _event_formatting
display = Display()
def plugin_info(value: PluginInfo) -> dict[str, str]:
def plugin_info(value: _messages.PluginInfo) -> dict[str, str]:
"""Render PluginInfo as a dictionary."""
return dataclasses.asdict(value)
def error_summary(value: ErrorSummary) -> str:
def plugin_type(value: _messages.PluginType) -> str:
"""Render PluginType as a string."""
return value.value
def error_summary(value: _messages.ErrorSummary) -> str:
"""Render ErrorSummary as a formatted traceback for backward-compatibility with pre-2.19 TaskResult.exception."""
return value.formatted_traceback or '(traceback unavailable)'
if _traceback._is_traceback_enabled(_traceback.TracebackEvent.ERROR):
return _event_formatting.format_event_traceback(value.event)
return '(traceback unavailable)'
def warning_summary(value: WarningSummary) -> str:
def warning_summary(value: _messages.WarningSummary) -> str:
"""Render WarningSummary as a simple message string for backward-compatibility with pre-2.19 TaskResult.warnings."""
return value._format()
return _event_utils.format_event_brief_message(value.event)
def deprecation_summary(value: DeprecationSummary) -> dict[str, t.Any]:
def deprecation_summary(value: _messages.DeprecationSummary) -> dict[str, t.Any]:
"""Render DeprecationSummary as dict values for backward-compatibility with pre-2.19 TaskResult.deprecations."""
# DTFIX-RELEASE: reconsider which deprecation fields should be exposed here, taking into account that collection_name is to be deprecated
result = value._as_simple_dict()
result.pop('details')
transformed = _event_utils.deprecation_as_dict(value)
transformed.update(deprecator=value.deprecator)
return result
return transformed
def encrypted_string(value: EncryptedString) -> str | VaultExceptionMarker:
@ -47,17 +54,17 @@ def encrypted_string(value: EncryptedString) -> str | VaultExceptionMarker:
except Exception as ex:
return VaultExceptionMarker(
ciphertext=VaultHelper.get_ciphertext(value, with_tags=True),
reason=_utils.get_chained_message(ex),
traceback=_traceback.maybe_extract_traceback(ex, _traceback.TracebackEvent.ERROR),
event=_error_factory.ControllerEventFactory.from_exception(ex, _traceback.is_traceback_enabled(_traceback.TracebackEvent.ERROR)),
)
_type_transform_mapping: dict[type, t.Callable[[t.Any], t.Any]] = {
_captured.CapturedErrorSummary: error_summary,
PluginInfo: plugin_info,
ErrorSummary: error_summary,
WarningSummary: warning_summary,
DeprecationSummary: deprecation_summary,
_messages.PluginInfo: plugin_info,
_messages.PluginType: plugin_type,
_messages.ErrorSummary: error_summary,
_messages.WarningSummary: warning_summary,
_messages.DeprecationSummary: deprecation_summary,
EncryptedString: encrypted_string,
}
"""This mapping is consulted by `Templar.template` to provide custom views of some objects."""

@ -99,7 +99,7 @@ Omit = object.__new__(_OmitType)
_datatag._untaggable_types.add(_OmitType)
# DTFIX-RELEASE: review these type sets to ensure they're not overly permissive/dynamic
# DTFIX5: review these type sets to ensure they're not overly permissive/dynamic
IGNORE_SCALAR_VAR_TYPES = {value for value in _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES if not issubclass(value, str)}
PASS_THROUGH_SCALAR_VAR_TYPES = _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES | {

@ -0,0 +1,26 @@
"""
Testing utilities for use in integration tests, not unit tests or non-test code.
Provides better error behavior than Python's `assert` statement.
"""
from __future__ import annotations
import contextlib
import typing as t
class _Checker:
@staticmethod
def check(value: object, msg: str | None = 'Value is not truthy.') -> None:
"""Raise an `AssertionError` if the given `value` is not truthy."""
if not value:
raise AssertionError(msg)
@contextlib.contextmanager
def hard_fail_context(msg: str) -> t.Generator[_Checker]:
"""Enter a context which converts all exceptions to `BaseException` and provides a `Checker` instance for making assertions."""
try:
yield _Checker()
except BaseException as ex:
raise BaseException(f"Hard failure: {msg}") from ex

@ -4,13 +4,13 @@ import abc
import copy
import typing as t
from yaml import Node
from yaml import Node, ScalarNode
from yaml.constructor import SafeConstructor
from yaml.resolver import BaseResolver
from ansible import constants as C
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils._internal._datatag import AnsibleTagHelper
from ansible.module_utils._internal._datatag import AnsibleTagHelper, AnsibleDatatagBase
from ansible._internal._datatag._tags import Origin, TrustedAsTemplate
from ansible.parsing.vault import EncryptedString
from ansible.utils.display import Display
@ -117,13 +117,13 @@ class AnsibleInstrumentedConstructor(_BaseConstructor):
items = [origin.tag(item) for item in items]
yield origin.tag(items)
def construct_yaml_str(self, node):
def construct_yaml_str(self, node: ScalarNode) -> str:
# Override the default string handling function
# to always return unicode objects
# DTFIX-FUTURE: is this to_text conversion still necessary under Py3?
value = to_text(self.construct_scalar(node))
tags = [self._node_position_info(node)]
tags: list[AnsibleDatatagBase] = [self._node_position_info(node)]
if self.trusted_as_template:
# NB: since we're not context aware, this will happily add trust to dictionary keys; this is actually necessary for

@ -4,8 +4,10 @@ import abc
import collections.abc as c
import typing as t
from yaml.representer import SafeRepresenter
from yaml.nodes import ScalarNode, Node
from ansible._internal._templating import _jinja_common
from ansible.module_utils import _internal
from ansible.module_utils._internal._datatag import AnsibleTaggedObject, Tripwire, AnsibleTagHelper
from ansible.parsing.vault import VaultHelper
from ansible.module_utils.common.yaml import HAS_LIBYAML
@ -32,30 +34,36 @@ class _BaseDumper(SafeDumper, metaclass=abc.ABCMeta):
class AnsibleDumper(_BaseDumper):
"""A simple stub class that allows us to add representers for our custom types."""
# DTFIX-RELEASE: need a better way to handle serialization controls during YAML dumping
def __init__(self, *args, dump_vault_tags: bool | None = None, **kwargs):
super().__init__(*args, **kwargs)
self._dump_vault_tags = dump_vault_tags
@classmethod
def _register_representers(cls) -> None:
cls.add_multi_representer(AnsibleTaggedObject, cls.represent_ansible_tagged_object)
cls.add_multi_representer(Tripwire, cls.represent_tripwire)
cls.add_multi_representer(c.Mapping, SafeRepresenter.represent_dict)
cls.add_multi_representer(c.Sequence, SafeRepresenter.represent_list)
def represent_ansible_tagged_object(self, data):
if self._dump_vault_tags is not False and (ciphertext := VaultHelper.get_ciphertext(data, with_tags=False)):
# deprecated: description='enable the deprecation warning below' core_version='2.23'
# if self._dump_vault_tags is None:
# Display().deprecated(
# msg="Implicit YAML dumping of vaulted value ciphertext is deprecated. Set `dump_vault_tags` to explicitly specify the desired behavior",
# version="2.27",
# )
cls.add_multi_representer(c.Mapping, cls.represent_dict)
cls.add_multi_representer(c.Collection, cls.represent_list)
cls.add_multi_representer(_jinja_common.VaultExceptionMarker, cls.represent_vault_exception_marker)
def get_node_from_ciphertext(self, data: object) -> ScalarNode | None:
if ciphertext := VaultHelper.get_ciphertext(data, with_tags=False):
return self.represent_scalar('!vault', ciphertext, style='|')
return None
def represent_vault_exception_marker(self, data: _jinja_common.VaultExceptionMarker) -> ScalarNode:
if node := self.get_node_from_ciphertext(data):
return node
data.trip()
def represent_ansible_tagged_object(self, data: AnsibleTaggedObject) -> Node:
if _internal.is_intermediate_mapping(data):
return self.represent_dict(data)
if _internal.is_intermediate_iterable(data):
return self.represent_list(data)
if node := self.get_node_from_ciphertext(data):
return node
return self.represent_data(AnsibleTagHelper.as_native_type(data)) # automatically decrypts encrypted strings
def represent_tripwire(self, data: Tripwire) -> t.NoReturn:

@ -7,7 +7,7 @@ import typing as t
from yaml import MarkedYAMLError
from yaml.constructor import ConstructorError
from ansible._internal._errors import _utils
from ansible._internal._errors import _error_utils
from ansible.errors import AnsibleParserError
from ansible._internal._datatag._tags import Origin
@ -34,7 +34,7 @@ class AnsibleYAMLParserError(AnsibleParserError):
if isinstance(exception, MarkedYAMLError):
origin = origin.replace(line_num=exception.problem_mark.line + 1, col_num=exception.problem_mark.column + 1)
source_context = _utils.SourceContext.from_origin(origin)
source_context = _error_utils.SourceContext.from_origin(origin)
target_line = source_context.target_line or '' # for these cases, we don't need to distinguish between None and empty string
@ -66,12 +66,12 @@ class AnsibleYAMLParserError(AnsibleParserError):
# There may be cases where there is a valid tab in a line that has other errors.
# That's OK, users should "fix" their tab usage anyway -- at which point later error handling logic will hopefully find the real issue.
elif (tab_idx := target_line.find('\t')) >= 0:
source_context = _utils.SourceContext.from_origin(origin.replace(col_num=tab_idx + 1))
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=tab_idx + 1))
message = "Tabs are usually invalid in YAML."
# Check for unquoted templates.
elif match := re.search(r'^\s*(?:-\s+)*(?:[\w\s]+:\s+)?(?P<value>\{\{.*}})', target_line):
source_context = _utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
message = 'This may be an issue with missing quotes around a template block.'
# FIXME: Use the captured value to show the actual fix required.
help_text = """
@ -95,7 +95,7 @@ Should be:
# look for an unquoted colon in the value
and (colon_match := re.search(r':($| )', target_fragment))
):
source_context = _utils.SourceContext.from_origin(origin.replace(col_num=value_match.start('value') + colon_match.start() + 1))
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=value_match.start('value') + colon_match.start() + 1))
message = 'Colons in unquoted values must be followed by a non-space character.'
# FIXME: Use the captured value to show the actual fix required.
help_text = """
@ -114,7 +114,7 @@ Should be:
first, last = suspected_value[0], suspected_value[-1]
if first != last: # "foo" in bar
source_context = _utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
message = 'Values starting with a quote must end with the same quote.'
# FIXME: Use the captured value to show the actual fix required, and use that same logic to improve the origin further.
help_text = """
@ -127,7 +127,7 @@ Should be:
raw: '"foo" in bar'
"""
elif first == last and target_line.count(first) > 2: # "foo" and "bar"
source_context = _utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
message = 'Values starting with a quote must end with the same quote, and not contain that quote.'
# FIXME: Use the captured value to show the actual fix required, and use that same logic to improve the origin further.
help_text = """

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save