mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-29 09:56:53 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1955fc6a8a | ||
|
|
1d97cc4b7d | ||
|
|
828968b0dd | ||
|
|
f8666061bc | ||
|
|
9a7a0ca526 | ||
|
|
755ee2b4d0 | ||
|
|
d9e72cf75e | ||
|
|
153456f6aa | ||
|
|
432dc1eb0d | ||
|
|
d74993cacf | ||
|
|
b0fe02d4a3 | ||
|
|
6cd90d30d7 | ||
|
|
437641d95f | ||
|
|
35fb3dd034 | ||
|
|
a7a2631333 | ||
|
|
c089260c88 | ||
|
|
bc7f952a29 | ||
|
|
874e0bdf9e | ||
|
|
6987a07887 | ||
|
|
359788e0cd | ||
|
|
8eec4767bd | ||
|
|
01d3a106ac | ||
|
|
820c69ba54 | ||
|
|
15a46508b4 | ||
|
|
0bf514c955 | ||
|
|
426f9d8734 | ||
|
|
80f4dcb09d | ||
|
|
877d6d76f5 | ||
|
|
f4d52cf235 | ||
|
|
3f66db14a3 | ||
|
|
17abb550c3 | ||
|
|
bfb18b3704 | ||
|
|
983b4d70e0 | ||
|
|
821ae9bc41 | ||
|
|
d5efd56dae | ||
|
|
88ef840750 | ||
|
|
08d04a7923 | ||
|
|
9d0b14b239 | ||
|
|
9e83a4cb34 | ||
|
|
dc5f012e52 | ||
|
|
210adc196e | ||
|
|
f30fcec398 | ||
|
|
0a904d60cd | ||
|
|
1ee2bba140 | ||
|
|
0aa9cd0e30 | ||
|
|
7009a768a4 | ||
|
|
2f5552da04 | ||
|
|
145686cfe0 | ||
|
|
08239919de | ||
|
|
1ab1f8f62b | ||
|
|
13c25154b5 | ||
|
|
d22199f82e | ||
|
|
82a07de870 | ||
|
|
895c1fe2a0 | ||
|
|
3e972990cb | ||
|
|
0ac6e44566 | ||
|
|
1308198eae | ||
|
|
d887985b46 | ||
|
|
7d6e7fa5fa | ||
|
|
b61b988b2e | ||
|
|
549c9e69ae | ||
|
|
fe35cff7a4 | ||
|
|
cef57b044b |
44
.github/BOTMETA.yml
vendored
44
.github/BOTMETA.yml
vendored
@@ -50,6 +50,8 @@ files:
|
||||
$callbacks/cgroup_memory_recap.py: {}
|
||||
$callbacks/context_demo.py: {}
|
||||
$callbacks/counter_enabled.py: {}
|
||||
$callbacks/default_without_diff.py:
|
||||
maintainers: felixfontein
|
||||
$callbacks/dense.py:
|
||||
maintainers: dagwieers
|
||||
$callbacks/diy.py:
|
||||
@@ -149,8 +151,18 @@ files:
|
||||
$filters/jc.py:
|
||||
maintainers: kellyjonbrazil
|
||||
$filters/json_query.py: {}
|
||||
$filters/lists.py:
|
||||
maintainers: cfiehe
|
||||
$filters/lists_difference.yml:
|
||||
maintainers: cfiehe
|
||||
$filters/lists_intersect.yml:
|
||||
maintainers: cfiehe
|
||||
$filters/lists_mergeby.py:
|
||||
maintainers: vbotka
|
||||
$filters/lists_symmetric_difference.yml:
|
||||
maintainers: cfiehe
|
||||
$filters/lists_union.yml:
|
||||
maintainers: cfiehe
|
||||
$filters/random_mac.py: {}
|
||||
$filters/time.py:
|
||||
maintainers: resmo
|
||||
@@ -200,7 +212,7 @@ files:
|
||||
labels: cloud opennebula
|
||||
maintainers: feldsam
|
||||
$inventories/proxmox.py:
|
||||
maintainers: $team_virt ilijamt
|
||||
maintainers: $team_virt ilijamt krauthosting
|
||||
$inventories/scaleway.py:
|
||||
labels: cloud scaleway
|
||||
maintainers: $team_scaleway
|
||||
@@ -562,8 +574,12 @@ files:
|
||||
maintainers: paytroff
|
||||
$modules/gitlab_issue.py:
|
||||
maintainers: zvaraondrej
|
||||
$modules/gitlab_label.py:
|
||||
maintainers: gpongelli
|
||||
$modules/gitlab_merge_request.py:
|
||||
maintainers: zvaraondrej
|
||||
$modules/gitlab_milestone.py:
|
||||
maintainers: gpongelli
|
||||
$modules/gitlab_project_variable.py:
|
||||
maintainers: markuman
|
||||
$modules/gitlab_instance_variable.py:
|
||||
@@ -572,6 +588,10 @@ files:
|
||||
maintainers: SamyCoenen
|
||||
$modules/gitlab_user.py:
|
||||
maintainers: LennertMertens stgrace
|
||||
$modules/gitlab_group_access_token.py:
|
||||
maintainers: pixslx
|
||||
$modules/gitlab_project_access_token.py:
|
||||
maintainers: pixslx
|
||||
$modules/grove.py:
|
||||
maintainers: zimbatm
|
||||
$modules/gunicorn.py:
|
||||
@@ -1039,27 +1059,27 @@ files:
|
||||
$modules/proxmox:
|
||||
keywords: kvm libvirt proxmox qemu
|
||||
labels: proxmox virt
|
||||
maintainers: $team_virt UnderGreen
|
||||
maintainers: $team_virt UnderGreen krauthosting
|
||||
ignore: tleguern
|
||||
$modules/proxmox.py:
|
||||
ignore: skvidal
|
||||
maintainers: UnderGreen
|
||||
maintainers: UnderGreen krauthosting
|
||||
$modules/proxmox_disk.py:
|
||||
maintainers: castorsky
|
||||
maintainers: castorsky krauthosting
|
||||
$modules/proxmox_kvm.py:
|
||||
ignore: skvidal
|
||||
maintainers: helldorado
|
||||
maintainers: helldorado krauthosting
|
||||
$modules/proxmox_nic.py:
|
||||
maintainers: Kogelvis
|
||||
maintainers: Kogelvis krauthosting
|
||||
$modules/proxmox_node_info.py:
|
||||
maintainers: jwbernin
|
||||
maintainers: jwbernin krauthosting
|
||||
$modules/proxmox_storage_contents_info.py:
|
||||
maintainers: l00ptr
|
||||
maintainers: l00ptr krauthosting
|
||||
$modules/proxmox_tasks_info:
|
||||
maintainers: paginabianca
|
||||
maintainers: paginabianca krauthosting
|
||||
$modules/proxmox_template.py:
|
||||
ignore: skvidal
|
||||
maintainers: UnderGreen
|
||||
maintainers: UnderGreen krauthosting
|
||||
$modules/pubnub_blocks.py:
|
||||
maintainers: parfeon pubnub
|
||||
$modules/pulp_repo.py:
|
||||
@@ -1436,6 +1456,8 @@ files:
|
||||
maintainers: felixfontein giner
|
||||
docs/docsite/rst/filter_guide_abstract_informations_grouping.rst:
|
||||
maintainers: felixfontein
|
||||
docs/docsite/rst/filter_guide_abstract_informations_lists_helper.rst:
|
||||
maintainers: cfiehe
|
||||
docs/docsite/rst/filter_guide_abstract_informations_merging_lists_of_dictionaries.rst:
|
||||
maintainers: vbotka
|
||||
docs/docsite/rst/filter_guide_conversions.rst:
|
||||
@@ -1490,7 +1512,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
|
||||
team_consul: sgargan apollo13
|
||||
team_cyberark_conjur: jvanderhoof ryanprior
|
||||
team_e_spirit: MatrixCrawler getjack
|
||||
team_flatpak: JayKayy oolongbrothers
|
||||
|
||||
20
.github/workflows/import-galaxy.yml
vendored
Normal file
20
.github/workflows/import-galaxy.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
# 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: import-galaxy
|
||||
'on':
|
||||
# Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- stable-*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
import-galaxy:
|
||||
permissions:
|
||||
contents: read
|
||||
name: Test to import built collection artifact with Galaxy importer
|
||||
uses: ansible-community/github-action-test-galaxy-import/.github/workflows/test-galaxy-import.yml@main
|
||||
9
.github/workflows/reuse.yml
vendored
9
.github/workflows/reuse.yml
vendored
@@ -26,10 +26,5 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install reuse
|
||||
|
||||
- name: Check REUSE compliance
|
||||
run: |
|
||||
reuse lint
|
||||
- name: REUSE Compliance Check
|
||||
uses: fsfe/reuse-action@v2
|
||||
|
||||
656
CHANGELOG.md
Normal file
656
CHANGELOG.md
Normal file
@@ -0,0 +1,656 @@
|
||||
# Community General Release Notes
|
||||
|
||||
**Topics**
|
||||
- <a href="#v8-4-0">v8\.4\.0</a>
|
||||
- <a href="#release-summary">Release Summary</a>
|
||||
- <a href="#minor-changes">Minor Changes</a>
|
||||
- <a href="#bugfixes">Bugfixes</a>
|
||||
- <a href="#new-plugins">New Plugins</a>
|
||||
- <a href="#callback">Callback</a>
|
||||
- <a href="#filter">Filter</a>
|
||||
- <a href="#new-modules">New Modules</a>
|
||||
- <a href="#v8-3-0">v8\.3\.0</a>
|
||||
- <a href="#release-summary-1">Release Summary</a>
|
||||
- <a href="#minor-changes-1">Minor Changes</a>
|
||||
- <a href="#deprecated-features">Deprecated Features</a>
|
||||
- <a href="#bugfixes-1">Bugfixes</a>
|
||||
- <a href="#new-modules-1">New Modules</a>
|
||||
- <a href="#v8-2-0">v8\.2\.0</a>
|
||||
- <a href="#release-summary-2">Release Summary</a>
|
||||
- <a href="#minor-changes-2">Minor Changes</a>
|
||||
- <a href="#bugfixes-2">Bugfixes</a>
|
||||
- <a href="#new-plugins-1">New Plugins</a>
|
||||
- <a href="#connection">Connection</a>
|
||||
- <a href="#filter-1">Filter</a>
|
||||
- <a href="#lookup">Lookup</a>
|
||||
- <a href="#new-modules-2">New Modules</a>
|
||||
- <a href="#v8-1-0">v8\.1\.0</a>
|
||||
- <a href="#release-summary-3">Release Summary</a>
|
||||
- <a href="#minor-changes-3">Minor Changes</a>
|
||||
- <a href="#bugfixes-3">Bugfixes</a>
|
||||
- <a href="#new-plugins-2">New Plugins</a>
|
||||
- <a href="#lookup-1">Lookup</a>
|
||||
- <a href="#test">Test</a>
|
||||
- <a href="#new-modules-3">New Modules</a>
|
||||
- <a href="#v8-0-2">v8\.0\.2</a>
|
||||
- <a href="#release-summary-4">Release Summary</a>
|
||||
- <a href="#bugfixes-4">Bugfixes</a>
|
||||
- <a href="#v8-0-1">v8\.0\.1</a>
|
||||
- <a href="#release-summary-5">Release Summary</a>
|
||||
- <a href="#bugfixes-5">Bugfixes</a>
|
||||
- <a href="#v8-0-0">v8\.0\.0</a>
|
||||
- <a href="#release-summary-6">Release Summary</a>
|
||||
- <a href="#minor-changes-4">Minor Changes</a>
|
||||
- <a href="#breaking-changes--porting-guide">Breaking Changes / Porting Guide</a>
|
||||
- <a href="#deprecated-features-1">Deprecated Features</a>
|
||||
- <a href="#removed-features-previously-deprecated">Removed Features \(previously deprecated\)</a>
|
||||
- <a href="#bugfixes-6">Bugfixes</a>
|
||||
- <a href="#known-issues">Known Issues</a>
|
||||
- <a href="#new-plugins-3">New Plugins</a>
|
||||
- <a href="#lookup-2">Lookup</a>
|
||||
- <a href="#new-modules-4">New Modules</a>
|
||||
This changelog describes changes after version 7\.0\.0\.
|
||||
|
||||
<a id="v8-4-0"></a>
|
||||
## v8\.4\.0
|
||||
|
||||
<a id="release-summary"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix and feature release\.
|
||||
|
||||
<a id="minor-changes"></a>
|
||||
### Minor Changes
|
||||
|
||||
* bitwarden lookup plugin \- add <code>bw\_session</code> option\, to pass session key instead of reading from env \([https\://github\.com/ansible\-collections/community\.general/pull/7994](https\://github\.com/ansible\-collections/community\.general/pull/7994)\)\.
|
||||
* gitlab\_deploy\_key\, gitlab\_group\_members\, gitlab\_group\_variable\, gitlab\_hook\, gitlab\_instance\_variable\, gitlab\_project\_badge\, gitlab\_project\_variable\, gitlab\_user \- improve API pagination and compatibility with different versions of <code>python\-gitlab</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7790](https\://github\.com/ansible\-collections/community\.general/pull/7790)\)\.
|
||||
* gitlab\_hook \- adds <code>releases\_events</code> parameter for supporting Releases events triggers on GitLab hooks \([https\://github\.com/ansible\-collections/community\.general/pull/7956](https\://github\.com/ansible\-collections/community\.general/pull/7956)\)\.
|
||||
* icinga2 inventory plugin \- add Jinja2 templating support to <code>url</code>\, <code>user</code>\, and <code>password</code> paramenters \([https\://github\.com/ansible\-collections/community\.general/issues/7074](https\://github\.com/ansible\-collections/community\.general/issues/7074)\, [https\://github\.com/ansible\-collections/community\.general/pull/7996](https\://github\.com/ansible\-collections/community\.general/pull/7996)\)\.
|
||||
* mssql\_script \- adds transactional \(rollback/commit\) support via optional boolean param <code>transaction</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7976](https\://github\.com/ansible\-collections/community\.general/pull/7976)\)\.
|
||||
* proxmox\_kvm \- add parameter <code>update\_unsafe</code> to avoid limitations when updating dangerous values \([https\://github\.com/ansible\-collections/community\.general/pull/7843](https\://github\.com/ansible\-collections/community\.general/pull/7843)\)\.
|
||||
* redfish\_config \- add command <code>SetServiceIdentification</code> to set service identification \([https\://github\.com/ansible\-collections/community\.general/issues/7916](https\://github\.com/ansible\-collections/community\.general/issues/7916)\)\.
|
||||
* sudoers \- add support for the <code>NOEXEC</code> tag in sudoers rules \([https\://github\.com/ansible\-collections/community\.general/pull/7983](https\://github\.com/ansible\-collections/community\.general/pull/7983)\)\.
|
||||
* terraform \- fix <code>diff\_mode</code> in state <code>absent</code> and when terraform <code>resource\_changes</code> does not exist \([https\://github\.com/ansible\-collections/community\.general/pull/7963](https\://github\.com/ansible\-collections/community\.general/pull/7963)\)\.
|
||||
|
||||
<a id="bugfixes"></a>
|
||||
### Bugfixes
|
||||
|
||||
* cargo \- fix idempotency issues when using a custom installation path for packages \(using the <code>\-\-path</code> parameter\)\. The initial installation runs fine\, but subsequent runs use the <code>get\_installed\(\)</code> function which did not check the given installation location\, before running <code>cargo install</code>\. This resulted in a false <code>changed</code> state\. Also the removal of packeges using <code>state\: absent</code> failed\, as the installation check did not use the given parameter \([https\://github\.com/ansible\-collections/community\.general/pull/7970](https\://github\.com/ansible\-collections/community\.general/pull/7970)\)\.
|
||||
* gitlab\_issue \- fix behavior to search GitLab issue\, using <code>search</code> keyword instead of <code>title</code> \([https\://github\.com/ansible\-collections/community\.general/issues/7846](https\://github\.com/ansible\-collections/community\.general/issues/7846)\)\.
|
||||
* gitlab\_runner \- fix pagination when checking for existing runners \([https\://github\.com/ansible\-collections/community\.general/pull/7790](https\://github\.com/ansible\-collections/community\.general/pull/7790)\)\.
|
||||
* keycloak\_client \- fixes issue when metadata is provided in desired state when task is in check mode \([https\://github\.com/ansible\-collections/community\.general/issues/1226](https\://github\.com/ansible\-collections/community\.general/issues/1226)\, [https\://github\.com/ansible\-collections/community\.general/pull/7881](https\://github\.com/ansible\-collections/community\.general/pull/7881)\)\.
|
||||
* modprobe \- listing modules files or modprobe files could trigger a FileNotFoundError if <code>/etc/modprobe\.d</code> or <code>/etc/modules\-load\.d</code> did not exist\. Relevant functions now return empty lists if the directories do not exist to avoid crashing the module \([https\://github\.com/ansible\-collections/community\.general/issues/7717](https\://github\.com/ansible\-collections/community\.general/issues/7717)\)\.
|
||||
* onepassword lookup plugin \- failed for fields that were in sections and had uppercase letters in the label/ID\. Field lookups are now case insensitive in all cases \([https\://github\.com/ansible\-collections/community\.general/pull/7919](https\://github\.com/ansible\-collections/community\.general/pull/7919)\)\.
|
||||
* pkgin \- pkgin \(pkgsrc package manager used by SmartOS\) raises erratic exceptions and spurious <code>changed\=true</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7971](https\://github\.com/ansible\-collections/community\.general/pull/7971)\)\.
|
||||
* redfish\_info \- allow for a GET operation invoked by <code>GetUpdateStatus</code> to allow for an empty response body for cases where a service returns 204 No Content \([https\://github\.com/ansible\-collections/community\.general/issues/8003](https\://github\.com/ansible\-collections/community\.general/issues/8003)\)\.
|
||||
* redfish\_info \- correct uncaught exception when attempting to retrieve <code>Chassis</code> information \([https\://github\.com/ansible\-collections/community\.general/pull/7952](https\://github\.com/ansible\-collections/community\.general/pull/7952)\)\.
|
||||
|
||||
<a id="new-plugins"></a>
|
||||
### New Plugins
|
||||
|
||||
<a id="callback"></a>
|
||||
#### Callback
|
||||
|
||||
* default\_without\_diff \- The default ansible callback without diff output
|
||||
|
||||
<a id="filter"></a>
|
||||
#### Filter
|
||||
|
||||
* lists\_difference \- Difference of lists with a predictive order
|
||||
* lists\_intersect \- Intersection of lists with a predictive order
|
||||
* lists\_symmetric\_difference \- Symmetric Difference of lists with a predictive order
|
||||
* lists\_union \- Union of lists with a predictive order
|
||||
|
||||
<a id="new-modules"></a>
|
||||
### New Modules
|
||||
|
||||
* gitlab\_group\_access\_token \- Manages GitLab group access tokens
|
||||
* gitlab\_project\_access\_token \- Manages GitLab project access tokens
|
||||
|
||||
<a id="v8-3-0"></a>
|
||||
## v8\.3\.0
|
||||
|
||||
<a id="release-summary-1"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix and feature release\.
|
||||
|
||||
<a id="minor-changes-1"></a>
|
||||
### Minor Changes
|
||||
|
||||
* consul\_auth\_method\, consul\_binding\_rule\, consul\_policy\, consul\_role\, consul\_session\, consul\_token \- added action group <code>community\.general\.consul</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7897](https\://github\.com/ansible\-collections/community\.general/pull/7897)\)\.
|
||||
* consul\_policy \- added support for diff and check mode \([https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* consul\_policy\, consul\_role\, consul\_session \- removed dependency on <code>requests</code> and factored out common parts \([https\://github\.com/ansible\-collections/community\.general/pull/7826](https\://github\.com/ansible\-collections/community\.general/pull/7826)\, [https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* consul\_role \- <code>node\_identities</code> now expects a <code>node\_name</code> option to match the Consul API\, the old <code>name</code> is still supported as alias \([https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* consul\_role \- <code>service\_identities</code> now expects a <code>service\_name</code> option to match the Consul API\, the old <code>name</code> is still supported as alias \([https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* consul\_role \- added support for diff mode \([https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* consul\_role \- added support for templated policies \([https\://github\.com/ansible\-collections/community\.general/pull/7878](https\://github\.com/ansible\-collections/community\.general/pull/7878)\)\.
|
||||
* redfish\_info \- add command <code>GetServiceIdentification</code> to get service identification \([https\://github\.com/ansible\-collections/community\.general/issues/7882](https\://github\.com/ansible\-collections/community\.general/issues/7882)\)\.
|
||||
* terraform \- add support for <code>diff\_mode</code> for terraform resource\_changes \([https\://github\.com/ansible\-collections/community\.general/pull/7896](https\://github\.com/ansible\-collections/community\.general/pull/7896)\)\.
|
||||
|
||||
<a id="deprecated-features"></a>
|
||||
### Deprecated Features
|
||||
|
||||
* consul\_acl \- the module has been deprecated and will be removed in community\.general 10\.0\.0\. <code>consul\_token</code> and <code>consul\_policy</code> can be used instead \([https\://github\.com/ansible\-collections/community\.general/pull/7901](https\://github\.com/ansible\-collections/community\.general/pull/7901)\)\.
|
||||
|
||||
<a id="bugfixes-1"></a>
|
||||
### Bugfixes
|
||||
|
||||
* homebrew \- detect already installed formulae and casks using JSON output from <code>brew info</code> \([https\://github\.com/ansible\-collections/community\.general/issues/864](https\://github\.com/ansible\-collections/community\.general/issues/864)\)\.
|
||||
* incus connection plugin \- treats <code>inventory\_hostname</code> as a variable instead of a literal in remote connections \([https\://github\.com/ansible\-collections/community\.general/issues/7874](https\://github\.com/ansible\-collections/community\.general/issues/7874)\)\.
|
||||
* ipa\_otptoken \- the module expect <code>ipatokendisabled</code> as string but the <code>ipatokendisabled</code> value is returned as a boolean \([https\://github\.com/ansible\-collections/community\.general/pull/7795](https\://github\.com/ansible\-collections/community\.general/pull/7795)\)\.
|
||||
* ldap \- previously the order number \(if present\) was expected to follow an equals sign in the DN\. This makes it so the order number string is identified correctly anywhere within the DN \([https\://github\.com/ansible\-collections/community\.general/issues/7646](https\://github\.com/ansible\-collections/community\.general/issues/7646)\)\.
|
||||
* mssql\_script \- make the module work with Python 2 \([https\://github\.com/ansible\-collections/community\.general/issues/7818](https\://github\.com/ansible\-collections/community\.general/issues/7818)\, [https\://github\.com/ansible\-collections/community\.general/pull/7821](https\://github\.com/ansible\-collections/community\.general/pull/7821)\)\.
|
||||
* nmcli \- fix <code>connection\.slave\-type</code> wired to <code>bond</code> and not with parameter <code>slave\_type</code> in case of connection type <code>wifi</code> \([https\://github\.com/ansible\-collections/community\.general/issues/7389](https\://github\.com/ansible\-collections/community\.general/issues/7389)\)\.
|
||||
* proxmox \- fix updating a container config if the setting does not already exist \([https\://github\.com/ansible\-collections/community\.general/pull/7872](https\://github\.com/ansible\-collections/community\.general/pull/7872)\)\.
|
||||
|
||||
<a id="new-modules-1"></a>
|
||||
### New Modules
|
||||
|
||||
* consul\_acl\_bootstrap \- Bootstrap ACLs in Consul
|
||||
* consul\_auth\_method \- Manipulate Consul auth methods
|
||||
* consul\_binding\_rule \- Manipulate Consul binding rules
|
||||
* consul\_token \- Manipulate Consul tokens
|
||||
* gitlab\_label \- Creates/updates/deletes GitLab Labels belonging to project or group\.
|
||||
* gitlab\_milestone \- Creates/updates/deletes GitLab Milestones belonging to project or group
|
||||
|
||||
<a id="v8-2-0"></a>
|
||||
## v8\.2\.0
|
||||
|
||||
<a id="release-summary-2"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix and feature release\.
|
||||
|
||||
<a id="minor-changes-2"></a>
|
||||
### Minor Changes
|
||||
|
||||
* ipa\_dnsrecord \- adds ability to manage NS record types \([https\://github\.com/ansible\-collections/community\.general/pull/7737](https\://github\.com/ansible\-collections/community\.general/pull/7737)\)\.
|
||||
* ipa\_pwpolicy \- refactor module and exchange a sequence <code>if</code> statements with a <code>for</code> loop \([https\://github\.com/ansible\-collections/community\.general/pull/7723](https\://github\.com/ansible\-collections/community\.general/pull/7723)\)\.
|
||||
* ipa\_pwpolicy \- update module to support <code>maxrepeat</code>\, <code>maxsequence</code>\, <code>dictcheck</code>\, <code>usercheck</code>\, <code>gracelimit</code> parameters in FreeIPA password policies \([https\://github\.com/ansible\-collections/community\.general/pull/7723](https\://github\.com/ansible\-collections/community\.general/pull/7723)\)\.
|
||||
* keycloak\_realm\_key \- the <code>config\.algorithm</code> option now supports 8 additional key algorithms \([https\://github\.com/ansible\-collections/community\.general/pull/7698](https\://github\.com/ansible\-collections/community\.general/pull/7698)\)\.
|
||||
* keycloak\_realm\_key \- the <code>config\.certificate</code> option value is no longer defined with <code>no\_log\=True</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7698](https\://github\.com/ansible\-collections/community\.general/pull/7698)\)\.
|
||||
* keycloak\_realm\_key \- the <code>provider\_id</code> option now supports RSA encryption key usage \(value <code>rsa\-enc</code>\) \([https\://github\.com/ansible\-collections/community\.general/pull/7698](https\://github\.com/ansible\-collections/community\.general/pull/7698)\)\.
|
||||
* keycloak\_user\_federation \- allow custom user storage providers to be set through <code>provider\_id</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7789](https\://github\.com/ansible\-collections/community\.general/pull/7789)\)\.
|
||||
* mail \- add <code>Message\-ID</code> header\; which is required by some mail servers \([https\://github\.com/ansible\-collections/community\.general/pull/7740](https\://github\.com/ansible\-collections/community\.general/pull/7740)\)\.
|
||||
* mail module\, mail callback plugin \- allow to configure the domain name of the Message\-ID header with a new <code>message\_id\_domain</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7765](https\://github\.com/ansible\-collections/community\.general/pull/7765)\)\.
|
||||
* ssh\_config \- new feature to set <code>AddKeysToAgent</code> option to <code>yes</code> or <code>no</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7703](https\://github\.com/ansible\-collections/community\.general/pull/7703)\)\.
|
||||
* ssh\_config \- new feature to set <code>IdentitiesOnly</code> option to <code>yes</code> or <code>no</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7704](https\://github\.com/ansible\-collections/community\.general/pull/7704)\)\.
|
||||
* xcc\_redfish\_command \- added support for raw POSTs \(<code>command\=PostResource</code> in <code>category\=Raw</code>\) without a specific action info \([https\://github\.com/ansible\-collections/community\.general/pull/7746](https\://github\.com/ansible\-collections/community\.general/pull/7746)\)\.
|
||||
|
||||
<a id="bugfixes-2"></a>
|
||||
### Bugfixes
|
||||
|
||||
* keycloak\_identity\_provider \- <code>mappers</code> processing was not idempotent if the mappers configuration list had not been sorted by name \(in ascending order\)\. Fix resolves the issue by sorting mappers in the desired state using the same key which is used for obtaining existing state \([https\://github\.com/ansible\-collections/community\.general/pull/7418](https\://github\.com/ansible\-collections/community\.general/pull/7418)\)\.
|
||||
* keycloak\_identity\_provider \- it was not possible to reconfigure \(add\, remove\) <code>mappers</code> once they were created initially\. Removal was ignored\, adding new ones resulted in dropping the pre\-existing unmodified mappers\. Fix resolves the issue by supplying correct input to the internal update call \([https\://github\.com/ansible\-collections/community\.general/pull/7418](https\://github\.com/ansible\-collections/community\.general/pull/7418)\)\.
|
||||
* keycloak\_user \- when <code>force</code> is set\, but user does not exist\, do not try to delete it \([https\://github\.com/ansible\-collections/community\.general/pull/7696](https\://github\.com/ansible\-collections/community\.general/pull/7696)\)\.
|
||||
* proxmox\_kvm \- running <code>state\=template</code> will first check whether VM is already a template \([https\://github\.com/ansible\-collections/community\.general/pull/7792](https\://github\.com/ansible\-collections/community\.general/pull/7792)\)\.
|
||||
* statusio\_maintenance \- fix error caused by incorrectly formed API data payload\. Was raising \"Failed to create maintenance HTTP Error 400 Bad Request\" caused by bad data type for date/time and deprecated dict keys \([https\://github\.com/ansible\-collections/community\.general/pull/7754](https\://github\.com/ansible\-collections/community\.general/pull/7754)\)\.
|
||||
|
||||
<a id="new-plugins-1"></a>
|
||||
### New Plugins
|
||||
|
||||
<a id="connection"></a>
|
||||
#### Connection
|
||||
|
||||
* incus \- Run tasks in Incus instances via the Incus CLI\.
|
||||
|
||||
<a id="filter-1"></a>
|
||||
#### Filter
|
||||
|
||||
* from\_ini \- Converts INI text input into a dictionary
|
||||
* to\_ini \- Converts a dictionary to the INI file format
|
||||
|
||||
<a id="lookup"></a>
|
||||
#### Lookup
|
||||
|
||||
* github\_app\_access\_token \- Obtain short\-lived Github App Access tokens
|
||||
|
||||
<a id="new-modules-2"></a>
|
||||
### New Modules
|
||||
|
||||
* dnf\_config\_manager \- Enable or disable dnf repositories using config\-manager
|
||||
* keycloak\_component\_info \- Retrive component info in Keycloak
|
||||
* keycloak\_realm\_rolemapping \- Allows administration of Keycloak realm role mappings into groups with the Keycloak API
|
||||
* proxmox\_node\_info \- Retrieve information about one or more Proxmox VE nodes
|
||||
* proxmox\_storage\_contents\_info \- List content from a Proxmox VE storage
|
||||
|
||||
<a id="v8-1-0"></a>
|
||||
## v8\.1\.0
|
||||
|
||||
<a id="release-summary-3"></a>
|
||||
### Release Summary
|
||||
|
||||
Regular bugfix and feature release\.
|
||||
|
||||
<a id="minor-changes-3"></a>
|
||||
### Minor Changes
|
||||
|
||||
* bitwarden lookup plugin \- when looking for items using an item ID\, the item is now accessed directly with <code>bw get item</code> instead of searching through all items\. This doubles the lookup speed \([https\://github\.com/ansible\-collections/community\.general/pull/7468](https\://github\.com/ansible\-collections/community\.general/pull/7468)\)\.
|
||||
* elastic callback plugin \- close elastic client to not leak resources \([https\://github\.com/ansible\-collections/community\.general/pull/7517](https\://github\.com/ansible\-collections/community\.general/pull/7517)\)\.
|
||||
* git\_config \- allow multiple git configs for the same name with the new <code>add\_mode</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7260](https\://github\.com/ansible\-collections/community\.general/pull/7260)\)\.
|
||||
* git\_config \- the <code>after</code> and <code>before</code> fields in the <code>diff</code> of the return value can be a list instead of a string in case more configs with the same key are affected \([https\://github\.com/ansible\-collections/community\.general/pull/7260](https\://github\.com/ansible\-collections/community\.general/pull/7260)\)\.
|
||||
* git\_config \- when a value is unset\, all configs with the same key are unset \([https\://github\.com/ansible\-collections/community\.general/pull/7260](https\://github\.com/ansible\-collections/community\.general/pull/7260)\)\.
|
||||
* gitlab modules \- add <code>ca\_path</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7472](https\://github\.com/ansible\-collections/community\.general/pull/7472)\)\.
|
||||
* gitlab modules \- remove duplicate <code>gitlab</code> package check \([https\://github\.com/ansible\-collections/community\.general/pull/7486](https\://github\.com/ansible\-collections/community\.general/pull/7486)\)\.
|
||||
* gitlab\_runner \- add support for new runner creation workflow \([https\://github\.com/ansible\-collections/community\.general/pull/7199](https\://github\.com/ansible\-collections/community\.general/pull/7199)\)\.
|
||||
* ipa\_config \- adds <code>passkey</code> choice to <code>ipauserauthtype</code> parameter\'s choices \([https\://github\.com/ansible\-collections/community\.general/pull/7588](https\://github\.com/ansible\-collections/community\.general/pull/7588)\)\.
|
||||
* ipa\_sudorule \- adds options to include denied commands or command groups \([https\://github\.com/ansible\-collections/community\.general/pull/7415](https\://github\.com/ansible\-collections/community\.general/pull/7415)\)\.
|
||||
* ipa\_user \- adds <code>idp</code> and <code>passkey</code> choice to <code>ipauserauthtype</code> parameter\'s choices \([https\://github\.com/ansible\-collections/community\.general/pull/7589](https\://github\.com/ansible\-collections/community\.general/pull/7589)\)\.
|
||||
* irc \- add <code>validate\_certs</code> option\, and rename <code>use\_ssl</code> to <code>use\_tls</code>\, while keeping <code>use\_ssl</code> as an alias\. The default value for <code>validate\_certs</code> is <code>false</code> for backwards compatibility\. We recommend to every user of this module to explicitly set <code>use\_tls\=true</code> and <em class="title-reference">validate\_certs\=true\`</em> whenever possible\, especially when communicating to IRC servers over the internet \([https\://github\.com/ansible\-collections/community\.general/pull/7550](https\://github\.com/ansible\-collections/community\.general/pull/7550)\)\.
|
||||
* keycloak module utils \- expose error message from Keycloak server for HTTP errors in some specific situations \([https\://github\.com/ansible\-collections/community\.general/pull/7645](https\://github\.com/ansible\-collections/community\.general/pull/7645)\)\.
|
||||
* keycloak\_user\_federation \- add option for <code>krbPrincipalAttribute</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7538](https\://github\.com/ansible\-collections/community\.general/pull/7538)\)\.
|
||||
* lvol \- change <code>pvs</code> argument type to list of strings \([https\://github\.com/ansible\-collections/community\.general/pull/7676](https\://github\.com/ansible\-collections/community\.general/pull/7676)\, [https\://github\.com/ansible\-collections/community\.general/issues/7504](https\://github\.com/ansible\-collections/community\.general/issues/7504)\)\.
|
||||
* lxd connection plugin \- tighten the detection logic for lxd <code>Instance not found</code> errors\, to avoid false detection on unrelated errors such as <code>/usr/bin/python3\: not found</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7521](https\://github\.com/ansible\-collections/community\.general/pull/7521)\)\.
|
||||
* netcup\_dns \- adds support for record types <code>OPENPGPKEY</code>\, <code>SMIMEA</code>\, and <code>SSHFP</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7489](https\://github\.com/ansible\-collections/community\.general/pull/7489)\)\.
|
||||
* nmcli \- add support for new connection type <code>loopback</code> \([https\://github\.com/ansible\-collections/community\.general/issues/6572](https\://github\.com/ansible\-collections/community\.general/issues/6572)\)\.
|
||||
* nmcli \- allow for <code>infiniband</code> slaves of <code>bond</code> interface types \([https\://github\.com/ansible\-collections/community\.general/pull/7569](https\://github\.com/ansible\-collections/community\.general/pull/7569)\)\.
|
||||
* nmcli \- allow for the setting of <code>MTU</code> for <code>infiniband</code> and <code>bond</code> interface types \([https\://github\.com/ansible\-collections/community\.general/pull/7499](https\://github\.com/ansible\-collections/community\.general/pull/7499)\)\.
|
||||
* onepassword lookup plugin \- support 1Password Connect with the opv2 client by setting the connect\_host and connect\_token parameters \([https\://github\.com/ansible\-collections/community\.general/pull/7116](https\://github\.com/ansible\-collections/community\.general/pull/7116)\)\.
|
||||
* onepassword\_raw lookup plugin \- support 1Password Connect with the opv2 client by setting the connect\_host and connect\_token parameters \([https\://github\.com/ansible\-collections/community\.general/pull/7116](https\://github\.com/ansible\-collections/community\.general/pull/7116)\)
|
||||
* passwordstore \- adds <code>timestamp</code> and <code>preserve</code> parameters to modify the stored password format \([https\://github\.com/ansible\-collections/community\.general/pull/7426](https\://github\.com/ansible\-collections/community\.general/pull/7426)\)\.
|
||||
* proxmox \- adds <code>template</code> value to the <code>state</code> parameter\, allowing conversion of container to a template \([https\://github\.com/ansible\-collections/community\.general/pull/7143](https\://github\.com/ansible\-collections/community\.general/pull/7143)\)\.
|
||||
* proxmox \- adds <code>update</code> parameter\, allowing update of an already existing containers configuration \([https\://github\.com/ansible\-collections/community\.general/pull/7540](https\://github\.com/ansible\-collections/community\.general/pull/7540)\)\.
|
||||
* proxmox inventory plugin \- adds an option to exclude nodes from the dynamic inventory generation\. The new setting is optional\, not using this option will behave as usual \([https\://github\.com/ansible\-collections/community\.general/issues/6714](https\://github\.com/ansible\-collections/community\.general/issues/6714)\, [https\://github\.com/ansible\-collections/community\.general/pull/7461](https\://github\.com/ansible\-collections/community\.general/pull/7461)\)\.
|
||||
* proxmox\_disk \- add ability to manipulate CD\-ROM drive \([https\://github\.com/ansible\-collections/community\.general/pull/7495](https\://github\.com/ansible\-collections/community\.general/pull/7495)\)\.
|
||||
* proxmox\_kvm \- adds <code>template</code> value to the <code>state</code> parameter\, allowing conversion of a VM to a template \([https\://github\.com/ansible\-collections/community\.general/pull/7143](https\://github\.com/ansible\-collections/community\.general/pull/7143)\)\.
|
||||
* proxmox\_kvm \- support the <code>hookscript</code> parameter \([https\://github\.com/ansible\-collections/community\.general/issues/7600](https\://github\.com/ansible\-collections/community\.general/issues/7600)\)\.
|
||||
* proxmox\_ostype \- it is now possible to specify the <code>ostype</code> when creating an LXC container \([https\://github\.com/ansible\-collections/community\.general/pull/7462](https\://github\.com/ansible\-collections/community\.general/pull/7462)\)\.
|
||||
* proxmox\_vm\_info \- add ability to retrieve configuration info \([https\://github\.com/ansible\-collections/community\.general/pull/7485](https\://github\.com/ansible\-collections/community\.general/pull/7485)\)\.
|
||||
* redfish\_info \- adding the <code>BootProgress</code> property when getting <code>Systems</code> info \([https\://github\.com/ansible\-collections/community\.general/pull/7626](https\://github\.com/ansible\-collections/community\.general/pull/7626)\)\.
|
||||
* ssh\_config \- adds <code>controlmaster</code>\, <code>controlpath</code> and <code>controlpersist</code> parameters \([https\://github\.com/ansible\-collections/community\.general/pull/7456](https\://github\.com/ansible\-collections/community\.general/pull/7456)\)\.
|
||||
|
||||
<a id="bugfixes-3"></a>
|
||||
### Bugfixes
|
||||
|
||||
* apt\-rpm \- the module did not upgrade packages if a newer version exists\. Now the package will be reinstalled if the candidate is newer than the installed version \([https\://github\.com/ansible\-collections/community\.general/issues/7414](https\://github\.com/ansible\-collections/community\.general/issues/7414)\)\.
|
||||
* cloudflare\_dns \- fix Cloudflare lookup of SHFP records \([https\://github\.com/ansible\-collections/community\.general/issues/7652](https\://github\.com/ansible\-collections/community\.general/issues/7652)\)\.
|
||||
* interface\_files \- also consider <code>address\_family</code> when changing <code>option\=method</code> \([https\://github\.com/ansible\-collections/community\.general/issues/7610](https\://github\.com/ansible\-collections/community\.general/issues/7610)\, [https\://github\.com/ansible\-collections/community\.general/pull/7612](https\://github\.com/ansible\-collections/community\.general/pull/7612)\)\.
|
||||
* irc \- replace <code>ssl\.wrap\_socket</code> that was removed from Python 3\.12 with code for creating a proper SSL context \([https\://github\.com/ansible\-collections/community\.general/pull/7542](https\://github\.com/ansible\-collections/community\.general/pull/7542)\)\.
|
||||
* keycloak\_\* \- fix Keycloak API client to quote <code>/</code> properly \([https\://github\.com/ansible\-collections/community\.general/pull/7641](https\://github\.com/ansible\-collections/community\.general/pull/7641)\)\.
|
||||
* keycloak\_authz\_permission \- resource payload variable for scope\-based permission was constructed as a string\, when it needs to be a list\, even for a single item \([https\://github\.com/ansible\-collections/community\.general/issues/7151](https\://github\.com/ansible\-collections/community\.general/issues/7151)\)\.
|
||||
* log\_entries callback plugin \- replace <code>ssl\.wrap\_socket</code> that was removed from Python 3\.12 with code for creating a proper SSL context \([https\://github\.com/ansible\-collections/community\.general/pull/7542](https\://github\.com/ansible\-collections/community\.general/pull/7542)\)\.
|
||||
* lvol \- test for output messages in both <code>stdout</code> and <code>stderr</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7601](https\://github\.com/ansible\-collections/community\.general/pull/7601)\, [https\://github\.com/ansible\-collections/community\.general/issues/7182](https\://github\.com/ansible\-collections/community\.general/issues/7182)\)\.
|
||||
* onepassword lookup plugin \- field and section titles are now case insensitive when using op CLI version two or later\. This matches the behavior of version one \([https\://github\.com/ansible\-collections/community\.general/pull/7564](https\://github\.com/ansible\-collections/community\.general/pull/7564)\)\.
|
||||
* redhat\_subscription \- use the D\-Bus registration on RHEL 7 only on 7\.4 and
|
||||
greater\; older versions of RHEL 7 do not have it
|
||||
\([https\://github\.com/ansible\-collections/community\.general/issues/7622](https\://github\.com/ansible\-collections/community\.general/issues/7622)\,
|
||||
[https\://github\.com/ansible\-collections/community\.general/pull/7624](https\://github\.com/ansible\-collections/community\.general/pull/7624)\)\.
|
||||
* terraform \- fix multiline string handling in complex variables \([https\://github\.com/ansible\-collections/community\.general/pull/7535](https\://github\.com/ansible\-collections/community\.general/pull/7535)\)\.
|
||||
|
||||
<a id="new-plugins-2"></a>
|
||||
### New Plugins
|
||||
|
||||
<a id="lookup-1"></a>
|
||||
#### Lookup
|
||||
|
||||
* onepassword\_doc \- Fetch documents stored in 1Password
|
||||
|
||||
<a id="test"></a>
|
||||
#### Test
|
||||
|
||||
* fqdn\_valid \- Validates fully\-qualified domain names against RFC 1123
|
||||
|
||||
<a id="new-modules-3"></a>
|
||||
### New Modules
|
||||
|
||||
* git\_config\_info \- Read git configuration
|
||||
* gitlab\_issue \- Create\, update\, or delete GitLab issues
|
||||
* nomad\_token \- Manage Nomad ACL tokens
|
||||
|
||||
<a id="v8-0-2"></a>
|
||||
## v8\.0\.2
|
||||
|
||||
<a id="release-summary-4"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release for inclusion in Ansible 9\.0\.0rc1\.
|
||||
|
||||
<a id="bugfixes-4"></a>
|
||||
### Bugfixes
|
||||
|
||||
* ocapi\_utils\, oci\_utils\, redfish\_utils module utils \- replace <code>type\(\)</code> calls with <code>isinstance\(\)</code> calls \([https\://github\.com/ansible\-collections/community\.general/pull/7501](https\://github\.com/ansible\-collections/community\.general/pull/7501)\)\.
|
||||
* pipx module utils \- change the CLI argument formatter for the <code>pip\_args</code> parameter \([https\://github\.com/ansible\-collections/community\.general/issues/7497](https\://github\.com/ansible\-collections/community\.general/issues/7497)\, [https\://github\.com/ansible\-collections/community\.general/pull/7506](https\://github\.com/ansible\-collections/community\.general/pull/7506)\)\.
|
||||
|
||||
<a id="v8-0-1"></a>
|
||||
## v8\.0\.1
|
||||
|
||||
<a id="release-summary-5"></a>
|
||||
### Release Summary
|
||||
|
||||
Bugfix release for inclusion in Ansible 9\.0\.0b1\.
|
||||
|
||||
<a id="bugfixes-5"></a>
|
||||
### Bugfixes
|
||||
|
||||
* gitlab\_group\_members \- fix gitlab constants call in <code>gitlab\_group\_members</code> module \([https\://github\.com/ansible\-collections/community\.general/issues/7467](https\://github\.com/ansible\-collections/community\.general/issues/7467)\)\.
|
||||
* gitlab\_project\_members \- fix gitlab constants call in <code>gitlab\_project\_members</code> module \([https\://github\.com/ansible\-collections/community\.general/issues/7467](https\://github\.com/ansible\-collections/community\.general/issues/7467)\)\.
|
||||
* gitlab\_protected\_branches \- fix gitlab constants call in <code>gitlab\_protected\_branches</code> module \([https\://github\.com/ansible\-collections/community\.general/issues/7467](https\://github\.com/ansible\-collections/community\.general/issues/7467)\)\.
|
||||
* gitlab\_user \- fix gitlab constants call in <code>gitlab\_user</code> module \([https\://github\.com/ansible\-collections/community\.general/issues/7467](https\://github\.com/ansible\-collections/community\.general/issues/7467)\)\.
|
||||
* proxmox\_pool\_member \- absent state for type VM did not delete VMs from the pools \([https\://github\.com/ansible\-collections/community\.general/pull/7464](https\://github\.com/ansible\-collections/community\.general/pull/7464)\)\.
|
||||
* redfish\_command \- fix usage of message parsing in <code>SimpleUpdate</code> and <code>MultipartHTTPPushUpdate</code> commands to treat the lack of a <code>MessageId</code> as no message \([https\://github\.com/ansible\-collections/community\.general/issues/7465](https\://github\.com/ansible\-collections/community\.general/issues/7465)\, [https\://github\.com/ansible\-collections/community\.general/pull/7471](https\://github\.com/ansible\-collections/community\.general/pull/7471)\)\.
|
||||
|
||||
<a id="v8-0-0"></a>
|
||||
## v8\.0\.0
|
||||
|
||||
<a id="release-summary-6"></a>
|
||||
### Release Summary
|
||||
|
||||
This is release 8\.0\.0 of <code>community\.general</code>\, released on 2023\-11\-01\.
|
||||
|
||||
<a id="minor-changes-4"></a>
|
||||
### Minor Changes
|
||||
|
||||
* The collection will start using semantic markup \([https\://github\.com/ansible\-collections/community\.general/pull/6539](https\://github\.com/ansible\-collections/community\.general/pull/6539)\)\.
|
||||
* VarDict module utils \- add method <code>VarDict\.as\_dict\(\)</code> to convert to a plain <code>dict</code> object \([https\://github\.com/ansible\-collections/community\.general/pull/6602](https\://github\.com/ansible\-collections/community\.general/pull/6602)\)\.
|
||||
* apt\_rpm \- extract package name from local <code>\.rpm</code> path when verifying
|
||||
installation success\. Allows installing packages from local <code>\.rpm</code> files
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/7396](https\://github\.com/ansible\-collections/community\.general/pull/7396)\)\.
|
||||
* cargo \- add option <code>executable</code>\, which allows user to specify path to the cargo binary \([https\://github\.com/ansible\-collections/community\.general/pull/7352](https\://github\.com/ansible\-collections/community\.general/pull/7352)\)\.
|
||||
* cargo \- add option <code>locked</code> which allows user to specify install the locked version of dependency instead of latest compatible version \([https\://github\.com/ansible\-collections/community\.general/pull/6134](https\://github\.com/ansible\-collections/community\.general/pull/6134)\)\.
|
||||
* chroot connection plugin \- add <code>disable\_root\_check</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7099](https\://github\.com/ansible\-collections/community\.general/pull/7099)\)\.
|
||||
* cloudflare\_dns \- add CAA record support \([https\://github\.com/ansible\-collections/community\.general/pull/7399](https\://github\.com/ansible\-collections/community\.general/pull/7399)\)\.
|
||||
* cobbler inventory plugin \- add <code>exclude\_mgmt\_classes</code> and <code>include\_mgmt\_classes</code> options to exclude or include hosts based on management classes \([https\://github\.com/ansible\-collections/community\.general/pull/7184](https\://github\.com/ansible\-collections/community\.general/pull/7184)\)\.
|
||||
* cobbler inventory plugin \- add <code>inventory\_hostname</code> option to allow using the system name for the inventory hostname \([https\://github\.com/ansible\-collections/community\.general/pull/6502](https\://github\.com/ansible\-collections/community\.general/pull/6502)\)\.
|
||||
* cobbler inventory plugin \- add <code>want\_ip\_addresses</code> option to collect all interface DNS name to IP address mapping \([https\://github\.com/ansible\-collections/community\.general/pull/6711](https\://github\.com/ansible\-collections/community\.general/pull/6711)\)\.
|
||||
* cobbler inventory plugin \- add primary IP addess to <code>cobbler\_ipv4\_address</code> and IPv6 address to <code>cobbler\_ipv6\_address</code> host variable \([https\://github\.com/ansible\-collections/community\.general/pull/6711](https\://github\.com/ansible\-collections/community\.general/pull/6711)\)\.
|
||||
* cobbler inventory plugin \- add warning for systems with empty profiles \([https\://github\.com/ansible\-collections/community\.general/pull/6502](https\://github\.com/ansible\-collections/community\.general/pull/6502)\)\.
|
||||
* cobbler inventory plugin \- convert Ansible unicode strings to native Python unicode strings before passing user/password to XMLRPC client \([https\://github\.com/ansible\-collections/community\.general/pull/6923](https\://github\.com/ansible\-collections/community\.general/pull/6923)\)\.
|
||||
* consul\_session \- drops requirement for the <code>python\-consul</code> library to communicate with the Consul API\, instead relying on the existing <code>requests</code> library requirement \([https\://github\.com/ansible\-collections/community\.general/pull/6755](https\://github\.com/ansible\-collections/community\.general/pull/6755)\)\.
|
||||
* copr \- respawn module to use the system python interpreter when the <code>dnf</code> python module is not available in <code>ansible\_python\_interpreter</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6522](https\://github\.com/ansible\-collections/community\.general/pull/6522)\)\.
|
||||
* cpanm \- minor refactor when creating the <code>CmdRunner</code> object \([https\://github\.com/ansible\-collections/community\.general/pull/7231](https\://github\.com/ansible\-collections/community\.general/pull/7231)\)\.
|
||||
* datadog\_monitor \- adds <code>notification\_preset\_name</code>\, <code>renotify\_occurrences</code> and <code>renotify\_statuses</code> parameters \([https\://github\.com/ansible\-collections/community\.general/issues/6521\,https\://github\.com/ansible\-collections/community\.general/issues/5823](https\://github\.com/ansible\-collections/community\.general/issues/6521\,https\://github\.com/ansible\-collections/community\.general/issues/5823)\)\.
|
||||
* dig lookup plugin \- add TCP option to enable the use of TCP connection during DNS lookup \([https\://github\.com/ansible\-collections/community\.general/pull/7343](https\://github\.com/ansible\-collections/community\.general/pull/7343)\)\.
|
||||
* ejabberd\_user \- module now using <code>CmdRunner</code> to execute external command \([https\://github\.com/ansible\-collections/community\.general/pull/7075](https\://github\.com/ansible\-collections/community\.general/pull/7075)\)\.
|
||||
* filesystem \- add <code>uuid</code> parameter for UUID change feature \([https\://github\.com/ansible\-collections/community\.general/pull/6680](https\://github\.com/ansible\-collections/community\.general/pull/6680)\)\.
|
||||
* gitlab\_group \- add option <code>force\_delete</code> \(default\: false\) which allows delete group even if projects exists in it \([https\://github\.com/ansible\-collections/community\.general/pull/7364](https\://github\.com/ansible\-collections/community\.general/pull/7364)\)\.
|
||||
* gitlab\_group\_variable \- add support for <code>raw</code> variables suboption \([https\://github\.com/ansible\-collections/community\.general/pull/7132](https\://github\.com/ansible\-collections/community\.general/pull/7132)\)\.
|
||||
* gitlab\_project\_variable \- add support for <code>raw</code> variables suboption \([https\://github\.com/ansible\-collections/community\.general/pull/7132](https\://github\.com/ansible\-collections/community\.general/pull/7132)\)\.
|
||||
* gitlab\_project\_variable \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6928](https\://github\.com/ansible\-collections/community\.general/pull/6928)\)\.
|
||||
* gitlab\_runner \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6927](https\://github\.com/ansible\-collections/community\.general/pull/6927)\)\.
|
||||
* htpasswd \- minor code improvements in the module \([https\://github\.com/ansible\-collections/community\.general/pull/6901](https\://github\.com/ansible\-collections/community\.general/pull/6901)\)\.
|
||||
* htpasswd \- the parameter <code>crypt\_scheme</code> is being renamed as <code>hash\_scheme</code> and added as an alias to it \([https\://github\.com/ansible\-collections/community\.general/pull/6841](https\://github\.com/ansible\-collections/community\.general/pull/6841)\)\.
|
||||
* icinga2\_host \- the <code>ip</code> option is no longer required\, since Icinga 2 allows for an empty address attribute \([https\://github\.com/ansible\-collections/community\.general/pull/7452](https\://github\.com/ansible\-collections/community\.general/pull/7452)\)\.
|
||||
* ini\_file \- add <code>ignore\_spaces</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7273](https\://github\.com/ansible\-collections/community\.general/pull/7273)\)\.
|
||||
* ini\_file \- add <code>modify\_inactive\_option</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7401](https\://github\.com/ansible\-collections/community\.general/pull/7401)\)\.
|
||||
* ipa\_config \- add module parameters to manage FreeIPA user and group objectclasses \([https\://github\.com/ansible\-collections/community\.general/pull/7019](https\://github\.com/ansible\-collections/community\.general/pull/7019)\)\.
|
||||
* ipa\_config \- adds <code>idp</code> choice to <code>ipauserauthtype</code> parameter\'s choices \([https\://github\.com/ansible\-collections/community\.general/pull/7051](https\://github\.com/ansible\-collections/community\.general/pull/7051)\)\.
|
||||
* jenkins\_build \- add new <code>detach</code> option\, which allows the module to exit successfully as long as the build is created \(default functionality is still waiting for the build to end before exiting\) \([https\://github\.com/ansible\-collections/community\.general/pull/7204](https\://github\.com/ansible\-collections/community\.general/pull/7204)\)\.
|
||||
* jenkins\_build \- add new <code>time\_between\_checks</code> option\, which allows to configure the wait time between requests to the Jenkins server \([https\://github\.com/ansible\-collections/community\.general/pull/7204](https\://github\.com/ansible\-collections/community\.general/pull/7204)\)\.
|
||||
* keycloak\_authentication \- added provider ID choices\, since Keycloak supports only those two specific ones \([https\://github\.com/ansible\-collections/community\.general/pull/6763](https\://github\.com/ansible\-collections/community\.general/pull/6763)\)\.
|
||||
* keycloak\_client\_rolemapping \- adds support for subgroups with additional parameter <code>parents</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6687](https\://github\.com/ansible\-collections/community\.general/pull/6687)\)\.
|
||||
* keycloak\_role \- add composite roles support for realm and client roles \([https\://github\.com/ansible\-collections/community\.general/pull/6469](https\://github\.com/ansible\-collections/community\.general/pull/6469)\)\.
|
||||
* keyring \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6927](https\://github\.com/ansible\-collections/community\.general/pull/6927)\)\.
|
||||
* ldap\_\* \- add new arguments <code>client\_cert</code> and <code>client\_key</code> to the LDAP modules in order to allow certificate authentication \([https\://github\.com/ansible\-collections/community\.general/pull/6668](https\://github\.com/ansible\-collections/community\.general/pull/6668)\)\.
|
||||
* ldap\_search \- add a new <code>page\_size</code> option to enable paged searches \([https\://github\.com/ansible\-collections/community\.general/pull/6648](https\://github\.com/ansible\-collections/community\.general/pull/6648)\)\.
|
||||
* locale\_gen \- module has been refactored to use <code>ModuleHelper</code> and <code>CmdRunner</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6903](https\://github\.com/ansible\-collections/community\.general/pull/6903)\)\.
|
||||
* locale\_gen \- module now using <code>CmdRunner</code> to execute external commands \([https\://github\.com/ansible\-collections/community\.general/pull/6820](https\://github\.com/ansible\-collections/community\.general/pull/6820)\)\.
|
||||
* lvg \- add <code>active</code> and <code>inactive</code> values to the <code>state</code> option for active state management feature \([https\://github\.com/ansible\-collections/community\.general/pull/6682](https\://github\.com/ansible\-collections/community\.general/pull/6682)\)\.
|
||||
* lvg \- add <code>reset\_vg\_uuid</code>\, <code>reset\_pv\_uuid</code> options for UUID reset feature \([https\://github\.com/ansible\-collections/community\.general/pull/6682](https\://github\.com/ansible\-collections/community\.general/pull/6682)\)\.
|
||||
* lxc connection plugin \- properly handle a change of the <code>remote\_addr</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7373](https\://github\.com/ansible\-collections/community\.general/pull/7373)\)\.
|
||||
* lxd connection plugin \- automatically translate <code>remote\_addr</code> from FQDN to \(short\) hostname \([https\://github\.com/ansible\-collections/community\.general/pull/7360](https\://github\.com/ansible\-collections/community\.general/pull/7360)\)\.
|
||||
* lxd connection plugin \- update error parsing to work with newer messages mentioning instances \([https\://github\.com/ansible\-collections/community\.general/pull/7360](https\://github\.com/ansible\-collections/community\.general/pull/7360)\)\.
|
||||
* lxd inventory plugin \- add <code>server\_cert</code> option for trust anchor to use for TLS verification of server certificates \([https\://github\.com/ansible\-collections/community\.general/pull/7392](https\://github\.com/ansible\-collections/community\.general/pull/7392)\)\.
|
||||
* lxd inventory plugin \- add <code>server\_check\_hostname</code> option to disable hostname verification of server certificates \([https\://github\.com/ansible\-collections/community\.general/pull/7392](https\://github\.com/ansible\-collections/community\.general/pull/7392)\)\.
|
||||
* make \- add new <code>targets</code> parameter allowing multiple targets to be used with <code>make</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6882](https\://github\.com/ansible\-collections/community\.general/pull/6882)\, [https\://github\.com/ansible\-collections/community\.general/issues/4919](https\://github\.com/ansible\-collections/community\.general/issues/4919)\)\.
|
||||
* make \- allows <code>params</code> to be used without value \([https\://github\.com/ansible\-collections/community\.general/pull/7180](https\://github\.com/ansible\-collections/community\.general/pull/7180)\)\.
|
||||
* mas \- disable sign\-in check for macOS 12\+ as <code>mas account</code> is non\-functional \([https\://github\.com/ansible\-collections/community\.general/pull/6520](https\://github\.com/ansible\-collections/community\.general/pull/6520)\)\.
|
||||
* newrelic\_deployment \- add option <code>app\_name\_exact\_match</code>\, which filters results for the exact app\_name provided \([https\://github\.com/ansible\-collections/community\.general/pull/7355](https\://github\.com/ansible\-collections/community\.general/pull/7355)\)\.
|
||||
* nmap inventory plugin \- now has a <code>use\_arp\_ping</code> option to allow the user to disable the default ARP ping query for a more reliable form \([https\://github\.com/ansible\-collections/community\.general/pull/7119](https\://github\.com/ansible\-collections/community\.general/pull/7119)\)\.
|
||||
* nmcli \- add support for <code>ipv4\.dns\-options</code> and <code>ipv6\.dns\-options</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6902](https\://github\.com/ansible\-collections/community\.general/pull/6902)\)\.
|
||||
* nomad\_job\, nomad\_job\_info \- add <code>port</code> parameter \([https\://github\.com/ansible\-collections/community\.general/pull/7412](https\://github\.com/ansible\-collections/community\.general/pull/7412)\)\.
|
||||
* npm \- minor improvement on parameter validation \([https\://github\.com/ansible\-collections/community\.general/pull/6848](https\://github\.com/ansible\-collections/community\.general/pull/6848)\)\.
|
||||
* npm \- module now using <code>CmdRunner</code> to execute external commands \([https\://github\.com/ansible\-collections/community\.general/pull/6989](https\://github\.com/ansible\-collections/community\.general/pull/6989)\)\.
|
||||
* onepassword lookup plugin \- add service account support \([https\://github\.com/ansible\-collections/community\.general/issues/6635](https\://github\.com/ansible\-collections/community\.general/issues/6635)\, [https\://github\.com/ansible\-collections/community\.general/pull/6660](https\://github\.com/ansible\-collections/community\.general/pull/6660)\)\.
|
||||
* onepassword lookup plugin \- introduce <code>account\_id</code> option which allows specifying which account to use \([https\://github\.com/ansible\-collections/community\.general/pull/7308](https\://github\.com/ansible\-collections/community\.general/pull/7308)\)\.
|
||||
* onepassword\_raw lookup plugin \- add service account support \([https\://github\.com/ansible\-collections/community\.general/issues/6635](https\://github\.com/ansible\-collections/community\.general/issues/6635)\, [https\://github\.com/ansible\-collections/community\.general/pull/6660](https\://github\.com/ansible\-collections/community\.general/pull/6660)\)\.
|
||||
* onepassword\_raw lookup plugin \- introduce <code>account\_id</code> option which allows specifying which account to use \([https\://github\.com/ansible\-collections/community\.general/pull/7308](https\://github\.com/ansible\-collections/community\.general/pull/7308)\)\.
|
||||
* opentelemetry callback plugin \- add span attributes in the span event \([https\://github\.com/ansible\-collections/community\.general/pull/6531](https\://github\.com/ansible\-collections/community\.general/pull/6531)\)\.
|
||||
* opkg \- add <code>executable</code> parameter allowing to specify the path of the <code>opkg</code> command \([https\://github\.com/ansible\-collections/community\.general/pull/6862](https\://github\.com/ansible\-collections/community\.general/pull/6862)\)\.
|
||||
* opkg \- remove default value <code>\"\"</code> for parameter <code>force</code> as it causes the same behaviour of not having that parameter \([https\://github\.com/ansible\-collections/community\.general/pull/6513](https\://github\.com/ansible\-collections/community\.general/pull/6513)\)\.
|
||||
* pagerduty \- adds in option to use v2 API for creating pagerduty incidents \([https\://github\.com/ansible\-collections/community\.general/issues/6151](https\://github\.com/ansible\-collections/community\.general/issues/6151)\)
|
||||
* parted \- on resize\, use <code>\-\-fix</code> option if available \([https\://github\.com/ansible\-collections/community\.general/pull/7304](https\://github\.com/ansible\-collections/community\.general/pull/7304)\)\.
|
||||
* pnpm \- set correct version when state is latest or version is not mentioned\. Resolves previous idempotency problem \([https\://github\.com/ansible\-collections/community\.general/pull/7339](https\://github\.com/ansible\-collections/community\.general/pull/7339)\)\.
|
||||
* pritunl module utils \- ensure <code>validate\_certs</code> parameter is honoured in all methods \([https\://github\.com/ansible\-collections/community\.general/pull/7156](https\://github\.com/ansible\-collections/community\.general/pull/7156)\)\.
|
||||
* proxmox \- add <code>vmid</code> \(and <code>taskid</code> when possible\) to return values \([https\://github\.com/ansible\-collections/community\.general/pull/7263](https\://github\.com/ansible\-collections/community\.general/pull/7263)\)\.
|
||||
* proxmox \- support <code>timezone</code> parameter at container creation \([https\://github\.com/ansible\-collections/community\.general/pull/6510](https\://github\.com/ansible\-collections/community\.general/pull/6510)\)\.
|
||||
* proxmox inventory plugin \- add composite variables support for Proxmox nodes \([https\://github\.com/ansible\-collections/community\.general/issues/6640](https\://github\.com/ansible\-collections/community\.general/issues/6640)\)\.
|
||||
* proxmox\_kvm \- added support for <code>tpmstate0</code> parameter to configure TPM \(Trusted Platform Module\) disk\. TPM is required for Windows 11 installations \([https\://github\.com/ansible\-collections/community\.general/pull/6533](https\://github\.com/ansible\-collections/community\.general/pull/6533)\)\.
|
||||
* proxmox\_kvm \- enabled force restart of VM\, bringing the <code>force</code> parameter functionality in line with what is described in the docs \([https\://github\.com/ansible\-collections/community\.general/pull/6914](https\://github\.com/ansible\-collections/community\.general/pull/6914)\)\.
|
||||
* proxmox\_kvm \- re\-use <code>timeout</code> module param to forcefully shutdown a virtual machine when <code>state</code> is <code>stopped</code> \([https\://github\.com/ansible\-collections/community\.general/issues/6257](https\://github\.com/ansible\-collections/community\.general/issues/6257)\)\.
|
||||
* proxmox\_snap \- add <code>retention</code> parameter to delete old snapshots \([https\://github\.com/ansible\-collections/community\.general/pull/6576](https\://github\.com/ansible\-collections/community\.general/pull/6576)\)\.
|
||||
* proxmox\_vm\_info \- <code>node</code> parameter is no longer required\. Information can be obtained for the whole cluster \([https\://github\.com/ansible\-collections/community\.general/pull/6976](https\://github\.com/ansible\-collections/community\.general/pull/6976)\)\.
|
||||
* proxmox\_vm\_info \- non\-existing provided by name/vmid VM would return empty results instead of failing \([https\://github\.com/ansible\-collections/community\.general/pull/7049](https\://github\.com/ansible\-collections/community\.general/pull/7049)\)\.
|
||||
* pubnub\_blocks \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6928](https\://github\.com/ansible\-collections/community\.general/pull/6928)\)\.
|
||||
* random\_string \- added new <code>ignore\_similar\_chars</code> and <code>similar\_chars</code> option to ignore certain chars \([https\://github\.com/ansible\-collections/community\.general/pull/7242](https\://github\.com/ansible\-collections/community\.general/pull/7242)\)\.
|
||||
* redfish\_command \- add <code>MultipartHTTPPushUpdate</code> command \([https\://github\.com/ansible\-collections/community\.general/issues/6471](https\://github\.com/ansible\-collections/community\.general/issues/6471)\, [https\://github\.com/ansible\-collections/community\.general/pull/6612](https\://github\.com/ansible\-collections/community\.general/pull/6612)\)\.
|
||||
* redfish\_command \- add <code>account\_types</code> and <code>oem\_account\_types</code> as optional inputs to <code>AddUser</code> \([https\://github\.com/ansible\-collections/community\.general/issues/6823](https\://github\.com/ansible\-collections/community\.general/issues/6823)\, [https\://github\.com/ansible\-collections/community\.general/pull/6871](https\://github\.com/ansible\-collections/community\.general/pull/6871)\)\.
|
||||
* redfish\_command \- add new option <code>update\_oem\_params</code> for the <code>MultipartHTTPPushUpdate</code> command \([https\://github\.com/ansible\-collections/community\.general/issues/7331](https\://github\.com/ansible\-collections/community\.general/issues/7331)\)\.
|
||||
* redfish\_config \- add <code>CreateVolume</code> command to allow creation of volumes on servers \([https\://github\.com/ansible\-collections/community\.general/pull/6813](https\://github\.com/ansible\-collections/community\.general/pull/6813)\)\.
|
||||
* redfish\_config \- add <code>DeleteAllVolumes</code> command to allow deletion of all volumes on servers \([https\://github\.com/ansible\-collections/community\.general/pull/6814](https\://github\.com/ansible\-collections/community\.general/pull/6814)\)\.
|
||||
* redfish\_config \- adding <code>SetSecureBoot</code> command \([https\://github\.com/ansible\-collections/community\.general/pull/7129](https\://github\.com/ansible\-collections/community\.general/pull/7129)\)\.
|
||||
* redfish\_info \- add <code>AccountTypes</code> and <code>OEMAccountTypes</code> to the output of <code>ListUsers</code> \([https\://github\.com/ansible\-collections/community\.general/issues/6823](https\://github\.com/ansible\-collections/community\.general/issues/6823)\, [https\://github\.com/ansible\-collections/community\.general/pull/6871](https\://github\.com/ansible\-collections/community\.general/pull/6871)\)\.
|
||||
* redfish\_info \- add support for <code>GetBiosRegistries</code> command \([https\://github\.com/ansible\-collections/community\.general/pull/7144](https\://github\.com/ansible\-collections/community\.general/pull/7144)\)\.
|
||||
* redfish\_info \- adds <code>LinkStatus</code> to NIC inventory \([https\://github\.com/ansible\-collections/community\.general/pull/7318](https\://github\.com/ansible\-collections/community\.general/pull/7318)\)\.
|
||||
* redfish\_info \- adds <code>ProcessorArchitecture</code> to CPU inventory \([https\://github\.com/ansible\-collections/community\.general/pull/6864](https\://github\.com/ansible\-collections/community\.general/pull/6864)\)\.
|
||||
* redfish\_info \- fix for <code>GetVolumeInventory</code>\, Controller name was getting populated incorrectly and duplicates were seen in the volumes retrieved \([https\://github\.com/ansible\-collections/community\.general/pull/6719](https\://github\.com/ansible\-collections/community\.general/pull/6719)\)\.
|
||||
* redfish\_info \- report <code>Id</code> in the output of <code>GetManagerInventory</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7140](https\://github\.com/ansible\-collections/community\.general/pull/7140)\)\.
|
||||
* redfish\_utils \- use <code>Controllers</code> key in redfish data to obtain Storage controllers properties \([https\://github\.com/ansible\-collections/community\.general/pull/7081](https\://github\.com/ansible\-collections/community\.general/pull/7081)\)\.
|
||||
* redfish\_utils module utils \- add support for <code>PowerCycle</code> reset type for <code>redfish\_command</code> responses feature \([https\://github\.com/ansible\-collections/community\.general/issues/7083](https\://github\.com/ansible\-collections/community\.general/issues/7083)\)\.
|
||||
* redfish\_utils module utils \- add support for following <code>\@odata\.nextLink</code> pagination in <code>software\_inventory</code> responses feature \([https\://github\.com/ansible\-collections/community\.general/pull/7020](https\://github\.com/ansible\-collections/community\.general/pull/7020)\)\.
|
||||
* redfish\_utils module utils \- support <code>Volumes</code> in response for <code>GetDiskInventory</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6819](https\://github\.com/ansible\-collections/community\.general/pull/6819)\)\.
|
||||
* redhat\_subscription \- the internal <code>RegistrationBase</code> class was folded
|
||||
into the other internal <code>Rhsm</code> class\, as the separation had no purpose
|
||||
anymore
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6658](https\://github\.com/ansible\-collections/community\.general/pull/6658)\)\.
|
||||
* redis\_info \- refactor the redis\_info module to use the redis module\_utils enabling to pass TLS parameters to the Redis client \([https\://github\.com/ansible\-collections/community\.general/pull/7267](https\://github\.com/ansible\-collections/community\.general/pull/7267)\)\.
|
||||
* rhsm\_release \- improve/harden the way <code>subscription\-manager</code> is run\;
|
||||
no behaviour change is expected
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6669](https\://github\.com/ansible\-collections/community\.general/pull/6669)\)\.
|
||||
* rhsm\_repository \- the interaction with <code>subscription\-manager</code> was
|
||||
refactored by grouping things together\, removing unused bits\, and hardening
|
||||
the way it is run\; also\, the parsing of <code>subscription\-manager repos \-\-list</code>
|
||||
was improved and made slightly faster\; no behaviour change is expected
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6783](https\://github\.com/ansible\-collections/community\.general/pull/6783)\,
|
||||
[https\://github\.com/ansible\-collections/community\.general/pull/6837](https\://github\.com/ansible\-collections/community\.general/pull/6837)\)\.
|
||||
* scaleway\_security\_group\_rule \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6928](https\://github\.com/ansible\-collections/community\.general/pull/6928)\)\.
|
||||
* shutdown \- use <code>shutdown \-p \.\.\.</code> with FreeBSD to halt and power off machine \([https\://github\.com/ansible\-collections/community\.general/pull/7102](https\://github\.com/ansible\-collections/community\.general/pull/7102)\)\.
|
||||
* snap \- add option <code>dangerous</code> to the module\, that will map into the command line argument <code>\-\-dangerous</code>\, allowing unsigned snap files to be installed \([https\://github\.com/ansible\-collections/community\.general/pull/6908](https\://github\.com/ansible\-collections/community\.general/pull/6908)\, [https\://github\.com/ansible\-collections/community\.general/issues/5715](https\://github\.com/ansible\-collections/community\.general/issues/5715)\)\.
|
||||
* snap \- module is now aware of channel when deciding whether to install or refresh the snap \([https\://github\.com/ansible\-collections/community\.general/pull/6435](https\://github\.com/ansible\-collections/community\.general/pull/6435)\, [https\://github\.com/ansible\-collections/community\.general/issues/1606](https\://github\.com/ansible\-collections/community\.general/issues/1606)\)\.
|
||||
* sorcery \- add grimoire \(repository\) management support \([https\://github\.com/ansible\-collections/community\.general/pull/7012](https\://github\.com/ansible\-collections/community\.general/pull/7012)\)\.
|
||||
* sorcery \- minor refactor \([https\://github\.com/ansible\-collections/community\.general/pull/6525](https\://github\.com/ansible\-collections/community\.general/pull/6525)\)\.
|
||||
* supervisorctl \- allow to stop matching running processes before removing them with <code>stop\_before\_removing\=true</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7284](https\://github\.com/ansible\-collections/community\.general/pull/7284)\)\.
|
||||
* tss lookup plugin \- allow to fetch secret IDs which are in a folder based on folder ID\. Previously\, we could not fetch secrets based on folder ID but now use <code>fetch\_secret\_ids\_from\_folder</code> option to indicate to fetch secret IDs based on folder ID \([https\://github\.com/ansible\-collections/community\.general/issues/6223](https\://github\.com/ansible\-collections/community\.general/issues/6223)\)\.
|
||||
* tss lookup plugin \- allow to fetch secret by path\. Previously\, we could not fetch secret by path but now use <code>secret\_path</code> option to indicate to fetch secret by secret path \([https\://github\.com/ansible\-collections/community\.general/pull/6881](https\://github\.com/ansible\-collections/community\.general/pull/6881)\)\.
|
||||
* unixy callback plugin \- add support for <code>check\_mode\_markers</code> option \([https\://github\.com/ansible\-collections/community\.general/pull/7179](https\://github\.com/ansible\-collections/community\.general/pull/7179)\)\.
|
||||
* vardict module utils \- added convenience methods to <code>VarDict</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6647](https\://github\.com/ansible\-collections/community\.general/pull/6647)\)\.
|
||||
* xenserver\_guest\_info \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6928](https\://github\.com/ansible\-collections/community\.general/pull/6928)\)\.
|
||||
* xenserver\_guest\_powerstate \- minor refactor removing unnecessary code statements \([https\://github\.com/ansible\-collections/community\.general/pull/6928](https\://github\.com/ansible\-collections/community\.general/pull/6928)\)\.
|
||||
* yum\_versionlock \- add support to pin specific package versions instead of only the package itself \([https\://github\.com/ansible\-collections/community\.general/pull/6861](https\://github\.com/ansible\-collections/community\.general/pull/6861)\, [https\://github\.com/ansible\-collections/community\.general/issues/4470](https\://github\.com/ansible\-collections/community\.general/issues/4470)\)\.
|
||||
|
||||
<a id="breaking-changes--porting-guide"></a>
|
||||
### Breaking Changes / Porting Guide
|
||||
|
||||
* collection\_version lookup plugin \- remove compatibility code for ansible\-base 2\.10 and ansible\-core 2\.11 \([https\://github\.com/ansible\-collections/community\.general/pull/7269](https\://github\.com/ansible\-collections/community\.general/pull/7269)\)\.
|
||||
* gitlab\_project \- add <code>default\_branch</code> support for project update\. If you used the module so far with <code>default\_branch</code> to update a project\, the value of <code>default\_branch</code> was ignored\. Make sure that you either do not pass a value if you are not sure whether it is the one you want to have to avoid unexpected breaking changes \([https\://github\.com/ansible\-collections/community\.general/pull/7158](https\://github\.com/ansible\-collections/community\.general/pull/7158)\)\.
|
||||
* selective callback plugin \- remove compatibility code for Ansible 2\.9 and ansible\-core 2\.10 \([https\://github\.com/ansible\-collections/community\.general/pull/7269](https\://github\.com/ansible\-collections/community\.general/pull/7269)\)\.
|
||||
* vardict module utils \- <code>VarDict</code> will no longer accept variables named <code>\_var</code>\, <code>get\_meta</code>\, and <code>as\_dict</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6647](https\://github\.com/ansible\-collections/community\.general/pull/6647)\)\.
|
||||
* version module util \- remove fallback for ansible\-core 2\.11\. All modules and plugins that do version collections no longer work with ansible\-core 2\.11 \([https\://github\.com/ansible\-collections/community\.general/pull/7269](https\://github\.com/ansible\-collections/community\.general/pull/7269)\)\.
|
||||
|
||||
<a id="deprecated-features-1"></a>
|
||||
### Deprecated Features
|
||||
|
||||
* CmdRunner module utils \- deprecate <code>cmd\_runner\_fmt\.as\_default\_type\(\)</code> formatter \([https\://github\.com/ansible\-collections/community\.general/pull/6601](https\://github\.com/ansible\-collections/community\.general/pull/6601)\)\.
|
||||
* MH VarsMixin module utils \- deprecates <code>VarsMixin</code> and supporting classes in favor of plain <code>vardict</code> module util \([https\://github\.com/ansible\-collections/community\.general/pull/6649](https\://github\.com/ansible\-collections/community\.general/pull/6649)\)\.
|
||||
* ansible\_galaxy\_install \- the <code>ack\_ansible29</code> and <code>ack\_min\_ansiblecore211</code> options have been deprecated and will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* consul \- the <code>ack\_params\_state\_absent</code> option has been deprecated and will be removed in community\.general 10\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* cpanm \- value <code>compatibility</code> is deprecated as default for parameter <code>mode</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6512](https\://github\.com/ansible\-collections/community\.general/pull/6512)\)\.
|
||||
* ejabberd\_user \- deprecate the parameter <code>logging</code> in favour of producing more detailed information in the module output \([https\://github\.com/ansible\-collections/community\.general/pull/7043](https\://github\.com/ansible\-collections/community\.general/pull/7043)\)\.
|
||||
* flowdock \- module relies entirely on no longer responsive API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6930](https\://github\.com/ansible\-collections/community\.general/pull/6930)\)\.
|
||||
* proxmox \- old feature flag <code>proxmox\_default\_behavior</code> will be removed in community\.general 10\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6836](https\://github\.com/ansible\-collections/community\.general/pull/6836)\)\.
|
||||
* proxmox\_kvm \- deprecate the option <code>proxmox\_default\_behavior</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7377](https\://github\.com/ansible\-collections/community\.general/pull/7377)\)\.
|
||||
* redfish\_info\, redfish\_config\, redfish\_command \- the default value <code>10</code> for the <code>timeout</code> option is deprecated and will change to <code>60</code> in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/7295](https\://github\.com/ansible\-collections/community\.general/pull/7295)\)\.
|
||||
* redhat module utils \- the <code>module\_utils\.redhat</code> module is deprecated\, as
|
||||
effectively unused\: the <code>Rhsm</code>\, <code>RhsmPool</code>\, and <code>RhsmPools</code> classes
|
||||
will be removed in community\.general 9\.0\.0\; the <code>RegistrationBase</code> class
|
||||
will be removed in community\.general 10\.0\.0 together with the
|
||||
<code>rhn\_register</code> module\, as it is the only user of this class\; this means
|
||||
that the whole <code>module\_utils\.redhat</code> module will be dropped in
|
||||
community\.general 10\.0\.0\, so importing it without even using anything of it
|
||||
will fail
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6663](https\://github\.com/ansible\-collections/community\.general/pull/6663)\)\.
|
||||
* redhat\_subscription \- the <code>autosubscribe</code> alias for the <code>auto\_attach</code> option has been
|
||||
deprecated for many years\, although only in the documentation\. Officially mark this alias
|
||||
as deprecated\, and it will be removed in community\.general 9\.0\.0
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6646](https\://github\.com/ansible\-collections/community\.general/pull/6646)\)\.
|
||||
* redhat\_subscription \- the <code>pool</code> option is deprecated in favour of the
|
||||
more precise and flexible <code>pool\_ids</code> option
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6650](https\://github\.com/ansible\-collections/community\.general/pull/6650)\)\.
|
||||
* rhsm\_repository \- <code>state\=present</code> has not been working as expected for many years\,
|
||||
and it seems it was not noticed so far\; also\, \"presence\" is not really a valid concept
|
||||
for subscription repositories\, which can only be enabled or disabled\. Hence\, mark the
|
||||
<code>present</code> and <code>absent</code> values of the <code>state</code> option as deprecated\, slating them
|
||||
for removal in community\.general 10\.0\.0
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6673](https\://github\.com/ansible\-collections/community\.general/pull/6673)\)\.
|
||||
* stackdriver \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6887](https\://github\.com/ansible\-collections/community\.general/pull/6887)\)\.
|
||||
* webfaction\_app \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6909](https\://github\.com/ansible\-collections/community\.general/pull/6909)\)\.
|
||||
* webfaction\_db \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6909](https\://github\.com/ansible\-collections/community\.general/pull/6909)\)\.
|
||||
* webfaction\_domain \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6909](https\://github\.com/ansible\-collections/community\.general/pull/6909)\)\.
|
||||
* webfaction\_mailbox \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6909](https\://github\.com/ansible\-collections/community\.general/pull/6909)\)\.
|
||||
* webfaction\_site \- module relies entirely on no longer existent API endpoints\, and it will be removed in community\.general 9\.0\.0 \([https\://github\.com/ansible\-collections/community\.general/pull/6909](https\://github\.com/ansible\-collections/community\.general/pull/6909)\)\.
|
||||
|
||||
<a id="removed-features-previously-deprecated"></a>
|
||||
### Removed Features \(previously deprecated\)
|
||||
|
||||
* The collection no longer supports ansible\-core 2\.11 and ansible\-core 2\.12\. Parts of the collection might still work on these ansible\-core versions\, but others might not \([https\://github\.com/ansible\-collections/community\.general/pull/7269](https\://github\.com/ansible\-collections/community\.general/pull/7269)\)\.
|
||||
* ansible\_galaxy\_install \- support for Ansible 2\.9 and ansible\-base 2\.10 has been removed \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* consul \- when <code>state\=absent</code>\, the options <code>script</code>\, <code>ttl</code>\, <code>tcp</code>\, <code>http</code>\, and <code>interval</code> can no longer be specified \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* gconftool2 \- <code>state\=get</code> has been removed\. Use the module <code>community\.general\.gconftool2\_info</code> instead \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* gitlab\_runner \- remove the default value for the <code>access\_level</code> option\. To restore the previous behavior\, explicitly set it to <code>ref\_protected</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* htpasswd \- removed code for passlib \<1\.6 \([https\://github\.com/ansible\-collections/community\.general/pull/6901](https\://github\.com/ansible\-collections/community\.general/pull/6901)\)\.
|
||||
* manageiq\_polices \- <code>state\=list</code> has been removed\. Use the module <code>community\.general\.manageiq\_policies\_info</code> instead \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* manageiq\_tags \- <code>state\=list</code> has been removed\. Use the module <code>community\.general\.manageiq\_tags\_info</code> instead \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* mh\.mixins\.cmd module utils \- the <code>ArgFormat</code> class has been removed \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* mh\.mixins\.cmd module utils \- the <code>CmdMixin</code> mixin has been removed\. Use <code>community\.general\.plugins\.module\_utils\.cmd\_runner\.CmdRunner</code> instead \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* mh\.mixins\.cmd module utils \- the mh\.mixins\.cmd module utils has been removed after all its contents were removed \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* mh\.module\_helper module utils \- the <code>CmdModuleHelper</code> and <code>CmdStateModuleHelper</code> classes have been removed\. Use <code>community\.general\.plugins\.module\_utils\.cmd\_runner\.CmdRunner</code> instead \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
* proxmox module utils \- removed unused imports \([https\://github\.com/ansible\-collections/community\.general/pull/6873](https\://github\.com/ansible\-collections/community\.general/pull/6873)\)\.
|
||||
* xfconf \- the deprecated <code>disable\_facts</code> option was removed \([https\://github\.com/ansible\-collections/community\.general/pull/7358](https\://github\.com/ansible\-collections/community\.general/pull/7358)\)\.
|
||||
|
||||
<a id="bugfixes-6"></a>
|
||||
### Bugfixes
|
||||
|
||||
* CmdRunner module utils \- does not attempt to resolve path if executable is a relative or absolute path \([https\://github\.com/ansible\-collections/community\.general/pull/7200](https\://github\.com/ansible\-collections/community\.general/pull/7200)\)\.
|
||||
* MH DependencyMixin module utils \- deprecation notice was popping up for modules not using dependencies \([https\://github\.com/ansible\-collections/community\.general/pull/6644](https\://github\.com/ansible\-collections/community\.general/pull/6644)\, [https\://github\.com/ansible\-collections/community\.general/issues/6639](https\://github\.com/ansible\-collections/community\.general/issues/6639)\)\.
|
||||
* bitwarden lookup plugin \- the plugin made assumptions about the structure of a Bitwarden JSON object which may have been broken by an update in the Bitwarden API\. Remove assumptions\, and allow queries for general fields such as <code>notes</code> \([https\://github\.com/ansible\-collections/community\.general/pull/7061](https\://github\.com/ansible\-collections/community\.general/pull/7061)\)\.
|
||||
* cmd\_runner module utils \- when a parameter in <code>argument\_spec</code> has no type\, meaning it is implicitly a <code>str</code>\, <code>CmdRunner</code> would fail trying to find the <code>type</code> key in that dictionary \([https\://github\.com/ansible\-collections/community\.general/pull/6968](https\://github\.com/ansible\-collections/community\.general/pull/6968)\)\.
|
||||
* cobbler inventory plugin \- fix calculation of cobbler\_ipv4/6\_address \([https\://github\.com/ansible\-collections/community\.general/pull/6925](https\://github\.com/ansible\-collections/community\.general/pull/6925)\)\.
|
||||
* composer \- fix impossible to run <code>working\_dir</code> dependent commands\. The module was throwing an error when trying to run a <code>working\_dir</code> dependent command\, because it tried to get the command help without passing the <code>working\_dir</code> \([https\://github\.com/ansible\-collections/community\.general/issues/3787](https\://github\.com/ansible\-collections/community\.general/issues/3787)\)\.
|
||||
* csv module utils \- detects and remove unicode BOM markers from incoming CSV content \([https\://github\.com/ansible\-collections/community\.general/pull/6662](https\://github\.com/ansible\-collections/community\.general/pull/6662)\)\.
|
||||
* datadog\_downtime \- presence of <code>rrule</code> param lead to the Datadog API returning Bad Request due to a missing recurrence type \([https\://github\.com/ansible\-collections/community\.general/pull/6811](https\://github\.com/ansible\-collections/community\.general/pull/6811)\)\.
|
||||
* ejabberd\_user \- module was failing to detect whether user was already created and/or password was changed \([https\://github\.com/ansible\-collections/community\.general/pull/7033](https\://github\.com/ansible\-collections/community\.general/pull/7033)\)\.
|
||||
* ejabberd\_user \- provide meaningful error message when the <code>ejabberdctl</code> command is not found \([https\://github\.com/ansible\-collections/community\.general/pull/7028](https\://github\.com/ansible\-collections/community\.general/pull/7028)\, [https\://github\.com/ansible\-collections/community\.general/issues/6949](https\://github\.com/ansible\-collections/community\.general/issues/6949)\)\.
|
||||
* github\_deploy\_key \- fix pagination behaviour causing a crash when only a single page of deploy keys exist \([https\://github\.com/ansible\-collections/community\.general/pull/7375](https\://github\.com/ansible\-collections/community\.general/pull/7375)\)\.
|
||||
* gitlab\_group \- the module passed parameters to the API call even when not set\. The module is now filtering out <code>None</code> values to remediate this \([https\://github\.com/ansible\-collections/community\.general/pull/6712](https\://github\.com/ansible\-collections/community\.general/pull/6712)\)\.
|
||||
* gitlab\_group\_variable \- deleted all variables when used with <code>purge\=true</code> due to missing <code>raw</code> property in KNOWN attributes \([https\://github\.com/ansible\-collections/community\.general/issues/7250](https\://github\.com/ansible\-collections/community\.general/issues/7250)\)\.
|
||||
* gitlab\_project\_variable \- deleted all variables when used with <code>purge\=true</code> due to missing <code>raw</code> property in KNOWN attributes \([https\://github\.com/ansible\-collections/community\.general/issues/7250](https\://github\.com/ansible\-collections/community\.general/issues/7250)\)\.
|
||||
* icinga2\_host \- fix a key error when updating an existing host \([https\://github\.com/ansible\-collections/community\.general/pull/6748](https\://github\.com/ansible\-collections/community\.general/pull/6748)\)\.
|
||||
* ini\_file \- add the <code>follow</code> paramter to follow the symlinks instead of replacing them \([https\://github\.com/ansible\-collections/community\.general/pull/6546](https\://github\.com/ansible\-collections/community\.general/pull/6546)\)\.
|
||||
* ini\_file \- fix a bug where the inactive options were not used when possible \([https\://github\.com/ansible\-collections/community\.general/pull/6575](https\://github\.com/ansible\-collections/community\.general/pull/6575)\)\.
|
||||
* ipa\_dnszone \- fix \'idnsallowsyncptr\' key error for reverse zone \([https\://github\.com/ansible\-collections/community\.general/pull/6906](https\://github\.com/ansible\-collections/community\.general/pull/6906)\, [https\://github\.com/ansible\-collections/community\.general/issues/6905](https\://github\.com/ansible\-collections/community\.general/issues/6905)\)\.
|
||||
* kernel\_blacklist \- simplified the mechanism to update the file\, fixing the error \([https\://github\.com/ansible\-collections/community\.general/pull/7382](https\://github\.com/ansible\-collections/community\.general/pull/7382)\, [https\://github\.com/ansible\-collections/community\.general/issues/7362](https\://github\.com/ansible\-collections/community\.general/issues/7362)\)\.
|
||||
* keycloak module util \- fix missing <code>http\_agent</code>\, <code>timeout</code>\, and <code>validate\_certs</code> <code>open\_url\(\)</code> parameters \([https\://github\.com/ansible\-collections/community\.general/pull/7067](https\://github\.com/ansible\-collections/community\.general/pull/7067)\)\.
|
||||
* keycloak module utils \- fix <code>is\_struct\_included</code> handling of lists of lists/dictionaries \([https\://github\.com/ansible\-collections/community\.general/pull/6688](https\://github\.com/ansible\-collections/community\.general/pull/6688)\)\.
|
||||
* keycloak module utils \- the function <code>get\_user\_by\_username</code> now return the user representation or <code>None</code> as stated in the documentation \([https\://github\.com/ansible\-collections/community\.general/pull/6758](https\://github\.com/ansible\-collections/community\.general/pull/6758)\)\.
|
||||
* keycloak\_authentication \- fix Keycloak authentication flow \(step or sub\-flow\) indexing during update\, if not specified by the user \([https\://github\.com/ansible\-collections/community\.general/pull/6734](https\://github\.com/ansible\-collections/community\.general/pull/6734)\)\.
|
||||
* keycloak\_client inventory plugin \- fix missing client secret \([https\://github\.com/ansible\-collections/community\.general/pull/6931](https\://github\.com/ansible\-collections/community\.general/pull/6931)\)\.
|
||||
* ldap\_search \- fix string normalization and the <code>base64\_attributes</code> option on Python 3 \([https\://github\.com/ansible\-collections/community\.general/issues/5704](https\://github\.com/ansible\-collections/community\.general/issues/5704)\, [https\://github\.com/ansible\-collections/community\.general/pull/7264](https\://github\.com/ansible\-collections/community\.general/pull/7264)\)\.
|
||||
* locale\_gen \- now works for locales without the underscore character such as <code>C\.UTF\-8</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6774](https\://github\.com/ansible\-collections/community\.general/pull/6774)\, [https\://github\.com/ansible\-collections/community\.general/issues/5142](https\://github\.com/ansible\-collections/community\.general/issues/5142)\, [https\://github\.com/ansible\-collections/community\.general/issues/4305](https\://github\.com/ansible\-collections/community\.general/issues/4305)\)\.
|
||||
* lvol \- add support for percentage of origin size specification when creating snapshot volumes \([https\://github\.com/ansible\-collections/community\.general/issues/1630](https\://github\.com/ansible\-collections/community\.general/issues/1630)\, [https\://github\.com/ansible\-collections/community\.general/pull/7053](https\://github\.com/ansible\-collections/community\.general/pull/7053)\)\.
|
||||
* lxc connection plugin \- now handles <code>remote\_addr</code> defaulting to <code>inventory\_hostname</code> correctly \([https\://github\.com/ansible\-collections/community\.general/pull/7104](https\://github\.com/ansible\-collections/community\.general/pull/7104)\)\.
|
||||
* lxc connection plugin \- properly evaluate options \([https\://github\.com/ansible\-collections/community\.general/pull/7369](https\://github\.com/ansible\-collections/community\.general/pull/7369)\)\.
|
||||
* machinectl become plugin \- mark plugin as <code>require\_tty</code> to automatically disable pipelining\, with which this plugin is not compatible \([https\://github\.com/ansible\-collections/community\.general/issues/6932](https\://github\.com/ansible\-collections/community\.general/issues/6932)\, [https\://github\.com/ansible\-collections/community\.general/pull/6935](https\://github\.com/ansible\-collections/community\.general/pull/6935)\)\.
|
||||
* mail \- skip headers containing equals characters due to missing <code>maxsplit</code> on header key/value parsing \([https\://github\.com/ansible\-collections/community\.general/pull/7303](https\://github\.com/ansible\-collections/community\.general/pull/7303)\)\.
|
||||
* memset module utils \- make compatible with ansible\-core 2\.17 \([https\://github\.com/ansible\-collections/community\.general/pull/7379](https\://github\.com/ansible\-collections/community\.general/pull/7379)\)\.
|
||||
* nmap inventory plugin \- fix <code>get\_option</code> calls \([https\://github\.com/ansible\-collections/community\.general/pull/7323](https\://github\.com/ansible\-collections/community\.general/pull/7323)\)\.
|
||||
* nmap inventory plugin \- now uses <code>get\_option</code> in all cases to get its configuration information \([https\://github\.com/ansible\-collections/community\.general/pull/7119](https\://github\.com/ansible\-collections/community\.general/pull/7119)\)\.
|
||||
* nmcli \- fix bond option <code>xmit\_hash\_policy</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6527](https\://github\.com/ansible\-collections/community\.general/pull/6527)\)\.
|
||||
* nmcli \- fix support for empty list \(in compare and scrape\) \([https\://github\.com/ansible\-collections/community\.general/pull/6769](https\://github\.com/ansible\-collections/community\.general/pull/6769)\)\.
|
||||
* nsupdate \- fix a possible <code>list index out of range</code> exception \([https\://github\.com/ansible\-collections/community\.general/issues/836](https\://github\.com/ansible\-collections/community\.general/issues/836)\)\.
|
||||
* oci\_utils module util \- fix inappropriate logical comparison expressions and makes them simpler\. The previous checks had logical short circuits \([https\://github\.com/ansible\-collections/community\.general/pull/7125](https\://github\.com/ansible\-collections/community\.general/pull/7125)\)\.
|
||||
* oci\_utils module utils \- avoid direct type comparisons \([https\://github\.com/ansible\-collections/community\.general/pull/7085](https\://github\.com/ansible\-collections/community\.general/pull/7085)\)\.
|
||||
* onepassword \- fix KeyError exception when trying to access value of a field that is not filled out in OnePassword item \([https\://github\.com/ansible\-collections/community\.general/pull/7241](https\://github\.com/ansible\-collections/community\.general/pull/7241)\)\.
|
||||
* openbsd\_pkg \- the pkg\_info\(1\) behavior has changed in OpenBSD \>7\.3\. The error message <code>Can\'t find</code> should not lead to an error case \([https\://github\.com/ansible\-collections/community\.general/pull/6785](https\://github\.com/ansible\-collections/community\.general/pull/6785)\)\.
|
||||
* pacman \- module recognizes the output of <code>yay</code> running as <code>root</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6713](https\://github\.com/ansible\-collections/community\.general/pull/6713)\)\.
|
||||
* portage \- fix <code>changed\_use</code> and <code>newuse</code> not triggering rebuilds \([https\://github\.com/ansible\-collections/community\.general/issues/6008](https\://github\.com/ansible\-collections/community\.general/issues/6008)\, [https\://github\.com/ansible\-collections/community\.general/pull/6548](https\://github\.com/ansible\-collections/community\.general/pull/6548)\)\.
|
||||
* pritunl module utils \- fix incorrect URL parameter for orgnization add method \([https\://github\.com/ansible\-collections/community\.general/pull/7161](https\://github\.com/ansible\-collections/community\.general/pull/7161)\)\.
|
||||
* proxmox \- fix error when a configuration had no <code>template</code> field \([https\://github\.com/ansible\-collections/community\.general/pull/6838](https\://github\.com/ansible\-collections/community\.general/pull/6838)\, [https\://github\.com/ansible\-collections/community\.general/issues/5372](https\://github\.com/ansible\-collections/community\.general/issues/5372)\)\.
|
||||
* proxmox module utils \- add logic to detect whether an old Promoxer complains about the <code>token\_name</code> and <code>token\_value</code> parameters and provide a better error message when that happens \([https\://github\.com/ansible\-collections/community\.general/pull/6839](https\://github\.com/ansible\-collections/community\.general/pull/6839)\, [https\://github\.com/ansible\-collections/community\.general/issues/5371](https\://github\.com/ansible\-collections/community\.general/issues/5371)\)\.
|
||||
* proxmox module utils \- fix proxmoxer library version check \([https\://github\.com/ansible\-collections/community\.general/issues/6974](https\://github\.com/ansible\-collections/community\.general/issues/6974)\, [https\://github\.com/ansible\-collections/community\.general/issues/6975](https\://github\.com/ansible\-collections/community\.general/issues/6975)\, [https\://github\.com/ansible\-collections/community\.general/pull/6980](https\://github\.com/ansible\-collections/community\.general/pull/6980)\)\.
|
||||
* proxmox\_disk \- fix unable to create <code>cdrom</code> media due to <code>size</code> always being appended \([https\://github\.com/ansible\-collections/community\.general/pull/6770](https\://github\.com/ansible\-collections/community\.general/pull/6770)\)\.
|
||||
* proxmox\_kvm \- <code>absent</code> state with <code>force</code> specified failed to stop the VM due to the <code>timeout</code> value not being passed to <code>stop\_vm</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6827](https\://github\.com/ansible\-collections/community\.general/pull/6827)\)\.
|
||||
* proxmox\_kvm \- <code>restarted</code> state did not actually restart a VM in some VM configurations\. The state now uses the Proxmox reboot endpoint instead of calling the <code>stop\_vm</code> and <code>start\_vm</code> functions \([https\://github\.com/ansible\-collections/community\.general/pull/6773](https\://github\.com/ansible\-collections/community\.general/pull/6773)\)\.
|
||||
* proxmox\_kvm \- allow creation of VM with existing name but new vmid \([https\://github\.com/ansible\-collections/community\.general/issues/6155](https\://github\.com/ansible\-collections/community\.general/issues/6155)\, [https\://github\.com/ansible\-collections/community\.general/pull/6709](https\://github\.com/ansible\-collections/community\.general/pull/6709)\)\.
|
||||
* proxmox\_kvm \- when <code>name</code> option is provided without <code>vmid</code> and VM with that name already exists then no new VM will be created \([https\://github\.com/ansible\-collections/community\.general/issues/6911](https\://github\.com/ansible\-collections/community\.general/issues/6911)\, [https\://github\.com/ansible\-collections/community\.general/pull/6981](https\://github\.com/ansible\-collections/community\.general/pull/6981)\)\.
|
||||
* proxmox\_tasks\_info \- remove <code>api\_user</code> \+ <code>api\_password</code> constraint from <code>required\_together</code> as it causes to require <code>api\_password</code> even when API token param is used \([https\://github\.com/ansible\-collections/community\.general/issues/6201](https\://github\.com/ansible\-collections/community\.general/issues/6201)\)\.
|
||||
* proxmox\_template \- require <code>requests\_toolbelt</code> module to fix issue with uploading large templates \([https\://github\.com/ansible\-collections/community\.general/issues/5579](https\://github\.com/ansible\-collections/community\.general/issues/5579)\, [https\://github\.com/ansible\-collections/community\.general/pull/6757](https\://github\.com/ansible\-collections/community\.general/pull/6757)\)\.
|
||||
* proxmox\_user\_info \- avoid direct type comparisons \([https\://github\.com/ansible\-collections/community\.general/pull/7085](https\://github\.com/ansible\-collections/community\.general/pull/7085)\)\.
|
||||
* redfish\_info \- fix <code>ListUsers</code> to not show empty account slots \([https\://github\.com/ansible\-collections/community\.general/issues/6771](https\://github\.com/ansible\-collections/community\.general/issues/6771)\, [https\://github\.com/ansible\-collections/community\.general/pull/6772](https\://github\.com/ansible\-collections/community\.general/pull/6772)\)\.
|
||||
* redhat\_subscription \- use the right D\-Bus options for the consumer type when
|
||||
registering a RHEL system older than 9 or a RHEL 9 system older than 9\.2
|
||||
and using <code>consumer\_type</code>
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/7378](https\://github\.com/ansible\-collections/community\.general/pull/7378)\)\.
|
||||
* refish\_utils module utils \- changing variable names to avoid issues occuring when fetching Volumes data \([https\://github\.com/ansible\-collections/community\.general/pull/6883](https\://github\.com/ansible\-collections/community\.general/pull/6883)\)\.
|
||||
* rhsm\_repository \- when using the <code>purge</code> option\, the <code>repositories</code>
|
||||
dictionary element in the returned JSON is now properly updated according
|
||||
to the pruning operation
|
||||
\([https\://github\.com/ansible\-collections/community\.general/pull/6676](https\://github\.com/ansible\-collections/community\.general/pull/6676)\)\.
|
||||
* rundeck \- fix <code>TypeError</code> on 404 API response \([https\://github\.com/ansible\-collections/community\.general/pull/6983](https\://github\.com/ansible\-collections/community\.general/pull/6983)\)\.
|
||||
* selective callback plugin \- fix length of task name lines in output always being 3 characters longer than desired \([https\://github\.com/ansible\-collections/community\.general/pull/7374](https\://github\.com/ansible\-collections/community\.general/pull/7374)\)\.
|
||||
* snap \- an exception was being raised when snap list was empty \([https\://github\.com/ansible\-collections/community\.general/pull/7124](https\://github\.com/ansible\-collections/community\.general/pull/7124)\, [https\://github\.com/ansible\-collections/community\.general/issues/7120](https\://github\.com/ansible\-collections/community\.general/issues/7120)\)\.
|
||||
* snap \- assume default track <code>latest</code> in parameter <code>channel</code> when not specified \([https\://github\.com/ansible\-collections/community\.general/pull/6835](https\://github\.com/ansible\-collections/community\.general/pull/6835)\, [https\://github\.com/ansible\-collections/community\.general/issues/6821](https\://github\.com/ansible\-collections/community\.general/issues/6821)\)\.
|
||||
* snap \- change the change detection mechanism from \"parsing installation\" to \"comparing end state with initial state\" \([https\://github\.com/ansible\-collections/community\.general/pull/7340](https\://github\.com/ansible\-collections/community\.general/pull/7340)\, [https\://github\.com/ansible\-collections/community\.general/issues/7265](https\://github\.com/ansible\-collections/community\.general/issues/7265)\)\.
|
||||
* snap \- fix crash when multiple snaps are specified and one has <code>\-\-\-</code> in its description \([https\://github\.com/ansible\-collections/community\.general/pull/7046](https\://github\.com/ansible\-collections/community\.general/pull/7046)\)\.
|
||||
* snap \- fix the processing of the commands\' output\, stripping spaces and newlines from it \([https\://github\.com/ansible\-collections/community\.general/pull/6826](https\://github\.com/ansible\-collections/community\.general/pull/6826)\, [https\://github\.com/ansible\-collections/community\.general/issues/6803](https\://github\.com/ansible\-collections/community\.general/issues/6803)\)\.
|
||||
* sorcery \- fix interruption of the multi\-stage process \([https\://github\.com/ansible\-collections/community\.general/pull/7012](https\://github\.com/ansible\-collections/community\.general/pull/7012)\)\.
|
||||
* sorcery \- fix queue generation before the whole system rebuild \([https\://github\.com/ansible\-collections/community\.general/pull/7012](https\://github\.com/ansible\-collections/community\.general/pull/7012)\)\.
|
||||
* sorcery \- latest state no longer triggers update\_cache \([https\://github\.com/ansible\-collections/community\.general/pull/7012](https\://github\.com/ansible\-collections/community\.general/pull/7012)\)\.
|
||||
* terraform \- prevents <code>\-backend\-config</code> option double encapsulating with <code>shlex\_quote</code> function\. \([https\://github\.com/ansible\-collections/community\.general/pull/7301](https\://github\.com/ansible\-collections/community\.general/pull/7301)\)\.
|
||||
* tss lookup plugin \- fix multiple issues when using <code>fetch\_attachments\=true</code> \([https\://github\.com/ansible\-collections/community\.general/pull/6720](https\://github\.com/ansible\-collections/community\.general/pull/6720)\)\.
|
||||
* zypper \- added handling of zypper exitcode 102\. Changed state is set correctly now and rc 102 is still preserved to be evaluated by the playbook \([https\://github\.com/ansible\-collections/community\.general/pull/6534](https\://github\.com/ansible\-collections/community\.general/pull/6534)\)\.
|
||||
|
||||
<a id="known-issues"></a>
|
||||
### Known Issues
|
||||
|
||||
* Ansible markup will show up in raw form on ansible\-doc text output for ansible\-core before 2\.15\. If you have trouble deciphering the documentation markup\, please upgrade to ansible\-core 2\.15 \(or newer\)\, or read the HTML documentation on [https\://docs\.ansible\.com/ansible/devel/collections/community/general/](https\://docs\.ansible\.com/ansible/devel/collections/community/general/) \([https\://github\.com/ansible\-collections/community\.general/pull/6539](https\://github\.com/ansible\-collections/community\.general/pull/6539)\)\.
|
||||
|
||||
<a id="new-plugins-3"></a>
|
||||
### New Plugins
|
||||
|
||||
<a id="lookup-2"></a>
|
||||
#### Lookup
|
||||
|
||||
* bitwarden\_secrets\_manager \- Retrieve secrets from Bitwarden Secrets Manager
|
||||
|
||||
<a id="new-modules-4"></a>
|
||||
### New Modules
|
||||
|
||||
* consul\_policy \- Manipulate Consul policies
|
||||
* consul\_role \- Manipulate Consul roles
|
||||
* facter\_facts \- Runs the discovery program C\(facter\) on the remote system and return Ansible facts
|
||||
* gio\_mime \- Set default handler for MIME type\, for applications using Gnome GIO
|
||||
* gitlab\_instance\_variable \- Creates\, updates\, or deletes GitLab instance variables
|
||||
* gitlab\_merge\_request \- Create\, update\, or delete GitLab merge requests
|
||||
* jenkins\_build\_info \- Get information about Jenkins builds
|
||||
* keycloak\_authentication\_required\_actions \- Allows administration of Keycloak authentication required actions
|
||||
* keycloak\_authz\_custom\_policy \- Allows administration of Keycloak client custom Javascript policies via Keycloak API
|
||||
* keycloak\_authz\_permission \- Allows administration of Keycloak client authorization permissions via Keycloak API
|
||||
* keycloak\_authz\_permission\_info \- Query Keycloak client authorization permissions information
|
||||
* keycloak\_realm\_key \- Allows administration of Keycloak realm keys via Keycloak API
|
||||
* keycloak\_user \- Create and configure a user in Keycloak
|
||||
* lvg\_rename \- Renames LVM volume groups
|
||||
* pnpm \- Manage node\.js packages with pnpm
|
||||
* proxmox\_pool \- Pool management for Proxmox VE cluster
|
||||
* proxmox\_pool\_member \- Add or delete members from Proxmox VE cluster pools
|
||||
* proxmox\_vm\_info \- Retrieve information about one or more Proxmox VE virtual machines
|
||||
* simpleinit\_msb \- Manage services on Source Mage GNU/Linux
|
||||
103
CHANGELOG.rst
103
CHANGELOG.rst
@@ -6,6 +6,109 @@ Community General Release Notes
|
||||
|
||||
This changelog describes changes after version 7.0.0.
|
||||
|
||||
v8.4.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- bitwarden lookup plugin - add ``bw_session`` option, to pass session key instead of reading from env (https://github.com/ansible-collections/community.general/pull/7994).
|
||||
- gitlab_deploy_key, gitlab_group_members, gitlab_group_variable, gitlab_hook, gitlab_instance_variable, gitlab_project_badge, gitlab_project_variable, gitlab_user - improve API pagination and compatibility with different versions of ``python-gitlab`` (https://github.com/ansible-collections/community.general/pull/7790).
|
||||
- gitlab_hook - adds ``releases_events`` parameter for supporting Releases events triggers on GitLab hooks (https://github.com/ansible-collections/community.general/pull/7956).
|
||||
- icinga2 inventory plugin - add Jinja2 templating support to ``url``, ``user``, and ``password`` paramenters (https://github.com/ansible-collections/community.general/issues/7074, https://github.com/ansible-collections/community.general/pull/7996).
|
||||
- mssql_script - adds transactional (rollback/commit) support via optional boolean param ``transaction`` (https://github.com/ansible-collections/community.general/pull/7976).
|
||||
- proxmox_kvm - add parameter ``update_unsafe`` to avoid limitations when updating dangerous values (https://github.com/ansible-collections/community.general/pull/7843).
|
||||
- redfish_config - add command ``SetServiceIdentification`` to set service identification (https://github.com/ansible-collections/community.general/issues/7916).
|
||||
- sudoers - add support for the ``NOEXEC`` tag in sudoers rules (https://github.com/ansible-collections/community.general/pull/7983).
|
||||
- terraform - fix ``diff_mode`` in state ``absent`` and when terraform ``resource_changes`` does not exist (https://github.com/ansible-collections/community.general/pull/7963).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cargo - fix idempotency issues when using a custom installation path for packages (using the ``--path`` parameter). The initial installation runs fine, but subsequent runs use the ``get_installed()`` function which did not check the given installation location, before running ``cargo install``. This resulted in a false ``changed`` state. Also the removal of packeges using ``state: absent`` failed, as the installation check did not use the given parameter (https://github.com/ansible-collections/community.general/pull/7970).
|
||||
- gitlab_issue - fix behavior to search GitLab issue, using ``search`` keyword instead of ``title`` (https://github.com/ansible-collections/community.general/issues/7846).
|
||||
- gitlab_runner - fix pagination when checking for existing runners (https://github.com/ansible-collections/community.general/pull/7790).
|
||||
- keycloak_client - fixes issue when metadata is provided in desired state when task is in check mode (https://github.com/ansible-collections/community.general/issues/1226, https://github.com/ansible-collections/community.general/pull/7881).
|
||||
- modprobe - listing modules files or modprobe files could trigger a FileNotFoundError if ``/etc/modprobe.d`` or ``/etc/modules-load.d`` did not exist. Relevant functions now return empty lists if the directories do not exist to avoid crashing the module (https://github.com/ansible-collections/community.general/issues/7717).
|
||||
- onepassword lookup plugin - failed for fields that were in sections and had uppercase letters in the label/ID. Field lookups are now case insensitive in all cases (https://github.com/ansible-collections/community.general/pull/7919).
|
||||
- pkgin - pkgin (pkgsrc package manager used by SmartOS) raises erratic exceptions and spurious ``changed=true`` (https://github.com/ansible-collections/community.general/pull/7971).
|
||||
- redfish_info - allow for a GET operation invoked by ``GetUpdateStatus`` to allow for an empty response body for cases where a service returns 204 No Content (https://github.com/ansible-collections/community.general/issues/8003).
|
||||
- redfish_info - correct uncaught exception when attempting to retrieve ``Chassis`` information (https://github.com/ansible-collections/community.general/pull/7952).
|
||||
|
||||
New Plugins
|
||||
-----------
|
||||
|
||||
Callback
|
||||
~~~~~~~~
|
||||
|
||||
- default_without_diff - The default ansible callback without diff output
|
||||
|
||||
Filter
|
||||
~~~~~~
|
||||
|
||||
- lists_difference - Difference of lists with a predictive order
|
||||
- lists_intersect - Intersection of lists with a predictive order
|
||||
- lists_symmetric_difference - Symmetric Difference of lists with a predictive order
|
||||
- lists_union - Union of lists with a predictive order
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- gitlab_group_access_token - Manages GitLab group access tokens
|
||||
- gitlab_project_access_token - Manages GitLab project access tokens
|
||||
|
||||
v8.3.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- consul_auth_method, consul_binding_rule, consul_policy, consul_role, consul_session, consul_token - added action group ``community.general.consul`` (https://github.com/ansible-collections/community.general/pull/7897).
|
||||
- consul_policy - added support for diff and check mode (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_policy, consul_role, consul_session - removed dependency on ``requests`` and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826, https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - ``node_identities`` now expects a ``node_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - ``service_identities`` now expects a ``service_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - added support for diff mode (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - added support for templated policies (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- redfish_info - add command ``GetServiceIdentification`` to get service identification (https://github.com/ansible-collections/community.general/issues/7882).
|
||||
- terraform - add support for ``diff_mode`` for terraform resource_changes (https://github.com/ansible-collections/community.general/pull/7896).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- consul_acl - the module has been deprecated and will be removed in community.general 10.0.0. ``consul_token`` and ``consul_policy`` can be used instead (https://github.com/ansible-collections/community.general/pull/7901).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- homebrew - detect already installed formulae and casks using JSON output from ``brew info`` (https://github.com/ansible-collections/community.general/issues/864).
|
||||
- incus connection plugin - treats ``inventory_hostname`` as a variable instead of a literal in remote connections (https://github.com/ansible-collections/community.general/issues/7874).
|
||||
- ipa_otptoken - the module expect ``ipatokendisabled`` as string but the ``ipatokendisabled`` value is returned as a boolean (https://github.com/ansible-collections/community.general/pull/7795).
|
||||
- ldap - previously the order number (if present) was expected to follow an equals sign in the DN. This makes it so the order number string is identified correctly anywhere within the DN (https://github.com/ansible-collections/community.general/issues/7646).
|
||||
- mssql_script - make the module work with Python 2 (https://github.com/ansible-collections/community.general/issues/7818, https://github.com/ansible-collections/community.general/pull/7821).
|
||||
- nmcli - fix ``connection.slave-type`` wired to ``bond`` and not with parameter ``slave_type`` in case of connection type ``wifi`` (https://github.com/ansible-collections/community.general/issues/7389).
|
||||
- proxmox - fix updating a container config if the setting does not already exist (https://github.com/ansible-collections/community.general/pull/7872).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
- consul_acl_bootstrap - Bootstrap ACLs in Consul
|
||||
- consul_auth_method - Manipulate Consul auth methods
|
||||
- consul_binding_rule - Manipulate Consul binding rules
|
||||
- consul_token - Manipulate Consul tokens
|
||||
- gitlab_label - Creates/updates/deletes GitLab Labels belonging to project or group.
|
||||
- gitlab_milestone - Creates/updates/deletes GitLab Milestones belonging to project or group
|
||||
|
||||
v8.2.0
|
||||
======
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ See the [Releasing guidelines](https://github.com/ansible/community-docs/blob/ma
|
||||
|
||||
## Release notes
|
||||
|
||||
See the [changelog](https://github.com/ansible-collections/community.general/blob/stable-8/CHANGELOG.rst).
|
||||
See the [changelog](https://github.com/ansible-collections/community.general/blob/stable-8/CHANGELOG.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -1072,3 +1072,173 @@ releases:
|
||||
name: github_app_access_token
|
||||
namespace: null
|
||||
release_date: '2024-01-01'
|
||||
8.3.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- homebrew - detect already installed formulae and casks using JSON output from
|
||||
``brew info`` (https://github.com/ansible-collections/community.general/issues/864).
|
||||
- incus connection plugin - treats ``inventory_hostname`` as a variable instead
|
||||
of a literal in remote connections (https://github.com/ansible-collections/community.general/issues/7874).
|
||||
- ipa_otptoken - the module expect ``ipatokendisabled`` as string but the ``ipatokendisabled``
|
||||
value is returned as a boolean (https://github.com/ansible-collections/community.general/pull/7795).
|
||||
- ldap - previously the order number (if present) was expected to follow an
|
||||
equals sign in the DN. This makes it so the order number string is identified
|
||||
correctly anywhere within the DN (https://github.com/ansible-collections/community.general/issues/7646).
|
||||
- mssql_script - make the module work with Python 2 (https://github.com/ansible-collections/community.general/issues/7818,
|
||||
https://github.com/ansible-collections/community.general/pull/7821).
|
||||
- nmcli - fix ``connection.slave-type`` wired to ``bond`` and not with parameter
|
||||
``slave_type`` in case of connection type ``wifi`` (https://github.com/ansible-collections/community.general/issues/7389).
|
||||
- proxmox - fix updating a container config if the setting does not already
|
||||
exist (https://github.com/ansible-collections/community.general/pull/7872).
|
||||
deprecated_features:
|
||||
- consul_acl - the module has been deprecated and will be removed in community.general
|
||||
10.0.0. ``consul_token`` and ``consul_policy`` can be used instead (https://github.com/ansible-collections/community.general/pull/7901).
|
||||
minor_changes:
|
||||
- consul_auth_method, consul_binding_rule, consul_policy, consul_role, consul_session,
|
||||
consul_token - added action group ``community.general.consul`` (https://github.com/ansible-collections/community.general/pull/7897).
|
||||
- consul_policy - added support for diff and check mode (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_policy, consul_role, consul_session - removed dependency on ``requests``
|
||||
and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826,
|
||||
https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - ``node_identities`` now expects a ``node_name`` option to match
|
||||
the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - ``service_identities`` now expects a ``service_name`` option
|
||||
to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - added support for diff mode (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- consul_role - added support for templated policies (https://github.com/ansible-collections/community.general/pull/7878).
|
||||
- redfish_info - add command ``GetServiceIdentification`` to get service identification
|
||||
(https://github.com/ansible-collections/community.general/issues/7882).
|
||||
- terraform - add support for ``diff_mode`` for terraform resource_changes (https://github.com/ansible-collections/community.general/pull/7896).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 7389-nmcli-issue-with-creating-a-wifi-bridge-slave.yml
|
||||
- 7646-fix-order-number-detection-in-dn.yml
|
||||
- 7797-ipa-fix-otp-idempotency.yml
|
||||
- 7821-mssql_script-py2.yml
|
||||
- 7826-consul-modules-refactoring.yaml
|
||||
- 7870-homebrew-cask-installed-detection.yml
|
||||
- 7872-proxmox_fix-update-if-setting-doesnt-exist.yaml
|
||||
- 7874-incus_connection_treats_inventory_hostname_as_literal_in_remotes.yml
|
||||
- 7882-add-redfish-get-service-identification.yml
|
||||
- 7896-add-terraform-diff-mode.yml
|
||||
- 7897-consul-action-group.yaml
|
||||
- 7901-consul-acl-deprecation.yaml
|
||||
- 8.3.0.yml
|
||||
modules:
|
||||
- description: Bootstrap ACLs in Consul
|
||||
name: consul_acl_bootstrap
|
||||
namespace: ''
|
||||
- description: Manipulate Consul auth methods
|
||||
name: consul_auth_method
|
||||
namespace: ''
|
||||
- description: Manipulate Consul binding rules
|
||||
name: consul_binding_rule
|
||||
namespace: ''
|
||||
- description: Manipulate Consul tokens
|
||||
name: consul_token
|
||||
namespace: ''
|
||||
- description: Creates/updates/deletes GitLab Labels belonging to project or group.
|
||||
name: gitlab_label
|
||||
namespace: ''
|
||||
- description: Creates/updates/deletes GitLab Milestones belonging to project
|
||||
or group
|
||||
name: gitlab_milestone
|
||||
namespace: ''
|
||||
release_date: '2024-01-29'
|
||||
8.4.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- 'cargo - fix idempotency issues when using a custom installation path for
|
||||
packages (using the ``--path`` parameter). The initial installation runs fine,
|
||||
but subsequent runs use the ``get_installed()`` function which did not check
|
||||
the given installation location, before running ``cargo install``. This resulted
|
||||
in a false ``changed`` state. Also the removal of packeges using ``state:
|
||||
absent`` failed, as the installation check did not use the given parameter
|
||||
(https://github.com/ansible-collections/community.general/pull/7970).'
|
||||
- gitlab_issue - fix behavior to search GitLab issue, using ``search`` keyword
|
||||
instead of ``title`` (https://github.com/ansible-collections/community.general/issues/7846).
|
||||
- gitlab_runner - fix pagination when checking for existing runners (https://github.com/ansible-collections/community.general/pull/7790).
|
||||
- keycloak_client - fixes issue when metadata is provided in desired state when
|
||||
task is in check mode (https://github.com/ansible-collections/community.general/issues/1226,
|
||||
https://github.com/ansible-collections/community.general/pull/7881).
|
||||
- modprobe - listing modules files or modprobe files could trigger a FileNotFoundError
|
||||
if ``/etc/modprobe.d`` or ``/etc/modules-load.d`` did not exist. Relevant
|
||||
functions now return empty lists if the directories do not exist to avoid
|
||||
crashing the module (https://github.com/ansible-collections/community.general/issues/7717).
|
||||
- onepassword lookup plugin - failed for fields that were in sections and had
|
||||
uppercase letters in the label/ID. Field lookups are now case insensitive
|
||||
in all cases (https://github.com/ansible-collections/community.general/pull/7919).
|
||||
- pkgin - pkgin (pkgsrc package manager used by SmartOS) raises erratic exceptions
|
||||
and spurious ``changed=true`` (https://github.com/ansible-collections/community.general/pull/7971).
|
||||
- redfish_info - allow for a GET operation invoked by ``GetUpdateStatus`` to
|
||||
allow for an empty response body for cases where a service returns 204 No
|
||||
Content (https://github.com/ansible-collections/community.general/issues/8003).
|
||||
- redfish_info - correct uncaught exception when attempting to retrieve ``Chassis``
|
||||
information (https://github.com/ansible-collections/community.general/pull/7952).
|
||||
minor_changes:
|
||||
- bitwarden lookup plugin - add ``bw_session`` option, to pass session key instead
|
||||
of reading from env (https://github.com/ansible-collections/community.general/pull/7994).
|
||||
- gitlab_deploy_key, gitlab_group_members, gitlab_group_variable, gitlab_hook,
|
||||
gitlab_instance_variable, gitlab_project_badge, gitlab_project_variable, gitlab_user
|
||||
- improve API pagination and compatibility with different versions of ``python-gitlab``
|
||||
(https://github.com/ansible-collections/community.general/pull/7790).
|
||||
- gitlab_hook - adds ``releases_events`` parameter for supporting Releases events
|
||||
triggers on GitLab hooks (https://github.com/ansible-collections/community.general/pull/7956).
|
||||
- icinga2 inventory plugin - add Jinja2 templating support to ``url``, ``user``,
|
||||
and ``password`` paramenters (https://github.com/ansible-collections/community.general/issues/7074,
|
||||
https://github.com/ansible-collections/community.general/pull/7996).
|
||||
- mssql_script - adds transactional (rollback/commit) support via optional boolean
|
||||
param ``transaction`` (https://github.com/ansible-collections/community.general/pull/7976).
|
||||
- proxmox_kvm - add parameter ``update_unsafe`` to avoid limitations when updating
|
||||
dangerous values (https://github.com/ansible-collections/community.general/pull/7843).
|
||||
- redfish_config - add command ``SetServiceIdentification`` to set service identification
|
||||
(https://github.com/ansible-collections/community.general/issues/7916).
|
||||
- sudoers - add support for the ``NOEXEC`` tag in sudoers rules (https://github.com/ansible-collections/community.general/pull/7983).
|
||||
- terraform - fix ``diff_mode`` in state ``absent`` and when terraform ``resource_changes``
|
||||
does not exist (https://github.com/ansible-collections/community.general/pull/7963).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 7717-prevent-modprobe-error.yml
|
||||
- 7790-gitlab-runner-api-pagination.yml
|
||||
- 7843-proxmox_kvm-update_unsafe.yml
|
||||
- 7847-gitlab-issue-title.yml
|
||||
- 7881-fix-keycloak-client-ckeckmode.yml
|
||||
- 7916-add-redfish-set-service-identification.yml
|
||||
- 7919-onepassword-fieldname-casing.yaml
|
||||
- 7951-fix-redfish_info-exception.yml
|
||||
- 7956-adding-releases_events-option-to-gitlab_hook-module.yaml
|
||||
- 7963-fix-terraform-diff-absent.yml
|
||||
- 7970-fix-cargo-path-idempotency.yaml
|
||||
- 7976-add-mssql_script-transactional-support.yml
|
||||
- 7983-sudoers-add-support-noexec.yml
|
||||
- 7994-bitwarden-session-arg.yaml
|
||||
- 7996-add-templating-support-to-icinga2-inventory.yml
|
||||
- 8.4.0.yml
|
||||
- 8003-redfish-get-update-status-empty-response.yml
|
||||
- pkgin.yml
|
||||
modules:
|
||||
- description: Manages GitLab group access tokens
|
||||
name: gitlab_group_access_token
|
||||
namespace: ''
|
||||
- description: Manages GitLab project access tokens
|
||||
name: gitlab_project_access_token
|
||||
namespace: ''
|
||||
plugins:
|
||||
callback:
|
||||
- description: The default ansible callback without diff output
|
||||
name: default_without_diff
|
||||
namespace: null
|
||||
filter:
|
||||
- description: Difference of lists with a predictive order
|
||||
name: lists_difference
|
||||
namespace: null
|
||||
- description: Intersection of lists with a predictive order
|
||||
name: lists_intersect
|
||||
namespace: null
|
||||
- description: Symmetric Difference of lists with a predictive order
|
||||
name: lists_symmetric_difference
|
||||
namespace: null
|
||||
- description: Union of lists with a predictive order
|
||||
name: lists_union
|
||||
namespace: null
|
||||
release_date: '2024-02-26'
|
||||
|
||||
@@ -12,6 +12,9 @@ mention_ancestor: true
|
||||
flatmap: true
|
||||
new_plugins_after_name: removed_features
|
||||
notesdir: fragments
|
||||
output_formats:
|
||||
- md
|
||||
- rst
|
||||
prelude_section_name: release_summary
|
||||
prelude_section_title: Release Summary
|
||||
sections:
|
||||
|
||||
@@ -12,4 +12,5 @@ Abstract transformations
|
||||
filter_guide_abstract_informations_dictionaries
|
||||
filter_guide_abstract_informations_grouping
|
||||
filter_guide_abstract_informations_merging_lists_of_dictionaries
|
||||
filter_guide_abstract_informations_lists_helper
|
||||
filter_guide_abstract_informations_counting_elements_in_sequence
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
..
|
||||
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
|
||||
|
||||
Union, intersection and difference of lists
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Starting with Ansible Core 2.16, the builtin filters :ansplugin:`ansible.builtin.union#filter`, :ansplugin:`ansible.builtin.intersect#filter`, :ansplugin:`ansible.builtin.difference#filter` and :ansplugin:`ansible.builtin.symmetric_difference#filter` began to behave differently and do no longer preserve the item order. Items in the resulting lists are returned in arbitrary order and the order can vary between subsequent runs.
|
||||
|
||||
The Ansible community.general collection provides the following additional list filters:
|
||||
|
||||
- :ansplugin:`community.general.lists_union#filter`
|
||||
- :ansplugin:`community.general.lists_intersect#filter`
|
||||
- :ansplugin:`community.general.lists_difference#filter`
|
||||
- :ansplugin:`community.general.lists_symmetric_difference#filter`
|
||||
|
||||
These filters preserve the item order, eliminate duplicates and are an extended version of the builtin ones, because they can operate on more than two lists.
|
||||
|
||||
.. note:: Stick to the builtin filters, when item order is not important or when you do not need the n-ary operating mode. The builtin filters are faster, because they rely mostly on sets as their underlying datastructure.
|
||||
|
||||
Let us use the lists below in the following examples:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
A: [9, 5, 7, 1, 9, 4, 10, 5, 9, 7]
|
||||
B: [4, 1, 2, 8, 3, 1, 7]
|
||||
C: [10, 2, 1, 9, 1]
|
||||
|
||||
The union of ``A`` and ``B`` can be written as:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
result: "{{ A | community.general.lists_union(B) }}"
|
||||
|
||||
This statement produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
result: [9, 5, 7, 1, 4, 10, 2, 8, 3]
|
||||
|
||||
If you want to calculate the intersection of ``A``, ``B`` and ``C``, you can use the following statement:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
result: "{{ A | community.general.lists_intersect(B, C) }}"
|
||||
|
||||
Alternatively, you can use a list of lists as an input of the filter
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
result: "{{ [A, B] | community.general.lists_intersect(C) }}"
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
result: "{{ [A, B, C] | community.general.lists_intersect(flatten=true) }}"
|
||||
|
||||
All three statements are equivalent and give:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
result: [1]
|
||||
|
||||
.. note:: Be aware that in most cases, filter calls without any argument require ``flatten=true``, otherwise the input is returned as result. The reason for this is, that the input is considered as a variable argument and is wrapped by an additional outer list. ``flatten=true`` ensures that this list is removed before the input is processed by the filter logic.
|
||||
|
||||
The filters ansplugin:`community.general.lists_difference#filter` or :ansplugin:`community.general.lists_symmetric_difference#filter` can be used in the same way as the filters in the examples above. They calculate the difference or the symmetric difference between two or more lists and preserve the item order.
|
||||
|
||||
For example, the symmetric difference of ``A``, ``B`` and ``C`` may be written as:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
result: "{{ A | community.general.lists_symmetric_difference(B, C) }}"
|
||||
|
||||
This gives:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
result: [5, 8, 3, 1]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace: community
|
||||
name: general
|
||||
version: 8.2.0
|
||||
version: 8.4.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
requires_ansible: '>=2.13.0'
|
||||
action_groups:
|
||||
consul:
|
||||
- consul_auth_method
|
||||
- consul_binding_rule
|
||||
- consul_policy
|
||||
- consul_role
|
||||
- consul_session
|
||||
- consul_token
|
||||
plugin_routing:
|
||||
connection:
|
||||
docker:
|
||||
@@ -22,6 +30,10 @@ plugin_routing:
|
||||
nios_next_network:
|
||||
redirect: infoblox.nios_modules.nios_next_network
|
||||
modules:
|
||||
consul_acl:
|
||||
deprecation:
|
||||
removal_version: 10.0.0
|
||||
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
|
||||
rax_cbs_attachments:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
@@ -153,9 +165,9 @@ plugin_routing:
|
||||
stackdriver:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: >
|
||||
This module relies on HTTPS APIs that do not exist anymore, and any new development in the
|
||||
direction of providing an alternative should happen in the context of the google.cloud collection.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore,
|
||||
and any new development in the direction of providing an alternative should
|
||||
happen in the context of the google.cloud collection.
|
||||
system.aix_devices:
|
||||
redirect: community.general.aix_devices
|
||||
deprecation:
|
||||
@@ -807,7 +819,8 @@ plugin_routing:
|
||||
flowdock:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
notification.flowdock:
|
||||
redirect: community.general.flowdock
|
||||
deprecation:
|
||||
@@ -4446,7 +4459,8 @@ plugin_routing:
|
||||
webfaction_app:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
cloud.webfaction.webfaction_app:
|
||||
redirect: community.general.webfaction_app
|
||||
deprecation:
|
||||
@@ -4457,7 +4471,8 @@ plugin_routing:
|
||||
webfaction_db:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
cloud.webfaction.webfaction_db:
|
||||
redirect: community.general.webfaction_db
|
||||
deprecation:
|
||||
@@ -4468,7 +4483,8 @@ plugin_routing:
|
||||
webfaction_domain:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
cloud.webfaction.webfaction_domain:
|
||||
redirect: community.general.webfaction_domain
|
||||
deprecation:
|
||||
@@ -4479,7 +4495,8 @@ plugin_routing:
|
||||
webfaction_mailbox:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
cloud.webfaction.webfaction_mailbox:
|
||||
redirect: community.general.webfaction_mailbox
|
||||
deprecation:
|
||||
@@ -4490,7 +4507,8 @@ plugin_routing:
|
||||
webfaction_site:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
|
||||
warning_text: This module relies on HTTPS APIs that do not exist anymore and
|
||||
there is no clear path to update.
|
||||
cloud.webfaction.webfaction_site:
|
||||
redirect: community.general.webfaction_site
|
||||
deprecation:
|
||||
@@ -4646,7 +4664,8 @@ plugin_routing:
|
||||
rackspace:
|
||||
deprecation:
|
||||
removal_version: 9.0.0
|
||||
warning_text: This doc fragment is used by rax modules, that rely on the deprecated package pyrax.
|
||||
warning_text: This doc fragment is used by rax modules, that rely on the deprecated
|
||||
package pyrax.
|
||||
_gcp:
|
||||
redirect: community.google._gcp
|
||||
docker:
|
||||
|
||||
46
plugins/callback/default_without_diff.py
Normal file
46
plugins/callback/default_without_diff.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 = r'''
|
||||
name: default_without_diff
|
||||
type: stdout
|
||||
short_description: The default ansible callback without diff output
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- This is basically the default ansible callback plugin (P(ansible.builtin.default#callback)) without
|
||||
showing diff output. This can be useful when using another callback which sends more detailed information
|
||||
to another service, like the L(ARA, https://ara.recordsansible.org/) callback, and you want diff output
|
||||
sent to that plugin but not shown on the console output.
|
||||
author: Felix Fontein (@felixfontein)
|
||||
extends_documentation_fragment:
|
||||
- ansible.builtin.default_callback
|
||||
- ansible.builtin.result_format_callback
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
# Enable callback in ansible.cfg:
|
||||
ansible_config: |
|
||||
[defaults]
|
||||
stdout_callback = community.general.default_without_diff
|
||||
|
||||
# Enable callback with environment variables:
|
||||
environment_variable: |
|
||||
ANSIBLE_STDOUT_CALLBACK=community.general.default_without_diff
|
||||
'''
|
||||
|
||||
from ansible.plugins.callback.default import CallbackModule as Default
|
||||
|
||||
|
||||
class CallbackModule(Default):
|
||||
CALLBACK_VERSION = 2.0
|
||||
CALLBACK_TYPE = 'stdout'
|
||||
CALLBACK_NAME = 'community.general.default_without_diff'
|
||||
|
||||
def v2_on_file_diff(self, result):
|
||||
pass
|
||||
@@ -21,6 +21,7 @@ DOCUMENTATION = """
|
||||
- The instance identifier.
|
||||
default: inventory_hostname
|
||||
vars:
|
||||
- name: inventory_hostname
|
||||
- name: ansible_host
|
||||
- name: ansible_incus_host
|
||||
executable:
|
||||
|
||||
@@ -10,9 +10,9 @@ __metaclass__ = type
|
||||
DOCUMENTATION = '''
|
||||
author: Matt Clay (@mattclay) <matt@mystile.com>
|
||||
name: lxd
|
||||
short_description: Run tasks in lxc containers via lxc CLI
|
||||
short_description: Run tasks in LXD instances via C(lxc) CLI
|
||||
description:
|
||||
- Run commands or put/fetch files to an existing lxc container using lxc CLI
|
||||
- Run commands or put/fetch files to an existing instance using C(lxc) CLI.
|
||||
options:
|
||||
remote_addr:
|
||||
description:
|
||||
@@ -26,7 +26,7 @@ DOCUMENTATION = '''
|
||||
- name: ansible_lxd_host
|
||||
executable:
|
||||
description:
|
||||
- shell to use for execution inside container
|
||||
- Shell to use for execution inside instance.
|
||||
default: /bin/sh
|
||||
vars:
|
||||
- name: ansible_executable
|
||||
@@ -71,7 +71,7 @@ class Connection(ConnectionBase):
|
||||
raise AnsibleError("lxc command not found in PATH")
|
||||
|
||||
if self._play_context.remote_user is not None and self._play_context.remote_user != 'root':
|
||||
self._display.warning('lxd does not support remote_user, using container default: root')
|
||||
self._display.warning('lxd does not support remote_user, using default: root')
|
||||
|
||||
def _host(self):
|
||||
""" translate remote_addr to lxd (short) hostname """
|
||||
|
||||
60
plugins/doc_fragments/consul.py
Normal file
60
plugins/doc_fragments/consul.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment:
|
||||
# Common parameters for Consul modules
|
||||
DOCUMENTATION = r"""
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
- Host of the consul agent, defaults to V(localhost).
|
||||
default: localhost
|
||||
type: str
|
||||
port:
|
||||
type: int
|
||||
description:
|
||||
- The port on which the consul agent is running.
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- The protocol scheme on which the consul agent is running.
|
||||
Defaults to V(http) and can be set to V(https) for secure connections.
|
||||
default: http
|
||||
type: str
|
||||
validate_certs:
|
||||
type: bool
|
||||
description:
|
||||
- Whether to verify the TLS certificate of the consul agent.
|
||||
default: true
|
||||
ca_path:
|
||||
description:
|
||||
- The CA bundle to use for https connections
|
||||
type: str
|
||||
"""
|
||||
|
||||
TOKEN = r"""
|
||||
options:
|
||||
token:
|
||||
description:
|
||||
- The token to use for authorization.
|
||||
type: str
|
||||
"""
|
||||
|
||||
ACTIONGROUP_CONSUL = r"""
|
||||
options: {}
|
||||
attributes:
|
||||
action_group:
|
||||
description: Use C(group/community.general.consul) in C(module_defaults) to set defaults for this module.
|
||||
support: full
|
||||
membership:
|
||||
- community.general.consul
|
||||
version_added: 8.3.0
|
||||
"""
|
||||
210
plugins/filter/lists.py
Normal file
210
plugins/filter/lists.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from ansible.module_utils.common.collections import is_sequence
|
||||
|
||||
|
||||
def remove_duplicates(lst):
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
result = []
|
||||
for item in lst:
|
||||
try:
|
||||
if item not in seen:
|
||||
seen_add(item)
|
||||
result.append(item)
|
||||
except TypeError:
|
||||
# This happens for unhashable values `item`. If this happens,
|
||||
# convert `seen` to a list and continue.
|
||||
seen = list(seen)
|
||||
seen_add = seen.append
|
||||
if item not in seen:
|
||||
seen_add(item)
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def flatten_list(lst):
|
||||
result = []
|
||||
for sublist in lst:
|
||||
if not is_sequence(sublist):
|
||||
msg = ("All arguments must be lists. %s is %s")
|
||||
raise AnsibleFilterError(msg % (sublist, type(sublist)))
|
||||
if len(sublist) > 0:
|
||||
if all(is_sequence(sub) for sub in sublist):
|
||||
for item in sublist:
|
||||
result.append(item)
|
||||
else:
|
||||
result.append(sublist)
|
||||
return result
|
||||
|
||||
|
||||
def lists_union(*args, **kwargs):
|
||||
lists = args
|
||||
flatten = kwargs.pop('flatten', False)
|
||||
|
||||
if kwargs:
|
||||
# Some unused kwargs remain
|
||||
raise AnsibleFilterError(
|
||||
"lists_union() got unexpected keywords arguments: {0}".format(
|
||||
", ".join(kwargs.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if flatten:
|
||||
lists = flatten_list(args)
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
a = lists[0]
|
||||
for b in lists[1:]:
|
||||
a = do_union(a, b)
|
||||
return remove_duplicates(a)
|
||||
|
||||
|
||||
def do_union(a, b):
|
||||
return a + b
|
||||
|
||||
|
||||
def lists_intersect(*args, **kwargs):
|
||||
lists = args
|
||||
flatten = kwargs.pop('flatten', False)
|
||||
|
||||
if kwargs:
|
||||
# Some unused kwargs remain
|
||||
raise AnsibleFilterError(
|
||||
"lists_intersect() got unexpected keywords arguments: {0}".format(
|
||||
", ".join(kwargs.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if flatten:
|
||||
lists = flatten_list(args)
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
a = remove_duplicates(lists[0])
|
||||
for b in lists[1:]:
|
||||
a = do_intersect(a, b)
|
||||
return a
|
||||
|
||||
|
||||
def do_intersect(a, b):
|
||||
isect = []
|
||||
try:
|
||||
other = set(b)
|
||||
isect = [item for item in a if item in other]
|
||||
except TypeError:
|
||||
# This happens for unhashable values,
|
||||
# use a list instead and redo.
|
||||
other = list(b)
|
||||
isect = [item for item in a if item in other]
|
||||
return isect
|
||||
|
||||
|
||||
def lists_difference(*args, **kwargs):
|
||||
lists = args
|
||||
flatten = kwargs.pop('flatten', False)
|
||||
|
||||
if kwargs:
|
||||
# Some unused kwargs remain
|
||||
raise AnsibleFilterError(
|
||||
"lists_difference() got unexpected keywords arguments: {0}".format(
|
||||
", ".join(kwargs.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if flatten:
|
||||
lists = flatten_list(args)
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
a = remove_duplicates(lists[0])
|
||||
for b in lists[1:]:
|
||||
a = do_difference(a, b)
|
||||
return a
|
||||
|
||||
|
||||
def do_difference(a, b):
|
||||
diff = []
|
||||
try:
|
||||
other = set(b)
|
||||
diff = [item for item in a if item not in other]
|
||||
except TypeError:
|
||||
# This happens for unhashable values,
|
||||
# use a list instead and redo.
|
||||
other = list(b)
|
||||
diff = [item for item in a if item not in other]
|
||||
return diff
|
||||
|
||||
|
||||
def lists_symmetric_difference(*args, **kwargs):
|
||||
lists = args
|
||||
flatten = kwargs.pop('flatten', False)
|
||||
|
||||
if kwargs:
|
||||
# Some unused kwargs remain
|
||||
raise AnsibleFilterError(
|
||||
"lists_difference() got unexpected keywords arguments: {0}".format(
|
||||
", ".join(kwargs.keys())
|
||||
)
|
||||
)
|
||||
|
||||
if flatten:
|
||||
lists = flatten_list(args)
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
a = lists[0]
|
||||
for b in lists[1:]:
|
||||
a = do_symmetric_difference(a, b)
|
||||
return a
|
||||
|
||||
|
||||
def do_symmetric_difference(a, b):
|
||||
sym_diff = []
|
||||
union = lists_union(a, b)
|
||||
try:
|
||||
isect = set(a) & set(b)
|
||||
sym_diff = [item for item in union if item not in isect]
|
||||
except TypeError:
|
||||
# This happens for unhashable values,
|
||||
# build the intersection of `a` and `b` backed
|
||||
# by a list instead of a set and redo.
|
||||
isect = lists_intersect(a, b)
|
||||
sym_diff = [item for item in union if item not in isect]
|
||||
return sym_diff
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
''' Ansible lists jinja2 filters '''
|
||||
|
||||
def filters(self):
|
||||
return {
|
||||
'lists_union': lists_union,
|
||||
'lists_intersect': lists_intersect,
|
||||
'lists_difference': lists_difference,
|
||||
'lists_symmetric_difference': lists_symmetric_difference,
|
||||
}
|
||||
48
plugins/filter/lists_difference.yml
Normal file
48
plugins/filter/lists_difference.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
DOCUMENTATION:
|
||||
name: lists_difference
|
||||
short_description: Difference of lists with a predictive order
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Provide a unique list of all the elements from the first which do not appear in the other lists.
|
||||
- The order of the items in the resulting list is preserved.
|
||||
options:
|
||||
_input:
|
||||
description: A list.
|
||||
type: list
|
||||
elements: any
|
||||
required: true
|
||||
flatten:
|
||||
description: Whether to remove one hierarchy level from the input list.
|
||||
type: boolean
|
||||
default: false
|
||||
author:
|
||||
- Christoph Fiehe (@cfiehe)
|
||||
|
||||
EXAMPLES: |
|
||||
- name: Return the difference of list1 and list2.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ list1 | community.general.lists_difference(list2) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
# => [10]
|
||||
|
||||
- name: Return the difference of list1, list2 and list3.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ [list1, list2, list3] | community.general.lists_difference(flatten=true) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
list3: [1, 2, 3, 4, 5, 10, 99, 101]
|
||||
# => []
|
||||
|
||||
RETURN:
|
||||
_value:
|
||||
description: A unique list of all the elements from the first list that do not appear on the other lists.
|
||||
type: list
|
||||
elements: any
|
||||
48
plugins/filter/lists_intersect.yml
Normal file
48
plugins/filter/lists_intersect.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
DOCUMENTATION:
|
||||
name: lists_intersect
|
||||
short_description: Intersection of lists with a predictive order
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Provide a unique list of all the common elements of two or more lists.
|
||||
- The order of the items in the resulting list is preserved.
|
||||
options:
|
||||
_input:
|
||||
description: A list.
|
||||
type: list
|
||||
elements: any
|
||||
required: true
|
||||
flatten:
|
||||
description: Whether to remove one hierarchy level from the input list.
|
||||
type: boolean
|
||||
default: false
|
||||
author:
|
||||
- Christoph Fiehe (@cfiehe)
|
||||
|
||||
EXAMPLES: |
|
||||
- name: Return the intersection of list1 and list2.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ list1 | community.general.lists_intersect(list2) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
# => [1, 2, 5, 3, 4]
|
||||
|
||||
- name: Return the intersection of list1, list2 and list3.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ [list1, list2, list3] | community.general.lists_intersect(flatten=true) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
list3: [1, 2, 3, 4, 5, 10, 99, 101]
|
||||
# => [1, 2, 5, 3, 4]
|
||||
|
||||
RETURN:
|
||||
_value:
|
||||
description: A unique list of all the common elements from the provided lists.
|
||||
type: list
|
||||
elements: any
|
||||
48
plugins/filter/lists_symmetric_difference.yml
Normal file
48
plugins/filter/lists_symmetric_difference.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
DOCUMENTATION:
|
||||
name: lists_symmetric_difference
|
||||
short_description: Symmetric Difference of lists with a predictive order
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Provide a unique list containing the symmetric difference of two or more lists.
|
||||
- The order of the items in the resulting list is preserved.
|
||||
options:
|
||||
_input:
|
||||
description: A list.
|
||||
type: list
|
||||
elements: any
|
||||
required: true
|
||||
flatten:
|
||||
description: Whether to remove one hierarchy level from the input list.
|
||||
type: boolean
|
||||
default: false
|
||||
author:
|
||||
- Christoph Fiehe (@cfiehe)
|
||||
|
||||
EXAMPLES: |
|
||||
- name: Return the symmetric difference of list1 and list2.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ list1 | community.general.lists_symmetric_difference(list2) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
# => [10, 11, 99]
|
||||
|
||||
- name: Return the symmetric difference of list1, list2 and list3.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ [list1, list2, list3] | community.general.lists_symmetric_difference(flatten=true) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
list3: [1, 2, 3, 4, 5, 10, 99, 101]
|
||||
# => [11, 1, 2, 3, 4, 5, 101]
|
||||
|
||||
RETURN:
|
||||
_value:
|
||||
description: A unique list containing the symmetric difference of two or more lists.
|
||||
type: list
|
||||
elements: any
|
||||
48
plugins/filter/lists_union.yml
Normal file
48
plugins/filter/lists_union.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
DOCUMENTATION:
|
||||
name: lists_union
|
||||
short_description: Union of lists with a predictive order
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Provide a unique list of all the elements of two or more lists.
|
||||
- The order of the items in the resulting list is preserved.
|
||||
options:
|
||||
_input:
|
||||
description: A list.
|
||||
type: list
|
||||
elements: any
|
||||
required: true
|
||||
flatten:
|
||||
description: Whether to remove one hierarchy level from the input list.
|
||||
type: boolean
|
||||
default: false
|
||||
author:
|
||||
- Christoph Fiehe (@cfiehe)
|
||||
|
||||
EXAMPLES: |
|
||||
- name: Return the union of list1, list2 and list3.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ list1 | community.general.lists_union(list2, list3) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
list3: [1, 2, 3, 4, 5, 10, 99, 101]
|
||||
# => [1, 2, 5, 3, 4, 10, 11, 99, 101]
|
||||
|
||||
- name: Return the union of list1 and list2.
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ [list1, list2] | community.general.lists_union(flatten=true) }}"
|
||||
vars:
|
||||
list1: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
# => [1, 2, 5, 3, 4, 10, 11, 99]
|
||||
|
||||
RETURN:
|
||||
_value:
|
||||
description: A unique list of all the elements from the provided lists.
|
||||
type: list
|
||||
elements: any
|
||||
@@ -277,12 +277,22 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
self._read_config_data(path)
|
||||
|
||||
# Store the options from the YAML file
|
||||
self.icinga2_url = self.get_option('url').rstrip('/') + '/v1'
|
||||
self.icinga2_url = self.get_option('url')
|
||||
self.icinga2_user = self.get_option('user')
|
||||
self.icinga2_password = self.get_option('password')
|
||||
self.ssl_verify = self.get_option('validate_certs')
|
||||
self.host_filter = self.get_option('host_filter')
|
||||
self.inventory_attr = self.get_option('inventory_attr')
|
||||
|
||||
if self.templar.is_template(self.icinga2_url):
|
||||
self.icinga2_url = self.templar.template(variable=self.icinga2_url, disable_lookups=False)
|
||||
if self.templar.is_template(self.icinga2_user):
|
||||
self.icinga2_user = self.templar.template(variable=self.icinga2_user, disable_lookups=False)
|
||||
if self.templar.is_template(self.icinga2_password):
|
||||
self.icinga2_password = self.templar.template(variable=self.icinga2_password, disable_lookups=False)
|
||||
|
||||
self.icinga2_url = self.icinga2_url.rstrip('/') + '/v1'
|
||||
|
||||
# Not currently enabled
|
||||
# self.cache_key = self.get_cache_key(path)
|
||||
# self.use_cache = cache and self.get_option('cache')
|
||||
|
||||
@@ -470,7 +470,7 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Helper to get the preferred interface provide by neme pattern from 'prefered_instance_network_interface'.
|
||||
|
||||
Args:
|
||||
str(containe_name): name of instance
|
||||
str(instance_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -495,7 +495,7 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Helper to get the VLAN_ID from the instance
|
||||
|
||||
Args:
|
||||
str(containe_name): name of instance
|
||||
str(instance_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
|
||||
@@ -39,6 +39,10 @@ DOCUMENTATION = """
|
||||
description: Collection ID to filter results by collection. Leave unset to skip filtering.
|
||||
type: str
|
||||
version_added: 6.3.0
|
||||
bw_session:
|
||||
description: Pass session key instead of reading from env.
|
||||
type: str
|
||||
version_added: 8.4.0
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
@@ -66,6 +70,11 @@ EXAMPLES = """
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='api_key') }}
|
||||
|
||||
- name: "Get 'password' from all Bitwarden records named 'a_test', using given session key"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ lookup('community.general.bitwarden', 'a_test', field='password', bw_session='bXZ9B5TXi6...') }}
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
@@ -94,11 +103,20 @@ class Bitwarden(object):
|
||||
|
||||
def __init__(self, path='bw'):
|
||||
self._cli_path = path
|
||||
self._session = None
|
||||
|
||||
@property
|
||||
def cli_path(self):
|
||||
return self._cli_path
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
@session.setter
|
||||
def session(self, value):
|
||||
self._session = value
|
||||
|
||||
@property
|
||||
def unlocked(self):
|
||||
out, err = self._run(['status'], stdin="")
|
||||
@@ -106,6 +124,9 @@ class Bitwarden(object):
|
||||
return decoded['status'] == 'unlocked'
|
||||
|
||||
def _run(self, args, stdin=None, expected_rc=0):
|
||||
if self.session:
|
||||
args += ['--session', self.session]
|
||||
|
||||
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
||||
out, err = p.communicate(to_bytes(stdin))
|
||||
rc = p.wait()
|
||||
@@ -179,6 +200,8 @@ class LookupModule(LookupBase):
|
||||
field = self.get_option('field')
|
||||
search_field = self.get_option('search')
|
||||
collection_id = self.get_option('collection_id')
|
||||
_bitwarden.session = self.get_option('bw_session')
|
||||
|
||||
if not _bitwarden.unlocked:
|
||||
raise AnsibleError("Bitwarden Vault locked. Run 'bw unlock'.")
|
||||
|
||||
|
||||
@@ -489,10 +489,10 @@ class OnePassCLIv2(OnePassCLIBase):
|
||||
current_section_title = section.get("label", section.get("id", "")).lower()
|
||||
if section_title == current_section_title:
|
||||
# In the correct section. Check "label" then "id" for the desired field_name
|
||||
if field.get("label") == field_name:
|
||||
if field.get("label", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
if field.get("id") == field_name:
|
||||
if field.get("id", "").lower() == field_name:
|
||||
return field.get("value", "")
|
||||
|
||||
return ""
|
||||
|
||||
@@ -5,25 +5,317 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
from ansible.module_utils.six.moves.urllib import error as urllib_error
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
|
||||
def get_consul_url(configuration):
|
||||
return '%s://%s:%s/v1' % (configuration.scheme,
|
||||
configuration.host, configuration.port)
|
||||
return "%s://%s:%s/v1" % (
|
||||
configuration.scheme,
|
||||
configuration.host,
|
||||
configuration.port,
|
||||
)
|
||||
|
||||
|
||||
def get_auth_headers(configuration):
|
||||
if configuration.token is None:
|
||||
return {}
|
||||
else:
|
||||
return {'X-Consul-Token': configuration.token}
|
||||
return {"X-Consul-Token": configuration.token}
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
pass
|
||||
def __init__(self, status, response_data=None):
|
||||
self.status = status
|
||||
self.response_data = response_data
|
||||
|
||||
def __str__(self):
|
||||
if self.response_data is None:
|
||||
# self.status is already the message (backwards compat)
|
||||
return self.status
|
||||
return "HTTP %d: %s" % (self.status, self.response_data)
|
||||
|
||||
|
||||
def handle_consul_response_error(response):
|
||||
if 400 <= response.status_code < 600:
|
||||
raise RequestError('%d %s' % (response.status_code, response.content))
|
||||
raise RequestError("%d %s" % (response.status_code, response.content))
|
||||
|
||||
|
||||
AUTH_ARGUMENTS_SPEC = dict(
|
||||
host=dict(default="localhost"),
|
||||
port=dict(type="int", default=8500),
|
||||
scheme=dict(default="http"),
|
||||
validate_certs=dict(type="bool", default=True),
|
||||
token=dict(no_log=True),
|
||||
ca_path=dict(),
|
||||
)
|
||||
|
||||
|
||||
def camel_case_key(key):
|
||||
parts = []
|
||||
for part in key.split("_"):
|
||||
if part in {"id", "ttl", "jwks", "jwt", "oidc", "iam", "sts"}:
|
||||
parts.append(part.upper())
|
||||
else:
|
||||
parts.append(part.capitalize())
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
STATE_PARAMETER = "state"
|
||||
STATE_PRESENT = "present"
|
||||
STATE_ABSENT = "absent"
|
||||
|
||||
OPERATION_READ = "read"
|
||||
OPERATION_CREATE = "create"
|
||||
OPERATION_UPDATE = "update"
|
||||
OPERATION_DELETE = "remove"
|
||||
|
||||
|
||||
def _normalize_params(params, arg_spec):
|
||||
final_params = {}
|
||||
for k, v in params.items():
|
||||
if k not in arg_spec: # Alias
|
||||
continue
|
||||
spec = arg_spec[k]
|
||||
if (
|
||||
spec.get("type") == "list"
|
||||
and spec.get("elements") == "dict"
|
||||
and spec.get("options")
|
||||
and v
|
||||
):
|
||||
v = [_normalize_params(d, spec["options"]) for d in v]
|
||||
elif spec.get("type") == "dict" and spec.get("options") and v:
|
||||
v = _normalize_params(v, spec["options"])
|
||||
final_params[k] = v
|
||||
return final_params
|
||||
|
||||
|
||||
class _ConsulModule:
|
||||
"""Base class for Consul modules.
|
||||
|
||||
This class is considered private, till the API is fully fleshed out.
|
||||
As such backwards incompatible changes can occur even in bugfix releases.
|
||||
"""
|
||||
|
||||
api_endpoint = None # type: str
|
||||
unique_identifier = None # type: str
|
||||
result_key = None # type: str
|
||||
create_only_fields = set()
|
||||
params = {}
|
||||
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
self.params = _normalize_params(module.params, module.argument_spec)
|
||||
self.api_params = {
|
||||
k: camel_case_key(k)
|
||||
for k in self.params
|
||||
if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC
|
||||
}
|
||||
|
||||
def execute(self):
|
||||
obj = self.read_object()
|
||||
|
||||
changed = False
|
||||
diff = {}
|
||||
if self.params[STATE_PARAMETER] == STATE_PRESENT:
|
||||
obj_from_module = self.module_to_obj(obj is not None)
|
||||
if obj is None:
|
||||
operation = OPERATION_CREATE
|
||||
new_obj = self.create_object(obj_from_module)
|
||||
diff = {"before": {}, "after": new_obj}
|
||||
changed = True
|
||||
else:
|
||||
operation = OPERATION_UPDATE
|
||||
if self._needs_update(obj, obj_from_module):
|
||||
new_obj = self.update_object(obj, obj_from_module)
|
||||
diff = {"before": obj, "after": new_obj}
|
||||
changed = True
|
||||
else:
|
||||
new_obj = obj
|
||||
elif self.params[STATE_PARAMETER] == STATE_ABSENT:
|
||||
operation = OPERATION_DELETE
|
||||
if obj is not None:
|
||||
self.delete_object(obj)
|
||||
changed = True
|
||||
diff = {"before": obj, "after": {}}
|
||||
else:
|
||||
diff = {"before": {}, "after": {}}
|
||||
new_obj = None
|
||||
else:
|
||||
raise RuntimeError("Unknown state supplied.")
|
||||
|
||||
result = {"changed": changed}
|
||||
if changed:
|
||||
result["operation"] = operation
|
||||
if self._module._diff:
|
||||
result["diff"] = diff
|
||||
if self.result_key:
|
||||
result[self.result_key] = new_obj
|
||||
self._module.exit_json(**result)
|
||||
|
||||
def module_to_obj(self, is_update):
|
||||
obj = {}
|
||||
for k, v in self.params.items():
|
||||
result = self.map_param(k, v, is_update)
|
||||
if result:
|
||||
obj[result[0]] = result[1]
|
||||
return obj
|
||||
|
||||
def map_param(self, k, v, is_update):
|
||||
def helper(item):
|
||||
return {camel_case_key(k): v for k, v in item.items()}
|
||||
|
||||
def needs_camel_case(k):
|
||||
spec = self._module.argument_spec[k]
|
||||
return (
|
||||
spec.get("type") == "list"
|
||||
and spec.get("elements") == "dict"
|
||||
and spec.get("options")
|
||||
) or (spec.get("type") == "dict" and spec.get("options"))
|
||||
|
||||
if k in self.api_params and v is not None:
|
||||
if isinstance(v, dict) and needs_camel_case(k):
|
||||
v = helper(v)
|
||||
elif isinstance(v, (list, tuple)) and needs_camel_case(k):
|
||||
v = [helper(i) for i in v]
|
||||
if is_update and k in self.create_only_fields:
|
||||
return
|
||||
return camel_case_key(k), v
|
||||
|
||||
def _needs_update(self, api_obj, module_obj):
|
||||
api_obj = copy.deepcopy(api_obj)
|
||||
module_obj = copy.deepcopy(module_obj)
|
||||
return self.needs_update(api_obj, module_obj)
|
||||
|
||||
def needs_update(self, api_obj, module_obj):
|
||||
for k, v in module_obj.items():
|
||||
if k not in api_obj:
|
||||
return True
|
||||
if api_obj[k] != v:
|
||||
return True
|
||||
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
|
||||
}
|
||||
for k, v in obj.items():
|
||||
existing[k] = v
|
||||
return existing
|
||||
|
||||
def endpoint_url(self, operation, identifier=None):
|
||||
if operation == OPERATION_CREATE:
|
||||
return self.api_endpoint
|
||||
elif identifier:
|
||||
return "/".join([self.api_endpoint, identifier])
|
||||
raise RuntimeError("invalid arguments passed")
|
||||
|
||||
def read_object(self):
|
||||
url = self.endpoint_url(OPERATION_READ, self.params.get(self.unique_identifier))
|
||||
try:
|
||||
return self.get(url)
|
||||
except RequestError as e:
|
||||
if e.status == 404:
|
||||
return
|
||||
elif e.status == 403 and b"ACL not found" in e.response_data:
|
||||
return
|
||||
raise
|
||||
|
||||
def create_object(self, obj):
|
||||
if self._module.check_mode:
|
||||
return obj
|
||||
else:
|
||||
return self.put(self.api_endpoint, data=self.prepare_object({}, 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)
|
||||
|
||||
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))
|
||||
)
|
||||
return self.delete(url)
|
||||
|
||||
def _request(self, method, url_parts, data=None, params=None):
|
||||
module_params = self.params
|
||||
|
||||
if not isinstance(url_parts, (tuple, list)):
|
||||
url_parts = [url_parts]
|
||||
if params:
|
||||
# Remove values that are None
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
ca_path = module_params.get("ca_path")
|
||||
base_url = "%s://%s:%s/v1" % (
|
||||
module_params["scheme"],
|
||||
module_params["host"],
|
||||
module_params["port"],
|
||||
)
|
||||
url = "/".join([base_url] + list(url_parts))
|
||||
|
||||
headers = {}
|
||||
token = self.params.get("token")
|
||||
if token:
|
||||
headers["X-Consul-Token"] = token
|
||||
|
||||
try:
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
headers["Content-Type"] = "application/json"
|
||||
if params:
|
||||
url = "%s?%s" % (url, urlencode(params))
|
||||
response = open_url(
|
||||
url,
|
||||
method=method,
|
||||
data=data,
|
||||
headers=headers,
|
||||
validate_certs=module_params["validate_certs"],
|
||||
ca_path=ca_path,
|
||||
)
|
||||
response_data = response.read()
|
||||
status = (
|
||||
response.status if hasattr(response, "status") else response.getcode()
|
||||
)
|
||||
|
||||
except urllib_error.URLError as e:
|
||||
if isinstance(e, urllib_error.HTTPError):
|
||||
status = e.code
|
||||
response_data = e.fp.read()
|
||||
else:
|
||||
self._module.fail_json(
|
||||
msg="Could not connect to consul agent at %s:%s, error was %s"
|
||||
% (module_params["host"], module_params["port"], str(e))
|
||||
)
|
||||
raise
|
||||
|
||||
if 400 <= status < 600:
|
||||
raise RequestError(status, response_data)
|
||||
|
||||
return json.loads(response_data)
|
||||
|
||||
def get(self, url_parts, **kwargs):
|
||||
return self._request("GET", url_parts, **kwargs)
|
||||
|
||||
def put(self, url_parts, **kwargs):
|
||||
return self._request("PUT", url_parts, **kwargs)
|
||||
|
||||
def delete(self, url_parts, **kwargs):
|
||||
return self._request("DELETE", url_parts, **kwargs)
|
||||
|
||||
@@ -21,15 +21,30 @@ except ImportError:
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
def _determine_list_all_kwargs(version):
|
||||
gitlab_version = LooseVersion(version)
|
||||
if gitlab_version >= LooseVersion('4.0.0'):
|
||||
# 4.0.0 removed 'as_list'
|
||||
return {'iterator': True, 'per_page': 100}
|
||||
elif gitlab_version >= LooseVersion('3.7.0'):
|
||||
# 3.7.0 added 'get_all'
|
||||
return {'as_list': False, 'get_all': True, 'per_page': 100}
|
||||
else:
|
||||
return {'as_list': False, 'all': True, 'per_page': 100}
|
||||
|
||||
|
||||
GITLAB_IMP_ERR = None
|
||||
try:
|
||||
import gitlab
|
||||
import requests
|
||||
HAS_GITLAB_PACKAGE = True
|
||||
list_all_kwargs = _determine_list_all_kwargs(gitlab.__version__)
|
||||
except Exception:
|
||||
gitlab = None
|
||||
GITLAB_IMP_ERR = traceback.format_exc()
|
||||
HAS_GITLAB_PACKAGE = False
|
||||
list_all_kwargs = {}
|
||||
|
||||
|
||||
def auth_argument_spec(spec=None):
|
||||
@@ -59,11 +74,11 @@ def find_project(gitlab_instance, identifier):
|
||||
|
||||
def find_group(gitlab_instance, identifier):
|
||||
try:
|
||||
project = gitlab_instance.groups.get(identifier)
|
||||
group = gitlab_instance.groups.get(identifier)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
return project
|
||||
return group
|
||||
|
||||
|
||||
def ensure_gitlab_package(module):
|
||||
|
||||
@@ -139,5 +139,7 @@ class LdapGeneric(object):
|
||||
|
||||
def _xorder_dn(self):
|
||||
# match X_ORDERed DNs
|
||||
regex = r"\w+=\{\d+\}.+"
|
||||
return re.match(regex, self.module.params['dn']) is not None
|
||||
regex = r".+\{\d+\}.+"
|
||||
explode_dn = ldap.dn.explode_dn(self.module.params['dn'])
|
||||
|
||||
return re.match(regex, explode_dn[0]) is not None
|
||||
|
||||
@@ -20,6 +20,8 @@ from ansible.module_utils.six import text_type
|
||||
from ansible.module_utils.six.moves import http_client
|
||||
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
GET_HEADERS = {'accept': 'application/json', 'OData-Version': '4.0'}
|
||||
POST_HEADERS = {'content-type': 'application/json', 'accept': 'application/json',
|
||||
@@ -130,7 +132,7 @@ class RedfishUtils(object):
|
||||
return resp
|
||||
|
||||
# The following functions are to send GET/POST/PATCH/DELETE requests
|
||||
def get_request(self, uri, override_headers=None):
|
||||
def get_request(self, uri, override_headers=None, allow_no_resp=False):
|
||||
req_headers = dict(GET_HEADERS)
|
||||
if override_headers:
|
||||
req_headers.update(override_headers)
|
||||
@@ -145,13 +147,19 @@ class RedfishUtils(object):
|
||||
force_basic_auth=basic_auth, validate_certs=False,
|
||||
follow_redirects='all',
|
||||
use_proxy=True, timeout=self.timeout)
|
||||
if override_headers:
|
||||
resp = gzip.open(BytesIO(resp.read()), 'rt', encoding='utf-8')
|
||||
data = json.loads(to_native(resp.read()))
|
||||
headers = req_headers
|
||||
else:
|
||||
data = json.loads(to_native(resp.read()))
|
||||
headers = dict((k.lower(), v) for (k, v) in resp.info().items())
|
||||
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'):
|
||||
# Older versions of Ansible do not automatically decompress the data
|
||||
# Starting in 2.14, open_url will decompress the response data by default
|
||||
data = json.loads(to_native(gzip.open(BytesIO(resp.read()), 'rt', encoding='utf-8').read()))
|
||||
else:
|
||||
data = json.loads(to_native(resp.read()))
|
||||
except Exception as e:
|
||||
# No response data; this is okay in certain cases
|
||||
data = None
|
||||
if not allow_no_resp:
|
||||
raise
|
||||
except HTTPError as e:
|
||||
msg = self._get_extended_message(e)
|
||||
return {'ret': False,
|
||||
@@ -1813,7 +1821,7 @@ class RedfishUtils(object):
|
||||
return {'ret': False, 'msg': 'Must provide a handle tracking the update.'}
|
||||
|
||||
# Get the task or job tracking the update
|
||||
response = self.get_request(self.root_uri + update_handle)
|
||||
response = self.get_request(self.root_uri + update_handle, allow_no_resp=True)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
|
||||
@@ -2907,8 +2915,7 @@ class RedfishUtils(object):
|
||||
|
||||
# Get a list of all Chassis and build URIs, then get all PowerSupplies
|
||||
# from each Power entry in the Chassis
|
||||
chassis_uri_list = self.chassis_uris
|
||||
for chassis_uri in chassis_uri_list:
|
||||
for chassis_uri in self.chassis_uris:
|
||||
response = self.get_request(self.root_uri + chassis_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
@@ -3365,7 +3372,8 @@ class RedfishUtils(object):
|
||||
inventory = {}
|
||||
# Get these entries, but does not fail if not found
|
||||
properties = ['Id', 'FirmwareVersion', 'ManagerType', 'Manufacturer', 'Model',
|
||||
'PartNumber', 'PowerState', 'SerialNumber', 'Status', 'UUID']
|
||||
'PartNumber', 'PowerState', 'SerialNumber', 'ServiceIdentification',
|
||||
'Status', 'UUID']
|
||||
|
||||
response = self.get_request(self.root_uri + manager_uri)
|
||||
if response['ret'] is False:
|
||||
@@ -3383,6 +3391,35 @@ class RedfishUtils(object):
|
||||
def get_multi_manager_inventory(self):
|
||||
return self.aggregate_managers(self.get_manager_inventory)
|
||||
|
||||
def get_service_identification(self, manager):
|
||||
result = {}
|
||||
if manager is None:
|
||||
if len(self.manager_uris) == 1:
|
||||
manager = self.manager_uris[0].split('/')[-1]
|
||||
elif len(self.manager_uris) > 1:
|
||||
entries = self.get_multi_manager_inventory()['entries']
|
||||
managers = [m[0]['manager_uri'] for m in entries if m[1].get('ServiceIdentification')]
|
||||
if len(managers) == 1:
|
||||
manager = managers[0].split('/')[-1]
|
||||
else:
|
||||
self.module.fail_json(msg=[
|
||||
"Multiple managers with ServiceIdentification were found: %s" % str(managers),
|
||||
"Please specify by using the 'manager' parameter in your playbook"])
|
||||
elif len(self.manager_uris) == 0:
|
||||
self.module.fail_json(msg="No manager identities were found")
|
||||
response = self.get_request(self.root_uri + '/redfish/v1/Managers/' + manager, override_headers=None)
|
||||
try:
|
||||
result['service_identification'] = response['data']['ServiceIdentification']
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Service ID not found for manager %s" % manager)
|
||||
result['ret'] = True
|
||||
return result
|
||||
|
||||
def set_service_identification(self, service_id):
|
||||
data = {"ServiceIdentification": service_id}
|
||||
resp = self.patch_request(self.root_uri + '/redfish/v1/Managers/' + self.resource_id, data, check_pyld=True)
|
||||
return resp
|
||||
|
||||
def set_session_service(self, sessions_config):
|
||||
if sessions_config is None:
|
||||
return {'ret': False, 'msg':
|
||||
@@ -3470,33 +3507,30 @@ class RedfishUtils(object):
|
||||
result = {}
|
||||
key = "Thermal"
|
||||
# Go through list
|
||||
for chassis_uri in self.chassis_uri_list:
|
||||
for chassis_uri in self.chassis_uris:
|
||||
response = self.get_request(self.root_uri + chassis_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
oem = data.get['Oem']
|
||||
hpe = oem.get['Hpe']
|
||||
thermal_config = hpe.get('ThermalConfiguration')
|
||||
result["current_thermal_config"] = thermal_config
|
||||
return result
|
||||
val = data.get('Oem', {}).get('Hpe', {}).get('ThermalConfiguration')
|
||||
if val is not None:
|
||||
return {"ret": True, "current_thermal_config": val}
|
||||
return {"ret": False}
|
||||
|
||||
def get_hpe_fan_percent_min(self):
|
||||
result = {}
|
||||
key = "Thermal"
|
||||
# Go through list
|
||||
for chassis_uri in self.chassis_uri_list:
|
||||
for chassis_uri in self.chassis_uris:
|
||||
response = self.get_request(self.root_uri + chassis_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
oem = data.get['Oem']
|
||||
hpe = oem.get['Hpe']
|
||||
fan_percent_min_config = hpe.get('FanPercentMinimum')
|
||||
result["fan_percent_min"] = fan_percent_min_config
|
||||
return result
|
||||
val = data.get('Oem', {}).get('Hpe', {}).get('FanPercentMinimum')
|
||||
if val is not None:
|
||||
return {"ret": True, "fan_percent_min": val}
|
||||
return {"ret": False}
|
||||
|
||||
def delete_volumes(self, storage_subsystem_id, volume_ids):
|
||||
# Find the Storage resource from the requested ComputerSystem resource
|
||||
|
||||
@@ -35,7 +35,9 @@ options:
|
||||
default: false
|
||||
name:
|
||||
description:
|
||||
- A package name, like V(foo), or multiple packages, like V(foo, bar).
|
||||
- A package name, like V(foo), or multiple packages, like V(foo,bar).
|
||||
- Do not include additional whitespace when specifying multiple packages as a string.
|
||||
Prefer YAML lists over comma-separating multiple package names.
|
||||
type: list
|
||||
elements: str
|
||||
no_cache:
|
||||
@@ -61,7 +63,7 @@ options:
|
||||
type: str
|
||||
update_cache:
|
||||
description:
|
||||
- Update repository indexes. Can be run with other steps or on it's own.
|
||||
- Update repository indexes. Can be run with other steps or on its own.
|
||||
type: bool
|
||||
default: false
|
||||
upgrade:
|
||||
|
||||
@@ -137,6 +137,10 @@ class Cargo(object):
|
||||
|
||||
def get_installed(self):
|
||||
cmd = ["install", "--list"]
|
||||
if self.path:
|
||||
cmd.append("--root")
|
||||
cmd.append(self.path)
|
||||
|
||||
data, dummy = self._exec(cmd, True, False, False)
|
||||
|
||||
package_regex = re.compile(r"^([\w\-]+) v(.+):$")
|
||||
|
||||
@@ -26,6 +26,10 @@ attributes:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
deprecated:
|
||||
removed_in: 10.0.0
|
||||
why: The legacy ACL system was removed from Consul.
|
||||
alternative: Use M(community.general.consul_token) and/or M(community.general.consul_policy) instead.
|
||||
options:
|
||||
mgmt_token:
|
||||
description:
|
||||
|
||||
108
plugins/modules/consul_acl_bootstrap.py
Normal file
108
plugins/modules/consul_acl_bootstrap.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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: consul_acl_bootstrap
|
||||
short_description: Bootstrap ACLs in Consul
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- Allows bootstrapping of ACLs in a Consul cluster, see
|
||||
U(https://developer.hashicorp.com/consul/api-docs/acl#bootstrap-acls) for details.
|
||||
author:
|
||||
- Florian Apolloner (@apollo13)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
diff_mode:
|
||||
support: none
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether the token should be present or absent.
|
||||
choices: ['present', 'bootstrapped']
|
||||
default: present
|
||||
type: str
|
||||
bootstrap_secret:
|
||||
description:
|
||||
- The secret to be used as secret ID for the initial token.
|
||||
- Needs to be an UUID.
|
||||
type: str
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Bootstrap the ACL system
|
||||
community.general.consul_acl_bootstrap:
|
||||
bootstrap_secret: 22eaeed1-bdbd-4651-724e-42ae6c43e387
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
result:
|
||||
description:
|
||||
- The bootstrap result as returned by the consul HTTP API.
|
||||
- "B(Note:) If O(bootstrap_secret) has been specified the C(SecretID) and
|
||||
C(ID) will not contain the secret but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER).
|
||||
If you pass O(bootstrap_secret), make sure your playbook/role does not depend
|
||||
on this return value!"
|
||||
returned: changed
|
||||
type: dict
|
||||
sample:
|
||||
AccessorID: 834a5881-10a9-a45b-f63c-490e28743557
|
||||
CreateIndex: 25
|
||||
CreateTime: '2024-01-21T20:26:27.114612038+01:00'
|
||||
Description: Bootstrap Token (Global Management)
|
||||
Hash: X2AgaFhnQGRhSSF/h0m6qpX1wj/HJWbyXcxkEM/5GrY=
|
||||
ID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
|
||||
Local: false
|
||||
ModifyIndex: 25
|
||||
Policies:
|
||||
- ID: 00000000-0000-0000-0000-000000000001
|
||||
Name: global-management
|
||||
SecretID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
RequestError,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
"state": dict(type="str", choices=["present", "bootstrapped"], default="present"),
|
||||
"bootstrap_secret": dict(type="str", no_log=True),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
_ARGUMENT_SPEC.pop("token")
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(_ARGUMENT_SPEC)
|
||||
consul_module = _ConsulModule(module)
|
||||
|
||||
data = {}
|
||||
if "bootstrap_secret" in module.params:
|
||||
data["BootstrapSecret"] = module.params["bootstrap_secret"]
|
||||
|
||||
try:
|
||||
response = consul_module.put("acl/bootstrap", data=data)
|
||||
except RequestError as e:
|
||||
if e.status == 403 and b"ACL bootstrap no longer allowed" in e.response_data:
|
||||
return module.exit_json(changed=False)
|
||||
raise
|
||||
else:
|
||||
return module.exit_json(changed=True, result=response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
207
plugins/modules/consul_auth_method.py
Normal file
207
plugins/modules/consul_auth_method.py
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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: consul_auth_method
|
||||
short_description: Manipulate Consul auth methods
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- Allows the addition, modification and deletion of auth methods in a consul
|
||||
cluster via the agent. For more details on using and configuring ACLs,
|
||||
see U(https://www.consul.io/docs/guides/acl.html).
|
||||
author:
|
||||
- Florian Apolloner (@apollo13)
|
||||
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 token should be present or absent.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Specifies a name for the ACL auth method.
|
||||
- The name can contain alphanumeric characters, dashes C(-), and underscores C(_).
|
||||
type: str
|
||||
required: true
|
||||
type:
|
||||
description:
|
||||
- The type of auth method being configured.
|
||||
- This field is immutable.
|
||||
- Required when the auth method is created.
|
||||
type: str
|
||||
choices: ['kubernetes', 'jwt', 'oidc', 'aws-iam']
|
||||
description:
|
||||
description:
|
||||
- Free form human readable description of the auth method.
|
||||
type: str
|
||||
display_name:
|
||||
description:
|
||||
- An optional name to use instead of O(name) when displaying information about this auth method.
|
||||
type: str
|
||||
max_token_ttl:
|
||||
description:
|
||||
- This specifies the maximum life of any token created by this auth method.
|
||||
- Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes, respectively).
|
||||
type: str
|
||||
token_locality:
|
||||
description:
|
||||
- Defines the kind of token that this auth method should produce.
|
||||
type: str
|
||||
choices: ['local', 'global']
|
||||
config:
|
||||
description:
|
||||
- The raw configuration to use for the chosen auth method.
|
||||
- Contents will vary depending upon the type chosen.
|
||||
- Required when the auth method is created.
|
||||
type: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create an auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
type: jwt
|
||||
config:
|
||||
jwt_validation_pubkeys:
|
||||
- |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
|
||||
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
|
||||
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
|
||||
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
|
||||
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
|
||||
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
token: "{{ consul_management_token }}"
|
||||
|
||||
- name: Delete auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
state: absent
|
||||
token: "{{ consul_management_token }}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
auth_method:
|
||||
description: The auth method as returned by the consul HTTP API.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
Config:
|
||||
JWTValidationPubkeys:
|
||||
- |-
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
|
||||
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
|
||||
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
|
||||
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
|
||||
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
|
||||
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
CreateIndex: 416
|
||||
ModifyIndex: 487
|
||||
Name: test
|
||||
Type: jwt
|
||||
operation:
|
||||
description: The operation performed.
|
||||
returned: changed
|
||||
type: str
|
||||
sample: update
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
_ConsulModule,
|
||||
camel_case_key,
|
||||
)
|
||||
|
||||
|
||||
def normalize_ttl(ttl):
|
||||
matches = re.findall(r"(\d+)(:h|m|s)", ttl)
|
||||
ttl = 0
|
||||
for value, unit in matches:
|
||||
value = int(value)
|
||||
if unit == "m":
|
||||
value *= 60
|
||||
elif unit == "h":
|
||||
value *= 60 * 60
|
||||
ttl += value
|
||||
|
||||
new_ttl = ""
|
||||
hours, remainder = divmod(ttl, 3600)
|
||||
if hours:
|
||||
new_ttl += "{0}h".format(hours)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
if minutes:
|
||||
new_ttl += "{0}m".format(minutes)
|
||||
if seconds:
|
||||
new_ttl += "{0}s".format(seconds)
|
||||
return new_ttl
|
||||
|
||||
|
||||
class ConsulAuthMethodModule(_ConsulModule):
|
||||
api_endpoint = "acl/auth-method"
|
||||
result_key = "auth_method"
|
||||
unique_identifier = "name"
|
||||
|
||||
def map_param(self, k, v, is_update):
|
||||
if k == "config" and v:
|
||||
v = {camel_case_key(k2): v2 for k2, v2 in v.items()}
|
||||
return super(ConsulAuthMethodModule, self).map_param(k, v, is_update)
|
||||
|
||||
def needs_update(self, api_obj, module_obj):
|
||||
if "MaxTokenTTL" in module_obj:
|
||||
module_obj["MaxTokenTTL"] = normalize_ttl(module_obj["MaxTokenTTL"])
|
||||
return super(ConsulAuthMethodModule, self).needs_update(api_obj, module_obj)
|
||||
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
"name": dict(type="str", required=True),
|
||||
"type": dict(type="str", choices=["kubernetes", "jwt", "oidc", "aws-iam"]),
|
||||
"description": dict(type="str"),
|
||||
"display_name": dict(type="str"),
|
||||
"max_token_ttl": dict(type="str", no_log=False),
|
||||
"token_locality": dict(type="str", choices=["local", "global"]),
|
||||
"config": dict(type="dict"),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
consul_module = ConsulAuthMethodModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
183
plugins/modules/consul_binding_rule.py
Normal file
183
plugins/modules/consul_binding_rule.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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: consul_binding_rule
|
||||
short_description: Manipulate Consul binding rules
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- Allows the addition, modification and deletion of binding rules in a consul
|
||||
cluster via the agent. For more details on using and configuring binding rules,
|
||||
see U(https://developer.hashicorp.com/consul/api-docs/acl/binding-rules).
|
||||
author:
|
||||
- Florian Apolloner (@apollo13)
|
||||
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 binding rule should be present or absent.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Specifies a name for the binding rule.
|
||||
- 'Note: This is used to identify the binding rule. But since the API does not support a name, it is prefixed to the description.'
|
||||
type: str
|
||||
required: true
|
||||
description:
|
||||
description:
|
||||
- Free form human readable description of the binding rule.
|
||||
type: str
|
||||
auth_method:
|
||||
description:
|
||||
- The name of the auth method that this rule applies to.
|
||||
type: str
|
||||
required: true
|
||||
selector:
|
||||
description:
|
||||
- Specifies the expression used to match this rule against valid identities returned from an auth method validation.
|
||||
- If empty this binding rule matches all valid identities returned from the auth method.
|
||||
type: str
|
||||
bind_type:
|
||||
description:
|
||||
- Specifies the way the binding rule affects a token created at login.
|
||||
type: str
|
||||
choices: [service, node, role, templated-policy]
|
||||
bind_name:
|
||||
description:
|
||||
- The name to bind to a token at login-time.
|
||||
- What it binds to can be adjusted with different values of the O(bind_type) parameter.
|
||||
type: str
|
||||
bind_vars:
|
||||
description:
|
||||
- Specifies the templated policy variables when O(bind_type) is set to V(templated-policy).
|
||||
type: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create a binding rule
|
||||
community.general.consul_binding_rule:
|
||||
name: my_name
|
||||
description: example rule
|
||||
auth_method: minikube
|
||||
bind_type: service
|
||||
bind_name: "{{ serviceaccount.name }}"
|
||||
token: "{{ consul_management_token }}"
|
||||
|
||||
- name: Remove a binding rule
|
||||
community.general.consul_binding_rule:
|
||||
name: my_name
|
||||
auth_method: minikube
|
||||
state: absent
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
binding_rule:
|
||||
description: The binding rule as returned by the consul HTTP API.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
Description: "my_name: example rule"
|
||||
AuthMethod: minikube
|
||||
Selector: serviceaccount.namespace==default
|
||||
BindType: service
|
||||
BindName: "{{ serviceaccount.name }}"
|
||||
CreateIndex: 30
|
||||
ID: 59c8a237-e481-4239-9202-45f117950c5f
|
||||
ModifyIndex: 33
|
||||
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,
|
||||
RequestError,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
|
||||
class ConsulBindingRuleModule(_ConsulModule):
|
||||
api_endpoint = "acl/binding-rule"
|
||||
result_key = "binding_rule"
|
||||
unique_identifier = "id"
|
||||
|
||||
def read_object(self):
|
||||
url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"])
|
||||
try:
|
||||
results = self.get(url)
|
||||
for result in results:
|
||||
if result.get("Description").startswith(
|
||||
"{0}: ".format(self.params["name"])
|
||||
):
|
||||
return result
|
||||
except RequestError as e:
|
||||
if e.status == 404:
|
||||
return
|
||||
elif e.status == 403 and b"ACL not found" in e.response_data:
|
||||
return
|
||||
raise
|
||||
|
||||
def module_to_obj(self, is_update):
|
||||
obj = super(ConsulBindingRuleModule, self).module_to_obj(is_update)
|
||||
del obj["Name"]
|
||||
return obj
|
||||
|
||||
def prepare_object(self, existing, obj):
|
||||
final = super(ConsulBindingRuleModule, self).prepare_object(existing, obj)
|
||||
name = self.params["name"]
|
||||
description = final.pop("Description", "").split(": ", 1)[-1]
|
||||
final["Description"] = "{0}: {1}".format(name, description)
|
||||
return final
|
||||
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
"name": dict(type="str", required=True),
|
||||
"description": dict(type="str"),
|
||||
"auth_method": dict(type="str", required=True),
|
||||
"selector": dict(type="str"),
|
||||
"bind_type": dict(
|
||||
type="str", choices=["service", "node", "role", "templated-policy"]
|
||||
),
|
||||
"bind_name": dict(type="str"),
|
||||
"bind_vars": dict(type="dict"),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
consul_module = ConsulBindingRuleModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,9 +6,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = """
|
||||
module: consul_policy
|
||||
short_description: Manipulate Consul policies
|
||||
version_added: 7.2.0
|
||||
@@ -19,24 +20,29 @@ description:
|
||||
author:
|
||||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.actiongroup_consul
|
||||
- community.general.consul.token
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
support: full
|
||||
version_added: 8.3.0
|
||||
diff_mode:
|
||||
support: none
|
||||
support: partial
|
||||
version_added: 8.3.0
|
||||
details:
|
||||
- In check mode the diff will miss operational attributes.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether the policy should be present or absent.
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
valid_datacenters:
|
||||
description:
|
||||
- Valid datacenters for the policy. All if list is empty.
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
name:
|
||||
@@ -48,45 +54,12 @@ options:
|
||||
description:
|
||||
description:
|
||||
- Description of the policy.
|
||||
required: false
|
||||
type: str
|
||||
default: ''
|
||||
rules:
|
||||
type: str
|
||||
description:
|
||||
- Rule document that should be associated with the current policy.
|
||||
required: false
|
||||
host:
|
||||
description:
|
||||
- Host of the consul agent, defaults to localhost.
|
||||
required: false
|
||||
default: localhost
|
||||
type: str
|
||||
port:
|
||||
type: int
|
||||
description:
|
||||
- The port on which the consul agent is running.
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- The protocol scheme on which the consul agent is running.
|
||||
required: false
|
||||
default: http
|
||||
type: str
|
||||
token:
|
||||
description:
|
||||
- A management token is required to manipulate the policies.
|
||||
type: str
|
||||
validate_certs:
|
||||
type: bool
|
||||
description:
|
||||
- Whether to verify the TLS certificate of the consul agent or not.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- requests
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create a policy with rules
|
||||
@@ -127,246 +100,64 @@ EXAMPLES = """
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
policy:
|
||||
description: The policy as returned by the consul HTTP API.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
CreateIndex: 632
|
||||
Description: Testing
|
||||
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
|
||||
Name: foo-access
|
||||
Rules: |-
|
||||
key "foo" {
|
||||
policy = "read"
|
||||
}
|
||||
key "private/foo" {
|
||||
policy = "deny"
|
||||
}
|
||||
operation:
|
||||
description: The operation performed on the policy.
|
||||
description: The operation performed.
|
||||
returned: changed
|
||||
type: str
|
||||
sample: update
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
from requests.exceptions import ConnectionError
|
||||
import requests
|
||||
has_requests = True
|
||||
except ImportError:
|
||||
has_requests = False
|
||||
|
||||
|
||||
TOKEN_PARAMETER_NAME = "token"
|
||||
HOST_PARAMETER_NAME = "host"
|
||||
SCHEME_PARAMETER_NAME = "scheme"
|
||||
VALIDATE_CERTS_PARAMETER_NAME = "validate_certs"
|
||||
NAME_PARAMETER_NAME = "name"
|
||||
DESCRIPTION_PARAMETER_NAME = "description"
|
||||
PORT_PARAMETER_NAME = "port"
|
||||
RULES_PARAMETER_NAME = "rules"
|
||||
VALID_DATACENTERS_PARAMETER_NAME = "valid_datacenters"
|
||||
STATE_PARAMETER_NAME = "state"
|
||||
|
||||
|
||||
PRESENT_STATE_VALUE = "present"
|
||||
ABSENT_STATE_VALUE = "absent"
|
||||
|
||||
REMOVE_OPERATION = "remove"
|
||||
UPDATE_OPERATION = "update"
|
||||
CREATE_OPERATION = "create"
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
OPERATION_READ,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
NAME_PARAMETER_NAME: dict(required=True),
|
||||
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=''),
|
||||
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
|
||||
RULES_PARAMETER_NAME: dict(type='str'),
|
||||
VALID_DATACENTERS_PARAMETER_NAME: dict(type='list', elements='str', default=[]),
|
||||
HOST_PARAMETER_NAME: dict(default='localhost'),
|
||||
SCHEME_PARAMETER_NAME: dict(default='http'),
|
||||
TOKEN_PARAMETER_NAME: dict(no_log=True),
|
||||
VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True),
|
||||
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
|
||||
"name": dict(required=True),
|
||||
"description": dict(required=False, type="str"),
|
||||
"rules": dict(type="str"),
|
||||
"valid_datacenters": dict(type="list", elements="str"),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def get_consul_url(configuration):
|
||||
return '%s://%s:%s/v1' % (configuration.scheme,
|
||||
configuration.host, configuration.port)
|
||||
class ConsulPolicyModule(_ConsulModule):
|
||||
api_endpoint = "acl/policy"
|
||||
result_key = "policy"
|
||||
unique_identifier = "id"
|
||||
|
||||
|
||||
def get_auth_headers(configuration):
|
||||
if configuration.token is None:
|
||||
return {}
|
||||
else:
|
||||
return {'X-Consul-Token': configuration.token}
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def handle_consul_response_error(response):
|
||||
if 400 <= response.status_code < 600:
|
||||
raise RequestError('%d %s' % (response.status_code, response.content))
|
||||
|
||||
|
||||
def update_policy(policy, configuration):
|
||||
url = '%s/acl/policy/%s' % (get_consul_url(configuration), policy['ID'])
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.put(url, headers=headers, json={
|
||||
'Name': configuration.name, # should be equal at this point.
|
||||
'Description': configuration.description,
|
||||
'Rules': configuration.rules,
|
||||
'Datacenters': configuration.valid_datacenters
|
||||
}, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
updated_policy = response.json()
|
||||
|
||||
changed = (
|
||||
policy.get('Rules', "") != updated_policy.get('Rules', "") or
|
||||
policy.get('Description', "") != updated_policy.get('Description', "") or
|
||||
policy.get('Datacenters', []) != updated_policy.get('Datacenters', [])
|
||||
)
|
||||
|
||||
return Output(changed=changed, operation=UPDATE_OPERATION, policy=updated_policy)
|
||||
|
||||
|
||||
def create_policy(configuration):
|
||||
url = '%s/acl/policy' % get_consul_url(configuration)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.put(url, headers=headers, json={
|
||||
'Name': configuration.name,
|
||||
'Description': configuration.description,
|
||||
'Rules': configuration.rules,
|
||||
'Datacenters': configuration.valid_datacenters
|
||||
}, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
created_policy = response.json()
|
||||
|
||||
return Output(changed=True, operation=CREATE_OPERATION, policy=created_policy)
|
||||
|
||||
|
||||
def remove_policy(configuration):
|
||||
policies = get_policies(configuration)
|
||||
|
||||
if configuration.name in policies:
|
||||
|
||||
policy_id = policies[configuration.name]['ID']
|
||||
policy = get_policy(policy_id, configuration)
|
||||
|
||||
url = '%s/acl/policy/%s' % (get_consul_url(configuration),
|
||||
policy['ID'])
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.delete(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
return Output(changed=changed, operation=REMOVE_OPERATION)
|
||||
|
||||
|
||||
def get_policies(configuration):
|
||||
url = '%s/acl/policies' % get_consul_url(configuration)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
policies = response.json()
|
||||
existing_policies_mapped_by_name = dict(
|
||||
(policy['Name'], policy) for policy in policies if policy['Name'] is not None)
|
||||
return existing_policies_mapped_by_name
|
||||
|
||||
|
||||
def get_policy(id, configuration):
|
||||
url = '%s/acl/policy/%s' % (get_consul_url(configuration), id)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
return response.json()
|
||||
|
||||
|
||||
def set_policy(configuration):
|
||||
policies = get_policies(configuration)
|
||||
|
||||
if configuration.name in policies:
|
||||
index_policy_object = policies[configuration.name]
|
||||
policy_id = policies[configuration.name]['ID']
|
||||
rest_policy_object = get_policy(policy_id, configuration)
|
||||
# merge dicts as some keys are only available in the partial policy
|
||||
policy = index_policy_object.copy()
|
||||
policy.update(rest_policy_object)
|
||||
return update_policy(policy, configuration)
|
||||
else:
|
||||
return create_policy(configuration)
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Configuration for this module.
|
||||
"""
|
||||
|
||||
def __init__(self, token=None, host=None, scheme=None, validate_certs=None, name=None, description=None, port=None,
|
||||
rules=None, valid_datacenters=None, state=None):
|
||||
self.token = token # type: str
|
||||
self.host = host # type: str
|
||||
self.scheme = scheme # type: str
|
||||
self.validate_certs = validate_certs # type: bool
|
||||
self.name = name # type: str
|
||||
self.description = description # type: str
|
||||
self.port = port # type: int
|
||||
self.rules = rules # type: str
|
||||
self.valid_datacenters = valid_datacenters # type: str
|
||||
self.state = state # type: str
|
||||
|
||||
|
||||
class Output:
|
||||
"""
|
||||
Output of an action of this module.
|
||||
"""
|
||||
|
||||
def __init__(self, changed=None, operation=None, policy=None):
|
||||
self.changed = changed # type: bool
|
||||
self.operation = operation # type: str
|
||||
self.policy = policy # type: dict
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""
|
||||
Checks that the required dependencies have been imported.
|
||||
:exception ImportError: if it is detected that any of the required dependencies have not been imported
|
||||
"""
|
||||
|
||||
if not has_requests:
|
||||
raise ImportError(
|
||||
"requests required for this module. See https://pypi.org/project/requests/")
|
||||
def endpoint_url(self, operation, identifier=None):
|
||||
if operation == OPERATION_READ:
|
||||
return [self.api_endpoint, "name", self.params["name"]]
|
||||
return super(ConsulPolicyModule, self).endpoint_url(operation, identifier)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main method.
|
||||
"""
|
||||
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=False)
|
||||
|
||||
try:
|
||||
check_dependencies()
|
||||
except ImportError as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
configuration = Configuration(
|
||||
token=module.params.get(TOKEN_PARAMETER_NAME),
|
||||
host=module.params.get(HOST_PARAMETER_NAME),
|
||||
scheme=module.params.get(SCHEME_PARAMETER_NAME),
|
||||
validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME),
|
||||
name=module.params.get(NAME_PARAMETER_NAME),
|
||||
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
|
||||
port=module.params.get(PORT_PARAMETER_NAME),
|
||||
rules=module.params.get(RULES_PARAMETER_NAME),
|
||||
valid_datacenters=module.params.get(VALID_DATACENTERS_PARAMETER_NAME),
|
||||
state=module.params.get(STATE_PARAMETER_NAME),
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
if configuration.state == PRESENT_STATE_VALUE:
|
||||
output = set_policy(configuration)
|
||||
else:
|
||||
output = remove_policy(configuration)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
configuration.host, configuration.port, str(e)))
|
||||
raise
|
||||
|
||||
return_values = dict(changed=output.changed, operation=output.operation, policy=output.policy)
|
||||
module.exit_json(**return_values)
|
||||
consul_module = ConsulPolicyModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = """
|
||||
module: consul_role
|
||||
short_description: Manipulate Consul roles
|
||||
version_added: 7.5.0
|
||||
@@ -19,12 +20,18 @@ description:
|
||||
author:
|
||||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.token
|
||||
- community.general.consul.actiongroup_consul
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
support: partial
|
||||
details:
|
||||
- In check mode the diff will miss operational attributes.
|
||||
version_added: 8.3.0
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
@@ -34,7 +41,6 @@ options:
|
||||
state:
|
||||
description:
|
||||
- whether the role should be present or absent.
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
@@ -42,7 +48,6 @@ options:
|
||||
description:
|
||||
- Description of the role.
|
||||
- If not specified, the assigned description will not be changed.
|
||||
required: false
|
||||
type: str
|
||||
policies:
|
||||
type: list
|
||||
@@ -51,7 +56,6 @@ options:
|
||||
- List of policies to attach to the role. Each policy is a dict.
|
||||
- If the parameter is left blank, any policies currently assigned will not be changed.
|
||||
- Any empty array (V([])) will clear any policies previously set.
|
||||
required: false
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
@@ -63,6 +67,23 @@ options:
|
||||
- The ID of the policy to attach to this role; see M(community.general.consul_policy) for more info.
|
||||
- Either this or O(policies[].name) must be specified.
|
||||
type: str
|
||||
templated_policies:
|
||||
description:
|
||||
- The list of templated policies that should be applied to the role.
|
||||
type: list
|
||||
elements: dict
|
||||
version_added: 8.3.0
|
||||
suboptions:
|
||||
template_name:
|
||||
description:
|
||||
- The templated policy name.
|
||||
type: str
|
||||
required: true
|
||||
template_variables:
|
||||
description:
|
||||
- The templated policy variables.
|
||||
- Not all templated policies require variables.
|
||||
type: dict
|
||||
service_identities:
|
||||
type: list
|
||||
elements: dict
|
||||
@@ -70,15 +91,18 @@ options:
|
||||
- List of service identities to attach to the role.
|
||||
- If not specified, any service identities currently assigned will not be changed.
|
||||
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
|
||||
required: false
|
||||
suboptions:
|
||||
name:
|
||||
service_name:
|
||||
description:
|
||||
- The name of the node.
|
||||
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
|
||||
- May only contain lowercase alphanumeric characters as well as - and _.
|
||||
- This suboption has been renamed from O(service_identities[].name) to O(service_identities[].service_name)
|
||||
in community.general 8.3.0. The old name can still be used.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- name
|
||||
datacenters:
|
||||
description:
|
||||
- The datacenters the policies will be effective.
|
||||
@@ -87,7 +111,6 @@ options:
|
||||
- including those which do not yet exist but may in the future.
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
node_identities:
|
||||
type: list
|
||||
elements: dict
|
||||
@@ -95,52 +118,25 @@ options:
|
||||
- List of node identities to attach to the role.
|
||||
- If not specified, any node identities currently assigned will not be changed.
|
||||
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
|
||||
required: false
|
||||
suboptions:
|
||||
name:
|
||||
node_name:
|
||||
description:
|
||||
- The name of the node.
|
||||
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
|
||||
- May only contain lowercase alphanumeric characters as well as - and _.
|
||||
- This suboption has been renamed from O(node_identities[].name) to O(node_identities[].node_name)
|
||||
in community.general 8.3.0. The old name can still be used.
|
||||
type: str
|
||||
required: true
|
||||
aliases:
|
||||
- name
|
||||
datacenter:
|
||||
description:
|
||||
- The nodes datacenter.
|
||||
- This will result in effective policy only being valid in this datacenter.
|
||||
type: str
|
||||
required: true
|
||||
host:
|
||||
description:
|
||||
- Host of the consul agent, defaults to V(localhost).
|
||||
required: false
|
||||
default: localhost
|
||||
type: str
|
||||
port:
|
||||
type: int
|
||||
description:
|
||||
- The port on which the consul agent is running.
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- The protocol scheme on which the consul agent is running.
|
||||
required: false
|
||||
default: http
|
||||
type: str
|
||||
token:
|
||||
description:
|
||||
- A management token is required to manipulate the roles.
|
||||
type: str
|
||||
validate_certs:
|
||||
type: bool
|
||||
description:
|
||||
- Whether to verify the TLS certificate of the consul agent.
|
||||
required: false
|
||||
default: true
|
||||
requirements:
|
||||
- requests
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create a role with 2 policies
|
||||
@@ -204,440 +200,81 @@ operation:
|
||||
"""
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
get_consul_url, get_auth_headers, handle_consul_response_error)
|
||||
import traceback
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
|
||||
try:
|
||||
from requests.exceptions import ConnectionError
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
REQUESTS_IMP_ERR = traceback.format_exc()
|
||||
|
||||
TOKEN_PARAMETER_NAME = "token"
|
||||
HOST_PARAMETER_NAME = "host"
|
||||
SCHEME_PARAMETER_NAME = "scheme"
|
||||
VALIDATE_CERTS_PARAMETER_NAME = "validate_certs"
|
||||
NAME_PARAMETER_NAME = "name"
|
||||
DESCRIPTION_PARAMETER_NAME = "description"
|
||||
PORT_PARAMETER_NAME = "port"
|
||||
POLICIES_PARAMETER_NAME = "policies"
|
||||
SERVICE_IDENTITIES_PARAMETER_NAME = "service_identities"
|
||||
NODE_IDENTITIES_PARAMETER_NAME = "node_identities"
|
||||
STATE_PARAMETER_NAME = "state"
|
||||
|
||||
PRESENT_STATE_VALUE = "present"
|
||||
ABSENT_STATE_VALUE = "absent"
|
||||
|
||||
REMOVE_OPERATION = "remove"
|
||||
UPDATE_OPERATION = "update"
|
||||
CREATE_OPERATION = "create"
|
||||
|
||||
POLICY_RULE_SPEC = dict(
|
||||
name=dict(type='str'),
|
||||
id=dict(type='str'),
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
OPERATION_READ,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
NODE_ID_RULE_SPEC = dict(
|
||||
name=dict(type='str', required=True),
|
||||
datacenter=dict(type='str', required=True),
|
||||
|
||||
class ConsulRoleModule(_ConsulModule):
|
||||
api_endpoint = "acl/role"
|
||||
result_key = "role"
|
||||
unique_identifier = "id"
|
||||
|
||||
def endpoint_url(self, operation, identifier=None):
|
||||
if operation == OPERATION_READ:
|
||||
return [self.api_endpoint, "name", self.params["name"]]
|
||||
return super(ConsulRoleModule, self).endpoint_url(operation, identifier)
|
||||
|
||||
|
||||
NAME_ID_SPEC = dict(
|
||||
name=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
)
|
||||
|
||||
SERVICE_ID_RULE_SPEC = dict(
|
||||
name=dict(type='str', required=True),
|
||||
datacenters=dict(type='list', elements='str', required=True),
|
||||
NODE_ID_SPEC = dict(
|
||||
node_name=dict(type="str", required=True, aliases=["name"]),
|
||||
datacenter=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
SERVICE_ID_SPEC = dict(
|
||||
service_name=dict(type="str", required=True, aliases=["name"]),
|
||||
datacenters=dict(type="list", elements="str"),
|
||||
)
|
||||
|
||||
TEMPLATE_POLICY_SPEC = dict(
|
||||
template_name=dict(type="str", required=True),
|
||||
template_variables=dict(type="dict"),
|
||||
)
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
TOKEN_PARAMETER_NAME: dict(no_log=True),
|
||||
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
|
||||
HOST_PARAMETER_NAME: dict(default='localhost'),
|
||||
SCHEME_PARAMETER_NAME: dict(default='http'),
|
||||
VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True),
|
||||
NAME_PARAMETER_NAME: dict(required=True),
|
||||
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=None),
|
||||
POLICIES_PARAMETER_NAME: dict(type='list', elements='dict', options=POLICY_RULE_SPEC,
|
||||
mutually_exclusive=[('name', 'id')], required_one_of=[('name', 'id')], default=None),
|
||||
SERVICE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=SERVICE_ID_RULE_SPEC, default=None),
|
||||
NODE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=NODE_ID_RULE_SPEC, default=None),
|
||||
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
|
||||
"name": dict(type="str", required=True),
|
||||
"description": dict(type="str"),
|
||||
"policies": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=NAME_ID_SPEC,
|
||||
mutually_exclusive=[("name", "id")],
|
||||
required_one_of=[("name", "id")],
|
||||
),
|
||||
"templated_policies": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=TEMPLATE_POLICY_SPEC,
|
||||
),
|
||||
"node_identities": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=NODE_ID_SPEC,
|
||||
),
|
||||
"service_identities": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=SERVICE_ID_SPEC,
|
||||
),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
|
||||
|
||||
def compare_consul_api_role_policy_objects(first, second):
|
||||
# compare two lists of dictionaries, ignoring the ID element
|
||||
for x in first:
|
||||
x.pop('ID', None)
|
||||
|
||||
for x in second:
|
||||
x.pop('ID', None)
|
||||
|
||||
return first == second
|
||||
|
||||
|
||||
def update_role(role, configuration):
|
||||
url = '%s/acl/role/%s' % (get_consul_url(configuration),
|
||||
role['ID'])
|
||||
headers = get_auth_headers(configuration)
|
||||
|
||||
update_role_data = {
|
||||
'Name': configuration.name,
|
||||
'Description': configuration.description,
|
||||
}
|
||||
|
||||
# check if the user omitted the description, policies, service identities, or node identities
|
||||
|
||||
description_specified = configuration.description is not None
|
||||
|
||||
policy_specified = True
|
||||
if len(configuration.policies) == 1 and configuration.policies[0] is None:
|
||||
policy_specified = False
|
||||
|
||||
service_id_specified = True
|
||||
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
|
||||
service_id_specified = False
|
||||
|
||||
node_id_specified = True
|
||||
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
|
||||
node_id_specified = False
|
||||
|
||||
if description_specified:
|
||||
update_role_data["Description"] = configuration.description
|
||||
|
||||
if policy_specified:
|
||||
update_role_data["Policies"] = [x.to_dict() for x in configuration.policies]
|
||||
|
||||
if configuration.version >= ConsulVersion("1.5.0") and service_id_specified:
|
||||
update_role_data["ServiceIdentities"] = [
|
||||
x.to_dict() for x in configuration.service_identities]
|
||||
|
||||
if configuration.version >= ConsulVersion("1.8.0") and node_id_specified:
|
||||
update_role_data["NodeIdentities"] = [
|
||||
x.to_dict() for x in configuration.node_identities]
|
||||
|
||||
if configuration.check_mode:
|
||||
description_changed = False
|
||||
if description_specified:
|
||||
description_changed = role.get('Description') != update_role_data["Description"]
|
||||
else:
|
||||
update_role_data["Description"] = role.get("Description")
|
||||
|
||||
policies_changed = False
|
||||
if policy_specified:
|
||||
policies_changed = not (
|
||||
compare_consul_api_role_policy_objects(role.get('Policies', []), update_role_data.get('Policies', [])))
|
||||
else:
|
||||
if role.get('Policies') is not None:
|
||||
update_role_data["Policies"] = role.get('Policies')
|
||||
|
||||
service_ids_changed = False
|
||||
if service_id_specified:
|
||||
service_ids_changed = role.get('ServiceIdentities') != update_role_data.get('ServiceIdentities')
|
||||
else:
|
||||
if role.get('ServiceIdentities') is not None:
|
||||
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
|
||||
|
||||
node_ids_changed = False
|
||||
if node_id_specified:
|
||||
node_ids_changed = role.get('NodeIdentities') != update_role_data.get('NodeIdentities')
|
||||
else:
|
||||
if role.get('NodeIdentities'):
|
||||
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
|
||||
|
||||
changed = (
|
||||
description_changed or
|
||||
policies_changed or
|
||||
service_ids_changed or
|
||||
node_ids_changed
|
||||
)
|
||||
return Output(changed=changed, operation=UPDATE_OPERATION, role=update_role_data)
|
||||
else:
|
||||
# if description, policies, service or node id are not specified; we need to get the existing value and apply it
|
||||
if not description_specified and role.get('Description') is not None:
|
||||
update_role_data["Description"] = role.get('Description')
|
||||
|
||||
if not policy_specified and role.get('Policies') is not None:
|
||||
update_role_data["Policies"] = role.get('Policies')
|
||||
|
||||
if not service_id_specified and role.get('ServiceIdentities') is not None:
|
||||
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
|
||||
|
||||
if not node_id_specified and role.get('NodeIdentities') is not None:
|
||||
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
|
||||
|
||||
response = requests.put(url, headers=headers, json=update_role_data, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
resulting_role = response.json()
|
||||
changed = (
|
||||
role['Description'] != resulting_role['Description'] or
|
||||
role.get('Policies', None) != resulting_role.get('Policies', None) or
|
||||
role.get('ServiceIdentities', None) != resulting_role.get('ServiceIdentities', None) or
|
||||
role.get('NodeIdentities', None) != resulting_role.get('NodeIdentities', None)
|
||||
)
|
||||
|
||||
return Output(changed=changed, operation=UPDATE_OPERATION, role=resulting_role)
|
||||
|
||||
|
||||
def create_role(configuration):
|
||||
url = '%s/acl/role' % get_consul_url(configuration)
|
||||
headers = get_auth_headers(configuration)
|
||||
|
||||
# check if the user omitted policies, service identities, or node identities
|
||||
policy_specified = True
|
||||
if len(configuration.policies) == 1 and configuration.policies[0] is None:
|
||||
policy_specified = False
|
||||
|
||||
service_id_specified = True
|
||||
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
|
||||
service_id_specified = False
|
||||
|
||||
node_id_specified = True
|
||||
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
|
||||
node_id_specified = False
|
||||
|
||||
# get rid of None item so we can set an empty list for policies, service identities and node identities
|
||||
if not policy_specified:
|
||||
configuration.policies.pop()
|
||||
|
||||
if not service_id_specified:
|
||||
configuration.service_identities.pop()
|
||||
|
||||
if not node_id_specified:
|
||||
configuration.node_identities.pop()
|
||||
|
||||
create_role_data = {
|
||||
'Name': configuration.name,
|
||||
'Description': configuration.description,
|
||||
'Policies': [x.to_dict() for x in configuration.policies],
|
||||
}
|
||||
if configuration.version >= ConsulVersion("1.5.0"):
|
||||
create_role_data["ServiceIdentities"] = [x.to_dict() for x in configuration.service_identities]
|
||||
|
||||
if configuration.version >= ConsulVersion("1.8.0"):
|
||||
create_role_data["NodeIdentities"] = [x.to_dict() for x in configuration.node_identities]
|
||||
|
||||
if not configuration.check_mode:
|
||||
response = requests.put(url, headers=headers, json=create_role_data, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
resulting_role = response.json()
|
||||
|
||||
return Output(changed=True, operation=CREATE_OPERATION, role=resulting_role)
|
||||
else:
|
||||
return Output(changed=True, operation=CREATE_OPERATION)
|
||||
|
||||
|
||||
def remove_role(configuration):
|
||||
roles = get_roles(configuration)
|
||||
|
||||
if configuration.name in roles:
|
||||
|
||||
role_id = roles[configuration.name]['ID']
|
||||
|
||||
if not configuration.check_mode:
|
||||
url = '%s/acl/role/%s' % (get_consul_url(configuration), role_id)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.delete(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
return Output(changed=changed, operation=REMOVE_OPERATION)
|
||||
|
||||
|
||||
def get_roles(configuration):
|
||||
url = '%s/acl/roles' % get_consul_url(configuration)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
roles = response.json()
|
||||
existing_roles_mapped_by_id = dict((role['Name'], role) for role in roles if role['Name'] is not None)
|
||||
return existing_roles_mapped_by_id
|
||||
|
||||
|
||||
def get_consul_version(configuration):
|
||||
url = '%s/agent/self' % get_consul_url(configuration)
|
||||
headers = get_auth_headers(configuration)
|
||||
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
|
||||
handle_consul_response_error(response)
|
||||
config = response.json()["Config"]
|
||||
return ConsulVersion(config["Version"])
|
||||
|
||||
|
||||
def set_role(configuration):
|
||||
roles = get_roles(configuration)
|
||||
|
||||
if configuration.name in roles:
|
||||
role = roles[configuration.name]
|
||||
return update_role(role, configuration)
|
||||
else:
|
||||
return create_role(configuration)
|
||||
|
||||
|
||||
class ConsulVersion:
|
||||
def __init__(self, version_string):
|
||||
split = version_string.split('.')
|
||||
self.major = split[0]
|
||||
self.minor = split[1]
|
||||
self.patch = split[2]
|
||||
|
||||
def __ge__(self, other):
|
||||
return int(self.major + self.minor +
|
||||
self.patch) >= int(other.major + other.minor + other.patch)
|
||||
|
||||
def __le__(self, other):
|
||||
return int(self.major + self.minor +
|
||||
self.patch) <= int(other.major + other.minor + other.patch)
|
||||
|
||||
|
||||
class ServiceIdentity:
|
||||
def __init__(self, input):
|
||||
if not isinstance(input, dict) or 'name' not in input:
|
||||
raise ValueError(
|
||||
"Each element of service_identities must be a dict with the keys name and optionally datacenters")
|
||||
self.name = input["name"]
|
||||
self.datacenters = input["datacenters"] if "datacenters" in input else None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ServiceName": self.name,
|
||||
"Datacenters": self.datacenters
|
||||
}
|
||||
|
||||
|
||||
class NodeIdentity:
|
||||
def __init__(self, input):
|
||||
if not isinstance(input, dict) or 'name' not in input:
|
||||
raise ValueError(
|
||||
"Each element of node_identities must be a dict with the keys name and optionally datacenter")
|
||||
self.name = input["name"]
|
||||
self.datacenter = input["datacenter"] if "datacenter" in input else None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"NodeName": self.name,
|
||||
"Datacenter": self.datacenter
|
||||
}
|
||||
|
||||
|
||||
class RoleLink:
|
||||
def __init__(self, dict):
|
||||
self.id = dict.get("id", None)
|
||||
self.name = dict.get("name", None)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ID": self.id,
|
||||
"Name": self.name
|
||||
}
|
||||
|
||||
|
||||
class PolicyLink:
|
||||
def __init__(self, dict):
|
||||
self.id = dict.get("id", None)
|
||||
self.name = dict.get("name", None)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"ID": self.id,
|
||||
"Name": self.name
|
||||
}
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Configuration for this module.
|
||||
"""
|
||||
|
||||
def __init__(self, token=None, host=None, scheme=None, validate_certs=None, name=None, description=None, port=None,
|
||||
policies=None, service_identities=None, node_identities=None, state=None, check_mode=None):
|
||||
self.token = token # type: str
|
||||
self.host = host # type: str
|
||||
self.port = port # type: int
|
||||
self.scheme = scheme # type: str
|
||||
self.validate_certs = validate_certs # type: bool
|
||||
self.name = name # type: str
|
||||
self.description = description # type: str
|
||||
if policies is not None:
|
||||
self.policies = [PolicyLink(p) for p in policies] # type: list(PolicyLink)
|
||||
else:
|
||||
self.policies = [None]
|
||||
if service_identities is not None:
|
||||
self.service_identities = [ServiceIdentity(s) for s in service_identities] # type: list(ServiceIdentity)
|
||||
else:
|
||||
self.service_identities = [None]
|
||||
if node_identities is not None:
|
||||
self.node_identities = [NodeIdentity(n) for n in node_identities] # type: list(NodeIdentity)
|
||||
else:
|
||||
self.node_identities = [None]
|
||||
self.state = state # type: str
|
||||
self.check_mode = check_mode # type: bool
|
||||
|
||||
|
||||
class Output:
|
||||
"""
|
||||
Output of an action of this module.
|
||||
"""
|
||||
|
||||
def __init__(self, changed=None, operation=None, role=None):
|
||||
self.changed = changed # type: bool
|
||||
self.operation = operation # type: str
|
||||
self.role = role # type: dict
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main method.
|
||||
"""
|
||||
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=True)
|
||||
|
||||
if not HAS_REQUESTS:
|
||||
module.fail_json(msg=missing_required_lib("requests"),
|
||||
exception=REQUESTS_IMP_ERR)
|
||||
|
||||
try:
|
||||
configuration = Configuration(
|
||||
token=module.params.get(TOKEN_PARAMETER_NAME),
|
||||
host=module.params.get(HOST_PARAMETER_NAME),
|
||||
port=module.params.get(PORT_PARAMETER_NAME),
|
||||
scheme=module.params.get(SCHEME_PARAMETER_NAME),
|
||||
validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME),
|
||||
name=module.params.get(NAME_PARAMETER_NAME),
|
||||
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
|
||||
policies=module.params.get(POLICIES_PARAMETER_NAME),
|
||||
service_identities=module.params.get(SERVICE_IDENTITIES_PARAMETER_NAME),
|
||||
node_identities=module.params.get(NODE_IDENTITIES_PARAMETER_NAME),
|
||||
state=module.params.get(STATE_PARAMETER_NAME),
|
||||
check_mode=module.check_mode
|
||||
|
||||
)
|
||||
except ValueError as err:
|
||||
module.fail_json(msg='Configuration error: %s' % str(err))
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
version = get_consul_version(configuration)
|
||||
configuration.version = version
|
||||
|
||||
if configuration.state == PRESENT_STATE_VALUE:
|
||||
output = set_role(configuration)
|
||||
else:
|
||||
output = remove_role(configuration)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
configuration.host, configuration.port, str(e)))
|
||||
raise
|
||||
|
||||
return_values = dict(changed=output.changed, operation=output.operation, role=output.role)
|
||||
module.exit_json(**return_values)
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
consul_module = ConsulRoleModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -16,12 +16,13 @@ description:
|
||||
cluster. These sessions can then be used in conjunction with key value pairs
|
||||
to implement distributed locks. In depth documentation for working with
|
||||
sessions can be found at http://www.consul.io/docs/internals/sessions.html
|
||||
requirements:
|
||||
- requests
|
||||
author:
|
||||
- Steve Gargan (@sgargan)
|
||||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.actiongroup_consul
|
||||
- community.general.consul.token
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
@@ -76,26 +77,6 @@ options:
|
||||
the associated lock delay has expired.
|
||||
type: list
|
||||
elements: str
|
||||
host:
|
||||
description:
|
||||
- The host of the consul agent defaults to localhost.
|
||||
type: str
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- The port on which the consul agent is running.
|
||||
type: int
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- The protocol scheme on which the consul agent is running.
|
||||
type: str
|
||||
default: http
|
||||
validate_certs:
|
||||
description:
|
||||
- Whether to verify the TLS certificate of the consul agent.
|
||||
type: bool
|
||||
default: true
|
||||
behavior:
|
||||
description:
|
||||
- The optional behavior that can be attached to the session when it
|
||||
@@ -109,10 +90,6 @@ options:
|
||||
type: int
|
||||
version_added: 5.4.0
|
||||
token:
|
||||
description:
|
||||
- The token key identifying an ACL rule set that controls access to
|
||||
the key value pair.
|
||||
type: str
|
||||
version_added: 5.6.0
|
||||
'''
|
||||
|
||||
@@ -148,95 +125,49 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
try:
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
has_requests = True
|
||||
except ImportError:
|
||||
has_requests = False
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
AUTH_ARGUMENTS_SPEC, _ConsulModule
|
||||
)
|
||||
|
||||
|
||||
def execute(module):
|
||||
def execute(module, consul_module):
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state in ['info', 'list', 'node']:
|
||||
lookup_sessions(module)
|
||||
lookup_sessions(module, consul_module)
|
||||
elif state == 'present':
|
||||
update_session(module)
|
||||
update_session(module, consul_module)
|
||||
else:
|
||||
remove_session(module)
|
||||
remove_session(module, consul_module)
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
pass
|
||||
def list_sessions(consul_module, datacenter):
|
||||
return consul_module.get(
|
||||
'session/list',
|
||||
params={'dc': datacenter})
|
||||
|
||||
|
||||
def handle_consul_response_error(response):
|
||||
if 400 <= response.status_code < 600:
|
||||
raise RequestError('%d %s' % (response.status_code, response.content))
|
||||
def list_sessions_for_node(consul_module, node, datacenter):
|
||||
return consul_module.get(
|
||||
('session', 'node', node),
|
||||
params={'dc': datacenter})
|
||||
|
||||
|
||||
def get_consul_url(module):
|
||||
return '%s://%s:%s/v1' % (module.params.get('scheme'),
|
||||
module.params.get('host'), module.params.get('port'))
|
||||
def get_session_info(consul_module, session_id, datacenter):
|
||||
return consul_module.get(
|
||||
('session', 'info', session_id),
|
||||
params={'dc': datacenter})
|
||||
|
||||
|
||||
def get_auth_headers(module):
|
||||
if 'token' in module.params and module.params.get('token') is not None:
|
||||
return {'X-Consul-Token': module.params.get('token')}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def list_sessions(module, datacenter):
|
||||
url = '%s/session/list' % get_consul_url(module)
|
||||
headers = get_auth_headers(module)
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
params={
|
||||
'dc': datacenter},
|
||||
verify=module.params.get('validate_certs'))
|
||||
handle_consul_response_error(response)
|
||||
return response.json()
|
||||
|
||||
|
||||
def list_sessions_for_node(module, node, datacenter):
|
||||
url = '%s/session/node/%s' % (get_consul_url(module), node)
|
||||
headers = get_auth_headers(module)
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
params={
|
||||
'dc': datacenter},
|
||||
verify=module.params.get('validate_certs'))
|
||||
handle_consul_response_error(response)
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_session_info(module, session_id, datacenter):
|
||||
url = '%s/session/info/%s' % (get_consul_url(module), session_id)
|
||||
headers = get_auth_headers(module)
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
params={
|
||||
'dc': datacenter},
|
||||
verify=module.params.get('validate_certs'))
|
||||
handle_consul_response_error(response)
|
||||
return response.json()
|
||||
|
||||
|
||||
def lookup_sessions(module):
|
||||
def lookup_sessions(module, consul_module):
|
||||
|
||||
datacenter = module.params.get('datacenter')
|
||||
|
||||
state = module.params.get('state')
|
||||
try:
|
||||
if state == 'list':
|
||||
sessions_list = list_sessions(module, datacenter)
|
||||
sessions_list = list_sessions(consul_module, datacenter)
|
||||
# Ditch the index, this can be grabbed from the results
|
||||
if sessions_list and len(sessions_list) >= 2:
|
||||
sessions_list = sessions_list[1]
|
||||
@@ -244,14 +175,14 @@ def lookup_sessions(module):
|
||||
sessions=sessions_list)
|
||||
elif state == 'node':
|
||||
node = module.params.get('node')
|
||||
sessions = list_sessions_for_node(module, node, datacenter)
|
||||
sessions = list_sessions_for_node(consul_module, node, datacenter)
|
||||
module.exit_json(changed=True,
|
||||
node=node,
|
||||
sessions=sessions)
|
||||
elif state == 'info':
|
||||
session_id = module.params.get('id')
|
||||
|
||||
session_by_id = get_session_info(module, session_id, datacenter)
|
||||
session_by_id = get_session_info(consul_module, session_id, datacenter)
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id,
|
||||
sessions=session_by_id)
|
||||
@@ -260,10 +191,8 @@ def lookup_sessions(module):
|
||||
module.fail_json(msg="Could not retrieve session info %s" % e)
|
||||
|
||||
|
||||
def create_session(module, name, behavior, ttl, node,
|
||||
def create_session(consul_module, name, behavior, ttl, node,
|
||||
lock_delay, datacenter, checks):
|
||||
url = '%s/session/create' % get_consul_url(module)
|
||||
headers = get_auth_headers(module)
|
||||
create_data = {
|
||||
"LockDelay": lock_delay,
|
||||
"Node": node,
|
||||
@@ -273,19 +202,15 @@ def create_session(module, name, behavior, ttl, node,
|
||||
}
|
||||
if ttl is not None:
|
||||
create_data["TTL"] = "%ss" % str(ttl) # TTL is in seconds
|
||||
response = requests.put(
|
||||
url,
|
||||
headers=headers,
|
||||
create_session_response_dict = consul_module.put(
|
||||
'session/create',
|
||||
params={
|
||||
'dc': datacenter},
|
||||
json=create_data,
|
||||
verify=module.params.get('validate_certs'))
|
||||
handle_consul_response_error(response)
|
||||
create_session_response_dict = response.json()
|
||||
data=create_data)
|
||||
return create_session_response_dict["ID"]
|
||||
|
||||
|
||||
def update_session(module):
|
||||
def update_session(module, consul_module):
|
||||
|
||||
name = module.params.get('name')
|
||||
delay = module.params.get('delay')
|
||||
@@ -296,7 +221,7 @@ def update_session(module):
|
||||
ttl = module.params.get('ttl')
|
||||
|
||||
try:
|
||||
session = create_session(module,
|
||||
session = create_session(consul_module,
|
||||
name=name,
|
||||
behavior=behavior,
|
||||
ttl=ttl,
|
||||
@@ -317,22 +242,15 @@ def update_session(module):
|
||||
module.fail_json(msg="Could not create/update session %s" % e)
|
||||
|
||||
|
||||
def destroy_session(module, session_id):
|
||||
url = '%s/session/destroy/%s' % (get_consul_url(module), session_id)
|
||||
headers = get_auth_headers(module)
|
||||
response = requests.put(
|
||||
url,
|
||||
headers=headers,
|
||||
verify=module.params.get('validate_certs'))
|
||||
handle_consul_response_error(response)
|
||||
return response.content == "true"
|
||||
def destroy_session(consul_module, session_id):
|
||||
return consul_module.put(('session', 'destroy', session_id))
|
||||
|
||||
|
||||
def remove_session(module):
|
||||
def remove_session(module, consul_module):
|
||||
session_id = module.params.get('id')
|
||||
|
||||
try:
|
||||
destroy_session(module, session_id)
|
||||
destroy_session(consul_module, session_id)
|
||||
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id)
|
||||
@@ -341,12 +259,6 @@ def remove_session(module):
|
||||
session_id, e))
|
||||
|
||||
|
||||
def test_dependencies(module):
|
||||
if not has_requests:
|
||||
raise ImportError(
|
||||
"requests required for this module. See https://pypi.org/project/requests/")
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
checks=dict(type='list', elements='str'),
|
||||
@@ -358,10 +270,6 @@ def main():
|
||||
'release',
|
||||
'delete']),
|
||||
ttl=dict(type='int'),
|
||||
host=dict(type='str', default='localhost'),
|
||||
port=dict(type='int', default=8500),
|
||||
scheme=dict(type='str', default='http'),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
id=dict(type='str'),
|
||||
name=dict(type='str'),
|
||||
node=dict(type='str'),
|
||||
@@ -375,7 +283,7 @@ def main():
|
||||
'node',
|
||||
'present']),
|
||||
datacenter=dict(type='str'),
|
||||
token=dict(type='str', no_log=True),
|
||||
**AUTH_ARGUMENTS_SPEC
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
@@ -387,14 +295,10 @@ def main():
|
||||
],
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
test_dependencies(module)
|
||||
consul_module = _ConsulModule(module)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
module.params.get('host'), module.params.get('port'), e))
|
||||
execute(module, consul_module)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
325
plugins/modules/consul_token.py
Normal file
325
plugins/modules/consul_token.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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: consul_token
|
||||
short_description: Manipulate Consul tokens
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- Allows the addition, modification and deletion of tokens in a consul
|
||||
cluster via the agent. For more details on using and configuring ACLs,
|
||||
see U(https://www.consul.io/docs/guides/acl.html).
|
||||
author:
|
||||
- Florian Apolloner (@apollo13)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.token
|
||||
- community.general.consul.actiongroup_consul
|
||||
- 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 token should be present or absent.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
accessor_id:
|
||||
description:
|
||||
- Specifies a UUID to use as the token's Accessor ID.
|
||||
If not specified a UUID will be generated for this field.
|
||||
type: str
|
||||
secret_id:
|
||||
description:
|
||||
- Specifies a UUID to use as the token's Secret ID.
|
||||
If not specified a UUID will be generated for this field.
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Free form human readable description of the token.
|
||||
type: str
|
||||
policies:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- List of policies to attach to the token. Each policy is a dict.
|
||||
- If the parameter is left blank, any policies currently assigned will not be changed.
|
||||
- Any empty array (V([])) will clear any policies previously set.
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the policy to attach to this token; see M(community.general.consul_policy) for more info.
|
||||
- Either this or O(policies[].id) must be specified.
|
||||
type: str
|
||||
id:
|
||||
description:
|
||||
- The ID of the policy to attach to this token; see M(community.general.consul_policy) for more info.
|
||||
- Either this or O(policies[].name) must be specified.
|
||||
type: str
|
||||
roles:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- List of roles to attach to the token. Each role is a dict.
|
||||
- If the parameter is left blank, any roles currently assigned will not be changed.
|
||||
- Any empty array (V([])) will clear any roles previously set.
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the role to attach to this token; see M(community.general.consul_role) for more info.
|
||||
- Either this or O(roles[].id) must be specified.
|
||||
type: str
|
||||
id:
|
||||
description:
|
||||
- The ID of the role to attach to this token; see M(community.general.consul_role) for more info.
|
||||
- Either this or O(roles[].name) must be specified.
|
||||
type: str
|
||||
templated_policies:
|
||||
description:
|
||||
- The list of templated policies that should be applied to the role.
|
||||
type: list
|
||||
elements: dict
|
||||
suboptions:
|
||||
template_name:
|
||||
description:
|
||||
- The templated policy name.
|
||||
type: str
|
||||
required: true
|
||||
template_variables:
|
||||
description:
|
||||
- The templated policy variables.
|
||||
- Not all templated policies require variables.
|
||||
type: dict
|
||||
service_identities:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- List of service identities to attach to the token.
|
||||
- If not specified, any service identities currently assigned will not be changed.
|
||||
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
|
||||
suboptions:
|
||||
service_name:
|
||||
description:
|
||||
- The name of the service.
|
||||
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
|
||||
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
|
||||
type: str
|
||||
required: true
|
||||
datacenters:
|
||||
description:
|
||||
- The datacenters the token will be effective.
|
||||
- If an empty array (V([])) is specified, the token will valid in all datacenters.
|
||||
- including those which do not yet exist but may in the future.
|
||||
type: list
|
||||
elements: str
|
||||
node_identities:
|
||||
type: list
|
||||
elements: dict
|
||||
description:
|
||||
- List of node identities to attach to the token.
|
||||
- If not specified, any node identities currently assigned will not be changed.
|
||||
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
|
||||
suboptions:
|
||||
node_name:
|
||||
description:
|
||||
- The name of the node.
|
||||
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
|
||||
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
|
||||
type: str
|
||||
required: true
|
||||
datacenter:
|
||||
description:
|
||||
- The nodes datacenter.
|
||||
- This will result in effective token only being valid in this datacenter.
|
||||
type: str
|
||||
required: true
|
||||
local:
|
||||
description:
|
||||
- If true, indicates that the token should not be replicated globally
|
||||
and instead be local to the current datacenter.
|
||||
type: bool
|
||||
expiration_ttl:
|
||||
description:
|
||||
- This is a convenience field and if set will initialize the C(expiration_time).
|
||||
Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes,
|
||||
respectively). Ingored when the token is updated!
|
||||
type: str
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create / Update a token by accessor_id
|
||||
community.general.consul_token:
|
||||
state: present
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
|
||||
roles:
|
||||
- name: role1
|
||||
- name: role2
|
||||
service_identities:
|
||||
- service_name: service1
|
||||
datacenters: [dc1, dc2]
|
||||
node_identities:
|
||||
- node_name: node1
|
||||
datacenter: dc1
|
||||
expiration_ttl: 50m
|
||||
|
||||
- name: Delete a token
|
||||
community.general.consul_token:
|
||||
state: absent
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
token:
|
||||
description: The token as returned by the consul HTTP API.
|
||||
returned: always
|
||||
type: dict
|
||||
sample:
|
||||
AccessorID: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
CreateIndex: 632
|
||||
CreateTime: "2024-01-14T21:53:01.402749174+01:00"
|
||||
Description: Testing
|
||||
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
|
||||
Local: false
|
||||
ModifyIndex: 633
|
||||
SecretID: bd380fba-da17-7cee-8576-8d6427c6c930
|
||||
ServiceIdentities: [{"ServiceName": "test"}]
|
||||
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,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
|
||||
def normalize_link_obj(api_obj, module_obj, key):
|
||||
api_objs = api_obj.get(key)
|
||||
module_objs = module_obj.get(key)
|
||||
if api_objs is None or module_objs is None:
|
||||
return
|
||||
name_to_id = {i["Name"]: i["ID"] for i in api_objs}
|
||||
id_to_name = {i["ID"]: i["Name"] for i in api_objs}
|
||||
|
||||
for obj in module_objs:
|
||||
identifier = obj.get("ID")
|
||||
name = obj.get("Name)")
|
||||
if identifier and not name and identifier in id_to_name:
|
||||
obj["Name"] = id_to_name[identifier]
|
||||
if not identifier and name and name in name_to_id:
|
||||
obj["ID"] = name_to_id[name]
|
||||
|
||||
|
||||
class ConsulTokenModule(_ConsulModule):
|
||||
api_endpoint = "acl/token"
|
||||
result_key = "token"
|
||||
unique_identifier = "accessor_id"
|
||||
|
||||
create_only_fields = {"expiration_ttl"}
|
||||
|
||||
def needs_update(self, api_obj, module_obj):
|
||||
# SecretID is usually not supplied
|
||||
if "SecretID" not in module_obj and "SecretID" in api_obj:
|
||||
del api_obj["SecretID"]
|
||||
normalize_link_obj(api_obj, module_obj, "Roles")
|
||||
normalize_link_obj(api_obj, module_obj, "Policies")
|
||||
# ExpirationTTL is only supported on create, not for update
|
||||
# it writes to ExpirationTime, so we need to remove that as well
|
||||
if "ExpirationTTL" in module_obj:
|
||||
del module_obj["ExpirationTTL"]
|
||||
return super(ConsulTokenModule, self).needs_update(api_obj, module_obj)
|
||||
|
||||
|
||||
NAME_ID_SPEC = dict(
|
||||
name=dict(type="str"),
|
||||
id=dict(type="str"),
|
||||
)
|
||||
|
||||
NODE_ID_SPEC = dict(
|
||||
node_name=dict(type="str", required=True),
|
||||
datacenter=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
SERVICE_ID_SPEC = dict(
|
||||
service_name=dict(type="str", required=True),
|
||||
datacenters=dict(type="list", elements="str"),
|
||||
)
|
||||
|
||||
TEMPLATE_POLICY_SPEC = dict(
|
||||
template_name=dict(type="str", required=True),
|
||||
template_variables=dict(type="dict"),
|
||||
)
|
||||
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
"description": dict(),
|
||||
"accessor_id": dict(),
|
||||
"secret_id": dict(no_log=True),
|
||||
"roles": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=NAME_ID_SPEC,
|
||||
mutually_exclusive=[("name", "id")],
|
||||
required_one_of=[("name", "id")],
|
||||
),
|
||||
"policies": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=NAME_ID_SPEC,
|
||||
mutually_exclusive=[("name", "id")],
|
||||
required_one_of=[("name", "id")],
|
||||
),
|
||||
"templated_policies": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=TEMPLATE_POLICY_SPEC,
|
||||
),
|
||||
"node_identities": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=NODE_ID_SPEC,
|
||||
),
|
||||
"service_identities": dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=SERVICE_ID_SPEC,
|
||||
),
|
||||
"local": dict(type="bool"),
|
||||
"expiration_ttl": dict(type="str"),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
required_if=[("state", "absent", ["accessor_id"])],
|
||||
supports_check_mode=True,
|
||||
)
|
||||
consul_module = ConsulTokenModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -120,7 +120,7 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, find_project, gitlab_authentication, gitlab
|
||||
auth_argument_spec, find_project, gitlab_authentication, gitlab, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -208,8 +208,7 @@ class GitLabDeployKey(object):
|
||||
@param key_title Title of the key
|
||||
'''
|
||||
def find_deploy_key(self, project, key_title):
|
||||
deploy_keys = project.keys.list(all=True)
|
||||
for deploy_key in deploy_keys:
|
||||
for deploy_key in project.keys.list(**list_all_kwargs):
|
||||
if (deploy_key.title == key_title):
|
||||
return deploy_key
|
||||
|
||||
|
||||
320
plugins/modules/gitlab_group_access_token.py
Normal file
320
plugins/modules/gitlab_group_access_token.py
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024, Zoran Krleza (zoran.krleza@true-north.hr)
|
||||
# Based on code:
|
||||
# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr)
|
||||
# Copyright (c) 2018, Marcus Watkins <marwatk@marcuswatkins.net>
|
||||
# Copyright (c) 2013, Phillip Gentry <phillip@cx.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 = r'''
|
||||
module: gitlab_group_access_token
|
||||
short_description: Manages GitLab group access tokens
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Creates and revokes group access tokens.
|
||||
author:
|
||||
- Zoran Krleza (@pixslx)
|
||||
requirements:
|
||||
- python-gitlab >= 3.1.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.auth_basic
|
||||
- community.general.gitlab
|
||||
- community.general.attributes
|
||||
notes:
|
||||
- Access tokens can not be changed. If a parameter needs to be changed, an acceess token has to be recreated.
|
||||
Whether tokens will be recreated is controlled by the O(recreate) option, which defaults to V(never).
|
||||
- Token string is contained in the result only when access token is created or recreated. It can not be fetched afterwards.
|
||||
- Token matching is done by comparing O(name) option.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
|
||||
options:
|
||||
group:
|
||||
description:
|
||||
- ID or full path of group in the form of group/subgroup.
|
||||
required: true
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Access token's name.
|
||||
required: true
|
||||
type: str
|
||||
scopes:
|
||||
description:
|
||||
- Scope of the access token.
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
aliases: ["scope"]
|
||||
choices: ["api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository", "create_runner", "ai_features", "k8s_proxy"]
|
||||
access_level:
|
||||
description:
|
||||
- Access level of the access token.
|
||||
type: str
|
||||
default: maintainer
|
||||
choices: ["guest", "reporter", "developer", "maintainer", "owner"]
|
||||
expires_at:
|
||||
description:
|
||||
- Expiration date of the access token in C(YYYY-MM-DD) format.
|
||||
- Make sure to quote this value in YAML to ensure it is kept as a string and not interpreted as a YAML date.
|
||||
type: str
|
||||
required: true
|
||||
recreate:
|
||||
description:
|
||||
- Whether the access token will be recreated if it already exists.
|
||||
- When V(never) the token will never be recreated.
|
||||
- When V(always) the token will always be recreated.
|
||||
- When V(state_change) the token will be recreated if there is a difference between desired state and actual state.
|
||||
type: str
|
||||
choices: ["never", "always", "state_change"]
|
||||
default: never
|
||||
state:
|
||||
description:
|
||||
- When V(present) the access token will be added to the group if it does not exist.
|
||||
- When V(absent) it will be removed from the group if it exists.
|
||||
default: present
|
||||
type: str
|
||||
choices: [ "present", "absent" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: "Creating a group access token"
|
||||
community.general.gitlab_group_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
group: "my_group/my_subgroup"
|
||||
name: "group_token"
|
||||
expires_at: "2024-12-31"
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
state: present
|
||||
|
||||
- name: "Revoking a group access token"
|
||||
community.general.gitlab_group_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
group: "my_group/my_group"
|
||||
name: "group_token"
|
||||
expires_at: "2024-12-31"
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
state: absent
|
||||
|
||||
- name: "Change (recreate) existing token if its actual state is different than desired state"
|
||||
community.general.gitlab_group_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
group: "my_group/my_group"
|
||||
name: "group_token"
|
||||
expires_at: "2024-12-31"
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
recreate: state_change
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
access_token:
|
||||
description:
|
||||
- API object.
|
||||
- Only contains the value of the token if the token was created or recreated.
|
||||
returned: success and O(state=present)
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, find_group, gitlab_authentication, gitlab
|
||||
)
|
||||
|
||||
ACCESS_LEVELS = dict(guest=10, reporter=20, developer=30, maintainer=40, owner=50)
|
||||
|
||||
|
||||
class GitLabGroupAccessToken(object):
|
||||
def __init__(self, module, gitlab_instance):
|
||||
self._module = module
|
||||
self._gitlab = gitlab_instance
|
||||
self.access_token_object = None
|
||||
|
||||
'''
|
||||
@param project Project Object
|
||||
@param group Group Object
|
||||
@param arguments Attributes of the access_token
|
||||
'''
|
||||
def create_access_token(self, group, arguments):
|
||||
changed = False
|
||||
if self._module.check_mode:
|
||||
return True
|
||||
|
||||
try:
|
||||
self.access_token_object = group.access_tokens.create(arguments)
|
||||
changed = True
|
||||
except (gitlab.exceptions.GitlabCreateError) as e:
|
||||
self._module.fail_json(msg="Failed to create access token: %s " % to_native(e))
|
||||
|
||||
return changed
|
||||
|
||||
'''
|
||||
@param project Project object
|
||||
@param group Group Object
|
||||
@param name of the access token
|
||||
'''
|
||||
def find_access_token(self, group, name):
|
||||
access_tokens = group.access_tokens.list(all=True)
|
||||
for access_token in access_tokens:
|
||||
if (access_token.name == name):
|
||||
self.access_token_object = access_token
|
||||
return False
|
||||
return False
|
||||
|
||||
def revoke_access_token(self):
|
||||
if self._module.check_mode:
|
||||
return True
|
||||
|
||||
changed = False
|
||||
try:
|
||||
self.access_token_object.delete()
|
||||
changed = True
|
||||
except (gitlab.exceptions.GitlabCreateError) as e:
|
||||
self._module.fail_json(msg="Failed to revoke access token: %s " % to_native(e))
|
||||
|
||||
return changed
|
||||
|
||||
def access_tokens_equal(self):
|
||||
if self.access_token_object.name != self._module.params['name']:
|
||||
return False
|
||||
if self.access_token_object.scopes != self._module.params['scopes']:
|
||||
return False
|
||||
if self.access_token_object.access_level != ACCESS_LEVELS[self._module.params['access_level']]:
|
||||
return False
|
||||
if self.access_token_object.expires_at != self._module.params['expires_at']:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = basic_auth_argument_spec()
|
||||
argument_spec.update(auth_argument_spec())
|
||||
argument_spec.update(dict(
|
||||
state=dict(type='str', default="present", choices=["absent", "present"]),
|
||||
group=dict(type='str', required=True),
|
||||
name=dict(type='str', required=True),
|
||||
scopes=dict(type='list',
|
||||
required=True,
|
||||
aliases=['scope'],
|
||||
elements='str',
|
||||
choices=['api',
|
||||
'read_api',
|
||||
'read_registry',
|
||||
'write_registry',
|
||||
'read_repository',
|
||||
'write_repository',
|
||||
'create_runner',
|
||||
'ai_features',
|
||||
'k8s_proxy']),
|
||||
access_level=dict(type='str', required=False, default='maintainer', choices=['guest', 'reporter', 'developer', 'maintainer', 'owner']),
|
||||
expires_at=dict(type='str', required=True),
|
||||
recreate=dict(type='str', default='never', choices=['never', 'always', 'state_change'])
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
['api_username', 'api_token'],
|
||||
['api_username', 'api_oauth_token'],
|
||||
['api_username', 'api_job_token'],
|
||||
['api_token', 'api_oauth_token'],
|
||||
['api_token', 'api_job_token']
|
||||
],
|
||||
required_together=[
|
||||
['api_username', 'api_password']
|
||||
],
|
||||
required_one_of=[
|
||||
['api_username', 'api_token', 'api_oauth_token', 'api_job_token']
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
group_identifier = module.params['group']
|
||||
name = module.params['name']
|
||||
scopes = module.params['scopes']
|
||||
access_level_str = module.params['access_level']
|
||||
expires_at = module.params['expires_at']
|
||||
recreate = module.params['recreate']
|
||||
|
||||
access_level = ACCESS_LEVELS[access_level_str]
|
||||
|
||||
try:
|
||||
datetime.strptime(expires_at, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
module.fail_json(msg="Argument expires_at is not in required format YYYY-MM-DD")
|
||||
|
||||
gitlab_instance = gitlab_authentication(module)
|
||||
|
||||
gitlab_access_token = GitLabGroupAccessToken(module, gitlab_instance)
|
||||
|
||||
group = find_group(gitlab_instance, group_identifier)
|
||||
if group is None:
|
||||
module.fail_json(msg="Failed to create access token: group %s does not exists" % group_identifier)
|
||||
|
||||
gitlab_access_token_exists = False
|
||||
gitlab_access_token.find_access_token(group, name)
|
||||
if gitlab_access_token.access_token_object is not None:
|
||||
gitlab_access_token_exists = True
|
||||
|
||||
if state == 'absent':
|
||||
if gitlab_access_token_exists:
|
||||
gitlab_access_token.revoke_access_token()
|
||||
module.exit_json(changed=True, msg="Successfully deleted access token %s" % name)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="Access token does not exists")
|
||||
|
||||
if state == 'present':
|
||||
if gitlab_access_token_exists:
|
||||
if gitlab_access_token.access_tokens_equal():
|
||||
if recreate == 'always':
|
||||
gitlab_access_token.revoke_access_token()
|
||||
gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="Access token already exists", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
if recreate == 'never':
|
||||
module.fail_json(msg="Access token already exists and its state is different. It can not be updated without recreating.")
|
||||
else:
|
||||
gitlab_access_token.revoke_access_token()
|
||||
gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
gitlab_access_token.create_access_token(group, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully created access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -160,7 +160,7 @@ from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, gitlab
|
||||
auth_argument_spec, gitlab_authentication, gitlab, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -171,16 +171,20 @@ class GitLabGroup(object):
|
||||
|
||||
# get user id if the user exists
|
||||
def get_user_id(self, gitlab_user):
|
||||
user_exists = self._gitlab.users.list(username=gitlab_user, all=True)
|
||||
if user_exists:
|
||||
return user_exists[0].id
|
||||
return next(
|
||||
(u.id for u in self._gitlab.users.list(username=gitlab_user, **list_all_kwargs)),
|
||||
None
|
||||
)
|
||||
|
||||
# get group id if group exists
|
||||
def get_group_id(self, gitlab_group):
|
||||
groups = self._gitlab.groups.list(search=gitlab_group, all=True)
|
||||
for group in groups:
|
||||
if group.full_path == gitlab_group:
|
||||
return group.id
|
||||
return next(
|
||||
(
|
||||
g.id for g in self._gitlab.groups.list(search=gitlab_group, **list_all_kwargs)
|
||||
if g.full_path == gitlab_group
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
# get all members in a group
|
||||
def get_members_in_a_group(self, gitlab_group_id):
|
||||
|
||||
@@ -206,7 +206,8 @@ group_variable:
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables,
|
||||
list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -221,14 +222,7 @@ class GitlabGroupVariables(object):
|
||||
return self.repo.groups.get(group_name)
|
||||
|
||||
def list_all_group_variables(self):
|
||||
page_nb = 1
|
||||
variables = []
|
||||
vars_page = self.group.variables.list(page=page_nb)
|
||||
while len(vars_page) > 0:
|
||||
variables += vars_page
|
||||
page_nb += 1
|
||||
vars_page = self.group.variables.list(page=page_nb)
|
||||
return variables
|
||||
return list(self.group.variables.list(**list_all_kwargs))
|
||||
|
||||
def create_variable(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
|
||||
@@ -97,6 +97,11 @@ options:
|
||||
- Trigger hook on wiki events.
|
||||
type: bool
|
||||
default: false
|
||||
releases_events:
|
||||
description:
|
||||
- Trigger hook on release events.
|
||||
type: bool
|
||||
version_added: '8.4.0'
|
||||
hook_validate_certs:
|
||||
description:
|
||||
- Whether GitLab will do SSL verification when triggering the hook.
|
||||
@@ -169,7 +174,7 @@ from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, find_project, gitlab_authentication
|
||||
auth_argument_spec, find_project, gitlab_authentication, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -201,6 +206,7 @@ class GitLabHook(object):
|
||||
'job_events': options['job_events'],
|
||||
'pipeline_events': options['pipeline_events'],
|
||||
'wiki_page_events': options['wiki_page_events'],
|
||||
'releases_events': options['releases_events'],
|
||||
'enable_ssl_verification': options['enable_ssl_verification'],
|
||||
'token': options['token'],
|
||||
})
|
||||
@@ -216,6 +222,7 @@ class GitLabHook(object):
|
||||
'job_events': options['job_events'],
|
||||
'pipeline_events': options['pipeline_events'],
|
||||
'wiki_page_events': options['wiki_page_events'],
|
||||
'releases_events': options['releases_events'],
|
||||
'enable_ssl_verification': options['enable_ssl_verification'],
|
||||
'token': options['token'],
|
||||
})
|
||||
@@ -264,8 +271,7 @@ class GitLabHook(object):
|
||||
@param hook_url Url to call on event
|
||||
'''
|
||||
def find_hook(self, project, hook_url):
|
||||
hooks = project.hooks.list(all=True)
|
||||
for hook in hooks:
|
||||
for hook in project.hooks.list(**list_all_kwargs):
|
||||
if (hook.url == hook_url):
|
||||
return hook
|
||||
|
||||
@@ -302,6 +308,7 @@ def main():
|
||||
job_events=dict(type='bool', default=False),
|
||||
pipeline_events=dict(type='bool', default=False),
|
||||
wiki_page_events=dict(type='bool', default=False),
|
||||
releases_events=dict(type='bool', default=None),
|
||||
hook_validate_certs=dict(type='bool', default=False, aliases=['enable_ssl_verification']),
|
||||
token=dict(type='str', no_log=True),
|
||||
))
|
||||
@@ -339,6 +346,7 @@ def main():
|
||||
job_events = module.params['job_events']
|
||||
pipeline_events = module.params['pipeline_events']
|
||||
wiki_page_events = module.params['wiki_page_events']
|
||||
releases_events = module.params['releases_events']
|
||||
enable_ssl_verification = module.params['hook_validate_certs']
|
||||
hook_token = module.params['token']
|
||||
|
||||
@@ -369,6 +377,7 @@ def main():
|
||||
"job_events": job_events,
|
||||
"pipeline_events": pipeline_events,
|
||||
"wiki_page_events": wiki_page_events,
|
||||
"releases_events": releases_events,
|
||||
"enable_ssl_verification": enable_ssl_verification,
|
||||
"token": hook_token,
|
||||
}):
|
||||
|
||||
@@ -138,7 +138,8 @@ instance_variable:
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables,
|
||||
list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -149,14 +150,7 @@ class GitlabInstanceVariables(object):
|
||||
self._module = module
|
||||
|
||||
def list_all_instance_variables(self):
|
||||
page_nb = 1
|
||||
variables = []
|
||||
gl_varibales_page = self.instance.variables.list(page=page_nb)
|
||||
while len(gl_varibales_page) > 0:
|
||||
variables += gl_varibales_page
|
||||
page_nb += 1
|
||||
gl_varibales_page = self.instance.variables.list(page=page_nb)
|
||||
return variables
|
||||
return list(self.instance.variables.list(**list_all_kwargs))
|
||||
|
||||
def create_variable(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
|
||||
@@ -183,7 +183,7 @@ class GitlabIssue(object):
|
||||
def get_issue(self, title, state_filter):
|
||||
issues = []
|
||||
try:
|
||||
issues = self.project.issues.list(title=title, state=state_filter)
|
||||
issues = self.project.issues.list(query_parameters={"search": title, "in": "title", "state": state_filter})
|
||||
except gitlab.exceptions.GitlabGetError as e:
|
||||
self._module.fail_json(msg="Failed to list the Issues: %s" % to_native(e))
|
||||
|
||||
|
||||
500
plugins/modules/gitlab_label.py
Normal file
500
plugins/modules/gitlab_label.py
Normal file
@@ -0,0 +1,500 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@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: gitlab_label
|
||||
short_description: Creates/updates/deletes GitLab Labels belonging to project or group.
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- When a label does not exist, it will be created.
|
||||
- When a label does exist, its value will be updated when the values are different.
|
||||
- Labels can be purged.
|
||||
author:
|
||||
- "Gabriele Pongelli (@gpongelli)"
|
||||
requirements:
|
||||
- python-gitlab python module
|
||||
extends_documentation_fragment:
|
||||
- community.general.auth_basic
|
||||
- community.general.gitlab
|
||||
- community.general.attributes
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Create or delete project or group label.
|
||||
default: present
|
||||
type: str
|
||||
choices: ["present", "absent"]
|
||||
purge:
|
||||
description:
|
||||
- When set to V(true), delete all labels which are not mentioned in the task.
|
||||
default: false
|
||||
type: bool
|
||||
required: false
|
||||
project:
|
||||
description:
|
||||
- The path and name of the project. Either this or O(group) is required.
|
||||
required: false
|
||||
type: str
|
||||
group:
|
||||
description:
|
||||
- The path of the group. Either this or O(project) is required.
|
||||
required: false
|
||||
type: str
|
||||
labels:
|
||||
description:
|
||||
- A list of dictionaries that represents gitlab project's or group's labels.
|
||||
type: list
|
||||
elements: dict
|
||||
required: false
|
||||
default: []
|
||||
suboptions:
|
||||
name:
|
||||
description:
|
||||
- The name of the label.
|
||||
type: str
|
||||
required: true
|
||||
color:
|
||||
description:
|
||||
- The color of the label.
|
||||
- Required when O(state=present).
|
||||
type: str
|
||||
priority:
|
||||
description:
|
||||
- Integer value to give priority to the label.
|
||||
type: int
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
description:
|
||||
- Label's description.
|
||||
type: str
|
||||
default: null
|
||||
new_name:
|
||||
description:
|
||||
- Optional field to change label's name.
|
||||
type: str
|
||||
default: null
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# same project's task can be executed for group
|
||||
- name: Create one Label
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
color: "#123456"
|
||||
state: present
|
||||
|
||||
- name: Create many group labels
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
group: "group1"
|
||||
labels:
|
||||
- name: label_one
|
||||
color: "#123456"
|
||||
description: this is a label
|
||||
priority: 20
|
||||
- name: label_two
|
||||
color: "#554422"
|
||||
state: present
|
||||
|
||||
- name: Create many project labels
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
color: "#123456"
|
||||
description: this is a label
|
||||
priority: 20
|
||||
- name: label_two
|
||||
color: "#554422"
|
||||
state: present
|
||||
|
||||
- name: Set or update some labels
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
color: "#224488"
|
||||
state: present
|
||||
|
||||
- name: Add label in check mode
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
color: "#224488"
|
||||
check_mode: true
|
||||
|
||||
- name: Delete Label
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
state: absent
|
||||
|
||||
- name: Change Label name
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
labels:
|
||||
- name: label_one
|
||||
new_name: label_two
|
||||
state: absent
|
||||
|
||||
- name: Purge all labels
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
purge: true
|
||||
|
||||
- name: Delete many labels
|
||||
community.general.gitlab_label:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
state: absent
|
||||
labels:
|
||||
- name: label-abc123
|
||||
- name: label-two
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
labels:
|
||||
description: Four lists of the labels which were added, updated, removed or exist.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
added:
|
||||
description: A list of labels which were created.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['abcd', 'label-one']
|
||||
untouched:
|
||||
description: A list of labels which exist.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-label']
|
||||
removed:
|
||||
description: A list of labels which were deleted.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-label']
|
||||
updated:
|
||||
description: A list pre-existing labels whose values have been set.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-label']
|
||||
labels_obj:
|
||||
description: API object.
|
||||
returned: success
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
|
||||
)
|
||||
|
||||
|
||||
class GitlabLabels(object):
|
||||
|
||||
def __init__(self, module, gitlab_instance, group_id, project_id):
|
||||
self._gitlab = gitlab_instance
|
||||
self.gitlab_object = group_id if group_id else project_id
|
||||
self.is_group_label = True if group_id else False
|
||||
self._module = module
|
||||
|
||||
def list_all_labels(self):
|
||||
page_nb = 1
|
||||
labels = []
|
||||
vars_page = self.gitlab_object.labels.list(page=page_nb)
|
||||
while len(vars_page) > 0:
|
||||
labels += vars_page
|
||||
page_nb += 1
|
||||
vars_page = self.gitlab_object.labels.list(page=page_nb)
|
||||
return labels
|
||||
|
||||
def create_label(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
|
||||
var = {
|
||||
"name": var_obj.get('name'),
|
||||
"color": var_obj.get('color'),
|
||||
}
|
||||
|
||||
if var_obj.get('description') is not None:
|
||||
var["description"] = var_obj.get('description')
|
||||
|
||||
if var_obj.get('priority') is not None:
|
||||
var["priority"] = var_obj.get('priority')
|
||||
|
||||
_obj = self.gitlab_object.labels.create(var)
|
||||
return True, _obj.asdict()
|
||||
|
||||
def update_label(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
_label = self.gitlab_object.labels.get(var_obj.get('name'))
|
||||
|
||||
if var_obj.get('new_name') is not None:
|
||||
_label.new_name = var_obj.get('new_name')
|
||||
|
||||
if var_obj.get('description') is not None:
|
||||
_label.description = var_obj.get('description')
|
||||
if var_obj.get('priority') is not None:
|
||||
_label.priority = var_obj.get('priority')
|
||||
|
||||
# save returns None
|
||||
_label.save()
|
||||
return True, _label.asdict()
|
||||
|
||||
def delete_label(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
_label = self.gitlab_object.labels.get(var_obj.get('name'))
|
||||
# delete returns None
|
||||
_label.delete()
|
||||
return True, _label.asdict()
|
||||
|
||||
|
||||
def compare(requested_labels, existing_labels, state):
|
||||
# we need to do this, because it was determined in a previous version - more or less buggy
|
||||
# basically it is not necessary and might result in more/other bugs!
|
||||
# but it is required and only relevant for check mode!!
|
||||
# logic represents state 'present' when not purge. all other can be derived from that
|
||||
# untouched => equal in both
|
||||
# updated => name and scope are equal
|
||||
# added => name and scope does not exist
|
||||
untouched = list()
|
||||
updated = list()
|
||||
added = list()
|
||||
|
||||
if state == 'present':
|
||||
_existing_labels = list()
|
||||
for item in existing_labels:
|
||||
_existing_labels.append({'name': item.get('name')})
|
||||
|
||||
for var in requested_labels:
|
||||
if var in existing_labels:
|
||||
untouched.append(var)
|
||||
else:
|
||||
compare_item = {'name': var.get('name')}
|
||||
if compare_item in _existing_labels:
|
||||
updated.append(var)
|
||||
else:
|
||||
added.append(var)
|
||||
|
||||
return untouched, updated, added
|
||||
|
||||
|
||||
def native_python_main(this_gitlab, purge, requested_labels, state, module):
|
||||
change = False
|
||||
return_value = dict(added=[], updated=[], removed=[], untouched=[])
|
||||
return_obj = dict(added=[], updated=[], removed=[])
|
||||
|
||||
labels_before = [x.asdict() for x in this_gitlab.list_all_labels()]
|
||||
|
||||
# filter out and enrich before compare
|
||||
for item in requested_labels:
|
||||
# add defaults when not present
|
||||
if item.get('description') is None:
|
||||
item['description'] = ""
|
||||
if item.get('new_name') is None:
|
||||
item['new_name'] = None
|
||||
if item.get('priority') is None:
|
||||
item['priority'] = None
|
||||
|
||||
# group label does not have priority, removing for comparison
|
||||
if this_gitlab.is_group_label:
|
||||
item.pop('priority')
|
||||
|
||||
for item in labels_before:
|
||||
# remove field only from server
|
||||
item.pop('id')
|
||||
item.pop('description_html')
|
||||
item.pop('text_color')
|
||||
item.pop('subscribed')
|
||||
# field present only when it's a project's label
|
||||
if 'is_project_label' in item:
|
||||
item.pop('is_project_label')
|
||||
item['new_name'] = None
|
||||
|
||||
if state == 'present':
|
||||
add_or_update = [x for x in requested_labels if x not in labels_before]
|
||||
for item in add_or_update:
|
||||
try:
|
||||
_rv, _obj = this_gitlab.create_label(item)
|
||||
if _rv:
|
||||
return_value['added'].append(item)
|
||||
return_obj['added'].append(_obj)
|
||||
except Exception:
|
||||
# create raises exception with following error message when label already exists
|
||||
_rv, _obj = this_gitlab.update_label(item)
|
||||
if _rv:
|
||||
return_value['updated'].append(item)
|
||||
return_obj['updated'].append(_obj)
|
||||
|
||||
if purge:
|
||||
# re-fetch
|
||||
_labels = this_gitlab.list_all_labels()
|
||||
|
||||
for item in labels_before:
|
||||
_rv, _obj = this_gitlab.delete_label(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
|
||||
elif state == 'absent':
|
||||
if not purge:
|
||||
_label_names_requested = [x['name'] for x in requested_labels]
|
||||
remove_requested = [x for x in labels_before if x['name'] in _label_names_requested]
|
||||
for item in remove_requested:
|
||||
_rv, _obj = this_gitlab.delete_label(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
else:
|
||||
for item in labels_before:
|
||||
_rv, _obj = this_gitlab.delete_label(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
|
||||
if module.check_mode:
|
||||
_untouched, _updated, _added = compare(requested_labels, labels_before, state)
|
||||
return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched)
|
||||
|
||||
if any(return_value[x] for x in ['added', 'removed', 'updated']):
|
||||
change = True
|
||||
|
||||
labels_after = [x.asdict() for x in this_gitlab.list_all_labels()]
|
||||
|
||||
return change, return_value, labels_before, labels_after, return_obj
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = basic_auth_argument_spec()
|
||||
argument_spec.update(auth_argument_spec())
|
||||
argument_spec.update(
|
||||
project=dict(type='str', required=False, default=None),
|
||||
group=dict(type='str', required=False, default=None),
|
||||
purge=dict(type='bool', required=False, default=False),
|
||||
labels=dict(type='list', elements='dict', required=False, default=list(),
|
||||
options=dict(
|
||||
name=dict(type='str', required=True),
|
||||
color=dict(type='str', required=False),
|
||||
description=dict(type='str', required=False),
|
||||
priority=dict(type='int', required=False),
|
||||
new_name=dict(type='str', required=False),)
|
||||
),
|
||||
state=dict(type='str', default="present", choices=["absent", "present"]),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
['api_username', 'api_token'],
|
||||
['api_username', 'api_oauth_token'],
|
||||
['api_username', 'api_job_token'],
|
||||
['api_token', 'api_oauth_token'],
|
||||
['api_token', 'api_job_token'],
|
||||
['project', 'group'],
|
||||
],
|
||||
required_together=[
|
||||
['api_username', 'api_password'],
|
||||
],
|
||||
required_one_of=[
|
||||
['api_username', 'api_token', 'api_oauth_token', 'api_job_token'],
|
||||
['project', 'group']
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
ensure_gitlab_package(module)
|
||||
|
||||
gitlab_project = module.params['project']
|
||||
gitlab_group = module.params['group']
|
||||
purge = module.params['purge']
|
||||
label_list = module.params['labels']
|
||||
state = module.params['state']
|
||||
|
||||
gitlab_version = gitlab.__version__
|
||||
_min_gitlab = '3.2.0'
|
||||
if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
|
||||
module.fail_json(msg="community.general.gitlab_label requires python-gitlab Python module >= %s "
|
||||
"(installed version: [%s]). Please upgrade "
|
||||
"python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
|
||||
|
||||
gitlab_instance = gitlab_authentication(module)
|
||||
|
||||
# find_project can return None, but the other must exist
|
||||
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
|
||||
|
||||
# find_group can return None, but the other must exist
|
||||
gitlab_group_id = find_group(gitlab_instance, gitlab_group)
|
||||
|
||||
# if both not found, module must exist
|
||||
if not gitlab_project_id and not gitlab_group_id:
|
||||
if gitlab_project and not gitlab_project_id:
|
||||
module.fail_json(msg="project '%s' not found." % gitlab_project)
|
||||
if gitlab_group and not gitlab_group_id:
|
||||
module.fail_json(msg="group '%s' not found." % gitlab_group)
|
||||
|
||||
this_gitlab = GitlabLabels(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id,
|
||||
project_id=gitlab_project_id)
|
||||
|
||||
if state == 'present':
|
||||
_existing_labels = [x.asdict()['name'] for x in this_gitlab.list_all_labels()]
|
||||
|
||||
# color is mandatory when creating label, but it's optional when changing name or updating other fields
|
||||
if any(x['color'] is None and x['new_name'] is None and x['name'] not in _existing_labels for x in label_list):
|
||||
module.fail_json(msg='color parameter is required for new labels')
|
||||
|
||||
change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, label_list, state, module)
|
||||
|
||||
if not module.check_mode:
|
||||
raw_return_value['untouched'] = [x for x in before if x in after]
|
||||
|
||||
added = [x.get('name') for x in raw_return_value['added']]
|
||||
updated = [x.get('name') for x in raw_return_value['updated']]
|
||||
removed = [x.get('name') for x in raw_return_value['removed']]
|
||||
untouched = [x.get('name') for x in raw_return_value['untouched']]
|
||||
return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched)
|
||||
|
||||
module.exit_json(changed=change, labels=return_value, labels_obj=_obj)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
496
plugins/modules/gitlab_milestone.py
Normal file
496
plugins/modules/gitlab_milestone.py
Normal file
@@ -0,0 +1,496 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@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: gitlab_milestone
|
||||
short_description: Creates/updates/deletes GitLab Milestones belonging to project or group
|
||||
version_added: 8.3.0
|
||||
description:
|
||||
- When a milestone does not exist, it will be created.
|
||||
- When a milestone does exist, its value will be updated when the values are different.
|
||||
- Milestones can be purged.
|
||||
author:
|
||||
- "Gabriele Pongelli (@gpongelli)"
|
||||
requirements:
|
||||
- python-gitlab python module
|
||||
extends_documentation_fragment:
|
||||
- community.general.auth_basic
|
||||
- community.general.gitlab
|
||||
- community.general.attributes
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Create or delete milestone.
|
||||
default: present
|
||||
type: str
|
||||
choices: ["present", "absent"]
|
||||
purge:
|
||||
description:
|
||||
- When set to V(true), delete all milestone which are not mentioned in the task.
|
||||
default: false
|
||||
type: bool
|
||||
required: false
|
||||
project:
|
||||
description:
|
||||
- The path and name of the project. Either this or O(group) is required.
|
||||
required: false
|
||||
type: str
|
||||
group:
|
||||
description:
|
||||
- The path of the group. Either this or O(project) is required.
|
||||
required: false
|
||||
type: str
|
||||
milestones:
|
||||
description:
|
||||
- A list of dictionaries that represents gitlab project's or group's milestones.
|
||||
type: list
|
||||
elements: dict
|
||||
required: false
|
||||
default: []
|
||||
suboptions:
|
||||
title:
|
||||
description:
|
||||
- The name of the milestone.
|
||||
type: str
|
||||
required: true
|
||||
due_date:
|
||||
description:
|
||||
- Milestone due date in YYYY-MM-DD format.
|
||||
type: str
|
||||
required: false
|
||||
default: null
|
||||
start_date:
|
||||
description:
|
||||
- Milestone start date in YYYY-MM-DD format.
|
||||
type: str
|
||||
required: false
|
||||
default: null
|
||||
description:
|
||||
description:
|
||||
- Milestone's description.
|
||||
type: str
|
||||
default: null
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# same project's task can be executed for group
|
||||
- name: Create one milestone
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
start_date: "2024-01-04"
|
||||
state: present
|
||||
|
||||
- name: Create many group milestones
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
group: "group1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
start_date: "2024-01-04"
|
||||
description: this is a milestone
|
||||
due_date: "2024-02-04"
|
||||
- title: milestone_two
|
||||
state: present
|
||||
|
||||
- name: Create many project milestones
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
start_date: "2024-01-04"
|
||||
description: this is a milestone
|
||||
due_date: "2024-02-04"
|
||||
- title: milestone_two
|
||||
state: present
|
||||
|
||||
- name: Set or update some milestones
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
start_date: "2024-05-04"
|
||||
state: present
|
||||
|
||||
- name: Add milestone in check mode
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
start_date: "2024-05-04"
|
||||
check_mode: true
|
||||
|
||||
- name: Delete milestone
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
milestones:
|
||||
- title: milestone_one
|
||||
state: absent
|
||||
|
||||
- name: Purge all milestones
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
purge: true
|
||||
|
||||
- name: Delete many milestones
|
||||
community.general.gitlab_milestone:
|
||||
api_url: https://gitlab.com
|
||||
api_token: secret_access_token
|
||||
project: "group1/project1"
|
||||
state: absent
|
||||
milestones:
|
||||
- title: milestone-abc123
|
||||
- title: milestone-two
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
milestones:
|
||||
description: Four lists of the milestones which were added, updated, removed or exist.
|
||||
returned: success
|
||||
type: dict
|
||||
contains:
|
||||
added:
|
||||
description: A list of milestones which were created.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['abcd', 'milestone-one']
|
||||
untouched:
|
||||
description: A list of milestones which exist.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-milestone']
|
||||
removed:
|
||||
description: A list of milestones which were deleted.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-milestone']
|
||||
updated:
|
||||
description: A list pre-existing milestones whose values have been set.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ['defg', 'new-milestone']
|
||||
milestones_obj:
|
||||
description: API object.
|
||||
returned: success
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
|
||||
)
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class GitlabMilestones(object):
|
||||
|
||||
def __init__(self, module, gitlab_instance, group_id, project_id):
|
||||
self._gitlab = gitlab_instance
|
||||
self.gitlab_object = group_id if group_id else project_id
|
||||
self.is_group_milestone = True if group_id else False
|
||||
self._module = module
|
||||
|
||||
def list_all_milestones(self):
|
||||
page_nb = 1
|
||||
milestones = []
|
||||
vars_page = self.gitlab_object.milestones.list(page=page_nb)
|
||||
while len(vars_page) > 0:
|
||||
milestones += vars_page
|
||||
page_nb += 1
|
||||
vars_page = self.gitlab_object.milestones.list(page=page_nb)
|
||||
return milestones
|
||||
|
||||
def create_milestone(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
|
||||
var = {
|
||||
"title": var_obj.get('title'),
|
||||
}
|
||||
|
||||
if var_obj.get('description') is not None:
|
||||
var["description"] = var_obj.get('description')
|
||||
|
||||
if var_obj.get('start_date') is not None:
|
||||
var["start_date"] = self.check_date(var_obj.get('start_date'))
|
||||
|
||||
if var_obj.get('due_date') is not None:
|
||||
var["due_date"] = self.check_date(var_obj.get('due_date'))
|
||||
|
||||
_obj = self.gitlab_object.milestones.create(var)
|
||||
return True, _obj.asdict()
|
||||
|
||||
def update_milestone(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
_milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title')))
|
||||
|
||||
if var_obj.get('description') is not None:
|
||||
_milestone.description = var_obj.get('description')
|
||||
|
||||
if var_obj.get('start_date') is not None:
|
||||
_milestone.start_date = var_obj.get('start_date')
|
||||
|
||||
if var_obj.get('due_date') is not None:
|
||||
_milestone.due_date = var_obj.get('due_date')
|
||||
|
||||
# save returns None
|
||||
_milestone.save()
|
||||
return True, _milestone.asdict()
|
||||
|
||||
def get_milestone_id(self, _title):
|
||||
_milestone_list = self.gitlab_object.milestones.list()
|
||||
_found = list(filter(lambda x: x.title == _title, _milestone_list))
|
||||
if _found:
|
||||
return _found[0].id
|
||||
else:
|
||||
self._module.fail_json(msg="milestone '%s' not found." % _title)
|
||||
|
||||
def check_date(self, _date):
|
||||
try:
|
||||
datetime.strptime(_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
self._module.fail_json(msg="milestone's date '%s' not in correct format." % _date)
|
||||
return _date
|
||||
|
||||
def delete_milestone(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
return True, True
|
||||
_milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title')))
|
||||
# delete returns None
|
||||
_milestone.delete()
|
||||
return True, _milestone.asdict()
|
||||
|
||||
|
||||
def compare(requested_milestones, existing_milestones, state):
|
||||
# we need to do this, because it was determined in a previous version - more or less buggy
|
||||
# basically it is not necessary and might result in more/other bugs!
|
||||
# but it is required and only relevant for check mode!!
|
||||
# logic represents state 'present' when not purge. all other can be derived from that
|
||||
# untouched => equal in both
|
||||
# updated => title are equal
|
||||
# added => title does not exist
|
||||
untouched = list()
|
||||
updated = list()
|
||||
added = list()
|
||||
|
||||
if state == 'present':
|
||||
_existing_milestones = list()
|
||||
for item in existing_milestones:
|
||||
_existing_milestones.append({'title': item.get('title')})
|
||||
|
||||
for var in requested_milestones:
|
||||
if var in existing_milestones:
|
||||
untouched.append(var)
|
||||
else:
|
||||
compare_item = {'title': var.get('title')}
|
||||
if compare_item in _existing_milestones:
|
||||
updated.append(var)
|
||||
else:
|
||||
added.append(var)
|
||||
|
||||
return untouched, updated, added
|
||||
|
||||
|
||||
def native_python_main(this_gitlab, purge, requested_milestones, state, module):
|
||||
change = False
|
||||
return_value = dict(added=[], updated=[], removed=[], untouched=[])
|
||||
return_obj = dict(added=[], updated=[], removed=[])
|
||||
|
||||
milestones_before = [x.asdict() for x in this_gitlab.list_all_milestones()]
|
||||
|
||||
# filter out and enrich before compare
|
||||
for item in requested_milestones:
|
||||
# add defaults when not present
|
||||
if item.get('description') is None:
|
||||
item['description'] = ""
|
||||
if item.get('due_date') is None:
|
||||
item['due_date'] = None
|
||||
if item.get('start_date') is None:
|
||||
item['start_date'] = None
|
||||
|
||||
for item in milestones_before:
|
||||
# remove field only from server
|
||||
item.pop('id')
|
||||
item.pop('iid')
|
||||
item.pop('created_at')
|
||||
item.pop('expired')
|
||||
item.pop('state')
|
||||
item.pop('updated_at')
|
||||
item.pop('web_url')
|
||||
# group milestone has group_id, while project has project_id
|
||||
if 'group_id' in item:
|
||||
item.pop('group_id')
|
||||
if 'project_id' in item:
|
||||
item.pop('project_id')
|
||||
|
||||
if state == 'present':
|
||||
add_or_update = [x for x in requested_milestones if x not in milestones_before]
|
||||
for item in add_or_update:
|
||||
try:
|
||||
_rv, _obj = this_gitlab.create_milestone(item)
|
||||
if _rv:
|
||||
return_value['added'].append(item)
|
||||
return_obj['added'].append(_obj)
|
||||
except Exception:
|
||||
# create raises exception with following error message when milestone already exists
|
||||
_rv, _obj = this_gitlab.update_milestone(item)
|
||||
if _rv:
|
||||
return_value['updated'].append(item)
|
||||
return_obj['updated'].append(_obj)
|
||||
|
||||
if purge:
|
||||
# re-fetch
|
||||
_milestones = this_gitlab.list_all_milestones()
|
||||
|
||||
for item in milestones_before:
|
||||
_rv, _obj = this_gitlab.delete_milestone(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
|
||||
elif state == 'absent':
|
||||
if not purge:
|
||||
_milestone_titles_requested = [x['title'] for x in requested_milestones]
|
||||
remove_requested = [x for x in milestones_before if x['title'] in _milestone_titles_requested]
|
||||
for item in remove_requested:
|
||||
_rv, _obj = this_gitlab.delete_milestone(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
else:
|
||||
for item in milestones_before:
|
||||
_rv, _obj = this_gitlab.delete_milestone(item)
|
||||
if _rv:
|
||||
return_value['removed'].append(item)
|
||||
return_obj['removed'].append(_obj)
|
||||
|
||||
if module.check_mode:
|
||||
_untouched, _updated, _added = compare(requested_milestones, milestones_before, state)
|
||||
return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched)
|
||||
|
||||
if any(return_value[x] for x in ['added', 'removed', 'updated']):
|
||||
change = True
|
||||
|
||||
milestones_after = [x.asdict() for x in this_gitlab.list_all_milestones()]
|
||||
|
||||
return change, return_value, milestones_before, milestones_after, return_obj
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = basic_auth_argument_spec()
|
||||
argument_spec.update(auth_argument_spec())
|
||||
argument_spec.update(
|
||||
project=dict(type='str', required=False, default=None),
|
||||
group=dict(type='str', required=False, default=None),
|
||||
purge=dict(type='bool', required=False, default=False),
|
||||
milestones=dict(type='list', elements='dict', required=False, default=list(),
|
||||
options=dict(
|
||||
title=dict(type='str', required=True),
|
||||
description=dict(type='str', required=False),
|
||||
due_date=dict(type='str', required=False),
|
||||
start_date=dict(type='str', required=False),)
|
||||
),
|
||||
state=dict(type='str', default="present", choices=["absent", "present"]),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
['api_username', 'api_token'],
|
||||
['api_username', 'api_oauth_token'],
|
||||
['api_username', 'api_job_token'],
|
||||
['api_token', 'api_oauth_token'],
|
||||
['api_token', 'api_job_token'],
|
||||
['project', 'group'],
|
||||
],
|
||||
required_together=[
|
||||
['api_username', 'api_password'],
|
||||
],
|
||||
required_one_of=[
|
||||
['api_username', 'api_token', 'api_oauth_token', 'api_job_token'],
|
||||
['project', 'group']
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
ensure_gitlab_package(module)
|
||||
|
||||
gitlab_project = module.params['project']
|
||||
gitlab_group = module.params['group']
|
||||
purge = module.params['purge']
|
||||
milestone_list = module.params['milestones']
|
||||
state = module.params['state']
|
||||
|
||||
gitlab_version = gitlab.__version__
|
||||
_min_gitlab = '3.2.0'
|
||||
if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
|
||||
module.fail_json(msg="community.general.gitlab_milestone requires python-gitlab Python module >= %s "
|
||||
"(installed version: [%s]). Please upgrade "
|
||||
"python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
|
||||
|
||||
gitlab_instance = gitlab_authentication(module)
|
||||
|
||||
# find_project can return None, but the other must exist
|
||||
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
|
||||
|
||||
# find_group can return None, but the other must exist
|
||||
gitlab_group_id = find_group(gitlab_instance, gitlab_group)
|
||||
|
||||
# if both not found, module must exist
|
||||
if not gitlab_project_id and not gitlab_group_id:
|
||||
if gitlab_project and not gitlab_project_id:
|
||||
module.fail_json(msg="project '%s' not found." % gitlab_project)
|
||||
if gitlab_group and not gitlab_group_id:
|
||||
module.fail_json(msg="group '%s' not found." % gitlab_group)
|
||||
|
||||
this_gitlab = GitlabMilestones(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id,
|
||||
project_id=gitlab_project_id)
|
||||
|
||||
change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, milestone_list, state,
|
||||
module)
|
||||
|
||||
if not module.check_mode:
|
||||
raw_return_value['untouched'] = [x for x in before if x in after]
|
||||
|
||||
added = [x.get('title') for x in raw_return_value['added']]
|
||||
updated = [x.get('title') for x in raw_return_value['updated']]
|
||||
removed = [x.get('title') for x in raw_return_value['removed']]
|
||||
untouched = [x.get('title') for x in raw_return_value['untouched']]
|
||||
return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched)
|
||||
|
||||
module.exit_json(changed=change, milestones=return_value, milestones_obj=_obj)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
318
plugins/modules/gitlab_project_access_token.py
Normal file
318
plugins/modules/gitlab_project_access_token.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024, Zoran Krleza (zoran.krleza@true-north.hr)
|
||||
# Based on code:
|
||||
# Copyright (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr)
|
||||
# Copyright (c) 2018, Marcus Watkins <marwatk@marcuswatkins.net>
|
||||
# Copyright (c) 2013, Phillip Gentry <phillip@cx.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 = r'''
|
||||
module: gitlab_project_access_token
|
||||
short_description: Manages GitLab project access tokens
|
||||
version_added: 8.4.0
|
||||
description:
|
||||
- Creates and revokes project access tokens.
|
||||
author:
|
||||
- Zoran Krleza (@pixslx)
|
||||
requirements:
|
||||
- python-gitlab >= 3.1.0
|
||||
extends_documentation_fragment:
|
||||
- community.general.auth_basic
|
||||
- community.general.gitlab
|
||||
- community.general.attributes
|
||||
notes:
|
||||
- Access tokens can not be changed. If a parameter needs to be changed, an acceess token has to be recreated.
|
||||
Whether tokens will be recreated is controlled by the O(recreate) option, which defaults to V(never).
|
||||
- Token string is contained in the result only when access token is created or recreated. It can not be fetched afterwards.
|
||||
- Token matching is done by comparing O(name) option.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
|
||||
options:
|
||||
project:
|
||||
description:
|
||||
- ID or full path of project in the form of group/name.
|
||||
required: true
|
||||
type: str
|
||||
name:
|
||||
description:
|
||||
- Access token's name.
|
||||
required: true
|
||||
type: str
|
||||
scopes:
|
||||
description:
|
||||
- Scope of the access token.
|
||||
required: true
|
||||
type: list
|
||||
elements: str
|
||||
aliases: ["scope"]
|
||||
choices: ["api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository", "create_runner", "ai_features", "k8s_proxy"]
|
||||
access_level:
|
||||
description:
|
||||
- Access level of the access token.
|
||||
type: str
|
||||
default: maintainer
|
||||
choices: ["guest", "reporter", "developer", "maintainer", "owner"]
|
||||
expires_at:
|
||||
description:
|
||||
- Expiration date of the access token in C(YYYY-MM-DD) format.
|
||||
- Make sure to quote this value in YAML to ensure it is kept as a string and not interpreted as a YAML date.
|
||||
type: str
|
||||
required: true
|
||||
recreate:
|
||||
description:
|
||||
- Whether the access token will be recreated if it already exists.
|
||||
- When V(never) the token will never be recreated.
|
||||
- When V(always) the token will always be recreated.
|
||||
- When V(state_change) the token will be recreated if there is a difference between desired state and actual state.
|
||||
type: str
|
||||
choices: ["never", "always", "state_change"]
|
||||
default: never
|
||||
state:
|
||||
description:
|
||||
- When V(present) the access token will be added to the project if it does not exist.
|
||||
- When V(absent) it will be removed from the project if it exists.
|
||||
default: present
|
||||
type: str
|
||||
choices: [ "present", "absent" ]
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: "Creating a project access token"
|
||||
community.general.gitlab_project_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
project: "my_group/my_project"
|
||||
name: "project_token"
|
||||
expires_at: "2024-12-31"
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
state: present
|
||||
|
||||
- name: "Revoking a project access token"
|
||||
community.general.gitlab_project_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
project: "my_group/my_project"
|
||||
name: "project_token"
|
||||
expires_at: "2024-12-31"
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
state: absent
|
||||
|
||||
- name: "Change (recreate) existing token if its actual state is different than desired state"
|
||||
community.general.gitlab_project_access_token:
|
||||
api_url: https://gitlab.example.com/
|
||||
api_token: "somegitlabapitoken"
|
||||
project: "my_group/my_project"
|
||||
name: "project_token"
|
||||
expires_at: "2024-12-31"
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
- read_repository
|
||||
- write_repository
|
||||
recreate: state_change
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
access_token:
|
||||
description:
|
||||
- API object.
|
||||
- Only contains the value of the token if the token was created or recreated.
|
||||
returned: success and O(state=present)
|
||||
type: dict
|
||||
'''
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, find_project, gitlab_authentication, gitlab
|
||||
)
|
||||
|
||||
ACCESS_LEVELS = dict(guest=10, reporter=20, developer=30, maintainer=40, owner=50)
|
||||
|
||||
|
||||
class GitLabProjectAccessToken(object):
|
||||
def __init__(self, module, gitlab_instance):
|
||||
self._module = module
|
||||
self._gitlab = gitlab_instance
|
||||
self.access_token_object = None
|
||||
|
||||
'''
|
||||
@param project Project Object
|
||||
@param arguments Attributes of the access_token
|
||||
'''
|
||||
def create_access_token(self, project, arguments):
|
||||
changed = False
|
||||
if self._module.check_mode:
|
||||
return True
|
||||
|
||||
try:
|
||||
self.access_token_object = project.access_tokens.create(arguments)
|
||||
changed = True
|
||||
except (gitlab.exceptions.GitlabCreateError) as e:
|
||||
self._module.fail_json(msg="Failed to create access token: %s " % to_native(e))
|
||||
|
||||
return changed
|
||||
|
||||
'''
|
||||
@param project Project object
|
||||
@param name of the access token
|
||||
'''
|
||||
def find_access_token(self, project, name):
|
||||
access_tokens = project.access_tokens.list(all=True)
|
||||
for access_token in access_tokens:
|
||||
if (access_token.name == name):
|
||||
self.access_token_object = access_token
|
||||
return False
|
||||
return False
|
||||
|
||||
def revoke_access_token(self):
|
||||
if self._module.check_mode:
|
||||
return True
|
||||
|
||||
changed = False
|
||||
try:
|
||||
self.access_token_object.delete()
|
||||
changed = True
|
||||
except (gitlab.exceptions.GitlabCreateError) as e:
|
||||
self._module.fail_json(msg="Failed to revoke access token: %s " % to_native(e))
|
||||
|
||||
return changed
|
||||
|
||||
def access_tokens_equal(self):
|
||||
if self.access_token_object.name != self._module.params['name']:
|
||||
return False
|
||||
if self.access_token_object.scopes != self._module.params['scopes']:
|
||||
return False
|
||||
if self.access_token_object.access_level != ACCESS_LEVELS[self._module.params['access_level']]:
|
||||
return False
|
||||
if self.access_token_object.expires_at != self._module.params['expires_at']:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = basic_auth_argument_spec()
|
||||
argument_spec.update(auth_argument_spec())
|
||||
argument_spec.update(dict(
|
||||
state=dict(type='str', default="present", choices=["absent", "present"]),
|
||||
project=dict(type='str', required=True),
|
||||
name=dict(type='str', required=True),
|
||||
scopes=dict(type='list',
|
||||
required=True,
|
||||
aliases=['scope'],
|
||||
elements='str',
|
||||
choices=['api',
|
||||
'read_api',
|
||||
'read_registry',
|
||||
'write_registry',
|
||||
'read_repository',
|
||||
'write_repository',
|
||||
'create_runner',
|
||||
'ai_features',
|
||||
'k8s_proxy']),
|
||||
access_level=dict(type='str', required=False, default='maintainer', choices=['guest', 'reporter', 'developer', 'maintainer', 'owner']),
|
||||
expires_at=dict(type='str', required=True),
|
||||
recreate=dict(type='str', default='never', choices=['never', 'always', 'state_change'])
|
||||
))
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
['api_username', 'api_token'],
|
||||
['api_username', 'api_oauth_token'],
|
||||
['api_username', 'api_job_token'],
|
||||
['api_token', 'api_oauth_token'],
|
||||
['api_token', 'api_job_token']
|
||||
],
|
||||
required_together=[
|
||||
['api_username', 'api_password']
|
||||
],
|
||||
required_one_of=[
|
||||
['api_username', 'api_token', 'api_oauth_token', 'api_job_token']
|
||||
],
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
project_identifier = module.params['project']
|
||||
name = module.params['name']
|
||||
scopes = module.params['scopes']
|
||||
access_level_str = module.params['access_level']
|
||||
expires_at = module.params['expires_at']
|
||||
recreate = module.params['recreate']
|
||||
|
||||
access_level = ACCESS_LEVELS[access_level_str]
|
||||
|
||||
try:
|
||||
datetime.strptime(expires_at, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
module.fail_json(msg="Argument expires_at is not in required format YYYY-MM-DD")
|
||||
|
||||
gitlab_instance = gitlab_authentication(module)
|
||||
|
||||
gitlab_access_token = GitLabProjectAccessToken(module, gitlab_instance)
|
||||
|
||||
project = find_project(gitlab_instance, project_identifier)
|
||||
if project is None:
|
||||
module.fail_json(msg="Failed to create access token: project %s does not exists" % project_identifier)
|
||||
|
||||
gitlab_access_token_exists = False
|
||||
gitlab_access_token.find_access_token(project, name)
|
||||
if gitlab_access_token.access_token_object is not None:
|
||||
gitlab_access_token_exists = True
|
||||
|
||||
if state == 'absent':
|
||||
if gitlab_access_token_exists:
|
||||
gitlab_access_token.revoke_access_token()
|
||||
module.exit_json(changed=True, msg="Successfully deleted access token %s" % name)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="Access token does not exists")
|
||||
|
||||
if state == 'present':
|
||||
if gitlab_access_token_exists:
|
||||
if gitlab_access_token.access_tokens_equal():
|
||||
if recreate == 'always':
|
||||
gitlab_access_token.revoke_access_token()
|
||||
gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
module.exit_json(changed=False, msg="Access token already exists", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
if recreate == 'never':
|
||||
module.fail_json(msg="Access token already exists and its state is different. It can not be updated without recreating.")
|
||||
else:
|
||||
gitlab_access_token.revoke_access_token()
|
||||
gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully recreated access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
else:
|
||||
gitlab_access_token.create_access_token(project, {'name': name, 'scopes': scopes, 'access_level': access_level, 'expires_at': expires_at})
|
||||
module.exit_json(changed=True, msg="Successfully created access token", access_token=gitlab_access_token.access_token_object._attrs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -97,7 +97,7 @@ from ansible.module_utils.api import basic_auth_argument_spec
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, find_project
|
||||
auth_argument_spec, gitlab_authentication, find_project, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ def present_strategy(module, gl, project, wished_badge):
|
||||
changed = False
|
||||
|
||||
existing_badge = None
|
||||
for badge in project.badges.list(iterator=True):
|
||||
for badge in project.badges.list(**list_all_kwargs):
|
||||
if badge.image_url == wished_badge["image_url"]:
|
||||
existing_badge = badge
|
||||
break
|
||||
@@ -135,7 +135,7 @@ def absent_strategy(module, gl, project, wished_badge):
|
||||
changed = False
|
||||
|
||||
existing_badge = None
|
||||
for badge in project.badges.list(iterator=True):
|
||||
for badge in project.badges.list(**list_all_kwargs):
|
||||
if badge.image_url == wished_badge["image_url"]:
|
||||
existing_badge = badge
|
||||
break
|
||||
|
||||
@@ -225,7 +225,8 @@ from ansible.module_utils.api import basic_auth_argument_spec
|
||||
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables
|
||||
auth_argument_spec, gitlab_authentication, filter_returned_variables, vars_to_variables,
|
||||
list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -240,14 +241,7 @@ class GitlabProjectVariables(object):
|
||||
return self.repo.projects.get(project_name)
|
||||
|
||||
def list_all_project_variables(self):
|
||||
page_nb = 1
|
||||
variables = []
|
||||
vars_page = self.project.variables.list(page=page_nb)
|
||||
while len(vars_page) > 0:
|
||||
variables += vars_page
|
||||
page_nb += 1
|
||||
vars_page = self.project.variables.list(page=page_nb)
|
||||
return variables
|
||||
return list(self.project.variables.list(**list_all_kwargs))
|
||||
|
||||
def create_variable(self, var_obj):
|
||||
if self._module.check_mode:
|
||||
|
||||
@@ -219,7 +219,7 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, gitlab_authentication, gitlab
|
||||
auth_argument_spec, gitlab_authentication, gitlab, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ class GitLabRunner(object):
|
||||
@param description Description of the runner
|
||||
'''
|
||||
def find_runner(self, description):
|
||||
runners = self._runners_endpoint(as_list=False)
|
||||
runners = self._runners_endpoint(**list_all_kwargs)
|
||||
|
||||
for runner in runners:
|
||||
# python-gitlab 2.2 through at least 2.5 returns a list of dicts for list() instead of a Runner
|
||||
|
||||
@@ -230,7 +230,7 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.gitlab import (
|
||||
auth_argument_spec, find_group, gitlab_authentication, gitlab
|
||||
auth_argument_spec, find_group, gitlab_authentication, gitlab, list_all_kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -345,9 +345,10 @@ class GitLabUser(object):
|
||||
@param sshkey_name Name of the ssh key
|
||||
'''
|
||||
def ssh_key_exists(self, user, sshkey_name):
|
||||
keyList = map(lambda k: k.title, user.keys.list(all=True))
|
||||
|
||||
return sshkey_name in keyList
|
||||
return any(
|
||||
k.title == sshkey_name
|
||||
for k in user.keys.list(**list_all_kwargs)
|
||||
)
|
||||
|
||||
'''
|
||||
@param user User object
|
||||
@@ -515,10 +516,13 @@ class GitLabUser(object):
|
||||
@param username Username of the user
|
||||
'''
|
||||
def find_user(self, username):
|
||||
users = self._gitlab.users.list(search=username, all=True)
|
||||
for user in users:
|
||||
if (user.username == username):
|
||||
return user
|
||||
return next(
|
||||
(
|
||||
user for user in self._gitlab.users.list(search=username, **list_all_kwargs)
|
||||
if user.username == username
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
'''
|
||||
@param username Username of the user
|
||||
|
||||
@@ -165,6 +165,7 @@ changed_pkgs:
|
||||
version_added: '0.2.0'
|
||||
'''
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
|
||||
@@ -184,6 +185,10 @@ def _create_regex_group_complement(s):
|
||||
chars = filter(None, (line.split('#')[0].strip() for line in lines))
|
||||
group = r'[^' + r''.join(chars) + r']'
|
||||
return re.compile(group)
|
||||
|
||||
|
||||
def _check_package_in_json(json_output, package_type):
|
||||
return bool(json_output.get(package_type, []) and json_output[package_type][0].get("installed"))
|
||||
# /utils ------------------------------------------------------------------ }}}
|
||||
|
||||
|
||||
@@ -479,17 +484,13 @@ class Homebrew(object):
|
||||
cmd = [
|
||||
"{brew_path}".format(brew_path=self.brew_path),
|
||||
"info",
|
||||
"--json=v2",
|
||||
self.current_package,
|
||||
]
|
||||
rc, out, err = self.module.run_command(cmd)
|
||||
for line in out.split('\n'):
|
||||
if (
|
||||
re.search(r'Built from source', line)
|
||||
or re.search(r'Poured from bottle', line)
|
||||
):
|
||||
return True
|
||||
data = json.loads(out)
|
||||
|
||||
return False
|
||||
return _check_package_in_json(data, "formulae") or _check_package_in_json(data, "casks")
|
||||
|
||||
def _current_package_is_outdated(self):
|
||||
if not self.valid_package(self.current_package):
|
||||
|
||||
@@ -237,7 +237,7 @@ def get_otptoken_dict(ansible_to_ipa, uniqueid=None, newuniqueid=None, otptype=N
|
||||
if owner is not None:
|
||||
otptoken[ansible_to_ipa['owner']] = owner
|
||||
if enabled is not None:
|
||||
otptoken[ansible_to_ipa['enabled']] = 'FALSE' if enabled else 'TRUE'
|
||||
otptoken[ansible_to_ipa['enabled']] = False if enabled else True
|
||||
if notbefore is not None:
|
||||
otptoken[ansible_to_ipa['notbefore']] = notbefore + 'Z'
|
||||
if notafter is not None:
|
||||
|
||||
@@ -103,6 +103,7 @@ options:
|
||||
userauthtype:
|
||||
description:
|
||||
- The authentication type to use for the user.
|
||||
- To remove all authentication types from the user, use an empty list V([]).
|
||||
- The choice V(idp) and V(passkey) has been added in community.general 8.1.0.
|
||||
choices: ["password", "radius", "otp", "pkinit", "hardened", "idp", "passkey"]
|
||||
type: list
|
||||
|
||||
@@ -69,7 +69,7 @@ options:
|
||||
type: str
|
||||
required: true
|
||||
notes:
|
||||
- The C(pycdlib) library states it supports Python 2.7 and 3.4 only.
|
||||
- The C(pycdlib) library states it supports Python 2.7 and 3.4+.
|
||||
- >
|
||||
The function C(add_file) in pycdlib will overwrite the existing file in ISO with type ISO9660 / Rock Ridge 1.12 / Joliet / UDF.
|
||||
But it will not overwrite the existing file in ISO with Rock Ridge 1.09 / 1.10.
|
||||
|
||||
@@ -717,13 +717,14 @@ 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
|
||||
import copy
|
||||
|
||||
|
||||
PROTOCOL_OPENID_CONNECT = 'openid-connect'
|
||||
PROTOCOL_SAML = 'saml'
|
||||
CLIENT_META_DATA = ['authorizationServicesEnabled']
|
||||
|
||||
|
||||
def normalise_cr(clientrep, remove_ids=False):
|
||||
@@ -946,7 +947,7 @@ def main():
|
||||
if module._diff:
|
||||
result['diff'] = dict(before=sanitize_cr(before_norm),
|
||||
after=sanitize_cr(desired_norm))
|
||||
result['changed'] = (before_norm != desired_norm)
|
||||
result['changed'] = not is_struct_included(desired_norm, before_norm, CLIENT_META_DATA)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
@@ -232,12 +232,16 @@ class Modprobe(object):
|
||||
|
||||
@property
|
||||
def modules_files(self):
|
||||
if not os.path.isdir(MODULES_LOAD_LOCATION):
|
||||
return []
|
||||
modules_paths = [os.path.join(MODULES_LOAD_LOCATION, path)
|
||||
for path in os.listdir(MODULES_LOAD_LOCATION)]
|
||||
return [path for path in modules_paths if os.path.isfile(path)]
|
||||
|
||||
@property
|
||||
def modprobe_files(self):
|
||||
if not os.path.isdir(PARAMETERS_FILES_LOCATION):
|
||||
return []
|
||||
modules_paths = [os.path.join(PARAMETERS_FILES_LOCATION, path)
|
||||
for path in os.listdir(PARAMETERS_FILES_LOCATION)]
|
||||
return [path for path in modules_paths if os.path.isfile(path)]
|
||||
|
||||
@@ -56,6 +56,14 @@ options:
|
||||
- Each batch must return at least one result set.
|
||||
required: true
|
||||
type: str
|
||||
transaction:
|
||||
description:
|
||||
- If transactional mode is requested, start a transaction and commit the change only if the script succeed.
|
||||
Otherwise, rollback the transaction.
|
||||
- If transactional mode is not requested (default), automatically commit the change.
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 8.4.0
|
||||
output:
|
||||
description:
|
||||
- With V(default) each row will be returned as a list of values. See RV(query_results).
|
||||
@@ -105,6 +113,19 @@ EXAMPLES = r'''
|
||||
- result_params.query_results[0][0][0][0] == 'msdb'
|
||||
- result_params.query_results[0][0][0][1] == 'ONLINE'
|
||||
|
||||
- name: Query within a transaction
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ mssql_login_user }}"
|
||||
login_password: "{{ mssql_login_password }}"
|
||||
login_host: "{{ mssql_host }}"
|
||||
login_port: "{{ mssql_port }}"
|
||||
script: |
|
||||
UPDATE sys.SomeTable SET desc = 'some_table_desc' WHERE name = %(dbname)s
|
||||
UPDATE sys.AnotherTable SET desc = 'another_table_desc' WHERE name = %(dbname)s
|
||||
transaction: true
|
||||
params:
|
||||
dbname: msdb
|
||||
|
||||
- name: two batches with default output
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ mssql_login_user }}"
|
||||
@@ -230,6 +251,7 @@ def run_module():
|
||||
script=dict(required=True),
|
||||
output=dict(default='default', choices=['dict', 'default']),
|
||||
params=dict(type='dict'),
|
||||
transaction=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
result = dict(
|
||||
@@ -252,6 +274,8 @@ def run_module():
|
||||
script = module.params['script']
|
||||
output = module.params['output']
|
||||
sql_params = module.params['params']
|
||||
# Added param to set the transactional mode (true/false)
|
||||
transaction = module.params['transaction']
|
||||
|
||||
login_querystring = login_host
|
||||
if login_port != 1433:
|
||||
@@ -273,7 +297,8 @@ def run_module():
|
||||
module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check your "
|
||||
"@sysconfdir@/freetds.conf / ${HOME}/.freetds.conf")
|
||||
|
||||
conn.autocommit(True)
|
||||
# If transactional mode is requested, start a transaction
|
||||
conn.autocommit(not transaction)
|
||||
|
||||
query_results_key = 'query_results'
|
||||
if output == 'dict':
|
||||
@@ -283,7 +308,7 @@ def run_module():
|
||||
# Process the script into batches
|
||||
queries = []
|
||||
current_batch = []
|
||||
for statement in script.splitlines(keepends=True):
|
||||
for statement in script.splitlines(True):
|
||||
# Ignore the Byte Order Mark, if found
|
||||
if statement.strip() == '\uFEFF':
|
||||
continue
|
||||
@@ -322,9 +347,16 @@ def run_module():
|
||||
):
|
||||
query_results.append([])
|
||||
else:
|
||||
# Rollback transaction before failing the module in case of error
|
||||
if transaction:
|
||||
conn.rollback()
|
||||
error_msg = '%s: %s' % (type(e).__name__, str(e))
|
||||
module.fail_json(msg="query failed", query=query, error=error_msg, **result)
|
||||
|
||||
# Commit transaction before exiting the module in case of no error
|
||||
if transaction:
|
||||
conn.commit()
|
||||
|
||||
# ensure that the result is json serializable
|
||||
qry_results = json.loads(json.dumps(query_results, default=clean_output))
|
||||
|
||||
|
||||
@@ -1832,7 +1832,7 @@ class Nmcli(object):
|
||||
elif self.type == 'wifi':
|
||||
options.update({
|
||||
'802-11-wireless.ssid': self.ssid,
|
||||
'connection.slave-type': 'bond' if self.master else None,
|
||||
'connection.slave-type': ('bond' if self.slave_type is None else self.slave_type) if self.master else None,
|
||||
})
|
||||
if self.wifi:
|
||||
for name, value in self.wifi.items():
|
||||
|
||||
@@ -88,11 +88,13 @@ options:
|
||||
EXAMPLES = '''
|
||||
- name: Import a key via local file
|
||||
community.general.pacman_key:
|
||||
id: 01234567890ABCDE01234567890ABCDE12345678
|
||||
data: "{{ lookup('file', 'keyfile.asc') }}"
|
||||
state: present
|
||||
|
||||
- name: Import a key via remote file
|
||||
community.general.pacman_key:
|
||||
id: 01234567890ABCDE01234567890ABCDE12345678
|
||||
file: /tmp/keyfile.asc
|
||||
state: present
|
||||
|
||||
|
||||
@@ -174,6 +174,13 @@ def query_package(module, name):
|
||||
# '<' - installed but out of date
|
||||
# '=' - installed and up to date
|
||||
# '>' - installed but newer than the repository version
|
||||
|
||||
if (package in ('reading local summary...',
|
||||
'processing local summary...',
|
||||
'downloading pkg_summary.xz done.')) or \
|
||||
(package.startswith('processing remote summary (')):
|
||||
continue
|
||||
|
||||
pkgname_with_version, raw_state = package.split(splitchar)[0:2]
|
||||
|
||||
# Search for package, stripping version
|
||||
@@ -317,7 +324,7 @@ def do_upgrade_packages(module, full=False):
|
||||
format_pkgin_command(module, cmd))
|
||||
|
||||
if rc == 0:
|
||||
if re.search('^nothing to do.\n$', out):
|
||||
if re.search('^(.*\n|)nothing to do.\n$', out):
|
||||
module.exit_json(changed=False, msg="nothing left to upgrade")
|
||||
else:
|
||||
module.fail_json(msg="could not %s packages" % cmd, stdout=out, stderr=err)
|
||||
|
||||
@@ -562,6 +562,10 @@ class ProxmoxLxcAnsible(ProxmoxAnsible):
|
||||
# compare the requested config against the current
|
||||
update_config = False
|
||||
for (arg, value) in kwargs.items():
|
||||
# if the arg isn't in the current config, it needs to be updated
|
||||
if arg not in current_config:
|
||||
update_config = True
|
||||
break
|
||||
# some values are lists, the order isn't always the same, so split them and compare by key
|
||||
if isinstance(value, str):
|
||||
current_values = current_config[arg].split(",")
|
||||
|
||||
@@ -522,9 +522,17 @@ options:
|
||||
- If V(true), the VM will be updated with new value.
|
||||
- Because of the operations of the API and security reasons, I have disabled the update of the following parameters
|
||||
O(net), O(virtio), O(ide), O(sata), O(scsi). Per example updating O(net) update the MAC address and C(virtio) create always new disk...
|
||||
This security feature can be disabled by setting the O(update_unsafe) to V(true).
|
||||
- Update of O(pool) is disabled. It needs an additional API endpoint not covered by this module.
|
||||
type: bool
|
||||
default: false
|
||||
update_unsafe:
|
||||
description:
|
||||
- If V(true), do not enforce limitations on parameters O(net), O(virtio), O(ide), O(sata), O(scsi), O(efidisk0), and O(tpmstate0).
|
||||
Use this option with caution because an improper configuration might result in a permanent loss of data (e.g. disk recreated).
|
||||
type: bool
|
||||
default: false
|
||||
version_added: 8.4.0
|
||||
vcpus:
|
||||
description:
|
||||
- Sets number of hotplugged vcpus.
|
||||
@@ -846,6 +854,20 @@ EXAMPLES = '''
|
||||
memory: 16384
|
||||
update: true
|
||||
|
||||
- name: Update VM configuration (incl. unsafe options)
|
||||
community.general.proxmox_kvm:
|
||||
api_user: root@pam
|
||||
api_password: secret
|
||||
api_host: helldorado
|
||||
name: spynal
|
||||
node: sabrewulf
|
||||
cores: 8
|
||||
memory: 16384
|
||||
net:
|
||||
net0: virtio,bridge=vmbr1
|
||||
update: true
|
||||
update_unsafe: true
|
||||
|
||||
- name: Delete QEMU parameters
|
||||
community.general.proxmox_kvm:
|
||||
api_user: root@pam
|
||||
@@ -981,7 +1003,7 @@ class ProxmoxKvmAnsible(ProxmoxAnsible):
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def create_vm(self, vmid, newid, node, name, memory, cpu, cores, sockets, update, **kwargs):
|
||||
def create_vm(self, vmid, newid, node, name, memory, cpu, cores, sockets, update, update_unsafe, **kwargs):
|
||||
# Available only in PVE 4
|
||||
only_v4 = ['force', 'protection', 'skiplock']
|
||||
only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig', 'tags']
|
||||
@@ -1018,23 +1040,24 @@ class ProxmoxKvmAnsible(ProxmoxAnsible):
|
||||
urlencoded_ssh_keys = quote(kwargs['sshkeys'], safe='')
|
||||
kwargs['sshkeys'] = str(urlencoded_ssh_keys)
|
||||
|
||||
# If update, don't update disk (virtio, efidisk0, tpmstate0, ide, sata, scsi) and network interface
|
||||
# If update, don't update disk (virtio, efidisk0, tpmstate0, ide, sata, scsi) and network interface, unless update_unsafe=True
|
||||
# pool parameter not supported by qemu/<vmid>/config endpoint on "update" (PVE 6.2) - only with "create"
|
||||
if update:
|
||||
if 'virtio' in kwargs:
|
||||
del kwargs['virtio']
|
||||
if 'sata' in kwargs:
|
||||
del kwargs['sata']
|
||||
if 'scsi' in kwargs:
|
||||
del kwargs['scsi']
|
||||
if 'ide' in kwargs:
|
||||
del kwargs['ide']
|
||||
if 'efidisk0' in kwargs:
|
||||
del kwargs['efidisk0']
|
||||
if 'tpmstate0' in kwargs:
|
||||
del kwargs['tpmstate0']
|
||||
if 'net' in kwargs:
|
||||
del kwargs['net']
|
||||
if update_unsafe is False:
|
||||
if 'virtio' in kwargs:
|
||||
del kwargs['virtio']
|
||||
if 'sata' in kwargs:
|
||||
del kwargs['sata']
|
||||
if 'scsi' in kwargs:
|
||||
del kwargs['scsi']
|
||||
if 'ide' in kwargs:
|
||||
del kwargs['ide']
|
||||
if 'efidisk0' in kwargs:
|
||||
del kwargs['efidisk0']
|
||||
if 'tpmstate0' in kwargs:
|
||||
del kwargs['tpmstate0']
|
||||
if 'net' in kwargs:
|
||||
del kwargs['net']
|
||||
if 'force' in kwargs:
|
||||
del kwargs['force']
|
||||
if 'pool' in kwargs:
|
||||
@@ -1286,6 +1309,7 @@ def main():
|
||||
version=dict(type='str', choices=['2.0', '1.2'], default='2.0')
|
||||
)),
|
||||
update=dict(type='bool', default=False),
|
||||
update_unsafe=dict(type='bool', default=False),
|
||||
vcpus=dict(type='int'),
|
||||
vga=dict(choices=['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4']),
|
||||
virtio=dict(type='dict'),
|
||||
@@ -1320,6 +1344,7 @@ def main():
|
||||
sockets = module.params['sockets']
|
||||
state = module.params['state']
|
||||
update = bool(module.params['update'])
|
||||
update_unsafe = bool(module.params['update_unsafe'])
|
||||
vmid = module.params['vmid']
|
||||
validate_certs = module.params['validate_certs']
|
||||
|
||||
@@ -1429,7 +1454,7 @@ def main():
|
||||
module.fail_json(msg="node '%s' does not exist in cluster" % node)
|
||||
|
||||
try:
|
||||
proxmox.create_vm(vmid, newid, node, name, memory, cpu, cores, sockets, update,
|
||||
proxmox.create_vm(vmid, newid, node, name, memory, cpu, cores, sockets, update, update_unsafe,
|
||||
archive=module.params['archive'],
|
||||
acpi=module.params['acpi'],
|
||||
agent=module.params['agent'],
|
||||
|
||||
@@ -88,6 +88,12 @@ options:
|
||||
- ID of the System, Manager or Chassis to modify.
|
||||
type: str
|
||||
version_added: '0.2.0'
|
||||
service_id:
|
||||
required: false
|
||||
description:
|
||||
- ID of the manager to update.
|
||||
type: str
|
||||
version_added: '8.4.0'
|
||||
nic_addr:
|
||||
required: false
|
||||
description:
|
||||
@@ -334,6 +340,15 @@ EXAMPLES = '''
|
||||
RAIDType: "RAID0"
|
||||
Drives:
|
||||
- "/redfish/v1/Systems/1/Storage/DE00B000/Drives/1"
|
||||
|
||||
- name: Set service identification to {{ service_id }}
|
||||
community.general.redfish_config:
|
||||
category: Manager
|
||||
command: SetServiceIdentification
|
||||
service_id: "{{ service_id }}"
|
||||
baseuri: "{{ baseuri }}"
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
@@ -353,7 +368,7 @@ from ansible.module_utils.common.text.converters import to_native
|
||||
CATEGORY_COMMANDS_ALL = {
|
||||
"Systems": ["SetBiosDefaultSettings", "SetBiosAttributes", "SetBootOrder",
|
||||
"SetDefaultBootOrder", "EnableSecureBoot", "SetSecureBoot", "DeleteVolumes", "CreateVolume"],
|
||||
"Manager": ["SetNetworkProtocols", "SetManagerNic", "SetHostInterface"],
|
||||
"Manager": ["SetNetworkProtocols", "SetManagerNic", "SetHostInterface", "SetServiceIdentification"],
|
||||
"Sessions": ["SetSessionService"],
|
||||
}
|
||||
|
||||
@@ -376,6 +391,7 @@ def main():
|
||||
default={}
|
||||
),
|
||||
resource_id=dict(),
|
||||
service_id=dict(),
|
||||
nic_addr=dict(default='null'),
|
||||
nic_config=dict(
|
||||
type='dict',
|
||||
@@ -445,6 +461,9 @@ def main():
|
||||
# HostInterface instance ID
|
||||
hostinterface_id = module.params['hostinterface_id']
|
||||
|
||||
# Service Identification
|
||||
service_id = module.params['service_id']
|
||||
|
||||
# Sessions config options
|
||||
sessions_config = module.params['sessions_config']
|
||||
|
||||
@@ -512,6 +531,8 @@ def main():
|
||||
result = rf_utils.set_manager_nic(nic_addr, nic_config)
|
||||
elif command == "SetHostInterface":
|
||||
result = rf_utils.set_hostinterface_attributes(hostinterface_config, hostinterface_id)
|
||||
elif command == "SetServiceIdentification":
|
||||
result = rf_utils.set_service_identification(service_id)
|
||||
|
||||
elif category == "Sessions":
|
||||
# execute only if we find a Sessions resource
|
||||
|
||||
@@ -55,6 +55,11 @@ options:
|
||||
- Security token for authenticating to OOB controller.
|
||||
type: str
|
||||
version_added: 2.3.0
|
||||
manager:
|
||||
description:
|
||||
- Name of manager on OOB controller to target.
|
||||
type: str
|
||||
version_added: '8.3.0'
|
||||
timeout:
|
||||
description:
|
||||
- Timeout in seconds for HTTP requests to OOB controller.
|
||||
@@ -248,6 +253,15 @@ EXAMPLES = '''
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
|
||||
- name: Get service identification
|
||||
community.general.redfish_info:
|
||||
category: Manager
|
||||
command: GetServiceIdentification
|
||||
manager: "{{ manager }}"
|
||||
baseuri: "{{ baseuri }}"
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
|
||||
- name: Get software inventory
|
||||
community.general.redfish_info:
|
||||
category: Update
|
||||
@@ -369,7 +383,7 @@ CATEGORY_COMMANDS_ALL = {
|
||||
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory",
|
||||
"GetUpdateStatus"],
|
||||
"Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols",
|
||||
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory"],
|
||||
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory", "GetServiceIdentification"],
|
||||
}
|
||||
|
||||
CATEGORY_COMMANDS_DEFAULT = {
|
||||
@@ -395,6 +409,7 @@ def main():
|
||||
auth_token=dict(no_log=True),
|
||||
timeout=dict(type='int'),
|
||||
update_handle=dict(),
|
||||
manager=dict(),
|
||||
),
|
||||
required_together=[
|
||||
('username', 'password'),
|
||||
@@ -429,6 +444,9 @@ def main():
|
||||
# update handle
|
||||
update_handle = module.params['update_handle']
|
||||
|
||||
# manager
|
||||
manager = module.params['manager']
|
||||
|
||||
# Build root URI
|
||||
root_uri = "https://" + module.params['baseuri']
|
||||
rf_utils = RedfishUtils(creds, root_uri, timeout, module)
|
||||
@@ -579,6 +597,8 @@ def main():
|
||||
result["host_interfaces"] = rf_utils.get_hostinterfaces()
|
||||
elif command == "GetManagerInventory":
|
||||
result["manager"] = rf_utils.get_multi_manager_inventory()
|
||||
elif command == "GetServiceIdentification":
|
||||
result["service_id"] = rf_utils.get_service_identification(manager)
|
||||
|
||||
# Return data back
|
||||
module.exit_json(redfish_facts=result)
|
||||
|
||||
@@ -45,6 +45,12 @@ options:
|
||||
- The name of the sudoers rule.
|
||||
- This will be used for the filename for the sudoers file managed by this rule.
|
||||
type: str
|
||||
noexec:
|
||||
description:
|
||||
- Whether a command is prevented to run further commands itself.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 8.4.0
|
||||
nopassword:
|
||||
description:
|
||||
- Whether a password will be required to run the sudo'd command.
|
||||
@@ -143,6 +149,15 @@ EXAMPLES = '''
|
||||
user: alice
|
||||
commands: /usr/local/bin/upload
|
||||
setenv: true
|
||||
|
||||
- name: >-
|
||||
Allow alice to sudo /usr/bin/less but prevent less from
|
||||
running further commands itself
|
||||
community.general.sudoers:
|
||||
name: allow-alice-restricted-less
|
||||
user: alice
|
||||
commands: /usr/bin/less
|
||||
noexec: true
|
||||
'''
|
||||
|
||||
import os
|
||||
@@ -162,6 +177,7 @@ class Sudoers(object):
|
||||
self.user = module.params['user']
|
||||
self.group = module.params['group']
|
||||
self.state = module.params['state']
|
||||
self.noexec = module.params['noexec']
|
||||
self.nopassword = module.params['nopassword']
|
||||
self.setenv = module.params['setenv']
|
||||
self.host = module.params['host']
|
||||
@@ -205,13 +221,15 @@ class Sudoers(object):
|
||||
owner = '%{group}'.format(group=self.group)
|
||||
|
||||
commands_str = ', '.join(self.commands)
|
||||
noexec_str = 'NOEXEC:' if self.noexec else ''
|
||||
nopasswd_str = 'NOPASSWD:' if self.nopassword else ''
|
||||
setenv_str = 'SETENV:' if self.setenv else ''
|
||||
runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else ''
|
||||
return "{owner} {host}={runas}{nopasswd}{setenv} {commands}\n".format(
|
||||
return "{owner} {host}={runas}{noexec}{nopasswd}{setenv} {commands}\n".format(
|
||||
owner=owner,
|
||||
host=self.host,
|
||||
runas=runas_str,
|
||||
noexec=noexec_str,
|
||||
nopasswd=nopasswd_str,
|
||||
setenv=setenv_str,
|
||||
commands=commands_str
|
||||
@@ -258,6 +276,10 @@ def main():
|
||||
'name': {
|
||||
'required': True,
|
||||
},
|
||||
'noexec': {
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
'nopassword': {
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
|
||||
@@ -21,7 +21,8 @@ attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
support: full
|
||||
version_added: 8.3.0
|
||||
options:
|
||||
state:
|
||||
choices: ['planned', 'present', 'absent']
|
||||
@@ -375,7 +376,7 @@ def remove_workspace(bin_path, project_path, workspace):
|
||||
_workspace_cmd(bin_path, project_path, 'delete', workspace)
|
||||
|
||||
|
||||
def build_plan(command, project_path, variables_args, state_file, targets, state, apply_args, plan_path=None):
|
||||
def build_plan(command, project_path, variables_args, state_file, targets, state, args, plan_path=None):
|
||||
if plan_path is None:
|
||||
f, plan_path = tempfile.mkstemp(suffix='.tfplan')
|
||||
|
||||
@@ -388,11 +389,15 @@ def build_plan(command, project_path, variables_args, state_file, targets, state
|
||||
plan_command.append(c)
|
||||
|
||||
if state == "present":
|
||||
for a in apply_args:
|
||||
for a in args:
|
||||
local_command.remove(a)
|
||||
for c in local_command[1:]:
|
||||
plan_command.append(c)
|
||||
|
||||
if state == "absent":
|
||||
for a in args:
|
||||
plan_command.append(a)
|
||||
|
||||
plan_command.extend(['-input=false', '-no-color', '-detailed-exitcode', '-out', plan_path])
|
||||
|
||||
for t in targets:
|
||||
@@ -428,6 +433,49 @@ def build_plan(command, project_path, variables_args, state_file, targets, state
|
||||
))
|
||||
|
||||
|
||||
def get_diff(diff_output):
|
||||
def get_tf_resource_address(e):
|
||||
return e['resource']
|
||||
|
||||
diff_json_output = json.loads(diff_output)
|
||||
|
||||
# Ignore diff if resource_changes does not exists in tfplan
|
||||
if 'resource_changes' in diff_json_output:
|
||||
tf_reosource_changes = diff_json_output['resource_changes']
|
||||
else:
|
||||
module.warn("Cannot find resource_changes in terraform plan, diff/check ignored")
|
||||
return False, {}
|
||||
|
||||
diff_after = []
|
||||
diff_before = []
|
||||
changed = False
|
||||
for item in tf_reosource_changes:
|
||||
item_change = item['change']
|
||||
tf_before_state = {'resource': item['address'], 'change': item['change']['before']}
|
||||
tf_after_state = {'resource': item['address'], 'change': item['change']['after']}
|
||||
|
||||
if item_change['actions'] == ['update'] or item_change['actions'] == ['delete', 'create']:
|
||||
diff_before.append(tf_before_state)
|
||||
diff_after.append(tf_after_state)
|
||||
changed = True
|
||||
|
||||
if item_change['actions'] == ['delete']:
|
||||
diff_before.append(tf_before_state)
|
||||
changed = True
|
||||
|
||||
if item_change['actions'] == ['create']:
|
||||
diff_after.append(tf_after_state)
|
||||
changed = True
|
||||
|
||||
diff_before.sort(key=get_tf_resource_address)
|
||||
diff_after.sort(key=get_tf_resource_address)
|
||||
|
||||
return changed, dict(
|
||||
before=({'data': diff_before}),
|
||||
after=({'data': diff_after}),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
@@ -619,6 +667,23 @@ def main():
|
||||
"Consider switching the 'check_destroy' to false to suppress this error")
|
||||
command.append(plan_file)
|
||||
|
||||
result_diff = dict()
|
||||
if module._diff or module.check_mode:
|
||||
if state == 'absent':
|
||||
plan_absent_args = ['-destroy']
|
||||
plan_file, needs_application, out, err, command = build_plan(command, project_path, variables_args, state_file,
|
||||
module.params.get('targets'), state, plan_absent_args, plan_file)
|
||||
diff_command = [command[0], 'show', '-json', plan_file]
|
||||
rc, diff_output, err = module.run_command(diff_command, check_rc=False, cwd=project_path)
|
||||
changed, result_diff = get_diff(diff_output)
|
||||
if rc != 0:
|
||||
if workspace_ctx["current"] != workspace:
|
||||
select_workspace(command[0], project_path, workspace_ctx["current"])
|
||||
module.fail_json(msg=err.rstrip(), rc=rc, stdout=out,
|
||||
stdout_lines=out.splitlines(), stderr=err,
|
||||
stderr_lines=err.splitlines(),
|
||||
cmd=' '.join(command))
|
||||
|
||||
if needs_application and not module.check_mode and state != 'planned':
|
||||
rc, out, err = module.run_command(command, check_rc=False, cwd=project_path)
|
||||
if rc != 0:
|
||||
@@ -651,7 +716,18 @@ def main():
|
||||
if state == 'absent' and workspace != 'default' and purge_workspace is True:
|
||||
remove_workspace(command[0], project_path, workspace)
|
||||
|
||||
module.exit_json(changed=changed, state=state, workspace=workspace, outputs=outputs, stdout=out, stderr=err, command=' '.join(command))
|
||||
result = {
|
||||
'state': state,
|
||||
'workspace': workspace,
|
||||
'outputs': outputs,
|
||||
'stdout': out,
|
||||
'stderr': err,
|
||||
'command': ' '.join(command),
|
||||
'changed': changed,
|
||||
'diff': result_diff,
|
||||
}
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
8
tests/galaxy-importer.cfg
Normal file
8
tests/galaxy-importer.cfg
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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
|
||||
|
||||
[galaxy-importer]
|
||||
# This is only needed to make Zuul's third-party-check happy.
|
||||
# It is not needed by anything else.
|
||||
run_ansible_doc=false
|
||||
13
tests/integration/targets/apk/aliases
Normal file
13
tests/integration/targets/apk/aliases
Normal file
@@ -0,0 +1,13 @@
|
||||
# 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
|
||||
needs/root
|
||||
destructive
|
||||
skip/aix
|
||||
skip/osx
|
||||
skip/macos
|
||||
skip/freebsd
|
||||
skip/rhel
|
||||
skip/ubuntu
|
||||
160
tests/integration/targets/apk/tasks/main.yml
Normal file
160
tests/integration/targets/apk/tasks/main.yml
Normal file
@@ -0,0 +1,160 @@
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2024, Max Maxopoly <max@dermax.org>
|
||||
# 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: Run apk tests on Alpine
|
||||
when: ansible_distribution in ['Alpine']
|
||||
block:
|
||||
- name: Ensure vim is not installed
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: absent
|
||||
|
||||
- name: Install vim
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- name: Ensure vim was installed
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
- (results.packages | length) >= 1 # vim has dependencies, so depending on the base image this number may vary
|
||||
|
||||
- name: Install vim again
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- name: Ensure vim was not installed again
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- (results.packages | default([]) | length) == 0
|
||||
|
||||
- name: Ensure vim is not installed
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: absent
|
||||
register: results
|
||||
|
||||
- name: Ensure vim was uninstalled
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
- (results.packages | length) >= 1
|
||||
|
||||
- name: Install vim without cache
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: present
|
||||
no_cache: true
|
||||
register: results
|
||||
|
||||
- name: Ensure vim was installed without cache
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
|
||||
- name: Install vim again without cache
|
||||
community.general.apk:
|
||||
name: vim
|
||||
state: present
|
||||
no_cache: true
|
||||
register: results
|
||||
|
||||
- name: Ensure vim was not installed again without cache
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- (results.packages | default([]) | length) == 0
|
||||
|
||||
- name: Ensure a bunch of packages aren't installed
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: absent
|
||||
|
||||
- name: Install a bunch of packages
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- name: Ensure a bunch of packages were installed
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
- (results.packages | length) >= 3
|
||||
|
||||
- name: Install a bunch of packages again
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: present
|
||||
register: results
|
||||
|
||||
- name: Ensure a bunch of packages were not installed again
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- (results.packages | default([]) | length) == 0
|
||||
|
||||
- name: Ensure a bunch of packages are not installed
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: absent
|
||||
register: results
|
||||
|
||||
- name: Ensure a bunch of packages were uninstalled
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
- (results.packages | length) >= 3
|
||||
|
||||
- name: Install a bunch of packages without cache
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: present
|
||||
no_cache: true
|
||||
register: results
|
||||
|
||||
- name: Ensure a bunch of packages were installed without cache
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is changed
|
||||
|
||||
- name: Install a bunch of packages again without cache
|
||||
community.general.apk:
|
||||
name:
|
||||
- less
|
||||
- nano
|
||||
- vim
|
||||
state: present
|
||||
no_cache: true
|
||||
register: results
|
||||
|
||||
- name: Ensure a bunch of packages were not installed again without cache
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- results is not changed
|
||||
- (results.packages | default([]) | length) == 0
|
||||
@@ -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
|
||||
|
||||
azp/posix/3
|
||||
needs/target/callback
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# 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
|
||||
|
||||
- block:
|
||||
- name: Create temporary file
|
||||
tempfile:
|
||||
register: tempfile
|
||||
|
||||
- name: Run tests
|
||||
include_role:
|
||||
name: callback
|
||||
vars:
|
||||
tests:
|
||||
- name: Basic file diff
|
||||
environment:
|
||||
ANSIBLE_NOCOLOR: 'true'
|
||||
ANSIBLE_FORCE_COLOR: 'false'
|
||||
ANSIBLE_DIFF_ALWAYS: 'true'
|
||||
ANSIBLE_PYTHON_INTERPRETER: "{{ ansible_python_interpreter }}"
|
||||
ANSIBLE_STDOUT_CALLBACK: community.general.default_without_diff
|
||||
playbook: |
|
||||
- hosts: testhost
|
||||
gather_facts: true
|
||||
tasks:
|
||||
- name: Create file
|
||||
copy:
|
||||
dest: "{{ tempfile.path }}"
|
||||
content: |
|
||||
Foo bar
|
||||
|
||||
- name: Modify file
|
||||
copy:
|
||||
dest: "{{ tempfile.path }}"
|
||||
content: |
|
||||
Foo bar
|
||||
Bar baz bam!
|
||||
expected_output: [
|
||||
"",
|
||||
"PLAY [testhost] ****************************************************************",
|
||||
"",
|
||||
"TASK [Gathering Facts] *********************************************************",
|
||||
"ok: [testhost]",
|
||||
"",
|
||||
"TASK [Create file] *************************************************************",
|
||||
"changed: [testhost]",
|
||||
"",
|
||||
"TASK [Modify file] *************************************************************",
|
||||
"changed: [testhost]",
|
||||
"",
|
||||
"PLAY RECAP *********************************************************************",
|
||||
"testhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ",
|
||||
]
|
||||
|
||||
always:
|
||||
- name: Clean up temp file
|
||||
file:
|
||||
path: "{{ tempfile.path }}"
|
||||
state: absent
|
||||
@@ -7,7 +7,8 @@
|
||||
ansible.builtin.copy:
|
||||
src: /bin/echo
|
||||
dest: "{{ item.copy_to }}/echo"
|
||||
mode: "755"
|
||||
mode: "0755"
|
||||
remote_src: true
|
||||
when: item.copy_to is defined
|
||||
|
||||
- name: test cmd_echo module ({{ item.name }})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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 an auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
type: jwt
|
||||
config:
|
||||
jwt_validation_pubkeys:
|
||||
- |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
|
||||
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
|
||||
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
|
||||
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
|
||||
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
|
||||
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.auth_method.Type == 'jwt'
|
||||
- result.operation == 'create'
|
||||
|
||||
- name: Update auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
max_token_ttl: 30m80s
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.auth_method.Type == 'jwt'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update auth method (noop)
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
max_token_ttl: 30m80s
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.auth_method.Type == 'jwt'
|
||||
- result.operation is not defined
|
||||
|
||||
- name: Delete auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
||||
|
||||
- name: Delete auth method (noop)
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.operation is not defined
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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 an auth method
|
||||
community.general.consul_auth_method:
|
||||
name: test
|
||||
type: jwt
|
||||
config:
|
||||
jwt_validation_pubkeys:
|
||||
- |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
|
||||
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
|
||||
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
|
||||
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
|
||||
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
|
||||
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
|
||||
- name: Create a binding rule
|
||||
community.general.consul_binding_rule:
|
||||
name: test-binding
|
||||
description: my description
|
||||
auth_method: test
|
||||
bind_type: service
|
||||
bind_name: yolo
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.binding_rule.AuthMethod == 'test'
|
||||
- result.binding.Description == 'test-binding: my description'
|
||||
- result.operation == 'create'
|
||||
|
||||
- name: Update a binding rule
|
||||
community.general.consul_binding_rule:
|
||||
name: test-binding
|
||||
auth_method: test
|
||||
bind_name: yolo2
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.binding.Description == 'test-binding: my description'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update a binding rule (noop)
|
||||
community.general.consul_binding_rule:
|
||||
name: test-binding
|
||||
auth_method: test
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.binding.Description == 'test-binding: my description'
|
||||
- result.operation is not defined
|
||||
|
||||
- name: Delete a binding rule
|
||||
community.general.consul_binding_rule:
|
||||
name: test-binding
|
||||
auth_method: test
|
||||
state: absent
|
||||
register: result
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
||||
76
tests/integration/targets/consul/tasks/consul_general.yml
Normal file
76
tests/integration/targets/consul/tasks/consul_general.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
# 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: ensure unknown scheme fails
|
||||
consul_session:
|
||||
state: info
|
||||
id: dummy
|
||||
scheme: non_existent
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
|
||||
- name: ensure SSL certificate is checked
|
||||
consul_session:
|
||||
state: info
|
||||
id: dummy
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- name: previous task should fail since certificate is not known
|
||||
assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'certificate verify failed' in result.msg"
|
||||
|
||||
- name: ensure SSL certificate isn't checked when validate_certs is disabled
|
||||
consul_session:
|
||||
state: info
|
||||
id: dummy
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
validate_certs: false
|
||||
register: result
|
||||
|
||||
- name: previous task should succeed since certificate isn't checked
|
||||
assert:
|
||||
that:
|
||||
- result is changed
|
||||
|
||||
- name: ensure a secure connection is possible
|
||||
consul_session:
|
||||
state: info
|
||||
id: dummy
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
ca_path: '{{ remote_dir }}/cert.pem'
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
|
||||
- name: ensure connection errors are handled properly
|
||||
consul_session:
|
||||
state: info
|
||||
id: dummy
|
||||
token: "{{ consul_management_token }}"
|
||||
port: 1234
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- result.msg.startswith('Could not connect to consul agent at localhost:1234, error was')
|
||||
57
tests/integration/targets/consul/tasks/consul_kv.yml
Normal file
57
tests/integration/targets/consul/tasks/consul_kv.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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 key
|
||||
consul_kv:
|
||||
key: somekey
|
||||
value: somevalue
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.data.Value == 'somevalue'
|
||||
|
||||
#- name: Test the lookup
|
||||
# assert:
|
||||
# that:
|
||||
# - lookup('community.general.consul_kv', 'somekey', token=consul_management_token) == 'somevalue'
|
||||
|
||||
- name: Update a key with the same data
|
||||
consul_kv:
|
||||
key: somekey
|
||||
value: somevalue
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.data.Value == 'somevalue'
|
||||
|
||||
- name: Remove a key from the store
|
||||
consul_kv:
|
||||
key: somekey
|
||||
state: absent
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.data.Value == 'somevalue'
|
||||
|
||||
- name: Remove a non-existant key from the store
|
||||
consul_kv:
|
||||
key: somekey
|
||||
state: absent
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- not result.data
|
||||
@@ -13,13 +13,14 @@
|
||||
key "private/foo" {
|
||||
policy = "deny"
|
||||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['policy']['Name'] == 'foo-access'
|
||||
- result.policy.Name == 'foo-access'
|
||||
- result.operation == 'create'
|
||||
|
||||
- name: Update the rules associated to a policy
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
@@ -33,11 +34,13 @@
|
||||
event "bbq" {
|
||||
policy = "write"
|
||||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update reports not changed when updating again without changes
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
@@ -51,17 +54,19 @@
|
||||
event "bbq" {
|
||||
policy = "write"
|
||||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.operation is not defined
|
||||
|
||||
- name: Remove a policy
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
token: "{{ consul_management_token }}"
|
||||
state: absent
|
||||
register: result
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
||||
@@ -13,7 +13,6 @@
|
||||
key "private/foo" {
|
||||
policy = "deny"
|
||||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: policy_result
|
||||
|
||||
- name: Create another policy with rules
|
||||
@@ -26,7 +25,6 @@
|
||||
key "private/bar" {
|
||||
policy = "deny"
|
||||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: policy_result
|
||||
|
||||
- name: Create a role with policy
|
||||
@@ -34,40 +32,40 @@
|
||||
name: foo-role-with-policy
|
||||
policies:
|
||||
- name: "foo-access-for-role"
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Name'] == 'foo-role-with-policy'
|
||||
- result.role.Name == 'foo-role-with-policy'
|
||||
- result.operation == 'create'
|
||||
|
||||
- name: Update policy description, in check mode
|
||||
consul_role:
|
||||
name: foo-role-with-policy
|
||||
description: "Testing updating description"
|
||||
token: "{{ consul_management_token }}"
|
||||
check_mode: yes
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Description'] == "Testing updating description"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.Description == "Testing updating description"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update policy to add the description
|
||||
consul_role:
|
||||
name: foo-role-with-policy
|
||||
description: "Role for testing policies"
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Description'] == "Role for testing policies"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.Description == "Role for testing policies"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update the role with another policy, also testing leaving description blank
|
||||
consul_role:
|
||||
@@ -75,19 +73,18 @@
|
||||
policies:
|
||||
- name: "foo-access-for-role"
|
||||
- name: "bar-access-for-role"
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result['role']['Policies'][1]['Name'] == 'bar-access-for-role'
|
||||
- result['role']['Description'] == "Role for testing policies"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.role.Policies.1.Name == 'bar-access-for-role'
|
||||
- result.role.Description == "Role for testing policies"
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Create a role with service identity
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
service_identities:
|
||||
- name: web
|
||||
@@ -98,12 +95,11 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
|
||||
- name: Update the role with service identity in check mode
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
service_identities:
|
||||
- name: web
|
||||
@@ -115,12 +111,11 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc2"
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc2"
|
||||
|
||||
- name: Update the role with service identity to add a policy, leaving the service id unchanged
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
policies:
|
||||
- name: "foo-access-for-role"
|
||||
@@ -129,13 +124,12 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
|
||||
- name: Update the role with service identity to remove the policies
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
policies: []
|
||||
register: result
|
||||
@@ -143,13 +137,12 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'] is not defined
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies is not defined
|
||||
|
||||
- name: Update the role with service identity to remove the node identities, in check mode
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
node_identities: []
|
||||
register: result
|
||||
@@ -158,14 +151,13 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'] is not defined
|
||||
- result['role']['NodeIdentities'] == [] # in check mode the cleared field is returned as an empty array
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies is not defined
|
||||
- result.role.NodeIdentities == [] # in check mode the cleared field is returned as an empty array
|
||||
|
||||
- name: Update the role with service identity to remove the service identities
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-service-identity
|
||||
service_identities: []
|
||||
register: result
|
||||
@@ -173,12 +165,11 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'] is not defined # in normal mode the dictionary is removed from the result
|
||||
- result['role']['Policies'] is not defined
|
||||
- result.role.ServiceIdentities is not defined # in normal mode the dictionary is removed from the result
|
||||
- result.role.Policies is not defined
|
||||
|
||||
- name: Create a role with node identity
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-node-identity
|
||||
node_identities:
|
||||
- name: node-1
|
||||
@@ -188,14 +179,16 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['NodeIdentities'][0]['NodeName'] == "node-1"
|
||||
- result['role']['NodeIdentities'][0]['Datacenter'] == "dc2"
|
||||
- result.role.NodeIdentities.0.NodeName == "node-1"
|
||||
- result.role.NodeIdentities.0.Datacenter == "dc2"
|
||||
|
||||
- name: Remove the last role
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-node-identity
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
||||
@@ -6,7 +6,6 @@
|
||||
- name: list sessions
|
||||
consul_session:
|
||||
state: list
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
@@ -18,7 +17,6 @@
|
||||
consul_session:
|
||||
state: present
|
||||
name: testsession
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
@@ -33,7 +31,6 @@
|
||||
- name: list sessions after creation
|
||||
consul_session:
|
||||
state: list
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- set_fact:
|
||||
@@ -61,7 +58,6 @@
|
||||
consul_session:
|
||||
state: info
|
||||
id: '{{ session_id }}'
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
@@ -72,7 +68,6 @@
|
||||
consul_session:
|
||||
state: info
|
||||
name: test
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
@@ -80,70 +75,10 @@
|
||||
that:
|
||||
- result is failed
|
||||
|
||||
- name: ensure unknown scheme fails
|
||||
consul_session:
|
||||
state: info
|
||||
id: '{{ session_id }}'
|
||||
scheme: non_existent
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
|
||||
- name: ensure SSL certificate is checked
|
||||
consul_session:
|
||||
state: info
|
||||
id: '{{ session_id }}'
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- name: previous task should fail since certificate is not known
|
||||
assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'certificate verify failed' in result.msg"
|
||||
|
||||
- name: ensure SSL certificate isn't checked when validate_certs is disabled
|
||||
consul_session:
|
||||
state: info
|
||||
id: '{{ session_id }}'
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
validate_certs: false
|
||||
register: result
|
||||
|
||||
- name: previous task should succeed since certificate isn't checked
|
||||
assert:
|
||||
that:
|
||||
- result is changed
|
||||
|
||||
- name: ensure a secure connection is possible
|
||||
consul_session:
|
||||
state: info
|
||||
id: '{{ session_id }}'
|
||||
port: 8501
|
||||
scheme: https
|
||||
token: "{{ consul_management_token }}"
|
||||
environment:
|
||||
REQUESTS_CA_BUNDLE: '{{ remote_dir }}/cert.pem'
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
|
||||
- name: delete a session
|
||||
consul_session:
|
||||
state: absent
|
||||
id: '{{ session_id }}'
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
@@ -153,7 +88,6 @@
|
||||
- name: list sessions after deletion
|
||||
consul_session:
|
||||
state: list
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
@@ -180,7 +114,6 @@
|
||||
state: present
|
||||
name: session-with-ttl
|
||||
ttl: 180 # sec
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
|
||||
77
tests/integration/targets/consul/tasks/consul_token.yml
Normal file
77
tests/integration/targets/consul/tasks/consul_token.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
# Copyright (c) 2024, Florian Apolloner (@apollo13)
|
||||
# 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 policy with rules
|
||||
community.general.consul_policy:
|
||||
name: "{{ item }}"
|
||||
rules: |
|
||||
key "foo" {
|
||||
policy = "read"
|
||||
}
|
||||
loop:
|
||||
- foo-access
|
||||
- foo-access2
|
||||
|
||||
- name: Create token
|
||||
community.general.consul_token:
|
||||
state: present
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
service_identities:
|
||||
- service_name: test
|
||||
datacenters: [test1, test2]
|
||||
node_identities:
|
||||
- node_name: test
|
||||
datacenter: test
|
||||
policies:
|
||||
- name: foo-access
|
||||
- name: foo-access2
|
||||
expiration_ttl: 1h
|
||||
register: create_result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- create_result is changed
|
||||
- create_result.token.AccessorID == "07a7de84-c9c7-448a-99cc-beaf682efd21"
|
||||
- create_result.operation == 'create'
|
||||
|
||||
- name: Update token
|
||||
community.general.consul_token:
|
||||
state: present
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
description: Testing
|
||||
policies:
|
||||
- id: "{{ create_result.token.Policies[-1].ID }}"
|
||||
service_identities: []
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update token (noop)
|
||||
community.general.consul_token:
|
||||
state: present
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
policies:
|
||||
- id: "{{ create_result.token.Policies[-1].ID }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.operation is not defined
|
||||
|
||||
- name: Remove token
|
||||
community.general.consul_token:
|
||||
state: absent
|
||||
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- not result.token
|
||||
- result.operation == 'remove'
|
||||
@@ -77,21 +77,30 @@
|
||||
- name: Start Consul (dev mode enabled)
|
||||
shell: nohup {{ consul_cmd }} agent -dev -config-file {{ remote_tmp_dir }}/consul_config.hcl </dev/null >/dev/null 2>&1 &
|
||||
- name: Bootstrap ACL
|
||||
command: '{{ consul_cmd }} acl bootstrap --format=json'
|
||||
register: consul_bootstrap_result_string
|
||||
consul_acl_bootstrap:
|
||||
register: consul_bootstrap_result
|
||||
- set_fact:
|
||||
consul_management_token: '{{ consul_bootstrap_json_result["SecretID"] }}'
|
||||
vars:
|
||||
consul_bootstrap_json_result: '{{ consul_bootstrap_result_string.stdout | from_json }}'
|
||||
consul_management_token: '{{ consul_bootstrap_result.result.SecretID }}'
|
||||
- name: Create some data
|
||||
command: '{{ consul_cmd }} kv put -token={{consul_management_token}} data/value{{ item }} foo{{ item }}'
|
||||
loop:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- import_tasks: consul_session.yml
|
||||
- import_tasks: consul_policy.yml
|
||||
- import_tasks: consul_role.yml
|
||||
- import_tasks: consul_general.yml
|
||||
- import_tasks: consul_kv.yml
|
||||
|
||||
- block:
|
||||
- import_tasks: consul_session.yml
|
||||
- import_tasks: consul_policy.yml
|
||||
- import_tasks: consul_role.yml
|
||||
- import_tasks: consul_token.yml
|
||||
- import_tasks: consul_auth_method.yml
|
||||
- import_tasks: consul_binding_rule.yml
|
||||
module_defaults:
|
||||
group/community.general.consul:
|
||||
token: "{{ consul_management_token }}"
|
||||
|
||||
always:
|
||||
- name: Kill consul process
|
||||
shell: kill $(cat {{ remote_tmp_dir }}/consul.pid)
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
# Test module with sparse files
|
||||
|
||||
- name: Create a huge sparse file of 4TB (check mode)
|
||||
- name: Create a huge sparse file of 2TB (check mode)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4TB
|
||||
size: 2TB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_01
|
||||
check_mode: true
|
||||
@@ -20,10 +20,10 @@
|
||||
register: filesize_stat_sparse_01
|
||||
|
||||
|
||||
- name: Create a huge sparse file of 4TB
|
||||
- name: Create a huge sparse file of 2TB
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4TB
|
||||
size: 2TB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_02
|
||||
|
||||
@@ -34,34 +34,34 @@
|
||||
register: filesize_stat_sparse_02
|
||||
|
||||
|
||||
- name: Create a huge sparse file of 4TB (4000GB) (check mode, idempotency)
|
||||
- name: Create a huge sparse file of 2TB (2000GB) (check mode, idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4000GB
|
||||
size: 2000GB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_03
|
||||
check_mode: true
|
||||
|
||||
- name: Create a huge sparse file of 4TB (4000GB) (idempotency)
|
||||
- name: Create a huge sparse file of 2TB (2000GB) (idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4000GB
|
||||
size: 2000GB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_04
|
||||
|
||||
- name: Create a huge sparse file of 4TB (4000000 × 1MB) (check mode, idempotency)
|
||||
- name: Create a huge sparse file of 2TB (2000000 × 1MB) (check mode, idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4000000
|
||||
size: 2000000
|
||||
blocksize: 1MB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_05
|
||||
check_mode: true
|
||||
|
||||
- name: Create a huge sparse file of 4TB (4000000 × 1MB) (idempotency)
|
||||
- name: Create a huge sparse file of 2TB (2000000 × 1MB) (idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4000000
|
||||
size: 2000000
|
||||
blocksize: 1MB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_06
|
||||
@@ -89,15 +89,15 @@
|
||||
- filesize_test_sparse_05.cmd is undefined
|
||||
- filesize_test_sparse_06.cmd is undefined
|
||||
|
||||
- filesize_test_sparse_01.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_02.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_03.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_04.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_05.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_06.filesize.bytes == 4*1000**4
|
||||
- filesize_test_sparse_01.filesize.bytes == 2*1000**4
|
||||
- filesize_test_sparse_02.filesize.bytes == 2*1000**4
|
||||
- filesize_test_sparse_03.filesize.bytes == 2*1000**4
|
||||
- filesize_test_sparse_04.filesize.bytes == 2*1000**4
|
||||
- filesize_test_sparse_05.filesize.bytes == 2*1000**4
|
||||
- filesize_test_sparse_06.filesize.bytes == 2*1000**4
|
||||
|
||||
- filesize_test_sparse_01.size_diff == 4*1000**4
|
||||
- filesize_test_sparse_02.size_diff == 4*1000**4
|
||||
- filesize_test_sparse_01.size_diff == 2*1000**4
|
||||
- filesize_test_sparse_02.size_diff == 2*1000**4
|
||||
- filesize_test_sparse_03.size_diff == 0
|
||||
- filesize_test_sparse_04.size_diff == 0
|
||||
- filesize_test_sparse_05.size_diff == 0
|
||||
@@ -106,24 +106,24 @@
|
||||
- filesize_test_sparse_01.state is undefined
|
||||
- filesize_test_sparse_02.state in ["file"]
|
||||
- filesize_test_sparse_01.size is undefined
|
||||
- filesize_test_sparse_02.size == 4*1000**4
|
||||
- filesize_test_sparse_03.size == 4*1000**4
|
||||
- filesize_test_sparse_04.size == 4*1000**4
|
||||
- filesize_test_sparse_05.size == 4*1000**4
|
||||
- filesize_test_sparse_06.size == 4*1000**4
|
||||
- filesize_test_sparse_02.size == 2*1000**4
|
||||
- filesize_test_sparse_03.size == 2*1000**4
|
||||
- filesize_test_sparse_04.size == 2*1000**4
|
||||
- filesize_test_sparse_05.size == 2*1000**4
|
||||
- filesize_test_sparse_06.size == 2*1000**4
|
||||
|
||||
- not filesize_stat_sparse_01.stat.exists
|
||||
- filesize_stat_sparse_02.stat.exists
|
||||
- filesize_stat_sparse_02.stat.isreg
|
||||
- filesize_stat_sparse_02.stat.size == 4*1000**4
|
||||
- filesize_stat_sparse_06.stat.size == 4*1000**4
|
||||
- filesize_stat_sparse_02.stat.size == 2*1000**4
|
||||
- filesize_stat_sparse_06.stat.size == 2*1000**4
|
||||
|
||||
|
||||
|
||||
- name: Change sparse file size to 4TiB (check mode)
|
||||
- name: Change sparse file size to 2TiB (check mode)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4TiB
|
||||
size: 2TiB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_11
|
||||
check_mode: true
|
||||
@@ -135,10 +135,10 @@
|
||||
register: filesize_stat_sparse_11
|
||||
|
||||
|
||||
- name: Change sparse file size to 4TiB
|
||||
- name: Change sparse file size to 2TiB
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4TiB
|
||||
size: 2TiB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_12
|
||||
|
||||
@@ -149,18 +149,18 @@
|
||||
register: filesize_stat_sparse_12
|
||||
|
||||
|
||||
- name: Change sparse file size to 4TiB (4096GiB) (check mode, idempotency)
|
||||
- name: Change sparse file size to 2TiB (2048GiB) (check mode, idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4096GiB
|
||||
size: 2048GiB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_13
|
||||
check_mode: true
|
||||
|
||||
- name: Change sparse file size to 4TiB (4096GiB) (idempotency)
|
||||
- name: Change sparse file size to 2TiB (2048GiB) (idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4096GiB
|
||||
size: 2048GiB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_14
|
||||
|
||||
@@ -183,26 +183,26 @@
|
||||
- filesize_test_sparse_13.cmd is undefined
|
||||
- filesize_test_sparse_14.cmd is undefined
|
||||
|
||||
- filesize_test_sparse_11.size_diff == 398046511104
|
||||
- filesize_test_sparse_12.size_diff == 398046511104
|
||||
- filesize_test_sparse_11.size_diff == 199023255552
|
||||
- filesize_test_sparse_12.size_diff == 199023255552
|
||||
- filesize_test_sparse_13.size_diff == 0
|
||||
- filesize_test_sparse_14.size_diff == 0
|
||||
|
||||
- filesize_test_sparse_11.size == 4000000000000
|
||||
- filesize_test_sparse_12.size == 4398046511104
|
||||
- filesize_test_sparse_13.size == 4398046511104
|
||||
- filesize_test_sparse_14.size == 4398046511104
|
||||
- filesize_test_sparse_11.size == 2000000000000
|
||||
- filesize_test_sparse_12.size == 2199023255552
|
||||
- filesize_test_sparse_13.size == 2199023255552
|
||||
- filesize_test_sparse_14.size == 2199023255552
|
||||
|
||||
- filesize_stat_sparse_11.stat.size == 4000000000000
|
||||
- filesize_stat_sparse_12.stat.size == 4398046511104
|
||||
- filesize_stat_sparse_14.stat.size == 4398046511104
|
||||
- filesize_stat_sparse_11.stat.size == 2000000000000
|
||||
- filesize_stat_sparse_12.stat.size == 2199023255552
|
||||
- filesize_stat_sparse_14.stat.size == 2199023255552
|
||||
|
||||
|
||||
|
||||
- name: Change sparse file size to 4.321TB (check mode)
|
||||
- name: Change sparse file size to 2.321TB (check mode)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4.321TB
|
||||
size: 2.321TB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_21
|
||||
check_mode: true
|
||||
@@ -214,10 +214,10 @@
|
||||
register: filesize_stat_sparse_21
|
||||
|
||||
|
||||
- name: Change sparse file size to 4.321TB
|
||||
- name: Change sparse file size to 2.321TB
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4.321TB
|
||||
size: 2.321TB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_22
|
||||
|
||||
@@ -228,19 +228,19 @@
|
||||
register: filesize_stat_sparse_22
|
||||
|
||||
|
||||
- name: Change sparse file size to 4321×1GB (check mode, idempotency)
|
||||
- name: Change sparse file size to 2321×1GB (check mode, idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4321
|
||||
size: 2321
|
||||
blocksize: 1GB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_23
|
||||
check_mode: true
|
||||
|
||||
- name: Change sparse file size to 4321×1GB (idempotency)
|
||||
- name: Change sparse file size to 2321×1GB (idempotency)
|
||||
community.general.filesize:
|
||||
path: "{{ filesize_testfile }}"
|
||||
size: 4321
|
||||
size: 2321
|
||||
blocksize: 1GB
|
||||
sparse: true
|
||||
register: filesize_test_sparse_24
|
||||
@@ -264,19 +264,19 @@
|
||||
- filesize_test_sparse_23.cmd is undefined
|
||||
- filesize_test_sparse_24.cmd is undefined
|
||||
|
||||
- filesize_test_sparse_21.size_diff == 4321*1000**3 - 4*1024**4
|
||||
- filesize_test_sparse_22.size_diff == 4321*1000**3 - 4*1024**4
|
||||
- filesize_test_sparse_21.size_diff == 2321*1000**3 - 2*1024**4
|
||||
- filesize_test_sparse_22.size_diff == 2321*1000**3 - 2*1024**4
|
||||
- filesize_test_sparse_23.size_diff == 0
|
||||
- filesize_test_sparse_24.size_diff == 0
|
||||
|
||||
- filesize_test_sparse_21.size == 4398046511104
|
||||
- filesize_test_sparse_22.size == 4321000000000
|
||||
- filesize_test_sparse_23.size == 4321000000000
|
||||
- filesize_test_sparse_24.size == 4321000000000
|
||||
- filesize_test_sparse_21.size == 2199023255552
|
||||
- filesize_test_sparse_22.size == 2321000000000
|
||||
- filesize_test_sparse_23.size == 2321000000000
|
||||
- filesize_test_sparse_24.size == 2321000000000
|
||||
|
||||
- filesize_stat_sparse_21.stat.size == 4398046511104
|
||||
- filesize_stat_sparse_22.stat.size == 4321000000000
|
||||
- filesize_stat_sparse_24.stat.size == 4321000000000
|
||||
- filesize_stat_sparse_21.stat.size == 2199023255552
|
||||
- filesize_stat_sparse_22.stat.size == 2321000000000
|
||||
- filesize_stat_sparse_24.stat.size == 2321000000000
|
||||
|
||||
|
||||
|
||||
|
||||
5
tests/integration/targets/filter_lists/aliases
Normal file
5
tests/integration/targets/filter_lists/aliases
Normal file
@@ -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
|
||||
64
tests/integration/targets/filter_lists/tasks/main.yml
Normal file
64
tests/integration/targets/filter_lists/tasks/main.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
# 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 predictive list union
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 'list1 | community.general.lists_union(list2, list3) == [1, 2, 5, 3, 4, 10, 11, 99, 101]'
|
||||
- '[list1, list2, list3] | community.general.lists_union(flatten=True) == [1, 2, 5, 3, 4, 10, 11, 99, 101]'
|
||||
- '[1, 2, 3] | community.general.lists_union([4, 5, 6]) == [1, 2, 3, 4, 5, 6]'
|
||||
- '[1, 2, 3] | community.general.lists_union([3, 4, 5, 6]) == [1, 2, 3, 4, 5, 6]'
|
||||
- '[1, 2, 3] | community.general.lists_union([3, 2, 1]) == [1, 2, 3]'
|
||||
- '["a", "A", "b"] | community.general.lists_union(["B", "c", "C"]) == ["a", "A", "b", "B", "c", "C"]'
|
||||
- '["a", "A", "b"] | community.general.lists_union(["b", "B", "c", "C"]) == ["a", "A", "b", "B", "c", "C"]'
|
||||
- '["a", "A", "b"] | community.general.lists_union(["b", "A", "a"]) == ["a", "A", "b"]'
|
||||
- '[["a"]] | community.general.lists_union([["b"], ["a"]]) == [["a"], ["b"]]'
|
||||
- '[["a"]] | community.general.lists_union([["b"]], ["a"]) == [["a"], ["b"], "a"]'
|
||||
- '[["a"]] | community.general.lists_union(["b"], ["a"]) == [["a"], "b", "a"]'
|
||||
|
||||
- name: Test predictive list intersection
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 'list1 | community.general.lists_intersect(list2, list3) == [1, 2, 5, 4]'
|
||||
- '[list1, list2, list3] | community.general.lists_intersect(flatten=True) == [1, 2, 5, 4]'
|
||||
- '[1, 2, 3] | community.general.lists_intersect([4, 5, 6]) == []'
|
||||
- '[1, 2, 3] | community.general.lists_intersect([3, 4, 5, 6]) == [3]'
|
||||
- '[1, 2, 3] | community.general.lists_intersect([3, 2, 1]) == [1, 2, 3]'
|
||||
- '["a", "A", "b"] | community.general.lists_intersect(["B", "c", "C"]) == []'
|
||||
- '["a", "A", "b"] | community.general.lists_intersect(["b", "B", "c", "C"]) == ["b"]'
|
||||
- '["a", "A", "b"] | community.general.lists_intersect(["b", "A", "a"]) == ["a", "A", "b"]'
|
||||
- '[["a"]] | community.general.lists_intersect([["b"], ["a"]]) == [["a"]]'
|
||||
- '[["a"]] | community.general.lists_intersect([["b"]], ["a"]) == []'
|
||||
- '[["a"]] | community.general.lists_intersect(["b"], ["a"]) == []'
|
||||
|
||||
- name: Test predictive list difference
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 'list1 | community.general.lists_difference(list2, list3) == []'
|
||||
- '[list1, list2, list3] | community.general.lists_difference(flatten=True) == []'
|
||||
- '[1, 2, 3] | community.general.lists_difference([4, 5, 6]) == [1, 2, 3]'
|
||||
- '[1, 2, 3] | community.general.lists_difference([3, 4, 5, 6]) == [1, 2]'
|
||||
- '[1, 2, 3] | community.general.lists_difference([3, 2, 1]) == []'
|
||||
- '["a", "A", "b"] | community.general.lists_difference(["B", "c", "C"]) == ["a", "A", "b"]'
|
||||
- '["a", "A", "b"] | community.general.lists_difference(["b", "B", "c", "C"]) == ["a", "A"]'
|
||||
- '["a", "A", "b"] | community.general.lists_difference(["b", "A", "a"]) == []'
|
||||
- '[["a"]] | community.general.lists_difference([["b"], ["a"]]) == []'
|
||||
- '[["a"]] | community.general.lists_difference([["b"]], ["a"]) == [["a"]]'
|
||||
- '[["a"]] | community.general.lists_difference(["b"], ["a"]) == [["a"]]'
|
||||
|
||||
- name: Test predictive list symmetric difference
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 'list1 | community.general.lists_symmetric_difference(list2, list3) == [11, 1, 2, 4, 5, 101]'
|
||||
- '[list1, list2, list3] | community.general.lists_symmetric_difference(flatten=True) == [11, 1, 2, 4, 5, 101]'
|
||||
- '[1, 2, 3] | community.general.lists_symmetric_difference([4, 5, 6]) == [1, 2, 3, 4, 5, 6]'
|
||||
- '[1, 2, 3] | community.general.lists_symmetric_difference([3, 4, 5, 6]) == [1, 2, 4, 5, 6]'
|
||||
- '[1, 2, 3] | community.general.lists_symmetric_difference([3, 2, 1]) == []'
|
||||
- '["a", "A", "b"] | community.general.lists_symmetric_difference(["B", "c", "C"]) == ["a", "A", "b", "B", "c", "C"]'
|
||||
- '["a", "A", "b"] | community.general.lists_symmetric_difference(["b", "B", "c", "C"]) == ["a", "A", "B", "c", "C"]'
|
||||
- '["a", "A", "b"] | community.general.lists_symmetric_difference(["b", "A", "a"]) == []'
|
||||
- '[["a"]] | community.general.lists_symmetric_difference([["b"], ["a"]]) == [["b"]]'
|
||||
- '[["a"]] | community.general.lists_symmetric_difference([["b"]], ["a"]) == [["a"], ["b"], "a"]'
|
||||
- '[["a"]] | community.general.lists_symmetric_difference(["b"], ["a"]) == [["a"], "b", "a"]'
|
||||
8
tests/integration/targets/filter_lists/vars/main.yml
Normal file
8
tests/integration/targets/filter_lists/vars/main.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
# 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: [1, 2, 5, 3, 4, 10]
|
||||
list2: [1, 2, 3, 4, 5, 11, 99]
|
||||
list3: [1, 2, 4, 5, 10, 99, 101]
|
||||
@@ -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
|
||||
|
||||
azp/posix/1
|
||||
gitlab/ci
|
||||
disabled
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2024, Zoran Krleza <zoran.krleza@true-north.hr>
|
||||
# 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
|
||||
|
||||
gitlab_api_token:
|
||||
gitlab_api_url:
|
||||
gitlab_validate_certs: false
|
||||
gitlab_group_name:
|
||||
gitlab_token_name:
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# Copyright (c) 2024, Zoran Krleza <zoran.krleza@true-north.hr>
|
||||
# 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: Install required libs
|
||||
pip:
|
||||
name: python-gitlab
|
||||
state: present
|
||||
|
||||
- block:
|
||||
- name: Try to create access token in nonexisting group
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "some_nonexisting_group"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2025-01-01'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: create_pfail_token_status
|
||||
always:
|
||||
- name: Assert that token creation in nonexisting group failed
|
||||
assert:
|
||||
that:
|
||||
- create_pfail_token_status is failed
|
||||
ignore_errors: true
|
||||
|
||||
- block:
|
||||
- name: Try to create access token with nonvalid expires_at
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "some_nonexisting_group"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2025-13-01'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: create_efail_token_status
|
||||
always:
|
||||
- name: Assert that token creation with invalid expires_at failed
|
||||
assert:
|
||||
that:
|
||||
- create_efail_token_status is failed
|
||||
ignore_errors: true
|
||||
|
||||
- name: Create access token
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2024-12-31'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: create_token_status
|
||||
- name: Assert that token creation with valid arguments is successfull
|
||||
assert:
|
||||
that:
|
||||
- create_token_status is changed
|
||||
- create_token_status.access_token.token is defined
|
||||
|
||||
- name: Check existing access token recreate=never (default)
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2024-12-31'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: check_token_status
|
||||
- name: Assert that token creation without changes and recreate=never succeeds with status not changed
|
||||
assert:
|
||||
that:
|
||||
- check_token_status is not changed
|
||||
- check_token_status.access_token.token is not defined
|
||||
|
||||
- name: Check existing access token with recreate=state_change
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2024-12-31'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
recreate: state_change
|
||||
register: check_recreate_token_status
|
||||
- name: Assert that token creation without changes and recreate=state_change succeeds with status not changed
|
||||
assert:
|
||||
that:
|
||||
- check_recreate_token_status is not changed
|
||||
- check_recreate_token_status.access_token.token is not defined
|
||||
|
||||
- block:
|
||||
- name: Try to change existing access token with recreate=never
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2025-01-01'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: change_token_status
|
||||
always:
|
||||
- name: Assert that token change with recreate=never fails
|
||||
assert:
|
||||
that:
|
||||
- change_token_status is failed
|
||||
ignore_errors: true
|
||||
|
||||
- name: Try to change existing access token with recreate=state_change
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2025-01-01'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
recreate: state_change
|
||||
register: change_recreate_token_status
|
||||
- name: Assert that token change with recreate=state_change succeeds
|
||||
assert:
|
||||
that:
|
||||
- change_recreate_token_status is changed
|
||||
- change_recreate_token_status.access_token.token is defined
|
||||
|
||||
- name: Try to change existing access token with recreate=always
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: present
|
||||
expires_at: '2025-01-01'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
recreate: always
|
||||
register: change_recreate1_token_status
|
||||
- name: Assert that token change with recreate=always succeeds
|
||||
assert:
|
||||
that:
|
||||
- change_recreate1_token_status is changed
|
||||
- change_recreate1_token_status.access_token.token is defined
|
||||
|
||||
- name: Revoke access token
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: absent
|
||||
expires_at: '2024-12-31'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: revoke_token_status
|
||||
- name: Assert that token revocation succeeds
|
||||
assert:
|
||||
that:
|
||||
- revoke_token_status is changed
|
||||
|
||||
- name: Revoke nonexisting access token
|
||||
community.general.gitlab_group_access_token:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_api_url }}"
|
||||
validate_certs: "{{ gitlab_validate_certs }}"
|
||||
group: "{{ gitlab_group_name }}"
|
||||
name: "{{ gitlab_token_name }}"
|
||||
state: absent
|
||||
expires_at: '2024-12-31'
|
||||
access_level: developer
|
||||
scopes:
|
||||
- api
|
||||
- read_api
|
||||
register: revoke_token_status
|
||||
- name: Assert that token revocation succeeds with status not changed
|
||||
assert:
|
||||
that:
|
||||
- revoke_token_status is not changed
|
||||
19
tests/integration/targets/gitlab_label/README.md
Normal file
19
tests/integration/targets/gitlab_label/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
# Gitlab integration tests
|
||||
|
||||
1. to run integration tests locally, I've setup a podman pod with both gitlab-ee image and the testing image
|
||||
2. gitlab's related information were taken from [here](https://docs.gitlab.com/ee/install/docker.html), about the variable it needs (hostname, ports, volumes); volumes were pre-made before launching the image
|
||||
3. image that run integration tests is started with `podman run --rm -it --pod <pod_name> --name <image_name> --network=host --volume <path_to_git_repo>/ansible_community/community.general:<container_path_to>/workspace/ansible_collections/community/general quay.io/ansible/azure-pipelines-test-container:4.0.1`
|
||||
4. into the testing image, run
|
||||
```sh
|
||||
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||
cd <container_path_to>/workspace/ansible_collections/community/general
|
||||
ansible-test integration gitlab_label -vvv
|
||||
```
|
||||
|
||||
While debugging with `q` package, open a second terminal and run `podman exec -it <image_name> /bin/bash` and inside it do `tail -f /tmp/q` .
|
||||
6
tests/integration/targets/gitlab_label/aliases
Normal file
6
tests/integration/targets/gitlab_label/aliases
Normal file
@@ -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
|
||||
|
||||
gitlab/ci
|
||||
disabled
|
||||
17
tests/integration/targets/gitlab_label/defaults/main.yml
Normal file
17
tests/integration/targets/gitlab_label/defaults/main.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
gitlab_project_name: ansible_test_project
|
||||
gitlab_host: ansible_test_host
|
||||
gitlab_api_token: ansible_test_api_token
|
||||
gitlab_project_group: ansible_test_group
|
||||
gitlab_branch: ansible_test_branch
|
||||
gitlab_first_label: ansible_test_label
|
||||
gitlab_first_label_color: "#112233"
|
||||
gitlab_first_label_description: "label description"
|
||||
gitlab_first_label_priority: 10
|
||||
gitlab_second_label: ansible_test_second_label
|
||||
gitlab_second_label_color: "#445566"
|
||||
gitlab_second_label_new_name: ansible_test_second_label_new_name
|
||||
463
tests/integration/targets/gitlab_label/tasks/main.yml
Normal file
463
tests/integration/targets/gitlab_label/tasks/main.yml
Normal file
@@ -0,0 +1,463 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# 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: Install required libs
|
||||
pip:
|
||||
name: python-gitlab
|
||||
state: present
|
||||
|
||||
- block:
|
||||
###
|
||||
### Group label
|
||||
###
|
||||
|
||||
- name: Create {{ gitlab_project_group }}
|
||||
gitlab_group:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_group }}"
|
||||
state: present
|
||||
|
||||
- name: Purge all group labels for check_mode test
|
||||
gitlab_label:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
|
||||
- name: Group label - Add a label in check_mode
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
check_mode: true
|
||||
register: gitlab_group_label_state
|
||||
|
||||
- name: Group label - Check_mode state must be changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_state is changed
|
||||
|
||||
- name: Group label - Create label {{ gitlab_first_label }} and {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_first_label_color }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_group_label_create
|
||||
|
||||
- name: Group label - Test Label Created
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_create is changed
|
||||
- gitlab_group_label_create.labels.added|length == 2
|
||||
- gitlab_group_label_create.labels.untouched|length == 0
|
||||
- gitlab_group_label_create.labels.removed|length == 0
|
||||
- gitlab_group_label_create.labels.updated|length == 0
|
||||
- gitlab_group_label_create.labels.added[0] == "{{ gitlab_first_label }}"
|
||||
- gitlab_group_label_create.labels.added[1] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Group label - Create Label ( Idempotency test )
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_first_label_color }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
state: present
|
||||
register: gitlab_group_label_create_idempotence
|
||||
|
||||
- name: Group label - Test Create Label is Idempotent
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_create_idempotence is not changed
|
||||
|
||||
- name: Group label - Update Label {{ gitlab_first_label }} changing color
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_group_label_update
|
||||
|
||||
- name: Group label - Test Label Updated
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_update.labels.added|length == 0
|
||||
- gitlab_group_label_update.labels.untouched|length == 0
|
||||
- gitlab_group_label_update.labels.removed|length == 0
|
||||
- gitlab_group_label_update.labels.updated|length == 1
|
||||
- gitlab_group_label_update.labels.updated[0] == "{{ gitlab_first_label }}"
|
||||
|
||||
- name: Group label - Change label {{ gitlab_second_label }} name to {{ gitlab_second_label_new_name }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
new_name: "{{ gitlab_second_label_new_name }}"
|
||||
state: present
|
||||
register: gitlab_group_label_new_name
|
||||
|
||||
- name: Group label - Test Label name changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_new_name.labels.added|length == 0
|
||||
- gitlab_group_label_new_name.labels.untouched|length == 0
|
||||
- gitlab_group_label_new_name.labels.removed|length == 0
|
||||
- gitlab_group_label_new_name.labels.updated|length == 1
|
||||
- gitlab_group_label_new_name.labels.updated[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Group label - Change label name back to {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label_new_name }}"
|
||||
new_name: "{{ gitlab_second_label }}"
|
||||
state: present
|
||||
register: gitlab_group_label_orig_name
|
||||
|
||||
- name: Group label - Test Label name changed back
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_orig_name.labels.added|length == 0
|
||||
- gitlab_group_label_orig_name.labels.untouched|length == 0
|
||||
- gitlab_group_label_orig_name.labels.removed|length == 0
|
||||
- gitlab_group_label_orig_name.labels.updated|length == 1
|
||||
- gitlab_group_label_orig_name.labels.updated[0] == "{{ gitlab_second_label_new_name }}"
|
||||
|
||||
- name: Group label - Update Label Test ( Additions )
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
state: present
|
||||
register: gitlab_group_label_update_additions
|
||||
|
||||
- name: Group label - Test Label Updated ( Additions )
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_update_additions.labels.added|length == 0
|
||||
- gitlab_group_label_update_additions.labels.untouched|length == 0
|
||||
- gitlab_group_label_update_additions.labels.removed|length == 0
|
||||
- gitlab_group_label_update_additions.labels.updated|length == 1
|
||||
- gitlab_group_label_update_additions.labels.updated[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Group label - Delete Label {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
state: absent
|
||||
register: gitlab_group_label_delete
|
||||
|
||||
- name: Group label - Test label is deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_delete is changed
|
||||
- gitlab_group_label_delete.labels.added|length == 0
|
||||
- gitlab_group_label_delete.labels.untouched|length == 0
|
||||
- gitlab_group_label_delete.labels.removed|length == 1
|
||||
- gitlab_group_label_delete.labels.updated|length == 0
|
||||
- gitlab_group_label_delete.labels.removed[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Group label - Create label {{ gitlab_second_label }} again purging the other
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_group_label_create_purging
|
||||
|
||||
- name: Group label - Test Label Created again
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_label_create_purging is changed
|
||||
- gitlab_group_label_create_purging.labels.added|length == 1
|
||||
- gitlab_group_label_create_purging.labels.untouched|length == 0
|
||||
- gitlab_group_label_create_purging.labels.removed|length == 1
|
||||
- gitlab_group_label_create_purging.labels.updated|length == 0
|
||||
- gitlab_group_label_create_purging.labels.added[0] == "{{ gitlab_second_label }}"
|
||||
- gitlab_group_label_create_purging.labels.removed[0] == "{{ gitlab_first_label }}"
|
||||
|
||||
###
|
||||
### Project label
|
||||
###
|
||||
|
||||
- name: Create {{ gitlab_project_name }}
|
||||
gitlab_project:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_name }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
default_branch: "{{ gitlab_branch }}"
|
||||
initialize_with_readme: true
|
||||
state: present
|
||||
|
||||
- name: Purge all labels for check_mode test
|
||||
gitlab_label:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
|
||||
- name: Add a label in check_mode
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
check_mode: true
|
||||
register: gitlab_first_label_state
|
||||
|
||||
- name: Check_mode state must be changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_state is changed
|
||||
|
||||
- name: Create label {{ gitlab_first_label }} and {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_first_label_color }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_first_label_create
|
||||
|
||||
- name: Test Label Created
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_create is changed
|
||||
- gitlab_first_label_create.labels.added|length == 2
|
||||
- gitlab_first_label_create.labels.untouched|length == 0
|
||||
- gitlab_first_label_create.labels.removed|length == 0
|
||||
- gitlab_first_label_create.labels.updated|length == 0
|
||||
- gitlab_first_label_create.labels.added[0] == "{{ gitlab_first_label }}"
|
||||
- gitlab_first_label_create.labels.added[1] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Create Label ( Idempotency test )
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_first_label_color }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
state: present
|
||||
register: gitlab_first_label_create_idempotence
|
||||
|
||||
- name: Test Create Label is Idempotent
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_create_idempotence is not changed
|
||||
|
||||
- name: Update Label {{ gitlab_first_label }} changing color
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_first_label_update
|
||||
|
||||
- name: Test Label Updated
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_update.labels.added|length == 0
|
||||
- gitlab_first_label_update.labels.untouched|length == 0
|
||||
- gitlab_first_label_update.labels.removed|length == 0
|
||||
- gitlab_first_label_update.labels.updated|length == 1
|
||||
- gitlab_first_label_update.labels.updated[0] == "{{ gitlab_first_label }}"
|
||||
|
||||
- name: Change label {{ gitlab_second_label }} name to {{ gitlab_second_label_new_name }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
new_name: "{{ gitlab_second_label_new_name }}"
|
||||
state: present
|
||||
register: gitlab_first_label_new_name
|
||||
|
||||
- name: Test Label name changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_new_name.labels.added|length == 0
|
||||
- gitlab_first_label_new_name.labels.untouched|length == 0
|
||||
- gitlab_first_label_new_name.labels.removed|length == 0
|
||||
- gitlab_first_label_new_name.labels.updated|length == 1
|
||||
- gitlab_first_label_new_name.labels.updated[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Change label name back to {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label_new_name }}"
|
||||
new_name: "{{ gitlab_second_label }}"
|
||||
state: present
|
||||
register: gitlab_first_label_orig_name
|
||||
|
||||
- name: Test Label name changed back
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_orig_name.labels.added|length == 0
|
||||
- gitlab_first_label_orig_name.labels.untouched|length == 0
|
||||
- gitlab_first_label_orig_name.labels.removed|length == 0
|
||||
- gitlab_first_label_orig_name.labels.updated|length == 1
|
||||
- gitlab_first_label_orig_name.labels.updated[0] == "{{ gitlab_second_label_new_name }}"
|
||||
|
||||
- name: Update Label Test ( Additions )
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
description: "{{ gitlab_first_label_description }}"
|
||||
priority: "{{ gitlab_first_label_priority }}"
|
||||
state: present
|
||||
register: gitlab_first_label_update_additions
|
||||
|
||||
- name: Test Label Updated ( Additions )
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_update_additions.labels.added|length == 0
|
||||
- gitlab_first_label_update_additions.labels.untouched|length == 0
|
||||
- gitlab_first_label_update_additions.labels.removed|length == 0
|
||||
- gitlab_first_label_update_additions.labels.updated|length == 1
|
||||
- gitlab_first_label_update_additions.labels.updated[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Delete Label {{ gitlab_second_label }}
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
state: absent
|
||||
register: gitlab_first_label_delete
|
||||
|
||||
- name: Test label is deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_delete is changed
|
||||
- gitlab_first_label_delete.labels.added|length == 0
|
||||
- gitlab_first_label_delete.labels.untouched|length == 0
|
||||
- gitlab_first_label_delete.labels.removed|length == 1
|
||||
- gitlab_first_label_delete.labels.updated|length == 0
|
||||
- gitlab_first_label_delete.labels.removed[0] == "{{ gitlab_second_label }}"
|
||||
|
||||
- name: Create label {{ gitlab_second_label }} again purging the other
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
labels:
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
color: "{{ gitlab_second_label_color }}"
|
||||
state: present
|
||||
register: gitlab_first_label_create_purging
|
||||
|
||||
- name: Test Label Created again
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_create_purging is changed
|
||||
- gitlab_first_label_create_purging.labels.added|length == 1
|
||||
- gitlab_first_label_create_purging.labels.untouched|length == 0
|
||||
- gitlab_first_label_create_purging.labels.removed|length == 1
|
||||
- gitlab_first_label_create_purging.labels.updated|length == 0
|
||||
- gitlab_first_label_create_purging.labels.added[0] == "{{ gitlab_second_label }}"
|
||||
- gitlab_first_label_create_purging.labels.removed[0] == "{{ gitlab_first_label }}"
|
||||
|
||||
always:
|
||||
- name: Delete Labels
|
||||
gitlab_label:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
labels:
|
||||
- name: "{{ gitlab_first_label }}"
|
||||
- name: "{{ gitlab_second_label }}"
|
||||
state: absent
|
||||
register: gitlab_first_label_always_delete
|
||||
|
||||
- name: Test label are deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_label_always_delete is changed
|
||||
- gitlab_first_label_always_delete.labels.added|length == 0
|
||||
- gitlab_first_label_always_delete.labels.untouched|length == 0
|
||||
- gitlab_first_label_always_delete.labels.removed|length > 0
|
||||
- gitlab_first_label_always_delete.labels.updated|length == 0
|
||||
|
||||
- name: Clean up {{ gitlab_project_name }}
|
||||
gitlab_project:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: false
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_name }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
state: absent
|
||||
|
||||
- name: Clean up {{ gitlab_project_group }}
|
||||
gitlab_group:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_group }}"
|
||||
state: absent
|
||||
19
tests/integration/targets/gitlab_milestone/README.md
Normal file
19
tests/integration/targets/gitlab_milestone/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
# Gitlab integration tests
|
||||
|
||||
1. to run integration tests locally, I've setup a podman pod with both gitlab-ee image and the testing image
|
||||
2. gitlab's related information were taken from [here](https://docs.gitlab.com/ee/install/docker.html), about the variable it needs (hostname, ports, volumes); volumes were pre-made before launching the image
|
||||
3. image that run integration tests is started with `podman run --rm -it --pod <pod_name> --name <image_name> --network=host --volume <path_to_git_repo>/ansible_community/community.general:<container_path_to>/workspace/ansible_collections/community/general quay.io/ansible/azure-pipelines-test-container:4.0.1`
|
||||
4. into the testing image, run
|
||||
```sh
|
||||
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||
cd <container_path_to>/workspace/ansible_collections/community/general
|
||||
ansible-test integration gitlab_milestone -vvv
|
||||
```
|
||||
|
||||
While debugging with `q` package, open a second terminal and run `podman exec -it <image_name> /bin/bash` and inside it do `tail -f /tmp/q` .
|
||||
6
tests/integration/targets/gitlab_milestone/aliases
Normal file
6
tests/integration/targets/gitlab_milestone/aliases
Normal file
@@ -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
|
||||
|
||||
gitlab/ci
|
||||
disabled
|
||||
18
tests/integration/targets/gitlab_milestone/defaults/main.yml
Normal file
18
tests/integration/targets/gitlab_milestone/defaults/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
gitlab_project_name: ansible_test_project
|
||||
gitlab_host: ansible_test_host
|
||||
gitlab_api_token: ansible_test_api_token
|
||||
gitlab_project_group: ansible_test_group
|
||||
gitlab_branch: ansible_test_branch
|
||||
gitlab_first_milestone: ansible_test_milestone
|
||||
gitlab_first_milestone_description: "milestone description"
|
||||
gitlab_first_milestone_start_date: "2024-01-01"
|
||||
gitlab_first_milestone_due_date: "2024-12-31"
|
||||
gitlab_first_milestone_new_start_date: "2024-05-01"
|
||||
gitlab_first_milestone_new_due_date: "2024-10-31"
|
||||
gitlab_second_milestone: ansible_test_second_milestone
|
||||
gitlab_second_milestone_description: "new milestone"
|
||||
388
tests/integration/targets/gitlab_milestone/tasks/main.yml
Normal file
388
tests/integration/targets/gitlab_milestone/tasks/main.yml
Normal file
@@ -0,0 +1,388 @@
|
||||
---
|
||||
####################################################################
|
||||
# WARNING: These are designed specifically for Ansible tests #
|
||||
# and should not be used as examples of how to write Ansible roles #
|
||||
####################################################################
|
||||
|
||||
# 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: Install required libs
|
||||
pip:
|
||||
name: python-gitlab
|
||||
state: present
|
||||
|
||||
- block:
|
||||
###
|
||||
### Group milestone
|
||||
###
|
||||
- name: Create {{ gitlab_project_group }}
|
||||
gitlab_group:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_group }}"
|
||||
state: present
|
||||
|
||||
- name: Purge all group milestones for check_mode test
|
||||
gitlab_milestone:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
|
||||
- name: Group milestone - Add a milestone in check_mode
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
check_mode: true
|
||||
register: gitlab_group_milestone_state
|
||||
|
||||
- name: Group milestone - Check_mode state must be changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_state is changed
|
||||
|
||||
- name: Purge all group milestones for project milestone test - cannot exist with same name
|
||||
gitlab_milestone:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
register: gitlab_group_milestone_purged
|
||||
|
||||
- name: Group milestone - Create milestone {{ gitlab_first_milestone }} and {{ gitlab_second_milestone }}
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_due_date }}"
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: present
|
||||
register: gitlab_group_milestone_create
|
||||
|
||||
- name: Group milestone - Test milestone Created
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_create is changed
|
||||
- gitlab_group_milestone_create.milestones.added|length == 2
|
||||
- gitlab_group_milestone_create.milestones.untouched|length == 0
|
||||
- gitlab_group_milestone_create.milestones.removed|length == 0
|
||||
- gitlab_group_milestone_create.milestones.updated|length == 0
|
||||
- gitlab_group_milestone_create.milestones.added[0] == "{{ gitlab_first_milestone }}"
|
||||
- gitlab_group_milestone_create.milestones.added[1] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Group milestone - Create milestone ( Idempotency test )
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_due_date }}"
|
||||
state: present
|
||||
register: gitlab_group_milestone_create_idempotence
|
||||
|
||||
- name: Group milestone - Test Create milestone is Idempotent
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_create_idempotence is not changed
|
||||
|
||||
- name: Group milestone - Update milestone {{ gitlab_first_milestone }} changing dates
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_new_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_new_due_date }}"
|
||||
state: present
|
||||
register: gitlab_group_milestone_update
|
||||
|
||||
- name: Group milestone - Test milestone Updated
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_update.milestones.added|length == 0
|
||||
- gitlab_group_milestone_update.milestones.untouched|length == 0
|
||||
- gitlab_group_milestone_update.milestones.removed|length == 0
|
||||
- gitlab_group_milestone_update.milestones.updated|length == 1
|
||||
- gitlab_group_milestone_update.milestones.updated[0] == "{{ gitlab_first_milestone }}"
|
||||
|
||||
- name: Group milestone - Update milestone Test ( Additions )
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
description: "{{ gitlab_first_milestone_description }}"
|
||||
state: present
|
||||
register: gitlab_group_milestone_update_additions
|
||||
|
||||
- name: Group milestone - Test milestone Updated ( Additions )
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_update_additions.milestones.added|length == 0
|
||||
- gitlab_group_milestone_update_additions.milestones.untouched|length == 0
|
||||
- gitlab_group_milestone_update_additions.milestones.removed|length == 0
|
||||
- gitlab_group_milestone_update_additions.milestones.updated|length == 1
|
||||
- gitlab_group_milestone_update_additions.milestones.updated[0] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Group milestone - Delete milestone {{ gitlab_second_milestone }}
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: absent
|
||||
register: gitlab_group_milestone_delete
|
||||
|
||||
- name: Group milestone - Test milestone is deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_delete is changed
|
||||
- gitlab_group_milestone_delete.milestones.added|length == 0
|
||||
- gitlab_group_milestone_delete.milestones.untouched|length == 0
|
||||
- gitlab_group_milestone_delete.milestones.removed|length == 1
|
||||
- gitlab_group_milestone_delete.milestones.updated|length == 0
|
||||
- gitlab_group_milestone_delete.milestones.removed[0] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Group milestone - Create group milestone {{ gitlab_second_milestone }} again purging the other
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: present
|
||||
register: gitlab_group_milestone_create_purging
|
||||
|
||||
- name: Group milestone - Test milestone Created again
|
||||
assert:
|
||||
that:
|
||||
- gitlab_group_milestone_create_purging is changed
|
||||
- gitlab_group_milestone_create_purging.milestones.added|length == 1
|
||||
- gitlab_group_milestone_create_purging.milestones.untouched|length == 0
|
||||
- gitlab_group_milestone_create_purging.milestones.removed|length == 1
|
||||
- gitlab_group_milestone_create_purging.milestones.updated|length == 0
|
||||
- gitlab_group_milestone_create_purging.milestones.added[0] == "{{ gitlab_second_milestone }}"
|
||||
- gitlab_group_milestone_create_purging.milestones.removed[0] == "{{ gitlab_first_milestone }}"
|
||||
|
||||
###
|
||||
### Project milestone
|
||||
###
|
||||
- name: Purge all group milestones for project milestone test - cannot exist with same name
|
||||
gitlab_milestone:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
purge: true
|
||||
register: gitlab_group_milestone_purged
|
||||
|
||||
- name: Create {{ gitlab_project_name }}
|
||||
gitlab_project:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_name }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
default_branch: "{{ gitlab_branch }}"
|
||||
initialize_with_readme: true
|
||||
state: present
|
||||
|
||||
- name: Purge all milestones for check_mode test
|
||||
gitlab_milestone:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
|
||||
- name: Add a milestone in check_mode
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
description: "{{ gitlab_second_milestone_description }}"
|
||||
check_mode: true
|
||||
register: gitlab_first_milestone_state
|
||||
|
||||
- name: Check_mode state must be changed
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_state is changed
|
||||
|
||||
- name: Create milestone {{ gitlab_first_milestone }} and {{ gitlab_second_milestone }}
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_due_date }}"
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: present
|
||||
register: gitlab_milestones_create
|
||||
|
||||
- name: Test milestone Created
|
||||
assert:
|
||||
that:
|
||||
- gitlab_milestones_create is changed
|
||||
- gitlab_milestones_create.milestones.added|length == 2
|
||||
- gitlab_milestones_create.milestones.untouched|length == 0
|
||||
- gitlab_milestones_create.milestones.removed|length == 0
|
||||
- gitlab_milestones_create.milestones.updated|length == 0
|
||||
- gitlab_milestones_create.milestones.added[0] == "{{ gitlab_first_milestone }}"
|
||||
- gitlab_milestones_create.milestones.added[1] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Create milestone ( Idempotency test )
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_due_date }}"
|
||||
state: present
|
||||
register: gitlab_first_milestone_create_idempotence
|
||||
|
||||
- name: Test Create milestone is Idempotent
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_create_idempotence is not changed
|
||||
|
||||
- name: Update milestone {{ gitlab_first_milestone }} changing dates
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
start_date: "{{ gitlab_first_milestone_new_start_date }}"
|
||||
due_date: "{{ gitlab_first_milestone_new_due_date }}"
|
||||
state: present
|
||||
register: gitlab_first_milestone_update
|
||||
|
||||
- name: Test milestone Updated
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_update.milestones.added|length == 0
|
||||
- gitlab_first_milestone_update.milestones.untouched|length == 0
|
||||
- gitlab_first_milestone_update.milestones.removed|length == 0
|
||||
- gitlab_first_milestone_update.milestones.updated|length == 1
|
||||
- gitlab_first_milestone_update.milestones.updated[0] == "{{ gitlab_first_milestone }}"
|
||||
|
||||
- name: Update milestone Test ( Additions )
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
description: "{{ gitlab_second_milestone_description }}"
|
||||
state: present
|
||||
register: gitlab_first_milestone_update_additions
|
||||
|
||||
- name: Test milestone Updated ( Additions )
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_update_additions.milestones.added|length == 0
|
||||
- gitlab_first_milestone_update_additions.milestones.untouched|length == 0
|
||||
- gitlab_first_milestone_update_additions.milestones.removed|length == 0
|
||||
- gitlab_first_milestone_update_additions.milestones.updated|length == 1
|
||||
- gitlab_first_milestone_update_additions.milestones.updated[0] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Delete milestone {{ gitlab_second_milestone }}
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: absent
|
||||
register: gitlab_first_milestone_delete
|
||||
|
||||
- name: Test milestone is deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_delete is changed
|
||||
- gitlab_first_milestone_delete.milestones.added|length == 0
|
||||
- gitlab_first_milestone_delete.milestones.untouched|length == 0
|
||||
- gitlab_first_milestone_delete.milestones.removed|length == 1
|
||||
- gitlab_first_milestone_delete.milestones.updated|length == 0
|
||||
- gitlab_first_milestone_delete.milestones.removed[0] == "{{ gitlab_second_milestone }}"
|
||||
|
||||
- name: Create milestone {{ gitlab_second_milestone }} again purging the other
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
milestones:
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: present
|
||||
register: gitlab_first_milestone_create_purging
|
||||
|
||||
- name: Test milestone Created again
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_create_purging is changed
|
||||
- gitlab_first_milestone_create_purging.milestones.added|length == 1
|
||||
- gitlab_first_milestone_create_purging.milestones.untouched|length == 0
|
||||
- gitlab_first_milestone_create_purging.milestones.removed|length == 1
|
||||
- gitlab_first_milestone_create_purging.milestones.updated|length == 0
|
||||
- gitlab_first_milestone_create_purging.milestones.added[0] == "{{ gitlab_second_milestone }}"
|
||||
- gitlab_first_milestone_create_purging.milestones.removed[0] == "{{ gitlab_first_milestone }}"
|
||||
|
||||
always:
|
||||
- name: Delete milestones
|
||||
gitlab_milestone:
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
api_url: "{{ gitlab_host }}"
|
||||
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
|
||||
purge: true
|
||||
milestones:
|
||||
- title: "{{ gitlab_first_milestone }}"
|
||||
- title: "{{ gitlab_second_milestone }}"
|
||||
state: absent
|
||||
register: gitlab_first_milestone_always_delete
|
||||
|
||||
- name: Test milestone are deleted
|
||||
assert:
|
||||
that:
|
||||
- gitlab_first_milestone_always_delete is changed
|
||||
- gitlab_first_milestone_always_delete.milestones.added|length == 0
|
||||
- gitlab_first_milestone_always_delete.milestones.untouched|length == 0
|
||||
- gitlab_first_milestone_always_delete.milestones.removed|length > 0
|
||||
- gitlab_first_milestone_always_delete.milestones.updated|length == 0
|
||||
|
||||
- name: Clean up {{ gitlab_project_name }}
|
||||
gitlab_project:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: false
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_name }}"
|
||||
group: "{{ gitlab_project_group }}"
|
||||
state: absent
|
||||
|
||||
- name: Clean up {{ gitlab_project_group }}
|
||||
gitlab_group:
|
||||
api_url: "{{ gitlab_host }}"
|
||||
validate_certs: true
|
||||
api_token: "{{ gitlab_api_token }}"
|
||||
name: "{{ gitlab_project_group }}"
|
||||
state: absent
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user