diff options
Diffstat (limited to 'ansible_collections/community/general')
112 files changed, 4376 insertions, 543 deletions
diff --git a/ansible_collections/community/general/.azure-pipelines/azure-pipelines.yml b/ansible_collections/community/general/.azure-pipelines/azure-pipelines.yml index 7dc438ad3..3f9293ac1 100644 --- a/ansible_collections/community/general/.azure-pipelines/azure-pipelines.yml +++ b/ansible_collections/community/general/.azure-pipelines/azure-pipelines.yml @@ -53,7 +53,7 @@ variables: resources: containers: - container: default - image: quay.io/ansible/azure-pipelines-test-container:4.0.1 + image: quay.io/ansible/azure-pipelines-test-container:6.0.0 pool: Standard @@ -127,6 +127,7 @@ stages: - test: '3.10' - test: '3.11' - test: '3.12' + - test: '3.13' - stage: Units_2_17 displayName: Units 2.17 dependsOn: [] @@ -354,6 +355,7 @@ stages: targets: - test: '3.8' - test: '3.11' + - test: '3.13' - stage: Generic_2_17 displayName: Generic 2.17 dependsOn: [] diff --git a/ansible_collections/community/general/.github/BOTMETA.yml b/ansible_collections/community/general/.github/BOTMETA.yml index add324935..faedb4260 100644 --- a/ansible_collections/community/general/.github/BOTMETA.yml +++ b/ansible_collections/community/general/.github/BOTMETA.yml @@ -157,6 +157,8 @@ files: $filters/jc.py: maintainers: kellyjonbrazil $filters/json_query.py: {} + $filters/keep_keys.py: + maintainers: vbotka $filters/lists.py: maintainers: cfiehe $filters/lists_difference.yml: @@ -170,6 +172,10 @@ files: $filters/lists_union.yml: maintainers: cfiehe $filters/random_mac.py: {} + $filters/remove_keys.py: + maintainers: vbotka + $filters/replace_keys.py: + maintainers: vbotka $filters/time.py: maintainers: resmo $filters/to_days.yml: @@ -502,12 +508,16 @@ files: maintainers: tintoy $modules/discord.py: maintainers: cwollinger + $modules/django_check.py: + maintainers: russoz + $modules/django_command.py: + maintainers: russoz + $modules/django_createcachetable.py: + maintainers: russoz $modules/django_manage.py: ignore: scottanderson42 tastychutney labels: django_manage maintainers: russoz - $modules/django_command.py: - maintainers: russoz $modules/dnf_versionlock.py: maintainers: moreda $modules/dnf_config_manager.py: @@ -1415,6 +1425,8 @@ files: ignore: matze labels: zypper maintainers: $team_suse + $plugin_utils/keys_filter.py: + maintainers: vbotka $plugin_utils/unsafe.py: maintainers: felixfontein $tests/a_module.py: @@ -1454,6 +1466,10 @@ files: maintainers: baldwinSPC nurfet-becirevic t0mk teebes docs/docsite/rst/guide_scaleway.rst: maintainers: $team_scaleway + docs/docsite/rst/guide_deps.rst: + maintainers: russoz + docs/docsite/rst/guide_vardict.rst: + maintainers: russoz docs/docsite/rst/test_guide.rst: maintainers: felixfontein ######################### @@ -1485,7 +1501,7 @@ macros: team_ansible_core: team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo - team_consul: sgargan apollo13 + team_consul: sgargan apollo13 Ilgmi team_cyberark_conjur: jvanderhoof ryanprior team_e_spirit: MatrixCrawler getjack team_flatpak: JayKayy oolongbrothers diff --git a/ansible_collections/community/general/CHANGELOG.md b/ansible_collections/community/general/CHANGELOG.md index 91922fb7a..7a752cb6a 100644 --- a/ansible_collections/community/general/CHANGELOG.md +++ b/ansible_collections/community/general/CHANGELOG.md @@ -2,42 +2,118 @@ **Topics** -- <a href="#v9-0-1">v9\.0\.1</a> +- <a href="#v9-1-0">v9\.1\.0</a> - <a href="#release-summary">Release Summary</a> - <a href="#minor-changes">Minor Changes</a> + - <a href="#deprecated-features">Deprecated Features</a> - <a href="#bugfixes">Bugfixes</a> -- <a href="#v9-0-0">v9\.0\.0</a> + - <a href="#known-issues">Known Issues</a> + - <a href="#new-plugins">New Plugins</a> + - <a href="#filter">Filter</a> + - <a href="#new-modules">New Modules</a> +- <a href="#v9-0-1">v9\.0\.1</a> - <a href="#release-summary-1">Release Summary</a> - <a href="#minor-changes-1">Minor Changes</a> + - <a href="#bugfixes-1">Bugfixes</a> +- <a href="#v9-0-0">v9\.0\.0</a> + - <a href="#release-summary-2">Release Summary</a> + - <a href="#minor-changes-2">Minor Changes</a> - <a href="#breaking-changes--porting-guide">Breaking Changes / Porting Guide</a> - - <a href="#deprecated-features">Deprecated Features</a> + - <a href="#deprecated-features-1">Deprecated Features</a> - <a href="#removed-features-previously-deprecated">Removed Features \(previously deprecated\)</a> - <a href="#security-fixes">Security Fixes</a> - - <a href="#bugfixes-1">Bugfixes</a> - - <a href="#new-plugins">New Plugins</a> + - <a href="#bugfixes-2">Bugfixes</a> + - <a href="#new-plugins-1">New Plugins</a> - <a href="#become">Become</a> - <a href="#callback">Callback</a> - <a href="#connection">Connection</a> - - <a href="#filter">Filter</a> + - <a href="#filter-1">Filter</a> - <a href="#lookup">Lookup</a> - <a href="#test">Test</a> - - <a href="#new-modules">New Modules</a> + - <a href="#new-modules-1">New Modules</a> This changelog describes changes after version 8\.0\.0\. +<a id="v9-1-0"></a> +## v9\.1\.0 + +<a id="release-summary"></a> +### Release Summary + +Regular feature and bugfix release\. + +<a id="minor-changes"></a> +### Minor Changes + +* CmdRunner module util \- argument formats can be specified as plain functions without calling <code>cmd\_runner\_fmt\.as\_func\(\)</code> \([https\://github\.com/ansible\-collections/community\.general/pull/8479](https\://github\.com/ansible\-collections/community\.general/pull/8479)\)\. +* ansible\_galaxy\_install \- add upgrade feature \([https\://github\.com/ansible\-collections/community\.general/pull/8431](https\://github\.com/ansible\-collections/community\.general/pull/8431)\, [https\://github\.com/ansible\-collections/community\.general/issues/8351](https\://github\.com/ansible\-collections/community\.general/issues/8351)\)\. +* cargo \- add option <code>directory</code>\, which allows source directory to be specified \([https\://github\.com/ansible\-collections/community\.general/pull/8480](https\://github\.com/ansible\-collections/community\.general/pull/8480)\)\. +* cmd\_runner module utils \- add decorator <code>cmd\_runner\_fmt\.stack</code> \([https\://github\.com/ansible\-collections/community\.general/pull/8415](https\://github\.com/ansible\-collections/community\.general/pull/8415)\)\. +* cmd\_runner\_fmt module utils \- simplify implementation of <code>cmd\_runner\_fmt\.as\_bool\_not\(\)</code> \([https\://github\.com/ansible\-collections/community\.general/pull/8512](https\://github\.com/ansible\-collections/community\.general/pull/8512)\)\. +* ipa\_dnsrecord \- adds <code>SSHFP</code> record type for managing SSH fingerprints in FreeIPA DNS \([https\://github\.com/ansible\-collections/community\.general/pull/8404](https\://github\.com/ansible\-collections/community\.general/pull/8404)\)\. +* keycloak\_client \- assign auth flow by name \([https\://github\.com/ansible\-collections/community\.general/pull/8428](https\://github\.com/ansible\-collections/community\.general/pull/8428)\)\. +* openbsd\_pkg \- adds diff support to show changes in installed package list\. This does not yet work for check mode \([https\://github\.com/ansible\-collections/community\.general/pull/8402](https\://github\.com/ansible\-collections/community\.general/pull/8402)\)\. +* proxmox \- allow specification of the API port when using proxmox\_\* \([https\://github\.com/ansible\-collections/community\.general/issues/8440](https\://github\.com/ansible\-collections/community\.general/issues/8440)\, [https\://github\.com/ansible\-collections/community\.general/pull/8441](https\://github\.com/ansible\-collections/community\.general/pull/8441)\)\. +* proxmox\_vm\_info \- add <code>network</code> option to retrieve current network information \([https\://github\.com/ansible\-collections/community\.general/pull/8471](https\://github\.com/ansible\-collections/community\.general/pull/8471)\)\. +* redfish\_command \- add <code>wait</code> and <code>wait\_timeout</code> options to allow a user to block a command until a service is accessible after performing the requested command \([https\://github\.com/ansible\-collections/community\.general/issues/8051](https\://github\.com/ansible\-collections/community\.general/issues/8051)\, [https\://github\.com/ansible\-collections/community\.general/pull/8434](https\://github\.com/ansible\-collections/community\.general/pull/8434)\)\. +* redfish\_info \- add command <code>CheckAvailability</code> to check if a service is accessible \([https\://github\.com/ansible\-collections/community\.general/issues/8051](https\://github\.com/ansible\-collections/community\.general/issues/8051)\, [https\://github\.com/ansible\-collections/community\.general/pull/8434](https\://github\.com/ansible\-collections/community\.general/pull/8434)\)\. +* redis\_info \- adds support for getting cluster info \([https\://github\.com/ansible\-collections/community\.general/pull/8464](https\://github\.com/ansible\-collections/community\.general/pull/8464)\)\. + +<a id="deprecated-features"></a> +### Deprecated Features + +* CmdRunner module util \- setting the value of the <code>ignore\_none</code> parameter within a <code>CmdRunner</code> context is deprecated and that feature should be removed in community\.general 12\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/8479](https\://github\.com/ansible\-collections/community\.general/pull/8479)\)\. +* git\_config \- the <code>list\_all</code> option has been deprecated and will be removed in community\.general 11\.0\.0\. Use the <code>community\.general\.git\_config\_info</code> module instead \([https\://github\.com/ansible\-collections/community\.general/pull/8453](https\://github\.com/ansible\-collections/community\.general/pull/8453)\)\. +* git\_config \- using <code>state\=present</code> without providing <code>value</code> is deprecated and will be disallowed in community\.general 11\.0\.0\. Use the <code>community\.general\.git\_config\_info</code> module instead to read a value \([https\://github\.com/ansible\-collections/community\.general/pull/8453](https\://github\.com/ansible\-collections/community\.general/pull/8453)\)\. + +<a id="bugfixes"></a> +### Bugfixes + +* git\_config \- fix behavior of <code>state\=absent</code> if <code>value</code> is present \([https\://github\.com/ansible\-collections/community\.general/issues/8436](https\://github\.com/ansible\-collections/community\.general/issues/8436)\, [https\://github\.com/ansible\-collections/community\.general/pull/8452](https\://github\.com/ansible\-collections/community\.general/pull/8452)\)\. +* keycloak\_realm \- add normalizations for <code>attributes</code> and <code>protocol\_mappers</code> \([https\://github\.com/ansible\-collections/community\.general/pull/8496](https\://github\.com/ansible\-collections/community\.general/pull/8496)\)\. +* launched \- correctly report changed status in check mode \([https\://github\.com/ansible\-collections/community\.general/pull/8406](https\://github\.com/ansible\-collections/community\.general/pull/8406)\)\. +* opennebula inventory plugin \- fix invalid reference to IP when inventory runs against NICs with no IPv4 address \([https\://github\.com/ansible\-collections/community\.general/pull/8489](https\://github\.com/ansible\-collections/community\.general/pull/8489)\)\. +* opentelemetry callback \- do not save the JSON response when using the <code>ansible\.builtin\.uri</code> module \([https\://github\.com/ansible\-collections/community\.general/pull/8430](https\://github\.com/ansible\-collections/community\.general/pull/8430)\)\. +* opentelemetry callback \- do not save the content response when using the <code>ansible\.builtin\.slurp</code> module \([https\://github\.com/ansible\-collections/community\.general/pull/8430](https\://github\.com/ansible\-collections/community\.general/pull/8430)\)\. +* paman \- do not fail if an empty list of packages has been provided and there is nothing to do \([https\://github\.com/ansible\-collections/community\.general/pull/8514](https\://github\.com/ansible\-collections/community\.general/pull/8514)\)\. + +<a id="known-issues"></a> +### Known Issues + +* homectl \- the module does not work under Python 3\.13 or newer\, since it relies on the removed <code>crypt</code> standard library module \([https\://github\.com/ansible\-collections/community\.general/issues/4691](https\://github\.com/ansible\-collections/community\.general/issues/4691)\, [https\://github\.com/ansible\-collections/community\.general/pull/8497](https\://github\.com/ansible\-collections/community\.general/pull/8497)\)\. +* udm\_user \- the module does not work under Python 3\.13 or newer\, since it relies on the removed <code>crypt</code> standard library module \([https\://github\.com/ansible\-collections/community\.general/issues/4690](https\://github\.com/ansible\-collections/community\.general/issues/4690)\, [https\://github\.com/ansible\-collections/community\.general/pull/8497](https\://github\.com/ansible\-collections/community\.general/pull/8497)\)\. + +<a id="new-plugins"></a> +### New Plugins + +<a id="filter"></a> +#### Filter + +* community\.general\.keep\_keys \- Keep specific keys from dictionaries in a list\. +* community\.general\.remove\_keys \- Remove specific keys from dictionaries in a list\. +* community\.general\.replace\_keys \- Replace specific keys in a list of dictionaries\. + +<a id="new-modules"></a> +### New Modules + +* community\.general\.consul\_agent\_check \- Add\, modify\, and delete checks within a consul cluster\. +* community\.general\.consul\_agent\_service \- Add\, modify and delete services within a consul cluster\. +* community\.general\.django\_check \- Wrapper for C\(django\-admin check\)\. +* community\.general\.django\_createcachetable \- Wrapper for C\(django\-admin createcachetable\)\. + <a id="v9-0-1"></a> ## v9\.0\.1 -<a id="release-summary"></a> +<a id="release-summary-1"></a> ### Release Summary Bugfix release for inclusion in Ansible 10\.0\.0rc1\. -<a id="minor-changes"></a> +<a id="minor-changes-1"></a> ### Minor Changes * ansible\_galaxy\_install \- minor refactor in the module \([https\://github\.com/ansible\-collections/community\.general/pull/8413](https\://github\.com/ansible\-collections/community\.general/pull/8413)\)\. -<a id="bugfixes"></a> +<a id="bugfixes-1"></a> ### Bugfixes * cpanm \- use new <code>VarDict</code> to prevent deprecation warning \([https\://github\.com/ansible\-collections/community\.general/issues/8410](https\://github\.com/ansible\-collections/community\.general/issues/8410)\, [https\://github\.com/ansible\-collections/community\.general/pull/8411](https\://github\.com/ansible\-collections/community\.general/pull/8411)\)\. @@ -56,12 +132,12 @@ Bugfix release for inclusion in Ansible 10\.0\.0rc1\. <a id="v9-0-0"></a> ## v9\.0\.0 -<a id="release-summary-1"></a> +<a id="release-summary-2"></a> ### Release Summary This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-05\-20\. -<a id="minor-changes-1"></a> +<a id="minor-changes-2"></a> ### Minor Changes * PythonRunner module utils \- specialisation of <code>CmdRunner</code> to execute Python scripts \([https\://github\.com/ansible\-collections/community\.general/pull/8289](https\://github\.com/ansible\-collections/community\.general/pull/8289)\)\. @@ -190,7 +266,7 @@ This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-0 * django\_manage \- the module will now fail if <code>virtualenv</code> is specified but no virtual environment exists at that location \([https\://github\.com/ansible\-collections/community\.general/pull/8198](https\://github\.com/ansible\-collections/community\.general/pull/8198)\)\. * redfish\_command\, redfish\_config\, redfish\_info \- change the default for <code>timeout</code> from 10 to 60 \([https\://github\.com/ansible\-collections/community\.general/pull/8198](https\://github\.com/ansible\-collections/community\.general/pull/8198)\)\. -<a id="deprecated-features"></a> +<a id="deprecated-features-1"></a> ### Deprecated Features * MH DependencyCtxMgr module\_utils \- deprecate <code>module\_utils\.mh\.mixin\.deps\.DependencyCtxMgr</code> in favour of <code>module\_utils\.deps</code> \([https\://github\.com/ansible\-collections/community\.general/pull/8280](https\://github\.com/ansible\-collections/community\.general/pull/8280)\)\. @@ -231,7 +307,7 @@ This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-0 * cobbler\, gitlab\_runners\, icinga2\, linode\, lxd\, nmap\, online\, opennebula\, proxmox\, scaleway\, stackpath\_compute\, virtualbox\, and xen\_orchestra inventory plugin \- make sure all data received from the remote servers is marked as unsafe\, so remote code execution by obtaining texts that can be evaluated as templates is not possible \([https\://www\.die\-welt\.net/2024/03/remote\-code\-execution\-in\-ansible\-dynamic\-inventory\-plugins/](https\://www\.die\-welt\.net/2024/03/remote\-code\-execution\-in\-ansible\-dynamic\-inventory\-plugins/)\, [https\://github\.com/ansible\-collections/community\.general/pull/8098](https\://github\.com/ansible\-collections/community\.general/pull/8098)\)\. * keycloak\_identity\_provider \- the client secret was not correctly sanitized by the module\. The return values <code>proposed</code>\, <code>existing</code>\, and <code>end\_state</code>\, as well as the diff\, did contain the client secret unmasked \([https\://github\.com/ansible\-collections/community\.general/pull/8355](https\://github\.com/ansible\-collections/community\.general/pull/8355)\)\. -<a id="bugfixes-1"></a> +<a id="bugfixes-2"></a> ### Bugfixes * aix\_filesystem \- fix <code>\_validate\_vg</code> not passing VG name to <code>lsvg\_cmd</code> \([https\://github\.com/ansible\-collections/community\.general/issues/8151](https\://github\.com/ansible\-collections/community\.general/issues/8151)\)\. @@ -306,7 +382,7 @@ This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-0 * to\_ini filter plugin \- disabling interpolation of <code>ConfigParser</code> to allow converting values with a <code>\%</code> sign \([https\://github\.com/ansible\-collections/community\.general/issues/8183](https\://github\.com/ansible\-collections/community\.general/issues/8183)\, [https\://github\.com/ansible\-collections/community\.general/pull/8185](https\://github\.com/ansible\-collections/community\.general/pull/8185)\)\. * xml \- make module work with lxml 5\.1\.1\, which removed some internals that the module was relying on \([https\://github\.com/ansible\-collections/community\.general/pull/8169](https\://github\.com/ansible\-collections/community\.general/pull/8169)\)\. -<a id="new-plugins"></a> +<a id="new-plugins-1"></a> ### New Plugins <a id="become"></a> @@ -325,7 +401,7 @@ This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-0 * community\.general\.incus \- Run tasks in Incus instances via the Incus CLI\. -<a id="filter"></a> +<a id="filter-1"></a> #### Filter * community\.general\.from\_ini \- Converts INI text input into a dictionary\. @@ -346,7 +422,7 @@ This is release 9\.0\.0 of <code>community\.general</code>\, released on 2024\-0 * community\.general\.fqdn\_valid \- Validates fully\-qualified domain names against RFC 1123\. -<a id="new-modules"></a> +<a id="new-modules-1"></a> ### New Modules * community\.general\.consul\_acl\_bootstrap \- Bootstrap ACLs in Consul\. diff --git a/ansible_collections/community/general/CHANGELOG.rst b/ansible_collections/community/general/CHANGELOG.rst index 384bee747..523acb975 100644 --- a/ansible_collections/community/general/CHANGELOG.rst +++ b/ansible_collections/community/general/CHANGELOG.rst @@ -6,6 +6,73 @@ Community General Release Notes This changelog describes changes after version 8.0.0. +v9.1.0 +====== + +Release Summary +--------------- + +Regular feature and bugfix release. + +Minor Changes +------------- + +- CmdRunner module util - argument formats can be specified as plain functions without calling ``cmd_runner_fmt.as_func()`` (https://github.com/ansible-collections/community.general/pull/8479). +- ansible_galaxy_install - add upgrade feature (https://github.com/ansible-collections/community.general/pull/8431, https://github.com/ansible-collections/community.general/issues/8351). +- cargo - add option ``directory``, which allows source directory to be specified (https://github.com/ansible-collections/community.general/pull/8480). +- cmd_runner module utils - add decorator ``cmd_runner_fmt.stack`` (https://github.com/ansible-collections/community.general/pull/8415). +- cmd_runner_fmt module utils - simplify implementation of ``cmd_runner_fmt.as_bool_not()`` (https://github.com/ansible-collections/community.general/pull/8512). +- ipa_dnsrecord - adds ``SSHFP`` record type for managing SSH fingerprints in FreeIPA DNS (https://github.com/ansible-collections/community.general/pull/8404). +- keycloak_client - assign auth flow by name (https://github.com/ansible-collections/community.general/pull/8428). +- openbsd_pkg - adds diff support to show changes in installed package list. This does not yet work for check mode (https://github.com/ansible-collections/community.general/pull/8402). +- proxmox - allow specification of the API port when using proxmox_* (https://github.com/ansible-collections/community.general/issues/8440, https://github.com/ansible-collections/community.general/pull/8441). +- proxmox_vm_info - add ``network`` option to retrieve current network information (https://github.com/ansible-collections/community.general/pull/8471). +- redfish_command - add ``wait`` and ``wait_timeout`` options to allow a user to block a command until a service is accessible after performing the requested command (https://github.com/ansible-collections/community.general/issues/8051, https://github.com/ansible-collections/community.general/pull/8434). +- redfish_info - add command ``CheckAvailability`` to check if a service is accessible (https://github.com/ansible-collections/community.general/issues/8051, https://github.com/ansible-collections/community.general/pull/8434). +- redis_info - adds support for getting cluster info (https://github.com/ansible-collections/community.general/pull/8464). + +Deprecated Features +------------------- + +- CmdRunner module util - setting the value of the ``ignore_none`` parameter within a ``CmdRunner`` context is deprecated and that feature should be removed in community.general 12.0.0 (https://github.com/ansible-collections/community.general/pull/8479). +- git_config - the ``list_all`` option has been deprecated and will be removed in community.general 11.0.0. Use the ``community.general.git_config_info`` module instead (https://github.com/ansible-collections/community.general/pull/8453). +- git_config - using ``state=present`` without providing ``value`` is deprecated and will be disallowed in community.general 11.0.0. Use the ``community.general.git_config_info`` module instead to read a value (https://github.com/ansible-collections/community.general/pull/8453). + +Bugfixes +-------- + +- git_config - fix behavior of ``state=absent`` if ``value`` is present (https://github.com/ansible-collections/community.general/issues/8436, https://github.com/ansible-collections/community.general/pull/8452). +- keycloak_realm - add normalizations for ``attributes`` and ``protocol_mappers`` (https://github.com/ansible-collections/community.general/pull/8496). +- launched - correctly report changed status in check mode (https://github.com/ansible-collections/community.general/pull/8406). +- opennebula inventory plugin - fix invalid reference to IP when inventory runs against NICs with no IPv4 address (https://github.com/ansible-collections/community.general/pull/8489). +- opentelemetry callback - do not save the JSON response when using the ``ansible.builtin.uri`` module (https://github.com/ansible-collections/community.general/pull/8430). +- opentelemetry callback - do not save the content response when using the ``ansible.builtin.slurp`` module (https://github.com/ansible-collections/community.general/pull/8430). +- paman - do not fail if an empty list of packages has been provided and there is nothing to do (https://github.com/ansible-collections/community.general/pull/8514). + +Known Issues +------------ + +- homectl - the module does not work under Python 3.13 or newer, since it relies on the removed ``crypt`` standard library module (https://github.com/ansible-collections/community.general/issues/4691, https://github.com/ansible-collections/community.general/pull/8497). +- udm_user - the module does not work under Python 3.13 or newer, since it relies on the removed ``crypt`` standard library module (https://github.com/ansible-collections/community.general/issues/4690, https://github.com/ansible-collections/community.general/pull/8497). + +New Plugins +----------- + +Filter +~~~~~~ + +- community.general.keep_keys - Keep specific keys from dictionaries in a list. +- community.general.remove_keys - Remove specific keys from dictionaries in a list. +- community.general.replace_keys - Replace specific keys in a list of dictionaries. + +New Modules +----------- + +- community.general.consul_agent_check - Add, modify, and delete checks within a consul cluster. +- community.general.consul_agent_service - Add, modify and delete services within a consul cluster. +- community.general.django_check - Wrapper for C(django-admin check). +- community.general.django_createcachetable - Wrapper for C(django-admin createcachetable). + v9.0.1 ====== diff --git a/ansible_collections/community/general/FILES.json b/ansible_collections/community/general/FILES.json index 87fdd5c9a..41f20623d 100644 --- a/ansible_collections/community/general/FILES.json +++ b/ansible_collections/community/general/FILES.json @@ -109,7 +109,7 @@ "name": ".azure-pipelines/azure-pipelines.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "48eb26e372a37363159b6a17ba403c830d01c2cb29c106bfa8e9748ad534ff50", + "chksum_sha256": "a79320cab46fce92c3205300c9f17c58bc8dacbb21868bebe83636d4a730e99d", "format": 1 }, { @@ -193,7 +193,7 @@ "name": ".github/BOTMETA.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "95b932042b339875673fcbe218094b962576d3900511a041b9b7fb91a858e68c", + "chksum_sha256": "cc8b862bd68049694fb8b89ff204b657855e0a8f80ea78a643647d19ed4997f3", "format": 1 }, { @@ -312,7 +312,7 @@ "name": "changelogs/changelog.yaml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0014fa74832def0e1d47b994b7aee9fbe393a30553ac051acc1f9a2ac886be6a", + "chksum_sha256": "7079dd86212c9c1ba677b86a9b240c210f0f9d591eba846bd2b871050032cbf0", "format": 1 }, { @@ -368,14 +368,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-001_vars/default-common.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bd2eef902441d9c71b84d997508e78803e648c534e75c8b12323e199eeca81d6", + "chksum_sha256": "1cf297e6880eb27f8fd1da74208b089d70647a972fb8357f613e58bf40334fcd", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-001_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "663a3b0429cd096b10b06038a561f01872a34bc0e611f535abc9e626a474b6a9", + "chksum_sha256": "3cdee8eb1544ce35baf81583ab7e7c722dbe81e5643c3013d2739f22919581c0", "format": 1 }, { @@ -389,14 +389,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-002_vars/default-common.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bd2eef902441d9c71b84d997508e78803e648c534e75c8b12323e199eeca81d6", + "chksum_sha256": "1cf297e6880eb27f8fd1da74208b089d70647a972fb8357f613e58bf40334fcd", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-002_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "852a8518e33c1d8a2144f1835f6a10b0c17fcc13850cf45475ce538a4312171e", + "chksum_sha256": "39da0ce92bf01050b04f3aafecad1870fba5b6970e7301a618f9445d885ddf94", "format": 1 }, { @@ -410,14 +410,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-003_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-003_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "73003165935630df144177b3cbb78954f7eeaccc3031d694a1e1c2f8b365d99d", + "chksum_sha256": "2f7de68a5c297dc9bec3127737002cf214052069d709b77412df2c42eb03d3bb", "format": 1 }, { @@ -431,14 +431,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-004_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-004_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1b04d243241c56ad51cdee8c34a6475af76482801a74a55a269a296122c3be44", + "chksum_sha256": "a260b433df2d9d689bf5cd61141e91a40f86677c944f8ad75e85da0c1fb02a31", "format": 1 }, { @@ -452,14 +452,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-005_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-005_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d4541eb704de6d64c4c34c12cc22328e19016c66811e99af22b4be27053d9526", + "chksum_sha256": "2a198e5156b90d540962bf0adfffdb445294449e815cf2c0471efcafdfd3a996", "format": 1 }, { @@ -473,14 +473,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-006_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-006_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "205959e977ba30d49bdfd879f4b7bb2f50b50762361d89b2360cbb419a6af931", + "chksum_sha256": "577f0bd0c766630aa359f0453691007d9a9d3a0f4535b9b9b161ce407bff2d6d", "format": 1 }, { @@ -494,14 +494,14 @@ "name": "docs/docsite/helper/lists_mergeby/example-007_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-007_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f5517facd2643e52b06c5181df7768b096b2916ac411f7403484f0ad1b7d8ee1", + "chksum_sha256": "41b95615fb4eca9d5beab31ea436ca9875a2b2d109dc036919cd68388fc64b17", "format": 1 }, { @@ -515,119 +515,154 @@ "name": "docs/docsite/helper/lists_mergeby/example-008_vars/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-008_vars/list3.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a833536f106aebd7a5f737743f96c791302260e72f9393be53bdda0a86a10c9a", + "chksum_sha256": "9b2d102869f86a54b1ad7c5b38bdd6515aa3b7e444747069dce98d32fa12f640", + "format": 1 + }, + { + "name": "docs/docsite/helper/lists_mergeby/example-009_vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/helper/lists_mergeby/example-009_vars/default-common.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1cf297e6880eb27f8fd1da74208b089d70647a972fb8357f613e58bf40334fcd", + "format": 1 + }, + { + "name": "docs/docsite/helper/lists_mergeby/example-009_vars/list3.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4523d397880b12fc1b8ec3c85403c30cf59e95fdfafc9b40e4e3279856f3fa5a", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/default-common.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bd2eef902441d9c71b84d997508e78803e648c534e75c8b12323e199eeca81d6", + "chksum_sha256": "1cf297e6880eb27f8fd1da74208b089d70647a972fb8357f613e58bf40334fcd", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/default-recursive-true.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce747f67c1635a6dfd887c7ebf3ee02c2014b9eced5773b08d747a11fd916a95", + "chksum_sha256": "40233cee2ac468835816e1e05b898c6c2444c89dafeb81f5aac16a26dc417734", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-001.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ec664dbed63f2d8f9d7173a44f6f53b8aac3917e4c152082157ae06b2b64b717", + "chksum_sha256": "b7f83ae4eda65288a5c7b1e448d3f379ac69f5c40c0bbb2b180d0ad71b33ac3b", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-002.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "174db79b357280e60b4b37b96e77708148894d4d81571fa10d65d9846bbcf365", + "chksum_sha256": "c5abd9609513167ba5a0df49223fb9269e288d3ecd597004a6ae066179c6d368", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-003.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5fac7cdbb3fc0b3addddb5ffaa58fcbf241df07195b41ad011f2550df1281fc7", + "chksum_sha256": "211edd2c682dcee996f4211f8c832896401ef0dc76f38f6e91aef6c5789d7721", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-004.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c7781daf8df6f6e6403fc8bd8ba40a6a5515e24b1ffa96f85b4d3cb2e23d926e", + "chksum_sha256": "05090716aaf46cb3ed48035d83482aec11ff0b3c30cc850f30fb5320844c8877", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-005.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c02a2a83fc72b27668c5ce96e0c3feb466ea89047f5fa8bc961260bce0aa97b5", + "chksum_sha256": "6dc6b4160e81b1d49936db28762428d182da87441fa66817146daa4b747998b2", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-006.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9eda979168ded0b39e43a4ae0e02c38cdccecc236604b61cbf80069869e762e3", + "chksum_sha256": "03adfa30300af062e28fb963bc2472cabf4102dce03e088470727ee35df488db", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-007.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "78a002ab23ee4a16c60322ce988af490a5d900131fb621ada85541afee932fdd", + "chksum_sha256": "c5ab5b8f580cd9bca71c93f2e0581593e170357d4a677fafefa10023524f6bd5", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/example-008.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "11c87a5bd327951a21baaf28e47d11c0c07e8a12cdcc5326fcd7f8b0d5217d56", + "chksum_sha256": "ac56a41a9e1bcdef1c80c590484f9081ec7f095106df18777a56e506a3a26970", + "format": 1 + }, + { + "name": "docs/docsite/helper/lists_mergeby/example-009.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1cb469396a08823b54768d987f12bf94414e82f218c3923d9af1c421833c09e1", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/examples.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "fe278ca276168ebfc2167cf5ad2e2d4b3cae0d6cdd246326b11620db259e2eb3", + "chksum_sha256": "f22163a6554b36a7df5d626e0061f3c4b8d83d4fe7f6c64b3f3ff3373598bb92", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/examples_all.rst.j2", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "31f8a0a79c3aebf801b74e0cc7587ea7486fc221a7ab948701211f0178310ace", + "chksum_sha256": "55874c71532504a32b927c036756f8e32a8dc6695f34c4451e91defa6c62b316", + "format": 1 + }, + { + "name": "docs/docsite/helper/lists_mergeby/extra-vars.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3cc1cfbd48140a9368dc9583683da172197ce124e3e1e44a75bb50e524945729", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "27fd7341d931b081d0caa6d9dbeacee7cd788bb5418019fb70e5794293f15582", + "chksum_sha256": "1f30ab3058307c531437e39abb3aaa7b0258b8a1537adcb82844f9b51d5bc1fc", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/list3.out.j2", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d228735e576466950b3a90b1de44b6a503de957521b3db41a492d742695d8d71", + "chksum_sha256": "e3a21ec55be646c12180396c2311c85d341ab5f75bf8a16ea09fe3c214b47da9", "format": 1 }, { "name": "docs/docsite/helper/lists_mergeby/playbook.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "825f6e9f15549cd02a8fa1cf5a6b6bdbc1dc39ff80ce59f5014af8c0358bee58", + "chksum_sha256": "840405c1b7fc7cd852404e9ee35337626f97cd6843457cc7ad5a4de50902a707", "format": 1 }, { @@ -683,7 +718,7 @@ "name": "docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a0a81486d2b415efa2e2f8576b3aadb98125e74b10b2113f13f6dcf1e652faed", + "chksum_sha256": "6b7a3ec73c58b87695919903c13805db6bac8c2b44d0a916f432651ac1e4ee58", "format": 1 }, { @@ -743,6 +778,13 @@ "format": 1 }, { + "name": "docs/docsite/rst/guide_deps.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "df07337b3596ae5246294cceecf12ca46287436ad2e381a7e7d25a84ed33e9bd", + "format": 1 + }, + { "name": "docs/docsite/rst/guide_online.rst", "ftype": "file", "chksum_type": "sha256", @@ -764,6 +806,13 @@ "format": 1 }, { + "name": "docs/docsite/rst/guide_vardict.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eabc2712be99330fba6016b38ef1410427b115631ff10bad98922c1c0dc1705a", + "format": 1 + }, + { "name": "docs/docsite/rst/test_guide.rst", "ftype": "file", "chksum_type": "sha256", @@ -781,7 +830,7 @@ "name": "docs/docsite/extra-docs.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f3f9cbc513d494da6c34cf6db49319f34645a8dd1ee79736360c0e48643e76af", + "chksum_sha256": "3099ca216ce7a424bef49f96819691f5c0411bf838ebeb814f3fd2c0ef8c8eeb", "format": 1 }, { @@ -802,7 +851,7 @@ "name": "meta/runtime.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bdd7473e27902ea009dac03f604dd957a93413ee0275b561aff956ff26caf391", + "chksum_sha256": "6934d562c2c37a3ffb229276c9bfbaf2f869a1d9e0538072b004885e09c0b13f", "format": 1 }, { @@ -1075,7 +1124,7 @@ "name": "plugins/callback/opentelemetry.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6eb005c02c46afa1b07d7d7861b54496d30ced27113f10b156719b3f151b3014", + "chksum_sha256": "f53258ebf00c73a1ac7908a1e963b100673c586e1f68da40592caf0af78bdb10", "format": 1 }, { @@ -1278,7 +1327,7 @@ "name": "plugins/doc_fragments/django.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0836d2f746a8b62a1408455fdd54bcbd8f41342eb2f07a681b378b0efef5f3c8", + "chksum_sha256": "e30b30e859e4c2861828eee9c1b35b54c535ef2aa0ac77ba12cfcf1611ab9bb4", "format": 1 }, { @@ -1453,7 +1502,7 @@ "name": "plugins/doc_fragments/proxmox.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "daa7d15e8a1ffc6e4c539712099537ab9c0f667684d2bcf4ba89e196caf9e460", + "chksum_sha256": "b64edf0d5addde754b70fd42e65b304913c0ff2e45004853d04d0b2bd5adbbca", "format": 1 }, { @@ -1604,6 +1653,13 @@ "format": 1 }, { + "name": "plugins/filter/keep_keys.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f4474076e83cfe8850482fbe29663644d0ca4b15f179a0a2646b9b92bbe7b824", + "format": 1 + }, + { "name": "plugins/filter/lists.py", "ftype": "file", "chksum_type": "sha256", @@ -1628,7 +1684,7 @@ "name": "plugins/filter/lists_mergeby.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4163df17512c4388bf020417cdd3f9919db9c6f1c23b2a57852bd10523a0abbe", + "chksum_sha256": "6ab23ff362b7940d0ec340eda1ef9a0d3262dc6894a9139fa1357ffdbb2674da", "format": 1 }, { @@ -1653,6 +1709,20 @@ "format": 1 }, { + "name": "plugins/filter/remove_keys.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d8c3d19a4129c1007a675dbb97f9f593aa410c35887891dfeb556e68a5aec62a", + "format": 1 + }, + { + "name": "plugins/filter/replace_keys.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c5ddedca5a0fa34413b894a9e40991aa91ba6cb112269fe3e4bf311c86dc99bf", + "format": 1 + }, + { "name": "plugins/filter/time.py", "ftype": "file", "chksum_type": "sha256", @@ -1803,7 +1873,7 @@ "name": "plugins/inventory/opennebula.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "954bb22612557fbe3be3510d3ebc7e230164cb3dcb689345a287270b85234970", + "chksum_sha256": "e3fd2fdd5abb43a0690b6b97daa79d45bfa334268e9d1051f11a5806b87956e4", "format": 1 }, { @@ -2342,14 +2412,14 @@ "name": "plugins/module_utils/cmd_runner.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9b9f5d0e0ed818b1bdc80e8a6a4a858ba23f92bcaf6d5b224073b4e4a5de1e4c", + "chksum_sha256": "d627300d236348a1df761fcb8624c5d55f72c33938a08250097c7e28259294d8", "format": 1 }, { "name": "plugins/module_utils/consul.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9e572ae8233e2f4a72bbdaba9d39f0e0d631ff4ccb3684a32f853d0b7b6581bd", + "chksum_sha256": "3f55f60552f8187b7db9c4ce3bc915cf6e278594f21c0e30c5024af736aff172", "format": 1 }, { @@ -2391,7 +2461,7 @@ "name": "plugins/module_utils/django.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "78e63b0a1b6304d42171f878bcb50bfd1ff96bfead0ba41220c0b43b6ad1f7cb", + "chksum_sha256": "46b9d866dcc4432d3c7bf67b4e31037d1fdd064bf421359a1c1660bd4331bc32", "format": 1 }, { @@ -2587,7 +2657,7 @@ "name": "plugins/module_utils/proxmox.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "756e2b829ba10cf0f2d177b8729b92007ef23edaf1a708c064b1cbdb9b96b02c", + "chksum_sha256": "235bc199d79987aaa2d49984ad2b8140fbb58f3fa536109e82d660381316d57b", "format": 1 }, { @@ -2615,7 +2685,7 @@ "name": "plugins/module_utils/redfish_utils.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce4679e15df51fc1a102f5f6f8ef1cb8f8153cfb5a896786c5531829d76ce0e2", + "chksum_sha256": "43fc29e5b41bc436a7c1d2face5334cc3a8a79861defbcece2dc5156b78c0081", "format": 1 }, { @@ -2811,7 +2881,7 @@ "name": "plugins/modules/ansible_galaxy_install.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "7d6e02e1a7306d048c97e5e9f2e5553fe8ece50ded61a39f014af2fecd51da2a", + "chksum_sha256": "a482de0224539003f97a7cc019a7ec499c7fb3b40a95964f421c47f19d397935", "format": 1 }, { @@ -2986,7 +3056,7 @@ "name": "plugins/modules/cargo.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1f937d2d6b2eb1d54baaaddc8aad6e45ff219c1762ea07d73e028b1030b47601", + "chksum_sha256": "59179736f71cff627a2c85788a5cd93bc0885347bfeb5037bfd1d4921ba20382", "format": 1 }, { @@ -3137,17 +3207,31 @@ "format": 1 }, { + "name": "plugins/modules/consul_agent_check.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4b47d0ab359a40730c169d01873d0eb1023ade6c0c42529910cb047ec0fb4be1", + "format": 1 + }, + { + "name": "plugins/modules/consul_agent_service.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dc493896df6b2b2754a68c557e7902214acfc9a34fcdaea25dd754e90a17c6e0", + "format": 1 + }, + { "name": "plugins/modules/consul_auth_method.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "776c407945f239f40ea36e24f42606d20141316ffa7f36e95cb3f00efa4c6bcb", + "chksum_sha256": "0ee3b66b9b873472ad25479c8809b34ea9da645e942efd602b9f0ff7fe6ccafd", "format": 1 }, { "name": "plugins/modules/consul_binding_rule.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bfc78b6b53d3ad705f099e2ed04eaff75ab0d1ef3d823b3de58d6881efe2178d", + "chksum_sha256": "78859326d12520ddec8e4f3b324f6e7b8150e8252a0babd4dc7932ae29dad096", "format": 1 }, { @@ -3161,14 +3245,14 @@ "name": "plugins/modules/consul_policy.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "05fcd6bd9085bb781d7f9bdc8a808f1e63f493d018299ce3dcb6a8c7f89ae729", + "chksum_sha256": "3757983feca43c6da4a0cf08fc6dbbf71f6095bf7e16cf6f3698d47fe171cb5f", "format": 1 }, { "name": "plugins/modules/consul_role.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f18b68eab0c232b18fefe5d66164dfa49e4e9c11396616200e23b0774c98c5ae", + "chksum_sha256": "33f6b5f02c216241fef4dd98371c08737c1cb8094f058f4a08f71828d4eeed0b", "format": 1 }, { @@ -3182,7 +3266,7 @@ "name": "plugins/modules/consul_token.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8572e2337ce15303534844ed997859fc6e26b3b5537ea80fbea280f1bfa17419", + "chksum_sha256": "34b3c9e7bed0b8566edb79ccdd46f49c466f7c261e0b69c1b1d748dcbbe6ab86", "format": 1 }, { @@ -3270,6 +3354,13 @@ "format": 1 }, { + "name": "plugins/modules/django_check.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e028e8dd895fef7c8002e310ec5ac92c183c4993fc8d061c80fa0de916c057c", + "format": 1 + }, + { "name": "plugins/modules/django_command.py", "ftype": "file", "chksum_type": "sha256", @@ -3277,6 +3368,13 @@ "format": 1 }, { + "name": "plugins/modules/django_createcachetable.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "66f18cc86d806a4447888afd48e3e7d008933f0ee93b109c0fea62b5b8e1ec4a", + "format": 1 + }, + { "name": "plugins/modules/django_manage.py", "ftype": "file", "chksum_type": "sha256", @@ -3441,7 +3539,7 @@ "name": "plugins/modules/git_config.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3764f124422fcd3be0c60b955a6286da22e458fff5023c57ba73d0e90d3842a7", + "chksum_sha256": "92c164fb3a497d2d0887b0f239657c66d82f1ecd22cf2950a01c2f2b5b35e174", "format": 1 }, { @@ -3707,7 +3805,7 @@ "name": "plugins/modules/homectl.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f5af3533ea90dda0925e64715ea23b8aea4418020c0fb66115fa0595d452d749", + "chksum_sha256": "c8dac7831a418cc0385c4616484aefea40eddc91eef77f85404e58b3a6e47633", "format": 1 }, { @@ -4022,7 +4120,7 @@ "name": "plugins/modules/ipa_dnsrecord.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c8ae9aa977adb6ec60404eb8bf40adbaa7b1ce897e6707769d5e90230b40f16c", + "chksum_sha256": "2f3e8e8a7e64541c19873a1fb534afb8295f163d9c2204e6687031f68675ef31", "format": 1 }, { @@ -4351,7 +4449,7 @@ "name": "plugins/modules/keycloak_client.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "7870103accbfbd0204f1b7adeee0e20b9a6b8cddad0edda0bd31ec252572f186", + "chksum_sha256": "1200158abd819a25429fc2ad8c563623c117c668e4740dc9ffa68b22b1660abc", "format": 1 }, { @@ -4372,7 +4470,7 @@ "name": "plugins/modules/keycloak_clientscope.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c712e8e6d6b9d27297f7fbed371d7338aea6c92c6fc940fea1cf9e8c4b897c71", + "chksum_sha256": "3bf4a3c511dca811693ec895233193c633ead77585e3f3710844e7f3f6cb3510", "format": 1 }, { @@ -4505,7 +4603,7 @@ "name": "plugins/modules/launchd.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "239bdd1ceebfc51da46babbede85732efd5aca73719214225bd505c202cc4915", + "chksum_sha256": "0008d09fc7fb13a3bd959dbbc7f0089a22a8d6c5720fdf0fb69acee361cddd62", "format": 1 }, { @@ -5233,7 +5331,7 @@ "name": "plugins/modules/openbsd_pkg.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "aa5f9a76a91826cfcf340da018cee24dec149b5d858e9161023cf94e5867ac09", + "chksum_sha256": "eea55d4a493ae779edcf4fa9d6d9d401c502b9cf0acc000c9c4c85fafe78215a", "format": 1 }, { @@ -5338,7 +5436,7 @@ "name": "plugins/modules/pacman.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3e92ff37a394cf3ee414ca9e55e928fae7d2875992630b8e80e7fad1c5d1273f", + "chksum_sha256": "77db65d60288cc9d30ab7fa148a1f94f969d61d6f5c23a34bf76fda8306fd478", "format": 1 }, { @@ -5597,7 +5695,7 @@ "name": "plugins/modules/proxmox_kvm.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "030cd0ca64eb1eac59e15bd62ce58239e6466b01391a6abc314feff1c8a32614", + "chksum_sha256": "afdba49a542d59df6afba627eb4a3ca187f9033070193b1db599667427102cfc", "format": 1 }, { @@ -5674,7 +5772,7 @@ "name": "plugins/modules/proxmox_vm_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9bb1491ad5da16bd483f659726e5d99a29ac784a23f615244165ad4d9bde0120", + "chksum_sha256": "84b5e54194e9b6f67c49b01b5e20b795bd67f3cd82adcece514d8256827bd2d0", "format": 1 }, { @@ -5730,7 +5828,7 @@ "name": "plugins/modules/redfish_command.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3a06abd918e13f440dcd7bd66a1eae9d77a414aa80dc231eba9ddad2c3fc16d7", + "chksum_sha256": "c3beab5266697dca3d00e13de6760bbcb8ca48dc39d1425ab1da361ba39eb051", "format": 1 }, { @@ -5744,7 +5842,7 @@ "name": "plugins/modules/redfish_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3f14d5645eb692b829ab83dac5e38eac9ea9fc7bae38f87ffd532ad2e8d49de5", + "chksum_sha256": "7a423fb3fd03d467af475d4823658c2ff3cba2f2c2a72ac63be4bca9292e5466", "format": 1 }, { @@ -5786,7 +5884,7 @@ "name": "plugins/modules/redis_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5c544a02b0415efa329c84abc4119e0039991bfadcbdae32d14d484c7443e47a", + "chksum_sha256": "ace8770d46241e343ba03a55a42f56d674acc9f88e4f12e06fe130c81bb1da41", "format": 1 }, { @@ -6444,7 +6542,7 @@ "name": "plugins/modules/udm_user.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "108d20d909305308b8e5e7a6bf5e323f154db4c473c02e47734e8fb0c8bc7b07", + "chksum_sha256": "b061e35e6e8ea1970710888fe431a17dd3739b40e59e6342846e50dbdd9154ef", "format": 1 }, { @@ -6798,6 +6896,13 @@ "format": 1 }, { + "name": "plugins/plugin_utils/keys_filter.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "950a97d3f8fc8d57b0495b08dc40be2ad47e4436e65c2ac1384a8b656c07c2b0", + "format": 1 + }, + { "name": "plugins/plugin_utils/unsafe.py", "ftype": "file", "chksum_type": "sha256", @@ -7130,7 +7235,7 @@ "name": "tests/integration/targets/ansible_galaxy_install/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "db8a8fd9d62a035862276fb8ce6bae558ccfe9ee6a03a498c561272236301307", + "chksum_sha256": "f6896e1d1f1b563728905f203d6623fcc9ced037e88132af50457c368d750fb8", "format": 1 }, { @@ -7662,7 +7767,7 @@ "name": "tests/integration/targets/cargo/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e5f66835ea6bd23ef85f61c95211e57f257d3f2ecab06d669afe311ba64045b3", + "chksum_sha256": "9370d274dfb0bec0b81f998ccc03441f09cbb731d9647e1810043a5857ff5c52", "format": 1 }, { @@ -7673,6 +7778,13 @@ "format": 1 }, { + "name": "tests/integration/targets/cargo/tasks/test_directory.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3aed8a73a3b83b455c377054f1198be385d5cfb5e055368be7222a70fc0f40a7", + "format": 1 + }, + { "name": "tests/integration/targets/cargo/tasks/test_general.yml", "ftype": "file", "chksum_type": "sha256", @@ -8051,6 +8163,20 @@ "format": 1 }, { + "name": "tests/integration/targets/consul/tasks/consul_agent_check.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac1e5b0cf2a5483ba484c99016b34975759a6a8060a503603146d7c9be552f75", + "format": 1 + }, + { + "name": "tests/integration/targets/consul/tasks/consul_agent_service.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4f7deacb2c709ea660887c954c8005acc6f8163958cc71e9a5f5cec547784d33", + "format": 1 + }, + { "name": "tests/integration/targets/consul/tasks/consul_auth_method.yml", "ftype": "file", "chksum_type": "sha256", @@ -8110,7 +8236,7 @@ "name": "tests/integration/targets/consul/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6bafc431979010557c06259d8b5cccc03f49d1ec2097a26118dd3a2091fd4cc7", + "chksum_sha256": "76dec6dcfe653795428e6eaa8f0eb4c8eccd6d04d7c3896b99362c386ac93ffc", "format": 1 }, { @@ -9297,6 +9423,55 @@ "format": 1 }, { + "name": "tests/integration/targets/filter_keep_keys", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ece426f8edfd07e80705b0925b97d70c0b020dc7ed6c2bd02dc7ed69834a85f", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "928ea7662292fac8633465e8405edf98c5e71c0c9de8b81d6cf1b33e7aabfe4c", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da6753430b6dea343d2a605d0a7af54e6ac66640bdca97159417c54c75696be0", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_keep_keys/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "96646c5ab7118c53b8b722f6abe91a7ed4d3eed9be401faefb399d6a8f427c2e", + "format": 1 + }, + { "name": "tests/integration/targets/filter_lists", "ftype": "dir", "chksum_type": null, @@ -9465,6 +9640,111 @@ "format": 1 }, { + "name": "tests/integration/targets/filter_remove_keys", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3815b9d04a798cc01c090d0494ca27544bec4a4a08bd1995fa8ff586e1201c40", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/tasks/remove_keys.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "430024a06df2b362cd3d5d658102a8389ddacc780613dde834d95c01cd5cff21", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "19b18860ad324c02cfb996c048e2516e53e7d6b84f13059abeff602e9802120c", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_remove_keys/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "96646c5ab7118c53b8b722f6abe91a7ed4d3eed9be401faefb399d6a8f427c2e", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/tasks/fn-test-replace_keys.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b62bfcb4f9b61bc818b033993b111eb88196dbd8ddf6fa0942f93badf3274338", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1efdb33269dba21292e7b4525611ca41360277f60aaed0d476d3115b6b683c74", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/tasks/replace_keys.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4f92e48603760a77ca302371847e004bec26b1599f863fe7719d6bcfa37d8e33", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/vars/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "12fb80e1cc0fed908d3bb9ae1bb3b1e1bbebeb7f2e259476f16cf4a75173cbc6", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_replace_keys/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "96646c5ab7118c53b8b722f6abe91a7ed4d3eed9be401faefb399d6a8f427c2e", + "format": 1 + }, + { "name": "tests/integration/targets/filter_time", "ftype": "dir", "chksum_type": null, @@ -10028,7 +10308,7 @@ "name": "tests/integration/targets/git_config/tasks/unset_value.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e31339e0f76aecbacc8839d3755b7fd21fa1882a8189d5cc79890e45d65e559c", + "chksum_sha256": "a8836b380c222836aea5c2aee38187580c031cbb7aec8d70fa55588f7a162f75", "format": 1 }, { @@ -13010,7 +13290,7 @@ "name": "tests/integration/targets/keycloak_client/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "10f83e1b6754b9dbc932e5bc5e6dfd85d028dffb2f85c5f9023f197211ea85bc", + "chksum_sha256": "dac1226601e47b2dc8127eca264d249e57054828048d590f6e9a260ef9b675ec", "format": 1 }, { @@ -23237,7 +23517,7 @@ "name": "tests/unit/plugins/module_utils/test_cmd_runner.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5e44ed79ba57b534227340b6e97849f84d20811597aaed62aa697d09ab6243f6", + "chksum_sha256": "98b8cc91992ff261d7bddcafc08f106a0ff38a17d6f45240f3387c4c019eecff", "format": 1 }, { @@ -26538,6 +26818,20 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_django_check.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71fb52b56b5957aa54aebeb786ff74f396902effb388a202a86627e0cf0256d5", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_django_check.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7f3e625f4f6aec1e810913ecd137e8340c8d3c1f97dc2dc81bffa0c1600eed1d", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_django_command.py", "ftype": "file", "chksum_type": "sha256", @@ -26552,6 +26846,20 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_django_createcachetable.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b12c8d85f2466e732a6f05e80485a6105d26fdd5c67477e4e46031c43f46086b", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_django_createcachetable.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4eaa1b63defefb1bf8e4be46b98ce23ac97c49a01b06225bf1da803b3ebcce63", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_dnf_config_manager.py", "ftype": "file", "chksum_type": "sha256", @@ -27654,7 +27962,7 @@ "name": "CHANGELOG.md", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2fc9c5d7b462624c77da1320ae1acaa13e7e4f765f0fe3393278146104281693", + "chksum_sha256": "d4edd4ccef5e7c4b176e19e06f7deb76de1ce252c72ad839644ee37f0a181476", "format": 1 }, { @@ -27668,7 +27976,7 @@ "name": "CHANGELOG.rst", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "28d86e5061f86ee8a49776681a2f660ee38c29d057b9c23c6d2ab473c0f218a5", + "chksum_sha256": "781bf561f0bec0521238cbf4b7481f2887d01fef494ed6ff450fae4daa870616", "format": 1 }, { diff --git a/ansible_collections/community/general/MANIFEST.json b/ansible_collections/community/general/MANIFEST.json index ffbae1e0e..26506ad50 100644 --- a/ansible_collections/community/general/MANIFEST.json +++ b/ansible_collections/community/general/MANIFEST.json @@ -2,7 +2,7 @@ "collection_info": { "namespace": "community", "name": "general", - "version": "9.0.1", + "version": "9.1.0", "authors": [ "Ansible (https://github.com/ansible)" ], @@ -23,7 +23,7 @@ "name": "FILES.json", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "be2f78923401a69f1bb6105804257106a4092634b2fa6a02058fea1e14de1bff", + "chksum_sha256": "98bfdff0974e81d3a63af8d9b29886515483d7536197a6f78e3c706272aba8f8", "format": 1 }, "format": 1 diff --git a/ansible_collections/community/general/changelogs/changelog.yaml b/ansible_collections/community/general/changelogs/changelog.yaml index 6215a89ce..609989343 100644 --- a/ansible_collections/community/general/changelogs/changelog.yaml +++ b/ansible_collections/community/general/changelogs/changelog.yaml @@ -794,3 +794,111 @@ releases: - 8413-galaxy-refactor.yml - 9.0.1.yml release_date: '2024-05-27' + 9.1.0: + changes: + bugfixes: + - git_config - fix behavior of ``state=absent`` if ``value`` is present (https://github.com/ansible-collections/community.general/issues/8436, + https://github.com/ansible-collections/community.general/pull/8452). + - keycloak_realm - add normalizations for ``attributes`` and ``protocol_mappers`` + (https://github.com/ansible-collections/community.general/pull/8496). + - launched - correctly report changed status in check mode (https://github.com/ansible-collections/community.general/pull/8406). + - opennebula inventory plugin - fix invalid reference to IP when inventory runs + against NICs with no IPv4 address (https://github.com/ansible-collections/community.general/pull/8489). + - opentelemetry callback - do not save the JSON response when using the ``ansible.builtin.uri`` + module (https://github.com/ansible-collections/community.general/pull/8430). + - opentelemetry callback - do not save the content response when using the ``ansible.builtin.slurp`` + module (https://github.com/ansible-collections/community.general/pull/8430). + - paman - do not fail if an empty list of packages has been provided and there + is nothing to do (https://github.com/ansible-collections/community.general/pull/8514). + deprecated_features: + - CmdRunner module util - setting the value of the ``ignore_none`` parameter + within a ``CmdRunner`` context is deprecated and that feature should be removed + in community.general 12.0.0 (https://github.com/ansible-collections/community.general/pull/8479). + - git_config - the ``list_all`` option has been deprecated and will be removed + in community.general 11.0.0. Use the ``community.general.git_config_info`` + module instead (https://github.com/ansible-collections/community.general/pull/8453). + - git_config - using ``state=present`` without providing ``value`` is deprecated + and will be disallowed in community.general 11.0.0. Use the ``community.general.git_config_info`` + module instead to read a value (https://github.com/ansible-collections/community.general/pull/8453). + known_issues: + - homectl - the module does not work under Python 3.13 or newer, since it relies + on the removed ``crypt`` standard library module (https://github.com/ansible-collections/community.general/issues/4691, + https://github.com/ansible-collections/community.general/pull/8497). + - udm_user - the module does not work under Python 3.13 or newer, since it relies + on the removed ``crypt`` standard library module (https://github.com/ansible-collections/community.general/issues/4690, + https://github.com/ansible-collections/community.general/pull/8497). + minor_changes: + - CmdRunner module util - argument formats can be specified as plain functions + without calling ``cmd_runner_fmt.as_func()`` (https://github.com/ansible-collections/community.general/pull/8479). + - ansible_galaxy_install - add upgrade feature (https://github.com/ansible-collections/community.general/pull/8431, + https://github.com/ansible-collections/community.general/issues/8351). + - cargo - add option ``directory``, which allows source directory to be specified + (https://github.com/ansible-collections/community.general/pull/8480). + - cmd_runner module utils - add decorator ``cmd_runner_fmt.stack`` (https://github.com/ansible-collections/community.general/pull/8415). + - cmd_runner_fmt module utils - simplify implementation of ``cmd_runner_fmt.as_bool_not()`` + (https://github.com/ansible-collections/community.general/pull/8512). + - ipa_dnsrecord - adds ``SSHFP`` record type for managing SSH fingerprints in + FreeIPA DNS (https://github.com/ansible-collections/community.general/pull/8404). + - keycloak_client - assign auth flow by name (https://github.com/ansible-collections/community.general/pull/8428). + - openbsd_pkg - adds diff support to show changes in installed package list. + This does not yet work for check mode (https://github.com/ansible-collections/community.general/pull/8402). + - proxmox - allow specification of the API port when using proxmox_* (https://github.com/ansible-collections/community.general/issues/8440, + https://github.com/ansible-collections/community.general/pull/8441). + - proxmox_vm_info - add ``network`` option to retrieve current network information + (https://github.com/ansible-collections/community.general/pull/8471). + - redfish_command - add ``wait`` and ``wait_timeout`` options to allow a user + to block a command until a service is accessible after performing the requested + command (https://github.com/ansible-collections/community.general/issues/8051, + https://github.com/ansible-collections/community.general/pull/8434). + - redfish_info - add command ``CheckAvailability`` to check if a service is + accessible (https://github.com/ansible-collections/community.general/issues/8051, + https://github.com/ansible-collections/community.general/pull/8434). + - redis_info - adds support for getting cluster info (https://github.com/ansible-collections/community.general/pull/8464). + release_summary: Regular feature and bugfix release. + fragments: + - 8051-Redfish-Wait-For-Service.yml + - 8402-add-diif-mode-openbsd-pkg.yml + - 8404-ipa_dnsrecord_sshfp.yml + - 8415-cmd-runner-stack.yml + - 8428-assign-auth-flow-by-name-keycloak-client.yaml + - 8430-fix-opentelemetry-when-using-logs-with-uri-or-slurp-tasks.yaml + - 8431-galaxy-upgrade.yml + - 8440-allow-api-port-specification.yaml + - 8452-git_config-absent.yml + - 8453-git_config-deprecate-read.yml + - 8464-redis-add-cluster-info.yml + - 8471-proxmox-vm-info-network.yml + - 8476-launchd-check-mode-changed.yaml + - 8479-cmdrunner-improvements.yml + - 8480-directory-feature-cargo.yml + - 8489-fix-opennebula-inventory-crash-when-nic-has-no-ip.yml + - 8496-keycloak_clientscope-add-normalizations.yaml + - 8497-crypt.yml + - 8512-as-bool-not.yml + - 8514-pacman-empty.yml + - 9.1.0.yml + modules: + - description: Add, modify, and delete checks within a consul cluster. + name: consul_agent_check + namespace: '' + - description: Add, modify and delete services within a consul cluster. + name: consul_agent_service + namespace: '' + - description: Wrapper for C(django-admin check). + name: django_check + namespace: '' + - description: Wrapper for C(django-admin createcachetable). + name: django_createcachetable + namespace: '' + plugins: + filter: + - description: Keep specific keys from dictionaries in a list. + name: keep_keys + namespace: null + - description: Remove specific keys from dictionaries in a list. + name: remove_keys + namespace: null + - description: Replace specific keys in a list of dictionaries. + name: replace_keys + namespace: null + release_date: '2024-06-17' diff --git a/ansible_collections/community/general/docs/docsite/extra-docs.yml b/ansible_collections/community/general/docs/docsite/extra-docs.yml index 529573606..3bed9e35f 100644 --- a/ansible_collections/community/general/docs/docsite/extra-docs.yml +++ b/ansible_collections/community/general/docs/docsite/extra-docs.yml @@ -14,3 +14,7 @@ sections: - guide_online - guide_packet - guide_scaleway + - title: Developer Guides + toctree: + - guide_deps + - guide_vardict diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-common.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-common.yml index fd874e5c9..4431fe27d 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-common.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-common.yml @@ -2,17 +2,11 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - - name: foo - extra: true - - name: bar - extra: false - - name: meh - extra: true + - {name: foo, extra: true} + - {name: bar, extra: false} + - {name: meh, extra: true} list2: - - name: foo - path: /foo - - name: baz - path: /baz + - {name: foo, path: /foo} + - {name: baz, path: /baz} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001.yml index 0cf6a9b8a..c27b019e5 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001.yml @@ -8,7 +8,7 @@ dir: example-001_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-001.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/default-common.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/default-common.yml index fd874e5c9..4431fe27d 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/default-common.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/default-common.yml @@ -2,17 +2,11 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - - name: foo - extra: true - - name: bar - extra: false - - name: meh - extra: true + - {name: foo, extra: true} + - {name: bar, extra: false} + - {name: meh, extra: true} list2: - - name: foo - path: /foo - - name: baz - path: /baz + - {name: foo, path: /foo} + - {name: baz, path: /baz} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/list3.yml index 0604feccb..8bd8bc8f2 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-001_vars/list3.yml @@ -2,6 +2,5 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ list1| +list3: "{{ list1 | community.general.lists_mergeby(list2, 'name') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002.yml index 5e6e0315d..e164db125 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002.yml @@ -8,7 +8,7 @@ dir: example-002_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-002.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/default-common.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/default-common.yml index fd874e5c9..4431fe27d 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/default-common.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/default-common.yml @@ -2,17 +2,11 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - - name: foo - extra: true - - name: bar - extra: false - - name: meh - extra: true + - {name: foo, extra: true} + - {name: bar, extra: false} + - {name: meh, extra: true} list2: - - name: foo - path: /foo - - name: baz - path: /baz + - {name: foo, path: /foo} + - {name: baz, path: /baz} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/list3.yml index 8ad752407..be6cfcbf3 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-002_vars/list3.yml @@ -2,6 +2,5 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003.yml index 2f93ab8a2..cbc5e43a5 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003.yml @@ -8,7 +8,7 @@ dir: example-003_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-003.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/list3.yml index d5374eece..2eff5df41 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-003_vars/list3.yml @@ -2,7 +2,6 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true) }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004.yml index 3ef067faf..68e77dea8 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004.yml @@ -8,7 +8,7 @@ dir: example-004_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-004.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/list3.yml index a054ea1e7..94c8ceed3 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-004_vars/list3.yml @@ -2,8 +2,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='keep') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005.yml index 57e7a779d..b7b81de29 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005.yml @@ -8,7 +8,7 @@ dir: example-005_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-005.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/list3.yml index 3480bf658..f0d7751f2 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-005_vars/list3.yml @@ -2,8 +2,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='append') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006.yml index 41fc88e49..1be3becbc 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006.yml @@ -8,7 +8,7 @@ dir: example-006_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-006.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/list3.yml index 97513b559..f555c8dcb 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-006_vars/list3.yml @@ -2,8 +2,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='prepend') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007.yml index 3de715844..8a596ea68 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007.yml @@ -8,7 +8,7 @@ dir: example-007_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug|d(false) | bool - template: src: list3.out.j2 dest: example-007.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/list3.yml index cb51653b4..d8ad16cf4 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-007_vars/list3.yml @@ -2,8 +2,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='append_rp') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008.yml index e33828bf9..6d5c03bc6 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008.yml @@ -8,7 +8,7 @@ dir: example-008_vars - debug: var: list3 - when: debug|d(false)|bool + when: debug | d(false) | bool - template: src: list3.out.j2 dest: example-008.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/default-recursive-true.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/default-recursive-true.yml index 133c8f2ae..eb83ea82e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/default-recursive-true.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/default-recursive-true.yml @@ -2,14 +2,12 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - list1: - name: myname01 param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -18,7 +16,6 @@ list2: param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/list3.yml index af7001fc4..b2051376e 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/list3.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-008_vars/list3.yml @@ -2,8 +2,7 @@ # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later - -list3: "{{ [list1, list2]| +list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='prepend_rp') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009.yml new file mode 100644 index 000000000..beef5d356 --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009.yml @@ -0,0 +1,14 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: 9. Merge single list by common attribute 'name' + include_vars: + dir: example-009_vars +- debug: + var: list3 + when: debug | d(false) | bool +- template: + src: list3.out.j2 + dest: example-009.out diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/default-common.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/default-common.yml new file mode 100644 index 000000000..4431fe27d --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/default-common.yml @@ -0,0 +1,12 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +list1: + - {name: foo, extra: true} + - {name: bar, extra: false} + - {name: meh, extra: true} + +list2: + - {name: foo, path: /foo} + - {name: baz, path: /baz} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/list3.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/list3.yml new file mode 100644 index 000000000..1708e3baf --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/example-009_vars/list3.yml @@ -0,0 +1,6 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +list3: "{{ [list1 + list2, []] | + community.general.lists_mergeby('name') }}" diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples.yml index 83b985084..34ad2d155 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples.yml @@ -4,51 +4,75 @@ # SPDX-License-Identifier: GPL-3.0-or-later examples: - - label: 'In the example below the lists are merged by the attribute ``name``:' + - title: Two lists + description: 'In the example below the lists are merged by the attribute ``name``:' file: example-001_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-001.out lang: 'yaml' - - label: 'It is possible to use a list of lists as an input of the filter:' + - title: List of two lists + description: 'It is possible to use a list of lists as an input of the filter:' file: example-002_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces the same result as in the previous example:' + - title: + description: 'This produces the same result as in the previous example:' file: example-002.out lang: 'yaml' - - label: 'Example ``list_merge=replace`` (default):' + - title: Single list + description: 'It is possible to merge single list:' + file: example-009_vars/list3.yml + lang: 'yaml+jinja' + - title: + description: 'This produces the same result as in the previous example:' + file: example-009.out + lang: 'yaml' + - title: list_merge=replace (default) + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=replace` (default):' file: example-003_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-003.out lang: 'yaml' - - label: 'Example ``list_merge=keep``:' + - title: list_merge=keep + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=keep`:' file: example-004_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-004.out lang: 'yaml' - - label: 'Example ``list_merge=append``:' + - title: list_merge=append + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append`:' file: example-005_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-005.out lang: 'yaml' - - label: 'Example ``list_merge=prepend``:' + - title: list_merge=prepend + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend`:' file: example-006_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-006.out lang: 'yaml' - - label: 'Example ``list_merge=append_rp``:' + - title: list_merge=append_rp + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append_rp`:' file: example-007_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-007.out lang: 'yaml' - - label: 'Example ``list_merge=prepend_rp``:' + - title: list_merge=prepend_rp + description: 'Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend_rp`:' file: example-008_vars/list3.yml lang: 'yaml+jinja' - - label: 'This produces:' + - title: + description: 'This produces:' file: example-008.out lang: 'yaml' diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples_all.rst.j2 b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples_all.rst.j2 index 95a0fafdd..88098683b 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples_all.rst.j2 +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/examples_all.rst.j2 @@ -4,10 +4,10 @@ SPDX-License-Identifier: GPL-3.0-or-later {% for i in examples %} -{{ i.label }} +{{ i.description }} .. code-block:: {{ i.lang }} - {{ lookup('file', i.file)|indent(2) }} + {{ lookup('file', i.file) | split('\n') | reject('match', '^(#|---)') | join ('\n') | indent(2) }} {% endfor %} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/extra-vars.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/extra-vars.yml new file mode 100644 index 000000000..0482c7ff2 --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/extra-vars.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +examples_one: true +examples_all: true +merging_lists_of_dictionaries: true diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2 b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2 index 71d0d5da6..ad74161dc 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2 +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2 @@ -6,57 +6,69 @@ Merging lists of dictionaries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the ``lists_mergeby`` filter. +If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the :ansplugin:`community.general.lists_mergeby <community.general.lists_mergeby#filter>` filter. -.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`. +.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See the documentation for the :ansplugin:`community.general.yaml callback plugin <community.general.yaml#callback>`. Let us use the lists below in the following examples: .. code-block:: yaml - {{ lookup('file', 'default-common.yml')|indent(2) }} + {{ lookup('file', 'default-common.yml') | split('\n') | reject('match', '^(#|---)') | join ('\n') | indent(2) }} {% for i in examples[0:2] %} -{{ i.label }} +{% if i.title | d('', true) | length > 0 %} +{{ i.title }} +{{ "%s" % ('"' * i.title|length) }} +{% endif %} +{{ i.description }} .. code-block:: {{ i.lang }} - {{ lookup('file', i.file)|indent(2) }} + {{ lookup('file', i.file) | split('\n') | reject('match', '^(#|---)') | join ('\n') | indent(2) }} {% endfor %} .. versionadded:: 2.0.0 -{% for i in examples[2:4] %} -{{ i.label }} +{% for i in examples[2:6] %} +{% if i.title | d('', true) | length > 0 %} +{{ i.title }} +{{ "%s" % ('"' * i.title|length) }} +{% endif %} +{{ i.description }} .. code-block:: {{ i.lang }} - {{ lookup('file', i.file)|indent(2) }} + {{ lookup('file', i.file) | split('\n') | reject('match', '^(#|---)') | join ('\n') | indent(2) }} {% endfor %} -The filter also accepts two optional parameters: ``recursive`` and ``list_merge``. These parameters are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9. This is available since community.general 4.4.0. +The filter also accepts two optional parameters: :ansopt:`community.general.lists_mergeby#filter:recursive` and :ansopt:`community.general.lists_mergeby#filter:list_merge`. This is available since community.general 4.4.0. **recursive** - Is a boolean, default to ``False``. Should the ``community.general.lists_mergeby`` recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``. + Is a boolean, default to ``false``. Should the :ansplugin:`community.general.lists_mergeby#filter` filter recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``. **list_merge** - Is a string, its possible values are ``replace`` (default), ``keep``, ``append``, ``prepend``, ``append_rp`` or ``prepend_rp``. It modifies the behaviour of ``community.general.lists_mergeby`` when the hashes to merge contain arrays/lists. + Is a string, its possible values are :ansval:`replace` (default), :ansval:`keep`, :ansval:`append`, :ansval:`prepend`, :ansval:`append_rp` or :ansval:`prepend_rp`. It modifies the behaviour of :ansplugin:`community.general.lists_mergeby#filter` when the hashes to merge contain arrays/lists. -The examples below set ``recursive=true`` and display the differences among all six options of ``list_merge``. Functionality of the parameters is exactly the same as in the filter ``combine``. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options. +The examples below set :ansopt:`community.general.lists_mergeby#filter:recursive=true` and display the differences among all six options of :ansopt:`community.general.lists_mergeby#filter:list_merge`. Functionality of the parameters is exactly the same as in the filter :ansplugin:`ansible.builtin.combine#filter`. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options. Let us use the lists below in the following examples .. code-block:: yaml - {{ lookup('file', 'default-recursive-true.yml')|indent(2) }} + {{ lookup('file', 'default-recursive-true.yml') | split('\n') | reject('match', '^(#|---)') | join ('\n') |indent(2) }} -{% for i in examples[4:16] %} -{{ i.label }} +{% for i in examples[6:] %} +{% if i.title | d('', true) | length > 0 %} +{{ i.title }} +{{ "%s" % ('"' * i.title|length) }} +{% endif %} +{{ i.description }} .. code-block:: {{ i.lang }} - {{ lookup('file', i.file)|indent(2) }} + {{ lookup('file', i.file) | split('\n') | reject('match', '^(#|---)') | join ('\n') |indent(2) }} {% endfor %} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/list3.out.j2 b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/list3.out.j2 index b51f6b868..a30a5c4ab 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/list3.out.j2 +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/list3.out.j2 @@ -4,4 +4,4 @@ GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://w SPDX-License-Identifier: GPL-3.0-or-later #} list3: -{{ list3|to_nice_yaml(indent=0) }} + {{ list3 | to_yaml(indent=2, sort_keys=false) | indent(2) }} diff --git a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/playbook.yml b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/playbook.yml index 793d23348..ab389fa12 100644 --- a/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/playbook.yml +++ b/ansible_collections/community/general/docs/docsite/helper/lists_mergeby/playbook.yml @@ -5,7 +5,7 @@ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # 1) Run all examples and create example-XXX.out -# shell> ansible-playbook playbook.yml -e examples=true +# shell> ansible-playbook playbook.yml -e examples_one=true # # 2) Optionally, for testing, create examples_all.rst # shell> ansible-playbook playbook.yml -e examples_all=true @@ -45,18 +45,20 @@ tags: t007 - import_tasks: example-008.yml tags: t008 - when: examples|d(false)|bool + - import_tasks: example-009.yml + tags: t009 + when: examples_one | d(false) | bool - block: - include_vars: examples.yml - template: src: examples_all.rst.j2 dest: examples_all.rst - when: examples_all|d(false)|bool + when: examples_all | d(false) | bool - block: - include_vars: examples.yml - template: src: filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2 dest: filter_guide_abstract_informations_merging_lists_of_dictionaries.rst - when: merging_lists_of_dictionaries|d(false)|bool + when: merging_lists_of_dictionaries | d(false) | bool diff --git a/ansible_collections/community/general/docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst b/ansible_collections/community/general/docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst index 06fa79d16..cafe04e5c 100644 --- a/ansible_collections/community/general/docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst +++ b/ansible_collections/community/general/docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst @@ -6,33 +6,30 @@ Merging lists of dictionaries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the :ansplugin:`community.general.lists_mergeby filter <community.general.lists_mergeby#filter>`. +If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the :ansplugin:`community.general.lists_mergeby <community.general.lists_mergeby#filter>` filter. -.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`. +.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See the documentation for the :ansplugin:`community.general.yaml callback plugin <community.general.yaml#callback>`. Let us use the lists below in the following examples: .. code-block:: yaml list1: - - name: foo - extra: true - - name: bar - extra: false - - name: meh - extra: true + - {name: foo, extra: true} + - {name: bar, extra: false} + - {name: meh, extra: true} list2: - - name: foo - path: /foo - - name: baz - path: /baz + - {name: foo, path: /foo} + - {name: baz, path: /baz} +Two lists +""""""""" In the example below the lists are merged by the attribute ``name``: .. code-block:: yaml+jinja - list3: "{{ list1| + list3: "{{ list1 | community.general.lists_mergeby(list2, 'name') }}" This produces: @@ -40,24 +37,21 @@ This produces: .. code-block:: yaml list3: - - extra: false - name: bar - - name: baz - path: /baz - - extra: true - name: foo - path: /foo - - extra: true - name: meh + - {name: bar, extra: false} + - {name: baz, path: /baz} + - {name: foo, extra: true, path: /foo} + - {name: meh, extra: true} .. versionadded:: 2.0.0 +List of two lists +""""""""""""""""" It is possible to use a list of lists as an input of the filter: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name') }}" This produces the same result as in the previous example: @@ -65,15 +59,29 @@ This produces the same result as in the previous example: .. code-block:: yaml list3: - - extra: false - name: bar - - name: baz - path: /baz - - extra: true - name: foo - path: /foo - - extra: true - name: meh + - {name: bar, extra: false} + - {name: baz, path: /baz} + - {name: foo, extra: true, path: /foo} + - {name: meh, extra: true} + +Single list +""""""""""" +It is possible to merge single list: + +.. code-block:: yaml+jinja + + list3: "{{ [list1 + list2, []] | + community.general.lists_mergeby('name') }}" + +This produces the same result as in the previous example: + +.. code-block:: yaml + + list3: + - {name: bar, extra: false} + - {name: baz, path: /baz} + - {name: foo, extra: true, path: /foo} + - {name: meh, extra: true} The filter also accepts two optional parameters: :ansopt:`community.general.lists_mergeby#filter:recursive` and :ansopt:`community.general.lists_mergeby#filter:list_merge`. This is available since community.general 4.4.0. @@ -95,8 +103,7 @@ Let us use the lists below in the following examples param01: x: default_value y: default_value - list: - - default_value + list: [default_value] - name: myname02 param01: [1, 1, 2, 3] @@ -105,16 +112,17 @@ Let us use the lists below in the following examples param01: y: patch_value z: patch_value - list: - - patch_value + list: [patch_value] - name: myname02 - param01: [3, 4, 4, {key: value}] + param01: [3, 4, 4] +list_merge=replace (default) +"""""""""""""""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=replace` (default): .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true) }}" @@ -123,25 +131,22 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - patch_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 3 - - 4 - - 4 - - key: value + - name: myname01 + param01: + x: default_value + y: patch_value + list: [patch_value] + z: patch_value + - name: myname02 + param01: [3, 4, 4] +list_merge=keep +""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=keep`: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='keep') }}" @@ -151,25 +156,22 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - default_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 1 - - 1 - - 2 - - 3 + - name: myname01 + param01: + x: default_value + y: patch_value + list: [default_value] + z: patch_value + - name: myname02 + param01: [1, 1, 2, 3] +list_merge=append +""""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append`: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='append') }}" @@ -179,30 +181,22 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - default_value - - patch_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 1 - - 1 - - 2 - - 3 - - 3 - - 4 - - 4 - - key: value + - name: myname01 + param01: + x: default_value + y: patch_value + list: [default_value, patch_value] + z: patch_value + - name: myname02 + param01: [1, 1, 2, 3, 3, 4, 4] +list_merge=prepend +"""""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend`: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='prepend') }}" @@ -212,30 +206,22 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - patch_value - - default_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 3 - - 4 - - 4 - - key: value - - 1 - - 1 - - 2 - - 3 + - name: myname01 + param01: + x: default_value + y: patch_value + list: [patch_value, default_value] + z: patch_value + - name: myname02 + param01: [3, 4, 4, 1, 1, 2, 3] +list_merge=append_rp +"""""""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append_rp`: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='append_rp') }}" @@ -245,29 +231,22 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - default_value - - patch_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 1 - - 1 - - 2 - - 3 - - 4 - - 4 - - key: value + - name: myname01 + param01: + x: default_value + y: patch_value + list: [default_value, patch_value] + z: patch_value + - name: myname02 + param01: [1, 1, 2, 3, 4, 4] +list_merge=prepend_rp +""""""""""""""""""""" Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend_rp`: .. code-block:: yaml+jinja - list3: "{{ [list1, list2]| + list3: "{{ [list1, list2] | community.general.lists_mergeby('name', recursive=true, list_merge='prepend_rp') }}" @@ -277,21 +256,12 @@ This produces: .. code-block:: yaml list3: - - name: myname01 - param01: - list: - - patch_value - - default_value - x: default_value - y: patch_value - z: patch_value - - name: myname02 - param01: - - 3 - - 4 - - 4 - - key: value - - 1 - - 1 - - 2 + - name: myname01 + param01: + x: default_value + y: patch_value + list: [patch_value, default_value] + z: patch_value + - name: myname02 + param01: [3, 4, 4, 1, 1, 2] diff --git a/ansible_collections/community/general/docs/docsite/rst/guide_deps.rst b/ansible_collections/community/general/docs/docsite/rst/guide_deps.rst new file mode 100644 index 000000000..4c0c4687a --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/rst/guide_deps.rst @@ -0,0 +1,74 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + +.. _ansible_collections.community.general.docsite.guide_deps: + +``deps`` Guide +============== + + +Using ``deps`` +^^^^^^^^^^^^^^ + +The ``ansible_collections.community.general.plugins.module_utils.deps`` module util simplifies +the importing of code as described in :ref:`Importing and using shared code <shared_code>`. +Please notice that ``deps`` is meant to be used specifically with Ansible modules, and not other types of plugins. + +The same example from the Developer Guide would become: + +.. code-block:: python + + from ansible_collections.community.general.plugins.module_utils import deps + + with deps.declare("foo"): + import foo + +Then in ``main()``, just after the argspec (or anywhere in the code, for that matter), do + +.. code-block:: python + + deps.validate(module) # assuming module is a valid AnsibleModule instance + +By default, ``deps`` will rely on ``ansible.module_utils.basic.missing_required_lib`` to generate +a message about a failing import. That function accepts parameters ``reason`` and ``url``, and +and so does ``deps```: + +.. code-block:: python + + with deps.declare("foo", reason="foo is needed to properly bar", url="https://foo.bar.io"): + import foo + +If you would rather write a custom message instead of using ``missing_required_lib`` then do: + +.. code-block:: python + + with deps.declare("foo", msg="Custom msg explaining why foo is needed"): + import foo + +``deps`` allows for multiple dependencies to be declared: + +.. code-block:: python + + with deps.declare("foo"): + import foo + + with deps.declare("bar"): + import bar + + with deps.declare("doe"): + import doe + +By default, ``deps.validate()`` will check on all the declared dependencies, but if so desired, +they can be validated selectively by doing: + +.. code-block:: python + + deps.validate(module, "foo") # only validates the "foo" dependency + + deps.validate(module, "doe:bar") # only validates the "doe" and "bar" dependencies + + deps.validate(module, "-doe:bar") # validates all dependencies except "doe" and "bar" + +.. versionadded:: 6.1.0 diff --git a/ansible_collections/community/general/docs/docsite/rst/guide_vardict.rst b/ansible_collections/community/general/docs/docsite/rst/guide_vardict.rst new file mode 100644 index 000000000..f65b09055 --- /dev/null +++ b/ansible_collections/community/general/docs/docsite/rst/guide_vardict.rst @@ -0,0 +1,176 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + +.. _ansible_collections.community.general.docsite.guide_vardict: + +VarDict Guide +============= + +Introduction +^^^^^^^^^^^^ + +The ``ansible_collections.community.general.plugins.module_utils.vardict`` module util provides the +``VarDict`` class to help manage the module variables. That class is a container for module variables, +especially the ones for which the module must keep track of state changes, and the ones that should +be published as return values. + +Each variable has extra behaviors controlled by associated metadata, simplifying the generation of +output values from the module. + +Quickstart +"""""""""" + +The simplest way of using ``VarDict`` is: + +.. code-block:: python + + from ansible_collections.community.general.plugins.module_utils.vardict import VarDict + +Then in ``main()``, or any other function called from there: + +.. code-block:: python + + vars = VarDict() + + # Next 3 statements are equivalent + vars.abc = 123 + vars["abc"] = 123 + vars.set("abc", 123) + + vars.xyz = "bananas" + vars.ghi = False + +And by the time the module is about to exit: + +.. code-block:: python + + results = vars.output() + module.exit_json(**results) + +That makes the return value of the module: + +.. code-block:: javascript + + { + "abc": 123, + "xyz": "bananas", + "ghi": false + } + +Metadata +"""""""" + +The metadata values associated with each variable are: + +- ``output: bool`` - marks the variable for module output as a module return value. +- ``fact: bool`` - marks the variable for module output as an Ansible fact. +- ``verbosity: int`` - sets the minimum level of verbosity for which the variable will be included in the output. +- ``change: bool`` - controls the detection of changes in the variable value. +- ``initial_value: any`` - when using ``change`` and need to forcefully set an intial value to the variable. +- ``diff: bool`` - used along with ``change``, this generates an Ansible-style diff ``dict``. + +See the sections below for more details on how to use the metadata. + + +Using VarDict +^^^^^^^^^^^^^ + +Basic Usage +""""""""""" + +As shown above, variables can be accessed using the ``[]`` operator, as in a ``dict`` object, +and also as an object attribute, such as ``vars.abc``. The form using the ``set()`` +method is special in the sense that you can use it to set metadata values: + +.. code-block:: python + + vars.set("abc", 123, output=False) + vars.set("abc", 123, output=True, change=True) + +Another way to set metadata after the variables have been created is: + +.. code-block:: python + + vars.set_meta("abc", output=False) + vars.set_meta("abc", output=True, change=True, diff=True) + +You can use either operator and attribute forms to access the value of the variable. Other ways to +access its value and its metadata are: + +.. code-block:: python + + print("abc value = {0}".format(vars.var("abc")["value"])) # get the value + print("abc output? {0}".format(vars.get_meta("abc")["output"])) # get the metadata like this + +The names of methods, such as ``set``, ``get_meta``, ``output`` amongst others, are reserved and +cannot be used as variable names. If you try to use a reserved name a ``ValueError`` exception +is raised with the message "Name <var> is reserved". + +Generating output +""""""""""""""""" + +By default, every variable create will be enable for output with minimum verbosity set to zero, in +other words, they will always be in the output by default. + +You can control that when creating the variable for the first time or later in the code: + +.. code-block:: python + + vars.set("internal", x + 4, output=False) + vars.set_meta("internal", output=False) + +You can also set the verbosity of some variable, like: + +.. code-block:: python + + vars.set("abc", x + 4) + vars.set("debug_x", x, verbosity=3) + + results = vars.output(module._verbosity) + module.exit_json(**results) + +If the module was invoked with verbosity lower than 3, then the output will only contain +the variable ``abc``. If running at higher verbosity, as in ``ansible-playbook -vvv``, +then the output will also contain ``debug_x``. + +Generating facts is very similar to regular output, but variables are not marked as facts by default. + +.. code-block:: python + + vars.set("modulefact", x + 4, fact=True) + vars.set("debugfact", x, fact=True, verbosity=3) + + results = vars.output(module._verbosity) + results["ansible_facts"] = {"module_name": vars.facts(module._verbosity)} + module.exit_json(**results) + +Handling change +""""""""""""""" + +You can use ``VarDict`` to determine whether variables have had their values changed. + +.. code-block:: python + + vars.set("abc", 42, change=True) + vars.abc = 90 + + results = vars.output() + results["changed"] = vars.has_changed + module.exit_json(**results) + +If tracking changes in variables, you may want to present the difference between the initial and the final +values of it. For that, you want to use: + +.. code-block:: python + + vars.set("abc", 42, change=True, diff=True) + vars.abc = 90 + + results = vars.output() + results["changed"] = vars.has_changed + results["diff"] = vars.diff() + module.exit_json(**results) + +.. versionadded:: 7.1.0 diff --git a/ansible_collections/community/general/meta/runtime.yml b/ansible_collections/community/general/meta/runtime.yml index edeb53005..4f5007b4a 100644 --- a/ansible_collections/community/general/meta/runtime.yml +++ b/ansible_collections/community/general/meta/runtime.yml @@ -6,6 +6,8 @@ requires_ansible: '>=2.13.0' action_groups: consul: + - consul_agent_check + - consul_agent_service - consul_auth_method - consul_binding_rule - consul_policy diff --git a/ansible_collections/community/general/plugins/callback/opentelemetry.py b/ansible_collections/community/general/plugins/callback/opentelemetry.py index 58cfa057b..c6e8a87c1 100644 --- a/ansible_collections/community/general/plugins/callback/opentelemetry.py +++ b/ansible_collections/community/general/plugins/callback/opentelemetry.py @@ -556,11 +556,19 @@ class CallbackModule(CallbackBase): self.otel_exporter_otlp_traces_protocol = self.get_option('otel_exporter_otlp_traces_protocol') - def dump_results(self, result): + def dump_results(self, task, result): """ dump the results if disable_logs is not enabled """ if self.disable_logs: return "" - return self._dump_results(result._result) + # ansible.builtin.uri contains the response in the json field + save = dict(result._result) + + if "json" in save and task.action in ("ansible.builtin.uri", "ansible.legacy.uri", "uri"): + save.pop("json") + # ansible.builtin.slurp contains the response in the content field + if "content" in save and task.action in ("ansible.builtin.slurp", "ansible.legacy.slurp", "slurp"): + save.pop("content") + return self._dump_results(save) def v2_playbook_on_start(self, playbook): self.ansible_playbook = basename(playbook._file_name) @@ -611,7 +619,7 @@ class CallbackModule(CallbackBase): self.tasks_data, status, result, - self.dump_results(result) + self.dump_results(self.tasks_data[result._task._uuid], result) ) def v2_runner_on_ok(self, result): @@ -619,7 +627,7 @@ class CallbackModule(CallbackBase): self.tasks_data, 'ok', result, - self.dump_results(result) + self.dump_results(self.tasks_data[result._task._uuid], result) ) def v2_runner_on_skipped(self, result): @@ -627,7 +635,7 @@ class CallbackModule(CallbackBase): self.tasks_data, 'skipped', result, - self.dump_results(result) + self.dump_results(self.tasks_data[result._task._uuid], result) ) def v2_playbook_on_include(self, included_file): diff --git a/ansible_collections/community/general/plugins/doc_fragments/django.py b/ansible_collections/community/general/plugins/doc_fragments/django.py index d92799937..f89ec9144 100644 --- a/ansible_collections/community/general/plugins/doc_fragments/django.py +++ b/ansible_collections/community/general/plugins/doc_fragments/django.py @@ -51,3 +51,12 @@ seealso: Please make sure that you select the right version of Django in the version selector on that page. link: https://docs.djangoproject.com/en/5.0/ref/django-admin/ ''' + + DATABASE = r''' +options: + database: + description: + - Specify the database to be used. + type: str + default: default +''' diff --git a/ansible_collections/community/general/plugins/doc_fragments/proxmox.py b/ansible_collections/community/general/plugins/doc_fragments/proxmox.py index cb533fefa..239dba06d 100644 --- a/ansible_collections/community/general/plugins/doc_fragments/proxmox.py +++ b/ansible_collections/community/general/plugins/doc_fragments/proxmox.py @@ -16,6 +16,13 @@ options: - Specify the target host of the Proxmox VE cluster. type: str required: true + api_port: + description: + - Specify the target port of the Proxmox VE cluster. + - Uses the E(PROXMOX_PORT) environment variable if not specified. + type: int + required: false + version_added: 9.1.0 api_user: description: - Specify the user to authenticate with. diff --git a/ansible_collections/community/general/plugins/filter/keep_keys.py b/ansible_collections/community/general/plugins/filter/keep_keys.py new file mode 100644 index 000000000..dffccba35 --- /dev/null +++ b/ansible_collections/community/general/plugins/filter/keep_keys.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Vladimir Botka <vbotka@gmail.com> +# Copyright (c) 2024 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: keep_keys + short_description: Keep specific keys from dictionaries in a list + version_added: "9.1.0" + author: + - Vladimir Botka (@vbotka) + - Felix Fontein (@felixfontein) + description: This filter keeps only specified keys from a provided list of dictionaries. + options: + _input: + description: + - A list of dictionaries. + - Top level keys must be strings. + type: list + elements: dictionary + required: true + target: + description: + - A single key or key pattern to keep, or a list of keys or keys patterns to keep. + - If O(matching_parameter=regex) there must be exactly one pattern provided. + type: raw + required: true + matching_parameter: + description: Specify the matching option of target keys. + type: str + default: equal + choices: + equal: Matches keys of exactly one of the O(target) items. + starts_with: Matches keys that start with one of the O(target) items. + ends_with: Matches keys that end with one of the O(target) items. + regex: + - Matches keys that match the regular expresion provided in O(target). + - In this case, O(target) must be a regex string or a list with single regex string. +''' + +EXAMPLES = ''' + l: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + + # 1) By default match keys that equal any of the items in the target. + t: [k0_x0, k1_x1] + r: "{{ l | community.general.keep_keys(target=t) }}" + + # 2) Match keys that start with any of the items in the target. + t: [k0, k1] + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='starts_with') }}" + + # 3) Match keys that end with any of the items in target. + t: [x0, x1] + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='ends_with') }}" + + # 4) Match keys by the regex. + t: ['^.*[01]_x.*$'] + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='regex') }}" + + # 5) Match keys by the regex. + t: '^.*[01]_x.*$' + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='regex') }}" + + # The results of above examples 1-5 are all the same. + r: + - {k0_x0: A0, k1_x1: B0} + - {k0_x0: A1, k1_x1: B1} + + # 6) By default match keys that equal the target. + t: k0_x0 + r: "{{ l | community.general.keep_keys(target=t) }}" + + # 7) Match keys that start with the target. + t: k0 + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='starts_with') }}" + + # 8) Match keys that end with the target. + t: x0 + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='ends_with') }}" + + # 9) Match keys by the regex. + t: '^.*0_x.*$' + r: "{{ l | community.general.keep_keys(target=t, matching_parameter='regex') }}" + + # The results of above examples 6-9 are all the same. + r: + - {k0_x0: A0} + - {k0_x0: A1} +''' + +RETURN = ''' + _value: + description: The list of dictionaries with selected keys. + type: list + elements: dictionary +''' + +from ansible_collections.community.general.plugins.plugin_utils.keys_filter import ( + _keys_filter_params, + _keys_filter_target_str) + + +def keep_keys(data, target=None, matching_parameter='equal'): + """keep specific keys from dictionaries in a list""" + + # test parameters + _keys_filter_params(data, matching_parameter) + # test and transform target + tt = _keys_filter_target_str(target, matching_parameter) + + if matching_parameter == 'equal': + def keep_key(key): + return key in tt + elif matching_parameter == 'starts_with': + def keep_key(key): + return key.startswith(tt) + elif matching_parameter == 'ends_with': + def keep_key(key): + return key.endswith(tt) + elif matching_parameter == 'regex': + def keep_key(key): + return tt.match(key) is not None + + return [dict((k, v) for k, v in d.items() if keep_key(k)) for d in data] + + +class FilterModule(object): + + def filters(self): + return { + 'keep_keys': keep_keys, + } diff --git a/ansible_collections/community/general/plugins/filter/lists_mergeby.py b/ansible_collections/community/general/plugins/filter/lists_mergeby.py index caf183492..0e47d5017 100644 --- a/ansible_collections/community/general/plugins/filter/lists_mergeby.py +++ b/ansible_collections/community/general/plugins/filter/lists_mergeby.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022, Vladimir Botka <vbotka@gmail.com> +# Copyright (c) 2020-2024, Vladimir Botka <vbotka@gmail.com> # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -12,22 +12,32 @@ DOCUMENTATION = ''' version_added: 2.0.0 author: Vladimir Botka (@vbotka) description: - - Merge two or more lists by attribute O(index). Optional parameters O(recursive) and O(list_merge) - control the merging of the lists in values. The function merge_hash from ansible.utils.vars - is used. To learn details on how to use the parameters O(recursive) and O(list_merge) see - Ansible User's Guide chapter "Using filters to manipulate data" section "Combining - hashes/dictionaries". + - Merge two or more lists by attribute O(index). Optional + parameters O(recursive) and O(list_merge) control the merging of + the nested dictionaries and lists. + - The function C(merge_hash) from C(ansible.utils.vars) is used. + - To learn details on how to use the parameters O(recursive) and + O(list_merge) see Ansible User's Guide chapter "Using filters to + manipulate data" section R(Combining hashes/dictionaries, combine_filter) or the + filter P(ansible.builtin.combine#filter). + positional: another_list, index options: _input: - description: A list of dictionaries. + description: + - A list of dictionaries, or a list of lists of dictionaries. + - The required type of the C(elements) is set to C(raw) + because all elements of O(_input) can be either dictionaries + or lists. type: list - elements: dictionary + elements: raw required: true another_list: - description: Another list of dictionaries. This parameter can be specified multiple times. + description: + - Another list of dictionaries, or a list of lists of dictionaries. + - This parameter can be specified multiple times. type: list - elements: dictionary + elements: raw index: description: - The dictionary key that must be present in every dictionary in every list that is used to @@ -55,40 +65,134 @@ DOCUMENTATION = ''' ''' EXAMPLES = ''' -- name: Merge two lists +# Some results below are manually formatted for better readability. The +# dictionaries' keys will be sorted alphabetically in real output. + +- name: Example 1. Merge two lists. The results r1 and r2 are the same. + ansible.builtin.debug: + msg: | + r1: {{ r1 }} + r2: {{ r2 }} + vars: + list1: + - {index: a, value: 123} + - {index: b, value: 4} + list2: + - {index: a, foo: bar} + - {index: c, foo: baz} + r1: "{{ list1 | community.general.lists_mergeby(list2, 'index') }}" + r2: "{{ [list1, list2] | community.general.lists_mergeby('index') }}" + +# r1: +# - {index: a, foo: bar, value: 123} +# - {index: b, value: 4} +# - {index: c, foo: baz} +# r2: +# - {index: a, foo: bar, value: 123} +# - {index: b, value: 4} +# - {index: c, foo: baz} + +- name: Example 2. Merge three lists + ansible.builtin.debug: + var: r + vars: + list1: + - {index: a, value: 123} + - {index: b, value: 4} + list2: + - {index: a, foo: bar} + - {index: c, foo: baz} + list3: + - {index: d, foo: qux} + r: "{{ [list1, list2, list3] | community.general.lists_mergeby('index') }}" + +# r: +# - {index: a, foo: bar, value: 123} +# - {index: b, value: 4} +# - {index: c, foo: baz} +# - {index: d, foo: qux} + +- name: Example 3. Merge single list. The result is the same as 2. + ansible.builtin.debug: + var: r + vars: + list1: + - {index: a, value: 123} + - {index: b, value: 4} + - {index: a, foo: bar} + - {index: c, foo: baz} + - {index: d, foo: qux} + r: "{{ [list1, []] | community.general.lists_mergeby('index') }}" + +# r: +# - {index: a, foo: bar, value: 123} +# - {index: b, value: 4} +# - {index: c, foo: baz} +# - {index: d, foo: qux} + +- name: Example 4. Merge two lists. By default, replace nested lists. + ansible.builtin.debug: + var: r + vars: + list1: + - {index: a, foo: [X1, X2]} + - {index: b, foo: [X1, X2]} + list2: + - {index: a, foo: [Y1, Y2]} + - {index: b, foo: [Y1, Y2]} + r: "{{ [list1, list2] | community.general.lists_mergeby('index') }}" + +# r: +# - {index: a, foo: [Y1, Y2]} +# - {index: b, foo: [Y1, Y2]} + +- name: Example 5. Merge two lists. Append nested lists. + ansible.builtin.debug: + var: r + vars: + list1: + - {index: a, foo: [X1, X2]} + - {index: b, foo: [X1, X2]} + list2: + - {index: a, foo: [Y1, Y2]} + - {index: b, foo: [Y1, Y2]} + r: "{{ [list1, list2] | community.general.lists_mergeby('index', list_merge='append') }}" + +# r: +# - {index: a, foo: [X1, X2, Y1, Y2]} +# - {index: b, foo: [X1, X2, Y1, Y2]} + +- name: Example 6. Merge two lists. By default, do not merge nested dictionaries. + ansible.builtin.debug: + var: r + vars: + list1: + - {index: a, foo: {x: 1, y: 2}} + - {index: b, foo: [X1, X2]} + list2: + - {index: a, foo: {y: 3, z: 4}} + - {index: b, foo: [Y1, Y2]} + r: "{{ [list1, list2] | community.general.lists_mergeby('index') }}" + +# r: +# - {index: a, foo: {y: 3, z: 4}} +# - {index: b, foo: [Y1, Y2]} + +- name: Example 7. Merge two lists. Merge nested dictionaries too. ansible.builtin.debug: - msg: >- - {{ list1 | community.general.lists_mergeby( - list2, - 'index', - recursive=True, - list_merge='append' - ) }}" + var: r vars: list1: - - index: a - value: 123 - - index: b - value: 42 + - {index: a, foo: {x: 1, y: 2}} + - {index: b, foo: [X1, X2]} list2: - - index: a - foo: bar - - index: c - foo: baz - # Produces the following list of dictionaries: - # { - # "index": "a", - # "foo": "bar", - # "value": 123 - # }, - # { - # "index": "b", - # "value": 42 - # }, - # { - # "index": "c", - # "foo": "baz" - # } + - {index: a, foo: {y: 3, z: 4}} + - {index: b, foo: [Y1, Y2]} + r: "{{ [list1, list2] | community.general.lists_mergeby('index', recursive=true) }}" + +# r: +# - {index: a, foo: {x:1, y: 3, z: 4}} +# - {index: b, foo: [Y1, Y2]} ''' RETURN = ''' @@ -108,13 +212,14 @@ from operator import itemgetter def list_mergeby(x, y, index, recursive=False, list_merge='replace'): - ''' Merge 2 lists by attribute 'index'. The function merge_hash from ansible.utils.vars is used. - This function is used by the function lists_mergeby. + '''Merge 2 lists by attribute 'index'. The function 'merge_hash' + from ansible.utils.vars is used. This function is used by the + function lists_mergeby. ''' d = defaultdict(dict) - for l in (x, y): - for elem in l: + for lst in (x, y): + for elem in lst: if not isinstance(elem, Mapping): msg = "Elements of list arguments for lists_mergeby must be dictionaries. %s is %s" raise AnsibleFilterError(msg % (elem, type(elem))) @@ -124,20 +229,9 @@ def list_mergeby(x, y, index, recursive=False, list_merge='replace'): def lists_mergeby(*terms, **kwargs): - ''' Merge 2 or more lists by attribute 'index'. Optional parameters 'recursive' and 'list_merge' - control the merging of the lists in values. The function merge_hash from ansible.utils.vars - is used. To learn details on how to use the parameters 'recursive' and 'list_merge' see - Ansible User's Guide chapter "Using filters to manipulate data" section "Combining - hashes/dictionaries". - - Example: - - debug: - msg: "{{ list1| - community.general.lists_mergeby(list2, - 'index', - recursive=True, - list_merge='append')| - list }}" + '''Merge 2 or more lists by attribute 'index'. To learn details + on how to use the parameters 'recursive' and 'list_merge' see + the filter ansible.builtin.combine. ''' recursive = kwargs.pop('recursive', False) @@ -155,7 +249,7 @@ def lists_mergeby(*terms, **kwargs): "must be lists. %s is %s") raise AnsibleFilterError(msg % (sublist, type(sublist))) if len(sublist) > 0: - if all(isinstance(l, Sequence) for l in sublist): + if all(isinstance(lst, Sequence) for lst in sublist): for item in sublist: flat_list.append(item) else: diff --git a/ansible_collections/community/general/plugins/filter/remove_keys.py b/ansible_collections/community/general/plugins/filter/remove_keys.py new file mode 100644 index 000000000..cabce1468 --- /dev/null +++ b/ansible_collections/community/general/plugins/filter/remove_keys.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Vladimir Botka <vbotka@gmail.com> +# Copyright (c) 2024 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: remove_keys + short_description: Remove specific keys from dictionaries in a list + version_added: "9.1.0" + author: + - Vladimir Botka (@vbotka) + - Felix Fontein (@felixfontein) + description: This filter removes only specified keys from a provided list of dictionaries. + options: + _input: + description: + - A list of dictionaries. + - Top level keys must be strings. + type: list + elements: dictionary + required: true + target: + description: + - A single key or key pattern to remove, or a list of keys or keys patterns to remove. + - If O(matching_parameter=regex) there must be exactly one pattern provided. + type: raw + required: true + matching_parameter: + description: Specify the matching option of target keys. + type: str + default: equal + choices: + equal: Matches keys of exactly one of the O(target) items. + starts_with: Matches keys that start with one of the O(target) items. + ends_with: Matches keys that end with one of the O(target) items. + regex: + - Matches keys that match the regular expresion provided in O(target). + - In this case, O(target) must be a regex string or a list with single regex string. +''' + +EXAMPLES = ''' + l: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + + # 1) By default match keys that equal any of the items in the target. + t: [k0_x0, k1_x1] + r: "{{ l | community.general.remove_keys(target=t) }}" + + # 2) Match keys that start with any of the items in the target. + t: [k0, k1] + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='starts_with') }}" + + # 3) Match keys that end with any of the items in target. + t: [x0, x1] + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='ends_with') }}" + + # 4) Match keys by the regex. + t: ['^.*[01]_x.*$'] + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='regex') }}" + + # 5) Match keys by the regex. + t: '^.*[01]_x.*$' + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='regex') }}" + + # The results of above examples 1-5 are all the same. + r: + - {k2_x2: [C0], k3_x3: foo} + - {k2_x2: [C1], k3_x3: bar} + + # 6) By default match keys that equal the target. + t: k0_x0 + r: "{{ l | community.general.remove_keys(target=t) }}" + + # 7) Match keys that start with the target. + t: k0 + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='starts_with') }}" + + # 8) Match keys that end with the target. + t: x0 + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='ends_with') }}" + + # 9) Match keys by the regex. + t: '^.*0_x.*$' + r: "{{ l | community.general.remove_keys(target=t, matching_parameter='regex') }}" + + # The results of above examples 6-9 are all the same. + r: + - {k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k1_x1: B1, k2_x2: [C1], k3_x3: bar} +''' + +RETURN = ''' + _value: + description: The list of dictionaries with selected keys removed. + type: list + elements: dictionary +''' + +from ansible_collections.community.general.plugins.plugin_utils.keys_filter import ( + _keys_filter_params, + _keys_filter_target_str) + + +def remove_keys(data, target=None, matching_parameter='equal'): + """remove specific keys from dictionaries in a list""" + + # test parameters + _keys_filter_params(data, matching_parameter) + # test and transform target + tt = _keys_filter_target_str(target, matching_parameter) + + if matching_parameter == 'equal': + def keep_key(key): + return key not in tt + elif matching_parameter == 'starts_with': + def keep_key(key): + return not key.startswith(tt) + elif matching_parameter == 'ends_with': + def keep_key(key): + return not key.endswith(tt) + elif matching_parameter == 'regex': + def keep_key(key): + return tt.match(key) is None + + return [dict((k, v) for k, v in d.items() if keep_key(k)) for d in data] + + +class FilterModule(object): + + def filters(self): + return { + 'remove_keys': remove_keys, + } diff --git a/ansible_collections/community/general/plugins/filter/replace_keys.py b/ansible_collections/community/general/plugins/filter/replace_keys.py new file mode 100644 index 000000000..d3b12c05d --- /dev/null +++ b/ansible_collections/community/general/plugins/filter/replace_keys.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Vladimir Botka <vbotka@gmail.com> +# Copyright (c) 2024 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: replace_keys + short_description: Replace specific keys in a list of dictionaries + version_added: "9.1.0" + author: + - Vladimir Botka (@vbotka) + - Felix Fontein (@felixfontein) + description: This filter replaces specified keys in a provided list of dictionaries. + options: + _input: + description: + - A list of dictionaries. + - Top level keys must be strings. + type: list + elements: dictionary + required: true + target: + description: + - A list of dictionaries with attributes C(before) and C(after). + - The value of O(target[].after) replaces key matching O(target[].before). + type: list + elements: dictionary + required: true + suboptions: + before: + description: + - A key or key pattern to change. + - The interpretation of O(target[].before) depends on O(matching_parameter). + - For a key that matches multiple O(target[].before)s, the B(first) matching O(target[].after) will be used. + type: str + after: + description: A matching key change to. + type: str + matching_parameter: + description: Specify the matching option of target keys. + type: str + default: equal + choices: + equal: Matches keys of exactly one of the O(target[].before) items. + starts_with: Matches keys that start with one of the O(target[].before) items. + ends_with: Matches keys that end with one of the O(target[].before) items. + regex: Matches keys that match one of the regular expressions provided in O(target[].before). +''' + +EXAMPLES = ''' + l: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + + # 1) By default, replace keys that are equal any of the attributes before. + t: + - {before: k0_x0, after: a0} + - {before: k1_x1, after: a1} + r: "{{ l | community.general.replace_keys(target=t) }}" + + # 2) Replace keys that starts with any of the attributes before. + t: + - {before: k0, after: a0} + - {before: k1, after: a1} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='starts_with') }}" + + # 3) Replace keys that ends with any of the attributes before. + t: + - {before: x0, after: a0} + - {before: x1, after: a1} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='ends_with') }}" + + # 4) Replace keys that match any regex of the attributes before. + t: + - {before: "^.*0_x.*$", after: a0} + - {before: "^.*1_x.*$", after: a1} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='regex') }}" + + # The results of above examples 1-4 are all the same. + r: + - {a0: A0, a1: B0, k2_x2: [C0], k3_x3: foo} + - {a0: A1, a1: B1, k2_x2: [C1], k3_x3: bar} + + # 5) If more keys match the same attribute before the last one will be used. + t: + - {before: "^.*_x.*$", after: X} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='regex') }}" + + # gives + + r: + - X: foo + - X: bar + + # 6) If there are items with equal attribute before the first one will be used. + t: + - {before: "^.*_x.*$", after: X} + - {before: "^.*_x.*$", after: Y} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='regex') }}" + + # gives + + r: + - X: foo + - X: bar + + # 7) If there are more matches for a key the first one will be used. + l: + - {aaa1: A, bbb1: B, ccc1: C} + - {aaa2: D, bbb2: E, ccc2: F} + t: + - {before: a, after: X} + - {before: aa, after: Y} + r: "{{ l | community.general.replace_keys(target=t, matching_parameter='starts_with') }}" + + # gives + + r: + - {X: A, bbb1: B, ccc1: C} + - {X: D, bbb2: E, ccc2: F} +''' + +RETURN = ''' + _value: + description: The list of dictionaries with replaced keys. + type: list + elements: dictionary +''' + +from ansible_collections.community.general.plugins.plugin_utils.keys_filter import ( + _keys_filter_params, + _keys_filter_target_dict) + + +def replace_keys(data, target=None, matching_parameter='equal'): + """replace specific keys in a list of dictionaries""" + + # test parameters + _keys_filter_params(data, matching_parameter) + # test and transform target + tz = _keys_filter_target_dict(target, matching_parameter) + + if matching_parameter == 'equal': + def replace_key(key): + for b, a in tz: + if key == b: + return a + return key + elif matching_parameter == 'starts_with': + def replace_key(key): + for b, a in tz: + if key.startswith(b): + return a + return key + elif matching_parameter == 'ends_with': + def replace_key(key): + for b, a in tz: + if key.endswith(b): + return a + return key + elif matching_parameter == 'regex': + def replace_key(key): + for b, a in tz: + if b.match(key): + return a + return key + + return [dict((replace_key(k), v) for k, v in d.items()) for d in data] + + +class FilterModule(object): + + def filters(self): + return { + 'replace_keys': replace_keys, + } diff --git a/ansible_collections/community/general/plugins/inventory/opennebula.py b/ansible_collections/community/general/plugins/inventory/opennebula.py index b097307c3..bf81758ef 100644 --- a/ansible_collections/community/general/plugins/inventory/opennebula.py +++ b/ansible_collections/community/general/plugins/inventory/opennebula.py @@ -143,7 +143,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable): nic = [nic] for net in nic: - return net['IP'] + if net.get('IP'): + return net['IP'] return False diff --git a/ansible_collections/community/general/plugins/module_utils/cmd_runner.py b/ansible_collections/community/general/plugins/module_utils/cmd_runner.py index 2bf2b32e8..da4f1b6fc 100644 --- a/ansible_collections/community/general/plugins/module_utils/cmd_runner.py +++ b/ansible_collections/community/general/plugins/module_utils/cmd_runner.py @@ -89,18 +89,31 @@ class FormatError(CmdRunnerException): class _ArgFormat(object): + # DEPRECATION: set default value for ignore_none to True in community.general 12.0.0 def __init__(self, func, ignore_none=None, ignore_missing_value=False): self.func = func self.ignore_none = ignore_none self.ignore_missing_value = ignore_missing_value - def __call__(self, value, ctx_ignore_none): + # DEPRECATION: remove parameter ctx_ignore_none in community.general 12.0.0 + def __call__(self, value, ctx_ignore_none=True): + # DEPRECATION: replace ctx_ignore_none with True in community.general 12.0.0 ignore_none = self.ignore_none if self.ignore_none is not None else ctx_ignore_none if value is None and ignore_none: return [] f = self.func return [str(x) for x in f(value)] + def __str__(self): + return "<ArgFormat: func={0}, ignore_none={1}, ignore_missing_value={2}>".format( + self.func, + self.ignore_none, + self.ignore_missing_value, + ) + + def __repr__(self): + return str(self) + class _Format(object): @staticmethod @@ -114,7 +127,7 @@ class _Format(object): @staticmethod def as_bool_not(args): - return _ArgFormat(lambda value: [] if value else _ensure_list(args), ignore_none=False) + return _Format.as_bool([], args, ignore_none=False) @staticmethod def as_optval(arg, ignore_none=None): @@ -184,6 +197,19 @@ class _Format(object): return func(**v) return wrapper + @staticmethod + def stack(fmt): + @wraps(fmt) + def wrapper(*args, **kwargs): + new_func = fmt(ignore_none=True, *args, **kwargs) + + def stacking(value): + stack = [new_func(v) for v in value if v] + stack = [x for args in stack for x in args] + return stack + return _ArgFormat(stacking, ignore_none=True) + return wrapper + class CmdRunner(object): """ @@ -204,7 +230,11 @@ class CmdRunner(object): self.default_args_order = self._prepare_args_order(default_args_order) if arg_formats is None: arg_formats = {} - self.arg_formats = dict(arg_formats) + self.arg_formats = {} + for fmt_name, fmt in arg_formats.items(): + if not isinstance(fmt, _ArgFormat): + fmt = _Format.as_func(func=fmt, ignore_none=True) + self.arg_formats[fmt_name] = fmt self.check_rc = check_rc self.force_lang = force_lang self.path_prefix = path_prefix @@ -223,7 +253,16 @@ class CmdRunner(object): def binary(self): return self.command[0] - def __call__(self, args_order=None, output_process=None, ignore_value_none=True, check_mode_skip=False, check_mode_return=None, **kwargs): + # remove parameter ignore_value_none in community.general 12.0.0 + def __call__(self, args_order=None, output_process=None, ignore_value_none=None, check_mode_skip=False, check_mode_return=None, **kwargs): + if ignore_value_none is None: + ignore_value_none = True + else: + self.module.deprecate( + "Using ignore_value_none when creating the runner context is now deprecated, " + "and the parameter will be removed in community.general 12.0.0. ", + version="12.0.0", collection_name="community.general" + ) if output_process is None: output_process = _process_as_is if args_order is None: @@ -235,7 +274,7 @@ class CmdRunner(object): return _CmdRunnerContext(runner=self, args_order=args_order, output_process=output_process, - ignore_value_none=ignore_value_none, + ignore_value_none=ignore_value_none, # DEPRECATION: remove in community.general 12.0.0 check_mode_skip=check_mode_skip, check_mode_return=check_mode_return, **kwargs) @@ -251,6 +290,7 @@ class _CmdRunnerContext(object): self.runner = runner self.args_order = tuple(args_order) self.output_process = output_process + # DEPRECATION: parameter ignore_value_none at the context level is deprecated and will be removed in community.general 12.0.0 self.ignore_value_none = ignore_value_none self.check_mode_skip = check_mode_skip self.check_mode_return = check_mode_return @@ -290,6 +330,7 @@ class _CmdRunnerContext(object): value = named_args[arg_name] elif not runner.arg_formats[arg_name].ignore_missing_value: raise MissingArgumentValue(self.args_order, arg_name) + # DEPRECATION: remove parameter ctx_ignore_none in 12.0.0 self.cmd.extend(runner.arg_formats[arg_name](value, ctx_ignore_none=self.ignore_value_none)) except MissingArgumentValue: raise @@ -306,7 +347,7 @@ class _CmdRunnerContext(object): @property def run_info(self): return dict( - ignore_value_none=self.ignore_value_none, + ignore_value_none=self.ignore_value_none, # DEPRECATION: remove in community.general 12.0.0 check_rc=self.check_rc, environ_update=self.environ_update, args_order=self.args_order, diff --git a/ansible_collections/community/general/plugins/module_utils/consul.py b/ansible_collections/community/general/plugins/module_utils/consul.py index 68c1a130b..cd54a105f 100644 --- a/ansible_collections/community/general/plugins/module_utils/consul.py +++ b/ansible_collections/community/general/plugins/module_utils/consul.py @@ -10,6 +10,7 @@ __metaclass__ = type import copy import json +import re from ansible.module_utils.six.moves.urllib import error as urllib_error from ansible.module_utils.six.moves.urllib.parse import urlencode @@ -68,6 +69,25 @@ def camel_case_key(key): return "".join(parts) +def validate_check(check): + validate_duration_keys = ['Interval', 'Ttl', 'Timeout'] + validate_tcp_regex = r"(?P<host>.*):(?P<port>(?:[0-9]+))$" + if check.get('Tcp') is not None: + match = re.match(validate_tcp_regex, check['Tcp']) + if not match: + raise Exception('tcp check must be in host:port format') + for duration in validate_duration_keys: + if duration in check and check[duration] is not None: + check[duration] = validate_duration(check[duration]) + + +def validate_duration(duration): + if duration: + if not re.search(r"\d+(?:ns|us|ms|s|m|h)", duration): + duration = "{0}s".format(duration) + return duration + + STATE_PARAMETER = "state" STATE_PRESENT = "present" STATE_ABSENT = "absent" @@ -81,7 +101,7 @@ OPERATION_DELETE = "remove" def _normalize_params(params, arg_spec): final_params = {} for k, v in params.items(): - if k not in arg_spec: # Alias + if k not in arg_spec or v is None: # Alias continue spec = arg_spec[k] if ( @@ -105,9 +125,10 @@ class _ConsulModule: """ api_endpoint = None # type: str - unique_identifier = None # type: str + unique_identifiers = None # type: list result_key = None # type: str create_only_fields = set() + operational_attributes = set() params = {} def __init__(self, module): @@ -119,6 +140,8 @@ class _ConsulModule: if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC } + self.operational_attributes.update({"CreateIndex", "CreateTime", "Hash", "ModifyIndex"}) + def execute(self): obj = self.read_object() @@ -203,14 +226,24 @@ class _ConsulModule: return False def prepare_object(self, existing, obj): - operational_attributes = {"CreateIndex", "CreateTime", "Hash", "ModifyIndex"} existing = { - k: v for k, v in existing.items() if k not in operational_attributes + k: v for k, v in existing.items() if k not in self.operational_attributes } for k, v in obj.items(): existing[k] = v return existing + def id_from_obj(self, obj, camel_case=False): + def key_func(key): + return camel_case_key(key) if camel_case else key + + if self.unique_identifiers: + for identifier in self.unique_identifiers: + identifier = key_func(identifier) + if identifier in obj: + return obj[identifier] + return None + def endpoint_url(self, operation, identifier=None): if operation == OPERATION_CREATE: return self.api_endpoint @@ -219,7 +252,8 @@ class _ConsulModule: raise RuntimeError("invalid arguments passed") def read_object(self): - url = self.endpoint_url(OPERATION_READ, self.params.get(self.unique_identifier)) + identifier = self.id_from_obj(self.params) + url = self.endpoint_url(OPERATION_READ, identifier) try: return self.get(url) except RequestError as e: @@ -233,25 +267,28 @@ class _ConsulModule: if self._module.check_mode: return obj else: - return self.put(self.api_endpoint, data=self.prepare_object({}, obj)) + url = self.endpoint_url(OPERATION_CREATE) + created_obj = self.put(url, data=self.prepare_object({}, obj)) + if created_obj is None: + created_obj = self.read_object() + return created_obj def update_object(self, existing, obj): - url = self.endpoint_url( - OPERATION_UPDATE, existing.get(camel_case_key(self.unique_identifier)) - ) merged_object = self.prepare_object(existing, obj) if self._module.check_mode: return merged_object else: - return self.put(url, data=merged_object) + url = self.endpoint_url(OPERATION_UPDATE, self.id_from_obj(existing, camel_case=True)) + updated_obj = self.put(url, data=merged_object) + if updated_obj is None: + updated_obj = self.read_object() + return updated_obj def delete_object(self, obj): if self._module.check_mode: return {} else: - url = self.endpoint_url( - OPERATION_DELETE, obj.get(camel_case_key(self.unique_identifier)) - ) + url = self.endpoint_url(OPERATION_DELETE, self.id_from_obj(obj, camel_case=True)) return self.delete(url) def _request(self, method, url_parts, data=None, params=None): @@ -309,7 +346,9 @@ class _ConsulModule: if 400 <= status < 600: raise RequestError(status, response_data) - return json.loads(response_data) + if response_data: + return json.loads(response_data) + return None def get(self, url_parts, **kwargs): return self._request("GET", url_parts, **kwargs) diff --git a/ansible_collections/community/general/plugins/module_utils/django.py b/ansible_collections/community/general/plugins/module_utils/django.py index fbaf840db..5fb375c6f 100644 --- a/ansible_collections/community/general/plugins/module_utils/django.py +++ b/ansible_collections/community/general/plugins/module_utils/django.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +from ansible.module_utils.common.dict_transformations import dict_merge from ansible_collections.community.general.plugins.module_utils.cmd_runner import cmd_runner_fmt from ansible_collections.community.general.plugins.module_utils.python_runner import PythonRunner from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper @@ -33,6 +34,18 @@ _django_std_arg_fmts = dict( skip_checks=cmd_runner_fmt.as_bool("--skip-checks"), ) +_django_database_args = dict( + database=dict(type="str", default="default"), +) + +_args_menu = dict( + std=(django_std_args, _django_std_arg_fmts), + database=(_django_database_args, {"database": cmd_runner_fmt.as_opt_eq_val("--database")}), + noinput=({}, {"noinput": cmd_runner_fmt.as_fixed("--noinput")}), + dry_run=({}, {"dry_run": cmd_runner_fmt.as_bool("--dry-run")}), + check=({}, {"check": cmd_runner_fmt.as_bool("--check")}), +) + class _DjangoRunner(PythonRunner): def __init__(self, module, arg_formats=None, **kwargs): @@ -55,15 +68,30 @@ class DjangoModuleHelper(ModuleHelper): arg_formats = {} django_admin_arg_order = () use_old_vardict = False + _django_args = [] + _check_mode_arg = "" def __init__(self): - argument_spec = dict(django_std_args) - argument_spec.update(self.module.get("argument_spec", {})) - self.module["argument_spec"] = argument_spec + self.module["argument_spec"], self.arg_formats = self._build_args(self.module.get("argument_spec", {}), + self.arg_formats, + *(["std"] + self._django_args)) super(DjangoModuleHelper, self).__init__(self.module) if self.django_admin_cmd is not None: self.vars.command = self.django_admin_cmd + @staticmethod + def _build_args(arg_spec, arg_format, *names): + res_arg_spec = {} + res_arg_fmts = {} + for name in names: + args, fmts = _args_menu[name] + res_arg_spec = dict_merge(res_arg_spec, args) + res_arg_fmts = dict_merge(res_arg_fmts, fmts) + res_arg_spec = dict_merge(res_arg_spec, arg_spec) + res_arg_fmts = dict_merge(res_arg_fmts, arg_format) + + return res_arg_spec, res_arg_fmts + def __run__(self): runner = _DjangoRunner(self.module, default_args_order=self.django_admin_arg_order, @@ -71,7 +99,10 @@ class DjangoModuleHelper(ModuleHelper): venv=self.vars.venv, check_rc=True) with runner() as ctx: - results = ctx.run() + run_params = self.vars.as_dict() + if self._check_mode_arg: + run_params.update({self._check_mode_arg: self.check_mode}) + results = ctx.run(**run_params) self.vars.stdout = ctx.results_out self.vars.stderr = ctx.results_err self.vars.cmd = ctx.cmd diff --git a/ansible_collections/community/general/plugins/module_utils/proxmox.py b/ansible_collections/community/general/plugins/module_utils/proxmox.py index 5fd783d65..05bf1874b 100644 --- a/ansible_collections/community/general/plugins/module_utils/proxmox.py +++ b/ansible_collections/community/general/plugins/module_utils/proxmox.py @@ -29,6 +29,9 @@ def proxmox_auth_argument_spec(): required=True, fallback=(env_fallback, ['PROXMOX_HOST']) ), + api_port=dict(type='int', + fallback=(env_fallback, ['PROXMOX_PORT']) + ), api_user=dict(type='str', required=True, fallback=(env_fallback, ['PROXMOX_USER']) @@ -82,6 +85,7 @@ class ProxmoxAnsible(object): def _connect(self): api_host = self.module.params['api_host'] + api_port = self.module.params['api_port'] api_user = self.module.params['api_user'] api_password = self.module.params['api_password'] api_token_id = self.module.params['api_token_id'] @@ -89,6 +93,10 @@ class ProxmoxAnsible(object): validate_certs = self.module.params['validate_certs'] auth_args = {'user': api_user} + + if api_port: + auth_args['port'] = api_port + if api_password: auth_args['password'] = api_password else: diff --git a/ansible_collections/community/general/plugins/module_utils/redfish_utils.py b/ansible_collections/community/general/plugins/module_utils/redfish_utils.py index 6935573d0..139628bd9 100644 --- a/ansible_collections/community/general/plugins/module_utils/redfish_utils.py +++ b/ansible_collections/community/general/plugins/module_utils/redfish_utils.py @@ -11,6 +11,7 @@ import os import random import string import gzip +import time from io import BytesIO from ansible.module_utils.urls import open_url from ansible.module_utils.common.text.converters import to_native @@ -132,11 +133,13 @@ class RedfishUtils(object): return resp # The following functions are to send GET/POST/PATCH/DELETE requests - def get_request(self, uri, override_headers=None, allow_no_resp=False): + def get_request(self, uri, override_headers=None, allow_no_resp=False, timeout=None): req_headers = dict(GET_HEADERS) if override_headers: req_headers.update(override_headers) username, password, basic_auth = self._auth_params(req_headers) + if timeout is None: + timeout = self.timeout try: # Service root is an unauthenticated resource; remove credentials # in case the caller will be using sessions later. @@ -146,7 +149,7 @@ class RedfishUtils(object): url_username=username, url_password=password, force_basic_auth=basic_auth, validate_certs=False, follow_redirects='all', - use_proxy=True, timeout=self.timeout) + use_proxy=True, timeout=timeout) headers = dict((k.lower(), v) for (k, v) in resp.info().items()) try: if headers.get('content-encoding') == 'gzip' and LooseVersion(ansible_version) < LooseVersion('2.14'): @@ -624,6 +627,24 @@ class RedfishUtils(object): allowable_values = default_values return allowable_values + def check_service_availability(self): + """ + Checks if the service is accessible. + + :return: dict containing the status of the service + """ + + # Get the service root + # Override the timeout since the service root is expected to be readily + # available. + service_root = self.get_request(self.root_uri + self.service_root, timeout=10) + if service_root['ret'] is False: + # Failed, either due to a timeout or HTTP error; not available + return {'ret': True, 'available': False} + + # Successfully accessed the service root; available + return {'ret': True, 'available': True} + def get_logs(self): log_svcs_uri_list = [] list_of_logs = [] @@ -1083,11 +1104,12 @@ class RedfishUtils(object): return self.manage_power(command, self.systems_uri, '#ComputerSystem.Reset') - def manage_manager_power(self, command): + def manage_manager_power(self, command, wait=False, wait_timeout=120): return self.manage_power(command, self.manager_uri, - '#Manager.Reset') + '#Manager.Reset', wait, wait_timeout) - def manage_power(self, command, resource_uri, action_name): + def manage_power(self, command, resource_uri, action_name, wait=False, + wait_timeout=120): key = "Actions" reset_type_values = ['On', 'ForceOff', 'GracefulShutdown', 'GracefulRestart', 'ForceRestart', 'Nmi', @@ -1147,6 +1169,30 @@ class RedfishUtils(object): response = self.post_request(self.root_uri + action_uri, payload) if response['ret'] is False: return response + + # If requested to wait for the service to be available again, block + # until it's ready + if wait: + elapsed_time = 0 + start_time = time.time() + # Start with a large enough sleep. Some services will process new + # requests while in the middle of shutting down, thus breaking out + # early. + time.sleep(30) + + # Periodically check for the service's availability. + while elapsed_time <= wait_timeout: + status = self.check_service_availability() + if status['available']: + # It's available; we're done + break + time.sleep(5) + elapsed_time = time.time() - start_time + + if elapsed_time > wait_timeout: + # Exhausted the wait timer; error + return {'ret': False, 'changed': True, + 'msg': 'The service did not become available after %d seconds' % wait_timeout} return {'ret': True, 'changed': True} def manager_reset_to_defaults(self, command): diff --git a/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py b/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py index d382ed93a..b0f3aeb5d 100644 --- a/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py +++ b/ansible_collections/community/general/plugins/modules/ansible_galaxy_install.py @@ -32,6 +32,19 @@ attributes: diff_mode: support: none options: + state: + description: + - > + If O(state=present) then the collection or role will be installed. + Note that the collections and roles are not updated with this option. + - > + Currently the O(state=latest) is ignored unless O(type=collection), and it will + ensure the collection is installed and updated to the latest available version. + - Please note that O(force=true) can be used to perform upgrade regardless of O(type). + type: str + choices: [ present, latest ] + default: present + version_added: 9.1.0 type: description: - The type of installation performed by C(ansible-galaxy). @@ -69,7 +82,8 @@ options: default: false force: description: - - Force overwriting an existing role or collection. + - Force overwriting existing roles and/or collections. + - It can be used for upgrading, but the module output will always report C(changed=true). - Using O(force=true) is mandatory when downgrading. type: bool default: false @@ -188,6 +202,7 @@ class AnsibleGalaxyInstall(ModuleHelper): output_params = ('type', 'name', 'dest', 'requirements_file', 'force', 'no_deps') module = dict( argument_spec=dict( + state=dict(type='str', choices=['present', 'latest'], default='present'), type=dict(type='str', choices=('collection', 'role', 'both'), required=True), name=dict(type='str'), requirements_file=dict(type='path'), @@ -206,6 +221,7 @@ class AnsibleGalaxyInstall(ModuleHelper): command_args_formats = dict( type=cmd_runner_fmt.as_func(lambda v: [] if v == 'both' else [v]), galaxy_cmd=cmd_runner_fmt.as_list(), + upgrade=cmd_runner_fmt.as_bool("--upgrade"), requirements_file=cmd_runner_fmt.as_opt_val('-r'), dest=cmd_runner_fmt.as_opt_val('-p'), force=cmd_runner_fmt.as_bool("--force"), @@ -244,9 +260,7 @@ class AnsibleGalaxyInstall(ModuleHelper): def __init_module__(self): self.runner, self.ansible_version = self._get_ansible_galaxy_version() if self.ansible_version < (2, 11): - self.module.fail_json( - msg="Support for Ansible 2.9 and ansible-base 2.10 has been removed." - ) + self.module.fail_json(msg="Support for Ansible 2.9 and ansible-base 2.10 has been removed.") self.vars.set("new_collections", {}, change=True) self.vars.set("new_roles", {}, change=True) if self.vars.type != "collection": @@ -299,8 +313,9 @@ class AnsibleGalaxyInstall(ModuleHelper): elif match.group("role"): self.vars.new_roles[match.group("role")] = match.group("rversion") - with self.runner("type galaxy_cmd force no_deps dest requirements_file name", output_process=process) as ctx: - ctx.run(galaxy_cmd="install") + upgrade = (self.vars.type == "collection" and self.vars.state == "latest") + with self.runner("type galaxy_cmd upgrade force no_deps dest requirements_file name", output_process=process) as ctx: + ctx.run(galaxy_cmd="install", upgrade=upgrade) if self.verbosity > 2: self.vars.set("run_info", ctx.run_info) diff --git a/ansible_collections/community/general/plugins/modules/cargo.py b/ansible_collections/community/general/plugins/modules/cargo.py index ba9c05ed7..2fc729da2 100644 --- a/ansible_collections/community/general/plugins/modules/cargo.py +++ b/ansible_collections/community/general/plugins/modules/cargo.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2021 Radek Sprta <mail@radeksprta.eu> +# Copyright (c) 2024 Colin Nolan <cn580@alumni.york.ac.uk> # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later @@ -65,6 +66,13 @@ options: type: str default: present choices: [ "present", "absent", "latest" ] + directory: + description: + - Path to the source directory to install the Rust package from. + - This is only used when installing packages. + type: path + required: false + version_added: 9.1.0 requirements: - cargo installed """ @@ -98,8 +106,14 @@ EXAMPLES = r""" community.general.cargo: name: ludusavi state: latest + +- name: Install "ludusavi" Rust package from source directory + community.general.cargo: + name: ludusavi + directory: /path/to/ludusavi/source """ +import json import os import re @@ -115,6 +129,7 @@ class Cargo(object): self.state = kwargs["state"] self.version = kwargs["version"] self.locked = kwargs["locked"] + self.directory = kwargs["directory"] @property def path(self): @@ -143,7 +158,7 @@ class Cargo(object): data, dummy = self._exec(cmd, True, False, False) - package_regex = re.compile(r"^([\w\-]+) v(.+):$") + package_regex = re.compile(r"^([\w\-]+) v(\S+).*:$") installed = {} for line in data.splitlines(): package_info = package_regex.match(line) @@ -163,19 +178,53 @@ class Cargo(object): if self.version: cmd.append("--version") cmd.append(self.version) + if self.directory: + cmd.append("--path") + cmd.append(self.directory) return self._exec(cmd) def is_outdated(self, name): installed_version = self.get_installed().get(name) + latest_version = ( + self.get_latest_published_version(name) + if not self.directory + else self.get_source_directory_version(name) + ) + return installed_version != latest_version + def get_latest_published_version(self, name): cmd = ["search", name, "--limit", "1"] data, dummy = self._exec(cmd, True, False, False) match = re.search(r'"(.+)"', data) - if match: - latest_version = match.group(1) - - return installed_version != latest_version + if not match: + self.module.fail_json( + msg="No published version for package %s found" % name + ) + return match.group(1) + + def get_source_directory_version(self, name): + cmd = [ + "metadata", + "--format-version", + "1", + "--no-deps", + "--manifest-path", + os.path.join(self.directory, "Cargo.toml"), + ] + data, dummy = self._exec(cmd, True, False, False) + manifest = json.loads(data) + + package = next( + (package for package in manifest["packages"] if package["name"] == name), + None, + ) + if not package: + self.module.fail_json( + msg="Package %s not defined in source, found: %s" + % (name, [x["name"] for x in manifest["packages"]]) + ) + return package["version"] def uninstall(self, packages=None): cmd = ["uninstall"] @@ -191,16 +240,21 @@ def main(): state=dict(default="present", choices=["present", "absent", "latest"]), version=dict(default=None, type="str"), locked=dict(default=False, type="bool"), + directory=dict(default=None, type="path"), ) module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True) name = module.params["name"] state = module.params["state"] version = module.params["version"] + directory = module.params["directory"] if not name: module.fail_json(msg="Package name must be specified") + if directory is not None and not os.path.isdir(directory): + module.fail_json(msg="Source directory does not exist") + # Set LANG env since we parse stdout module.run_command_environ_update = dict( LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C" diff --git a/ansible_collections/community/general/plugins/modules/consul_agent_check.py b/ansible_collections/community/general/plugins/modules/consul_agent_check.py new file mode 100644 index 000000000..373926004 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_agent_check.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Michael Ilg +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +module: consul_agent_check +short_description: Add, modify, and delete checks within a consul cluster +version_added: 9.1.0 +description: + - Allows the addition, modification and deletion of checks in a consul + cluster via the agent. For more details on using and configuring Checks, + see U(https://developer.hashicorp.com/consul/api-docs/agent/check). + - Currently, there is no complete way to retrieve the script, interval or TTL + metadata for a registered check. Without this metadata it is not possible to + tell if the data supplied with ansible represents a change to a check. As a + result this does not attempt to determine changes and will always report a + changed occurred. An API method is planned to supply this metadata so at that + stage change management will be added. +author: + - Michael Ilg (@Ilgmi) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + details: + - The result is the object as it is defined in the module options and not the object structure of the consul API. + For a better overview of what the object structure looks like, + take a look at U(https://developer.hashicorp.com/consul/api-docs/agent/check#list-checks). + diff_mode: + support: partial + details: + - In check mode the diff will show the object as it is defined in the module options and not the object structure of the consul API. +options: + state: + description: + - Whether the check should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Required name for the service check. + type: str + id: + description: + - Specifies a unique ID for this check on the node. This defaults to the O(name) parameter, but it may be necessary to provide + an ID for uniqueness. This value will return in the response as "CheckId". + type: str + interval: + description: + - The interval at which the service check will be run. + This is a number with a V(s) or V(m) suffix to signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Required if one of the parameters O(args), O(http), or O(tcp) is specified. + type: str + notes: + description: + - Notes to attach to check when registering it. + type: str + args: + description: + - Specifies command arguments to run to update the status of the check. + - Requires O(interval) to be provided. + - Mutually exclusive with O(ttl), O(tcp) and O(http). + type: list + elements: str + ttl: + description: + - Checks can be registered with a TTL instead of a O(args) and O(interval) + this means that the service will check in with the agent before the + TTL expires. If it doesn't the check will be considered failed. + Required if registering a check and the script an interval are missing + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Mutually exclusive with O(args), O(tcp) and O(http). + type: str + tcp: + description: + - Checks can be registered with a TCP port. This means that consul + will check if the connection attempt to that port is successful (that is, the port is currently accepting connections). + The format is V(host:port), for example V(localhost:80). + - Requires O(interval) to be provided. + - Mutually exclusive with O(args), O(ttl) and O(http). + type: str + version_added: '1.3.0' + http: + description: + - Checks can be registered with an HTTP endpoint. This means that consul + will check that the http endpoint returns a successful HTTP status. + - Requires O(interval) to be provided. + - Mutually exclusive with O(args), O(ttl) and O(tcp). + type: str + timeout: + description: + - A custom HTTP check timeout. The consul default is 10 seconds. + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + type: str + service_id: + description: + - The ID for the service, must be unique per node. If O(state=absent), + defaults to the service name if supplied. + type: str +''' + +EXAMPLES = ''' +- name: Register tcp check for service 'nginx' + community.general.consul_agent_check: + name: nginx_tcp_check + service_id: nginx + interval: 60s + tcp: localhost:80 + notes: "Nginx Check" + +- name: Register http check for service 'nginx' + community.general.consul_agent_check: + name: nginx_http_check + service_id: nginx + interval: 60s + http: http://localhost:80/status + notes: "Nginx Check" + +- name: Remove check for service 'nginx' + community.general.consul_agent_check: + state: absent + id: nginx_http_check + service_id: "{{ nginx_service.ID }}" +''' + +RETURN = """ +check: + description: The check as returned by the consul HTTP API. + returned: always + type: dict + sample: + CheckID: nginx_check + ServiceID: nginx + Interval: 30s + Type: http + Notes: Nginx Check +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_CREATE, + OPERATION_UPDATE, + OPERATION_DELETE, + OPERATION_READ, + _ConsulModule, + validate_check, +) + +_ARGUMENT_SPEC = { + "state": dict(default="present", choices=["present", "absent"]), + "name": dict(type='str'), + "id": dict(type='str'), + "interval": dict(type='str'), + "notes": dict(type='str'), + "args": dict(type='list', elements='str'), + "http": dict(type='str'), + "tcp": dict(type='str'), + "ttl": dict(type='str'), + "timeout": dict(type='str'), + "service_id": dict(type='str'), +} + +_MUTUALLY_EXCLUSIVE = [ + ('args', 'ttl', 'tcp', 'http'), +] + +_REQUIRED_IF = [ + ('state', 'present', ['name']), + ('state', 'absent', ('id', 'name'), True), +] + +_REQUIRED_BY = { + 'args': 'interval', + 'http': 'interval', + 'tcp': 'interval', +} + +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +class ConsulAgentCheckModule(_ConsulModule): + api_endpoint = "agent/check" + result_key = "check" + unique_identifiers = ["id", "name"] + operational_attributes = {"Node", "CheckID", "Output", "ServiceName", "ServiceTags", + "Status", "Type", "ExposedPort", "Definition"} + + def endpoint_url(self, operation, identifier=None): + if operation == OPERATION_READ: + return "agent/checks" + if operation in [OPERATION_CREATE, OPERATION_UPDATE]: + return "/".join([self.api_endpoint, "register"]) + if operation == OPERATION_DELETE: + return "/".join([self.api_endpoint, "deregister", identifier]) + + return super(ConsulAgentCheckModule, self).endpoint_url(operation, identifier) + + def read_object(self): + url = self.endpoint_url(OPERATION_READ) + checks = self.get(url) + identifier = self.id_from_obj(self.params) + if identifier in checks: + return checks[identifier] + return None + + def prepare_object(self, existing, obj): + existing = super(ConsulAgentCheckModule, self).prepare_object(existing, obj) + validate_check(existing) + return existing + + def delete_object(self, obj): + if not self._module.check_mode: + self.put(self.endpoint_url(OPERATION_DELETE, obj.get("CheckID"))) + return {} + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + mutually_exclusive=_MUTUALLY_EXCLUSIVE, + required_if=_REQUIRED_IF, + required_by=_REQUIRED_BY, + supports_check_mode=True, + ) + + consul_module = ConsulAgentCheckModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_agent_service.py b/ansible_collections/community/general/plugins/modules/consul_agent_service.py new file mode 100644 index 000000000..a8ef09897 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/consul_agent_service.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Michael Ilg +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +module: consul_agent_service +short_description: Add, modify and delete services within a consul cluster +version_added: 9.1.0 +description: + - Allows the addition, modification and deletion of services in a consul + cluster via the agent. + - There are currently no plans to create services and checks in one. + This is because the Consul API does not provide checks for a service and + the checks themselves do not match the module parameters. + Therefore, only a service without checks can be created in this module. +author: + - Michael Ilg (@Ilgmi) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the service should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Unique name for the service on a node, must be unique per node, + required if registering a service. + type: str + id: + description: + - Specifies a unique ID for this service. This must be unique per agent. This defaults to the O(name) parameter if not provided. + If O(state=absent), defaults to the service name if supplied. + type: str + tags: + description: + - Tags that will be attached to the service registration. + type: list + elements: str + address: + description: + - The address to advertise that the service will be listening on. + This value will be passed as the C(address) parameter to Consul's + C(/v1/agent/service/register) API method, so refer to the Consul API + documentation for further details. + type: str + meta: + description: + - Optional meta data used for filtering. + For keys, the characters C(A-Z), C(a-z), C(0-9), C(_), C(-) are allowed. + Not allowed characters are replaced with underscores. + type: dict + service_port: + description: + - The port on which the service is listening. Can optionally be supplied for + registration of a service, that is if O(name) or O(id) is set. + type: int + enable_tag_override: + description: + - Specifies to disable the anti-entropy feature for this service's tags. + If EnableTagOverride is set to true then external agents can update this service in the catalog and modify the tags. + type: bool + default: False + weights: + description: + - Specifies weights for the service + type: dict + suboptions: + passing: + description: + - Weights for passing. + type: int + default: 1 + warning: + description: + - Weights for warning. + type: int + default: 1 + default: {"passing": 1, "warning": 1} +''' + +EXAMPLES = ''' +- name: Register nginx service with the local consul agent + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register nginx with a tcp check + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register nginx with an http check + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register external service nginx available at 10.1.5.23 + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + address: 10.1.5.23 + +- name: Register nginx with some service tags + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + tags: + - prod + - webservers + +- name: Register nginx with some service meta + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + meta: + nginx_version: 1.25.3 + +- name: Remove nginx service + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + service_id: nginx + state: absent + +- name: Register celery worker service + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: celery-worker + tags: + - prod + - worker +''' + +RETURN = """ +service: + description: The service as returned by the consul HTTP API. + returned: always + type: dict + sample: + ID: nginx + Service: nginx + Address: localhost + Port: 80 + Tags: + - http + Meta: + - nginx_version: 1.23.3 + Datacenter: dc1 + Weights: + Passing: 1 + Warning: 1 + ContentHash: 61a245cd985261ac + EnableTagOverride: false +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_CREATE, + OPERATION_UPDATE, + OPERATION_DELETE, + _ConsulModule +) + +_CHECK_MUTUALLY_EXCLUSIVE = [('args', 'ttl', 'tcp', 'http')] +_CHECK_REQUIRED_BY = { + 'args': 'interval', + 'http': 'interval', + 'tcp': 'interval', +} + +_ARGUMENT_SPEC = { + "state": dict(default="present", choices=["present", "absent"]), + "name": dict(type='str'), + "id": dict(type='str'), + "tags": dict(type='list', elements='str'), + "address": dict(type='str'), + "meta": dict(type='dict'), + "service_port": dict(type='int'), + "enable_tag_override": dict(type='bool', default=False), + "weights": dict(type='dict', options=dict( + passing=dict(type='int', default=1, no_log=False), + warning=dict(type='int', default=1) + ), default={"passing": 1, "warning": 1}) +} + +_REQUIRED_IF = [ + ('state', 'present', ['name']), + ('state', 'absent', ('id', 'name'), True), +] + +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +class ConsulAgentServiceModule(_ConsulModule): + api_endpoint = "agent/service" + result_key = "service" + unique_identifiers = ["id", "name"] + operational_attributes = {"Service", "ContentHash", "Datacenter"} + + def endpoint_url(self, operation, identifier=None): + if operation in [OPERATION_CREATE, OPERATION_UPDATE]: + return "/".join([self.api_endpoint, "register"]) + if operation == OPERATION_DELETE: + return "/".join([self.api_endpoint, "deregister", identifier]) + + return super(ConsulAgentServiceModule, self).endpoint_url(operation, identifier) + + def prepare_object(self, existing, obj): + existing = super(ConsulAgentServiceModule, self).prepare_object(existing, obj) + if "ServicePort" in existing: + existing["Port"] = existing.pop("ServicePort") + + if "ID" not in existing: + existing["ID"] = existing["Name"] + + return existing + + def needs_update(self, api_obj, module_obj): + obj = {} + if "Service" in api_obj: + obj["Service"] = api_obj["Service"] + api_obj = self.prepare_object(api_obj, obj) + + if "Name" in module_obj: + module_obj["Service"] = module_obj.pop("Name") + if "ServicePort" in module_obj: + module_obj["Port"] = module_obj.pop("ServicePort") + + return super(ConsulAgentServiceModule, self).needs_update(api_obj, module_obj) + + def delete_object(self, obj): + if not self._module.check_mode: + url = self.endpoint_url(OPERATION_DELETE, self.id_from_obj(obj, camel_case=True)) + self.put(url) + return {} + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + required_if=_REQUIRED_IF, + supports_check_mode=True, + ) + + consul_module = ConsulAgentServiceModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/community/general/plugins/modules/consul_auth_method.py b/ansible_collections/community/general/plugins/modules/consul_auth_method.py index afe549f6e..e28474c31 100644 --- a/ansible_collections/community/general/plugins/modules/consul_auth_method.py +++ b/ansible_collections/community/general/plugins/modules/consul_auth_method.py @@ -168,7 +168,7 @@ def normalize_ttl(ttl): class ConsulAuthMethodModule(_ConsulModule): api_endpoint = "acl/auth-method" result_key = "auth_method" - unique_identifier = "name" + unique_identifiers = ["name"] def map_param(self, k, v, is_update): if k == "config" and v: diff --git a/ansible_collections/community/general/plugins/modules/consul_binding_rule.py b/ansible_collections/community/general/plugins/modules/consul_binding_rule.py index 88496f867..6a2882cee 100644 --- a/ansible_collections/community/general/plugins/modules/consul_binding_rule.py +++ b/ansible_collections/community/general/plugins/modules/consul_binding_rule.py @@ -124,7 +124,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import ( class ConsulBindingRuleModule(_ConsulModule): api_endpoint = "acl/binding-rule" result_key = "binding_rule" - unique_identifier = "id" + unique_identifiers = ["id"] def read_object(self): url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"]) diff --git a/ansible_collections/community/general/plugins/modules/consul_policy.py b/ansible_collections/community/general/plugins/modules/consul_policy.py index 2ed6021b0..36139ac09 100644 --- a/ansible_collections/community/general/plugins/modules/consul_policy.py +++ b/ansible_collections/community/general/plugins/modules/consul_policy.py @@ -145,7 +145,7 @@ _ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) class ConsulPolicyModule(_ConsulModule): api_endpoint = "acl/policy" result_key = "policy" - unique_identifier = "id" + unique_identifiers = ["id"] def endpoint_url(self, operation, identifier=None): if operation == OPERATION_READ: diff --git a/ansible_collections/community/general/plugins/modules/consul_role.py b/ansible_collections/community/general/plugins/modules/consul_role.py index e07e2036f..d6c4e4dd9 100644 --- a/ansible_collections/community/general/plugins/modules/consul_role.py +++ b/ansible_collections/community/general/plugins/modules/consul_role.py @@ -212,7 +212,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import ( class ConsulRoleModule(_ConsulModule): api_endpoint = "acl/role" result_key = "role" - unique_identifier = "id" + unique_identifiers = ["id"] def endpoint_url(self, operation, identifier=None): if operation == OPERATION_READ: diff --git a/ansible_collections/community/general/plugins/modules/consul_token.py b/ansible_collections/community/general/plugins/modules/consul_token.py index 02bc544da..c8bc8bc27 100644 --- a/ansible_collections/community/general/plugins/modules/consul_token.py +++ b/ansible_collections/community/general/plugins/modules/consul_token.py @@ -235,13 +235,13 @@ def normalize_link_obj(api_obj, module_obj, key): class ConsulTokenModule(_ConsulModule): api_endpoint = "acl/token" result_key = "token" - unique_identifier = "accessor_id" + unique_identifiers = ["accessor_id"] create_only_fields = {"expiration_ttl"} def read_object(self): # if `accessor_id` is not supplied we can only create objects and are not idempotent - if not self.params.get(self.unique_identifier): + if not self.id_from_obj(self.params): return None return super(ConsulTokenModule, self).read_object() diff --git a/ansible_collections/community/general/plugins/modules/django_check.py b/ansible_collections/community/general/plugins/modules/django_check.py new file mode 100644 index 000000000..1553da7a3 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/django_check.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Alexei Znamensky <russoz@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +module: django_check +author: + - Alexei Znamensky (@russoz) +short_description: Wrapper for C(django-admin check) +version_added: 9.1.0 +description: + - This module is a wrapper for the execution of C(django-admin check). +extends_documentation_fragment: + - community.general.attributes + - community.general.django +options: + database: + description: + - Specify databases to run checks against. + - If not specified, Django will not run database tests. + type: list + elements: str + deploy: + description: + - Include additional checks relevant in a deployment setting. + type: bool + default: false + fail_level: + description: + - Message level that will trigger failure. + - Default is the Django default value. Check the documentation for the version being used. + type: str + choices: [CRITICAL, ERROR, WARNING, INFO, DEBUG] + tags: + description: + - Restrict checks to specific tags. + type: list + elements: str + apps: + description: + - Restrict checks to specific applications. + - Default is to check all applications. + type: list + elements: str +notes: + - The outcome of the module is found in the common return values RV(ignore:stdout), RV(ignore:stderr), RV(ignore:rc). + - The module will fail if RV(ignore:rc) is not zero. +attributes: + check_mode: + support: full + diff_mode: + support: none +""" + +EXAMPLES = """ +- name: Check the entire project + community.general.django_check: + settings: myproject.settings + +- name: Create the project using specific databases + community.general.django_check: + database: + - somedb + - myotherdb + settings: fancysite.settings + pythonpath: /home/joedoe/project/fancysite + venv: /home/joedoe/project/fancysite/venv +""" + +RETURN = """ +run_info: + description: Command-line execution information. + type: dict + returned: success and C(verbosity) >= 3 +""" + +from ansible_collections.community.general.plugins.module_utils.django import DjangoModuleHelper +from ansible_collections.community.general.plugins.module_utils.cmd_runner import cmd_runner_fmt + + +class DjangoCheck(DjangoModuleHelper): + module = dict( + argument_spec=dict( + database=dict(type="list", elements="str"), + deploy=dict(type="bool", default=False), + fail_level=dict(type="str", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]), + tags=dict(type="list", elements="str"), + apps=dict(type="list", elements="str"), + ), + supports_check_mode=True, + ) + arg_formats = dict( + database=cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_val)("--database"), + deploy=cmd_runner_fmt.as_bool("--deploy"), + fail_level=cmd_runner_fmt.as_opt_val("--fail-level"), + tags=cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_val)("--tag"), + apps=cmd_runner_fmt.as_list(), + ) + django_admin_cmd = "check" + django_admin_arg_order = "database deploy fail_level tags apps" + + +def main(): + DjangoCheck.execute() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/django_createcachetable.py b/ansible_collections/community/general/plugins/modules/django_createcachetable.py new file mode 100644 index 000000000..b038e0358 --- /dev/null +++ b/ansible_collections/community/general/plugins/modules/django_createcachetable.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Alexei Znamensky <russoz@gmail.com> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +module: django_createcachetable +author: + - Alexei Znamensky (@russoz) +short_description: Wrapper for C(django-admin createcachetable) +version_added: 9.1.0 +description: + - This module is a wrapper for the execution of C(django-admin createcachetable). +extends_documentation_fragment: + - community.general.attributes + - community.general.django + - community.general.django.database +attributes: + check_mode: + support: full + diff_mode: + support: none +""" + +EXAMPLES = """ +- name: Create cache table in the default database + community.general.django_createcachetable: + settings: myproject.settings + +- name: Create cache table in the other database + community.general.django_createcachetable: + database: myotherdb + settings: fancysite.settings + pythonpath: /home/joedoe/project/fancysite + venv: /home/joedoe/project/fancysite/venv +""" + +RETURN = """ +run_info: + description: Command-line execution information. + type: dict + returned: success and O(verbosity) >= 3 +""" + +from ansible_collections.community.general.plugins.module_utils.django import DjangoModuleHelper + + +class DjangoCreateCacheTable(DjangoModuleHelper): + module = dict( + supports_check_mode=True, + ) + django_admin_cmd = "createcachetable" + django_admin_arg_order = "noinput database dry_run" + _django_args = ["noinput", "database", "dry_run"] + _check_mode_arg = "dry_run" + + +def main(): + DjangoCreateCacheTable.execute() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/general/plugins/modules/git_config.py b/ansible_collections/community/general/plugins/modules/git_config.py index a8d2ebe97..95969c1b3 100644 --- a/ansible_collections/community/general/plugins/modules/git_config.py +++ b/ansible_collections/community/general/plugins/modules/git_config.py @@ -18,7 +18,7 @@ author: - Matthew Gamble (@djmattyg007) - Marius Gedminas (@mgedmin) requirements: ['git'] -short_description: Read and write git configuration +short_description: Update git configuration description: - The M(community.general.git_config) module changes git configuration by invoking C(git config). This is needed if you do not want to use M(ansible.builtin.template) for the entire git @@ -36,6 +36,8 @@ options: list_all: description: - List all settings (optionally limited to a given O(scope)). + - This option is B(deprecated) and will be removed from community.general 11.0.0. + Please use M(community.general.git_config_info) instead. type: bool default: false name: @@ -74,6 +76,8 @@ options: description: - When specifying the name of a single setting, supply a value to set that setting to the given value. + - From community.general 11.0.0 on, O(value) will be required if O(state=present). + To read values, use the M(community.general.git_config_info) module instead. type: str add_mode: description: @@ -143,29 +147,6 @@ EXAMPLES = ''' repo: /etc scope: local value: 'root@{{ ansible_fqdn }}' - -- name: Read individual values from git config - community.general.git_config: - name: alias.ci - scope: global - -- name: Scope system is also assumed when reading values, unless list_all=true - community.general.git_config: - name: alias.diffc - -- name: Read all values from git config - community.general.git_config: - list_all: true - scope: global - -- name: When list_all is yes and no scope is specified, you get configuration from all scopes - community.general.git_config: - list_all: true - -- name: Specify a repository to include local settings - community.general.git_config: - list_all: true - repo: /path/to/repo.git ''' RETURN = ''' @@ -193,7 +174,7 @@ from ansible.module_utils.basic import AnsibleModule def main(): module = AnsibleModule( argument_spec=dict( - list_all=dict(required=False, type='bool', default=False), + list_all=dict(required=False, type='bool', default=False, removed_in_version='11.0.0', removed_from_collection='community.general'), name=dict(type='str'), repo=dict(type='path'), file=dict(type='path'), @@ -222,6 +203,14 @@ def main(): new_value = params['value'] or '' add_mode = params['add_mode'] + if not unset and not new_value and not params['list_all']: + module.deprecate( + 'If state=present, a value must be specified from community.general 11.0.0 on.' + ' To read a config value, use the community.general.git_config_info module instead.', + version='11.0.0', + collection_name='community.general', + ) + scope = determine_scope(params) cwd = determine_cwd(scope, params) @@ -263,7 +252,7 @@ def main(): module.exit_json(changed=False, msg='', config_value=old_values[0] if old_values else '') elif unset and not out: module.exit_json(changed=False, msg='no setting to unset') - elif new_value in old_values and (len(old_values) == 1 or add_mode == "add"): + elif new_value in old_values and (len(old_values) == 1 or add_mode == "add") and not unset: module.exit_json(changed=False, msg="") # Until this point, the git config was just read and in case no change is needed, the module has already exited. diff --git a/ansible_collections/community/general/plugins/modules/homectl.py b/ansible_collections/community/general/plugins/modules/homectl.py index ca4c19a87..7751651c8 100644 --- a/ansible_collections/community/general/plugins/modules/homectl.py +++ b/ansible_collections/community/general/plugins/modules/homectl.py @@ -17,6 +17,12 @@ short_description: Manage user accounts with systemd-homed version_added: 4.4.0 description: - Manages a user's home directory managed by systemd-homed. +notes: + - This module does B(not) work with Python 3.13 or newer. It uses the deprecated L(crypt Python module, + https://docs.python.org/3.12/library/crypt.html) from the Python standard library, which was removed + from Python 3.13. +requirements: + - Python 3.12 or earlier extends_documentation_fragment: - community.general.attributes attributes: @@ -263,12 +269,21 @@ data: } ''' -import crypt import json -from ansible.module_utils.basic import AnsibleModule +import traceback +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.basic import jsonify from ansible.module_utils.common.text.formatters import human_to_bytes +try: + import crypt +except ImportError: + HAS_CRYPT = False + CRYPT_IMPORT_ERROR = traceback.format_exc() +else: + HAS_CRYPT = True + CRYPT_IMPORT_ERROR = None + class Homectl(object): '''#TODO DOC STRINGS''' @@ -591,6 +606,12 @@ def main(): ] ) + if not HAS_CRYPT: + module.fail_json( + msg=missing_required_lib('crypt (part of Python 3.13 standard library)'), + exception=CRYPT_IMPORT_ERROR, + ) + homectl = Homectl(module) homectl.result['state'] = homectl.state diff --git a/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py b/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py index cb4ce03dd..59475a55b 100644 --- a/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py +++ b/ansible_collections/community/general/plugins/modules/ipa_dnsrecord.py @@ -35,13 +35,14 @@ options: record_type: description: - The type of DNS record name. - - Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV' and 'MX' are supported. + - Currently, 'A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV', 'MX' and 'SSHFP' are supported. - "'A6', 'CNAME', 'DNAME' and 'TXT' are added in version 2.5." - "'SRV' and 'MX' are added in version 2.8." - "'NS' are added in comunity.general 8.2.0." + - "'SSHFP' are added in community.general 9.1.0." required: false default: 'A' - choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT'] + choices: ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT', 'SSHFP'] type: str record_value: description: @@ -57,6 +58,7 @@ options: - In the case of 'TXT' record type, this will be a text. - In the case of 'SRV' record type, this will be a service record. - In the case of 'MX' record type, this will be a mail exchanger record. + - In the case of 'SSHFP' record type, this will be an SSH fingerprint record. type: str record_values: description: @@ -71,6 +73,7 @@ options: - In the case of 'TXT' record type, this will be a text. - In the case of 'SRV' record type, this will be a service record. - In the case of 'MX' record type, this will be a mail exchanger record. + - In the case of 'SSHFP' record type, this will be an SSH fingerprint record. type: list elements: str record_ttl: @@ -175,6 +178,20 @@ EXAMPLES = r''' ipa_host: ipa.example.com ipa_user: admin ipa_pass: ChangeMe! + +- name: Retrieve the current sshfp fingerprints + ansible.builtin.command: ssh-keyscan -D localhost + register: ssh_hostkeys + +- name: Update the SSHFP records in DNS + community.general.ipa_dnsrecord: + name: "{{ inventory_hostname}}" + zone_name: example.com + record_type: 'SSHFP' + record_values: "{{ ssh_hostkeys.stdout.split('\n') | map('split', 'SSHFP ') | map('last') | list }}" + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: ChangeMe! ''' RETURN = r''' @@ -228,6 +245,8 @@ class DNSRecordIPAClient(IPAClient): item.update(srvrecord=value) elif details['record_type'] == 'MX': item.update(mxrecord=value) + elif details['record_type'] == 'SSHFP': + item.update(sshfprecord=value) self._post_json(method='dnsrecord_add', name=zone_name, item=item) @@ -266,6 +285,8 @@ def get_dnsrecord_dict(details=None): module_dnsrecord.update(srvrecord=details['record_values']) elif details['record_type'] == 'MX' and details['record_values']: module_dnsrecord.update(mxrecord=details['record_values']) + elif details['record_type'] == 'SSHFP' and details['record_values']: + module_dnsrecord.update(sshfprecord=details['record_values']) if details.get('record_ttl'): module_dnsrecord.update(dnsttl=details['record_ttl']) @@ -328,7 +349,7 @@ def ensure(module, client): def main(): - record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV', 'MX'] + record_types = ['A', 'AAAA', 'A6', 'CNAME', 'DNAME', 'NS', 'PTR', 'TXT', 'SRV', 'MX', 'SSHFP'] argument_spec = ipa_argument_spec() argument_spec.update( zone_name=dict(type='str', required=True), diff --git a/ansible_collections/community/general/plugins/modules/keycloak_client.py b/ansible_collections/community/general/plugins/modules/keycloak_client.py index 3628e5a51..efaa66e26 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_client.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_client.py @@ -340,6 +340,42 @@ options: description: - Override realm authentication flow bindings. type: dict + suboptions: + browser: + description: + - Flow ID of the browser authentication flow. + - O(authentication_flow_binding_overrides.browser) + and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. + type: str + + browser_name: + description: + - Flow name of the browser authentication flow. + - O(authentication_flow_binding_overrides.browser) + and O(authentication_flow_binding_overrides.browser_name) are mutually exclusive. + aliases: + - browserName + type: str + version_added: 9.1.0 + + direct_grant: + description: + - Flow ID of the direct grant authentication flow. + - O(authentication_flow_binding_overrides.direct_grant) + and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. + aliases: + - directGrant + type: str + + direct_grant_name: + description: + - Flow name of the direct grant authentication flow. + - O(authentication_flow_binding_overrides.direct_grant) + and O(authentication_flow_binding_overrides.direct_grant_name) are mutually exclusive. + aliases: + - directGrantName + type: str + version_added: 9.1.0 aliases: - authenticationFlowBindingOverrides version_added: 3.4.0 @@ -781,6 +817,64 @@ def sanitize_cr(clientrep): return normalise_cr(result) +def get_authentication_flow_id(flow_name, realm, kc): + """ Get the authentication flow ID based on the flow name, realm, and Keycloak client. + + Args: + flow_name (str): The name of the authentication flow. + realm (str): The name of the realm. + kc (KeycloakClient): The Keycloak client instance. + + Returns: + str: The ID of the authentication flow. + + Raises: + KeycloakAPIException: If the authentication flow with the given name is not found in the realm. + """ + flow = kc.get_authentication_flow_by_alias(flow_name, realm) + if flow: + return flow["id"] + kc.module.fail_json(msg='Authentification flow %s not found in realm %s' % (flow_name, realm)) + + +def flow_binding_from_dict_to_model(newClientFlowBinding, realm, kc): + """ Convert a dictionary representing client flow bindings to a model representation. + + Args: + newClientFlowBinding (dict): A dictionary containing client flow bindings. + realm (str): The name of the realm. + kc (KeycloakClient): An instance of the KeycloakClient class. + + Returns: + dict: A dictionary representing the model flow bindings. The dictionary has two keys: + - "browser" (str or None): The ID of the browser authentication flow binding, or None if not provided. + - "direct_grant" (str or None): The ID of the direct grant authentication flow binding, or None if not provided. + + Raises: + KeycloakAPIException: If the authentication flow with the given name is not found in the realm. + + """ + + modelFlow = { + "browser": None, + "direct_grant": None + } + + for k, v in newClientFlowBinding.items(): + if not v: + continue + if k == "browser": + modelFlow["browser"] = v + elif k == "browser_name": + modelFlow["browser"] = get_authentication_flow_id(v, realm, kc) + elif k == "direct_grant": + modelFlow["direct_grant"] = v + elif k == "direct_grant_name": + modelFlow["direct_grant"] = get_authentication_flow_id(v, realm, kc) + + return modelFlow + + def main(): """ Module execution @@ -799,6 +893,13 @@ def main(): config=dict(type='dict'), ) + authentication_flow_spec = dict( + browser=dict(type='str'), + browser_name=dict(type='str', aliases=['browserName']), + direct_grant=dict(type='str', aliases=['directGrant']), + direct_grant_name=dict(type='str', aliases=['directGrantName']), + ) + meta_args = dict( state=dict(default='present', choices=['present', 'absent']), realm=dict(type='str', default='master'), @@ -838,7 +939,13 @@ def main(): use_template_scope=dict(type='bool', aliases=['useTemplateScope']), use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']), always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']), - authentication_flow_binding_overrides=dict(type='dict', aliases=['authenticationFlowBindingOverrides']), + authentication_flow_binding_overrides=dict( + type='dict', + aliases=['authenticationFlowBindingOverrides'], + options=authentication_flow_spec, + required_one_of=[['browser', 'direct_grant', 'browser_name', 'direct_grant_name']], + mutually_exclusive=[['browser', 'browser_name'], ['direct_grant', 'direct_grant_name']], + ), protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']), authorization_settings=dict(type='dict', aliases=['authorizationSettings']), default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']), @@ -900,6 +1007,8 @@ def main(): # they are not specified if client_param == 'protocol_mappers': new_param_value = [dict((k, v) for k, v in x.items() if x[k] is not None) for x in new_param_value] + elif client_param == 'authentication_flow_binding_overrides': + new_param_value = flow_binding_from_dict_to_model(new_param_value, realm, kc) changeset[camel(client_param)] = new_param_value diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py index d24e0f1f2..b962b932c 100644 --- a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py +++ b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py @@ -301,10 +301,37 @@ end_state: ''' from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, camel, \ - keycloak_argument_spec, get_token, KeycloakError + keycloak_argument_spec, get_token, KeycloakError, is_struct_included from ansible.module_utils.basic import AnsibleModule +def normalise_cr(clientscoperep, remove_ids=False): + """ Re-sorts any properties where the order so that diff's is minimised, and adds default values where appropriate so that the + the change detection is more effective. + + :param clientscoperep: the clientscoperep dict to be sanitized + :param remove_ids: If set to true, then the unique ID's of objects is removed to make the diff and checks for changed + not alert when the ID's of objects are not usually known, (e.g. for protocol_mappers) + :return: normalised clientscoperep dict + """ + # Avoid the dict passed in to be modified + clientscoperep = clientscoperep.copy() + + if 'attributes' in clientscoperep: + clientscoperep['attributes'] = list(sorted(clientscoperep['attributes'])) + + if 'protocolMappers' in clientscoperep: + clientscoperep['protocolMappers'] = sorted(clientscoperep['protocolMappers'], key=lambda x: (x.get('name'), x.get('protocol'), x.get('protocolMapper'))) + for mapper in clientscoperep['protocolMappers']: + if remove_ids: + mapper.pop('id', None) + + # Set to a default value. + mapper['consentRequired'] = mapper.get('consentRequired', False) + + return clientscoperep + + def sanitize_cr(clientscoperep): """ Removes probably sensitive details from a clientscoperep representation. @@ -317,7 +344,7 @@ def sanitize_cr(clientscoperep): if 'attributes' in result: if 'saml.signing.private.key' in result['attributes']: result['attributes']['saml.signing.private.key'] = 'no_log' - return result + return normalise_cr(result) def main(): @@ -458,6 +485,13 @@ def main(): result['diff'] = dict(before=sanitize_cr(before_clientscope), after=sanitize_cr(desired_clientscope)) if module.check_mode: + # We can only compare the current clientscope with the proposed updates we have + before_norm = normalise_cr(before_clientscope, remove_ids=True) + desired_norm = normalise_cr(desired_clientscope, remove_ids=True) + if module._diff: + result['diff'] = dict(before=sanitize_cr(before_norm), + after=sanitize_cr(desired_norm)) + result['changed'] = not is_struct_included(desired_norm, before_norm) module.exit_json(**result) # do the update diff --git a/ansible_collections/community/general/plugins/modules/launchd.py b/ansible_collections/community/general/plugins/modules/launchd.py index e5942ea7c..a6427bdb2 100644 --- a/ansible_collections/community/general/plugins/modules/launchd.py +++ b/ansible_collections/community/general/plugins/modules/launchd.py @@ -514,7 +514,8 @@ def main(): result['status']['current_pid'] != result['status']['previous_pid']): result['changed'] = True if module.check_mode: - result['changed'] = True + if result['status']['current_state'] != action: + result['changed'] = True module.exit_json(**result) diff --git a/ansible_collections/community/general/plugins/modules/openbsd_pkg.py b/ansible_collections/community/general/plugins/modules/openbsd_pkg.py index c83113611..69ac7bff8 100644 --- a/ansible_collections/community/general/plugins/modules/openbsd_pkg.py +++ b/ansible_collections/community/general/plugins/modules/openbsd_pkg.py @@ -24,7 +24,10 @@ attributes: check_mode: support: full diff_mode: - support: none + support: partial + version_added: 9.1.0 + details: + - Only works when check mode is not enabled. options: name: description: @@ -159,6 +162,20 @@ def execute_command(cmd, module): return module.run_command(cmd_args, environ_update={'TERM': 'dumb'}) +def get_all_installed(module): + """ + Get all installed packaged. Used to support diff mode + """ + command = 'pkg_info -Iq' + + rc, stdout, stderr = execute_command(command, module) + + if stderr: + module.fail_json(msg="failed in get_all_installed(): %s" % stderr) + + return stdout + + # Function used to find out if a package is currently installed. def get_package_state(names, pkg_spec, module): info_cmd = 'pkg_info -Iq' @@ -573,10 +590,13 @@ def main(): result['name'] = name result['state'] = state result['build'] = build + result['diff'] = {} # The data structure used to keep track of package information. pkg_spec = {} + new_package_list = original_package_list = get_all_installed(module) + if build is True: if not os.path.isdir(ports_dir): module.fail_json(msg="the ports source directory %s does not exist" % (ports_dir)) @@ -661,6 +681,10 @@ def main(): result['changed'] = combined_changed + if result['changed'] and not module.check_mode: + new_package_list = get_all_installed(module) + result['diff'] = dict(before=original_package_list, after=new_package_list) + module.exit_json(**result) diff --git a/ansible_collections/community/general/plugins/modules/pacman.py b/ansible_collections/community/general/plugins/modules/pacman.py index 7f67b9103..f13bde317 100644 --- a/ansible_collections/community/general/plugins/modules/pacman.py +++ b/ansible_collections/community/general/plugins/modules/pacman.py @@ -367,8 +367,9 @@ class Pacman(object): self.install_packages(pkgs) self.success() - # This shouldn't happen... - self.fail("This is a bug") + # This happens if an empty list has been provided for name + self.add_exit_infos(msg='Nothing to do') + self.success() def install_packages(self, pkgs): pkgs_to_install = [] diff --git a/ansible_collections/community/general/plugins/modules/proxmox_kvm.py b/ansible_collections/community/general/plugins/modules/proxmox_kvm.py index 9fe805c7a..71cbb51fc 100644 --- a/ansible_collections/community/general/plugins/modules/proxmox_kvm.py +++ b/ansible_collections/community/general/plugins/modules/proxmox_kvm.py @@ -174,6 +174,7 @@ options: - Allow to force stop VM. - Can be used with states V(stopped), V(restarted), and V(absent). - This option has no default unless O(proxmox_default_behavior) is set to V(compatibility); then the default is V(false). + - Requires parameter O(archive). type: bool format: description: diff --git a/ansible_collections/community/general/plugins/modules/proxmox_vm_info.py b/ansible_collections/community/general/plugins/modules/proxmox_vm_info.py index 39d8307a4..e10b9dff6 100644 --- a/ansible_collections/community/general/plugins/modules/proxmox_vm_info.py +++ b/ansible_collections/community/general/plugins/modules/proxmox_vm_info.py @@ -57,6 +57,13 @@ options: - pending default: none version_added: 8.1.0 + network: + description: + - Whether to retrieve the current network status. + - Requires enabled/running qemu-guest-agent on qemu VMs. + type: bool + default: false + version_added: 9.1.0 extends_documentation_fragment: - community.general.proxmox.actiongroup_proxmox - community.general.proxmox.documentation @@ -172,7 +179,7 @@ class ProxmoxVmInfoAnsible(ProxmoxAnsible): msg="Failed to retrieve VMs information from cluster resources: %s" % e ) - def get_vms_from_nodes(self, cluster_machines, type, vmid=None, name=None, node=None, config=None): + def get_vms_from_nodes(self, cluster_machines, type, vmid=None, name=None, node=None, config=None, network=False): # Leave in dict only machines that user wants to know about filtered_vms = { vm: info for vm, info in cluster_machines.items() if not ( @@ -201,17 +208,23 @@ class ProxmoxVmInfoAnsible(ProxmoxAnsible): config_type = 0 if config == "pending" else 1 # GET /nodes/{node}/qemu/{vmid}/config current=[0/1] desired_vm["config"] = call_vm_getter(this_vm_id).config().get(current=config_type) + if network: + if type == "qemu": + desired_vm["network"] = call_vm_getter(this_vm_id).agent("network-get-interfaces").get()['result'] + elif type == "lxc": + desired_vm["network"] = call_vm_getter(this_vm_id).interfaces.get() + return filtered_vms - def get_qemu_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None): + def get_qemu_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None, network=False): try: - return self.get_vms_from_nodes(cluster_machines, "qemu", vmid, name, node, config) + return self.get_vms_from_nodes(cluster_machines, "qemu", vmid, name, node, config, network) except Exception as e: self.module.fail_json(msg="Failed to retrieve QEMU VMs information: %s" % e) - def get_lxc_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None): + def get_lxc_vms(self, cluster_machines, vmid=None, name=None, node=None, config=None, network=False): try: - return self.get_vms_from_nodes(cluster_machines, "lxc", vmid, name, node, config) + return self.get_vms_from_nodes(cluster_machines, "lxc", vmid, name, node, config, network) except Exception as e: self.module.fail_json(msg="Failed to retrieve LXC VMs information: %s" % e) @@ -229,6 +242,7 @@ def main(): type="str", choices=["none", "current", "pending"], default="none", required=False ), + network=dict(type="bool", default=False, required=False), ) module_args.update(vm_info_args) @@ -245,6 +259,7 @@ def main(): vmid = module.params["vmid"] name = module.params["name"] config = module.params["config"] + network = module.params["network"] result = dict(changed=False) @@ -256,12 +271,12 @@ def main(): vms = {} if type == "lxc": - vms = proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config) + vms = proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config, network) elif type == "qemu": - vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config) + vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config, network) else: - vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config) - vms.update(proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config)) + vms = proxmox.get_qemu_vms(cluster_machines, vmid, name, node, config, network) + vms.update(proxmox.get_lxc_vms(cluster_machines, vmid, name, node, config, network)) result["proxmox_vms"] = [info for vm, info in sorted(vms.items())] module.exit_json(**result) diff --git a/ansible_collections/community/general/plugins/modules/redfish_command.py b/ansible_collections/community/general/plugins/modules/redfish_command.py index d351e7c1d..0f7a64b81 100644 --- a/ansible_collections/community/general/plugins/modules/redfish_command.py +++ b/ansible_collections/community/general/plugins/modules/redfish_command.py @@ -288,6 +288,20 @@ options: type: str choices: [ ResetAll, PreserveNetworkAndUsers, PreserveNetwork ] version_added: 8.6.0 + wait: + required: false + description: + - Block until the service is ready again. + type: bool + default: false + version_added: 9.1.0 + wait_timeout: + required: false + description: + - How long to block until the service is ready again before giving up. + type: int + default: 120 + version_added: 9.1.0 author: - "Jose Delarosa (@jose-delarosa)" @@ -685,6 +699,16 @@ EXAMPLES = ''' username: "{{ username }}" password: "{{ password }}" + - name: Restart manager power gracefully and wait for it to be available + community.general.redfish_command: + category: Manager + command: GracefulRestart + resource_id: BMC + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + wait: True + - name: Restart manager power gracefully community.general.redfish_command: category: Manager @@ -841,7 +865,9 @@ def main(): ), strip_etag_quotes=dict(type='bool', default=False), reset_to_defaults_mode=dict(choices=['ResetAll', 'PreserveNetworkAndUsers', 'PreserveNetwork']), - bios_attributes=dict(type="dict") + bios_attributes=dict(type="dict"), + wait=dict(type='bool', default=False), + wait_timeout=dict(type='int', default=120), ), required_together=[ ('username', 'password'), @@ -1016,7 +1042,7 @@ def main(): command = 'PowerGracefulRestart' if command.startswith('Power'): - result = rf_utils.manage_manager_power(command) + result = rf_utils.manage_manager_power(command, module.params['wait'], module.params['wait_timeout']) elif command == 'ClearLogs': result = rf_utils.clear_logs() elif command == 'VirtualMediaInsert': diff --git a/ansible_collections/community/general/plugins/modules/redfish_info.py b/ansible_collections/community/general/plugins/modules/redfish_info.py index 3b594b7a2..efcb34f01 100644 --- a/ansible_collections/community/general/plugins/modules/redfish_info.py +++ b/ansible_collections/community/general/plugins/modules/redfish_info.py @@ -359,6 +359,16 @@ EXAMPLES = ''' baseuri: "{{ baseuri }}" username: "{{ username }}" password: "{{ password }}" + + - name: Check the availability of the service with a timeout of 5 seconds + community.general.redfish_info: + category: Service + command: CheckAvailability + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + timeout: 5 + register: result ''' RETURN = ''' @@ -385,6 +395,7 @@ CATEGORY_COMMANDS_ALL = { "GetUpdateStatus"], "Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols", "GetHealthReport", "GetHostInterfaces", "GetManagerInventory", "GetServiceIdentification"], + "Service": ["CheckAvailability"], } CATEGORY_COMMANDS_DEFAULT = { @@ -393,7 +404,8 @@ CATEGORY_COMMANDS_DEFAULT = { "Accounts": "ListUsers", "Update": "GetFirmwareInventory", "Sessions": "GetSessions", - "Manager": "GetManagerNicInventory" + "Manager": "GetManagerNicInventory", + "Service": "CheckAvailability", } @@ -473,7 +485,13 @@ def main(): module.fail_json(msg="Invalid Category: %s" % category) # Organize by Categories / Commands - if category == "Systems": + if category == "Service": + # service-level commands are always available + for command in command_list: + if command == "CheckAvailability": + result["service"] = rf_utils.check_service_availability() + + elif category == "Systems": # execute only if we find a Systems resource resource = rf_utils._find_systems_resource() if resource['ret'] is False: diff --git a/ansible_collections/community/general/plugins/modules/redis_info.py b/ansible_collections/community/general/plugins/modules/redis_info.py index f352d53d7..c75abcf21 100644 --- a/ansible_collections/community/general/plugins/modules/redis_info.py +++ b/ansible_collections/community/general/plugins/modules/redis_info.py @@ -30,6 +30,11 @@ options: version_added: 7.5.0 ca_certs: version_added: 7.5.0 + cluster: + default: false + description: Get informations about cluster status as RV(cluster). + type: bool + version_added: 9.1.0 seealso: - module: community.general.redis author: "Pavlo Bashynskyi (@levonet)" @@ -43,6 +48,15 @@ EXAMPLES = r''' - name: Print server information ansible.builtin.debug: var: result.info + +- name: Get server cluster information + community.general.redis_info: + cluster: true + register: result + +- name: Print server cluster information + ansible.builtin.debug: + var: result.cluster_info ''' RETURN = r''' @@ -178,6 +192,25 @@ info: "used_memory_scripts_human": "0B", "used_memory_startup": 791264 } +cluster: + description: The default set of cluster information sections U(https://redis.io/commands/cluster-info). + returned: success if O(cluster=true) + version_added: 9.1.0 + type: dict + sample: { + "cluster_state": ok, + "cluster_slots_assigned": 16384, + "cluster_slots_ok": 16384, + "cluster_slots_pfail": 0, + "cluster_slots_fail": 0, + "cluster_known_nodes": 6, + "cluster_size": 3, + "cluster_current_epoch": 6, + "cluster_my_epoch": 2, + "cluster_stats_messages_sent": 1483972, + "cluster_stats_messages_received": 1483968, + "total_cluster_links_buffer_limit_exceeded": 0 + } ''' import traceback @@ -202,14 +235,19 @@ def redis_client(**client_params): # Module execution. def main(): + module_args = dict( + cluster=dict(type='bool', default=False), + ) + module_args.update(redis_auth_argument_spec(tls_default=False)) module = AnsibleModule( - argument_spec=redis_auth_argument_spec(tls_default=False), + argument_spec=module_args, supports_check_mode=True, ) fail_imports(module, module.params['tls']) redis_params = redis_auth_params(module) + cluster = module.params['cluster'] # Connect and check client = redis_client(**redis_params) @@ -219,7 +257,13 @@ def main(): module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) info = client.info() - module.exit_json(changed=False, info=info) + + result = dict(changed=False, info=info) + + if cluster: + result['cluster_info'] = client.execute_command('CLUSTER INFO') + + module.exit_json(**result) if __name__ == '__main__': diff --git a/ansible_collections/community/general/plugins/modules/udm_user.py b/ansible_collections/community/general/plugins/modules/udm_user.py index dcbf0ec85..5a2e09049 100644 --- a/ansible_collections/community/general/plugins/modules/udm_user.py +++ b/ansible_collections/community/general/plugins/modules/udm_user.py @@ -20,6 +20,12 @@ description: - "This module allows to manage posix users on a univention corporate server (UCS). It uses the python API of the UCS to create a new object or edit it." +notes: + - This module does B(not) work with Python 3.13 or newer. It uses the deprecated L(crypt Python module, + https://docs.python.org/3.12/library/crypt.html) from the Python standard library, which was removed + from Python 3.13. +requirements: + - Python 3.12 or earlier extends_documentation_fragment: - community.general.attributes attributes: @@ -324,10 +330,10 @@ EXAMPLES = ''' RETURN = '''# ''' -import crypt from datetime import date, timedelta +import traceback -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.community.general.plugins.module_utils.univention_umc import ( umc_module_for_add, umc_module_for_edit, @@ -335,6 +341,15 @@ from ansible_collections.community.general.plugins.module_utils.univention_umc i base_dn, ) +try: + import crypt +except ImportError: + HAS_CRYPT = False + CRYPT_IMPORT_ERROR = traceback.format_exc() +else: + HAS_CRYPT = True + CRYPT_IMPORT_ERROR = None + def main(): expiry = date.strftime(date.today() + timedelta(days=365), "%Y-%m-%d") @@ -451,6 +466,13 @@ def main(): ('state', 'present', ['firstname', 'lastname', 'password']) ]) ) + + if not HAS_CRYPT: + module.fail_json( + msg=missing_required_lib('crypt (part of Python 3.13 standard library)'), + exception=CRYPT_IMPORT_ERROR, + ) + username = module.params['username'] position = module.params['position'] ou = module.params['ou'] diff --git a/ansible_collections/community/general/plugins/plugin_utils/keys_filter.py b/ansible_collections/community/general/plugins/plugin_utils/keys_filter.py new file mode 100644 index 000000000..94234a15d --- /dev/null +++ b/ansible_collections/community/general/plugins/plugin_utils/keys_filter.py @@ -0,0 +1,141 @@ +# Copyright (c) 2024 Vladimir Botka <vbotka@gmail.com> +# Copyright (c) 2024 Felix Fontein <felix@fontein.de> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common._collections_compat import Mapping, Sequence + + +def _keys_filter_params(data, matching_parameter): + """test parameters: + * data must be a list of dictionaries. All keys must be strings. + * matching_parameter is member of a list. + """ + + mp = matching_parameter + ml = ['equal', 'starts_with', 'ends_with', 'regex'] + + if not isinstance(data, Sequence): + msg = "First argument must be a list. %s is %s" + raise AnsibleFilterError(msg % (data, type(data))) + + for elem in data: + if not isinstance(elem, Mapping): + msg = "The data items must be dictionaries. %s is %s" + raise AnsibleFilterError(msg % (elem, type(elem))) + + for elem in data: + if not all(isinstance(item, string_types) for item in elem.keys()): + msg = "Top level keys must be strings. keys: %s" + raise AnsibleFilterError(msg % elem.keys()) + + if mp not in ml: + msg = "The matching_parameter must be one of %s. matching_parameter=%s" + raise AnsibleFilterError(msg % (ml, mp)) + + return + + +def _keys_filter_target_str(target, matching_parameter): + """ + Test: + * target is a non-empty string or list. + * If target is list all items are strings. + * target is a string or list with single string if matching_parameter=regex. + Convert target and return: + * tuple of unique target items, or + * tuple with single item, or + * compiled regex if matching_parameter=regex. + """ + + if not isinstance(target, Sequence): + msg = "The target must be a string or a list. target is %s." + raise AnsibleFilterError(msg % type(target)) + + if len(target) == 0: + msg = "The target can't be empty." + raise AnsibleFilterError(msg) + + if isinstance(target, list): + for elem in target: + if not isinstance(elem, string_types): + msg = "The target items must be strings. %s is %s" + raise AnsibleFilterError(msg % (elem, type(elem))) + + if matching_parameter == 'regex': + if isinstance(target, string_types): + r = target + else: + if len(target) > 1: + msg = "Single item is required in the target list if matching_parameter=regex." + raise AnsibleFilterError(msg) + else: + r = target[0] + try: + tt = re.compile(r) + except re.error: + msg = "The target must be a valid regex if matching_parameter=regex. target is %s" + raise AnsibleFilterError(msg % r) + elif isinstance(target, string_types): + tt = (target, ) + else: + tt = tuple(set(target)) + + return tt + + +def _keys_filter_target_dict(target, matching_parameter): + """ + Test: + * target is a list of dictionaries with attributes 'after' and 'before'. + * Attributes 'before' must be valid regex if matching_parameter=regex. + * Otherwise, the attributes 'before' must be strings. + Convert target and return: + * iterator that aggregates attributes 'before' and 'after', or + * iterator that aggregates compiled regex of attributes 'before' and 'after' if matching_parameter=regex. + """ + + if not isinstance(target, list): + msg = "The target must be a list. target is %s." + raise AnsibleFilterError(msg % (target, type(target))) + + if len(target) == 0: + msg = "The target can't be empty." + raise AnsibleFilterError(msg) + + for elem in target: + if not isinstance(elem, Mapping): + msg = "The target items must be dictionaries. %s is %s" + raise AnsibleFilterError(msg % (elem, type(elem))) + if not all(k in elem for k in ('before', 'after')): + msg = "All dictionaries in target must include attributes: after, before." + raise AnsibleFilterError(msg) + if not isinstance(elem['before'], string_types): + msg = "The attributes before must be strings. %s is %s" + raise AnsibleFilterError(msg % (elem['before'], type(elem['before']))) + if not isinstance(elem['after'], string_types): + msg = "The attributes after must be strings. %s is %s" + raise AnsibleFilterError(msg % (elem['after'], type(elem['after']))) + + before = [d['before'] for d in target] + after = [d['after'] for d in target] + + if matching_parameter == 'regex': + try: + tr = map(re.compile, before) + tz = list(zip(tr, after)) + except re.error: + msg = ("The attributes before must be valid regex if matching_parameter=regex." + " Not all items are valid regex in: %s") + raise AnsibleFilterError(msg % before) + else: + tz = list(zip(before, after)) + + return tz diff --git a/ansible_collections/community/general/tests/integration/targets/ansible_galaxy_install/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/ansible_galaxy_install/tasks/main.yml index 1ecd9980d..5c4af6d16 100644 --- a/ansible_collections/community/general/tests/integration/targets/ansible_galaxy_install/tasks/main.yml +++ b/ansible_collections/community/general/tests/integration/targets/ansible_galaxy_install/tasks/main.yml @@ -4,10 +4,16 @@ # SPDX-License-Identifier: GPL-3.0-or-later ################################################### +- name: Make directory install_c + ansible.builtin.file: + path: "{{ remote_tmp_dir }}/install_c" + state: directory + - name: Install collection netbox.netbox community.general.ansible_galaxy_install: type: collection name: netbox.netbox + dest: "{{ remote_tmp_dir }}/install_c" register: install_c0 - name: Assert collection netbox.netbox was installed @@ -20,6 +26,7 @@ community.general.ansible_galaxy_install: type: collection name: netbox.netbox + dest: "{{ remote_tmp_dir }}/install_c" register: install_c1 - name: Assert collection was not installed @@ -28,10 +35,16 @@ - install_c1 is not changed ################################################### +- name: Make directory install_r + ansible.builtin.file: + path: "{{ remote_tmp_dir }}/install_r" + state: directory + - name: Install role ansistrano.deploy community.general.ansible_galaxy_install: type: role name: ansistrano.deploy + dest: "{{ remote_tmp_dir }}/install_r" register: install_r0 - name: Assert collection ansistrano.deploy was installed @@ -44,6 +57,7 @@ community.general.ansible_galaxy_install: type: role name: ansistrano.deploy + dest: "{{ remote_tmp_dir }}/install_r" register: install_r1 - name: Assert role was not installed @@ -86,3 +100,44 @@ assert: that: - install_rq1 is not changed + +################################################### +- name: Make directory upgrade_c + ansible.builtin.file: + path: "{{ remote_tmp_dir }}/upgrade_c" + state: directory + +- name: Install collection netbox.netbox 3.17.0 + community.general.ansible_galaxy_install: + type: collection + name: netbox.netbox:3.17.0 + dest: "{{ remote_tmp_dir }}/upgrade_c" + register: upgrade_c0 + +- name: Assert collection netbox.netbox was installed + assert: + that: + - upgrade_c0 is changed + - '"netbox.netbox" in upgrade_c0.new_collections' + +- name: Upgrade collection netbox.netbox + community.general.ansible_galaxy_install: + state: latest + type: collection + name: netbox.netbox + dest: "{{ remote_tmp_dir }}/upgrade_c" + register: upgrade_c1 + +- name: Upgrade collection netbox.netbox (again) + community.general.ansible_galaxy_install: + state: latest + type: collection + name: netbox.netbox + dest: "{{ remote_tmp_dir }}/upgrade_c" + register: upgrade_c2 + +- name: Assert collection was not installed + assert: + that: + - upgrade_c1 is changed + - upgrade_c2 is not changed diff --git a/ansible_collections/community/general/tests/integration/targets/cargo/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/cargo/tasks/main.yml index 29f27c3fd..89f13960a 100644 --- a/ansible_collections/community/general/tests/integration/targets/cargo/tasks/main.yml +++ b/ansible_collections/community/general/tests/integration/targets/cargo/tasks/main.yml @@ -16,6 +16,7 @@ - block: - import_tasks: test_general.yml - import_tasks: test_version.yml + - import_tasks: test_directory.yml environment: "{{ cargo_environment }}" when: has_cargo | default(false) - import_tasks: test_rustup_cargo.yml diff --git a/ansible_collections/community/general/tests/integration/targets/cargo/tasks/test_directory.yml b/ansible_collections/community/general/tests/integration/targets/cargo/tasks/test_directory.yml new file mode 100644 index 000000000..f4275ede6 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/cargo/tasks/test_directory.yml @@ -0,0 +1,122 @@ +--- +# Copyright (c) 2024 Colin Nolan <cn580@alumni.york.ac.uk> +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create temp directory + tempfile: + state: directory + register: temp_directory + +- name: Test block + vars: + manifest_path: "{{ temp_directory.path }}/Cargo.toml" + package_name: hello-world-directory-test + block: + - name: Initialize package + ansible.builtin.command: + cmd: "cargo init --name {{ package_name }}" + args: + chdir: "{{ temp_directory.path }}" + + - name: Set package version (1.0.0) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "1.0.0"' + + - name: Ensure package is uninstalled + community.general.cargo: + name: "{{ package_name }}" + state: absent + directory: "{{ temp_directory.path }}" + register: uninstall_absent + + - name: Install package + community.general.cargo: + name: "{{ package_name }}" + directory: "{{ temp_directory.path }}" + register: install_absent + + - name: Change package version (1.0.1) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "1.0.1"' + + - name: Install package again (present) + community.general.cargo: + name: "{{ package_name }}" + state: present + directory: "{{ temp_directory.path }}" + register: install_present_state + + - name: Install package again (latest) + community.general.cargo: + name: "{{ package_name }}" + state: latest + directory: "{{ temp_directory.path }}" + register: install_latest_state + + - name: Change package version (2.0.0) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "2.0.0"' + + - name: Install package with given version (matched) + community.general.cargo: + name: "{{ package_name }}" + version: "2.0.0" + directory: "{{ temp_directory.path }}" + register: install_given_version_matched + + - name: Install package with given version (unmatched) + community.general.cargo: + name: "{{ package_name }}" + version: "2.0.1" + directory: "{{ temp_directory.path }}" + register: install_given_version_unmatched + ignore_errors: true + + - name: Uninstall package + community.general.cargo: + name: "{{ package_name }}" + state: absent + directory: "{{ temp_directory.path }}" + register: uninstall_present + + - name: Install non-existant package + community.general.cargo: + name: "{{ package_name }}-non-existant" + state: present + directory: "{{ temp_directory.path }}" + register: install_non_existant + ignore_errors: true + + - name: Install non-existant source directory + community.general.cargo: + name: "{{ package_name }}" + state: present + directory: "{{ temp_directory.path }}/non-existant" + register: install_non_existant_source + ignore_errors: true + + always: + - name: Remove temp directory + file: + path: "{{ temp_directory.path }}" + state: absent + +- name: Check assertions + assert: + that: + - uninstall_absent is not changed + - install_absent is changed + - install_present_state is not changed + - install_latest_state is changed + - install_given_version_matched is changed + - install_given_version_unmatched is failed + - uninstall_present is changed + - install_non_existant is failed + - install_non_existant_source is failed diff --git a/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_check.yml b/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_check.yml new file mode 100644 index 000000000..e1229c794 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_check.yml @@ -0,0 +1,114 @@ +--- +# Copyright (c) 2024, Michael Ilg (@Ilgmi) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a service + community.general.consul_agent_service: + name: nginx + service_port: 80 + address: localhost + tags: + - http + meta: + nginx_version: 1.25.3 + register: result + +- set_fact: + nginx_service: "{{result.service}}" + +- assert: + that: + - result is changed + - result.service.ID is defined + +- name: Add a check for service + community.general.consul_agent_check: + name: nginx_check + id: nginx_check + interval: 30s + http: http://localhost:80/morestatus + notes: "Nginx Check" + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.CheckID == 'nginx_check' + - result.check.ServiceID == 'nginx' + - result.check.Interval == '30s' + - result.check.Type == 'http' + - result.check.Notes == 'Nginx Check' + +- set_fact: + nginx_service_check: "{{ result.check }}" + +- name: Update check for service + community.general.consul_agent_check: + name: "{{ nginx_service_check.Name }}" + id: "{{ nginx_service_check.CheckID }}" + interval: 60s + http: http://localhost:80/morestatus + notes: "New Nginx Check" + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.CheckID == 'nginx_check' + - result.check.ServiceID == 'nginx' + - result.check.Interval == '1m0s' + - result.check.Type == 'http' + - result.check.Notes == 'New Nginx Check' + +- name: Remove check + community.general.consul_agent_check: + id: "{{ nginx_service_check.Name }}" + state: absent + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result is not failed + - result.operation == 'remove' + +- name: Add a check + community.general.consul_agent_check: + name: check + id: check + interval: 30s + tcp: localhost:80 + notes: "check" + register: result + +- assert: + that: + - result is changed + - result.check is defined + +- name: Update a check + community.general.consul_agent_check: + name: check + id: check + interval: 60s + tcp: localhost:80 + notes: "check" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.Interval == '1m0s' + +- name: Remove check + community.general.consul_agent_check: + id: check + state: absent + register: result
\ No newline at end of file diff --git a/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_service.yml b/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_service.yml new file mode 100644 index 000000000..95270f74b --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/consul/tasks/consul_agent_service.yml @@ -0,0 +1,89 @@ +--- +# Copyright (c) 2024, Michael Ilg (@Ilgmi) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a service + community.general.consul_agent_service: + name: nginx + service_port: 80 + address: localhost + tags: + - http + meta: + nginx_version: 1.25.3 + register: result + +- set_fact: + nginx_service: "{{result.service}}" + +- assert: + that: + - result is changed + - result.service.ID is defined + - result.service.Service == 'nginx' + - result.service.Address == 'localhost' + - result.service.Port == 80 + - result.service.Tags[0] == 'http' + - result.service.Meta.nginx_version is defined + - result.service.Meta.nginx_version == '1.25.3' + - result.service.ContentHash is defined + +- name: Update service + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + name: "{{ nginx_service.Service }}" + service_port: 8080 + address: 127.0.0.1 + tags: + - http + - new_tag + meta: + nginx_version: 1.0.0 + nginx: 1.25.3 + register: result +- assert: + that: + - result is changed + - result.service.ID is defined + - result.service.Service == 'nginx' + - result.service.Address == '127.0.0.1' + - result.service.Port == 8080 + - result.service.Tags[0] == 'http' + - result.service.Tags[1] == 'new_tag' + - result.service.Meta.nginx_version is defined + - result.service.Meta.nginx_version == '1.0.0' + - result.service.Meta.nginx is defined + - result.service.Meta.nginx == '1.25.3' + - result.service.ContentHash is defined + +- name: Update service not changed when updating again without changes + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + name: "{{ nginx_service.Service }}" + service_port: 8080 + address: 127.0.0.1 + tags: + - http + - new_tag + meta: + nginx_version: 1.0.0 + nginx: 1.25.3 + register: result + +- assert: + that: + - result is not changed + - result.operation is not defined + +- name: Remove service + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + state: absent + register: result + +- assert: + that: + - result is changed + - result is not failed + - result.operation == 'remove'
\ No newline at end of file diff --git a/ansible_collections/community/general/tests/integration/targets/consul/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/consul/tasks/main.yml index 6fef2b998..0ac58fc40 100644 --- a/ansible_collections/community/general/tests/integration/targets/consul/tasks/main.yml +++ b/ansible_collections/community/general/tests/integration/targets/consul/tasks/main.yml @@ -97,6 +97,8 @@ - import_tasks: consul_token.yml - import_tasks: consul_auth_method.yml - import_tasks: consul_binding_rule.yml + - import_tasks: consul_agent_service.yml + - import_tasks: consul_agent_check.yml module_defaults: group/community.general.consul: token: "{{ consul_management_token }}" diff --git a/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/aliases b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/aliases new file mode 100644 index 000000000..12d1d6617 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml new file mode 100644 index 000000000..94825c9d6 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/keep_keys.yml @@ -0,0 +1,79 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Debug ansible_version + ansible.builtin.debug: + var: ansible_version + when: not quite_test | d(true) | bool + tags: ansible_version + +- name: Test keep keys equal (default) + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=tt) }}" + tt: [k0_x0, k1_x1] + tags: equal_default + +- name: Test keep keys regex string + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=tt, matching_parameter=mp) }}" + mp: regex + tt: '^.*[01]_x.*$' + tags: regex_string + +- name: Test keep keys targets1 + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + loop: "{{ targets1 }}" + loop_control: + label: "{{ item.mp }}: {{ item.tt }}" + vars: + rr: "{{ list1 | community.general.keep_keys(target=item.tt, matching_parameter=item.mp) }}" + tags: targets1 + +- name: Test keep keys targets2 + ansible.builtin.assert: + that: + - (rr | difference(result2) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + loop: "{{ targets2 }}" + loop_control: + label: "{{ item.mp }}: {{ item.tt }}" + vars: + rr: "{{ list2 | community.general.keep_keys(target=item.tt, matching_parameter=item.mp) }}" + tags: targets2 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/main.yml new file mode 100644 index 000000000..23457d1e1 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/tasks/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test keep_keys + import_tasks: keep_keys.yml diff --git a/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/vars/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/vars/main.yml new file mode 100644 index 000000000..b25325253 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_keep_keys/vars/main.yml @@ -0,0 +1,33 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +targets1: + - {mp: equal, tt: [k0_x0, k1_x1]} + - {mp: starts_with, tt: [k0, k1]} + - {mp: ends_with, tt: [x0, x1]} + - {mp: regex, tt: ['^.*[01]_x.*$']} + - {mp: regex, tt: '^.*[01]_x.*$'} + +list1: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result1: + - {k0_x0: A0, k1_x1: B0} + - {k0_x0: A1, k1_x1: B1} + +targets2: + - {mp: equal, tt: k0_x0} + - {mp: starts_with, tt: k0} + - {mp: ends_with, tt: x0} + - {mp: regex, tt: '^.*0_x.*$'} + +list2: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result2: + - {k0_x0: A0} + - {k0_x0: A1} diff --git a/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/aliases b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/aliases new file mode 100644 index 000000000..12d1d6617 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/main.yml new file mode 100644 index 000000000..d4215d8c5 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test remove_keys + import_tasks: remove_keys.yml diff --git a/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/remove_keys.yml b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/remove_keys.yml new file mode 100644 index 000000000..121cd88cf --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/tasks/remove_keys.yml @@ -0,0 +1,79 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Debug ansible_version + ansible.builtin.debug: + var: ansible_version + when: not quite_test | d(true) | bool + tags: ansible_version + +- name: Test remove keys equal (default) + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.remove_keys(target=tt) }}" + tt: [k0_x0, k1_x1] + tags: equal_default + +- name: Test remove keys regex string + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.remove_keys(target=tt, matching_parameter=mp) }}" + mp: regex + tt: '^.*[01]_x.*$' + tags: regex_string + +- name: Test remove keys targets1 + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + loop: "{{ targets1 }}" + loop_control: + label: "{{ item.mp }}: {{ item.tt }}" + vars: + rr: "{{ list1 | community.general.remove_keys(target=item.tt, matching_parameter=item.mp) }}" + tags: targets1 + +- name: Test remove keys targets2 + ansible.builtin.assert: + that: + - (rr | difference(result2) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + loop: "{{ targets2 }}" + loop_control: + label: "{{ item.mp }}: {{ item.tt }}" + vars: + rr: "{{ list2 | community.general.remove_keys(target=item.tt, matching_parameter=item.mp) }}" + tags: targets1 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/vars/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/vars/main.yml new file mode 100644 index 000000000..a52d09a34 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_remove_keys/vars/main.yml @@ -0,0 +1,33 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +targets1: + - {mp: equal, tt: [k0_x0, k1_x1]} + - {mp: starts_with, tt: [k0, k1]} + - {mp: ends_with, tt: [x0, x1]} + - {mp: regex, tt: ['^.*[01]_x.*$']} + - {mp: regex, tt: '^.*[01]_x.*$'} + +list1: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result1: + - {k2_x2: [C0], k3_x3: foo} + - {k2_x2: [C1], k3_x3: bar} + +targets2: + - {mp: equal, tt: k0_x0} + - {mp: starts_with, tt: k0} + - {mp: ends_with, tt: x0} + - {mp: regex, tt: '^.*0_x.*$'} + +list2: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result2: + - {k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k1_x1: B1, k2_x2: [C1], k3_x3: bar} diff --git a/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/aliases b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/aliases new file mode 100644 index 000000000..12d1d6617 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/fn-test-replace_keys.yml b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/fn-test-replace_keys.yml new file mode 100644 index 000000000..e324376a5 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/fn-test-replace_keys.yml @@ -0,0 +1,21 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test replace keys + ansible.builtin.assert: + that: + - (rr | difference(item.result) | length) == 0 + success_msg: | + [OK] {{ item.label }} + result: + {{ rr | to_nice_yaml(indent=2) | indent(2) }} + fail_msg: | + [ERR] {{ item.label }} + result: + {{ rr | to_nice_yaml(indent=2) | indent(2) }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ item.data | + community.general.replace_keys(target=item.target, matching_parameter=item.match) }}" diff --git a/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/main.yml new file mode 100644 index 000000000..35addaf94 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/main.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Test replace_keys + import_tasks: replace_keys.yml diff --git a/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/replace_keys.yml b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/replace_keys.yml new file mode 100644 index 000000000..a57921b81 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/tasks/replace_keys.yml @@ -0,0 +1,56 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Debug ansible_version + ansible.builtin.debug: + var: ansible_version + when: not quiet_test | d(true) | bool + tags: ansible_version + +- name: Test replace keys equal (default) + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + vars: + rr: "{{ list1 | community.general.replace_keys(target=tt) }}" + tt: + - {before: k0_x0, after: a0} + - {before: k1_x1, after: a1} + tags: equal_default + +- name: Test replace keys targets1 + ansible.builtin.assert: + that: + - (rr | difference(result1) | length) == 0 + success_msg: | + [OK] result: + {{ rr | to_yaml }} + fail_msg: | + [ERR] result: + {{ rr | to_yaml }} + quiet: "{{ quiet_test | d(true) | bool }}" + loop: "{{ targets1 | dict2items }}" + loop_control: + label: "{{ item.key }}" + vars: + rr: "{{ list1 | community.general.replace_keys(target=item.value, matching_parameter=item.key) }}" + tags: targets1 + +- name: Test replace keys targets2 + include_tasks: + file: fn-test-replace_keys.yml + apply: + tags: targets2 + loop: "{{ targets2 }}" + loop_control: + label: "{{ item.label }}" + tags: targets2 diff --git a/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/vars/main.yml b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/vars/main.yml new file mode 100644 index 000000000..167e08396 --- /dev/null +++ b/ansible_collections/community/general/tests/integration/targets/filter_replace_keys/vars/main.yml @@ -0,0 +1,58 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +list1: + - {k0_x0: A0, k1_x1: B0, k2_x2: [C0], k3_x3: foo} + - {k0_x0: A1, k1_x1: B1, k2_x2: [C1], k3_x3: bar} + +result1: + - {a0: A0, a1: B0, k2_x2: [C0], k3_x3: foo} + - {a0: A1, a1: B1, k2_x2: [C1], k3_x3: bar} + +targets1: + equal: + - {before: k0_x0, after: a0} + - {before: k1_x1, after: a1} + starts_with: + - {before: k0, after: a0} + - {before: k1, after: a1} + ends_with: + - {before: x0, after: a0} + - {before: x1, after: a1} + regex: + - {before: "^.*0_x.*$", after: a0} + - {before: "^.*1_x.*$", after: a1} + +list2: + - {aaa1: A, bbb1: B, ccc1: C} + - {aaa2: D, bbb2: E, ccc2: F} + +targets2: + - label: If more keys match the same attribute before the last one will be used. + match: regex + target: + - {before: "^.*_x.*$", after: X} + data: "{{ list1 }}" + result: + - X: foo + - X: bar + - label: If there are items with equal attribute before the first one will be used. + match: regex + target: + - {before: "^.*_x.*$", after: X} + - {before: "^.*_x.*$", after: Y} + data: "{{ list1 }}" + result: + - X: foo + - X: bar + - label: If there are more matches for a key the first one will be used. + match: starts_with + target: + - {before: a, after: X} + - {before: aa, after: Y} + data: "{{ list2 }}" + result: + - {X: A, bbb1: B, ccc1: C} + - {X: D, bbb2: E, ccc2: F} diff --git a/ansible_collections/community/general/tests/integration/targets/git_config/tasks/unset_value.yml b/ansible_collections/community/general/tests/integration/targets/git_config/tasks/unset_value.yml index dfa535a2d..5f8c52c96 100644 --- a/ansible_collections/community/general/tests/integration/targets/git_config/tasks/unset_value.yml +++ b/ansible_collections/community/general/tests/integration/targets/git_config/tasks/unset_value.yml @@ -25,4 +25,28 @@ - unset_result.diff.before == option_value + "\n" - unset_result.diff.after == "\n" - get_result.config_value == '' + +- import_tasks: setup_value.yml + +- name: unsetting value with value specified + git_config: + name: "{{ option_name }}" + scope: "{{ option_scope }}" + value: "{{ option_value }}" + state: absent + register: unset_result + +- name: getting value + git_config: + name: "{{ option_name }}" + scope: "{{ option_scope }}" + register: get_result + +- name: assert unset changed and deleted value + assert: + that: + - unset_result is changed + - unset_result.diff.before == option_value + "\n" + - unset_result.diff.after == "\n" + - get_result.config_value == '' ... diff --git a/ansible_collections/community/general/tests/integration/targets/keycloak_client/tasks/main.yml b/ansible_collections/community/general/tests/integration/targets/keycloak_client/tasks/main.yml index 5e7c7fae3..e1a7d2ebf 100644 --- a/ansible_collections/community/general/tests/integration/targets/keycloak_client/tasks/main.yml +++ b/ansible_collections/community/general/tests/integration/targets/keycloak_client/tasks/main.yml @@ -103,3 +103,131 @@ assert: that: - check_client_when_present_and_changed is changed + +- name: Desire client with flow binding overrides + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser_name: browser + direct_grant_name: direct grant + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are set + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser | length > 0 + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant | length > 0 + +- name: Backup flow UUIDs + set_fact: + flow_browser_uuid: "{{ desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser }}" + flow_direct_grant_uuid: "{{ desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant }}" + +- name: Desire client with flow binding overrides remove direct_grant_name + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser_name: browser + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser | length > 0 + - "'direct_grant' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + +- name: Desire client with flow binding overrides remove browser add direct_grant + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + direct_grant_name: direct grant + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - "'browser' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant | length > 0 + +- name: Desire client with flow binding overrides with UUIDs + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser: "{{ flow_browser_uuid }}" + direct_grant: "{{ flow_direct_grant_uuid }}" + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are updated + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.browser == flow_browser_uuid + - desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides.direct_grant == flow_direct_grant_uuid + +- name: Unset flow binding overrides + community.general.keycloak_client: + auth_keycloak_url: "{{ url }}" + auth_realm: "{{ admin_realm }}" + auth_username: "{{ admin_user }}" + auth_password: "{{ admin_password }}" + realm: "{{ realm }}" + client_id: "{{ client_id }}" + state: present + redirect_uris: '{{redirect_uris1}}' + attributes: '{{client_attributes1}}' + protocol_mappers: '{{protocol_mappers1}}' + authentication_flow_binding_overrides: + browser: "{{ None }}" + direct_grant: null + register: desire_client_with_flow_binding_overrides + +- name: Assert flows are removed + assert: + that: + - desire_client_with_flow_binding_overrides is changed + - "'authenticationFlowBindingOverrides' in desire_client_with_flow_binding_overrides.end_state" + - "'browser' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides" + - "'direct_grant' not in desire_client_with_flow_binding_overrides.end_state.authenticationFlowBindingOverrides"
\ No newline at end of file diff --git a/ansible_collections/community/general/tests/unit/plugins/module_utils/test_cmd_runner.py b/ansible_collections/community/general/tests/unit/plugins/module_utils/test_cmd_runner.py index 6816afb34..fcdffe7d2 100644 --- a/ansible_collections/community/general/tests/unit/plugins/module_utils/test_cmd_runner.py +++ b/ansible_collections/community/general/tests/unit/plugins/module_utils/test_cmd_runner.py @@ -47,6 +47,9 @@ TC_FORMATS = dict( simple_fixed_false=(partial(cmd_runner_fmt.as_fixed, ["--always-here", "--forever"]), False, ["--always-here", "--forever"], None), simple_fixed_none=(partial(cmd_runner_fmt.as_fixed, ["--always-here", "--forever"]), None, ["--always-here", "--forever"], None), simple_fixed_str=(partial(cmd_runner_fmt.as_fixed, ["--always-here", "--forever"]), "something", ["--always-here", "--forever"], None), + stack_optval__str=(partial(cmd_runner_fmt.stack(cmd_runner_fmt.as_optval), "-t"), ["potatoes", "bananas"], ["-tpotatoes", "-tbananas"], None), + stack_opt_val__str=(partial(cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_val), "-t"), ["potatoes", "bananas"], ["-t", "potatoes", "-t", "bananas"], None), + stack_opt_eq_val__int=(partial(cmd_runner_fmt.stack(cmd_runner_fmt.as_opt_eq_val), "--answer"), [42, 17], ["--answer=42", "--answer=17"], None), ) if tuple(version_info) >= (3, 1): from collections import OrderedDict @@ -67,7 +70,7 @@ TC_FORMATS_IDS = sorted(TC_FORMATS.keys()) def test_arg_format(func, value, expected, exception): fmt_func = func() try: - actual = fmt_func(value, ctx_ignore_none=True) + actual = fmt_func(value) print("formatted string = {0}".format(actual)) assert actual == expected, "actual = {0}".format(actual) except Exception as e: diff --git a/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.py b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.py new file mode 100644 index 000000000..8aec71900 --- /dev/null +++ b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.py @@ -0,0 +1,13 @@ +# Copyright (c) Alexei Znamensky (russoz@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.community.general.plugins.modules import django_check +from .helper import Helper + + +Helper.from_module(django_check, __name__) diff --git a/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.yaml b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.yaml new file mode 100644 index 000000000..6156aaa2c --- /dev/null +++ b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_check.yaml @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alexei Znamensky (russoz@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +--- +- id: success + input: + settings: whatever.settings + run_command_calls: + - command: [/testbin/python, -m, django, check, --no-color, --settings=whatever.settings] + environ: &env-def {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: true} + rc: 0 + out: "whatever\n" + err: "" +- id: multiple_databases + input: + settings: whatever.settings + database: + - abc + - def + run_command_calls: + - command: [/testbin/python, -m, django, check, --no-color, --settings=whatever.settings, --database, abc, --database, def] + environ: *env-def + rc: 0 + out: "whatever\n" + err: "" diff --git a/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.py b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.py new file mode 100644 index 000000000..5a4b89c0c --- /dev/null +++ b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.py @@ -0,0 +1,13 @@ +# Copyright (c) Alexei Znamensky (russoz@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.community.general.plugins.modules import django_createcachetable +from .helper import Helper + + +Helper.from_module(django_createcachetable, __name__) diff --git a/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.yaml b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.yaml new file mode 100644 index 000000000..1808b163f --- /dev/null +++ b/ansible_collections/community/general/tests/unit/plugins/modules/test_django_createcachetable.yaml @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alexei Znamensky (russoz@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +--- +- id: command_success + input: + settings: whatever.settings + run_command_calls: + - command: [/testbin/python, -m, django, createcachetable, --no-color, --settings=whatever.settings, --noinput, --database=default] + environ: &env-def {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: true} + rc: 0 + out: "whatever\n" + err: "" |