mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-28 17:36:49 +00:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96f609d1f2 | ||
|
|
03b128aeff | ||
|
|
ab9a4cb58a | ||
|
|
6b21599def | ||
|
|
ca93145e76 | ||
|
|
a163ec3afa | ||
|
|
868a6303be | ||
|
|
759e82d403 | ||
|
|
ed0c768aaf | ||
|
|
e933ed782f | ||
|
|
69e5a0dbf1 | ||
|
|
c4d166d3bc | ||
|
|
9ae8e544cb | ||
|
|
94aef4526d | ||
|
|
aeece5a107 | ||
|
|
bdc4ee496f | ||
|
|
5f59ec2d01 | ||
|
|
a25e4f679e | ||
|
|
3876df9052 | ||
|
|
12f2ba251b | ||
|
|
e43a9b6974 | ||
|
|
9e2cb4363c | ||
|
|
b61cb29023 | ||
|
|
90d31b9403 | ||
|
|
4d22d0790d | ||
|
|
bffe4c2a3b | ||
|
|
dfdb0a6fe6 | ||
|
|
dd04e11094 | ||
|
|
5b029c66c5 | ||
|
|
760843b9e5 | ||
|
|
19ba15a783 | ||
|
|
70a3dae965 | ||
|
|
26d5409a87 | ||
|
|
2f3a7a981d | ||
|
|
6a74c46e1c | ||
|
|
bec382df87 | ||
|
|
78f69224be | ||
|
|
34682addb8 | ||
|
|
2c106d66a4 | ||
|
|
9c4fd63a4d | ||
|
|
d04c18ffce | ||
|
|
41fe6663d9 | ||
|
|
9f8612f34e | ||
|
|
22b72e6684 | ||
|
|
8e7bee4217 | ||
|
|
cef6b81e5b | ||
|
|
182c365d87 | ||
|
|
587cdc82e7 | ||
|
|
cb1a50a273 | ||
|
|
f0df50e665 | ||
|
|
47aa93d970 | ||
|
|
e89648a114 | ||
|
|
6f1bdb3e49 | ||
|
|
fbf11668f4 | ||
|
|
3376442aa2 | ||
|
|
868edfa664 | ||
|
|
2fcb77f7fb | ||
|
|
17135dd082 | ||
|
|
7516018cfb | ||
|
|
58df1df107 | ||
|
|
e9b3705809 | ||
|
|
743e9c851f | ||
|
|
a7883ee489 | ||
|
|
518af70b77 | ||
|
|
ce7d98aa6f | ||
|
|
9f91f4b5cd | ||
|
|
c45c38f04b | ||
|
|
f7efb2e394 | ||
|
|
093b83c34f | ||
|
|
579fdbbc1c | ||
|
|
c970c14c71 | ||
|
|
24f6493cd4 | ||
|
|
68364df409 | ||
|
|
fb61da5246 | ||
|
|
cf9b01ec6b | ||
|
|
89663a0688 | ||
|
|
7fcb21e044 | ||
|
|
1bf9caa90f | ||
|
|
c6ecc0f3f8 | ||
|
|
4d74aa05a8 | ||
|
|
7fb44b0643 | ||
|
|
7ddb2eb438 | ||
|
|
3158495572 | ||
|
|
58f110ae9c | ||
|
|
5695c919f1 | ||
|
|
6e1a1c028e | ||
|
|
d02b8507d1 | ||
|
|
14d43b10c1 | ||
|
|
92c41a5f55 | ||
|
|
012f684133 | ||
|
|
77b7a65002 | ||
|
|
7f4cd86fe5 | ||
|
|
06980d8239 | ||
|
|
d4740ff387 | ||
|
|
a0b22e4402 | ||
|
|
a56879c1b0 | ||
|
|
d7b31655c4 | ||
|
|
70a7f66d4c | ||
|
|
391c3aa850 | ||
|
|
deb95ea6bf | ||
|
|
806ca0a9e0 | ||
|
|
a171d9bb90 | ||
|
|
dd70c8b031 | ||
|
|
30e707aa79 | ||
|
|
7be95c8bbe | ||
|
|
8e9a348e92 | ||
|
|
2622513d65 | ||
|
|
37d37b20cb | ||
|
|
631d555f8a | ||
|
|
c4a53243d5 | ||
|
|
c0008e976f | ||
|
|
f60c90873f | ||
|
|
c08a57a7c1 | ||
|
|
3d2caf3933 | ||
|
|
df6a00dc89 | ||
|
|
bdddc50358 | ||
|
|
8a01ad200d | ||
|
|
b6ccac372c | ||
|
|
3b1b7966ca | ||
|
|
1f522c414e | ||
|
|
cf60761cf9 | ||
|
|
4b28b036c9 | ||
|
|
ec7c39351d | ||
|
|
b3963fd3c7 | ||
|
|
271bafb637 | ||
|
|
6f5152d053 | ||
|
|
f8842e39be | ||
|
|
b1459b13fe | ||
|
|
57fa900f40 | ||
|
|
f0a232d7a7 | ||
|
|
64f91aafa8 | ||
|
|
7600fec752 | ||
|
|
5af1ac26ac | ||
|
|
5c85b2d891 | ||
|
|
0a8aa03425 | ||
|
|
fa689ffadc | ||
|
|
7d2332626e | ||
|
|
bdc7e48779 | ||
|
|
815638f2ec | ||
|
|
a678029bd2 | ||
|
|
fab30c5e55 | ||
|
|
3e25c692d7 | ||
|
|
e1a4b50074 | ||
|
|
3a270cea95 | ||
|
|
41672c20d3 | ||
|
|
57f5ceece8 | ||
|
|
945bb91e04 | ||
|
|
b48a5c264f | ||
|
|
5bae017de9 | ||
|
|
e568a760ac | ||
|
|
8132568d2f | ||
|
|
0e320641b8 | ||
|
|
8679d59376 | ||
|
|
2554b4b0f4 | ||
|
|
379b6d3523 | ||
|
|
fe4f4198af | ||
|
|
db84ea4ab6 | ||
|
|
de5970d17a | ||
|
|
433d0571b4 | ||
|
|
53b95fd182 | ||
|
|
ad1f25e576 | ||
|
|
49eda7270e | ||
|
|
9c4799c903 | ||
|
|
88bf99b272 | ||
|
|
3ca6e8525e | ||
|
|
0169cb8358 | ||
|
|
499f4b4066 | ||
|
|
ff08c20f12 | ||
|
|
d27c06faeb | ||
|
|
0f98b63944 | ||
|
|
55c70dfb72 | ||
|
|
f78993ba12 | ||
|
|
b97ce10156 | ||
|
|
9250430d7d | ||
|
|
d61305d267 | ||
|
|
198b813b55 | ||
|
|
9e6df4f1c9 | ||
|
|
a477044fb7 | ||
|
|
2a97812856 |
@@ -69,6 +69,19 @@ stages:
|
||||
- test: 3
|
||||
- test: 4
|
||||
- test: extra
|
||||
- stage: Sanity_2_13
|
||||
displayName: Sanity 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Test {0}
|
||||
testFormat: 2.13/sanity/{0}
|
||||
targets:
|
||||
- test: 1
|
||||
- test: 2
|
||||
- test: 3
|
||||
- test: 4
|
||||
- stage: Sanity_2_12
|
||||
displayName: Sanity 2.12
|
||||
dependsOn: []
|
||||
@@ -138,6 +151,19 @@ stages:
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- test: '3.10'
|
||||
- stage: Units_2_13
|
||||
displayName: Units 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.13/units/{0}/1
|
||||
targets:
|
||||
- test: 2.7
|
||||
- test: 3.6
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- stage: Units_2_12
|
||||
displayName: Units 2.12
|
||||
dependsOn: []
|
||||
@@ -148,12 +174,8 @@ stages:
|
||||
testFormat: 2.12/units/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
- test: 3.7
|
||||
- test: 3.8
|
||||
- test: '3.10'
|
||||
- stage: Units_2_11
|
||||
displayName: Units 2.11
|
||||
dependsOn: []
|
||||
@@ -166,9 +188,6 @@ stages:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
- test: 3.7
|
||||
- test: 3.8
|
||||
- test: 3.9
|
||||
- stage: Units_2_10
|
||||
displayName: Units 2.10
|
||||
@@ -191,11 +210,7 @@ stages:
|
||||
testFormat: 2.9/units/{0}/1
|
||||
targets:
|
||||
- test: 2.6
|
||||
- test: 2.7
|
||||
- test: 3.5
|
||||
- test: 3.6
|
||||
- test: 3.7
|
||||
- test: 3.8
|
||||
|
||||
## Remote
|
||||
- stage: Remote_devel
|
||||
@@ -206,20 +221,36 @@ stages:
|
||||
parameters:
|
||||
testFormat: devel/{0}
|
||||
targets:
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: macOS 12.0
|
||||
test: macos/12.0
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- name: FreeBSD 12.3
|
||||
test: freebsd/12.3
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- stage: Remote_2_13
|
||||
displayName: Remote 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.13/{0}
|
||||
targets:
|
||||
- name: macOS 12.0
|
||||
test: macos/12.0
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- stage: Remote_2_12
|
||||
displayName: Remote 2.12
|
||||
dependsOn: []
|
||||
@@ -249,8 +280,8 @@ stages:
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.3
|
||||
test: rhel/8.3
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
#- name: FreeBSD 12.2
|
||||
# test: freebsd/12.2
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
@@ -281,8 +312,8 @@ stages:
|
||||
test: rhel/8.2
|
||||
- name: RHEL 7.8
|
||||
test: rhel/7.8
|
||||
- name: FreeBSD 12.0
|
||||
test: freebsd/12.0
|
||||
#- name: FreeBSD 12.0
|
||||
# test: freebsd/12.0
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
@@ -302,14 +333,32 @@ stages:
|
||||
test: fedora34
|
||||
- name: Fedora 35
|
||||
test: fedora35
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
- name: openSUSE 15
|
||||
test: opensuse15
|
||||
- name: Ubuntu 18.04
|
||||
test: ubuntu1804
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
- name: Alpine 3
|
||||
test: alpine3
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- stage: Docker_2_13
|
||||
displayName: Docker 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: 2.13/linux/{0}
|
||||
targets:
|
||||
- name: Fedora 35
|
||||
test: fedora35
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: Alpine 3
|
||||
test: alpine3
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
@@ -324,12 +373,8 @@ stages:
|
||||
targets:
|
||||
- name: CentOS 6
|
||||
test: centos6
|
||||
- name: CentOS 8
|
||||
test: centos8
|
||||
- name: Fedora 34
|
||||
test: fedora34
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
- name: Ubuntu 20.04
|
||||
test: ubuntu2004
|
||||
groups:
|
||||
@@ -344,12 +389,10 @@ stages:
|
||||
parameters:
|
||||
testFormat: 2.11/linux/{0}
|
||||
targets:
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: Alpine 3
|
||||
test: alpine3
|
||||
groups:
|
||||
- 2
|
||||
- 3
|
||||
@@ -378,12 +421,30 @@ stages:
|
||||
targets:
|
||||
- name: Fedora 31
|
||||
test: fedora31
|
||||
- name: openSUSE 15 py3
|
||||
test: opensuse15
|
||||
groups:
|
||||
- 2
|
||||
- 3
|
||||
|
||||
### Community Docker
|
||||
- stage: Docker_community_devel
|
||||
displayName: Docker (community images) devel
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
testFormat: devel/linux-community/{0}
|
||||
targets:
|
||||
- name: Debian Bullseye
|
||||
test: debian-bullseye/3.9
|
||||
- name: ArchLinux
|
||||
test: archlinux/3.10
|
||||
- name: CentOS Stream 8
|
||||
test: centos-stream8/3.8
|
||||
groups:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
### Cloud
|
||||
- stage: Cloud_devel
|
||||
displayName: Cloud devel
|
||||
@@ -395,6 +456,16 @@ stages:
|
||||
testFormat: devel/cloud/{0}/1
|
||||
targets:
|
||||
- test: 2.7
|
||||
- test: '3.10'
|
||||
- stage: Cloud_2_13
|
||||
displayName: Cloud 2.13
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: templates/matrix.yml
|
||||
parameters:
|
||||
nameFormat: Python {0}
|
||||
testFormat: 2.13/cloud/{0}/1
|
||||
targets:
|
||||
- test: 3.9
|
||||
- stage: Cloud_2_12
|
||||
displayName: Cloud 2.12
|
||||
@@ -444,25 +515,31 @@ stages:
|
||||
- Sanity_2_10
|
||||
- Sanity_2_11
|
||||
- Sanity_2_12
|
||||
- Sanity_2_13
|
||||
- Units_devel
|
||||
- Units_2_9
|
||||
- Units_2_10
|
||||
- Units_2_11
|
||||
- Units_2_12
|
||||
- Units_2_13
|
||||
- Remote_devel
|
||||
- Remote_2_9
|
||||
- Remote_2_10
|
||||
- Remote_2_11
|
||||
- Remote_2_12
|
||||
- Remote_2_13
|
||||
- Docker_devel
|
||||
- Docker_2_9
|
||||
- Docker_2_10
|
||||
- Docker_2_11
|
||||
- Docker_2_12
|
||||
- Docker_2_13
|
||||
- Docker_community_devel
|
||||
- Cloud_devel
|
||||
- Cloud_2_9
|
||||
- Cloud_2_10
|
||||
- Cloud_2_11
|
||||
- Cloud_2_12
|
||||
- Cloud_2_13
|
||||
jobs:
|
||||
- template: templates/coverage.yml
|
||||
|
||||
19
.github/BOTMETA.yml
vendored
19
.github/BOTMETA.yml
vendored
@@ -260,6 +260,8 @@ files:
|
||||
$module_utils/module_helper.py:
|
||||
maintainers: russoz
|
||||
labels: module_helper
|
||||
$module_utils/net_tools/pritunl/:
|
||||
maintainers: Lowess
|
||||
$module_utils/oracle/oci_utils.py:
|
||||
maintainers: $team_oracle
|
||||
labels: cloud
|
||||
@@ -310,6 +312,8 @@ files:
|
||||
ignore: hnakamur
|
||||
$modules/cloud/lxd/lxd_profile.py:
|
||||
maintainers: conloos
|
||||
$modules/cloud/lxd/lxd_project.py:
|
||||
maintainers: we10710aa
|
||||
$modules/cloud/memset/:
|
||||
maintainers: glitchcrab
|
||||
$modules/cloud/misc/cloud_init_data_facts.py:
|
||||
@@ -418,6 +422,8 @@ files:
|
||||
maintainers: Spredzy
|
||||
$modules/cloud/scaleway/scaleway_organization_info.py:
|
||||
maintainers: Spredzy
|
||||
$modules/cloud/scaleway/scaleway_private_network.py:
|
||||
maintainers: pastral
|
||||
$modules/cloud/scaleway/scaleway_security_group.py:
|
||||
maintainers: DenBeke
|
||||
$modules/cloud/scaleway/scaleway_security_group_info.py:
|
||||
@@ -556,6 +562,8 @@ files:
|
||||
maintainers: phumpal
|
||||
labels: airbrake_deployment
|
||||
ignore: bpennypacker
|
||||
$modules/monitoring/alerta_customer.py:
|
||||
maintainers: cwollinger
|
||||
$modules/monitoring/bigpanda.py:
|
||||
maintainers: hkariti
|
||||
$modules/monitoring/circonus_annotation.py:
|
||||
@@ -819,7 +827,7 @@ files:
|
||||
$modules/packaging/os/opkg.py:
|
||||
maintainers: skinp
|
||||
$modules/packaging/os/pacman.py:
|
||||
maintainers: elasticdog indrajitr tchernomax
|
||||
maintainers: elasticdog indrajitr tchernomax jraby
|
||||
labels: pacman
|
||||
ignore: elasticdog
|
||||
$modules/packaging/os/pacman_key.py:
|
||||
@@ -977,6 +985,8 @@ files:
|
||||
maintainers: farhan7500 gautamphegde
|
||||
$modules/storage/ibm/:
|
||||
maintainers: tzure
|
||||
$modules/storage/pmem/pmem.py:
|
||||
maintainers: mizumm
|
||||
$modules/storage/vexata/:
|
||||
maintainers: vexata
|
||||
$modules/storage/zfs/:
|
||||
@@ -1022,6 +1032,8 @@ files:
|
||||
$modules/system/gconftool2.py:
|
||||
maintainers: Akasurde kevensen
|
||||
labels: gconftool2
|
||||
$modules/system/homectl.py:
|
||||
maintainers: jameslivulpi
|
||||
$modules/system/interfaces_file.py:
|
||||
maintainers: obourdon hryamzik
|
||||
labels: interfaces_file
|
||||
@@ -1162,7 +1174,8 @@ files:
|
||||
$modules/web_infrastructure/jenkins_script.py:
|
||||
maintainers: hogarthj
|
||||
$modules/web_infrastructure/jira.py:
|
||||
maintainers: Slezhuk tarka pertoft DWSR
|
||||
maintainers: Slezhuk tarka pertoft
|
||||
ignore: DWSR
|
||||
labels: jira
|
||||
$modules/web_infrastructure/nginx_status_info.py:
|
||||
maintainers: resmo
|
||||
@@ -1231,7 +1244,7 @@ macros:
|
||||
team_cyberark_conjur: jvanderhoof ryanprior
|
||||
team_e_spirit: MatrixCrawler getjack
|
||||
team_flatpak: JayKayy oolongbrothers
|
||||
team_gitlab: Lunik Shaps dj-wasabi marwatk waheedi zanssa scodeman metanovii sh0shin
|
||||
team_gitlab: Lunik Shaps dj-wasabi marwatk waheedi zanssa scodeman metanovii sh0shin nejch lgatellier suukit
|
||||
team_hpux: bcoca davx8342
|
||||
team_huawei: QijunPan TommyLike edisonxiang freesky-edward hwDCN niuzhenguo xuxiaowei0512 yanzhangi zengchen1024 zhongjun2
|
||||
team_ipa: Akasurde Nosmoht fxfitz justchris1
|
||||
|
||||
298
CHANGELOG.rst
298
CHANGELOG.rst
@@ -6,6 +6,304 @@ Community General Release Notes
|
||||
|
||||
This changelog describes changes after version 3.0.0.
|
||||
|
||||
v4.8.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release. Please note that this is the last minor 4.x.0 release. Further releases with major version 4 will be bugfix releases 4.8.y.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- alternatives - add ``state`` parameter, which provides control over whether the alternative should be set as the active selection for its alternatives group (https://github.com/ansible-collections/community.general/issues/4543, https://github.com/ansible-collections/community.general/pull/4557).
|
||||
- atomic_container - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- clc_alert_policy - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_group - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_loadbalancer - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_server - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- cmd_runner module util - reusable command runner with consistent argument formatting and sensible defaults (https://github.com/ansible-collections/community.general/pull/4476).
|
||||
- datadog_monitor - support new datadog event monitor of type `event-v2 alert` (https://github.com/ansible-collections/community.general/pull/4457)
|
||||
- filesystem - add support for resizing btrfs (https://github.com/ansible-collections/community.general/issues/4465).
|
||||
- lxd_container - adds ``project`` option to allow selecting project for LXD instance (https://github.com/ansible-collections/community.general/pull/4479).
|
||||
- lxd_profile - adds ``project`` option to allow selecting project for LXD profile (https://github.com/ansible-collections/community.general/pull/4479).
|
||||
- nmap inventory plugin - add ``sudo`` option in plugin in order to execute ``sudo nmap`` so that ``nmap`` runs with elevated privileges (https://github.com/ansible-collections/community.general/pull/4506).
|
||||
- nomad_job - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- nomad_job_info - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_device - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_sshkey - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_volume - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- profitbricks - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- proxmox - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- proxmox inventory plugin - add token authentication as an alternative to username/password (https://github.com/ansible-collections/community.general/pull/4540).
|
||||
- proxmox inventory plugin - parse LXC configs returned by the proxmox API (https://github.com/ansible-collections/community.general/pull/4472).
|
||||
- proxmox_snap - add restore snapshot option (https://github.com/ansible-collections/community.general/pull/4377).
|
||||
- proxmox_snap - fixed timeout value to correctly reflect time in seconds. The timeout was off by one second (https://github.com/ansible-collections/community.general/pull/4377).
|
||||
- redfish_command - add ``IndicatorLedOn``, ``IndicatorLedOff``, and ``IndicatorLedBlink`` commands to the Systems category for controling system LEDs (https://github.com/ansible-collections/community.general/issues/4084).
|
||||
- seport - minor refactoring (https://github.com/ansible-collections/community.general/pull/4471).
|
||||
- smartos_image_info - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- terraform - adds ``terraform_upgrade`` parameter which allows ``terraform init`` to satisfy new provider constraints in an existing Terraform project (https://github.com/ansible-collections/community.general/issues/4333).
|
||||
- udm_group - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- udm_share - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- vmadm - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- webfaction_app - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- webfaction_db - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- xfconf - added missing value types ``char``, ``uchar``, ``int64`` and ``uint64`` (https://github.com/ansible-collections/community.general/pull/4534).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- nmcli - deprecate default hairpin mode for a bridge. This so we can change it to ``false`` in community.general 7.0.0, as this is also the default in ``nmcli`` (https://github.com/ansible-collections/community.general/pull/4334).
|
||||
- proxmox inventory plugin - the current default ``true`` of the ``want_proxmox_nodes_ansible_host`` option has been deprecated. The default will change to ``false`` in community.general 6.0.0. To keep the current behavior, explicitly set ``want_proxmox_nodes_ansible_host`` to ``true`` in your inventory configuration. We suggest to already switch to the new behavior by explicitly setting it to ``false``, and by using ``compose:`` to set ``ansible_host`` to the correct value. See the examples in the plugin documentation for details (https://github.com/ansible-collections/community.general/pull/4466).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- dnsmadeeasy - fix failure on deleting DNS entries when API response does not contain monitor value (https://github.com/ansible-collections/community.general/issues/3620).
|
||||
- git_branch - remove deprecated and unnecessary branch ``unprotect`` method (https://github.com/ansible-collections/community.general/pull/4496).
|
||||
- gitlab_group - improve searching for projects inside group on deletion (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
- gitlab_group_members - handle more than 20 groups when finding a group (https://github.com/ansible-collections/community.general/pull/4491, https://github.com/ansible-collections/community.general/issues/4460, https://github.com/ansible-collections/community.general/issues/3729).
|
||||
- gitlab_hook - handle more than 20 hooks when finding a hook (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
- gitlab_project - handle more than 20 namespaces when finding a namespace (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
- gitlab_project_members - handle more than 20 projects and users when finding a project resp. user (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
- gitlab_user - handle more than 20 users and SSH keys when finding a user resp. SSH key (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
- keycloak - fix parameters types for ``defaultDefaultClientScopes`` and ``defaultOptionalClientScopes`` from list of dictionaries to list of strings (https://github.com/ansible-collections/community.general/pull/4526).
|
||||
- opennebula inventory plugin - complete the implementation of ``constructable`` for opennebula inventory plugin. Now ``keyed_groups``, ``compose``, ``groups`` actually work (https://github.com/ansible-collections/community.general/issues/4497).
|
||||
- pacman - fixed bug where ``absent`` state did not work for locally installed packages (https://github.com/ansible-collections/community.general/pull/4464).
|
||||
- pritunl - fixed bug where pritunl plugin api add unneeded data in ``auth_string`` parameter (https://github.com/ansible-collections/community.general/issues/4527).
|
||||
- proxmox inventory plugin - fix error when parsing container with LXC configs (https://github.com/ansible-collections/community.general/issues/4472, https://github.com/ansible-collections/community.general/pull/4472).
|
||||
- proxmox_kvm - fix a bug when getting a state of VM without name will fail (https://github.com/ansible-collections/community.general/pull/4508).
|
||||
- xbps - fix error message that is reported when installing packages fails (https://github.com/ansible-collections/community.general/pull/4438).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
Cloud
|
||||
~~~~~
|
||||
|
||||
lxd
|
||||
^^^
|
||||
|
||||
- lxd_project - Manage LXD projects
|
||||
|
||||
Monitoring
|
||||
~~~~~~~~~~
|
||||
|
||||
- alerta_customer - Manage customers in Alerta
|
||||
|
||||
v4.7.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- ipa_service - add ``skip_host_check`` parameter. (https://github.com/ansible-collections/community.general/pull/4417).
|
||||
- keycloak_client - add ``always_display_in_console`` parameter (https://github.com/ansible-collections/community.general/issues/4390).
|
||||
- keycloak_client - add ``default_client_scopes`` and ``optional_client_scopes`` parameters. (https://github.com/ansible-collections/community.general/pull/4385).
|
||||
- proxmox inventory plugin - add support for templating the ``url``, ``user``, and ``password`` options (https://github.com/ansible-collections/community.general/pull/4418).
|
||||
- sudoers - add support for ``runas`` parameter (https://github.com/ansible-collections/community.general/issues/4379).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- dsv lookup plugin - raise an Ansible error if the wrong ``python-dsv-sdk`` version is installed (https://github.com/ansible-collections/community.general/pull/4422).
|
||||
- keycloak_* - the documented ``validate_certs`` parameter was not taken into account when calling the ``open_url`` function in some cases, thus enforcing certificate validation even when ``validate_certs`` was set to ``false``. (https://github.com/ansible-collections/community.general/pull/4382)
|
||||
- nmcli - fix returning "changed" when routes parameters set, also suggest new routes4 and routes6 format (https://github.com/ansible-collections/community.general/issues/4131).
|
||||
- proxmox inventory plugin - fixed the ``tags_parsed`` field when Proxmox returns a single space for the ``tags`` entry (https://github.com/ansible-collections/community.general/pull/4378).
|
||||
- zypper - fixed bug that caused zypper to always report [ok] and do nothing on ``state=present`` when all packages in ``name`` had a version specification (https://github.com/ansible-collections/community.general/issues/4371, https://github.com/ansible-collections/community.general/pull/4421).
|
||||
|
||||
v4.6.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Extraordinary bugfix release to fix a breaking change in ``terraform``.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- lxd inventory plugin - do not crash if OS and release metadata are not present
|
||||
(https://github.com/ansible-collections/community.general/pull/4351).
|
||||
- terraform - revert bugfix https://github.com/ansible-collections/community.general/pull/4281 that tried to fix ``variable`` handling to allow complex values. It turned out that this was breaking several valid use-cases (https://github.com/ansible-collections/community.general/issues/4367, https://github.com/ansible-collections/community.general/pull/4370).
|
||||
|
||||
v4.6.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- jira - when creating a comment, ``fields`` now is used for additional data (https://github.com/ansible-collections/community.general/pull/4304).
|
||||
- ldap_entry - add support for recursive deletion (https://github.com/ansible-collections/community.general/issues/3613).
|
||||
- mksysb - revamped the module using ``ModuleHelper`` (https://github.com/ansible-collections/community.general/pull/3295).
|
||||
- nmcli - add missing connection aliases ``802-3-ethernet`` and ``802-11-wireless`` (https://github.com/ansible-collections/community.general/pull/4108).
|
||||
- nmcli - remove nmcli modify dependency on ``type`` parameter (https://github.com/ansible-collections/community.general/issues/2858).
|
||||
- npm - add ability to use ``production`` flag when ``ci`` is set (https://github.com/ansible-collections/community.general/pull/4299).
|
||||
- pacman - add ``remove_nosave`` parameter to avoid saving modified configuration files as ``.pacsave`` files. (https://github.com/ansible-collections/community.general/pull/4316, https://github.com/ansible-collections/community.general/issues/4315).
|
||||
- pacman - now implements proper change detection for ``update_cache=true``. Adds ``cache_updated`` return value to when ``update_cache=true`` to report this result independently of the module's overall changed return value (https://github.com/ansible-collections/community.general/pull/4337).
|
||||
- pipx - added options ``editable`` and ``pip_args`` (https://github.com/ansible-collections/community.general/issues/4300).
|
||||
- proxmox inventory plugin - add support for client-side jinja filters (https://github.com/ansible-collections/community.general/issues/3553).
|
||||
- redis - add authentication parameters ``login_user``, ``tls``, ``validate_certs``, and ``ca_certs`` (https://github.com/ansible-collections/community.general/pull/4207).
|
||||
- syslog_json - add option to skip logging of ``gather_facts`` playbook tasks; use v2 callback API (https://github.com/ansible-collections/community.general/pull/4223).
|
||||
- zypper - add support for ``--clean-deps`` option to remove packages that depend on a package being removed (https://github.com/ansible-collections/community.general/pull/4195).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- pacman - from community.general 5.0.0 on, the ``changed`` status of ``update_cache`` will no longer be ignored if ``name`` or ``upgrade`` is specified. To keep the old behavior, add something like ``register: result`` and ``changed_when: result.packages | length > 0`` to your task (https://github.com/ansible-collections/community.general/pull/4329).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- filesize - add support for busybox dd implementation, that is used by default on Alpine linux (https://github.com/ansible-collections/community.general/pull/4288, https://github.com/ansible-collections/community.general/issues/4259).
|
||||
- linode inventory plugin - fix configuration handling relating to inventory filtering (https://github.com/ansible-collections/community.general/pull/4336).
|
||||
- mksysb - fixed bug for parameter ``backup_dmapi_fs`` was passing the wrong CLI argument (https://github.com/ansible-collections/community.general/pull/3295).
|
||||
- pacman - Use ``--groups`` instead of ``--group`` (https://github.com/ansible-collections/community.general/pull/4312).
|
||||
- pacman - fix URL based package installation (https://github.com/ansible-collections/community.general/pull/4286, https://github.com/ansible-collections/community.general/issues/4285).
|
||||
- pacman - fix ``upgrade=yes`` (https://github.com/ansible-collections/community.general/pull/4275, https://github.com/ansible-collections/community.general/issues/4274).
|
||||
- pacman - make sure that ``packages`` is always returned when ``name`` or ``upgrade`` is specified, also if nothing is done (https://github.com/ansible-collections/community.general/pull/4329).
|
||||
- pacman - when the ``update_cache`` option is combined with another option such as ``upgrade``, report ``changed`` based on the actions performed by the latter option. This was the behavior in community.general 4.4.0 and before. In community.general 4.5.0, a task combining these options would always report ``changed`` (https://github.com/ansible-collections/community.general/pull/4318).
|
||||
- proxmox inventory plugin - always convert strings that follow the ``key=value[,key=value[...]]`` form into dictionaries (https://github.com/ansible-collections/community.general/pull/4349).
|
||||
- proxmox inventory plugin - fixed the ``description`` field being ignored if it contained a comma (https://github.com/ansible-collections/community.general/issues/4348).
|
||||
- proxmox_kvm - fix error in check when creating or cloning (https://github.com/ansible-collections/community.general/pull/4306).
|
||||
- proxmox_kvm - fix error when checking whether Proxmox VM exists (https://github.com/ansible-collections/community.general/pull/4287).
|
||||
- terraform - fix ``variable`` handling to allow complex values (https://github.com/ansible-collections/community.general/pull/4281).
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
- pacman - ``update_cache`` cannot differentiate between up to date and outdated package lists and will report ``changed`` in both situations (https://github.com/ansible-collections/community.general/pull/4318).
|
||||
- pacman - binaries specified in the ``executable`` parameter must support ``--print-format`` in order to be used by this module. In particular, AUR helper ``yay`` is known not to currently support it (https://github.com/ansible-collections/community.general/pull/4312).
|
||||
|
||||
v4.5.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API available since at least Ansible 2.9. This fixes some instances added since the last time this was fixed (https://github.com/ansible-collections/community.general/pull/4232).
|
||||
- ansible_galaxy_install - added option ``no_deps`` to the module (https://github.com/ansible-collections/community.general/issues/4174).
|
||||
- gitlab_group_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/pull/4038 and https://github.com/ansible-collections/community.general/issues/4074).
|
||||
- keycloak_* modules - added connection timeout parameter when calling server (https://github.com/ansible-collections/community.general/pull/4168).
|
||||
- linode inventory plugin - add support for caching inventory results (https://github.com/ansible-collections/community.general/pull/4179).
|
||||
- opentelemetry_plugin - enrich service when using the ``jenkins``, ``hetzner`` or ``jira`` modules (https://github.com/ansible-collections/community.general/pull/4105).
|
||||
- pacman - the module has been rewritten and is now much faster when using ``state=latest``. Operations are now done all packages at once instead of package per package and the configured output format of ``pacman`` no longer affect the module's operation. (https://github.com/ansible-collections/community.general/pull/3907, https://github.com/ansible-collections/community.general/issues/3783, https://github.com/ansible-collections/community.general/issues/4079)
|
||||
- passwordstore lookup plugin - add configurable ``lock`` and ``locktimeout`` options to avoid race conditions in itself and in the ``pass`` utility it calls. By default, the plugin now locks on write operations (https://github.com/ansible-collections/community.general/pull/4194).
|
||||
- proxmox modules - move common code into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4029).
|
||||
- proxmox_kvm - added EFI disk support when creating VM with OVMF UEFI BIOS with new ``efidisk0`` option (https://github.com/ansible-collections/community.general/pull/4106, https://github.com/ansible-collections/community.general/issues/1638).
|
||||
- proxmox_kwm - add ``win11`` to ``ostype`` parameter for Windows 11 and Windows Server 2022 support (https://github.com/ansible-collections/community.general/issues/4023, https://github.com/ansible-collections/community.general/pull/4191).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- dconf - skip processes that disappeared while we inspected them (https://github.com/ansible-collections/community.general/issues/4151).
|
||||
- gitlab_group_variable - add missing documentation about GitLab versions that support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
- gitlab_group_variable - allow to set same variable name under different environment scopes. Due this change, the return value ``group_variable`` differs from previous version in check mode. It was counting ``updated`` values, because it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
- gitlab_group_variable - fix idempotent change behaviour for float and integer variables (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
- gitlab_project_variable - ``value`` is not necessary when deleting variables (https://github.com/ansible-collections/community.general/pull/4150).
|
||||
- gitlab_runner - make ``project`` and ``owned`` mutually exclusive (https://github.com/ansible-collections/community.general/pull/4136).
|
||||
- homebrew_cask - fix force install operation (https://github.com/ansible-collections/community.general/issues/3703).
|
||||
- imc_rest - fixes the module failure due to the usage of ``itertools.izip_longest`` which is not available in Python 3 (https://github.com/ansible-collections/community.general/issues/4206).
|
||||
- ini_file - when removing nothing do not report changed (https://github.com/ansible-collections/community.general/issues/4154).
|
||||
- keycloak_user_federation - creating a user federation while specifying an ID (that does not exist yet) no longer fail with a 404 Not Found (https://github.com/ansible-collections/community.general/pull/4212).
|
||||
- keycloak_user_federation - mappers auto-created by keycloak are matched and merged by their name and no longer create duplicated entries (https://github.com/ansible-collections/community.general/pull/4212).
|
||||
- mail callback plugin - fix encoding of the name of sender and recipient (https://github.com/ansible-collections/community.general/issues/4060, https://github.com/ansible-collections/community.general/pull/4061).
|
||||
- passwordstore lookup plugin - fix error detection for non-English locales (https://github.com/ansible-collections/community.general/pull/4219).
|
||||
- passwordstore lookup plugin - prevent returning path names as passwords by accident (https://github.com/ansible-collections/community.general/issues/4185, https://github.com/ansible-collections/community.general/pull/4192).
|
||||
- vdo - fix options error (https://github.com/ansible-collections/community.general/pull/4163).
|
||||
- yum_versionlock - fix matching of existing entries with names passed to the module. Match yum and dnf lock format (https://github.com/ansible-collections/community.general/pull/4183).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
Cloud
|
||||
~~~~~
|
||||
|
||||
scaleway
|
||||
^^^^^^^^
|
||||
|
||||
- scaleway_private_network - Scaleway private network management
|
||||
|
||||
Storage
|
||||
~~~~~~~
|
||||
|
||||
pmem
|
||||
^^^^
|
||||
|
||||
- pmem - Configure Intel Optane Persistent Memory modules
|
||||
|
||||
v4.4.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular features and bugfixes release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- cobbler inventory plugin - add ``include_profiles`` option (https://github.com/ansible-collections/community.general/pull/4068).
|
||||
- gitlab_project_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- icinga2 inventory plugin - implemented constructed interface (https://github.com/ansible-collections/community.general/pull/4088).
|
||||
- linode inventory plugin - allow templating of ``access_token`` variable in Linode inventory plugin (https://github.com/ansible-collections/community.general/pull/4040).
|
||||
- lists_mergeby filter plugin - add parameters ``list_merge`` and ``recursive``. These are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9 (https://github.com/ansible-collections/community.general/pull/4058).
|
||||
- lxc_container - added ``wait_for_container`` parameter. If ``true`` the module will wait until the running task reports success as the status (https://github.com/ansible-collections/community.general/pull/4039).
|
||||
- mail callback plugin - add ``Message-ID`` and ``Date`` headers (https://github.com/ansible-collections/community.general/issues/4055, https://github.com/ansible-collections/community.general/pull/4056).
|
||||
- mail callback plugin - properly use Ansible's option handling to split lists (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
- nmcli - adds ``routes6`` and ``route_metric6`` parameters for supporting IPv6 routes (https://github.com/ansible-collections/community.general/issues/4059).
|
||||
- opennebula - add the release action for VMs in the ``HOLD`` state (https://github.com/ansible-collections/community.general/pull/4036).
|
||||
- opentelemetry_plugin - enrich service when using the ``docker_login`` (https://github.com/ansible-collections/community.general/pull/4104).
|
||||
- proxmox modules - move ``HAS_PROXMOXER`` check into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4030).
|
||||
- scaleway inventory plugin - add profile parameter ``scw_profile`` (https://github.com/ansible-collections/community.general/pull/4049).
|
||||
- snap - add option ``options`` permitting to set options using the ``snap set`` command (https://github.com/ansible-collections/community.general/pull/3943).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- mail callback plugin - not specifying ``sender`` is deprecated and will be disallowed in community.general 6.0.0 (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cargo - fix detection of outdated packages when ``state=latest`` (https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- cargo - fix incorrectly reported changed status for packages with a name containing a hyphen (https://github.com/ansible-collections/community.general/issues/4044, https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- gitlab_project_variable - add missing documentation about GitLab versions that support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_project_variable - allow to set same variable name under different environment scopes. Due this change, the return value ``project_variable`` differs from previous version in check mode. It was counting ``updated`` values, because it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_project_variable - fix idempotent change behaviour for float and integer variables (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_runner - use correct API endpoint to create and retrieve project level runners when using ``project`` (https://github.com/ansible-collections/community.general/pull/3965).
|
||||
- listen_ports_facts - local port regex was not handling well IPv6 only binding. Fixes the regex for ``ss`` (https://github.com/ansible-collections/community.general/pull/4092).
|
||||
- mail callback plugin - fix crash on Python 3 (https://github.com/ansible-collections/community.general/issues/4025, https://github.com/ansible-collections/community.general/pull/4026).
|
||||
- opentelemetry - fix generating a trace with a task containing ``no_log: true`` (https://github.com/ansible-collections/community.general/pull/4043).
|
||||
- python_requirements_info - store ``mismatched`` return values per package as documented in the module (https://github.com/ansible-collections/community.general/pull/4078).
|
||||
- yarn - fix incorrect handling of ``yarn list`` and ``yarn global list`` output that could result in fatal error (https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix incorrectly reported status when installing a package globally (https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix missing ``~`` expansion in yarn global install folder which resulted in incorrect task status (https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4048).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
System
|
||||
~~~~~~
|
||||
|
||||
- homectl - Manage user accounts with systemd-homed
|
||||
|
||||
v4.3.0
|
||||
======
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Also, consider taking up a valuable, reviewed, but abandoned pull request which
|
||||
|
||||
* Try committing your changes with an informative but short commit message.
|
||||
* Do not squash your commits and force-push to your branch if not needed. Reviews of your pull request are much easier with individual commits to comprehend the pull request history. All commits of your pull request branch will be squashed into one commit by GitHub upon merge.
|
||||
* Do not add merge commits to your PR. The bot will complain and you will have to rebase ([instructions for rebasing](https://docs.ansible.com/ansible/latest/dev_guide/developing_rebasing.html)) to remove them before your PR can be merged. To avoid that git automatically does merges during pulls, you can configure it to do rebases instead by running `git config pull.rebase true` inside the respository checkout.
|
||||
* Do not add merge commits to your PR. The bot will complain and you will have to rebase ([instructions for rebasing](https://docs.ansible.com/ansible/latest/dev_guide/developing_rebasing.html)) to remove them before your PR can be merged. To avoid that git automatically does merges during pulls, you can configure it to do rebases instead by running `git config pull.rebase true` inside the repository checkout.
|
||||
* Make sure your PR includes a [changelog fragment](https://docs.ansible.com/ansible/devel/community/development_process.html#changelogs-how-to). (You must not include a fragment for new modules or new plugins, except for test and filter plugins. Also you shouldn't include one for docs-only changes. If you're not sure, simply don't include one, we'll tell you whether one is needed or not :) )
|
||||
* Avoid reformatting unrelated parts of the codebase in your PR. These types of changes will likely be requested for reversion, create additional work for reviewers, and may cause approval to be delayed.
|
||||
|
||||
@@ -36,6 +36,54 @@ If you want to test a PR locally, refer to [our testing guide](https://github.co
|
||||
|
||||
If you find any inconsistencies or places in this document which can be improved, feel free to raise an issue or pull request to fix it.
|
||||
|
||||
## Run sanity, unit or integration tests locally
|
||||
|
||||
You have to check out the repository into a specific path structure to be able to run `ansible-test`. The path to the git checkout must end with `.../ansible_collections/community/general`. Please see [our testing guide](https://github.com/ansible/community-docs/blob/main/test_pr_locally_guide.rst) for instructions on how to check out the repository into a correct path structure. The short version of these instructions is:
|
||||
|
||||
```.bash
|
||||
mkdir -p ~/dev/ansible_collections/community
|
||||
git clone https://github.com/ansible-collections/community.general.git ~/dev/ansible_collections/community/general
|
||||
cd ~/dev/ansible_collections/community/general
|
||||
```
|
||||
|
||||
Then you can run `ansible-test` (which is a part of [ansible-core](https://pypi.org/project/ansible-core/)) inside the checkout. The following example commands expect that you have installed Docker or Podman. Note that Podman has only been supported by more recent ansible-core releases. If you are using Docker, the following will work with Ansible 2.9+.
|
||||
|
||||
The following commands show how to run sanity tests:
|
||||
|
||||
```.bash
|
||||
# Run sanity tests for all files in the collection:
|
||||
ansible-test sanity --docker -v
|
||||
|
||||
# Run sanity tests for the given files and directories:
|
||||
ansible-test sanity --docker -v plugins/modules/system/pids.py tests/integration/targets/pids/
|
||||
```
|
||||
|
||||
The following commands show how to run unit tests:
|
||||
|
||||
```.bash
|
||||
# Run all unit tests:
|
||||
ansible-test units --docker -v
|
||||
|
||||
# Run all unit tests for one Python version (a lot faster):
|
||||
ansible-test units --docker -v --python 3.8
|
||||
|
||||
# Run a specific unit test (for the nmcli module) for one Python version:
|
||||
ansible-test units --docker -v --python 3.8 tests/unit/plugins/modules/net_tools/test_nmcli.py
|
||||
```
|
||||
|
||||
The following commands show how to run integration tests:
|
||||
|
||||
```.bash
|
||||
# Run integration tests for the interfaces_files module in a Docker container using the
|
||||
# fedora35 operating system image (the supported images depend on your ansible-core version):
|
||||
ansible-test integration --docker fedora35 -v interfaces_file
|
||||
|
||||
# Run integration tests for the flattened lookup **without any isolation**:
|
||||
ansible-test integration -v lookup_flattened
|
||||
```
|
||||
|
||||
If you are unsure about the integration test target name for a module or plugin, you can take a look in `tests/integration/targets/`. Tests for plugins have the plugin type prepended.
|
||||
|
||||
## Creating new modules or plugins
|
||||
|
||||
Creating new modules and plugins requires a bit more work than other Pull Requests.
|
||||
|
||||
@@ -17,7 +17,7 @@ If you encounter abusive behavior violating the [Ansible Code of Conduct](https:
|
||||
|
||||
## Tested with Ansible
|
||||
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
|
||||
|
||||
## External requirements
|
||||
|
||||
|
||||
@@ -1309,3 +1309,476 @@ releases:
|
||||
name: counter
|
||||
namespace: null
|
||||
release_date: '2022-01-11'
|
||||
4.4.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- cargo - fix detection of outdated packages when ``state=latest`` (https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- cargo - fix incorrectly reported changed status for packages with a name containing
|
||||
a hyphen (https://github.com/ansible-collections/community.general/issues/4044,
|
||||
https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- gitlab_project_variable - add missing documentation about GitLab versions
|
||||
that support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- 'gitlab_project_variable - allow to set same variable name under different
|
||||
environment scopes. Due this change, the return value ``project_variable``
|
||||
differs from previous version in check mode. It was counting ``updated`` values,
|
||||
because it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
|
||||
'
|
||||
- gitlab_project_variable - fix idempotent change behaviour for float and integer
|
||||
variables (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_runner - use correct API endpoint to create and retrieve project level
|
||||
runners when using ``project`` (https://github.com/ansible-collections/community.general/pull/3965).
|
||||
- listen_ports_facts - local port regex was not handling well IPv6 only binding.
|
||||
Fixes the regex for ``ss`` (https://github.com/ansible-collections/community.general/pull/4092).
|
||||
- mail callback plugin - fix crash on Python 3 (https://github.com/ansible-collections/community.general/issues/4025,
|
||||
https://github.com/ansible-collections/community.general/pull/4026).
|
||||
- 'opentelemetry - fix generating a trace with a task containing ``no_log: true``
|
||||
(https://github.com/ansible-collections/community.general/pull/4043).'
|
||||
- python_requirements_info - store ``mismatched`` return values per package
|
||||
as documented in the module (https://github.com/ansible-collections/community.general/pull/4078).
|
||||
- yarn - fix incorrect handling of ``yarn list`` and ``yarn global list`` output
|
||||
that could result in fatal error (https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix incorrectly reported status when installing a package globally
|
||||
(https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix missing ``~`` expansion in yarn global install folder which resulted
|
||||
in incorrect task status (https://github.com/ansible-collections/community.general/issues/4045,
|
||||
https://github.com/ansible-collections/community.general/pull/4048).
|
||||
deprecated_features:
|
||||
- mail callback plugin - not specifying ``sender`` is deprecated and will be
|
||||
disallowed in community.general 6.0.0 (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
minor_changes:
|
||||
- cobbler inventory plugin - add ``include_profiles`` option (https://github.com/ansible-collections/community.general/pull/4068).
|
||||
- gitlab_project_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- icinga2 inventory plugin - implemented constructed interface (https://github.com/ansible-collections/community.general/pull/4088).
|
||||
- linode inventory plugin - allow templating of ``access_token`` variable in
|
||||
Linode inventory plugin (https://github.com/ansible-collections/community.general/pull/4040).
|
||||
- lists_mergeby filter plugin - add parameters ``list_merge`` and ``recursive``.
|
||||
These are only supported when used with ansible-base 2.10 or ansible-core,
|
||||
but not with Ansible 2.9 (https://github.com/ansible-collections/community.general/pull/4058).
|
||||
- lxc_container - added ``wait_for_container`` parameter. If ``true`` the module
|
||||
will wait until the running task reports success as the status (https://github.com/ansible-collections/community.general/pull/4039).
|
||||
- mail callback plugin - add ``Message-ID`` and ``Date`` headers (https://github.com/ansible-collections/community.general/issues/4055,
|
||||
https://github.com/ansible-collections/community.general/pull/4056).
|
||||
- mail callback plugin - properly use Ansible's option handling to split lists
|
||||
(https://github.com/ansible-collections/community.general/pull/4140).
|
||||
- nmcli - adds ``routes6`` and ``route_metric6`` parameters for supporting IPv6
|
||||
routes (https://github.com/ansible-collections/community.general/issues/4059).
|
||||
- opennebula - add the release action for VMs in the ``HOLD`` state (https://github.com/ansible-collections/community.general/pull/4036).
|
||||
- opentelemetry_plugin - enrich service when using the ``docker_login`` (https://github.com/ansible-collections/community.general/pull/4104).
|
||||
- proxmox modules - move ``HAS_PROXMOXER`` check into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4030).
|
||||
- scaleway inventory plugin - add profile parameter ``scw_profile`` (https://github.com/ansible-collections/community.general/pull/4049).
|
||||
- snap - add option ``options`` permitting to set options using the ``snap set``
|
||||
command (https://github.com/ansible-collections/community.general/pull/3943).
|
||||
release_summary: Regular features and bugfixes release.
|
||||
fragments:
|
||||
- 3935-use-gitlab-instance-runner-to-create-runner.yml
|
||||
- 3943-add-option-options-to-snap-module.yml
|
||||
- 4.4.0.yml
|
||||
- 4026-fix-mail-callback.yml
|
||||
- 4030-proxmox-has-proxmoxer.yml
|
||||
- 4036-onevm-add-release-action.yaml
|
||||
- 4038-fix-and-rework-gitlb-project-variable.yml
|
||||
- 4039-cluster-container-wait.yml
|
||||
- 4040-linode-token-templating.yaml
|
||||
- 4043-fix-no-log-opentelemetry.yml
|
||||
- 4048-expand-tilde-in-yarn-global-install-folder.yaml
|
||||
- 4049-profile-for-scaleway-inventory.yml
|
||||
- 4050-properly-parse-json-lines-output-from-yarn.yaml
|
||||
- 4052-fix-detection-of-installed-cargo-packages-with-hyphens.yaml
|
||||
- 4056-add-missing-mail-headers.yml
|
||||
- 4058-lists_mergeby-add-parameters.yml
|
||||
- 4062-nmcli-ipv6-routes-support.yml
|
||||
- 4068-add-include_file-option.yml
|
||||
- 4078-python_requirements_info.yaml
|
||||
- 4088-add-constructed-interface-for-icinga2-inventory.yml
|
||||
- 4092-fix_local_ports_regex_listen_ports_facts.yaml
|
||||
- 4104-opentelemetry_plugin-enrich_docker_login.yaml
|
||||
- 4140-mail-callback-options.yml
|
||||
modules:
|
||||
- description: Manage user accounts with systemd-homed
|
||||
name: homectl
|
||||
namespace: system
|
||||
release_date: '2022-02-01'
|
||||
4.5.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- dconf - skip processes that disappeared while we inspected them (https://github.com/ansible-collections/community.general/issues/4151).
|
||||
- gitlab_group_variable - add missing documentation about GitLab versions that
|
||||
support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
- 'gitlab_group_variable - allow to set same variable name under different environment
|
||||
scopes. Due this change, the return value ``group_variable`` differs from
|
||||
previous version in check mode. It was counting ``updated`` values, because
|
||||
it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
|
||||
'
|
||||
- gitlab_group_variable - fix idempotent change behaviour for float and integer
|
||||
variables (https://github.com/ansible-collections/community.general/pull/4038).
|
||||
- gitlab_project_variable - ``value`` is not necessary when deleting variables
|
||||
(https://github.com/ansible-collections/community.general/pull/4150).
|
||||
- gitlab_runner - make ``project`` and ``owned`` mutually exclusive (https://github.com/ansible-collections/community.general/pull/4136).
|
||||
- homebrew_cask - fix force install operation (https://github.com/ansible-collections/community.general/issues/3703).
|
||||
- imc_rest - fixes the module failure due to the usage of ``itertools.izip_longest``
|
||||
which is not available in Python 3 (https://github.com/ansible-collections/community.general/issues/4206).
|
||||
- ini_file - when removing nothing do not report changed (https://github.com/ansible-collections/community.general/issues/4154).
|
||||
- keycloak_user_federation - creating a user federation while specifying an
|
||||
ID (that does not exist yet) no longer fail with a 404 Not Found (https://github.com/ansible-collections/community.general/pull/4212).
|
||||
- keycloak_user_federation - mappers auto-created by keycloak are matched and
|
||||
merged by their name and no longer create duplicated entries (https://github.com/ansible-collections/community.general/pull/4212).
|
||||
- mail callback plugin - fix encoding of the name of sender and recipient (https://github.com/ansible-collections/community.general/issues/4060,
|
||||
https://github.com/ansible-collections/community.general/pull/4061).
|
||||
- passwordstore lookup plugin - fix error detection for non-English locales
|
||||
(https://github.com/ansible-collections/community.general/pull/4219).
|
||||
- passwordstore lookup plugin - prevent returning path names as passwords by
|
||||
accident (https://github.com/ansible-collections/community.general/issues/4185,
|
||||
https://github.com/ansible-collections/community.general/pull/4192).
|
||||
- vdo - fix options error (https://github.com/ansible-collections/community.general/pull/4163).
|
||||
- yum_versionlock - fix matching of existing entries with names passed to the
|
||||
module. Match yum and dnf lock format (https://github.com/ansible-collections/community.general/pull/4183).
|
||||
minor_changes:
|
||||
- Avoid internal ansible-core module_utils in favor of equivalent public API
|
||||
available since at least Ansible 2.9. This fixes some instances added since
|
||||
the last time this was fixed (https://github.com/ansible-collections/community.general/pull/4232).
|
||||
- ansible_galaxy_install - added option ``no_deps`` to the module (https://github.com/ansible-collections/community.general/issues/4174).
|
||||
- gitlab_group_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/pull/4038
|
||||
and https://github.com/ansible-collections/community.general/issues/4074).
|
||||
- keycloak_* modules - added connection timeout parameter when calling server
|
||||
(https://github.com/ansible-collections/community.general/pull/4168).
|
||||
- linode inventory plugin - add support for caching inventory results (https://github.com/ansible-collections/community.general/pull/4179).
|
||||
- opentelemetry_plugin - enrich service when using the ``jenkins``, ``hetzner``
|
||||
or ``jira`` modules (https://github.com/ansible-collections/community.general/pull/4105).
|
||||
- pacman - the module has been rewritten and is now much faster when using ``state=latest``.
|
||||
Operations are now done all packages at once instead of package per package
|
||||
and the configured output format of ``pacman`` no longer affect the module's
|
||||
operation. (https://github.com/ansible-collections/community.general/pull/3907,
|
||||
https://github.com/ansible-collections/community.general/issues/3783, https://github.com/ansible-collections/community.general/issues/4079)
|
||||
- passwordstore lookup plugin - add configurable ``lock`` and ``locktimeout``
|
||||
options to avoid race conditions in itself and in the ``pass`` utility it
|
||||
calls. By default, the plugin now locks on write operations (https://github.com/ansible-collections/community.general/pull/4194).
|
||||
- proxmox modules - move common code into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4029).
|
||||
- proxmox_kvm - added EFI disk support when creating VM with OVMF UEFI BIOS
|
||||
with new ``efidisk0`` option (https://github.com/ansible-collections/community.general/pull/4106,
|
||||
https://github.com/ansible-collections/community.general/issues/1638).
|
||||
- proxmox_kwm - add ``win11`` to ``ostype`` parameter for Windows 11 and Windows
|
||||
Server 2022 support (https://github.com/ansible-collections/community.general/issues/4023,
|
||||
https://github.com/ansible-collections/community.general/pull/4191).
|
||||
release_summary: Regular feature and bugfix release.
|
||||
fragments:
|
||||
- 3703-force-install-homebrew-cask.yml
|
||||
- 3907-pacman-speedup.yml
|
||||
- 3916-fix-vdo-options-type.yml
|
||||
- 4.5.0.yml
|
||||
- 4029-proxmox-refactor.yml
|
||||
- 4061-fix-mail-recipient-encoding.yml
|
||||
- 4086-rework_of_gitlab_proyect_variable_over_gitlab_group_variable.yml
|
||||
- 4105-opentelemetry_plugin-enrich_jira_hetzner_jenkins_services.yaml
|
||||
- 4106-proxmox-efidisk0-support.yaml
|
||||
- 4136-gitlab_runner-make-project-owned-mutually-exclusive.yml
|
||||
- 4150-gitlab-project-variable-absent-fix.yml
|
||||
- 4151-dconf-catch-psutil-nosuchprocess.yaml
|
||||
- 4154-ini_file_changed.yml
|
||||
- 4168-add-keycloak-url-timeout.yml
|
||||
- 4179-linode-inventory-cache.yaml
|
||||
- 4183-fix-yum_versionlock.yaml
|
||||
- 4191-proxmox-add-win11.yml
|
||||
- 4192-improve-passwordstore-consistency.yml
|
||||
- 4194-configurable-passwordstore-locking.yml
|
||||
- 4206-imc-rest-module.yaml
|
||||
- 4212-fixes-for-keycloak-user-federation.yml
|
||||
- 4219-passwordstore-locale-fix.yml
|
||||
- 4232-text-converter-import.yml
|
||||
- 4240-ansible_galaxy_install-no_deps.yml
|
||||
modules:
|
||||
- description: Configure Intel Optane Persistent Memory modules
|
||||
name: pmem
|
||||
namespace: storage.pmem
|
||||
- description: Scaleway private network management
|
||||
name: scaleway_private_network
|
||||
namespace: cloud.scaleway
|
||||
release_date: '2022-02-22'
|
||||
4.6.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- filesize - add support for busybox dd implementation, that is used by default
|
||||
on Alpine linux (https://github.com/ansible-collections/community.general/pull/4288,
|
||||
https://github.com/ansible-collections/community.general/issues/4259).
|
||||
- linode inventory plugin - fix configuration handling relating to inventory
|
||||
filtering (https://github.com/ansible-collections/community.general/pull/4336).
|
||||
- mksysb - fixed bug for parameter ``backup_dmapi_fs`` was passing the wrong
|
||||
CLI argument (https://github.com/ansible-collections/community.general/pull/3295).
|
||||
- pacman - Use ``--groups`` instead of ``--group`` (https://github.com/ansible-collections/community.general/pull/4312).
|
||||
- pacman - fix URL based package installation (https://github.com/ansible-collections/community.general/pull/4286,
|
||||
https://github.com/ansible-collections/community.general/issues/4285).
|
||||
- pacman - fix ``upgrade=yes`` (https://github.com/ansible-collections/community.general/pull/4275,
|
||||
https://github.com/ansible-collections/community.general/issues/4274).
|
||||
- pacman - make sure that ``packages`` is always returned when ``name`` or ``upgrade``
|
||||
is specified, also if nothing is done (https://github.com/ansible-collections/community.general/pull/4329).
|
||||
- pacman - when the ``update_cache`` option is combined with another option
|
||||
such as ``upgrade``, report ``changed`` based on the actions performed by
|
||||
the latter option. This was the behavior in community.general 4.4.0 and before.
|
||||
In community.general 4.5.0, a task combining these options would always report
|
||||
``changed`` (https://github.com/ansible-collections/community.general/pull/4318).
|
||||
- proxmox inventory plugin - always convert strings that follow the ``key=value[,key=value[...]]``
|
||||
form into dictionaries (https://github.com/ansible-collections/community.general/pull/4349).
|
||||
- proxmox inventory plugin - fixed the ``description`` field being ignored if
|
||||
it contained a comma (https://github.com/ansible-collections/community.general/issues/4348).
|
||||
- proxmox_kvm - fix error in check when creating or cloning (https://github.com/ansible-collections/community.general/pull/4306).
|
||||
- proxmox_kvm - fix error when checking whether Proxmox VM exists (https://github.com/ansible-collections/community.general/pull/4287).
|
||||
- terraform - fix ``variable`` handling to allow complex values (https://github.com/ansible-collections/community.general/pull/4281).
|
||||
deprecated_features:
|
||||
- 'pacman - from community.general 5.0.0 on, the ``changed`` status of ``update_cache``
|
||||
will no longer be ignored if ``name`` or ``upgrade`` is specified. To keep
|
||||
the old behavior, add something like ``register: result`` and ``changed_when:
|
||||
result.packages | length > 0`` to your task (https://github.com/ansible-collections/community.general/pull/4329).'
|
||||
known_issues:
|
||||
- pacman - ``update_cache`` cannot differentiate between up to date and outdated
|
||||
package lists and will report ``changed`` in both situations (https://github.com/ansible-collections/community.general/pull/4318).
|
||||
- pacman - binaries specified in the ``executable`` parameter must support ``--print-format``
|
||||
in order to be used by this module. In particular, AUR helper ``yay`` is known
|
||||
not to currently support it (https://github.com/ansible-collections/community.general/pull/4312).
|
||||
minor_changes:
|
||||
- jira - when creating a comment, ``fields`` now is used for additional data
|
||||
(https://github.com/ansible-collections/community.general/pull/4304).
|
||||
- ldap_entry - add support for recursive deletion (https://github.com/ansible-collections/community.general/issues/3613).
|
||||
- mksysb - revamped the module using ``ModuleHelper`` (https://github.com/ansible-collections/community.general/pull/3295).
|
||||
- nmcli - add missing connection aliases ``802-3-ethernet`` and ``802-11-wireless``
|
||||
(https://github.com/ansible-collections/community.general/pull/4108).
|
||||
- nmcli - remove nmcli modify dependency on ``type`` parameter (https://github.com/ansible-collections/community.general/issues/2858).
|
||||
- npm - add ability to use ``production`` flag when ``ci`` is set (https://github.com/ansible-collections/community.general/pull/4299).
|
||||
- pacman - add ``remove_nosave`` parameter to avoid saving modified configuration
|
||||
files as ``.pacsave`` files. (https://github.com/ansible-collections/community.general/pull/4316,
|
||||
https://github.com/ansible-collections/community.general/issues/4315).
|
||||
- pacman - now implements proper change detection for ``update_cache=true``.
|
||||
Adds ``cache_updated`` return value to when ``update_cache=true`` to report
|
||||
this result independently of the module's overall changed return value (https://github.com/ansible-collections/community.general/pull/4337).
|
||||
- pipx - added options ``editable`` and ``pip_args`` (https://github.com/ansible-collections/community.general/issues/4300).
|
||||
- proxmox inventory plugin - add support for client-side jinja filters (https://github.com/ansible-collections/community.general/issues/3553).
|
||||
- redis - add authentication parameters ``login_user``, ``tls``, ``validate_certs``,
|
||||
and ``ca_certs`` (https://github.com/ansible-collections/community.general/pull/4207).
|
||||
- syslog_json - add option to skip logging of ``gather_facts`` playbook tasks;
|
||||
use v2 callback API (https://github.com/ansible-collections/community.general/pull/4223).
|
||||
- zypper - add support for ``--clean-deps`` option to remove packages that depend
|
||||
on a package being removed (https://github.com/ansible-collections/community.general/pull/4195).
|
||||
release_summary: Regular feature and bugfix release.
|
||||
fragments:
|
||||
- 3295-mksysb-revamp.yaml
|
||||
- 4.6.0.yml
|
||||
- 4108-nmcli-support-modifcation-without-type-param.yml
|
||||
- 4192-zypper-add-clean-deps.yml
|
||||
- 4207-add-redis-tls-support.yml
|
||||
- 4223-syslog-json-skip-syslog-option.yml
|
||||
- 4275-pacman-sysupgrade.yml
|
||||
- 4281-terraform-complex-variables.yml
|
||||
- 4286-pacman-url-pkgs.yml
|
||||
- 4287-fix-proxmox-vm-chek.yml
|
||||
- 4288-fix-4259-support-busybox-dd.yml
|
||||
- 4299-npm-add-production-with-ci-flag.yml
|
||||
- 4303-pipx-editable.yml
|
||||
- 4304-jira-fields-in-comment.yml
|
||||
- 4306-proxmox-fix-error-on-vm-clone.yml
|
||||
- 4312-pacman-groups.yml
|
||||
- 4316-pacman-remove-nosave.yml
|
||||
- 4318-pacman-restore-old-changed-behavior.yml
|
||||
- 4330-pacman-packages-update_cache.yml
|
||||
- 4336-linode-inventory-filtering.yaml
|
||||
- 4337-pacman-update_cache.yml
|
||||
- 4349-proxmox-inventory-dict-facts.yml
|
||||
- 4352-proxmox-inventory-filters.yml
|
||||
- 4355-ldap-recursive-delete.yml
|
||||
release_date: '2022-03-15'
|
||||
4.6.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- 'lxd inventory plugin - do not crash if OS and release metadata are not present
|
||||
|
||||
(https://github.com/ansible-collections/community.general/pull/4351).
|
||||
|
||||
'
|
||||
- terraform - revert bugfix https://github.com/ansible-collections/community.general/pull/4281
|
||||
that tried to fix ``variable`` handling to allow complex values. It turned
|
||||
out that this was breaking several valid use-cases (https://github.com/ansible-collections/community.general/issues/4367,
|
||||
https://github.com/ansible-collections/community.general/pull/4370).
|
||||
release_summary: Extraordinary bugfix release to fix a breaking change in ``terraform``.
|
||||
fragments:
|
||||
- 4.6.1.yml
|
||||
- 4351-inventory-lxd-handling_metadata_wo_os_and_release.yml
|
||||
- 4368-reverts-4281.yml
|
||||
release_date: '2022-03-16'
|
||||
4.7.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- dsv lookup plugin - raise an Ansible error if the wrong ``python-dsv-sdk``
|
||||
version is installed (https://github.com/ansible-collections/community.general/pull/4422).
|
||||
- keycloak_* - the documented ``validate_certs`` parameter was not taken into
|
||||
account when calling the ``open_url`` function in some cases, thus enforcing
|
||||
certificate validation even when ``validate_certs`` was set to ``false``.
|
||||
(https://github.com/ansible-collections/community.general/pull/4382)
|
||||
- nmcli - fix returning "changed" when routes parameters set, also suggest new
|
||||
routes4 and routes6 format (https://github.com/ansible-collections/community.general/issues/4131).
|
||||
- proxmox inventory plugin - fixed the ``tags_parsed`` field when Proxmox returns
|
||||
a single space for the ``tags`` entry (https://github.com/ansible-collections/community.general/pull/4378).
|
||||
- zypper - fixed bug that caused zypper to always report [ok] and do nothing
|
||||
on ``state=present`` when all packages in ``name`` had a version specification
|
||||
(https://github.com/ansible-collections/community.general/issues/4371, https://github.com/ansible-collections/community.general/pull/4421).
|
||||
minor_changes:
|
||||
- ipa_service - add ``skip_host_check`` parameter. (https://github.com/ansible-collections/community.general/pull/4417).
|
||||
- keycloak_client - add ``always_display_in_console`` parameter (https://github.com/ansible-collections/community.general/issues/4390).
|
||||
- keycloak_client - add ``default_client_scopes`` and ``optional_client_scopes``
|
||||
parameters. (https://github.com/ansible-collections/community.general/pull/4385).
|
||||
- proxmox inventory plugin - add support for templating the ``url``, ``user``,
|
||||
and ``password`` options (https://github.com/ansible-collections/community.general/pull/4418).
|
||||
- sudoers - add support for ``runas`` parameter (https://github.com/ansible-collections/community.general/issues/4379).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 4.7.0.yml
|
||||
- 4131-nmcli_fix_reports_changed_for_routes4_parameter.yml
|
||||
- 4378-proxmox-inventory-tags.yml
|
||||
- 4380-sudoers-runas-parameter.yml
|
||||
- 4382-keycloak-add-missing-validate_certs-parameters.yml
|
||||
- 4385-keycloak-client-default-optional-scopes.yml
|
||||
- 4386-proxmox-support-templating-in-inventory-file.yml
|
||||
- 4417-ipa_service-add-skip_host_check.yml
|
||||
- 4421-zypper_package_version_handling_fix.yml
|
||||
- 4422-warn-user-if-incorrect-SDK-version-is-installed.yaml
|
||||
- 4429-keycloak-client-add-always-display-in-console.yml
|
||||
release_date: '2022-04-05'
|
||||
4.8.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- dnsmadeeasy - fix failure on deleting DNS entries when API response does not
|
||||
contain monitor value (https://github.com/ansible-collections/community.general/issues/3620).
|
||||
- git_branch - remove deprecated and unnecessary branch ``unprotect`` method
|
||||
(https://github.com/ansible-collections/community.general/pull/4496).
|
||||
- 'gitlab_group - improve searching for projects inside group on deletion (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
|
||||
'
|
||||
- 'gitlab_group_members - handle more than 20 groups when finding a group (https://github.com/ansible-collections/community.general/pull/4491,
|
||||
https://github.com/ansible-collections/community.general/issues/4460, https://github.com/ansible-collections/community.general/issues/3729).
|
||||
|
||||
'
|
||||
- 'gitlab_hook - handle more than 20 hooks when finding a hook (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
|
||||
'
|
||||
- 'gitlab_project - handle more than 20 namespaces when finding a namespace
|
||||
(https://github.com/ansible-collections/community.general/pull/4491).
|
||||
|
||||
'
|
||||
- 'gitlab_project_members - handle more than 20 projects and users when finding
|
||||
a project resp. user (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
|
||||
'
|
||||
- 'gitlab_user - handle more than 20 users and SSH keys when finding a user
|
||||
resp. SSH key (https://github.com/ansible-collections/community.general/pull/4491).
|
||||
|
||||
'
|
||||
- keycloak - fix parameters types for ``defaultDefaultClientScopes`` and ``defaultOptionalClientScopes``
|
||||
from list of dictionaries to list of strings (https://github.com/ansible-collections/community.general/pull/4526).
|
||||
- opennebula inventory plugin - complete the implementation of ``constructable``
|
||||
for opennebula inventory plugin. Now ``keyed_groups``, ``compose``, ``groups``
|
||||
actually work (https://github.com/ansible-collections/community.general/issues/4497).
|
||||
- pacman - fixed bug where ``absent`` state did not work for locally installed
|
||||
packages (https://github.com/ansible-collections/community.general/pull/4464).
|
||||
- pritunl - fixed bug where pritunl plugin api add unneeded data in ``auth_string``
|
||||
parameter (https://github.com/ansible-collections/community.general/issues/4527).
|
||||
- proxmox inventory plugin - fix error when parsing container with LXC configs
|
||||
(https://github.com/ansible-collections/community.general/issues/4472, https://github.com/ansible-collections/community.general/pull/4472).
|
||||
- proxmox_kvm - fix a bug when getting a state of VM without name will fail
|
||||
(https://github.com/ansible-collections/community.general/pull/4508).
|
||||
- xbps - fix error message that is reported when installing packages fails (https://github.com/ansible-collections/community.general/pull/4438).
|
||||
deprecated_features:
|
||||
- nmcli - deprecate default hairpin mode for a bridge. This so we can change
|
||||
it to ``false`` in community.general 7.0.0, as this is also the default in
|
||||
``nmcli`` (https://github.com/ansible-collections/community.general/pull/4334).
|
||||
- proxmox inventory plugin - the current default ``true`` of the ``want_proxmox_nodes_ansible_host``
|
||||
option has been deprecated. The default will change to ``false`` in community.general
|
||||
6.0.0. To keep the current behavior, explicitly set ``want_proxmox_nodes_ansible_host``
|
||||
to ``true`` in your inventory configuration. We suggest to already switch
|
||||
to the new behavior by explicitly setting it to ``false``, and by using ``compose:``
|
||||
to set ``ansible_host`` to the correct value. See the examples in the plugin
|
||||
documentation for details (https://github.com/ansible-collections/community.general/pull/4466).
|
||||
minor_changes:
|
||||
- alternatives - add ``state`` parameter, which provides control over whether
|
||||
the alternative should be set as the active selection for its alternatives
|
||||
group (https://github.com/ansible-collections/community.general/issues/4543,
|
||||
https://github.com/ansible-collections/community.general/pull/4557).
|
||||
- atomic_container - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- clc_alert_policy - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_group - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_loadbalancer - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- clc_server - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- cmd_runner module util - reusable command runner with consistent argument
|
||||
formatting and sensible defaults (https://github.com/ansible-collections/community.general/pull/4476).
|
||||
- datadog_monitor - support new datadog event monitor of type `event-v2 alert`
|
||||
(https://github.com/ansible-collections/community.general/pull/4457)
|
||||
- filesystem - add support for resizing btrfs (https://github.com/ansible-collections/community.general/issues/4465).
|
||||
- lxd_container - adds ``project`` option to allow selecting project for LXD
|
||||
instance (https://github.com/ansible-collections/community.general/pull/4479).
|
||||
- lxd_profile - adds ``project`` option to allow selecting project for LXD profile
|
||||
(https://github.com/ansible-collections/community.general/pull/4479).
|
||||
- nmap inventory plugin - add ``sudo`` option in plugin in order to execute
|
||||
``sudo nmap`` so that ``nmap`` runs with elevated privileges (https://github.com/ansible-collections/community.general/pull/4506).
|
||||
- nomad_job - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- nomad_job_info - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_device - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_sshkey - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- packet_volume - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- profitbricks - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- proxmox - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- proxmox inventory plugin - add token authentication as an alternative to username/password
|
||||
(https://github.com/ansible-collections/community.general/pull/4540).
|
||||
- proxmox inventory plugin - parse LXC configs returned by the proxmox API (https://github.com/ansible-collections/community.general/pull/4472).
|
||||
- proxmox_snap - add restore snapshot option (https://github.com/ansible-collections/community.general/pull/4377).
|
||||
- proxmox_snap - fixed timeout value to correctly reflect time in seconds. The
|
||||
timeout was off by one second (https://github.com/ansible-collections/community.general/pull/4377).
|
||||
- redfish_command - add ``IndicatorLedOn``, ``IndicatorLedOff``, and ``IndicatorLedBlink``
|
||||
commands to the Systems category for controling system LEDs (https://github.com/ansible-collections/community.general/issues/4084).
|
||||
- seport - minor refactoring (https://github.com/ansible-collections/community.general/pull/4471).
|
||||
- smartos_image_info - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- terraform - adds ``terraform_upgrade`` parameter which allows ``terraform
|
||||
init`` to satisfy new provider constraints in an existing Terraform project
|
||||
(https://github.com/ansible-collections/community.general/issues/4333).
|
||||
- udm_group - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- udm_share - minor refactoring (https://github.com/ansible-collections/community.general/pull/4556).
|
||||
- vmadm - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- webfaction_app - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- webfaction_db - minor refactoring (https://github.com/ansible-collections/community.general/pull/4567).
|
||||
- xfconf - added missing value types ``char``, ``uchar``, ``int64`` and ``uint64``
|
||||
(https://github.com/ansible-collections/community.general/pull/4534).
|
||||
release_summary: Regular feature and bugfix release. Please note that this is
|
||||
the last minor 4.x.0 release. Further releases with major version 4 will be
|
||||
bugfix releases 4.8.y.
|
||||
fragments:
|
||||
- 4.8.0.yml
|
||||
- 4084-add-redfish-system-indicator-led.yml
|
||||
- 4320-nmcli-hairpin.yml
|
||||
- 4377-allow-proxmox-snapshot-restoring.yml
|
||||
- 4438-fix-error-message.yaml
|
||||
- 4455-terraform-provider-upgrade.yml
|
||||
- 4457-support-datadog-monitors-type-event-v2.yaml
|
||||
- 4459-only-get-monitor-if-it-is-not-null-api-response.yaml
|
||||
- 4464-pacman-fix-local-remove.yaml
|
||||
- 4465-btrfs-resize.yml
|
||||
- 4466-proxmox-ansible_host-deprecation.yml
|
||||
- 4471-seport-refactor.yaml
|
||||
- 4476-cmd_runner.yml
|
||||
- 4479-add-project-support-for-lxd_container-and-lxd_profile.yml
|
||||
- 4491-specify_all_in_list_calls.yaml
|
||||
- 4492-proxmox_kvm_fix_vm_without_name.yaml
|
||||
- 4496-remove-deprecated-method-in-gitlab-branch-module.yml
|
||||
- 4506-sudo-in-nmap-inv-plugin.yaml
|
||||
- 4524-update-opennebula-inventory-plugin-to-match-documentation.yaml
|
||||
- 4526-keycloak-realm-types.yaml
|
||||
- 4530-fix-unauthorized-pritunl-request.yaml
|
||||
- 4534-xfconf-added-value-types.yaml
|
||||
- 4540-proxmox-inventory-token-auth.yml
|
||||
- 4555-proxmox-lxc-key.yml
|
||||
- 4556-remove-default-none-1.yml
|
||||
- 4557-alternatives-add-state-parameter.yml
|
||||
- 4567-remove-default-none-2.yml
|
||||
modules:
|
||||
- description: Manage customers in Alerta
|
||||
name: alerta_customer
|
||||
namespace: monitoring
|
||||
- description: Manage LXD projects
|
||||
name: lxd_project
|
||||
namespace: cloud.lxd
|
||||
release_date: '2022-04-26'
|
||||
|
||||
13
docs/docsite/helper/lists_mergeby/default-common.yml
Normal file
13
docs/docsite/helper/lists_mergeby/default-common.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
19
docs/docsite/helper/lists_mergeby/default-recursive-true.yml
Normal file
19
docs/docsite/helper/lists_mergeby/default-recursive-true.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
10
docs/docsite/helper/lists_mergeby/example-001.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-001.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 1. Merge two lists by common attribute 'name'
|
||||
include_vars:
|
||||
dir: example-001_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-001.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-common.yml
|
||||
@@ -0,0 +1,2 @@
|
||||
list3: "{{ list1|
|
||||
community.general.lists_mergeby(list2, 'name') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-002.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-002.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 2. Merge two lists by common attribute 'name'
|
||||
include_vars:
|
||||
dir: example-002_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-002.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-common.yml
|
||||
@@ -0,0 +1,2 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-003.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-003.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 3. Merge recursive by 'name', replace lists (default)
|
||||
include_vars:
|
||||
dir: example-003_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-003.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,3 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true) }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-004.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-004.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 4. Merge recursive by 'name', keep lists
|
||||
include_vars:
|
||||
dir: example-004_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-004.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,4 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='keep') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-005.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-005.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 5. Merge recursive by 'name', append lists
|
||||
include_vars:
|
||||
dir: example-005_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-005.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,4 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-006.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-006.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 6. Merge recursive by 'name', prepend lists
|
||||
include_vars:
|
||||
dir: example-006_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-006.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,4 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-007.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-007.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 7. Merge recursive by 'name', append lists 'remove present'
|
||||
include_vars:
|
||||
dir: example-007_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-007.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,4 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append_rp') }}"
|
||||
10
docs/docsite/helper/lists_mergeby/example-008.yml
Normal file
10
docs/docsite/helper/lists_mergeby/example-008.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: 8. Merge recursive by 'name', prepend lists 'remove present'
|
||||
include_vars:
|
||||
dir: example-008_vars
|
||||
- debug:
|
||||
var: list3
|
||||
when: debug|d(false)|bool
|
||||
- template:
|
||||
src: list3.out.j2
|
||||
dest: example-008.out
|
||||
@@ -0,0 +1 @@
|
||||
../default-recursive-true.yml
|
||||
@@ -0,0 +1,4 @@
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend_rp') }}"
|
||||
50
docs/docsite/helper/lists_mergeby/examples.yml
Normal file
50
docs/docsite/helper/lists_mergeby/examples.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
examples:
|
||||
- label: 'In the example below the lists are merged by the attribute ``name``:'
|
||||
file: example-001_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-001.out
|
||||
lang: 'yaml'
|
||||
- label: 'It is possible to use a list of lists as an input of the filter:'
|
||||
file: example-002_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces the same result as in the previous example:'
|
||||
file: example-002.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=replace`` (default):'
|
||||
file: example-003_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-003.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=keep``:'
|
||||
file: example-004_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-004.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=append``:'
|
||||
file: example-005_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-005.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=prepend``:'
|
||||
file: example-006_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-006.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=append_rp``:'
|
||||
file: example-007_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-007.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=prepend_rp``:'
|
||||
file: example-008_vars/list3.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-008.out
|
||||
lang: 'yaml'
|
||||
8
docs/docsite/helper/lists_mergeby/examples_all.rst.j2
Normal file
8
docs/docsite/helper/lists_mergeby/examples_all.rst.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
{% for i in examples %}
|
||||
{{ i.label }}
|
||||
|
||||
.. code-block:: {{ i.lang }}
|
||||
|
||||
{{ lookup('file', i.file)|indent(2) }}
|
||||
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,57 @@
|
||||
Merging lists of dictionaries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the ``lists_mergeby`` filter.
|
||||
|
||||
.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`.
|
||||
|
||||
Let us use the lists below in the following examples:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
{{ lookup('file', 'default-common.yml')|indent(2) }}
|
||||
|
||||
{% for i in examples[0:2] %}
|
||||
{{ i.label }}
|
||||
|
||||
.. code-block:: {{ i.lang }}
|
||||
|
||||
{{ lookup('file', i.file)|indent(2) }}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
{% for i in examples[2:4] %}
|
||||
{{ i.label }}
|
||||
|
||||
.. code-block:: {{ i.lang }}
|
||||
|
||||
{{ lookup('file', i.file)|indent(2) }}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
The filter also accepts two optional parameters: ``recursive`` and ``list_merge``. These parameters are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9. This is available since community.general 4.4.0.
|
||||
|
||||
**recursive**
|
||||
Is a boolean, default to ``False``. Should the ``community.general.lists_mergeby`` recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``.
|
||||
|
||||
**list_merge**
|
||||
Is a string, its possible values are ``replace`` (default), ``keep``, ``append``, ``prepend``, ``append_rp`` or ``prepend_rp``. It modifies the behaviour of ``community.general.lists_mergeby`` when the hashes to merge contain arrays/lists.
|
||||
|
||||
The examples below set ``recursive=true`` and display the differences among all six options of ``list_merge``. Functionality of the parameters is exactly the same as in the filter ``combine``. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options.
|
||||
|
||||
Let us use the lists below in the following examples
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
{{ lookup('file', 'default-recursive-true.yml')|indent(2) }}
|
||||
|
||||
{% for i in examples[4:16] %}
|
||||
{{ i.label }}
|
||||
|
||||
.. code-block:: {{ i.lang }}
|
||||
|
||||
{{ lookup('file', i.file)|indent(2) }}
|
||||
|
||||
{% endfor %}
|
||||
2
docs/docsite/helper/lists_mergeby/list3.out.j2
Normal file
2
docs/docsite/helper/lists_mergeby/list3.out.j2
Normal file
@@ -0,0 +1,2 @@
|
||||
list3:
|
||||
{{ list3|to_nice_yaml(indent=0) }}
|
||||
59
docs/docsite/helper/lists_mergeby/playbook.yml
Normal file
59
docs/docsite/helper/lists_mergeby/playbook.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# 1) Run all examples and create example-XXX.out
|
||||
# shell> ansible-playbook playbook.yml -e examples=true
|
||||
#
|
||||
# 2) Optionally, for testing, create examples_all.rst
|
||||
# shell> ansible-playbook playbook.yml -e examples_all=true
|
||||
#
|
||||
# 3) Create docs REST files
|
||||
# shell> ansible-playbook playbook.yml -e merging_lists_of_dictionaries=true
|
||||
#
|
||||
# Notes:
|
||||
# * Use YAML callback, e.g. set ANSIBLE_STDOUT_CALLBACK=community.general.yaml
|
||||
# * Use sphinx-view to render and review the REST files
|
||||
# shell> sphinx-view <path_to_helper>/examples_all.rst
|
||||
# * Proofread and copy completed docs *.rst files into the directory rst.
|
||||
# * Then delete the *.rst and *.out files from this directory. Do not
|
||||
# add *.rst and *.out in this directory to the version control.
|
||||
#
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
# community.general/docs/docsite/helper/lists_mergeby/playbook.yml
|
||||
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
tasks:
|
||||
|
||||
- block:
|
||||
- import_tasks: example-001.yml
|
||||
tags: t001
|
||||
- import_tasks: example-002.yml
|
||||
tags: t002
|
||||
- import_tasks: example-003.yml
|
||||
tags: t003
|
||||
- import_tasks: example-004.yml
|
||||
tags: t004
|
||||
- import_tasks: example-005.yml
|
||||
tags: t005
|
||||
- import_tasks: example-006.yml
|
||||
tags: t006
|
||||
- import_tasks: example-007.yml
|
||||
tags: t007
|
||||
- import_tasks: example-008.yml
|
||||
tags: t008
|
||||
when: examples|d(false)|bool
|
||||
|
||||
- block:
|
||||
- include_vars: examples.yml
|
||||
- template:
|
||||
src: examples_all.rst.j2
|
||||
dest: examples_all.rst
|
||||
when: examples_all|d(false)|bool
|
||||
|
||||
- block:
|
||||
- include_vars: examples.yml
|
||||
- template:
|
||||
src: filter_guide_abstract_informations_merging_lists_of_dictionaries.rst.j2
|
||||
dest: filter_guide_abstract_informations_merging_lists_of_dictionaries.rst
|
||||
when: merging_lists_of_dictionaries|d(false)|bool
|
||||
23
docs/docsite/links.yml
Normal file
23
docs/docsite/links.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
edit_on_github:
|
||||
repository: ansible-collections/community.general
|
||||
branch: main
|
||||
path_prefix: ''
|
||||
|
||||
extra_links:
|
||||
- description: Submit a bug report
|
||||
url: https://github.com/ansible-collections/community.general/issues/new?assignees=&labels=&template=bug_report.yml
|
||||
- description: Request a feature
|
||||
url: https://github.com/ansible-collections/community.general/issues/new?assignees=&labels=&template=feature_request.yml
|
||||
|
||||
communication:
|
||||
matrix_rooms:
|
||||
- topic: General usage and support questions
|
||||
room: '#users:ansible.im'
|
||||
irc_channels:
|
||||
- topic: General usage and support questions
|
||||
network: Libera
|
||||
channel: '#ansible'
|
||||
mailing_lists:
|
||||
- topic: Ansible Project List
|
||||
url: https://groups.google.com/g/ansible-project
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
.. _ansible_collections.community.general.docsite.filter_guide:
|
||||
|
||||
community.general Filter Guide
|
||||
@@ -5,858 +6,14 @@ community.general Filter Guide
|
||||
|
||||
The :ref:`community.general collection <plugins_in_community.general>` offers several useful filter plugins.
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
Paths
|
||||
-----
|
||||
|
||||
The ``path_join`` filter has been added in ansible-base 2.10. If you want to use this filter, but also need to support Ansible 2.9, you can use ``community.general``'s ``path_join`` shim, ``community.general.path_join``. This filter redirects to ``path_join`` for ansible-base 2.10 and ansible-core 2.11 or newer, and re-implements the filter for Ansible 2.9.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
# ansible-base 2.10 or newer:
|
||||
path: {{ ('/etc', path, 'subdir', file) | path_join }}
|
||||
|
||||
# Also works with Ansible 2.9:
|
||||
path: {{ ('/etc', path, 'subdir', file) | community.general.path_join }}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
Abstract transformations
|
||||
------------------------
|
||||
|
||||
Dictionaries
|
||||
^^^^^^^^^^^^
|
||||
|
||||
You can use the ``dict_kv`` filter to create a single-entry dictionary with ``value | community.general.dict_kv(key)``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a single-entry dictionary
|
||||
debug:
|
||||
msg: "{{ myvar | community.general.dict_kv('thatsmyvar') }}"
|
||||
vars:
|
||||
myvar: myvalue
|
||||
|
||||
- name: Create a list of dictionaries where the 'server' field is taken from a list
|
||||
debug:
|
||||
msg: >-
|
||||
{{ myservers | map('community.general.dict_kv', 'server')
|
||||
| map('combine', common_config) }}
|
||||
vars:
|
||||
common_config:
|
||||
type: host
|
||||
database: all
|
||||
myservers:
|
||||
- server1
|
||||
- server2
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a single-entry dictionary] **************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"thatsmyvar": "myvalue"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries where the 'server' field is taken from a list] *******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server1",
|
||||
"type": "host"
|
||||
},
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server2",
|
||||
"type": "host"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
If you need to convert a list of key-value pairs to a dictionary, you can use the ``dict`` function. Unfortunately, this function cannot be used with ``map``. For this, the ``community.general.dict`` filter can be used:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a dictionary with the dict function
|
||||
debug:
|
||||
msg: "{{ dict([[1, 2], ['a', 'b']]) }}"
|
||||
|
||||
- name: Create a dictionary with the community.general.dict filter
|
||||
debug:
|
||||
msg: "{{ [[1, 2], ['a', 'b']] | community.general.dict }}"
|
||||
|
||||
- name: Create a list of dictionaries with map and the community.general.dict filter
|
||||
debug:
|
||||
msg: >-
|
||||
{{ values | map('zip', ['k1', 'k2', 'k3'])
|
||||
| map('map', 'reverse')
|
||||
| map('community.general.dict') }}
|
||||
vars:
|
||||
values:
|
||||
- - foo
|
||||
- 23
|
||||
- a
|
||||
- - bar
|
||||
- 42
|
||||
- b
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a dictionary with the dict function] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a dictionary with the community.general.dict filter] ************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries with map and the community.general.dict filter] ******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"k1": "foo",
|
||||
"k2": 23,
|
||||
"k3": "a"
|
||||
},
|
||||
{
|
||||
"k1": "bar",
|
||||
"k2": 42,
|
||||
"k3": "b"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
Grouping
|
||||
^^^^^^^^
|
||||
|
||||
If you have a list of dictionaries, the Jinja2 ``groupby`` filter allows to group the list by an attribute. This results in a list of ``(grouper, list)`` namedtuples, where ``list`` contains all dictionaries where the selected attribute equals ``grouper``. If you know that for every ``grouper``, there will be a most one entry in that list, you can use the ``community.general.groupby_as_dict`` filter to convert the original list into a dictionary which maps ``grouper`` to the corresponding dictionary.
|
||||
|
||||
One example is ``ansible_facts.mounts``, which is a list of dictionaries where each has one ``device`` element to indicate the device which is mounted. Therefore, ``ansible_facts.mounts | community.general.groupby_as_dict('device')`` is a dictionary mapping a device to the mount information:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Output mount facts grouped by device name
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('device')
|
||||
|
||||
- name: Output mount facts grouped by mount point
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('mount')
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Output mount facts grouped by device name] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('device')": {
|
||||
"/dev/sda1": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
},
|
||||
"/dev/sda2": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "abcdef01-2345-6789-0abc-def012345678"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Output mount facts grouped by mount point] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('mount')": {
|
||||
"/": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "bdf50b7d-4859-40af-8665-c637ee7a7808"
|
||||
},
|
||||
"/boot": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Merging lists of dictionaries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have two lists of dictionaries and want to combine them into a list of merged dictionaries, where two dictionaries are merged if they coincide in one attribute, you can use the ``lists_mergeby`` filter.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
debug:
|
||||
var: list1 | community.general.lists_mergeby(list2, 'name')
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /bazzz
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Merge two lists by common attribute 'name'] ****************************************
|
||||
ok: [localhost] => {
|
||||
"list1 | community.general.lists_mergeby(list2, 'name')": [
|
||||
{
|
||||
"extra": false,
|
||||
"name": "bar"
|
||||
},
|
||||
{
|
||||
"name": "baz",
|
||||
"path": "/bazzz"
|
||||
},
|
||||
{
|
||||
"extra": true,
|
||||
"name": "foo",
|
||||
"path": "/foo"
|
||||
},
|
||||
{
|
||||
"extra": true,
|
||||
"name": "meh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
|
||||
Counting elements in a sequence
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``community.general.counter`` filter plugin allows you to count (hashable) elements in a sequence. Elements are returned as dictionary keys and their counts are stored as dictionary values.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Count character occurrences in a string
|
||||
debug:
|
||||
msg: "{{ 'abccbaabca' | community.general.counter }}"
|
||||
|
||||
- name: Count items in a list
|
||||
debug:
|
||||
msg: "{{ ['car', 'car', 'bike', 'plane', 'bike'] | community.general.counter }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Count character occurrences in a string] ********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"a": 4,
|
||||
"b": 3,
|
||||
"c": 3
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Count items in a list] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"bike": 2,
|
||||
"car": 2,
|
||||
"plane": 1
|
||||
}
|
||||
}
|
||||
|
||||
This plugin is useful for selecting resources based on current allocation:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks
|
||||
debug:
|
||||
msg: >-
|
||||
{{
|
||||
( disks | dict2items | map(attribute='value.adapter') | list
|
||||
| community.general.counter | dict2items
|
||||
| rejectattr('value', '>=', 4) | sort(attribute='value') | first
|
||||
).key
|
||||
}}
|
||||
vars:
|
||||
disks:
|
||||
sda:
|
||||
adapter: scsi_1
|
||||
sdb:
|
||||
adapter: scsi_1
|
||||
sdc:
|
||||
adapter: scsi_1
|
||||
sdd:
|
||||
adapter: scsi_1
|
||||
sde:
|
||||
adapter: scsi_2
|
||||
sdf:
|
||||
adapter: scsi_3
|
||||
sdg:
|
||||
adapter: scsi_3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks]
|
||||
ok: [localhost] => {
|
||||
"msg": "scsi_2"
|
||||
}
|
||||
|
||||
.. versionadded:: 4.3.0
|
||||
|
||||
Working with times
|
||||
------------------
|
||||
|
||||
The ``to_time_unit`` filter allows to convert times from a human-readable string to a unit. For example, ``'4h 30min 12second' | community.general.to_time_unit('hour')`` gives the number of hours that correspond to 4 hours, 30 minutes and 12 seconds.
|
||||
|
||||
There are shorthands to directly convert to various units, like ``to_hours``, ``to_minutes``, ``to_seconds``, and so on. The following table lists all units that can be used:
|
||||
|
||||
.. list-table:: Units
|
||||
:widths: 25 25 25 25
|
||||
:header-rows: 1
|
||||
|
||||
* - Unit name
|
||||
- Unit value in seconds
|
||||
- Unit strings for filter
|
||||
- Shorthand filter
|
||||
* - Millisecond
|
||||
- 1/1000 second
|
||||
- ``ms``, ``millisecond``, ``milliseconds``, ``msec``, ``msecs``, ``msecond``, ``mseconds``
|
||||
- ``to_milliseconds``
|
||||
* - Second
|
||||
- 1 second
|
||||
- ``s``, ``sec``, ``secs``, ``second``, ``seconds``
|
||||
- ``to_seconds``
|
||||
* - Minute
|
||||
- 60 seconds
|
||||
- ``m``, ``min``, ``mins``, ``minute``, ``minutes``
|
||||
- ``to_minutes``
|
||||
* - Hour
|
||||
- 60*60 seconds
|
||||
- ``h``, ``hour``, ``hours``
|
||||
- ``to_hours``
|
||||
* - Day
|
||||
- 24*60*60 seconds
|
||||
- ``d``, ``day``, ``days``
|
||||
- ``to_days``
|
||||
* - Week
|
||||
- 7*24*60*60 seconds
|
||||
- ``w``, ``week``, ``weeks``
|
||||
- ``to_weeks``
|
||||
* - Month
|
||||
- 30*24*60*60 seconds
|
||||
- ``mo``, ``month``, ``months``
|
||||
- ``to_months``
|
||||
* - Year
|
||||
- 365*24*60*60 seconds
|
||||
- ``y``, ``year``, ``years``
|
||||
- ``to_years``
|
||||
|
||||
Note that months and years are using a simplified representation: a month is 30 days, and a year is 365 days. If you need different definitions of months or years, you can pass them as keyword arguments. For example, if you want a year to be 365.25 days, and a month to be 30.5 days, you can write ``'11months 4' | community.general.to_years(year=365.25, month=30.5)``. These keyword arguments can be specified to ``to_time_unit`` and to all shorthand filters.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Convert string to seconds
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_time_unit('seconds') }}"
|
||||
|
||||
- name: Convert string to hours
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_hours }}"
|
||||
|
||||
- name: Convert string to years (using 365.25 days == 1 year)
|
||||
debug:
|
||||
msg: "{{ '400d 15h' | community.general.to_years(year=365.25) }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Convert string to seconds] **********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "109210.123"
|
||||
}
|
||||
|
||||
TASK [Convert string to hours] ************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "30.336145277778"
|
||||
}
|
||||
|
||||
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
||||
ok: [localhost] => {
|
||||
"msg": "1.096851471595"
|
||||
}
|
||||
|
||||
.. versionadded: 0.2.0
|
||||
|
||||
Working with versions
|
||||
---------------------
|
||||
|
||||
If you need to sort a list of version numbers, the Jinja ``sort`` filter is problematic. Since it sorts lexicographically, ``2.10`` will come before ``2.9``. To treat version numbers correctly, you can use the ``version_sort`` filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Sort list by version number
|
||||
debug:
|
||||
var: ansible_versions | community.general.version_sort
|
||||
vars:
|
||||
ansible_versions:
|
||||
- '2.8.0'
|
||||
- '2.11.0'
|
||||
- '2.7.0'
|
||||
- '2.10.0'
|
||||
- '2.9.0'
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Sort list by version number] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_versions | community.general.version_sort": [
|
||||
"2.7.0",
|
||||
"2.8.0",
|
||||
"2.9.0",
|
||||
"2.10.0",
|
||||
"2.11.0"
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.2.0
|
||||
|
||||
Creating identifiers
|
||||
--------------------
|
||||
|
||||
The following filters allow to create identifiers.
|
||||
|
||||
Hashids
|
||||
^^^^^^^
|
||||
|
||||
`Hashids <https://hashids.org/>`_ allow to convert sequences of integers to short unique string identifiers. This filter needs the `hashids Python library <https://pypi.org/project/hashids/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create hashid"
|
||||
debug:
|
||||
msg: "{{ [1234, 5, 6] | community.general.hashids_encode }}"
|
||||
|
||||
- name: "Decode hashid"
|
||||
debug:
|
||||
msg: "{{ 'jm2Cytn' | community.general.hashids_decode }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "jm2Cytn"
|
||||
}
|
||||
|
||||
TASK [Decode hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
1234,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
|
||||
The hashids filters accept keyword arguments to allow fine-tuning the hashids generated:
|
||||
|
||||
:salt: String to use as salt when hashing.
|
||||
:alphabet: String of 16 or more unique characters to produce a hash.
|
||||
:min_length: Minimum length of hash produced.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Random MACs
|
||||
^^^^^^^^^^^
|
||||
|
||||
You can use the ``random_mac`` filter to complete a partial `MAC address <https://en.wikipedia.org/wiki/MAC_address>`_ to a random 6-byte MAC address.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create a random MAC starting with ff:"
|
||||
debug:
|
||||
msg: "{{ 'FF' | community.general.random_mac }}"
|
||||
|
||||
- name: "Create a random MAC starting with 00:11:22:"
|
||||
debug:
|
||||
msg: "{{ '00:11:22' | community.general.random_mac }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a random MAC starting with ff:] **********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "ff:69:d3:78:7f:b4"
|
||||
}
|
||||
|
||||
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "00:11:22:71:5d:3b"
|
||||
}
|
||||
|
||||
You can also initialize the random number generator from a seed to create random-but-idempotent MAC addresses:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
|
||||
|
||||
Conversions
|
||||
-----------
|
||||
|
||||
Parsing CSV files
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ansible offers the :ref:`community.general.read_csv module <ansible_collections.community.general.read_csv_module>` to read CSV files. Sometimes you need to convert strings to CSV files instead. For this, the ``from_csv`` filter exists.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Parse CSV from string"
|
||||
debug:
|
||||
msg: "{{ csv_string | community.general.from_csv }}"
|
||||
vars:
|
||||
csv_string: |
|
||||
foo,bar,baz
|
||||
1,2,3
|
||||
you,this,then
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Parse CSV from string] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"bar": "2",
|
||||
"baz": "3",
|
||||
"foo": "1"
|
||||
},
|
||||
{
|
||||
"bar": "this",
|
||||
"baz": "then",
|
||||
"foo": "you"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The ``from_csv`` filter has several keyword arguments to control its behavior:
|
||||
|
||||
:dialect: Dialect of the CSV file. Default is ``excel``. Other possible choices are ``excel-tab`` and ``unix``. If one of ``delimiter``, ``skipinitialspace`` or ``strict`` is specified, ``dialect`` is ignored.
|
||||
:fieldnames: A set of column names to use. If not provided, the first line of the CSV is assumed to contain the column names.
|
||||
:delimiter: Sets the delimiter to use. Default depends on the dialect used.
|
||||
:skipinitialspace: Set to ``true`` to ignore space directly after the delimiter. Default depends on the dialect used (usually ``false``).
|
||||
:strict: Set to ``true`` to error out on invalid CSV input.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Converting to JSON
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`JC <https://pypi.org/project/jc/>`_ is a CLI tool and Python library which allows to interpret output of various CLI programs as JSON. It is also available as a filter in community.general. This filter needs the `jc Python library <https://pypi.org/project/jc/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Run 'ls' to list files in /
|
||||
command: ls /
|
||||
register: result
|
||||
|
||||
- name: Parse the ls output
|
||||
debug:
|
||||
msg: "{{ result.stdout | community.general.jc('ls') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Run 'ls' to list files in /] ********************************************************
|
||||
changed: [localhost]
|
||||
|
||||
TASK [Parse the ls output] ****************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"filename": "bin"
|
||||
},
|
||||
{
|
||||
"filename": "boot"
|
||||
},
|
||||
{
|
||||
"filename": "dev"
|
||||
},
|
||||
{
|
||||
"filename": "etc"
|
||||
},
|
||||
{
|
||||
"filename": "home"
|
||||
},
|
||||
{
|
||||
"filename": "lib"
|
||||
},
|
||||
{
|
||||
"filename": "proc"
|
||||
},
|
||||
{
|
||||
"filename": "root"
|
||||
},
|
||||
{
|
||||
"filename": "run"
|
||||
},
|
||||
{
|
||||
"filename": "tmp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
|
||||
.. _ansible_collections.community.general.docsite.json_query_filter:
|
||||
|
||||
Selecting JSON data: JSON queries
|
||||
---------------------------------
|
||||
|
||||
To select a single element or a data subset from a complex data structure in JSON format (for example, Ansible facts), use the ``json_query`` filter. The ``json_query`` filter lets you query a complex JSON structure and iterate over it using a loop structure.
|
||||
|
||||
.. note:: You must manually install the **jmespath** dependency on the Ansible controller before using this filter. This filter is built upon **jmespath**, and you can use the same syntax. For examples, see `jmespath examples <http://jmespath.org/examples.html>`_.
|
||||
|
||||
Consider this data structure:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
{
|
||||
"domain_definition": {
|
||||
"domain": {
|
||||
"cluster": [
|
||||
{
|
||||
"name": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "cluster2"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
"name": "server11",
|
||||
"cluster": "cluster1",
|
||||
"port": "8080"
|
||||
},
|
||||
{
|
||||
"name": "server12",
|
||||
"cluster": "cluster1",
|
||||
"port": "8090"
|
||||
},
|
||||
{
|
||||
"name": "server21",
|
||||
"cluster": "cluster2",
|
||||
"port": "9080"
|
||||
},
|
||||
{
|
||||
"name": "server22",
|
||||
"cluster": "cluster2",
|
||||
"port": "9090"
|
||||
}
|
||||
],
|
||||
"library": [
|
||||
{
|
||||
"name": "lib1",
|
||||
"target": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "lib2",
|
||||
"target": "cluster2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
To extract all clusters from this structure, you can use the following query:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all cluster names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"
|
||||
|
||||
To extract all server names:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[*].name') }}"
|
||||
|
||||
To extract ports from cluster1:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"
|
||||
|
||||
.. note:: You can use a variable to make the query more readable.
|
||||
|
||||
To print out the ports from cluster1 in a comma separated string:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1 as a string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | community.general.json_query('domain.server[?cluster==`cluster1`].port') | join(', ') }}"
|
||||
|
||||
.. note:: In the example above, quoting literals using backticks avoids escaping quotes and maintains readability.
|
||||
|
||||
You can use YAML `single quote escaping <https://yaml.org/spec/current.html#id2534365>`_:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[?cluster==''cluster1''].port') }}"
|
||||
|
||||
.. note:: Escaping single quotes within single quotes in YAML is done by doubling the single quote.
|
||||
|
||||
To get a hash map with all ports and names of a cluster:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server ports and names from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"
|
||||
|
||||
To extract ports from all clusters with name starting with 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?starts_with(name,'server1')].port"
|
||||
|
||||
To extract ports from all clusters with name containing 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?contains(name,'server1')].port"
|
||||
|
||||
.. note:: while using ``starts_with`` and ``contains``, you have to use `` to_json | from_json `` filter for correct parsing of data structure.
|
||||
|
||||
Working with Unicode
|
||||
---------------------
|
||||
|
||||
`Unicode <https://unicode.org/main.html>`_ makes it possible to produce two strings which may be visually equivalent, but are comprised of distinctly different characters/character sequences. To address this ``Unicode`` defines `normalization forms <https://unicode.org/reports/tr15/>`_ which avoid these distinctions by choosing a unique character sequence for a given visual representation.
|
||||
|
||||
You can use the ``community.general.unicode_normalize`` filter to normalize ``Unicode`` strings within your playbooks.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Compare Unicode representations
|
||||
debug:
|
||||
msg: "{{ with_combining_character | community.general.unicode_normalize == without_combining_character }}"
|
||||
vars:
|
||||
with_combining_character: "{{ 'Mayagu\u0308ez' }}"
|
||||
without_combining_character: Mayagüez
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Compare Unicode representations] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": true
|
||||
}
|
||||
|
||||
The ``community.general.unicode_normalize`` filter accepts a keyword argument to select the ``Unicode`` form used to normalize the input string.
|
||||
|
||||
:form: One of ``'NFC'`` (default), ``'NFD'``, ``'NFKC'``, or ``'NFKD'``. See the `Unicode reference <https://unicode.org/reports/tr15/>`_ for more information.
|
||||
|
||||
.. versionadded:: 3.7.0
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
filter_guide_paths
|
||||
filter_guide_abstract_informations
|
||||
filter_guide_working_with_times
|
||||
filter_guide_working_with_versions
|
||||
filter_guide_creating_identifiers
|
||||
filter_guide_conversions
|
||||
filter_guide_selecting_json_data
|
||||
filter_guide_working_with_unicode
|
||||
|
||||
10
docs/docsite/rst/filter_guide_abstract_informations.rst
Normal file
10
docs/docsite/rst/filter_guide_abstract_informations.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Abstract transformations
|
||||
------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
filter_guide_abstract_informations_dictionaries
|
||||
filter_guide_abstract_informations_grouping
|
||||
filter_guide_abstract_informations_merging_lists_of_dictionaries
|
||||
filter_guide_abstract_informations_counting_elements_in_sequence
|
||||
@@ -0,0 +1,77 @@
|
||||
Counting elements in a sequence
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``community.general.counter`` filter plugin allows you to count (hashable) elements in a sequence. Elements are returned as dictionary keys and their counts are stored as dictionary values.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Count character occurrences in a string
|
||||
debug:
|
||||
msg: "{{ 'abccbaabca' | community.general.counter }}"
|
||||
|
||||
- name: Count items in a list
|
||||
debug:
|
||||
msg: "{{ ['car', 'car', 'bike', 'plane', 'bike'] | community.general.counter }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Count character occurrences in a string] ********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"a": 4,
|
||||
"b": 3,
|
||||
"c": 3
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Count items in a list] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"bike": 2,
|
||||
"car": 2,
|
||||
"plane": 1
|
||||
}
|
||||
}
|
||||
|
||||
This plugin is useful for selecting resources based on current allocation:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks
|
||||
debug:
|
||||
msg: >-
|
||||
{{
|
||||
( disks | dict2items | map(attribute='value.adapter') | list
|
||||
| community.general.counter | dict2items
|
||||
| rejectattr('value', '>=', 4) | sort(attribute='value') | first
|
||||
).key
|
||||
}}
|
||||
vars:
|
||||
disks:
|
||||
sda:
|
||||
adapter: scsi_1
|
||||
sdb:
|
||||
adapter: scsi_1
|
||||
sdc:
|
||||
adapter: scsi_1
|
||||
sdd:
|
||||
adapter: scsi_1
|
||||
sde:
|
||||
adapter: scsi_2
|
||||
sdf:
|
||||
adapter: scsi_3
|
||||
sdg:
|
||||
adapter: scsi_3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks]
|
||||
ok: [localhost] => {
|
||||
"msg": "scsi_2"
|
||||
}
|
||||
|
||||
.. versionadded:: 4.3.0
|
||||
@@ -0,0 +1,119 @@
|
||||
Dictionaries
|
||||
^^^^^^^^^^^^
|
||||
|
||||
You can use the ``dict_kv`` filter to create a single-entry dictionary with ``value | community.general.dict_kv(key)``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a single-entry dictionary
|
||||
debug:
|
||||
msg: "{{ myvar | community.general.dict_kv('thatsmyvar') }}"
|
||||
vars:
|
||||
myvar: myvalue
|
||||
|
||||
- name: Create a list of dictionaries where the 'server' field is taken from a list
|
||||
debug:
|
||||
msg: >-
|
||||
{{ myservers | map('community.general.dict_kv', 'server')
|
||||
| map('combine', common_config) }}
|
||||
vars:
|
||||
common_config:
|
||||
type: host
|
||||
database: all
|
||||
myservers:
|
||||
- server1
|
||||
- server2
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a single-entry dictionary] **************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"thatsmyvar": "myvalue"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries where the 'server' field is taken from a list] *******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server1",
|
||||
"type": "host"
|
||||
},
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server2",
|
||||
"type": "host"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
If you need to convert a list of key-value pairs to a dictionary, you can use the ``dict`` function. Unfortunately, this function cannot be used with ``map``. For this, the ``community.general.dict`` filter can be used:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a dictionary with the dict function
|
||||
debug:
|
||||
msg: "{{ dict([[1, 2], ['a', 'b']]) }}"
|
||||
|
||||
- name: Create a dictionary with the community.general.dict filter
|
||||
debug:
|
||||
msg: "{{ [[1, 2], ['a', 'b']] | community.general.dict }}"
|
||||
|
||||
- name: Create a list of dictionaries with map and the community.general.dict filter
|
||||
debug:
|
||||
msg: >-
|
||||
{{ values | map('zip', ['k1', 'k2', 'k3'])
|
||||
| map('map', 'reverse')
|
||||
| map('community.general.dict') }}
|
||||
vars:
|
||||
values:
|
||||
- - foo
|
||||
- 23
|
||||
- a
|
||||
- - bar
|
||||
- 42
|
||||
- b
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a dictionary with the dict function] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a dictionary with the community.general.dict filter] ************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries with map and the community.general.dict filter] ******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"k1": "foo",
|
||||
"k2": 23,
|
||||
"k3": "a"
|
||||
},
|
||||
{
|
||||
"k1": "bar",
|
||||
"k2": 42,
|
||||
"k3": "b"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
@@ -0,0 +1,98 @@
|
||||
Grouping
|
||||
^^^^^^^^
|
||||
|
||||
If you have a list of dictionaries, the Jinja2 ``groupby`` filter allows to group the list by an attribute. This results in a list of ``(grouper, list)`` namedtuples, where ``list`` contains all dictionaries where the selected attribute equals ``grouper``. If you know that for every ``grouper``, there will be a most one entry in that list, you can use the ``community.general.groupby_as_dict`` filter to convert the original list into a dictionary which maps ``grouper`` to the corresponding dictionary.
|
||||
|
||||
One example is ``ansible_facts.mounts``, which is a list of dictionaries where each has one ``device`` element to indicate the device which is mounted. Therefore, ``ansible_facts.mounts | community.general.groupby_as_dict('device')`` is a dictionary mapping a device to the mount information:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Output mount facts grouped by device name
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('device')
|
||||
|
||||
- name: Output mount facts grouped by mount point
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('mount')
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Output mount facts grouped by device name] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('device')": {
|
||||
"/dev/sda1": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
},
|
||||
"/dev/sda2": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "abcdef01-2345-6789-0abc-def012345678"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Output mount facts grouped by mount point] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('mount')": {
|
||||
"/": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "bdf50b7d-4859-40af-8665-c637ee7a7808"
|
||||
},
|
||||
"/boot": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
@@ -0,0 +1,292 @@
|
||||
Merging lists of dictionaries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the ``lists_mergeby`` filter.
|
||||
|
||||
.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`.
|
||||
|
||||
Let us use the lists below in the following examples:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
|
||||
In the example below the lists are merged by the attribute ``name``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ list1|
|
||||
community.general.lists_mergeby(list2, 'name') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
It is possible to use a list of lists as an input of the filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name') }}"
|
||||
|
||||
This produces the same result as in the previous example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
|
||||
|
||||
The filter also accepts two optional parameters: ``recursive`` and ``list_merge``. These parameters are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9. This is available since community.general 4.4.0.
|
||||
|
||||
**recursive**
|
||||
Is a boolean, default to ``False``. Should the ``community.general.lists_mergeby`` recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``.
|
||||
|
||||
**list_merge**
|
||||
Is a string, its possible values are ``replace`` (default), ``keep``, ``append``, ``prepend``, ``append_rp`` or ``prepend_rp``. It modifies the behaviour of ``community.general.lists_mergeby`` when the hashes to merge contain arrays/lists.
|
||||
|
||||
The examples below set ``recursive=true`` and display the differences among all six options of ``list_merge``. Functionality of the parameters is exactly the same as in the filter ``combine``. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options.
|
||||
|
||||
Let us use the lists below in the following examples
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
|
||||
Example ``list_merge=replace`` (default):
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true) }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=keep``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='keep') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
Example ``list_merge=append``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=prepend``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
Example ``list_merge=append_rp``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append_rp') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=prepend_rp``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend_rp') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
|
||||
108
docs/docsite/rst/filter_guide_conversions.rst
Normal file
108
docs/docsite/rst/filter_guide_conversions.rst
Normal file
@@ -0,0 +1,108 @@
|
||||
Conversions
|
||||
-----------
|
||||
|
||||
Parsing CSV files
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ansible offers the :ref:`community.general.read_csv module <ansible_collections.community.general.read_csv_module>` to read CSV files. Sometimes you need to convert strings to CSV files instead. For this, the ``from_csv`` filter exists.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Parse CSV from string"
|
||||
debug:
|
||||
msg: "{{ csv_string | community.general.from_csv }}"
|
||||
vars:
|
||||
csv_string: |
|
||||
foo,bar,baz
|
||||
1,2,3
|
||||
you,this,then
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Parse CSV from string] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"bar": "2",
|
||||
"baz": "3",
|
||||
"foo": "1"
|
||||
},
|
||||
{
|
||||
"bar": "this",
|
||||
"baz": "then",
|
||||
"foo": "you"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The ``from_csv`` filter has several keyword arguments to control its behavior:
|
||||
|
||||
:dialect: Dialect of the CSV file. Default is ``excel``. Other possible choices are ``excel-tab`` and ``unix``. If one of ``delimiter``, ``skipinitialspace`` or ``strict`` is specified, ``dialect`` is ignored.
|
||||
:fieldnames: A set of column names to use. If not provided, the first line of the CSV is assumed to contain the column names.
|
||||
:delimiter: Sets the delimiter to use. Default depends on the dialect used.
|
||||
:skipinitialspace: Set to ``true`` to ignore space directly after the delimiter. Default depends on the dialect used (usually ``false``).
|
||||
:strict: Set to ``true`` to error out on invalid CSV input.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Converting to JSON
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`JC <https://pypi.org/project/jc/>`_ is a CLI tool and Python library which allows to interpret output of various CLI programs as JSON. It is also available as a filter in community.general. This filter needs the `jc Python library <https://pypi.org/project/jc/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Run 'ls' to list files in /
|
||||
command: ls /
|
||||
register: result
|
||||
|
||||
- name: Parse the ls output
|
||||
debug:
|
||||
msg: "{{ result.stdout | community.general.jc('ls') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Run 'ls' to list files in /] ********************************************************
|
||||
changed: [localhost]
|
||||
|
||||
TASK [Parse the ls output] ****************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"filename": "bin"
|
||||
},
|
||||
{
|
||||
"filename": "boot"
|
||||
},
|
||||
{
|
||||
"filename": "dev"
|
||||
},
|
||||
{
|
||||
"filename": "etc"
|
||||
},
|
||||
{
|
||||
"filename": "home"
|
||||
},
|
||||
{
|
||||
"filename": "lib"
|
||||
},
|
||||
{
|
||||
"filename": "proc"
|
||||
},
|
||||
{
|
||||
"filename": "root"
|
||||
},
|
||||
{
|
||||
"filename": "run"
|
||||
},
|
||||
{
|
||||
"filename": "tmp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
80
docs/docsite/rst/filter_guide_creating_identifiers.rst
Normal file
80
docs/docsite/rst/filter_guide_creating_identifiers.rst
Normal file
@@ -0,0 +1,80 @@
|
||||
Creating identifiers
|
||||
--------------------
|
||||
|
||||
The following filters allow to create identifiers.
|
||||
|
||||
Hashids
|
||||
^^^^^^^
|
||||
|
||||
`Hashids <https://hashids.org/>`_ allow to convert sequences of integers to short unique string identifiers. This filter needs the `hashids Python library <https://pypi.org/project/hashids/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create hashid"
|
||||
debug:
|
||||
msg: "{{ [1234, 5, 6] | community.general.hashids_encode }}"
|
||||
|
||||
- name: "Decode hashid"
|
||||
debug:
|
||||
msg: "{{ 'jm2Cytn' | community.general.hashids_decode }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "jm2Cytn"
|
||||
}
|
||||
|
||||
TASK [Decode hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
1234,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
|
||||
The hashids filters accept keyword arguments to allow fine-tuning the hashids generated:
|
||||
|
||||
:salt: String to use as salt when hashing.
|
||||
:alphabet: String of 16 or more unique characters to produce a hash.
|
||||
:min_length: Minimum length of hash produced.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Random MACs
|
||||
^^^^^^^^^^^
|
||||
|
||||
You can use the ``random_mac`` filter to complete a partial `MAC address <https://en.wikipedia.org/wiki/MAC_address>`_ to a random 6-byte MAC address.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create a random MAC starting with ff:"
|
||||
debug:
|
||||
msg: "{{ 'FF' | community.general.random_mac }}"
|
||||
|
||||
- name: "Create a random MAC starting with 00:11:22:"
|
||||
debug:
|
||||
msg: "{{ '00:11:22' | community.general.random_mac }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a random MAC starting with ff:] **********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "ff:69:d3:78:7f:b4"
|
||||
}
|
||||
|
||||
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "00:11:22:71:5d:3b"
|
||||
}
|
||||
|
||||
You can also initialize the random number generator from a seed to create random-but-idempotent MAC addresses:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
|
||||
14
docs/docsite/rst/filter_guide_paths.rst
Normal file
14
docs/docsite/rst/filter_guide_paths.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Paths
|
||||
-----
|
||||
|
||||
The ``path_join`` filter has been added in ansible-base 2.10. If you want to use this filter, but also need to support Ansible 2.9, you can use ``community.general``'s ``path_join`` shim, ``community.general.path_join``. This filter redirects to ``path_join`` for ansible-base 2.10 and ansible-core 2.11 or newer, and re-implements the filter for Ansible 2.9.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
# ansible-base 2.10 or newer:
|
||||
path: {{ ('/etc', path, 'subdir', file) | path_join }}
|
||||
|
||||
# Also works with Ansible 2.9:
|
||||
path: {{ ('/etc', path, 'subdir', file) | community.general.path_join }}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
144
docs/docsite/rst/filter_guide_selecting_json_data.rst
Normal file
144
docs/docsite/rst/filter_guide_selecting_json_data.rst
Normal file
@@ -0,0 +1,144 @@
|
||||
.. _ansible_collections.community.general.docsite.json_query_filter:
|
||||
|
||||
Selecting JSON data: JSON queries
|
||||
---------------------------------
|
||||
|
||||
To select a single element or a data subset from a complex data structure in JSON format (for example, Ansible facts), use the ``json_query`` filter. The ``json_query`` filter lets you query a complex JSON structure and iterate over it using a loop structure.
|
||||
|
||||
.. note:: You must manually install the **jmespath** dependency on the Ansible controller before using this filter. This filter is built upon **jmespath**, and you can use the same syntax. For examples, see `jmespath examples <http://jmespath.org/examples.html>`_.
|
||||
|
||||
Consider this data structure:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
{
|
||||
"domain_definition": {
|
||||
"domain": {
|
||||
"cluster": [
|
||||
{
|
||||
"name": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "cluster2"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
"name": "server11",
|
||||
"cluster": "cluster1",
|
||||
"port": "8080"
|
||||
},
|
||||
{
|
||||
"name": "server12",
|
||||
"cluster": "cluster1",
|
||||
"port": "8090"
|
||||
},
|
||||
{
|
||||
"name": "server21",
|
||||
"cluster": "cluster2",
|
||||
"port": "9080"
|
||||
},
|
||||
{
|
||||
"name": "server22",
|
||||
"cluster": "cluster2",
|
||||
"port": "9090"
|
||||
}
|
||||
],
|
||||
"library": [
|
||||
{
|
||||
"name": "lib1",
|
||||
"target": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "lib2",
|
||||
"target": "cluster2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
To extract all clusters from this structure, you can use the following query:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all cluster names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"
|
||||
|
||||
To extract all server names:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[*].name') }}"
|
||||
|
||||
To extract ports from cluster1:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"
|
||||
|
||||
.. note:: You can use a variable to make the query more readable.
|
||||
|
||||
To print out the ports from cluster1 in a comma separated string:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1 as a string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | community.general.json_query('domain.server[?cluster==`cluster1`].port') | join(', ') }}"
|
||||
|
||||
.. note:: In the example above, quoting literals using backticks avoids escaping quotes and maintains readability.
|
||||
|
||||
You can use YAML `single quote escaping <https://yaml.org/spec/current.html#id2534365>`_:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[?cluster==''cluster1''].port') }}"
|
||||
|
||||
.. note:: Escaping single quotes within single quotes in YAML is done by doubling the single quote.
|
||||
|
||||
To get a hash map with all ports and names of a cluster:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server ports and names from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"
|
||||
|
||||
To extract ports from all clusters with name starting with 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?starts_with(name,'server1')].port"
|
||||
|
||||
To extract ports from all clusters with name containing 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?contains(name,'server1')].port"
|
||||
|
||||
.. note:: while using ``starts_with`` and ``contains``, you have to use `` to_json | from_json `` filter for correct parsing of data structure.
|
||||
84
docs/docsite/rst/filter_guide_working_with_times.rst
Normal file
84
docs/docsite/rst/filter_guide_working_with_times.rst
Normal file
@@ -0,0 +1,84 @@
|
||||
Working with times
|
||||
------------------
|
||||
|
||||
The ``to_time_unit`` filter allows to convert times from a human-readable string to a unit. For example, ``'4h 30min 12second' | community.general.to_time_unit('hour')`` gives the number of hours that correspond to 4 hours, 30 minutes and 12 seconds.
|
||||
|
||||
There are shorthands to directly convert to various units, like ``to_hours``, ``to_minutes``, ``to_seconds``, and so on. The following table lists all units that can be used:
|
||||
|
||||
.. list-table:: Units
|
||||
:widths: 25 25 25 25
|
||||
:header-rows: 1
|
||||
|
||||
* - Unit name
|
||||
- Unit value in seconds
|
||||
- Unit strings for filter
|
||||
- Shorthand filter
|
||||
* - Millisecond
|
||||
- 1/1000 second
|
||||
- ``ms``, ``millisecond``, ``milliseconds``, ``msec``, ``msecs``, ``msecond``, ``mseconds``
|
||||
- ``to_milliseconds``
|
||||
* - Second
|
||||
- 1 second
|
||||
- ``s``, ``sec``, ``secs``, ``second``, ``seconds``
|
||||
- ``to_seconds``
|
||||
* - Minute
|
||||
- 60 seconds
|
||||
- ``m``, ``min``, ``mins``, ``minute``, ``minutes``
|
||||
- ``to_minutes``
|
||||
* - Hour
|
||||
- 60*60 seconds
|
||||
- ``h``, ``hour``, ``hours``
|
||||
- ``to_hours``
|
||||
* - Day
|
||||
- 24*60*60 seconds
|
||||
- ``d``, ``day``, ``days``
|
||||
- ``to_days``
|
||||
* - Week
|
||||
- 7*24*60*60 seconds
|
||||
- ``w``, ``week``, ``weeks``
|
||||
- ``to_weeks``
|
||||
* - Month
|
||||
- 30*24*60*60 seconds
|
||||
- ``mo``, ``month``, ``months``
|
||||
- ``to_months``
|
||||
* - Year
|
||||
- 365*24*60*60 seconds
|
||||
- ``y``, ``year``, ``years``
|
||||
- ``to_years``
|
||||
|
||||
Note that months and years are using a simplified representation: a month is 30 days, and a year is 365 days. If you need different definitions of months or years, you can pass them as keyword arguments. For example, if you want a year to be 365.25 days, and a month to be 30.5 days, you can write ``'11months 4' | community.general.to_years(year=365.25, month=30.5)``. These keyword arguments can be specified to ``to_time_unit`` and to all shorthand filters.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Convert string to seconds
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_time_unit('seconds') }}"
|
||||
|
||||
- name: Convert string to hours
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_hours }}"
|
||||
|
||||
- name: Convert string to years (using 365.25 days == 1 year)
|
||||
debug:
|
||||
msg: "{{ '400d 15h' | community.general.to_years(year=365.25) }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Convert string to seconds] **********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "109210.123"
|
||||
}
|
||||
|
||||
TASK [Convert string to hours] ************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "30.336145277778"
|
||||
}
|
||||
|
||||
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
||||
ok: [localhost] => {
|
||||
"msg": "1.096851471595"
|
||||
}
|
||||
|
||||
.. versionadded: 0.2.0
|
||||
30
docs/docsite/rst/filter_guide_working_with_unicode.rst
Normal file
30
docs/docsite/rst/filter_guide_working_with_unicode.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
Working with Unicode
|
||||
---------------------
|
||||
|
||||
`Unicode <https://unicode.org/main.html>`_ makes it possible to produce two strings which may be visually equivalent, but are comprised of distinctly different characters/character sequences. To address this ``Unicode`` defines `normalization forms <https://unicode.org/reports/tr15/>`_ which avoid these distinctions by choosing a unique character sequence for a given visual representation.
|
||||
|
||||
You can use the ``community.general.unicode_normalize`` filter to normalize ``Unicode`` strings within your playbooks.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Compare Unicode representations
|
||||
debug:
|
||||
msg: "{{ with_combining_character | community.general.unicode_normalize == without_combining_character }}"
|
||||
vars:
|
||||
with_combining_character: "{{ 'Mayagu\u0308ez' }}"
|
||||
without_combining_character: Mayagüez
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Compare Unicode representations] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": true
|
||||
}
|
||||
|
||||
The ``community.general.unicode_normalize`` filter accepts a keyword argument to select the ``Unicode`` form used to normalize the input string.
|
||||
|
||||
:form: One of ``'NFC'`` (default), ``'NFD'``, ``'NFKC'``, or ``'NFKD'``. See the `Unicode reference <https://unicode.org/reports/tr15/>`_ for more information.
|
||||
|
||||
.. versionadded:: 3.7.0
|
||||
34
docs/docsite/rst/filter_guide_working_with_versions.rst
Normal file
34
docs/docsite/rst/filter_guide_working_with_versions.rst
Normal file
@@ -0,0 +1,34 @@
|
||||
Working with versions
|
||||
---------------------
|
||||
|
||||
If you need to sort a list of version numbers, the Jinja ``sort`` filter is problematic. Since it sorts lexicographically, ``2.10`` will come before ``2.9``. To treat version numbers correctly, you can use the ``version_sort`` filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Sort list by version number
|
||||
debug:
|
||||
var: ansible_versions | community.general.version_sort
|
||||
vars:
|
||||
ansible_versions:
|
||||
- '2.8.0'
|
||||
- '2.11.0'
|
||||
- '2.7.0'
|
||||
- '2.10.0'
|
||||
- '2.9.0'
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Sort list by version number] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_versions | community.general.version_sort": [
|
||||
"2.7.0",
|
||||
"2.8.0",
|
||||
"2.9.0",
|
||||
"2.10.0",
|
||||
"2.11.0"
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: general
|
||||
version: 4.3.0
|
||||
version: 4.8.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
||||
1
plugins/cache/memcached.py
vendored
1
plugins/cache/memcached.py
vendored
@@ -20,6 +20,7 @@ DOCUMENTATION = '''
|
||||
- List of connection information for the memcached DBs
|
||||
default: ['127.0.0.1:11211']
|
||||
type: list
|
||||
elements: string
|
||||
env:
|
||||
- name: ANSIBLE_CACHE_PLUGIN_CONNECTION
|
||||
ini:
|
||||
|
||||
@@ -11,14 +11,16 @@ name: mail
|
||||
type: notification
|
||||
short_description: Sends failure events via email
|
||||
description:
|
||||
- This callback will report failures via email
|
||||
- This callback will report failures via email.
|
||||
author:
|
||||
- Dag Wieers (@dagwieers)
|
||||
requirements:
|
||||
- whitelisting in configuration
|
||||
options:
|
||||
mta:
|
||||
description: Mail Transfer Agent, server that accepts SMTP
|
||||
description:
|
||||
- Mail Transfer Agent, server that accepts SMTP.
|
||||
type: str
|
||||
env:
|
||||
- name: SMTPHOST
|
||||
ini:
|
||||
@@ -26,39 +28,53 @@ options:
|
||||
key: smtphost
|
||||
default: localhost
|
||||
mtaport:
|
||||
description: Mail Transfer Agent Port, port at which server SMTP
|
||||
description:
|
||||
- Mail Transfer Agent Port.
|
||||
- Port at which server SMTP.
|
||||
type: int
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: smtpport
|
||||
default: 25
|
||||
to:
|
||||
description: Mail recipient
|
||||
description:
|
||||
- Mail recipient.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: to
|
||||
default: root
|
||||
default: [root]
|
||||
sender:
|
||||
description: Mail sender
|
||||
description:
|
||||
- Mail sender.
|
||||
- Note that this will be required from community.general 6.0.0 on.
|
||||
type: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: sender
|
||||
cc:
|
||||
description: CC'd recipient
|
||||
description:
|
||||
- CC'd recipients.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: cc
|
||||
bcc:
|
||||
description: BCC'd recipient
|
||||
description:
|
||||
- BCC'd recipients.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: bcc
|
||||
notes:
|
||||
- "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
|
||||
'''
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import email.utils
|
||||
import smtplib
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
@@ -88,9 +104,13 @@ class CallbackModule(CallbackBase):
|
||||
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||
|
||||
self.sender = self.get_option('sender')
|
||||
if self.sender is None:
|
||||
self._display.deprecated(
|
||||
'The sender for the mail callback has not been specified. This will be an error in the future',
|
||||
version='6.0.0', collection_name='community.general')
|
||||
self.to = self.get_option('to')
|
||||
self.smtphost = self.get_option('mta')
|
||||
self.smtpport = int(self.get_option('mtaport'))
|
||||
self.smtpport = self.get_option('mtaport')
|
||||
self.cc = self.get_option('cc')
|
||||
self.bcc = self.get_option('bcc')
|
||||
|
||||
@@ -100,28 +120,34 @@ class CallbackModule(CallbackBase):
|
||||
|
||||
smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)
|
||||
|
||||
b_sender = to_bytes(self.sender)
|
||||
b_to = to_bytes(self.to)
|
||||
b_cc = to_bytes(self.cc)
|
||||
b_bcc = to_bytes(self.bcc)
|
||||
b_subject = to_bytes(subject)
|
||||
b_body = to_bytes(body)
|
||||
|
||||
b_content = b'From: %s\n' % b_sender
|
||||
b_content += b'To: %s\n' % b_to
|
||||
sender_address = email.utils.parseaddr(self.sender)
|
||||
if self.to:
|
||||
to_addresses = email.utils.getaddresses(self.to)
|
||||
if self.cc:
|
||||
b_content += b'Cc: %s\n' % b_cc
|
||||
b_content += b'Subject: %s\n\n' % b_subject
|
||||
b_content += b_body
|
||||
|
||||
b_addresses = b_to.split(b',')
|
||||
if self.cc:
|
||||
b_addresses += b_cc.split(b',')
|
||||
cc_addresses = email.utils.getaddresses(self.cc)
|
||||
if self.bcc:
|
||||
b_addresses += b_bcc.split(b',')
|
||||
bcc_addresses = email.utils.getaddresses(self.bcc)
|
||||
|
||||
for b_address in b_addresses:
|
||||
smtp.sendmail(b_sender, b_address, b_content)
|
||||
content = 'Date: %s\n' % email.utils.formatdate()
|
||||
content += 'From: %s\n' % email.utils.formataddr(sender_address)
|
||||
if self.to:
|
||||
content += 'To: %s\n' % ', '.join([email.utils.formataddr(pair) for pair in to_addresses])
|
||||
if self.cc:
|
||||
content += 'Cc: %s\n' % ', '.join([email.utils.formataddr(pair) for pair in cc_addresses])
|
||||
content += 'Message-ID: %s\n' % email.utils.make_msgid()
|
||||
content += 'Subject: %s\n\n' % subject.strip()
|
||||
content += body
|
||||
|
||||
addresses = to_addresses
|
||||
if self.cc:
|
||||
addresses += cc_addresses
|
||||
if self.bcc:
|
||||
addresses += bcc_addresses
|
||||
|
||||
if not addresses:
|
||||
self._display.warning('No receiver has been specified for the mail callback plugin.')
|
||||
|
||||
smtp.sendmail(self.sender, [address for name, address in addresses], to_bytes(content))
|
||||
|
||||
smtp.quit()
|
||||
|
||||
|
||||
@@ -319,9 +319,9 @@ class OpenTelemetrySource(object):
|
||||
@staticmethod
|
||||
def url_from_args(args):
|
||||
# the order matters
|
||||
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url")
|
||||
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url", "registry_url", "endpoint", "uri", "updates_url")
|
||||
for arg in url_args:
|
||||
if args.get(arg):
|
||||
if args is not None and args.get(arg):
|
||||
return args.get(arg)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -41,6 +41,16 @@ DOCUMENTATION = '''
|
||||
ini:
|
||||
- section: callback_syslog_json
|
||||
key: syslog_facility
|
||||
setup:
|
||||
description: Log setup tasks.
|
||||
env:
|
||||
- name: ANSIBLE_SYSLOG_SETUP
|
||||
type: bool
|
||||
default: true
|
||||
ini:
|
||||
- section: callback_syslog_json
|
||||
key: syslog_setup
|
||||
version_added: 4.5.0
|
||||
'''
|
||||
|
||||
import os
|
||||
@@ -86,23 +96,36 @@ class CallbackModule(CallbackBase):
|
||||
self.logger.addHandler(self.handler)
|
||||
self.hostname = socket.gethostname()
|
||||
|
||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
res = result._result
|
||||
host = result._host.get_name()
|
||||
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||
|
||||
def runner_on_ok(self, host, res):
|
||||
self.logger.info('%s ansible-command: task execution OK; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||
def v2_runner_on_ok(self, result):
|
||||
res = result._result
|
||||
host = result._host.get_name()
|
||||
if result._task.action != "gather_facts" or self.get_option("setup"):
|
||||
self.logger.info('%s ansible-command: task execution OK; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||
|
||||
def runner_on_skipped(self, host, item=None):
|
||||
def v2_runner_on_skipped(self, result):
|
||||
host = result._host.get_name()
|
||||
self.logger.info('%s ansible-command: task execution SKIPPED; host: %s; message: %s', self.hostname, host, 'skipped')
|
||||
|
||||
def runner_on_unreachable(self, host, res):
|
||||
def v2_runner_on_unreachable(self, result):
|
||||
res = result._result
|
||||
host = result._host.get_name()
|
||||
self.logger.error('%s ansible-command: task execution UNREACHABLE; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||
|
||||
def runner_on_async_failed(self, host, res, jid):
|
||||
def v2_runner_on_async_failed(self, result):
|
||||
res = result._result
|
||||
host = result._host.get_name()
|
||||
jid = result._result.get('ansible_job_id')
|
||||
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||
|
||||
def playbook_on_import_for_host(self, host, imported_file):
|
||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||
host = result._host.get_name()
|
||||
self.logger.info('%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s', self.hostname, host, imported_file)
|
||||
|
||||
def playbook_on_not_import_for_host(self, host, missing_file):
|
||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||
host = result._host.get_name()
|
||||
self.logger.info('%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s', self.hostname, host, missing_file)
|
||||
|
||||
@@ -61,4 +61,11 @@ options:
|
||||
- Verify TLS certificates (do not disable this in production).
|
||||
type: bool
|
||||
default: yes
|
||||
|
||||
connection_timeout:
|
||||
description:
|
||||
- Controls the HTTP connections timeout period (in seconds) to Keycloak API.
|
||||
type: int
|
||||
default: 10
|
||||
version_added: 4.5.0
|
||||
'''
|
||||
|
||||
@@ -38,8 +38,10 @@ options:
|
||||
version_added: 2.0.0
|
||||
server_uri:
|
||||
description:
|
||||
- A URI to the LDAP server.
|
||||
- The I(server_uri) parameter may be a comma- or whitespace-separated list of URIs containing only the schema, the host, and the port fields.
|
||||
- The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location.
|
||||
- Note that when using multiple URIs you cannot determine to which URI your client gets connected.
|
||||
- For URIs containing additional fields, particularly when using commas, behavior is undefined.
|
||||
type: str
|
||||
default: ldapi:///
|
||||
start_tls:
|
||||
|
||||
@@ -1,43 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Vladimir Botka <vbotka@gmail.com>
|
||||
# Copyright (c) 2020-2022, Vladimir Botka <vbotka@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFilterError
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.common._collections_compat import Mapping, Sequence
|
||||
from ansible.utils.vars import merge_hash
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
|
||||
def lists_mergeby(l1, l2, index):
|
||||
''' merge lists by attribute index. Example:
|
||||
- debug: msg="{{ l1|community.general.lists_mergeby(l2, 'index')|list }}" '''
|
||||
def merge_hash_wrapper(x, y, recursive=False, list_merge='replace'):
|
||||
''' Wrapper of the function merge_hash from ansible.utils.vars. Only 2 paramaters are allowed
|
||||
for Ansible 2.9 and lower.'''
|
||||
|
||||
if not isinstance(l1, Sequence):
|
||||
raise AnsibleFilterError('First argument for community.general.lists_mergeby must be list. %s is %s' %
|
||||
(l1, type(l1)))
|
||||
if LooseVersion(ansible_version) < LooseVersion('2.10'):
|
||||
if list_merge != 'replace' or recursive:
|
||||
msg = ("Non default options of list_merge(default=replace) or recursive(default=False) "
|
||||
"are not allowed in Ansible version 2.9 or lower. Ansible version is %s, "
|
||||
"recursive=%s, and list_merge=%s.")
|
||||
raise AnsibleFilterError(msg % (ansible_version, recursive, list_merge))
|
||||
else:
|
||||
return merge_hash(x, y)
|
||||
else:
|
||||
return merge_hash(x, y, recursive, list_merge)
|
||||
|
||||
if not isinstance(l2, Sequence):
|
||||
raise AnsibleFilterError('Second argument for community.general.lists_mergeby must be list. %s is %s' %
|
||||
(l2, type(l2)))
|
||||
|
||||
if not isinstance(index, string_types):
|
||||
raise AnsibleFilterError('Third argument for community.general.lists_mergeby must be string. %s is %s' %
|
||||
(index, type(index)))
|
||||
def list_mergeby(x, y, index, recursive=False, list_merge='replace'):
|
||||
''' Merge 2 lists by attribute 'index'. The function merge_hash from ansible.utils.vars is used.
|
||||
This function is used by the function lists_mergeby.
|
||||
'''
|
||||
|
||||
d = defaultdict(dict)
|
||||
for l in (l1, l2):
|
||||
for l in (x, y):
|
||||
for elem in l:
|
||||
if not isinstance(elem, Mapping):
|
||||
raise AnsibleFilterError('Elements of list arguments for lists_mergeby must be dictionaries. Found {0!r}.'.format(elem))
|
||||
msg = "Elements of list arguments for lists_mergeby must be dictionaries. %s is %s"
|
||||
raise AnsibleFilterError(msg % (elem, type(elem)))
|
||||
if index in elem.keys():
|
||||
d[elem[index]].update(elem)
|
||||
d[elem[index]].update(merge_hash_wrapper(d[elem[index]], elem, recursive, list_merge))
|
||||
return sorted(d.values(), key=itemgetter(index))
|
||||
|
||||
|
||||
def lists_mergeby(*terms, **kwargs):
|
||||
''' Merge 2 or more lists by attribute 'index'. Optional parameters 'recursive' and 'list_merge'
|
||||
control the merging of the lists in values. The function merge_hash from ansible.utils.vars
|
||||
is used. To learn details on how to use the parameters 'recursive' and 'list_merge' see
|
||||
Ansible User's Guide chapter "Using filters to manipulate data" section "Combining
|
||||
hashes/dictionaries".
|
||||
|
||||
Example:
|
||||
- debug:
|
||||
msg: "{{ list1|
|
||||
community.general.lists_mergeby(list2,
|
||||
'index',
|
||||
recursive=True,
|
||||
list_merge='append')|
|
||||
list }}"
|
||||
'''
|
||||
|
||||
recursive = kwargs.pop('recursive', False)
|
||||
list_merge = kwargs.pop('list_merge', 'replace')
|
||||
if kwargs:
|
||||
raise AnsibleFilterError("'recursive' and 'list_merge' are the only valid keyword arguments.")
|
||||
if len(terms) < 2:
|
||||
raise AnsibleFilterError("At least one list and index are needed.")
|
||||
|
||||
# allow the user to do `[list1, list2, ...] | lists_mergeby('index')`
|
||||
flat_list = []
|
||||
for sublist in terms[:-1]:
|
||||
if not isinstance(sublist, Sequence):
|
||||
msg = ("All arguments before the argument index for community.general.lists_mergeby "
|
||||
"must be lists. %s is %s")
|
||||
raise AnsibleFilterError(msg % (sublist, type(sublist)))
|
||||
if len(sublist) > 0:
|
||||
if all(isinstance(l, Sequence) for l in sublist):
|
||||
for item in sublist:
|
||||
flat_list.append(item)
|
||||
else:
|
||||
flat_list.append(sublist)
|
||||
lists = flat_list
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
index = terms[-1]
|
||||
|
||||
if not isinstance(index, string_types):
|
||||
msg = ("First argument after the lists for community.general.lists_mergeby must be string. "
|
||||
"%s is %s")
|
||||
raise AnsibleFilterError(msg % (index, type(index)))
|
||||
|
||||
high_to_low_prio_list_iterator = reversed(lists)
|
||||
result = next(high_to_low_prio_list_iterator)
|
||||
for list in high_to_low_prio_list_iterator:
|
||||
result = list_mergeby(list, result, index, recursive, list_merge)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
''' Ansible list filters '''
|
||||
|
||||
|
||||
@@ -40,13 +40,25 @@ DOCUMENTATION = '''
|
||||
type: boolean
|
||||
default: no
|
||||
exclude_profiles:
|
||||
description: Profiles to exclude from inventory
|
||||
description:
|
||||
- Profiles to exclude from inventory.
|
||||
- Ignored if I(include_profiles) is specified.
|
||||
type: list
|
||||
default: []
|
||||
elements: str
|
||||
include_profiles:
|
||||
description:
|
||||
- Profiles to include from inventory.
|
||||
- If specified, all other profiles will be excluded.
|
||||
- I(exclude_profiles) is ignored if I(include_profiles) is specified.
|
||||
type: list
|
||||
default: []
|
||||
elements: str
|
||||
version_added: 4.4.0
|
||||
group_by:
|
||||
description: Keys to group hosts by
|
||||
type: list
|
||||
elements: string
|
||||
default: [ 'mgmt_classes', 'owners', 'status' ]
|
||||
group:
|
||||
description: Group to place all hosts into
|
||||
@@ -71,8 +83,7 @@ password: secure
|
||||
import socket
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.common._collections_compat import MutableMapping
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, to_safe_group_name
|
||||
|
||||
@@ -94,18 +105,9 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
NAME = 'community.general.cobbler'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
# from config
|
||||
self.cobbler_url = None
|
||||
self.exclude_profiles = [] # A list of profiles to exclude
|
||||
|
||||
self.connection = None
|
||||
self.token = None
|
||||
|
||||
self.cache_key = None
|
||||
self.use_cache = None
|
||||
self.connection = None
|
||||
|
||||
def verify_file(self, path):
|
||||
valid = False
|
||||
@@ -177,6 +179,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
self.inventory.add_child(group_name, child)
|
||||
return group_name
|
||||
|
||||
def _exclude_profile(self, profile):
|
||||
if self.include_profiles:
|
||||
return profile not in self.include_profiles
|
||||
else:
|
||||
return profile in self.exclude_profiles
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
@@ -190,15 +198,16 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
self.use_cache = cache and self.get_option('cache')
|
||||
|
||||
self.exclude_profiles = self.get_option('exclude_profiles')
|
||||
self.include_profiles = self.get_option('include_profiles')
|
||||
self.group_by = self.get_option('group_by')
|
||||
|
||||
for profile in self._get_profiles():
|
||||
if profile['parent']:
|
||||
self.display.vvvv('Processing profile %s with parent %s\n' % (profile['name'], profile['parent']))
|
||||
if profile['parent'] not in self.exclude_profiles:
|
||||
if not self._exclude_profile(profile['parent']):
|
||||
parent_group_name = self._add_safe_group_name(profile['parent'])
|
||||
self.display.vvvv('Added profile parent group %s\n' % parent_group_name)
|
||||
if profile['name'] not in self.exclude_profiles:
|
||||
if not self._exclude_profile(profile['name']):
|
||||
group_name = self._add_safe_group_name(profile['name'])
|
||||
self.display.vvvv('Added profile group %s\n' % group_name)
|
||||
self.inventory.add_child(parent_group_name, group_name)
|
||||
@@ -210,7 +219,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
while i < len(profile_elements) - 1:
|
||||
profile_group = '-'.join(profile_elements[0:i + 1])
|
||||
profile_group_child = '-'.join(profile_elements[0:i + 2])
|
||||
if profile_group in self.exclude_profiles:
|
||||
if self._exclude_profile(profile_group):
|
||||
self.display.vvvv('Excluding profile %s\n' % profile_group)
|
||||
break
|
||||
group_name = self._add_safe_group_name(profile_group)
|
||||
@@ -231,7 +240,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
hostname = host['hostname'] # None
|
||||
interfaces = host['interfaces']
|
||||
|
||||
if host['profile'] in self.exclude_profiles:
|
||||
if self._exclude_profile(host['profile']):
|
||||
self.display.vvvv('Excluding host %s in profile %s\n' % (host['name'], host['profile']))
|
||||
continue
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ DOCUMENTATION = '''
|
||||
version_added: 1.0.0
|
||||
type: str
|
||||
required: true
|
||||
default: https://gitlab.com
|
||||
api_token:
|
||||
description: GitLab token for logging in.
|
||||
env:
|
||||
|
||||
@@ -16,7 +16,17 @@ DOCUMENTATION = '''
|
||||
- Get inventory hosts from the Icinga2 API.
|
||||
- "Uses a configuration file as an inventory source, it must end in
|
||||
C(.icinga2.yml) or C(.icinga2.yaml)."
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
options:
|
||||
strict:
|
||||
version_added: 4.4.0
|
||||
compose:
|
||||
version_added: 4.4.0
|
||||
groups:
|
||||
version_added: 4.4.0
|
||||
keyed_groups:
|
||||
version_added: 4.4.0
|
||||
plugin:
|
||||
description: Name of the plugin.
|
||||
required: true
|
||||
@@ -63,6 +73,20 @@ password: secure
|
||||
host_filter: \"linux-servers\" in host.groups
|
||||
validate_certs: false
|
||||
inventory_attr: name
|
||||
groups:
|
||||
# simple name matching
|
||||
webservers: inventory_hostname.startswith('web')
|
||||
|
||||
# using icinga2 template
|
||||
databaseservers: "'db-template' in (icinga2_attributes.templates|list)"
|
||||
|
||||
compose:
|
||||
# set all icinga2 attributes to a host variable 'icinga2_attrs'
|
||||
icinga2_attrs: icinga2_attributes
|
||||
|
||||
# set 'ansible_user' and 'ansible_port' from icinga2 host vars
|
||||
ansible_user: icinga2_attributes.vars.ansible_user
|
||||
ansible_port: icinga2_attributes.vars.ansible_port | default(22)
|
||||
'''
|
||||
|
||||
import json
|
||||
@@ -180,7 +204,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
"""Query for all hosts """
|
||||
self.display.vvv("Querying Icinga2 for inventory")
|
||||
query_args = {
|
||||
"attrs": ["address", "display_name", "state_type", "state", "groups"],
|
||||
"attrs": ["address", "address6", "name", "display_name", "state_type", "state", "templates", "groups", "vars", "zone"],
|
||||
}
|
||||
if self.host_filter is not None:
|
||||
query_args['host_filter'] = self.host_filter
|
||||
@@ -190,6 +214,12 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
ansible_inv = self._convert_inv(results_json)
|
||||
return ansible_inv
|
||||
|
||||
def _apply_constructable(self, name, variables):
|
||||
strict = self.get_option('strict')
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=strict)
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=strict)
|
||||
self._set_composite_vars(self.get_option('compose'), variables, name, strict=strict)
|
||||
|
||||
def _populate(self):
|
||||
groups = self._to_json(self.get_inventory_from_icinga())
|
||||
return groups
|
||||
@@ -232,6 +262,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
host_attrs['state'])
|
||||
self.inventory.set_variable(host_name, 'state_type',
|
||||
host_attrs['state_type'])
|
||||
# Adds all attributes to a variable 'icinga2_attributes'
|
||||
construct_vars = dict(self.inventory.get_host(host_name).get_vars())
|
||||
construct_vars['icinga2_attributes'] = host_attrs
|
||||
self._apply_constructable(host_name, construct_vars)
|
||||
return groups_dict
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
|
||||
@@ -21,7 +21,18 @@ DOCUMENTATION = r'''
|
||||
Linode) and not tags.
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
options:
|
||||
cache:
|
||||
version_added: 4.5.0
|
||||
cache_plugin:
|
||||
version_added: 4.5.0
|
||||
cache_timeout:
|
||||
version_added: 4.5.0
|
||||
cache_connection:
|
||||
version_added: 4.5.0
|
||||
cache_prefix:
|
||||
version_added: 4.5.0
|
||||
plugin:
|
||||
description: Marks this as an instance of the 'linode' plugin.
|
||||
required: true
|
||||
@@ -43,15 +54,18 @@ DOCUMENTATION = r'''
|
||||
description: Populate inventory with instances in this region.
|
||||
default: []
|
||||
type: list
|
||||
elements: string
|
||||
tags:
|
||||
description: Populate inventory only with instances which have at least one of the tags listed here.
|
||||
default: []
|
||||
type: list
|
||||
elements: string
|
||||
version_added: 2.0.0
|
||||
types:
|
||||
description: Populate inventory with instances with this type.
|
||||
default: []
|
||||
type: list
|
||||
elements: string
|
||||
strict:
|
||||
version_added: 2.0.0
|
||||
compose:
|
||||
@@ -66,6 +80,12 @@ EXAMPLES = r'''
|
||||
# Minimal example. `LINODE_ACCESS_TOKEN` is exposed in environment.
|
||||
plugin: community.general.linode
|
||||
|
||||
# You can use Jinja to template the access token.
|
||||
plugin: community.general.linode
|
||||
access_token: "{{ lookup('ini', 'token', section='your_username', file='~/.config/linode-cli') }}"
|
||||
# For older Ansible versions, you need to write this as:
|
||||
# access_token: "{{ lookup('ini', 'token section=your_username file=~/.config/linode-cli') }}"
|
||||
|
||||
# Example with regions, types, groups and access token
|
||||
plugin: community.general.linode
|
||||
access_token: foobar
|
||||
@@ -104,25 +124,31 @@ import os
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
from ansible.template import Templar
|
||||
|
||||
|
||||
try:
|
||||
from linode_api4 import LinodeClient
|
||||
from linode_api4.objects.linode import Instance
|
||||
from linode_api4.errors import ApiError as LinodeApiError
|
||||
HAS_LINODE = True
|
||||
except ImportError:
|
||||
HAS_LINODE = False
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
NAME = 'community.general.linode'
|
||||
|
||||
def _build_client(self):
|
||||
def _build_client(self, loader):
|
||||
"""Build the Linode client."""
|
||||
|
||||
t = Templar(loader=loader)
|
||||
|
||||
access_token = self.get_option('access_token')
|
||||
if t.is_template(access_token):
|
||||
access_token = t.template(variable=access_token, disable_lookups=False)
|
||||
|
||||
if access_token is None:
|
||||
try:
|
||||
@@ -158,20 +184,23 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
for linode_group in self.linode_groups:
|
||||
self.inventory.add_group(linode_group)
|
||||
|
||||
def _filter_by_config(self, regions, types, tags):
|
||||
def _filter_by_config(self):
|
||||
"""Filter instances by user specified configuration."""
|
||||
regions = self.get_option('regions')
|
||||
if regions:
|
||||
self.instances = [
|
||||
instance for instance in self.instances
|
||||
if instance.region.id in regions
|
||||
]
|
||||
|
||||
types = self.get_option('types')
|
||||
if types:
|
||||
self.instances = [
|
||||
instance for instance in self.instances
|
||||
if instance.type.id in types
|
||||
]
|
||||
|
||||
tags = self.get_option('tags')
|
||||
if tags:
|
||||
self.instances = [
|
||||
instance for instance in self.instances
|
||||
@@ -224,76 +253,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
)
|
||||
return data
|
||||
|
||||
def _validate_option(self, name, desired_type, option_value):
|
||||
"""Validate user specified configuration data against types."""
|
||||
if isinstance(option_value, string_types) and desired_type == list:
|
||||
option_value = [option_value]
|
||||
|
||||
if option_value is None:
|
||||
option_value = desired_type()
|
||||
|
||||
if not isinstance(option_value, desired_type):
|
||||
raise AnsibleParserError(
|
||||
'The option %s (%s) must be a %s' % (
|
||||
name, option_value, desired_type
|
||||
)
|
||||
)
|
||||
|
||||
return option_value
|
||||
|
||||
def _get_query_options(self, config_data):
|
||||
"""Get user specified query options from the configuration."""
|
||||
options = {
|
||||
'regions': {
|
||||
'type_to_be': list,
|
||||
'value': config_data.get('regions', [])
|
||||
},
|
||||
'types': {
|
||||
'type_to_be': list,
|
||||
'value': config_data.get('types', [])
|
||||
},
|
||||
'tags': {
|
||||
'type_to_be': list,
|
||||
'value': config_data.get('tags', [])
|
||||
},
|
||||
}
|
||||
|
||||
for name in options:
|
||||
options[name]['value'] = self._validate_option(
|
||||
name,
|
||||
options[name]['type_to_be'],
|
||||
options[name]['value']
|
||||
)
|
||||
|
||||
regions = options['regions']['value']
|
||||
types = options['types']['value']
|
||||
tags = options['tags']['value']
|
||||
|
||||
return regions, types, tags
|
||||
|
||||
def verify_file(self, path):
|
||||
"""Verify the Linode configuration file."""
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
endings = ('linode.yaml', 'linode.yml')
|
||||
if any((path.endswith(ending) for ending in endings)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
"""Dynamically parse Linode the cloud inventory."""
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
|
||||
if not HAS_LINODE:
|
||||
raise AnsibleError('the Linode dynamic inventory plugin requires linode_api4.')
|
||||
|
||||
config_data = self._read_config_data(path)
|
||||
self._build_client()
|
||||
|
||||
self._get_instances_inventory()
|
||||
def _cacheable_inventory(self):
|
||||
return [i._raw_json for i in self.instances]
|
||||
|
||||
def populate(self):
|
||||
strict = self.get_option('strict')
|
||||
regions, types, tags = self._get_query_options(config_data)
|
||||
self._filter_by_config(regions, types, tags)
|
||||
|
||||
self._filter_by_config()
|
||||
|
||||
self._add_groups()
|
||||
self._add_instances_to_groups()
|
||||
@@ -315,3 +281,44 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
variables,
|
||||
instance.label,
|
||||
strict=strict)
|
||||
|
||||
def verify_file(self, path):
|
||||
"""Verify the Linode configuration file."""
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
endings = ('linode.yaml', 'linode.yml')
|
||||
if any((path.endswith(ending) for ending in endings)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
"""Dynamically parse Linode the cloud inventory."""
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
self.instances = None
|
||||
|
||||
if not HAS_LINODE:
|
||||
raise AnsibleError('the Linode dynamic inventory plugin requires linode_api4.')
|
||||
|
||||
self._read_config_data(path)
|
||||
|
||||
cache_key = self.get_cache_key(path)
|
||||
|
||||
if cache:
|
||||
cache = self.get_option('cache')
|
||||
|
||||
update_cache = False
|
||||
if cache:
|
||||
try:
|
||||
self.instances = [Instance(None, i["id"], i) for i in self._cache[cache_key]]
|
||||
except KeyError:
|
||||
update_cache = True
|
||||
|
||||
# Check for None rather than False in order to allow
|
||||
# for empty sets of cached instances
|
||||
if self.instances is None:
|
||||
self._build_client(loader)
|
||||
self._get_instances_inventory()
|
||||
|
||||
if update_cache:
|
||||
self._cache[cache_key] = self._cacheable_inventory()
|
||||
|
||||
self.populate()
|
||||
|
||||
@@ -666,9 +666,13 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
# add network informations
|
||||
self.build_inventory_network(instance_name)
|
||||
# add os
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(instance_name)).lower())
|
||||
v = self._get_data_entry('inventory/{0}/os'.format(instance_name))
|
||||
if v:
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_os', v.lower())
|
||||
# add release
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(instance_name)).lower())
|
||||
v = self._get_data_entry('inventory/{0}/release'.format(instance_name))
|
||||
if v:
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_release', v.lower())
|
||||
# add profile
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(instance_name)))
|
||||
# add state
|
||||
|
||||
@@ -21,12 +21,18 @@ DOCUMENTATION = '''
|
||||
description: token that ensures this is a source file for the 'nmap' plugin.
|
||||
required: True
|
||||
choices: ['nmap', 'community.general.nmap']
|
||||
sudo:
|
||||
description: Set to C(true) to execute a C(sudo nmap) plugin scan.
|
||||
version_added: 4.8.0
|
||||
default: false
|
||||
type: boolean
|
||||
address:
|
||||
description: Network IP or range of IPs to scan, you can use a simple range (10.2.2.15-25) or CIDR notation.
|
||||
required: True
|
||||
exclude:
|
||||
description: list of addresses to exclude
|
||||
type: list
|
||||
elements: string
|
||||
ports:
|
||||
description: Enable/disable scanning for open ports
|
||||
type: boolean
|
||||
@@ -48,6 +54,13 @@ EXAMPLES = '''
|
||||
plugin: community.general.nmap
|
||||
strict: False
|
||||
address: 192.168.0.0/24
|
||||
|
||||
|
||||
# a sudo nmap scan to fully use nmap scan power.
|
||||
plugin: community.general.nmap
|
||||
sudo: true
|
||||
strict: False
|
||||
address: 192.168.0.0/24
|
||||
'''
|
||||
|
||||
import os
|
||||
@@ -134,6 +147,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
if not user_cache_setting or cache_needs_update:
|
||||
# setup command
|
||||
cmd = [self._nmap]
|
||||
|
||||
if self._options['sudo']:
|
||||
cmd.insert(0, 'sudo')
|
||||
|
||||
if not self._options['ports']:
|
||||
cmd.append('-sP')
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ DOCUMENTATION = r'''
|
||||
hostnames:
|
||||
description: List of preference about what to use as an hostname.
|
||||
type: list
|
||||
elements: string
|
||||
default:
|
||||
- public_ipv4
|
||||
choices:
|
||||
@@ -37,6 +38,7 @@ DOCUMENTATION = r'''
|
||||
groups:
|
||||
description: List of groups.
|
||||
type: list
|
||||
elements: string
|
||||
choices:
|
||||
- location
|
||||
- offer
|
||||
|
||||
@@ -95,7 +95,7 @@ except ImportError:
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from collections import namedtuple
|
||||
import os
|
||||
@@ -206,28 +206,40 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
def _populate(self):
|
||||
hostname_preference = self.get_option('hostname')
|
||||
group_by_labels = self.get_option('group_by_labels')
|
||||
strict = self.get_option('strict')
|
||||
|
||||
# Add a top group 'one'
|
||||
self.inventory.add_group(group='all')
|
||||
|
||||
filter_by_label = self.get_option('filter_by_label')
|
||||
for server in self._retrieve_servers(filter_by_label):
|
||||
servers = self._retrieve_servers(filter_by_label)
|
||||
for server in servers:
|
||||
hostname = server['name']
|
||||
# check for labels
|
||||
if group_by_labels and server['LABELS']:
|
||||
for label in server['LABELS']:
|
||||
self.inventory.add_group(group=label)
|
||||
self.inventory.add_host(host=server['name'], group=label)
|
||||
self.inventory.add_host(host=hostname, group=label)
|
||||
|
||||
self.inventory.add_host(host=server['name'], group='all')
|
||||
self.inventory.add_host(host=hostname, group='all')
|
||||
|
||||
for attribute, value in server.items():
|
||||
self.inventory.set_variable(server['name'], attribute, value)
|
||||
self.inventory.set_variable(hostname, attribute, value)
|
||||
|
||||
if hostname_preference != 'name':
|
||||
self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference])
|
||||
self.inventory.set_variable(hostname, 'ansible_host', server[hostname_preference])
|
||||
|
||||
if server.get('SSH_PORT'):
|
||||
self.inventory.set_variable(server['name'], 'ansible_port', server['SSH_PORT'])
|
||||
self.inventory.set_variable(hostname, 'ansible_port', server['SSH_PORT'])
|
||||
|
||||
# handle construcable implementation: get composed variables if any
|
||||
self._set_composite_vars(self.get_option('compose'), server, hostname, strict=strict)
|
||||
|
||||
# groups based on jinja conditionals get added to specific groups
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), server, hostname, strict=strict)
|
||||
|
||||
# groups based on variables associated with them in the inventory
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), server, hostname, strict=strict)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
if not HAS_PYONE:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Copyright (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
@@ -31,6 +32,7 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- URL to Proxmox cluster.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_URL) will be used instead.
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the I(url).
|
||||
default: 'http://localhost:8006'
|
||||
type: str
|
||||
env:
|
||||
@@ -40,6 +42,7 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- Proxmox authentication user.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_USER) will be used instead.
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the I(user).
|
||||
required: yes
|
||||
type: str
|
||||
env:
|
||||
@@ -49,11 +52,33 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- Proxmox authentication password.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_PASSWORD) will be used instead.
|
||||
required: yes
|
||||
- Since community.general 4.7.0 you can also use templating to specify the value of the I(password).
|
||||
- If you do not specify a password, you must set I(token_id) and I(token_secret) instead.
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_PASSWORD
|
||||
version_added: 2.0.0
|
||||
token_id:
|
||||
description:
|
||||
- Proxmox authentication token ID.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_TOKEN_ID) will be used instead.
|
||||
- To use token authentication, you must also specify I(token_secret). If you do not specify I(token_id) and I(token_secret),
|
||||
you must set a password instead.
|
||||
- Make sure to grant explicit pve permissions to the token or disable 'privilege separation' to use the users' privileges instead.
|
||||
version_added: 4.8.0
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_TOKEN_ID
|
||||
token_secret:
|
||||
description:
|
||||
- Proxmox authentication token secret.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_TOKEN_SECRET) will be used instead.
|
||||
- To use token authentication, you must also specify I(token_id). If you do not specify I(token_id) and I(token_secret),
|
||||
you must set a password instead.
|
||||
version_added: 4.8.0
|
||||
type: str
|
||||
env:
|
||||
- name: PROXMOX_TOKEN_SECRET
|
||||
validate_certs:
|
||||
description: Verify SSL certificate if using HTTPS.
|
||||
type: boolean
|
||||
@@ -75,8 +100,16 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- Whether to set C(ansbile_host) for proxmox nodes.
|
||||
- When set to C(true) (default), will use the first available interface. This can be different from what you expect.
|
||||
default: true
|
||||
- This currently defaults to C(true), but the default is deprecated since community.general 4.8.0.
|
||||
The default will change to C(false) in community.general 6.0.0. To avoid a deprecation warning, please
|
||||
set this parameter explicitly.
|
||||
type: bool
|
||||
filters:
|
||||
version_added: 4.6.0
|
||||
description: A list of Jinja templates that allow filtering hosts.
|
||||
type: list
|
||||
elements: str
|
||||
default: []
|
||||
strict:
|
||||
version_added: 2.5.0
|
||||
compose:
|
||||
@@ -94,6 +127,25 @@ EXAMPLES = '''
|
||||
plugin: community.general.proxmox
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
# Note that this can easily give you wrong values as ansible_host. See further below for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
|
||||
# Instead of login with password, proxmox supports api token authentication since release 6.2.
|
||||
plugin: community.general.proxmox
|
||||
user: ci@pve
|
||||
token_id: gitlab-1
|
||||
token_secret: fa256e9c-26ab-41ec-82da-707a2c079829
|
||||
|
||||
# The secret can also be a vault string or passed via the environment variable TOKEN_SECRET.
|
||||
token_secret: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
62353634333163633336343265623632626339313032653563653165313262343931643431656138
|
||||
6134333736323265656466646539663134306166666237630a653363623262636663333762316136
|
||||
34616361326263383766366663393837626437316462313332663736623066656237386531663731
|
||||
3037646432383064630a663165303564623338666131353366373630656661333437393937343331
|
||||
32643131386134396336623736393634373936356332623632306561356361323737313663633633
|
||||
6231313333666361656537343562333337323030623732323833
|
||||
|
||||
# More complete example demonstrating the use of 'want_facts' and the constructed options
|
||||
# Note that using facts returned by 'want_facts' in constructed options requires 'want_facts=true'
|
||||
@@ -114,15 +166,51 @@ groups:
|
||||
mailservers: "'mail' in (proxmox_tags_parsed|list)"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
# Note that this can easily give you wrong values as ansible_host. See further below for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
|
||||
# Using the inventory to allow ansible to connect via the first IP address of the VM / Container
|
||||
# (Default is connection by name of QEMU/LXC guests)
|
||||
# Note: my_inv_var demonstrates how to add a string variable to every host used by the inventory.
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
url: http://pve.domain.com:8006
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
validate_certs: false
|
||||
want_facts: true
|
||||
want_proxmox_nodes_ansible_host: false
|
||||
compose:
|
||||
ansible_host: proxmox_ipconfig0.ip | default(proxmox_net0.ip) | ipaddr('address')
|
||||
my_inv_var_1: "'my_var1_value'"
|
||||
my_inv_var_2: >
|
||||
"my_var_2_value"
|
||||
|
||||
# Specify the url, user and password using templating
|
||||
# my.proxmox.yml
|
||||
plugin: community.general.proxmox
|
||||
url: "{{ lookup('ansible.builtin.ini', 'url', section='proxmox', file='file.ini') }}"
|
||||
user: "{{ lookup('ansible.builtin.env','PM_USER') | default('ansible@pve') }}"
|
||||
password: "{{ lookup('community.general.random_string', base64=True) }}"
|
||||
# Note that this can easily give you wrong values as ansible_host. See further up for
|
||||
# an example where this is set to `false` and where ansible_host is set with `compose`.
|
||||
want_proxmox_nodes_ansible_host: true
|
||||
|
||||
'''
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from ansible.module_utils.common._collections_compat import MutableMapping
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.utils.display import Display
|
||||
from ansible.template import Templar
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
@@ -135,6 +223,8 @@ try:
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
''' Host inventory parser for ansible using Proxmox as source. '''
|
||||
@@ -171,15 +261,24 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
def _get_auth(self):
|
||||
credentials = urlencode({'username': self.proxmox_user, 'password': self.proxmox_password, })
|
||||
|
||||
a = self._get_session()
|
||||
ret = a.post('%s/api2/json/access/ticket' % self.proxmox_url, data=credentials)
|
||||
if self.proxmox_password:
|
||||
|
||||
json = ret.json()
|
||||
credentials = urlencode({'username': self.proxmox_user, 'password': self.proxmox_password, })
|
||||
|
||||
self.credentials = {
|
||||
'ticket': json['data']['ticket'],
|
||||
'CSRFPreventionToken': json['data']['CSRFPreventionToken'],
|
||||
}
|
||||
a = self._get_session()
|
||||
ret = a.post('%s/api2/json/access/ticket' % self.proxmox_url, data=credentials)
|
||||
|
||||
json = ret.json()
|
||||
|
||||
self.headers = {
|
||||
# only required for POST/PUT/DELETE methods, which we are not using currently
|
||||
# 'CSRFPreventionToken': json['data']['CSRFPreventionToken'],
|
||||
'Cookie': 'PVEAuthCookie={0}'.format(json['data']['ticket'])
|
||||
}
|
||||
|
||||
else:
|
||||
|
||||
self.headers = {'Authorization': 'PVEAPIToken={0}!{1}={2}'.format(self.proxmox_user, self.proxmox_token_id, self.proxmox_token_secret)}
|
||||
|
||||
def _get_json(self, url, ignore_errors=None):
|
||||
|
||||
@@ -191,8 +290,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
data = []
|
||||
s = self._get_session()
|
||||
while True:
|
||||
headers = {'Cookie': 'PVEAuthCookie={0}'.format(self.credentials['ticket'])}
|
||||
ret = s.get(url, headers=headers)
|
||||
ret = s.get(url, headers=self.headers)
|
||||
if ignore_errors and ret.status_code in ignore_errors:
|
||||
break
|
||||
ret.raise_for_status()
|
||||
@@ -275,28 +373,19 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
return result
|
||||
|
||||
def _get_vm_config(self, node, vmid, vmtype, name):
|
||||
def _get_vm_config(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/config" % (self.proxmox_url, node, vmtype, vmid))
|
||||
|
||||
node_key = 'node'
|
||||
node_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), node_key.lower()))
|
||||
self.inventory.set_variable(name, node_key, node)
|
||||
|
||||
vmid_key = 'vmid'
|
||||
vmid_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), vmid_key.lower()))
|
||||
self.inventory.set_variable(name, vmid_key, vmid)
|
||||
|
||||
vmtype_key = 'vmtype'
|
||||
vmtype_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), vmtype_key.lower()))
|
||||
self.inventory.set_variable(name, vmtype_key, vmtype)
|
||||
properties[self._fact('node')] = node
|
||||
properties[self._fact('vmid')] = vmid
|
||||
properties[self._fact('vmtype')] = vmtype
|
||||
|
||||
plaintext_configs = [
|
||||
'tags',
|
||||
'description',
|
||||
]
|
||||
|
||||
for config in ret:
|
||||
key = config
|
||||
key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), key.lower()))
|
||||
key = self._fact(config)
|
||||
value = ret[config]
|
||||
try:
|
||||
# fixup disk images as they have no key
|
||||
@@ -305,46 +394,48 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
# Additional field containing parsed tags as list
|
||||
if config == 'tags':
|
||||
parsed_key = self.to_safe('%s%s' % (key, "_parsed"))
|
||||
parsed_value = [tag.strip() for tag in value.split(",")]
|
||||
self.inventory.set_variable(name, parsed_key, parsed_value)
|
||||
stripped_value = value.strip()
|
||||
if stripped_value:
|
||||
parsed_key = key + "_parsed"
|
||||
properties[parsed_key] = [tag.strip() for tag in stripped_value.split(",")]
|
||||
|
||||
# The first field in the agent string tells you whether the agent is enabled
|
||||
# the rest of the comma separated string is extra config for the agent
|
||||
if config == 'agent' and int(value.split(',')[0]):
|
||||
agent_iface_key = self.to_safe('%s%s' % (key, "_interfaces"))
|
||||
agent_iface_value = self._get_agent_network_interfaces(node, vmid, vmtype)
|
||||
if agent_iface_value:
|
||||
self.inventory.set_variable(name, agent_iface_key, agent_iface_value)
|
||||
agent_iface_key = self.to_safe('%s%s' % (key, "_interfaces"))
|
||||
properties[agent_iface_key] = agent_iface_value
|
||||
|
||||
if not (isinstance(value, int) or ',' not in value):
|
||||
if config == 'lxc':
|
||||
out_val = {}
|
||||
for k, v in value:
|
||||
if k.startswith('lxc.'):
|
||||
k = k[len('lxc.'):]
|
||||
out_val[k] = v
|
||||
value = out_val
|
||||
|
||||
if config not in plaintext_configs and isinstance(value, string_types) \
|
||||
and all("=" in v for v in value.split(",")):
|
||||
# split off strings with commas to a dict
|
||||
# skip over any keys that cannot be processed
|
||||
try:
|
||||
value = dict(key.split("=") for key in value.split(","))
|
||||
value = dict(key.split("=", 1) for key in value.split(","))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self.inventory.set_variable(name, key, value)
|
||||
properties[key] = value
|
||||
except NameError:
|
||||
return None
|
||||
|
||||
def _get_vm_status(self, node, vmid, vmtype, name):
|
||||
def _get_vm_status(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/status/current" % (self.proxmox_url, node, vmtype, vmid))
|
||||
properties[self._fact('status')] = ret['status']
|
||||
|
||||
status = ret['status']
|
||||
status_key = 'status'
|
||||
status_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), status_key.lower()))
|
||||
self.inventory.set_variable(name, status_key, status)
|
||||
|
||||
def _get_vm_snapshots(self, node, vmid, vmtype, name):
|
||||
def _get_vm_snapshots(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/snapshot" % (self.proxmox_url, node, vmtype, vmid))
|
||||
|
||||
snapshots_key = 'snapshots'
|
||||
snapshots_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), snapshots_key.lower()))
|
||||
|
||||
snapshots = [snapshot['name'] for snapshot in ret if snapshot['name'] != 'current']
|
||||
self.inventory.set_variable(name, snapshots_key, snapshots)
|
||||
properties[self._fact('snapshots')] = snapshots
|
||||
|
||||
def to_safe(self, word):
|
||||
'''Converts 'bad' characters in a string to underscores so they can be used as Ansible groups
|
||||
@@ -354,109 +445,140 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
regex = r"[^A-Za-z0-9\_]"
|
||||
return re.sub(regex, "_", word.replace(" ", ""))
|
||||
|
||||
def _apply_constructable(self, name, variables):
|
||||
strict = self.get_option('strict')
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=strict)
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=strict)
|
||||
self._set_composite_vars(self.get_option('compose'), variables, name, strict=strict)
|
||||
def _fact(self, name):
|
||||
'''Generate a fact's full name from the common prefix and a name.'''
|
||||
return self.to_safe('%s%s' % (self.facts_prefix, name.lower()))
|
||||
|
||||
def _group(self, name):
|
||||
'''Generate a group's full name from the common prefix and a name.'''
|
||||
return self.to_safe('%s%s' % (self.group_prefix, name.lower()))
|
||||
|
||||
def _can_add_host(self, name, properties):
|
||||
'''Ensure that a host satisfies all defined hosts filters. If strict mode is
|
||||
enabled, any error during host filter compositing will lead to an AnsibleError
|
||||
being raised, otherwise the filter will be ignored.
|
||||
'''
|
||||
for host_filter in self.host_filters:
|
||||
try:
|
||||
if not self._compose(host_filter, properties):
|
||||
return False
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
message = "Could not evaluate host filter %s for host %s - %s" % (host_filter, name, to_native(e))
|
||||
if self.strict:
|
||||
raise AnsibleError(message)
|
||||
display.warning(message)
|
||||
return True
|
||||
|
||||
def _add_host(self, name, variables):
|
||||
self.inventory.add_host(name)
|
||||
for k, v in variables.items():
|
||||
self.inventory.set_variable(name, k, v)
|
||||
variables = self.inventory.get_host(name).get_vars()
|
||||
self._set_composite_vars(self.get_option('compose'), variables, name, strict=self.strict)
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=self.strict)
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=self.strict)
|
||||
|
||||
def _handle_item(self, node, ittype, item):
|
||||
'''Handle an item from the list of LXC containers and Qemu VM. The
|
||||
return value will be either None if the item was skipped or the name of
|
||||
the item if it was added to the inventory.'''
|
||||
if item.get('template'):
|
||||
return None
|
||||
|
||||
properties = dict()
|
||||
name, vmid = item['name'], item['vmid']
|
||||
|
||||
# get status, config and snapshots if want_facts == True
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_status(properties, node, vmid, ittype, name)
|
||||
self._get_vm_config(properties, node, vmid, ittype, name)
|
||||
self._get_vm_snapshots(properties, node, vmid, ittype, name)
|
||||
|
||||
# ensure the host satisfies filters
|
||||
if not self._can_add_host(name, properties):
|
||||
return None
|
||||
|
||||
# add the host to the inventory
|
||||
self._add_host(name, properties)
|
||||
node_type_group = self._group('%s_%s' % (node, ittype))
|
||||
self.inventory.add_child(self._group('all_' + ittype), name)
|
||||
self.inventory.add_child(node_type_group, name)
|
||||
if item['status'] == 'stopped':
|
||||
self.inventory.add_child(self._group('all_stopped'), name)
|
||||
elif item['status'] == 'running':
|
||||
self.inventory.add_child(self._group('all_running'), name)
|
||||
|
||||
return name
|
||||
|
||||
def _populate_pool_groups(self, added_hosts):
|
||||
'''Generate groups from Proxmox resource pools, ignoring VMs and
|
||||
containers that were skipped.'''
|
||||
for pool in self._get_pools():
|
||||
poolid = pool.get('poolid')
|
||||
if not poolid:
|
||||
continue
|
||||
pool_group = self._group('pool_' + poolid)
|
||||
self.inventory.add_group(pool_group)
|
||||
|
||||
for member in self._get_members_per_pool(poolid):
|
||||
name = member.get('name')
|
||||
if name and name in added_hosts:
|
||||
self.inventory.add_child(pool_group, name)
|
||||
|
||||
def _populate(self):
|
||||
|
||||
self._get_auth()
|
||||
# create common groups
|
||||
self.inventory.add_group(self._group('all_lxc'))
|
||||
self.inventory.add_group(self._group('all_qemu'))
|
||||
self.inventory.add_group(self._group('all_running'))
|
||||
self.inventory.add_group(self._group('all_stopped'))
|
||||
nodes_group = self._group('nodes')
|
||||
self.inventory.add_group(nodes_group)
|
||||
|
||||
want_proxmox_nodes_ansible_host = self.get_option("want_proxmox_nodes_ansible_host")
|
||||
if want_proxmox_nodes_ansible_host is None:
|
||||
display.deprecated(
|
||||
'The want_proxmox_nodes_ansible_host option of the community.general.proxmox inventory plugin'
|
||||
' currently defaults to `true`, but this default has been deprecated and will change to `false`'
|
||||
' in community.general 6.0.0. To keep the current behavior and remove this deprecation warning,'
|
||||
' explicitly set `want_proxmox_nodes_ansible_host` to `true` in your inventory configuration',
|
||||
version='6.0.0', collection_name='community.general')
|
||||
want_proxmox_nodes_ansible_host = True
|
||||
|
||||
# gather vm's on nodes
|
||||
self._get_auth()
|
||||
hosts = []
|
||||
for node in self._get_nodes():
|
||||
# FIXME: this can probably be cleaner
|
||||
# create groups
|
||||
lxc_group = 'all_lxc'
|
||||
lxc_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), lxc_group.lower()))
|
||||
self.inventory.add_group(lxc_group)
|
||||
qemu_group = 'all_qemu'
|
||||
qemu_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), qemu_group.lower()))
|
||||
self.inventory.add_group(qemu_group)
|
||||
nodes_group = 'nodes'
|
||||
nodes_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), nodes_group.lower()))
|
||||
self.inventory.add_group(nodes_group)
|
||||
running_group = 'all_running'
|
||||
running_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), running_group.lower()))
|
||||
self.inventory.add_group(running_group)
|
||||
stopped_group = 'all_stopped'
|
||||
stopped_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), stopped_group.lower()))
|
||||
self.inventory.add_group(stopped_group)
|
||||
if not node.get('node'):
|
||||
continue
|
||||
|
||||
if node.get('node'):
|
||||
self.inventory.add_host(node['node'])
|
||||
self.inventory.add_host(node['node'])
|
||||
if node['type'] == 'node':
|
||||
self.inventory.add_child(nodes_group, node['node'])
|
||||
|
||||
if node['type'] == 'node':
|
||||
self.inventory.add_child(nodes_group, node['node'])
|
||||
if node['status'] == 'offline':
|
||||
continue
|
||||
|
||||
if node['status'] == 'offline':
|
||||
continue
|
||||
# get node IP address
|
||||
if want_proxmox_nodes_ansible_host:
|
||||
ip = self._get_node_ip(node['node'])
|
||||
self.inventory.set_variable(node['node'], 'ansible_host', ip)
|
||||
|
||||
# get node IP address
|
||||
if self.get_option("want_proxmox_nodes_ansible_host"):
|
||||
ip = self._get_node_ip(node['node'])
|
||||
self.inventory.set_variable(node['node'], 'ansible_host', ip)
|
||||
# add LXC/Qemu groups for the node
|
||||
for ittype in ('lxc', 'qemu'):
|
||||
node_type_group = self._group('%s_%s' % (node['node'], ittype))
|
||||
self.inventory.add_group(node_type_group)
|
||||
|
||||
# get LXC containers for this node
|
||||
node_lxc_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), ('%s_lxc' % node['node']).lower()))
|
||||
self.inventory.add_group(node_lxc_group)
|
||||
for lxc in self._get_lxc_per_node(node['node']):
|
||||
self.inventory.add_host(lxc['name'])
|
||||
self.inventory.add_child(lxc_group, lxc['name'])
|
||||
self.inventory.add_child(node_lxc_group, lxc['name'])
|
||||
|
||||
# get LXC status when want_facts == True
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_status(node['node'], lxc['vmid'], 'lxc', lxc['name'])
|
||||
if lxc['status'] == 'stopped':
|
||||
self.inventory.add_child(stopped_group, lxc['name'])
|
||||
elif lxc['status'] == 'running':
|
||||
self.inventory.add_child(running_group, lxc['name'])
|
||||
|
||||
# get LXC config and snapshots for facts
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_config(node['node'], lxc['vmid'], 'lxc', lxc['name'])
|
||||
self._get_vm_snapshots(node['node'], lxc['vmid'], 'lxc', lxc['name'])
|
||||
|
||||
self._apply_constructable(lxc["name"], self.inventory.get_host(lxc['name']).get_vars())
|
||||
|
||||
# get QEMU vm's for this node
|
||||
node_qemu_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), ('%s_qemu' % node['node']).lower()))
|
||||
self.inventory.add_group(node_qemu_group)
|
||||
for qemu in self._get_qemu_per_node(node['node']):
|
||||
if qemu.get('template'):
|
||||
continue
|
||||
|
||||
self.inventory.add_host(qemu['name'])
|
||||
self.inventory.add_child(qemu_group, qemu['name'])
|
||||
self.inventory.add_child(node_qemu_group, qemu['name'])
|
||||
|
||||
# get QEMU status
|
||||
self._get_vm_status(node['node'], qemu['vmid'], 'qemu', qemu['name'])
|
||||
if qemu['status'] == 'stopped':
|
||||
self.inventory.add_child(stopped_group, qemu['name'])
|
||||
elif qemu['status'] == 'running':
|
||||
self.inventory.add_child(running_group, qemu['name'])
|
||||
|
||||
# get QEMU config and snapshots for facts
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_config(node['node'], qemu['vmid'], 'qemu', qemu['name'])
|
||||
self._get_vm_snapshots(node['node'], qemu['vmid'], 'qemu', qemu['name'])
|
||||
|
||||
self._apply_constructable(qemu["name"], self.inventory.get_host(qemu['name']).get_vars())
|
||||
# get LXC containers and Qemu VMs for this node
|
||||
lxc_objects = zip(itertools.repeat('lxc'), self._get_lxc_per_node(node['node']))
|
||||
qemu_objects = zip(itertools.repeat('qemu'), self._get_qemu_per_node(node['node']))
|
||||
for ittype, item in itertools.chain(lxc_objects, qemu_objects):
|
||||
name = self._handle_item(node['node'], ittype, item)
|
||||
if name is not None:
|
||||
hosts.append(name)
|
||||
|
||||
# gather vm's in pools
|
||||
for pool in self._get_pools():
|
||||
if pool.get('poolid'):
|
||||
pool_group = 'pool_' + pool['poolid']
|
||||
pool_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), pool_group.lower()))
|
||||
self.inventory.add_group(pool_group)
|
||||
|
||||
for member in self._get_members_per_pool(pool['poolid']):
|
||||
if member.get('name'):
|
||||
if not member.get('template'):
|
||||
self.inventory.add_child(pool_group, member['name'])
|
||||
self._populate_pool_groups(hosts)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
if not HAS_REQUESTS:
|
||||
@@ -468,12 +590,43 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
# read config from file, this sets 'options'
|
||||
self._read_config_data(path)
|
||||
|
||||
# get connection host
|
||||
self.proxmox_url = self.get_option('url').rstrip('/')
|
||||
self.proxmox_user = self.get_option('user')
|
||||
self.proxmox_password = self.get_option('password')
|
||||
t = Templar(loader=loader)
|
||||
|
||||
# read options
|
||||
proxmox_url = self.get_option('url')
|
||||
if t.is_template(proxmox_url):
|
||||
proxmox_url = t.template(variable=proxmox_url, disable_lookups=False)
|
||||
self.proxmox_url = proxmox_url.rstrip('/')
|
||||
|
||||
proxmox_user = self.get_option('user')
|
||||
if t.is_template(proxmox_user):
|
||||
proxmox_user = t.template(variable=proxmox_user, disable_lookups=False)
|
||||
self.proxmox_user = proxmox_user
|
||||
|
||||
proxmox_password = self.get_option('password')
|
||||
if t.is_template(proxmox_password):
|
||||
proxmox_password = t.template(variable=proxmox_password, disable_lookups=False)
|
||||
self.proxmox_password = proxmox_password
|
||||
|
||||
proxmox_token_id = self.get_option('token_id')
|
||||
if t.is_template(proxmox_token_id):
|
||||
proxmox_token_id = t.template(variable=proxmox_token_id, disable_lookups=False)
|
||||
self.proxmox_token_id = proxmox_token_id
|
||||
|
||||
proxmox_token_secret = self.get_option('token_secret')
|
||||
if t.is_template(proxmox_token_secret):
|
||||
proxmox_token_secret = t.template(variable=proxmox_token_secret, disable_lookups=False)
|
||||
self.proxmox_token_secret = proxmox_token_secret
|
||||
|
||||
if proxmox_password is None and (proxmox_token_id is None or proxmox_token_secret is None):
|
||||
raise AnsibleError('You must specify either a password or both token_id and token_secret.')
|
||||
|
||||
self.cache_key = self.get_cache_key(path)
|
||||
self.use_cache = cache and self.get_option('cache')
|
||||
self.host_filters = self.get_option('filters')
|
||||
self.group_prefix = self.get_option('group_prefix')
|
||||
self.facts_prefix = self.get_option('facts_prefix')
|
||||
self.strict = self.get_option('strict')
|
||||
|
||||
# actually populate inventory
|
||||
self._populate()
|
||||
|
||||
@@ -23,6 +23,7 @@ DOCUMENTATION = r'''
|
||||
regions:
|
||||
description: Filter results on a specific Scaleway region.
|
||||
type: list
|
||||
elements: string
|
||||
default:
|
||||
- ams1
|
||||
- par1
|
||||
@@ -31,6 +32,13 @@ DOCUMENTATION = r'''
|
||||
tags:
|
||||
description: Filter results on a specific tag.
|
||||
type: list
|
||||
elements: string
|
||||
scw_profile:
|
||||
description:
|
||||
- The config profile to use in config file.
|
||||
- By default uses the one specified as C(active_profile) in the config file, or falls back to C(default) if that is not defined.
|
||||
type: string
|
||||
version_added: 4.4.0
|
||||
oauth_token:
|
||||
description:
|
||||
- Scaleway OAuth token.
|
||||
@@ -45,6 +53,7 @@ DOCUMENTATION = r'''
|
||||
hostnames:
|
||||
description: List of preference about what to use as an hostname.
|
||||
type: list
|
||||
elements: string
|
||||
default:
|
||||
- public_ipv4
|
||||
choices:
|
||||
@@ -303,7 +312,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
if not oauth_token and os.path.exists(scw_config_path):
|
||||
with open(scw_config_path) as fh:
|
||||
scw_config = yaml.safe_load(fh)
|
||||
active_profile = scw_config.get('active_profile', 'default')
|
||||
ansible_profile = self.get_option('scw_profile')
|
||||
|
||||
if ansible_profile:
|
||||
active_profile = ansible_profile
|
||||
else:
|
||||
active_profile = scw_config.get('active_profile', 'default')
|
||||
|
||||
if active_profile == 'default':
|
||||
oauth_token = scw_config.get('secret_key')
|
||||
else:
|
||||
|
||||
@@ -62,17 +62,16 @@ DOCUMENTATION = '''
|
||||
|
||||
EXAMPLES = '''
|
||||
# file must be named xen_orchestra.yaml or xen_orchestra.yml
|
||||
simple_config_file:
|
||||
plugin: community.general.xen_orchestra
|
||||
api_host: 192.168.1.255
|
||||
user: xo
|
||||
password: xo_pwd
|
||||
validate_certs: true
|
||||
use_ssl: true
|
||||
groups:
|
||||
kube_nodes: "'kube_node' in tags"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
plugin: community.general.xen_orchestra
|
||||
api_host: 192.168.1.255
|
||||
user: xo
|
||||
password: xo_pwd
|
||||
validate_certs: true
|
||||
use_ssl: true
|
||||
groups:
|
||||
kube_nodes: "'kube_node' in tags"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
|
||||
'''
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ DOCUMENTATION = '''
|
||||
_raw:
|
||||
description: List of key(s) to retrieve.
|
||||
type: list
|
||||
elements: string
|
||||
recurse:
|
||||
type: boolean
|
||||
description: If true, will retrieve all the values that have the given key as prefix.
|
||||
|
||||
@@ -17,11 +17,11 @@ DOCUMENTATION = '''
|
||||
_terms:
|
||||
description: term or list of terms to lookup in the credit store
|
||||
type: list
|
||||
required: True
|
||||
elements: string
|
||||
required: true
|
||||
table:
|
||||
description: name of the credstash table to query
|
||||
default: 'credential-store'
|
||||
required: True
|
||||
version:
|
||||
description: Credstash version
|
||||
region:
|
||||
|
||||
@@ -18,6 +18,7 @@ DOCUMENTATION = '''
|
||||
description: domain or list of domains to query TXT records from
|
||||
required: True
|
||||
type: list
|
||||
elements: string
|
||||
'''
|
||||
|
||||
EXAMPLES = """
|
||||
|
||||
@@ -105,11 +105,15 @@ display = Display()
|
||||
class LookupModule(LookupBase):
|
||||
@staticmethod
|
||||
def Client(vault_parameters):
|
||||
return SecretsVault(**vault_parameters)
|
||||
try:
|
||||
vault = SecretsVault(**vault_parameters)
|
||||
return vault
|
||||
except TypeError:
|
||||
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
if sdk_is_missing:
|
||||
raise AnsibleError("python-dsv-sdk must be installed to use this plugin")
|
||||
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
|
||||
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ DOCUMENTATION = '''
|
||||
description:
|
||||
- Enables Ansible to retrieve, create or update passwords from the passwordstore.org pass utility.
|
||||
It also retrieves YAML style keys stored as multilines in the passwordfile.
|
||||
- To avoid problems when accessing multiple secrets at once, add C(auto-expand-secmem) to
|
||||
C(~/.gnupg/gpg-agent.conf). Where this is not possible, consider using I(lock=readwrite) instead.
|
||||
options:
|
||||
_terms:
|
||||
description: query key.
|
||||
@@ -77,54 +79,89 @@ DOCUMENTATION = '''
|
||||
- warn
|
||||
- empty
|
||||
- create
|
||||
lock:
|
||||
description:
|
||||
- How to synchronize operations.
|
||||
- The default of C(write) only synchronizes write operations.
|
||||
- C(readwrite) synchronizes all operations (including read). This makes sure that gpg-agent is never called in parallel.
|
||||
- C(none) does not do any synchronization.
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: lock
|
||||
type: str
|
||||
default: write
|
||||
choices:
|
||||
- readwrite
|
||||
- write
|
||||
- none
|
||||
version_added: 4.5.0
|
||||
locktimeout:
|
||||
description:
|
||||
- Lock timeout applied when I(lock) is not C(none).
|
||||
- Time with a unit suffix, C(s), C(m), C(h) for seconds, minutes, and hours, respectively. For example, C(900s) equals C(15m).
|
||||
- Correlates with C(pinentry-timeout) in C(~/.gnupg/gpg-agent.conf), see C(man gpg-agent) for details.
|
||||
ini:
|
||||
- section: passwordstore_lookup
|
||||
key: locktimeout
|
||||
type: str
|
||||
default: 15m
|
||||
version_added: 4.5.0
|
||||
'''
|
||||
EXAMPLES = """
|
||||
# Debug is used for examples, BAD IDEA to show passwords on screen
|
||||
- name: Basic lookup. Fails if example/test doesn't exist
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test')}}"
|
||||
ansible.cfg: |
|
||||
[passwordstore_lookup]
|
||||
lock=readwrite
|
||||
locktimeout=45s
|
||||
|
||||
- name: Basic lookup. Warns if example/test does not exist and returns empty string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test missing=warn')}}"
|
||||
playbook.yml: |
|
||||
---
|
||||
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test create=true')}}"
|
||||
# Debug is used for examples, BAD IDEA to show passwords on screen
|
||||
- name: Basic lookup. Fails if example/test does not exist
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test')}}"
|
||||
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test missing=create')}}"
|
||||
- name: Basic lookup. Warns if example/test does not exist and returns empty string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test missing=warn')}}"
|
||||
|
||||
- name: Prints 'abc' if example/test does not exist, just give the password otherwise
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test missing=empty') | default('abc', true) }}"
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test create=true')}}"
|
||||
|
||||
- name: Different size password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true length=42')}}"
|
||||
- name: Create pass with random 16 character password. If password exists just give the password
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test missing=create')}}"
|
||||
|
||||
- name: Create password and overwrite the password if it exists. As a bonus, this module includes the old password inside the pass file
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true overwrite=true')}}"
|
||||
- name: Prints 'abc' if example/test does not exist, just give the password otherwise
|
||||
ansible.builtin.debug:
|
||||
var: mypassword
|
||||
vars:
|
||||
mypassword: "{{ lookup('community.general.passwordstore', 'example/test missing=empty') | default('abc', true) }}"
|
||||
|
||||
- name: Create an alphanumeric password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true nosymbols=true') }}"
|
||||
- name: Different size password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true length=42')}}"
|
||||
|
||||
- name: Return the value for user in the KV pair user, username
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test subkey=user')}}"
|
||||
- name: Create password and overwrite the password if it exists. As a bonus, this module includes the old password inside the pass file
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true overwrite=true')}}"
|
||||
|
||||
- name: Return the entire password file content
|
||||
ansible.builtin.set_fact:
|
||||
passfilecontent: "{{ lookup('community.general.passwordstore', 'example/test returnall=true')}}"
|
||||
- name: Create an alphanumeric password
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test create=true nosymbols=true') }}"
|
||||
|
||||
- name: Return the value for user in the KV pair user, username
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ lookup('community.general.passwordstore', 'example/test subkey=user')}}"
|
||||
|
||||
- name: Return the entire password file content
|
||||
ansible.builtin.set_fact:
|
||||
passfilecontent: "{{ lookup('community.general.passwordstore', 'example/test returnall=true')}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
@@ -135,13 +172,15 @@ _raw:
|
||||
elements: str
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import yaml
|
||||
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.module_utils.common.file import FileLock
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.utils.display import Display
|
||||
@@ -154,6 +193,7 @@ display = Display()
|
||||
|
||||
# backhacked check_output with input for python 2.7
|
||||
# http://stackoverflow.com/questions/10103551/passing-data-to-subprocess-check-output
|
||||
# note: contains special logic for calling 'pass', so not a drop-in replacement for check_output
|
||||
def check_output2(*popenargs, **kwargs):
|
||||
if 'stdout' in kwargs:
|
||||
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||
@@ -175,9 +215,10 @@ def check_output2(*popenargs, **kwargs):
|
||||
process.wait()
|
||||
raise
|
||||
retcode = process.poll()
|
||||
if retcode != 0 or \
|
||||
b'encryption failed: Unusable public key' in b_out or \
|
||||
b'encryption failed: Unusable public key' in b_err:
|
||||
if retcode == 0 and (b'encryption failed: Unusable public key' in b_out or
|
||||
b'encryption failed: Unusable public key' in b_err):
|
||||
retcode = 78 # os.EX_CONFIG
|
||||
if retcode != 0:
|
||||
cmd = kwargs.get("args")
|
||||
if cmd is None:
|
||||
cmd = popenargs[0]
|
||||
@@ -227,13 +268,13 @@ class LookupModule(LookupBase):
|
||||
|
||||
# Collect pass environment variables from the plugin's parameters.
|
||||
self.env = os.environ.copy()
|
||||
self.env['LANGUAGE'] = 'C' # make sure to get errors in English as required by check_output2
|
||||
|
||||
# Set PASSWORD_STORE_DIR if directory is set
|
||||
if self.paramvals['directory']:
|
||||
if os.path.isdir(self.paramvals['directory']):
|
||||
self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory']
|
||||
else:
|
||||
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
|
||||
# Set PASSWORD_STORE_DIR
|
||||
if os.path.isdir(self.paramvals['directory']):
|
||||
self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory']
|
||||
else:
|
||||
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
|
||||
|
||||
# Set PASSWORD_STORE_UMASK if umask is set
|
||||
if 'umask' in self.paramvals:
|
||||
@@ -261,19 +302,20 @@ class LookupModule(LookupBase):
|
||||
if ':' in line:
|
||||
name, value = line.split(':', 1)
|
||||
self.passdict[name.strip()] = value.strip()
|
||||
if os.path.isfile(os.path.join(self.paramvals['directory'], self.passname + ".gpg")):
|
||||
# Only accept password as found, if there a .gpg file for it (might be a tree node otherwise)
|
||||
return True
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
if e.returncode != 0 and 'not in the password store' in e.output:
|
||||
# if pass returns 1 and return string contains 'is not in the password store.'
|
||||
# We need to determine if this is valid or Error.
|
||||
if self.paramvals['missing'] == 'error':
|
||||
raise AnsibleError('passwordstore: passname {0} not found and missing=error is set'.format(self.passname))
|
||||
else:
|
||||
if self.paramvals['missing'] == 'warn':
|
||||
display.warning('passwordstore: passname {0} not found'.format(self.passname))
|
||||
return False
|
||||
else:
|
||||
# 'not in password store' is the expected error if a password wasn't found
|
||||
if 'not in the password store' not in e.output:
|
||||
raise AnsibleError(e)
|
||||
return True
|
||||
|
||||
if self.paramvals['missing'] == 'error':
|
||||
raise AnsibleError('passwordstore: passname {0} not found and missing=error is set'.format(self.passname))
|
||||
elif self.paramvals['missing'] == 'warn':
|
||||
display.warning('passwordstore: passname {0} not found'.format(self.passname))
|
||||
|
||||
return False
|
||||
|
||||
def get_newpass(self):
|
||||
if self.paramvals['nosymbols']:
|
||||
@@ -325,11 +367,30 @@ class LookupModule(LookupBase):
|
||||
else:
|
||||
return None
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
result = []
|
||||
@contextmanager
|
||||
def opt_lock(self, type):
|
||||
if self.get_option('lock') == type:
|
||||
tmpdir = os.environ.get('TMPDIR', '/tmp')
|
||||
lockfile = os.path.join(tmpdir, '.passwordstore.lock')
|
||||
with FileLock().lock_file(lockfile, tmpdir, self.lock_timeout):
|
||||
self.locked = type
|
||||
yield
|
||||
self.locked = None
|
||||
else:
|
||||
yield
|
||||
|
||||
def setup(self, variables):
|
||||
self.locked = None
|
||||
timeout = self.get_option('locktimeout')
|
||||
if not re.match('^[0-9]+[smh]$', timeout):
|
||||
raise AnsibleError("{0} is not a correct value for locktimeout".format(timeout))
|
||||
unit_to_seconds = {"s": 1, "m": 60, "h": 3600}
|
||||
self.lock_timeout = int(timeout[:-1]) * unit_to_seconds[timeout[-1]]
|
||||
self.paramvals = {
|
||||
'subkey': 'password',
|
||||
'directory': variables.get('passwordstore'),
|
||||
'directory': variables.get('passwordstore', os.environ.get(
|
||||
'PASSWORD_STORE_DIR',
|
||||
os.path.expanduser('~/.password-store'))),
|
||||
'create': False,
|
||||
'returnall': False,
|
||||
'overwrite': False,
|
||||
@@ -340,17 +401,27 @@ class LookupModule(LookupBase):
|
||||
'missing': 'error',
|
||||
}
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
self.setup(variables)
|
||||
result = []
|
||||
|
||||
for term in terms:
|
||||
self.parse_params(term) # parse the input into paramvals
|
||||
if self.check_pass(): # password exists
|
||||
if self.paramvals['overwrite'] and self.paramvals['subkey'] == 'password':
|
||||
result.append(self.update_password())
|
||||
else:
|
||||
result.append(self.get_passresult())
|
||||
else: # password does not exist
|
||||
if self.paramvals['missing'] == 'create':
|
||||
result.append(self.generate_password())
|
||||
else:
|
||||
result.append(None)
|
||||
with self.opt_lock('readwrite'):
|
||||
if self.check_pass(): # password exists
|
||||
if self.paramvals['overwrite'] and self.paramvals['subkey'] == 'password':
|
||||
with self.opt_lock('write'):
|
||||
result.append(self.update_password())
|
||||
else:
|
||||
result.append(self.get_passresult())
|
||||
else: # password does not exist
|
||||
if self.paramvals['missing'] == 'create':
|
||||
with self.opt_lock('write'):
|
||||
if self.locked == 'write' and self.check_pass(): # lookup password again if under write lock
|
||||
result.append(self.get_passresult())
|
||||
else:
|
||||
result.append(self.generate_password())
|
||||
else:
|
||||
result.append(None)
|
||||
|
||||
return result
|
||||
|
||||
@@ -14,6 +14,10 @@ description:
|
||||
- Uses the Thycotic Secret Server Python SDK to get Secrets from Secret
|
||||
Server using token authentication with I(username) and I(password) on
|
||||
the REST API at I(base_url).
|
||||
- When using self-signed certificates the environment variable
|
||||
C(REQUESTS_CA_BUNDLE) can be set to a file containing the trusted certificates
|
||||
(in C(.pem) format).
|
||||
- For example, C(export REQUESTS_CA_BUNDLE='/etc/ssl/certs/ca-bundle.trust.crt').
|
||||
requirements:
|
||||
- python-tss-sdk - https://pypi.org/project/python-tss-sdk/
|
||||
options:
|
||||
|
||||
291
plugins/module_utils/cmd_runner.py
Normal file
291
plugins/module_utils/cmd_runner.py
Normal file
@@ -0,0 +1,291 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# (c) 2022, Alexei Znamensky <russoz@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from ansible.module_utils.common.collections import is_sequence
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
|
||||
def _ensure_list(value):
|
||||
return list(value) if is_sequence(value) else [value]
|
||||
|
||||
|
||||
def _process_as_is(rc, out, err):
|
||||
return rc, out, err
|
||||
|
||||
|
||||
class CmdRunnerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingArgumentFormat(CmdRunnerException):
|
||||
def __init__(self, arg, args_order, args_formats):
|
||||
self.args_order = args_order
|
||||
self.arg = arg
|
||||
self.args_formats = args_formats
|
||||
|
||||
def __repr__(self):
|
||||
return "MissingArgumentFormat({0!r}, {1!r}, {2!r})".format(
|
||||
self.arg,
|
||||
self.args_order,
|
||||
self.args_formats,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Cannot find format for parameter {0} {1} in: {2}".format(
|
||||
self.arg,
|
||||
self.args_order,
|
||||
self.args_formats,
|
||||
)
|
||||
|
||||
|
||||
class MissingArgumentValue(CmdRunnerException):
|
||||
def __init__(self, args_order, arg):
|
||||
self.args_order = args_order
|
||||
self.arg = arg
|
||||
|
||||
def __repr__(self):
|
||||
return "MissingArgumentValue({0!r}, {1!r})".format(
|
||||
self.args_order,
|
||||
self.arg,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Cannot find value for parameter {0} in {1}".format(
|
||||
self.arg,
|
||||
self.args_order,
|
||||
)
|
||||
|
||||
|
||||
class FormatError(CmdRunnerException):
|
||||
def __init__(self, name, value, args_formats, exc):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.args_formats = args_formats
|
||||
self.exc = exc
|
||||
super(FormatError, self).__init__()
|
||||
|
||||
def __repr__(self):
|
||||
return "FormatError({0!r}, {1!r}, {2!r}, {3!r})".format(
|
||||
self.name,
|
||||
self.value,
|
||||
self.args_formats,
|
||||
self.exc,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Failed to format parameter {0} with value {1}: {2}".format(
|
||||
self.name,
|
||||
self.value,
|
||||
self.exc,
|
||||
)
|
||||
|
||||
|
||||
class _ArgFormat(object):
|
||||
def __init__(self, func, ignore_none=None):
|
||||
self.func = func
|
||||
self.ignore_none = ignore_none
|
||||
|
||||
def __call__(self, value, ctx_ignore_none):
|
||||
ignore_none = self.ignore_none if self.ignore_none is not None else ctx_ignore_none
|
||||
if value is None and ignore_none:
|
||||
return []
|
||||
f = self.func
|
||||
return [str(x) for x in f(value)]
|
||||
|
||||
|
||||
class _Format(object):
|
||||
@staticmethod
|
||||
def as_bool(args):
|
||||
return _ArgFormat(lambda value: _ensure_list(args) if value else [])
|
||||
|
||||
@staticmethod
|
||||
def as_bool_not(args):
|
||||
return _ArgFormat(lambda value: [] if value else _ensure_list(args), ignore_none=False)
|
||||
|
||||
@staticmethod
|
||||
def as_optval(arg, ignore_none=None):
|
||||
return _ArgFormat(lambda value: ["{0}{1}".format(arg, value)], ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_opt_val(arg, ignore_none=None):
|
||||
return _ArgFormat(lambda value: [arg, value], ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_opt_eq_val(arg, ignore_none=None):
|
||||
return _ArgFormat(lambda value: ["{0}={1}".format(arg, value)], ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_list(ignore_none=None):
|
||||
return _ArgFormat(_ensure_list, ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_fixed(args):
|
||||
return _ArgFormat(lambda value: _ensure_list(args), ignore_none=False)
|
||||
|
||||
@staticmethod
|
||||
def as_func(func, ignore_none=None):
|
||||
return _ArgFormat(func, ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_map(_map, default=None, ignore_none=None):
|
||||
return _ArgFormat(lambda value: _ensure_list(_map.get(value, default)), ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def as_default_type(_type, arg="", ignore_none=None):
|
||||
fmt = _Format
|
||||
if _type == "dict":
|
||||
return fmt.as_func(lambda d: ["--{0}={1}".format(*a) for a in iteritems(d)],
|
||||
ignore_none=ignore_none)
|
||||
if _type == "list":
|
||||
return fmt.as_func(lambda value: ["--{0}".format(x) for x in value], ignore_none=ignore_none)
|
||||
if _type == "bool":
|
||||
return fmt.as_bool("--{0}".format(arg))
|
||||
|
||||
return fmt.as_opt_val("--{0}".format(arg), ignore_none=ignore_none)
|
||||
|
||||
@staticmethod
|
||||
def unpack_args(func):
|
||||
@wraps(func)
|
||||
def wrapper(v):
|
||||
return func(*v)
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def unpack_kwargs(func):
|
||||
@wraps(func)
|
||||
def wrapper(v):
|
||||
return func(**v)
|
||||
return wrapper
|
||||
|
||||
|
||||
class CmdRunner(object):
|
||||
"""
|
||||
Wrapper for ``AnsibleModule.run_command()``.
|
||||
|
||||
It aims to provide a reusable runner with consistent argument formatting
|
||||
and sensible defaults.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _prepare_args_order(order):
|
||||
return tuple(order) if is_sequence(order) else tuple(order.split())
|
||||
|
||||
def __init__(self, module, command, arg_formats=None, default_args_order=(),
|
||||
check_rc=False, force_lang="C", path_prefix=None, environ_update=None):
|
||||
self.module = module
|
||||
self.command = _ensure_list(command)
|
||||
self.default_args_order = self._prepare_args_order(default_args_order)
|
||||
if arg_formats is None:
|
||||
arg_formats = {}
|
||||
self.arg_formats = dict(arg_formats)
|
||||
self.check_rc = check_rc
|
||||
self.force_lang = force_lang
|
||||
self.path_prefix = path_prefix
|
||||
if environ_update is None:
|
||||
environ_update = {}
|
||||
self.environ_update = environ_update
|
||||
|
||||
self.command[0] = module.get_bin_path(command[0], opt_dirs=path_prefix, required=True)
|
||||
|
||||
for mod_param_name, spec in iteritems(module.argument_spec):
|
||||
if mod_param_name not in self.arg_formats:
|
||||
self.arg_formats[mod_param_name] = _Format.as_default_type(spec['type'], mod_param_name)
|
||||
|
||||
def context(self, args_order=None, output_process=None, ignore_value_none=True, **kwargs):
|
||||
if output_process is None:
|
||||
output_process = _process_as_is
|
||||
if args_order is None:
|
||||
args_order = self.default_args_order
|
||||
args_order = self._prepare_args_order(args_order)
|
||||
for p in args_order:
|
||||
if p not in self.arg_formats:
|
||||
raise MissingArgumentFormat(p, args_order, tuple(self.arg_formats.keys()))
|
||||
return _CmdRunnerContext(runner=self,
|
||||
args_order=args_order,
|
||||
output_process=output_process,
|
||||
ignore_value_none=ignore_value_none, **kwargs)
|
||||
|
||||
def has_arg_format(self, arg):
|
||||
return arg in self.arg_formats
|
||||
|
||||
|
||||
class _CmdRunnerContext(object):
|
||||
def __init__(self, runner, args_order, output_process, ignore_value_none, **kwargs):
|
||||
self.runner = runner
|
||||
self.args_order = tuple(args_order)
|
||||
self.output_process = output_process
|
||||
self.ignore_value_none = ignore_value_none
|
||||
self.run_command_args = dict(kwargs)
|
||||
|
||||
self.environ_update = runner.environ_update
|
||||
self.environ_update.update(self.run_command_args.get('environ_update', {}))
|
||||
if runner.force_lang:
|
||||
self.environ_update.update({
|
||||
'LANGUAGE': runner.force_lang,
|
||||
'LC_ALL': runner.force_lang,
|
||||
})
|
||||
self.run_command_args['environ_update'] = self.environ_update
|
||||
|
||||
if 'check_rc' not in self.run_command_args:
|
||||
self.run_command_args['check_rc'] = runner.check_rc
|
||||
self.check_rc = self.run_command_args['check_rc']
|
||||
|
||||
self.cmd = None
|
||||
self.results_rc = None
|
||||
self.results_out = None
|
||||
self.results_err = None
|
||||
self.results_processed = None
|
||||
|
||||
def run(self, **kwargs):
|
||||
runner = self.runner
|
||||
module = self.runner.module
|
||||
self.cmd = list(runner.command)
|
||||
self.context_run_args = dict(kwargs)
|
||||
|
||||
named_args = dict(module.params)
|
||||
named_args.update(kwargs)
|
||||
for arg_name in self.args_order:
|
||||
value = None
|
||||
try:
|
||||
value = named_args[arg_name]
|
||||
self.cmd.extend(runner.arg_formats[arg_name](value, ctx_ignore_none=self.ignore_value_none))
|
||||
except KeyError:
|
||||
raise MissingArgumentValue(self.args_order, arg_name)
|
||||
except Exception as e:
|
||||
raise FormatError(arg_name, value, runner.arg_formats[arg_name], e)
|
||||
|
||||
results = module.run_command(self.cmd, **self.run_command_args)
|
||||
self.results_rc, self.results_out, self.results_err = results
|
||||
self.results_processed = self.output_process(*results)
|
||||
return self.results_processed
|
||||
|
||||
@property
|
||||
def run_info(self):
|
||||
return dict(
|
||||
ignore_value_none=self.ignore_value_none,
|
||||
check_rc=self.check_rc,
|
||||
environ_update=self.environ_update,
|
||||
args_order=self.args_order,
|
||||
cmd=self.cmd,
|
||||
run_command_args=self.run_command_args,
|
||||
context_run_args=self.context_run_args,
|
||||
results_rc=self.results_rc,
|
||||
results_out=self.results_out,
|
||||
results_err=self.results_err,
|
||||
results_processed=self.results_processed,
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
return False
|
||||
|
||||
|
||||
fmt = _Format()
|
||||
@@ -102,6 +102,7 @@ def keycloak_argument_spec():
|
||||
auth_username=dict(type='str', aliases=['username']),
|
||||
auth_password=dict(type='str', aliases=['password'], no_log=True),
|
||||
validate_certs=dict(type='bool', default=True),
|
||||
connection_timeout=dict(type='int', default=10),
|
||||
token=dict(type='str', no_log=True),
|
||||
)
|
||||
|
||||
@@ -134,6 +135,7 @@ def get_token(module_params):
|
||||
auth_username = module_params.get('auth_username')
|
||||
auth_password = module_params.get('auth_password')
|
||||
client_secret = module_params.get('auth_client_secret')
|
||||
connection_timeout = module_params.get('connection_timeout')
|
||||
auth_url = URL_TOKEN.format(url=base_url, realm=auth_realm)
|
||||
temp_payload = {
|
||||
'grant_type': 'password',
|
||||
@@ -147,7 +149,7 @@ def get_token(module_params):
|
||||
(k, v) for k, v in temp_payload.items() if v is not None)
|
||||
try:
|
||||
r = json.loads(to_native(open_url(auth_url, method='POST',
|
||||
validate_certs=validate_certs,
|
||||
validate_certs=validate_certs, timeout=connection_timeout,
|
||||
data=urlencode(payload)).read()))
|
||||
except ValueError as e:
|
||||
raise KeycloakError(
|
||||
@@ -229,6 +231,7 @@ class KeycloakAPI(object):
|
||||
self.module = module
|
||||
self.baseurl = self.module.params.get('auth_keycloak_url')
|
||||
self.validate_certs = self.module.params.get('validate_certs')
|
||||
self.connection_timeout = self.module.params.get('connection_timeout')
|
||||
self.restheaders = connection_header
|
||||
|
||||
def get_realm_info_by_id(self, realm='master'):
|
||||
@@ -240,7 +243,7 @@ class KeycloakAPI(object):
|
||||
realm_info_url = URL_REALM_INFO.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(realm_info_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(realm_info_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -265,7 +268,7 @@ class KeycloakAPI(object):
|
||||
realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(realm_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(realm_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -290,7 +293,7 @@ class KeycloakAPI(object):
|
||||
realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(realm_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(realm_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(realmrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update realm %s: %s' % (realm, str(e)),
|
||||
@@ -304,7 +307,7 @@ class KeycloakAPI(object):
|
||||
realm_url = URL_REALMS.format(url=self.baseurl)
|
||||
|
||||
try:
|
||||
return open_url(realm_url, method='POST', headers=self.restheaders,
|
||||
return open_url(realm_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(realmrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create realm %s: %s' % (realmrep['id'], str(e)),
|
||||
@@ -319,7 +322,7 @@ class KeycloakAPI(object):
|
||||
realm_url = URL_REALM.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(realm_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(realm_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete realm %s: %s' % (realm, str(e)),
|
||||
@@ -337,7 +340,7 @@ class KeycloakAPI(object):
|
||||
clientlist_url += '?clientId=%s' % filter
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientlist_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(clientlist_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of clients for realm %s: %s'
|
||||
@@ -368,7 +371,7 @@ class KeycloakAPI(object):
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(client_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(client_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -407,7 +410,7 @@ class KeycloakAPI(object):
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(client_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clientrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update client %s in realm %s: %s'
|
||||
@@ -422,7 +425,7 @@ class KeycloakAPI(object):
|
||||
client_url = URL_CLIENTS.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='POST', headers=self.restheaders,
|
||||
return open_url(client_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clientrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create client %s in realm %s: %s'
|
||||
@@ -438,7 +441,7 @@ class KeycloakAPI(object):
|
||||
client_url = URL_CLIENT.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(client_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(client_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete client %s in realm %s: %s'
|
||||
@@ -453,7 +456,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
client_roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(client_roles_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(client_roles_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch rolemappings for client %s in realm %s: %s"
|
||||
@@ -485,7 +488,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
|
||||
try:
|
||||
rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", headers=self.restheaders,
|
||||
rolemappings = json.loads(to_native(open_url(rolemappings_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
for role in rolemappings:
|
||||
if rid == role['id']:
|
||||
@@ -505,7 +508,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_AVAILABLE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
|
||||
@@ -521,7 +524,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS_COMPOSITE.format(url=self.baseurl, realm=realm, id=gid, client=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(available_rolemappings_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
|
||||
@@ -538,7 +541,8 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
|
||||
try:
|
||||
open_url(available_rolemappings_url, method="POST", headers=self.restheaders, data=json.dumps(role_rep), validate_certs=self.validate_certs)
|
||||
open_url(available_rolemappings_url, method="POST", headers=self.restheaders, data=json.dumps(role_rep),
|
||||
validate_certs=self.validate_certs, timeout=self.connection_timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch available rolemappings for client %s in group %s, realm %s: %s"
|
||||
% (cid, gid, realm, str(e)))
|
||||
@@ -554,7 +558,8 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
available_rolemappings_url = URL_CLIENT_ROLEMAPPINGS.format(url=self.baseurl, realm=realm, id=gid, client=cid)
|
||||
try:
|
||||
open_url(available_rolemappings_url, method="DELETE", headers=self.restheaders, validate_certs=self.validate_certs)
|
||||
open_url(available_rolemappings_url, method="DELETE", headers=self.restheaders,
|
||||
validate_certs=self.validate_certs, timeout=self.connection_timeout)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not delete available rolemappings for client %s in group %s, realm %s: %s"
|
||||
% (cid, gid, realm, str(e)))
|
||||
@@ -568,7 +573,7 @@ class KeycloakAPI(object):
|
||||
url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of client templates for realm %s: %s'
|
||||
@@ -587,7 +592,7 @@ class KeycloakAPI(object):
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, id=id, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain client templates %s for realm %s: %s'
|
||||
@@ -633,7 +638,7 @@ class KeycloakAPI(object):
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(url, method='PUT', headers=self.restheaders,
|
||||
return open_url(url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clienttrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update client template %s in realm %s: %s'
|
||||
@@ -648,7 +653,7 @@ class KeycloakAPI(object):
|
||||
url = URL_CLIENTTEMPLATES.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return open_url(url, method='POST', headers=self.restheaders,
|
||||
return open_url(url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clienttrep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create client template %s in realm %s: %s'
|
||||
@@ -664,7 +669,7 @@ class KeycloakAPI(object):
|
||||
url = URL_CLIENTTEMPLATE.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete client template %s in realm %s: %s'
|
||||
@@ -681,7 +686,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientscopes_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(clientscopes_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of clientscopes in realm %s: %s"
|
||||
@@ -698,7 +703,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(clientscope_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(clientscope_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -743,7 +748,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
clientscopes_url = URL_CLIENTSCOPES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(clientscopes_url, method='POST', headers=self.restheaders,
|
||||
return open_url(clientscopes_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create clientscope %s in realm %s: %s"
|
||||
@@ -758,7 +763,7 @@ class KeycloakAPI(object):
|
||||
clientscope_url = URL_CLIENTSCOPE.format(url=self.baseurl, realm=realm, id=clientscoperep['id'])
|
||||
|
||||
try:
|
||||
return open_url(clientscope_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(clientscope_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(clientscoperep), validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
@@ -796,7 +801,7 @@ class KeycloakAPI(object):
|
||||
# should have a good cid by here.
|
||||
clientscope_url = URL_CLIENTSCOPE.format(realm=realm, id=cid, url=self.baseurl)
|
||||
try:
|
||||
return open_url(clientscope_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(clientscope_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
@@ -814,7 +819,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(id=cid, url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(protocolmappers_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(protocolmappers_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of protocolmappers in realm %s: %s"
|
||||
@@ -833,7 +838,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=pid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(protocolmapper_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(protocolmapper_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -880,7 +885,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
protocolmappers_url = URL_CLIENTSCOPE_PROTOCOLMAPPERS.format(url=self.baseurl, id=cid, realm=realm)
|
||||
try:
|
||||
return open_url(protocolmappers_url, method='POST', headers=self.restheaders,
|
||||
return open_url(protocolmappers_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create protocolmapper %s in realm %s: %s"
|
||||
@@ -896,7 +901,7 @@ class KeycloakAPI(object):
|
||||
protocolmapper_url = URL_CLIENTSCOPE_PROTOCOLMAPPER.format(url=self.baseurl, realm=realm, id=cid, mapper_id=mapper_rep['id'])
|
||||
|
||||
try:
|
||||
return open_url(protocolmapper_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(protocolmapper_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(mapper_rep), validate_certs=self.validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
@@ -913,7 +918,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not fetch list of groups in realm %s: %s"
|
||||
@@ -930,7 +935,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
groups_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=gid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(groups_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
@@ -976,7 +981,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
groups_url = URL_GROUPS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(groups_url, method='POST', headers=self.restheaders,
|
||||
return open_url(groups_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(grouprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Could not create group %s in realm %s: %s"
|
||||
@@ -991,7 +996,7 @@ class KeycloakAPI(object):
|
||||
group_url = URL_GROUP.format(url=self.baseurl, realm=realm, groupid=grouprep['id'])
|
||||
|
||||
try:
|
||||
return open_url(group_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(group_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(grouprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update group %s in realm %s: %s'
|
||||
@@ -1028,7 +1033,7 @@ class KeycloakAPI(object):
|
||||
# should have a good groupid by here.
|
||||
group_url = URL_GROUP.format(realm=realm, groupid=groupid, url=self.baseurl)
|
||||
try:
|
||||
return open_url(group_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(group_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to delete group %s: %s" % (groupid, str(e)))
|
||||
@@ -1041,7 +1046,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
rolelist_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for realm %s: %s'
|
||||
@@ -1059,7 +1064,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name))
|
||||
try:
|
||||
return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
@@ -1079,7 +1084,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
roles_url = URL_REALM_ROLES.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(roles_url, method='POST', headers=self.restheaders,
|
||||
return open_url(roles_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(rolerep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create role %s in realm %s: %s'
|
||||
@@ -1093,7 +1098,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(rolerep['name']))
|
||||
try:
|
||||
return open_url(role_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(role_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(rolerep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update role %s in realm %s: %s'
|
||||
@@ -1107,7 +1112,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
role_url = URL_REALM_ROLE.format(url=self.baseurl, realm=realm, name=quote(name))
|
||||
try:
|
||||
return open_url(role_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(role_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete role %s in realm %s: %s'
|
||||
@@ -1126,7 +1131,7 @@ class KeycloakAPI(object):
|
||||
% (clientid, realm))
|
||||
rolelist_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(rolelist_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of roles for client %s in realm %s: %s'
|
||||
@@ -1150,7 +1155,7 @@ class KeycloakAPI(object):
|
||||
% (clientid, realm))
|
||||
role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name))
|
||||
try:
|
||||
return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(role_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
@@ -1176,7 +1181,7 @@ class KeycloakAPI(object):
|
||||
% (clientid, realm))
|
||||
roles_url = URL_CLIENT_ROLES.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return open_url(roles_url, method='POST', headers=self.restheaders,
|
||||
return open_url(roles_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(rolerep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create role %s for client %s in realm %s: %s'
|
||||
@@ -1196,7 +1201,7 @@ class KeycloakAPI(object):
|
||||
% (clientid, realm))
|
||||
role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(rolerep['name']))
|
||||
try:
|
||||
return open_url(role_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(role_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(rolerep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update role %s for client %s in realm %s: %s'
|
||||
@@ -1215,7 +1220,7 @@ class KeycloakAPI(object):
|
||||
% (clientid, realm))
|
||||
role_url = URL_CLIENT_ROLE.format(url=self.baseurl, realm=realm, id=cid, name=quote(name))
|
||||
try:
|
||||
return open_url(role_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(role_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete role %s for client %s in realm %s: %s'
|
||||
@@ -1231,7 +1236,8 @@ class KeycloakAPI(object):
|
||||
try:
|
||||
authentication_flow = {}
|
||||
# Check if the authentication flow exists on the Keycloak serveraders
|
||||
authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET', headers=self.restheaders))
|
||||
authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET',
|
||||
headers=self.restheaders, timeout=self.connection_timeout, validate_certs=self.validate_certs))
|
||||
for authentication in authentications:
|
||||
if authentication["alias"] == alias:
|
||||
authentication_flow = authentication
|
||||
@@ -1250,7 +1256,7 @@ class KeycloakAPI(object):
|
||||
flow_url = URL_AUTHENTICATION_FLOW.format(url=self.baseurl, realm=realm, id=id)
|
||||
|
||||
try:
|
||||
return open_url(flow_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(flow_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not delete authentication flow %s in realm %s: %s'
|
||||
@@ -1274,13 +1280,17 @@ class KeycloakAPI(object):
|
||||
copyfrom=quote(config["copyFrom"])),
|
||||
method='POST',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(new_name))
|
||||
data=json.dumps(new_name),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
flow_list = json.load(
|
||||
open_url(
|
||||
URL_AUTHENTICATION_FLOWS.format(url=self.baseurl,
|
||||
realm=realm),
|
||||
method='GET',
|
||||
headers=self.restheaders))
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs))
|
||||
for flow in flow_list:
|
||||
if flow["alias"] == config["alias"]:
|
||||
return flow
|
||||
@@ -1309,14 +1319,18 @@ class KeycloakAPI(object):
|
||||
realm=realm),
|
||||
method='POST',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(new_flow))
|
||||
data=json.dumps(new_flow),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
flow_list = json.load(
|
||||
open_url(
|
||||
URL_AUTHENTICATION_FLOWS.format(
|
||||
url=self.baseurl,
|
||||
realm=realm),
|
||||
method='GET',
|
||||
headers=self.restheaders))
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs))
|
||||
for flow in flow_list:
|
||||
if flow["alias"] == config["alias"]:
|
||||
return flow
|
||||
@@ -1340,7 +1354,9 @@ class KeycloakAPI(object):
|
||||
flowalias=quote(flowAlias)),
|
||||
method='PUT',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(updatedExec))
|
||||
data=json.dumps(updatedExec),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to update executions %s: %s" % (updatedExec, str(e)))
|
||||
|
||||
@@ -1359,7 +1375,9 @@ class KeycloakAPI(object):
|
||||
id=executionId),
|
||||
method='POST',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(authenticationConfig))
|
||||
data=json.dumps(authenticationConfig),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e)))
|
||||
|
||||
@@ -1382,7 +1400,9 @@ class KeycloakAPI(object):
|
||||
flowalias=quote(flowAlias)),
|
||||
method='POST',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(newSubFlow))
|
||||
data=json.dumps(newSubFlow),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to create new subflow %s: %s" % (subflowName, str(e)))
|
||||
|
||||
@@ -1404,7 +1424,9 @@ class KeycloakAPI(object):
|
||||
flowalias=quote(flowAlias)),
|
||||
method='POST',
|
||||
headers=self.restheaders,
|
||||
data=json.dumps(newExec))
|
||||
data=json.dumps(newExec),
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to create new execution %s: %s" % (execution["provider"], str(e)))
|
||||
|
||||
@@ -1425,7 +1447,9 @@ class KeycloakAPI(object):
|
||||
realm=realm,
|
||||
id=executionId),
|
||||
method='POST',
|
||||
headers=self.restheaders)
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
elif diff < 0:
|
||||
for i in range(-diff):
|
||||
open_url(
|
||||
@@ -1434,7 +1458,9 @@ class KeycloakAPI(object):
|
||||
realm=realm,
|
||||
id=executionId),
|
||||
method='POST',
|
||||
headers=self.restheaders)
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg="Unable to change execution priority %s: %s" % (executionId, str(e)))
|
||||
|
||||
@@ -1454,7 +1480,9 @@ class KeycloakAPI(object):
|
||||
realm=realm,
|
||||
flowalias=quote(config["alias"])),
|
||||
method='GET',
|
||||
headers=self.restheaders))
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs))
|
||||
for execution in executions:
|
||||
if "authenticationConfig" in execution:
|
||||
execConfigId = execution["authenticationConfig"]
|
||||
@@ -1465,7 +1493,9 @@ class KeycloakAPI(object):
|
||||
realm=realm,
|
||||
id=execConfigId),
|
||||
method='GET',
|
||||
headers=self.restheaders))
|
||||
headers=self.restheaders,
|
||||
timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs))
|
||||
execution["authenticationConfig"] = execConfig
|
||||
return executions
|
||||
except Exception as e:
|
||||
@@ -1479,7 +1509,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return json.loads(to_native(open_url(idps_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(idps_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of identity providers for realm %s: %s'
|
||||
@@ -1496,7 +1526,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias)
|
||||
try:
|
||||
return json.loads(to_native(open_url(idp_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(idp_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
@@ -1516,7 +1546,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
idps_url = URL_IDENTITY_PROVIDERS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
return open_url(idps_url, method='POST', headers=self.restheaders,
|
||||
return open_url(idps_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(idprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create identity provider %s in realm %s: %s'
|
||||
@@ -1530,7 +1560,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=idprep['alias'])
|
||||
try:
|
||||
return open_url(idp_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(idp_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(idprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update identity provider %s in realm %s: %s'
|
||||
@@ -1543,7 +1573,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
idp_url = URL_IDENTITY_PROVIDER.format(url=self.baseurl, realm=realm, alias=alias)
|
||||
try:
|
||||
return open_url(idp_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(idp_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete identity provider %s in realm %s: %s'
|
||||
@@ -1557,7 +1587,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias)
|
||||
try:
|
||||
return json.loads(to_native(open_url(mappers_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(mappers_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of identity provider mappers for idp %s in realm %s: %s'
|
||||
@@ -1575,7 +1605,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(mapper_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(mapper_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
@@ -1596,7 +1626,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
mappers_url = URL_IDENTITY_PROVIDER_MAPPERS.format(url=self.baseurl, realm=realm, alias=alias)
|
||||
try:
|
||||
return open_url(mappers_url, method='POST', headers=self.restheaders,
|
||||
return open_url(mappers_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(mapper), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create identity provider mapper %s for idp %s in realm %s: %s'
|
||||
@@ -1611,7 +1641,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mapper['id'])
|
||||
try:
|
||||
return open_url(mapper_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(mapper_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(mapper), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update mapper %s for identity provider %s in realm %s: %s'
|
||||
@@ -1625,7 +1655,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
mapper_url = URL_IDENTITY_PROVIDER_MAPPER.format(url=self.baseurl, realm=realm, alias=alias, id=mid)
|
||||
try:
|
||||
return open_url(mapper_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(mapper_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete mapper %s for identity provider %s in realm %s: %s'
|
||||
@@ -1642,7 +1672,7 @@ class KeycloakAPI(object):
|
||||
comps_url += '?%s' % filter
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(comps_url, method='GET', headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(comps_url, method='GET', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain list of components for realm %s: %s'
|
||||
@@ -1659,7 +1689,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
@@ -1679,13 +1709,13 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
comps_url = URL_COMPONENTS.format(url=self.baseurl, realm=realm)
|
||||
try:
|
||||
resp = open_url(comps_url, method='POST', headers=self.restheaders,
|
||||
resp = open_url(comps_url, method='POST', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(comprep), validate_certs=self.validate_certs)
|
||||
comp_url = resp.getheader('Location')
|
||||
if comp_url is None:
|
||||
self.module.fail_json(msg='Could not create component in realm %s: %s'
|
||||
% (realm, 'unexpected response'))
|
||||
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders,
|
||||
return json.loads(to_native(open_url(comp_url, method="GET", headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not create component in realm %s: %s'
|
||||
@@ -1702,7 +1732,7 @@ class KeycloakAPI(object):
|
||||
self.module.fail_json(msg='Cannot update component without id')
|
||||
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return open_url(comp_url, method='PUT', headers=self.restheaders,
|
||||
return open_url(comp_url, method='PUT', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
data=json.dumps(comprep), validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not update component %s in realm %s: %s'
|
||||
@@ -1715,7 +1745,7 @@ class KeycloakAPI(object):
|
||||
"""
|
||||
comp_url = URL_COMPONENT.format(url=self.baseurl, realm=realm, id=cid)
|
||||
try:
|
||||
return open_url(comp_url, method='DELETE', headers=self.restheaders,
|
||||
return open_url(comp_url, method='DELETE', headers=self.restheaders, timeout=self.connection_timeout,
|
||||
validate_certs=self.validate_certs)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Unable to delete component %s in realm %s: %s'
|
||||
|
||||
@@ -75,11 +75,14 @@ class LXDClient(object):
|
||||
else:
|
||||
raise LXDClientException('URL scheme must be unix: or https:')
|
||||
|
||||
def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None):
|
||||
def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None, wait_for_container=None):
|
||||
resp_json = self._send_request(method, url, body_json=body_json, ok_error_codes=ok_error_codes, timeout=timeout)
|
||||
if resp_json['type'] == 'async':
|
||||
url = '{0}/wait'.format(resp_json['operation'])
|
||||
resp_json = self._send_request('GET', url)
|
||||
if wait_for_container:
|
||||
while resp_json['metadata']['status'] == 'Running':
|
||||
resp_json = self._send_request('GET', url)
|
||||
if resp_json['metadata']['status'] != 'Success':
|
||||
self._raise_err_from_json(resp_json)
|
||||
return resp_json
|
||||
|
||||
@@ -337,7 +337,6 @@ def pritunl_auth_request(
|
||||
|
||||
auth_string = "&".join(
|
||||
[api_token, auth_timestamp, auth_nonce, method.upper(), path]
|
||||
+ ([data] if data else [])
|
||||
)
|
||||
|
||||
auth_signature = base64.b64encode(
|
||||
|
||||
@@ -21,6 +21,8 @@ except ImportError:
|
||||
|
||||
|
||||
from ansible.module_utils.basic import env_fallback, missing_required_lib
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
|
||||
def proxmox_auth_argument_spec():
|
||||
@@ -68,6 +70,9 @@ def ansible_to_proxmox_bool(value):
|
||||
class ProxmoxAnsible(object):
|
||||
"""Base class for Proxmox modules"""
|
||||
def __init__(self, module):
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
self.module = module
|
||||
self.proxmox_api = self._connect()
|
||||
# Test token validity
|
||||
@@ -95,3 +100,46 @@ class ProxmoxAnsible(object):
|
||||
return ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='%s' % e, exception=traceback.format_exc())
|
||||
|
||||
def version(self):
|
||||
apireturn = self.proxmox_api.version.get()
|
||||
return LooseVersion(apireturn['version'])
|
||||
|
||||
def get_node(self, node):
|
||||
nodes = [n for n in self.proxmox_api.nodes.get() if n['node'] == node]
|
||||
return nodes[0] if nodes else None
|
||||
|
||||
def get_nextvmid(self):
|
||||
vmid = self.proxmox_api.cluster.nextid.get()
|
||||
return vmid
|
||||
|
||||
def get_vmid(self, name, ignore_missing=False, choose_first_if_multiple=False):
|
||||
vms = [vm['vmid'] for vm in self.proxmox_api.cluster.resources.get(type='vm') if vm.get('name') == name]
|
||||
|
||||
if not vms:
|
||||
if ignore_missing:
|
||||
return None
|
||||
|
||||
self.module.fail_json(msg='No VM with name %s found' % name)
|
||||
elif len(vms) > 1:
|
||||
if choose_first_if_multiple:
|
||||
self.module.deprecate(
|
||||
'Multiple VMs with name %s found, choosing the first one. ' % name +
|
||||
'This will be an error in the future. To ensure the correct VM is used, ' +
|
||||
'also pass the vmid parameter.',
|
||||
version='5.0.0', collection_name='community.general')
|
||||
else:
|
||||
self.module.fail_json(msg='Multiple VMs with name %s found, provide vmid instead' % name)
|
||||
|
||||
return vms[0]
|
||||
|
||||
def get_vm(self, vmid, ignore_missing=False):
|
||||
vms = [vm for vm in self.proxmox_api.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid)]
|
||||
|
||||
if vms:
|
||||
return vms[0]
|
||||
else:
|
||||
if ignore_missing:
|
||||
return None
|
||||
|
||||
self.module.fail_json(msg='VM with vmid %s does not exist in cluster' % vmid)
|
||||
|
||||
@@ -732,14 +732,22 @@ class RedfishUtils(object):
|
||||
def get_multi_volume_inventory(self):
|
||||
return self.aggregate_systems(self.get_volume_inventory)
|
||||
|
||||
def manage_indicator_led(self, command):
|
||||
def manage_system_indicator_led(self, command):
|
||||
return self.manage_indicator_led(command, self.systems_uri)
|
||||
|
||||
def manage_chassis_indicator_led(self, command):
|
||||
return self.manage_indicator_led(command, self.chassis_uri)
|
||||
|
||||
def manage_indicator_led(self, command, resource_uri=None):
|
||||
result = {}
|
||||
key = 'IndicatorLED'
|
||||
if resource_uri is None:
|
||||
resource_uri = self.chassis_uri
|
||||
|
||||
payloads = {'IndicatorLedOn': 'Lit', 'IndicatorLedOff': 'Off', "IndicatorLedBlink": 'Blinking'}
|
||||
|
||||
result = {}
|
||||
response = self.get_request(self.root_uri + self.chassis_uri)
|
||||
response = self.get_request(self.root_uri + resource_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
result['ret'] = True
|
||||
@@ -749,7 +757,7 @@ class RedfishUtils(object):
|
||||
|
||||
if command in payloads.keys():
|
||||
payload = {'IndicatorLED': payloads[command]}
|
||||
response = self.patch_request(self.root_uri + self.chassis_uri, payload)
|
||||
response = self.patch_request(self.root_uri + resource_uri, payload)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
else:
|
||||
|
||||
@@ -27,20 +27,20 @@ except ImportError:
|
||||
HAS_CERTIFI_PACKAGE = False
|
||||
|
||||
|
||||
def fail_imports(module):
|
||||
def fail_imports(module, needs_certifi=True):
|
||||
errors = []
|
||||
traceback = []
|
||||
if not HAS_REDIS_PACKAGE:
|
||||
errors.append(missing_required_lib('redis'))
|
||||
traceback.append(REDIS_IMP_ERR)
|
||||
if not HAS_CERTIFI_PACKAGE:
|
||||
if not HAS_CERTIFI_PACKAGE and needs_certifi:
|
||||
errors.append(missing_required_lib('certifi'))
|
||||
traceback.append(CERTIFI_IMPORT_ERROR)
|
||||
if errors:
|
||||
module.fail_json(errors=errors, traceback='\n'.join(traceback))
|
||||
|
||||
|
||||
def redis_auth_argument_spec():
|
||||
def redis_auth_argument_spec(tls_default=True):
|
||||
return dict(
|
||||
login_host=dict(type='str',
|
||||
default='localhost',),
|
||||
@@ -50,7 +50,7 @@ def redis_auth_argument_spec():
|
||||
),
|
||||
login_port=dict(type='int', default=6379),
|
||||
tls=dict(type='bool',
|
||||
default=True),
|
||||
default=tls_default),
|
||||
validate_certs=dict(type='bool',
|
||||
default=True
|
||||
),
|
||||
@@ -58,6 +58,30 @@ def redis_auth_argument_spec():
|
||||
)
|
||||
|
||||
|
||||
def redis_auth_params(module):
|
||||
login_host = module.params['login_host']
|
||||
login_user = module.params['login_user']
|
||||
login_password = module.params['login_password']
|
||||
login_port = module.params['login_port']
|
||||
tls = module.params['tls']
|
||||
validate_certs = 'required' if module.params['validate_certs'] else None
|
||||
ca_certs = module.params['ca_certs']
|
||||
if tls and ca_certs is None:
|
||||
ca_certs = str(certifi.where())
|
||||
if tuple(map(int, redis_version.split('.'))) < (3, 4, 0) and login_user is not None:
|
||||
module.fail_json(
|
||||
msg='The option `username` in only supported with redis >= 3.4.0.')
|
||||
params = {'host': login_host,
|
||||
'port': login_port,
|
||||
'password': login_password,
|
||||
'ssl_ca_certs': ca_certs,
|
||||
'ssl_cert_reqs': validate_certs,
|
||||
'ssl': tls}
|
||||
if login_user is not None:
|
||||
params['username'] = login_user
|
||||
return params
|
||||
|
||||
|
||||
class RedisAnsible(object):
|
||||
'''Base class for Redis module'''
|
||||
|
||||
@@ -66,28 +90,8 @@ class RedisAnsible(object):
|
||||
self.connection = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
login_host = self.module.params['login_host']
|
||||
login_user = self.module.params['login_user']
|
||||
login_password = self.module.params['login_password']
|
||||
login_port = self.module.params['login_port']
|
||||
tls = self.module.params['tls']
|
||||
validate_certs = 'required' if self.module.params['validate_certs'] else None
|
||||
ca_certs = self.module.params['ca_certs']
|
||||
if tls and ca_certs is None:
|
||||
ca_certs = str(certifi.where())
|
||||
if tuple(map(int, redis_version.split('.'))) < (3, 4, 0) and login_user is not None:
|
||||
self.module.fail_json(
|
||||
msg='The option `username` in only supported with redis >= 3.4.0.')
|
||||
params = {'host': login_host,
|
||||
'port': login_port,
|
||||
'password': login_password,
|
||||
'ssl_ca_certs': ca_certs,
|
||||
'ssl_cert_reqs': validate_certs,
|
||||
'ssl': tls}
|
||||
if login_user is not None:
|
||||
params['username'] = login_user
|
||||
try:
|
||||
return Redis(**params)
|
||||
return Redis(**redis_auth_params(self.module))
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='{0}'.format(str(e)))
|
||||
return None
|
||||
|
||||
@@ -167,17 +167,61 @@ class Scaleway(object):
|
||||
|
||||
|
||||
SCALEWAY_LOCATION = {
|
||||
'par1': {'name': 'Paris 1', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-1'},
|
||||
'EMEA-FR-PAR1': {'name': 'Paris 1', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-1'},
|
||||
'par1': {
|
||||
'name': 'Paris 1',
|
||||
'country': 'FR',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-1'
|
||||
},
|
||||
|
||||
'par2': {'name': 'Paris 2', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-2'},
|
||||
'EMEA-FR-PAR2': {'name': 'Paris 2', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-2'},
|
||||
'EMEA-FR-PAR1': {
|
||||
'name': 'Paris 1',
|
||||
'country': 'FR',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-1'
|
||||
},
|
||||
|
||||
'ams1': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/nl-ams-1'},
|
||||
'EMEA-NL-EVS': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/nl-ams-1'},
|
||||
'par2': {
|
||||
'name': 'Paris 2',
|
||||
'country': 'FR',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-2',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-2'
|
||||
},
|
||||
|
||||
'waw1': {'name': 'Warsaw 1', 'country': 'PL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/pl-waw-1'},
|
||||
'EMEA-PL-WAW1': {'name': 'Warsaw 1', 'country': 'PL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/pl-waw-1'},
|
||||
'EMEA-FR-PAR2': {
|
||||
'name': 'Paris 2',
|
||||
'country': 'FR',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-2',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-2'
|
||||
},
|
||||
|
||||
'ams1': {
|
||||
'name': 'Amsterdam 1',
|
||||
'country': 'NL',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/nl-ams-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/nl-ams-10'
|
||||
},
|
||||
|
||||
'EMEA-NL-EVS': {
|
||||
'name': 'Amsterdam 1',
|
||||
'country': 'NL',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/nl-ams-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/nl-ams-1'
|
||||
},
|
||||
|
||||
'waw1': {
|
||||
'name': 'Warsaw 1',
|
||||
'country': 'PL',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/pl-waw-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/pl-waw-1'
|
||||
},
|
||||
|
||||
'EMEA-PL-WAW1': {
|
||||
'name': 'Warsaw 1',
|
||||
'country': 'PL',
|
||||
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/pl-waw-1',
|
||||
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/pl-waw-1'
|
||||
},
|
||||
}
|
||||
|
||||
SCALEWAY_ENDPOINT = "https://api.scaleway.com"
|
||||
|
||||
1
plugins/modules/alerta_customer.py
Symbolic link
1
plugins/modules/alerta_customer.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./monitoring/alerta_customer.py
|
||||
@@ -182,10 +182,10 @@ def core(module):
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
mode=dict(default=None, choices=['user', 'system']),
|
||||
mode=dict(choices=['user', 'system']),
|
||||
name=dict(required=True),
|
||||
image=dict(required=True),
|
||||
rootfs=dict(default=None),
|
||||
rootfs=dict(),
|
||||
state=dict(default='latest', choices=['present', 'absent', 'latest', 'rollback']),
|
||||
backend=dict(required=True, choices=['docker', 'ostree']),
|
||||
values=dict(type='list', default=[], elements='str'),
|
||||
|
||||
@@ -228,8 +228,7 @@ class ClcAlertPolicy:
|
||||
choices=[
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk'],
|
||||
default=None),
|
||||
'disk']),
|
||||
duration=dict(type='str'),
|
||||
threshold=dict(type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent'])
|
||||
|
||||
@@ -42,7 +42,7 @@ options:
|
||||
description:
|
||||
- Whether to wait for the tasks to finish before returning.
|
||||
type: str
|
||||
default: True
|
||||
default: 'True'
|
||||
required: False
|
||||
requirements:
|
||||
- python = 2.7
|
||||
|
||||
@@ -66,8 +66,8 @@ options:
|
||||
description:
|
||||
- Whether the firewall policy is enabled or disabled
|
||||
type: str
|
||||
choices: [True, False]
|
||||
default: True
|
||||
choices: ['True', 'False']
|
||||
default: 'True'
|
||||
requirements:
|
||||
- python = 2.7
|
||||
- requests >= 2.5.0
|
||||
|
||||
@@ -297,9 +297,9 @@ class ClcGroup(object):
|
||||
"""
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(default=None),
|
||||
parent=dict(default=None),
|
||||
location=dict(default=None),
|
||||
description=dict(),
|
||||
parent=dict(),
|
||||
location=dict(),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
wait=dict(type='bool', default=True))
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ options:
|
||||
description:
|
||||
- Port to configure on the public-facing side of the load balancer pool
|
||||
type: str
|
||||
choices: [80, 443]
|
||||
choices: ['80', '443']
|
||||
nodes:
|
||||
description:
|
||||
- A list of nodes that needs to be added to the load balancer pool
|
||||
@@ -865,7 +865,7 @@ class ClcLoadBalancer:
|
||||
"""
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
description=dict(default=None),
|
||||
description=dict(),
|
||||
location=dict(required=True),
|
||||
alias=dict(required=True),
|
||||
port=dict(choices=[80, 443]),
|
||||
|
||||
@@ -567,31 +567,31 @@ class ClcServer:
|
||||
template=dict(),
|
||||
group=dict(default='Default Group'),
|
||||
network_id=dict(),
|
||||
location=dict(default=None),
|
||||
location=dict(),
|
||||
cpu=dict(default=1, type='int'),
|
||||
memory=dict(default=1, type='int'),
|
||||
alias=dict(default=None),
|
||||
password=dict(default=None, no_log=True),
|
||||
ip_address=dict(default=None),
|
||||
alias=dict(),
|
||||
password=dict(no_log=True),
|
||||
ip_address=dict(),
|
||||
storage_type=dict(
|
||||
default='standard',
|
||||
choices=[
|
||||
'standard',
|
||||
'hyperscale']),
|
||||
type=dict(default='standard', choices=['standard', 'hyperscale', 'bareMetal']),
|
||||
primary_dns=dict(default=None),
|
||||
secondary_dns=dict(default=None),
|
||||
primary_dns=dict(),
|
||||
secondary_dns=dict(),
|
||||
additional_disks=dict(type='list', default=[], elements='dict'),
|
||||
custom_fields=dict(type='list', default=[], elements='dict'),
|
||||
ttl=dict(default=None),
|
||||
ttl=dict(),
|
||||
managed_os=dict(type='bool', default=False),
|
||||
description=dict(default=None),
|
||||
source_server_password=dict(default=None, no_log=True),
|
||||
cpu_autoscale_policy_id=dict(default=None),
|
||||
anti_affinity_policy_id=dict(default=None),
|
||||
anti_affinity_policy_name=dict(default=None),
|
||||
alert_policy_id=dict(default=None),
|
||||
alert_policy_name=dict(default=None),
|
||||
description=dict(),
|
||||
source_server_password=dict(no_log=True),
|
||||
cpu_autoscale_policy_id=dict(),
|
||||
anti_affinity_policy_id=dict(),
|
||||
anti_affinity_policy_name=dict(),
|
||||
alert_policy_id=dict(),
|
||||
alert_policy_name=dict(),
|
||||
packages=dict(type='list', default=[], elements='dict'),
|
||||
state=dict(
|
||||
default='present',
|
||||
@@ -601,7 +601,7 @@ class ClcServer:
|
||||
'started',
|
||||
'stopped']),
|
||||
count=dict(type='int', default=1),
|
||||
exact_count=dict(type='int', default=None),
|
||||
exact_count=dict(type='int', ),
|
||||
count_group=dict(),
|
||||
server_ids=dict(type='list', default=[], elements='str'),
|
||||
add_public_ip=dict(type='bool', default=False),
|
||||
@@ -612,14 +612,13 @@ class ClcServer:
|
||||
'UDP',
|
||||
'ICMP']),
|
||||
public_ip_ports=dict(type='list', default=[], elements='dict'),
|
||||
configuration_id=dict(default=None),
|
||||
os_type=dict(default=None,
|
||||
choices=[
|
||||
'redHat6_64Bit',
|
||||
'centOS6_64Bit',
|
||||
'windows2012R2Standard_64Bit',
|
||||
'ubuntu14_64Bit'
|
||||
]),
|
||||
configuration_id=dict(),
|
||||
os_type=dict(choices=[
|
||||
'redHat6_64Bit',
|
||||
'centOS6_64Bit',
|
||||
'windows2012R2Standard_64Bit',
|
||||
'ubuntu14_64Bit'
|
||||
]),
|
||||
wait=dict(type='bool', default=True))
|
||||
|
||||
mutually_exclusive = [
|
||||
|
||||
@@ -36,7 +36,7 @@ options:
|
||||
wait:
|
||||
description:
|
||||
- Whether to wait for the provisioning tasks to finish before returning.
|
||||
default: True
|
||||
default: 'True'
|
||||
required: False
|
||||
type: str
|
||||
requirements:
|
||||
|
||||
@@ -51,7 +51,6 @@ options:
|
||||
group.s
|
||||
type: str
|
||||
required: false
|
||||
default: 0
|
||||
vpc_id:
|
||||
description:
|
||||
- Specifies the resource ID of the VPC to which the security group
|
||||
|
||||
@@ -21,6 +21,13 @@ options:
|
||||
- Name of an instance.
|
||||
type: str
|
||||
required: true
|
||||
project:
|
||||
description:
|
||||
- 'Project of an instance.
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/projects.md).'
|
||||
required: false
|
||||
type: str
|
||||
version_added: 4.8.0
|
||||
architecture:
|
||||
description:
|
||||
- 'The architecture for the instance (for example C(x86_64) or C(i686)).
|
||||
@@ -124,6 +131,13 @@ options:
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
wait_for_container:
|
||||
description:
|
||||
- If set to C(true), the tasks will wait till the task reports a
|
||||
success status when performing container operations.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 4.4.0
|
||||
force_stop:
|
||||
description:
|
||||
- If this is true, the C(lxd_container) forces to stop the instance
|
||||
@@ -241,6 +255,26 @@ EXAMPLES = '''
|
||||
wait_for_ipv4_addresses: true
|
||||
timeout: 600
|
||||
|
||||
# An example for creating container in project other than default
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Create a started container in project mytestproject
|
||||
community.general.lxd_container:
|
||||
name: mycontainer
|
||||
project: mytestproject
|
||||
ignore_volatile_options: true
|
||||
state: started
|
||||
source:
|
||||
protocol: simplestreams
|
||||
type: image
|
||||
mode: pull
|
||||
server: https://images.linuxcontainers.org
|
||||
alias: ubuntu/20.04/cloud
|
||||
profiles: ["default"]
|
||||
wait_for_ipv4_addresses: true
|
||||
timeout: 600
|
||||
|
||||
# An example for deleting a container
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
@@ -405,6 +439,7 @@ class LXDContainerManagement(object):
|
||||
"""
|
||||
self.module = module
|
||||
self.name = self.module.params['name']
|
||||
self.project = self.module.params['project']
|
||||
self._build_config()
|
||||
|
||||
self.state = self.module.params['state']
|
||||
@@ -414,6 +449,7 @@ class LXDContainerManagement(object):
|
||||
self.force_stop = self.module.params['force_stop']
|
||||
self.addresses = None
|
||||
self.target = self.module.params['target']
|
||||
self.wait_for_container = self.module.params['wait_for_container']
|
||||
|
||||
self.type = self.module.params['type']
|
||||
|
||||
@@ -460,16 +496,16 @@ class LXDContainerManagement(object):
|
||||
self.config[attr] = param_val
|
||||
|
||||
def _get_instance_json(self):
|
||||
return self.client.do(
|
||||
'GET', '{0}/{1}'.format(self.api_endpoint, self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
return self.client.do('GET', url, ok_error_codes=[404])
|
||||
|
||||
def _get_instance_state_json(self):
|
||||
return self.client.do(
|
||||
'GET', '{0}/{1}/state'.format(self.api_endpoint, self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
url = '{0}/{1}/state'.format(self.api_endpoint, self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
return self.client.do('GET', url, ok_error_codes=[404])
|
||||
|
||||
@staticmethod
|
||||
def _instance_json_to_module_state(resp_json):
|
||||
@@ -478,18 +514,26 @@ class LXDContainerManagement(object):
|
||||
return ANSIBLE_LXD_STATES[resp_json['metadata']['status']]
|
||||
|
||||
def _change_state(self, action, force_stop=False):
|
||||
url = '{0}/{1}/state'.format(self.api_endpoint, self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
body_json = {'action': action, 'timeout': self.timeout}
|
||||
if force_stop:
|
||||
body_json['force'] = True
|
||||
return self.client.do('PUT', '{0}/{1}/state'.format(self.api_endpoint, self.name), body_json=body_json)
|
||||
return self.client.do('PUT', url, body_json=body_json)
|
||||
|
||||
def _create_instance(self):
|
||||
url = self.api_endpoint
|
||||
url_params = dict()
|
||||
if self.target:
|
||||
url_params['target'] = self.target
|
||||
if self.project:
|
||||
url_params['project'] = self.project
|
||||
if url_params:
|
||||
url = '{0}?{1}'.format(url, urlencode(url_params))
|
||||
config = self.config.copy()
|
||||
config['name'] = self.name
|
||||
if self.target:
|
||||
self.client.do('POST', '{0}?{1}'.format(self.api_endpoint, urlencode(dict(target=self.target))), config)
|
||||
else:
|
||||
self.client.do('POST', self.api_endpoint, config)
|
||||
self.client.do('POST', url, config, wait_for_container=self.wait_for_container)
|
||||
self.actions.append('create')
|
||||
|
||||
def _start_instance(self):
|
||||
@@ -505,7 +549,10 @@ class LXDContainerManagement(object):
|
||||
self.actions.append('restart')
|
||||
|
||||
def _delete_instance(self):
|
||||
self.client.do('DELETE', '{0}/{1}'.format(self.api_endpoint, self.name))
|
||||
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
self.client.do('DELETE', url)
|
||||
self.actions.append('delete')
|
||||
|
||||
def _freeze_instance(self):
|
||||
@@ -658,7 +705,10 @@ class LXDContainerManagement(object):
|
||||
if self._needs_to_change_instance_config('profiles'):
|
||||
body_json['profiles'] = self.config['profiles']
|
||||
|
||||
self.client.do('PUT', '{0}/{1}'.format(self.api_endpoint, self.name), body_json=body_json)
|
||||
url = '{0}/{1}'.format(self.api_endpoint, self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
self.client.do('PUT', url, body_json=body_json)
|
||||
self.actions.append('apply_instance_configs')
|
||||
|
||||
def run(self):
|
||||
@@ -707,6 +757,9 @@ def main():
|
||||
type='str',
|
||||
required=True
|
||||
),
|
||||
project=dict(
|
||||
type='str',
|
||||
),
|
||||
architecture=dict(
|
||||
type='str',
|
||||
),
|
||||
@@ -745,6 +798,10 @@ def main():
|
||||
default='container',
|
||||
choices=['container', 'virtual-machine'],
|
||||
),
|
||||
wait_for_container=dict(
|
||||
type='bool',
|
||||
default=False
|
||||
),
|
||||
wait_for_ipv4_addresses=dict(
|
||||
type='bool',
|
||||
default=False
|
||||
|
||||
@@ -21,6 +21,13 @@ options:
|
||||
- Name of a profile.
|
||||
required: true
|
||||
type: str
|
||||
project:
|
||||
description:
|
||||
- 'Project of a profile.
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/projects.md).'
|
||||
type: str
|
||||
required: false
|
||||
version_added: 4.8.0
|
||||
description:
|
||||
description:
|
||||
- Description of the profile.
|
||||
@@ -129,6 +136,19 @@ EXAMPLES = '''
|
||||
parent: br0
|
||||
type: nic
|
||||
|
||||
# An example for creating a profile in project mytestproject
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Create a profile
|
||||
community.general.lxd_profile:
|
||||
name: testprofile
|
||||
project: mytestproject
|
||||
state: present
|
||||
config: {}
|
||||
description: test profile in project mytestproject
|
||||
devices: {}
|
||||
|
||||
# An example for creating a profile via http connection
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
@@ -208,6 +228,7 @@ actions:
|
||||
import os
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
|
||||
# ANSIBLE_LXD_DEFAULT_URL is a default value of the lxd endpoint
|
||||
ANSIBLE_LXD_DEFAULT_URL = 'unix:/var/lib/lxd/unix.socket'
|
||||
@@ -232,6 +253,7 @@ class LXDProfileManagement(object):
|
||||
"""
|
||||
self.module = module
|
||||
self.name = self.module.params['name']
|
||||
self.project = self.module.params['project']
|
||||
self._build_config()
|
||||
self.state = self.module.params['state']
|
||||
self.new_name = self.module.params.get('new_name', None)
|
||||
@@ -272,10 +294,10 @@ class LXDProfileManagement(object):
|
||||
self.config[attr] = param_val
|
||||
|
||||
def _get_profile_json(self):
|
||||
return self.client.do(
|
||||
'GET', '/1.0/profiles/{0}'.format(self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
url = '/1.0/profiles/{0}'.format(self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
return self.client.do('GET', url, ok_error_codes=[404])
|
||||
|
||||
@staticmethod
|
||||
def _profile_json_to_module_state(resp_json):
|
||||
@@ -307,14 +329,20 @@ class LXDProfileManagement(object):
|
||||
changed=False)
|
||||
|
||||
def _create_profile(self):
|
||||
url = '/1.0/profiles'
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
config = self.config.copy()
|
||||
config['name'] = self.name
|
||||
self.client.do('POST', '/1.0/profiles', config)
|
||||
self.client.do('POST', url, config)
|
||||
self.actions.append('create')
|
||||
|
||||
def _rename_profile(self):
|
||||
url = '/1.0/profiles/{0}'.format(self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
config = {'name': self.new_name}
|
||||
self.client.do('POST', '/1.0/profiles/{0}'.format(self.name), config)
|
||||
self.client.do('POST', url, config)
|
||||
self.actions.append('rename')
|
||||
self.name = self.new_name
|
||||
|
||||
@@ -421,11 +449,17 @@ class LXDProfileManagement(object):
|
||||
config = self._generate_new_config(config)
|
||||
|
||||
# upload config to lxd
|
||||
self.client.do('PUT', '/1.0/profiles/{0}'.format(self.name), config)
|
||||
url = '/1.0/profiles/{0}'.format(self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
self.client.do('PUT', url, config)
|
||||
self.actions.append('apply_profile_configs')
|
||||
|
||||
def _delete_profile(self):
|
||||
self.client.do('DELETE', '/1.0/profiles/{0}'.format(self.name))
|
||||
url = '/1.0/profiles/{0}'.format(self.name)
|
||||
if self.project:
|
||||
url = '{0}?{1}'.format(url, urlencode(dict(project=self.project)))
|
||||
self.client.do('DELETE', url)
|
||||
self.actions.append('delete')
|
||||
|
||||
def run(self):
|
||||
@@ -469,6 +503,9 @@ def main():
|
||||
type='str',
|
||||
required=True
|
||||
),
|
||||
project=dict(
|
||||
type='str',
|
||||
),
|
||||
new_name=dict(
|
||||
type='str',
|
||||
),
|
||||
|
||||
451
plugins/modules/cloud/lxd/lxd_project.py
Normal file
451
plugins/modules/cloud/lxd/lxd_project.py
Normal file
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: lxd_project
|
||||
short_description: Manage LXD projects
|
||||
version_added: 4.8.0
|
||||
description:
|
||||
- Management of LXD projects.
|
||||
author: "Raymond Chang (@we10710aa)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the project.
|
||||
required: true
|
||||
type: str
|
||||
description:
|
||||
description:
|
||||
- Description of the project.
|
||||
type: str
|
||||
config:
|
||||
description:
|
||||
- 'The config for the project (for example C({"features.profiles": "true"})).
|
||||
See U(https://linuxcontainers.org/lxd/docs/master/projects/).'
|
||||
- If the project already exists and its "config" value in metadata
|
||||
obtained from
|
||||
C(GET /1.0/projects/<name>)
|
||||
U(https://linuxcontainers.org/lxd/docs/master/api/#/projects/project_get)
|
||||
are different, then this module tries to apply the configurations.
|
||||
type: dict
|
||||
new_name:
|
||||
description:
|
||||
- A new name of a project.
|
||||
- If this parameter is specified a project will be renamed to this name.
|
||||
See U(https://linuxcontainers.org/lxd/docs/master/api/#/projects/project_post).
|
||||
required: false
|
||||
type: str
|
||||
merge_project:
|
||||
description:
|
||||
- Merge the configuration of the present project with the new desired configuration,
|
||||
instead of replacing it. If configuration is the same after merged, no change will be made.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
state:
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
description:
|
||||
- Define the state of a project.
|
||||
required: false
|
||||
default: present
|
||||
type: str
|
||||
url:
|
||||
description:
|
||||
- The Unix domain socket path or the https URL for the LXD server.
|
||||
required: false
|
||||
default: unix:/var/lib/lxd/unix.socket
|
||||
type: str
|
||||
snap_url:
|
||||
description:
|
||||
- The Unix domain socket path when LXD is installed by snap package manager.
|
||||
required: false
|
||||
default: unix:/var/snap/lxd/common/lxd/unix.socket
|
||||
type: str
|
||||
client_key:
|
||||
description:
|
||||
- The client certificate key file path.
|
||||
- If not specified, it defaults to C($HOME/.config/lxc/client.key).
|
||||
required: false
|
||||
aliases: [ key_file ]
|
||||
type: path
|
||||
client_cert:
|
||||
description:
|
||||
- The client certificate file path.
|
||||
- If not specified, it defaults to C($HOME/.config/lxc/client.crt).
|
||||
required: false
|
||||
aliases: [ cert_file ]
|
||||
type: path
|
||||
trust_password:
|
||||
description:
|
||||
- The client trusted password.
|
||||
- 'You need to set this password on the LXD server before
|
||||
running this module using the following command:
|
||||
C(lxc config set core.trust_password <some random password>)
|
||||
See U(https://www.stgraber.org/2016/04/18/lxd-api-direct-interaction/).'
|
||||
- If I(trust_password) is set, this module send a request for
|
||||
authentication before sending any requests.
|
||||
required: false
|
||||
type: str
|
||||
notes:
|
||||
- Projects must have a unique name. If you attempt to create a project
|
||||
with a name that already existed in the users namespace the module will
|
||||
simply return as "unchanged".
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# An example for creating a project
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Create a project
|
||||
community.general.lxd_project:
|
||||
name: ansible-test-project
|
||||
state: present
|
||||
config: {}
|
||||
description: my new project
|
||||
|
||||
# An example for renaming a project
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Rename ansible-test-project to ansible-test-project-new-name
|
||||
community.general.lxd_project:
|
||||
name: ansible-test-project
|
||||
new_name: ansible-test-project-new-name
|
||||
state: present
|
||||
config: {}
|
||||
description: my new project
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
old_state:
|
||||
description: The old state of the project.
|
||||
returned: success
|
||||
type: str
|
||||
sample: "absent"
|
||||
logs:
|
||||
description: The logs of requests and responses.
|
||||
returned: when ansible-playbook is invoked with -vvvv.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
type:
|
||||
description: Type of actions performed, currently only C(sent request).
|
||||
type: str
|
||||
sample: "sent request"
|
||||
request:
|
||||
description: HTTP request sent to LXD server.
|
||||
type: dict
|
||||
contains:
|
||||
method:
|
||||
description: Method of HTTP request.
|
||||
type: str
|
||||
sample: "GET"
|
||||
url:
|
||||
description: URL path of HTTP request.
|
||||
type: str
|
||||
sample: "/1.0/projects/test-project"
|
||||
json:
|
||||
description: JSON body of HTTP request.
|
||||
type: str
|
||||
sample: "(too long to be placed here)"
|
||||
timeout:
|
||||
description: Timeout of HTTP request, C(null) if unset.
|
||||
type: int
|
||||
sample: null
|
||||
response:
|
||||
description: HTTP response received from LXD server.
|
||||
type: dict
|
||||
contains:
|
||||
json:
|
||||
description: JSON of HTTP response.
|
||||
type: str
|
||||
sample: "(too long to be placed here)"
|
||||
actions:
|
||||
description: List of actions performed for the project.
|
||||
returned: success
|
||||
type: list
|
||||
elements: str
|
||||
sample: ["create"]
|
||||
'''
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
import os
|
||||
|
||||
# ANSIBLE_LXD_DEFAULT_URL is a default value of the lxd endpoint
|
||||
ANSIBLE_LXD_DEFAULT_URL = 'unix:/var/lib/lxd/unix.socket'
|
||||
|
||||
# PROJECTS_STATES is a list for states supported
|
||||
PROJECTS_STATES = [
|
||||
'present', 'absent'
|
||||
]
|
||||
|
||||
# CONFIG_PARAMS is a list of config attribute names.
|
||||
CONFIG_PARAMS = [
|
||||
'config', 'description'
|
||||
]
|
||||
|
||||
|
||||
class LXDProjectManagement(object):
|
||||
def __init__(self, module):
|
||||
"""Management of LXC projects via Ansible.
|
||||
|
||||
:param module: Processed Ansible Module.
|
||||
:type module: ``object``
|
||||
"""
|
||||
self.module = module
|
||||
self.name = self.module.params['name']
|
||||
self._build_config()
|
||||
self.state = self.module.params['state']
|
||||
self.new_name = self.module.params.get('new_name', None)
|
||||
|
||||
self.key_file = self.module.params.get('client_key')
|
||||
if self.key_file is None:
|
||||
self.key_file = os.path.expanduser('~/.config/lxc/client.key')
|
||||
self.cert_file = self.module.params.get('client_cert')
|
||||
if self.cert_file is None:
|
||||
self.cert_file = os.path.expanduser('~/.config/lxc/client.crt')
|
||||
self.debug = self.module._verbosity >= 4
|
||||
|
||||
try:
|
||||
if self.module.params['url'] != ANSIBLE_LXD_DEFAULT_URL:
|
||||
self.url = self.module.params['url']
|
||||
elif os.path.exists(self.module.params['snap_url'].replace('unix:', '')):
|
||||
self.url = self.module.params['snap_url']
|
||||
else:
|
||||
self.url = self.module.params['url']
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg=e.msg)
|
||||
|
||||
try:
|
||||
self.client = LXDClient(
|
||||
self.url, key_file=self.key_file, cert_file=self.cert_file,
|
||||
debug=self.debug
|
||||
)
|
||||
except LXDClientException as e:
|
||||
self.module.fail_json(msg=e.msg)
|
||||
self.trust_password = self.module.params.get('trust_password', None)
|
||||
self.actions = []
|
||||
|
||||
def _build_config(self):
|
||||
self.config = {}
|
||||
for attr in CONFIG_PARAMS:
|
||||
param_val = self.module.params.get(attr, None)
|
||||
if param_val is not None:
|
||||
self.config[attr] = param_val
|
||||
|
||||
def _get_project_json(self):
|
||||
return self.client.do(
|
||||
'GET', '/1.0/projects/{0}'.format(self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _project_json_to_module_state(resp_json):
|
||||
if resp_json['type'] == 'error':
|
||||
return 'absent'
|
||||
return 'present'
|
||||
|
||||
def _update_project(self):
|
||||
if self.state == 'present':
|
||||
if self.old_state == 'absent':
|
||||
if self.new_name is None:
|
||||
self._create_project()
|
||||
else:
|
||||
self.module.fail_json(
|
||||
msg='new_name must not be set when the project does not exist and the state is present',
|
||||
changed=False)
|
||||
else:
|
||||
if self.new_name is not None and self.new_name != self.name:
|
||||
self._rename_project()
|
||||
if self._needs_to_apply_project_configs():
|
||||
self._apply_project_configs()
|
||||
elif self.state == 'absent':
|
||||
if self.old_state == 'present':
|
||||
if self.new_name is None:
|
||||
self._delete_project()
|
||||
else:
|
||||
self.module.fail_json(
|
||||
msg='new_name must not be set when the project exists and the specified state is absent',
|
||||
changed=False)
|
||||
|
||||
def _create_project(self):
|
||||
config = self.config.copy()
|
||||
config['name'] = self.name
|
||||
self.client.do('POST', '/1.0/projects', config)
|
||||
self.actions.append('create')
|
||||
|
||||
def _rename_project(self):
|
||||
config = {'name': self.new_name}
|
||||
self.client.do('POST', '/1.0/projects/{0}'.format(self.name), config)
|
||||
self.actions.append('rename')
|
||||
self.name = self.new_name
|
||||
|
||||
def _needs_to_change_project_config(self, key):
|
||||
if key not in self.config:
|
||||
return False
|
||||
old_configs = self.old_project_json['metadata'].get(key, None)
|
||||
return self.config[key] != old_configs
|
||||
|
||||
def _needs_to_apply_project_configs(self):
|
||||
return (
|
||||
self._needs_to_change_project_config('config') or
|
||||
self._needs_to_change_project_config('description')
|
||||
)
|
||||
|
||||
def _merge_dicts(self, source, destination):
|
||||
""" Return a new dict taht merge two dict,
|
||||
with values in source dict overwrite destination dict
|
||||
|
||||
Args:
|
||||
dict(source): source dict
|
||||
dict(destination): destination dict
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
dict(destination): merged dict"""
|
||||
result = destination.copy()
|
||||
for key, value in source.items():
|
||||
if isinstance(value, dict):
|
||||
# get node or create one
|
||||
node = result.setdefault(key, {})
|
||||
self._merge_dicts(value, node)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def _apply_project_configs(self):
|
||||
""" Selection of the procedure: rebuild or merge
|
||||
|
||||
The standard behavior is that all information not contained
|
||||
in the play is discarded.
|
||||
|
||||
If "merge_project" is provides in the play and "True", then existing
|
||||
configurations from the project and new ones defined are merged.
|
||||
|
||||
Args:
|
||||
None
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
None"""
|
||||
old_config = dict()
|
||||
old_metadata = self.old_project_json['metadata'].copy()
|
||||
for attr in CONFIG_PARAMS:
|
||||
old_config[attr] = old_metadata[attr]
|
||||
|
||||
if self.module.params['merge_project']:
|
||||
config = self._merge_dicts(self.config, old_config)
|
||||
if config == old_config:
|
||||
# no need to call api if merged config is the same
|
||||
# as old config
|
||||
return
|
||||
else:
|
||||
config = self.config.copy()
|
||||
# upload config to lxd
|
||||
self.client.do('PUT', '/1.0/projects/{0}'.format(self.name), config)
|
||||
self.actions.append('apply_projects_configs')
|
||||
|
||||
def _delete_project(self):
|
||||
self.client.do('DELETE', '/1.0/projects/{0}'.format(self.name))
|
||||
self.actions.append('delete')
|
||||
|
||||
def run(self):
|
||||
"""Run the main method."""
|
||||
|
||||
try:
|
||||
if self.trust_password is not None:
|
||||
self.client.authenticate(self.trust_password)
|
||||
|
||||
self.old_project_json = self._get_project_json()
|
||||
self.old_state = self._project_json_to_module_state(
|
||||
self.old_project_json)
|
||||
self._update_project()
|
||||
|
||||
state_changed = len(self.actions) > 0
|
||||
result_json = {
|
||||
'changed': state_changed,
|
||||
'old_state': self.old_state,
|
||||
'actions': self.actions
|
||||
}
|
||||
if self.client.debug:
|
||||
result_json['logs'] = self.client.logs
|
||||
self.module.exit_json(**result_json)
|
||||
except LXDClientException as e:
|
||||
state_changed = len(self.actions) > 0
|
||||
fail_params = {
|
||||
'msg': e.msg,
|
||||
'changed': state_changed,
|
||||
'actions': self.actions
|
||||
}
|
||||
if self.client.debug:
|
||||
fail_params['logs'] = e.kwargs['logs']
|
||||
self.module.fail_json(**fail_params)
|
||||
|
||||
|
||||
def main():
|
||||
"""Ansible Main module."""
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(
|
||||
type='str',
|
||||
required=True
|
||||
),
|
||||
new_name=dict(
|
||||
type='str',
|
||||
),
|
||||
config=dict(
|
||||
type='dict',
|
||||
),
|
||||
description=dict(
|
||||
type='str',
|
||||
),
|
||||
merge_project=dict(
|
||||
type='bool',
|
||||
default=False
|
||||
),
|
||||
state=dict(
|
||||
choices=PROJECTS_STATES,
|
||||
default='present'
|
||||
),
|
||||
url=dict(
|
||||
type='str',
|
||||
default=ANSIBLE_LXD_DEFAULT_URL
|
||||
),
|
||||
snap_url=dict(
|
||||
type='str',
|
||||
default='unix:/var/snap/lxd/common/lxd/unix.socket'
|
||||
),
|
||||
client_key=dict(
|
||||
type='path',
|
||||
aliases=['key_file']
|
||||
),
|
||||
client_cert=dict(
|
||||
type='path',
|
||||
aliases=['cert_file']
|
||||
),
|
||||
trust_password=dict(type='str', no_log=True)
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
|
||||
lxd_manage = LXDProjectManagement(module=module)
|
||||
lxd_manage.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright: Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
@@ -392,229 +393,189 @@ import traceback
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from proxmoxer import ProxmoxAPI
|
||||
HAS_PROXMOXER = True
|
||||
except ImportError:
|
||||
HAS_PROXMOXER = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
ansible_to_proxmox_bool
|
||||
)
|
||||
|
||||
ansible_to_proxmox_bool, proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
VZ_TYPE = None
|
||||
|
||||
|
||||
def get_nextvmid(module, proxmox):
|
||||
try:
|
||||
vmid = proxmox.cluster.nextid.get()
|
||||
return vmid
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Unable to get next vmid. Failed with exception: %s" % to_native(e),
|
||||
exception=traceback.format_exc())
|
||||
class ProxmoxLxcAnsible(ProxmoxAnsible):
|
||||
def content_check(self, node, ostemplate, template_store):
|
||||
return [True for cnt in self.proxmox_api.nodes(node).storage(template_store).content.get() if cnt['volid'] == ostemplate]
|
||||
|
||||
def is_template_container(self, node, vmid):
|
||||
"""Check if the specified container is a template."""
|
||||
proxmox_node = self.proxmox_api.nodes(node)
|
||||
config = getattr(proxmox_node, VZ_TYPE)(vmid).config.get()
|
||||
return config['template']
|
||||
|
||||
def get_vmid(proxmox, hostname):
|
||||
return [vm['vmid'] for vm in proxmox.cluster.resources.get(type='vm') if 'name' in vm and vm['name'] == hostname]
|
||||
def create_instance(self, vmid, node, disk, storage, cpus, memory, swap, timeout, clone, **kwargs):
|
||||
proxmox_node = self.proxmox_api.nodes(node)
|
||||
|
||||
# Remove all empty kwarg entries
|
||||
kwargs = dict((k, v) for k, v in kwargs.items() if v is not None)
|
||||
|
||||
def get_instance(proxmox, vmid):
|
||||
return [vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid)]
|
||||
|
||||
|
||||
def content_check(proxmox, node, ostemplate, template_store):
|
||||
return [True for cnt in proxmox.nodes(node).storage(template_store).content.get() if cnt['volid'] == ostemplate]
|
||||
|
||||
|
||||
def is_template_container(proxmox, node, vmid):
|
||||
"""Check if the specified container is a template."""
|
||||
proxmox_node = proxmox.nodes(node)
|
||||
config = getattr(proxmox_node, VZ_TYPE)(vmid).config.get()
|
||||
return config['template']
|
||||
|
||||
|
||||
def node_check(proxmox, node):
|
||||
return [True for nd in proxmox.nodes.get() if nd['node'] == node]
|
||||
|
||||
|
||||
def proxmox_version(proxmox):
|
||||
apireturn = proxmox.version.get()
|
||||
return LooseVersion(apireturn['version'])
|
||||
|
||||
|
||||
def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone, **kwargs):
|
||||
proxmox_node = proxmox.nodes(node)
|
||||
|
||||
# Remove all empty kwarg entries
|
||||
kwargs = dict((k, v) for k, v in kwargs.items() if v is not None)
|
||||
|
||||
if VZ_TYPE == 'lxc':
|
||||
kwargs['cpulimit'] = cpus
|
||||
kwargs['rootfs'] = disk
|
||||
if 'netif' in kwargs:
|
||||
kwargs.update(kwargs['netif'])
|
||||
del kwargs['netif']
|
||||
if 'mounts' in kwargs:
|
||||
kwargs.update(kwargs['mounts'])
|
||||
del kwargs['mounts']
|
||||
if 'pubkey' in kwargs:
|
||||
if proxmox_version(proxmox) >= LooseVersion('4.2'):
|
||||
kwargs['ssh-public-keys'] = kwargs['pubkey']
|
||||
del kwargs['pubkey']
|
||||
else:
|
||||
kwargs['cpus'] = cpus
|
||||
kwargs['disk'] = disk
|
||||
|
||||
if clone is not None:
|
||||
if VZ_TYPE != 'lxc':
|
||||
module.fail_json(changed=False, msg="Clone operator is only supported for LXC enabled proxmox clusters.")
|
||||
|
||||
clone_is_template = is_template_container(proxmox, node, clone)
|
||||
|
||||
# By default, create a full copy only when the cloned container is not a template.
|
||||
create_full_copy = not clone_is_template
|
||||
|
||||
# Only accept parameters that are compatible with the clone endpoint.
|
||||
valid_clone_parameters = ['hostname', 'pool', 'description']
|
||||
if module.params['storage'] is not None and clone_is_template:
|
||||
# Cloning a template, so create a full copy instead of a linked copy
|
||||
create_full_copy = True
|
||||
elif module.params['storage'] is None and not clone_is_template:
|
||||
# Not cloning a template, but also no defined storage. This isn't possible.
|
||||
module.fail_json(changed=False, msg="Cloned container is not a template, storage needs to be specified.")
|
||||
|
||||
if module.params['clone_type'] == 'linked':
|
||||
if not clone_is_template:
|
||||
module.fail_json(changed=False, msg="'linked' clone type is specified, but cloned container is not a template container.")
|
||||
# Don't need to do more, by default create_full_copy is set to false already
|
||||
elif module.params['clone_type'] == 'opportunistic':
|
||||
if not clone_is_template:
|
||||
# Cloned container is not a template, so we need our 'storage' parameter
|
||||
valid_clone_parameters.append('storage')
|
||||
elif module.params['clone_type'] == 'full':
|
||||
create_full_copy = True
|
||||
valid_clone_parameters.append('storage')
|
||||
|
||||
clone_parameters = {}
|
||||
|
||||
if create_full_copy:
|
||||
clone_parameters['full'] = '1'
|
||||
if VZ_TYPE == 'lxc':
|
||||
kwargs['cpulimit'] = cpus
|
||||
kwargs['rootfs'] = disk
|
||||
if 'netif' in kwargs:
|
||||
kwargs.update(kwargs['netif'])
|
||||
del kwargs['netif']
|
||||
if 'mounts' in kwargs:
|
||||
kwargs.update(kwargs['mounts'])
|
||||
del kwargs['mounts']
|
||||
if 'pubkey' in kwargs:
|
||||
if self.version() >= LooseVersion('4.2'):
|
||||
kwargs['ssh-public-keys'] = kwargs['pubkey']
|
||||
del kwargs['pubkey']
|
||||
else:
|
||||
clone_parameters['full'] = '0'
|
||||
for param in valid_clone_parameters:
|
||||
if module.params[param] is not None:
|
||||
clone_parameters[param] = module.params[param]
|
||||
kwargs['cpus'] = cpus
|
||||
kwargs['disk'] = disk
|
||||
|
||||
taskid = getattr(proxmox_node, VZ_TYPE)(clone).clone.post(newid=vmid, **clone_parameters)
|
||||
else:
|
||||
taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)
|
||||
if clone is not None:
|
||||
if VZ_TYPE != 'lxc':
|
||||
self.module.fail_json(changed=False, msg="Clone operator is only supported for LXC enabled proxmox clusters.")
|
||||
|
||||
while timeout:
|
||||
if (proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s' %
|
||||
proxmox_node.tasks(taskid).log.get()[:1])
|
||||
clone_is_template = self.is_template_container(node, clone)
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
# By default, create a full copy only when the cloned container is not a template.
|
||||
create_full_copy = not clone_is_template
|
||||
|
||||
# Only accept parameters that are compatible with the clone endpoint.
|
||||
valid_clone_parameters = ['hostname', 'pool', 'description']
|
||||
if self.module.params['storage'] is not None and clone_is_template:
|
||||
# Cloning a template, so create a full copy instead of a linked copy
|
||||
create_full_copy = True
|
||||
elif self.module.params['storage'] is None and not clone_is_template:
|
||||
# Not cloning a template, but also no defined storage. This isn't possible.
|
||||
self.module.fail_json(changed=False, msg="Cloned container is not a template, storage needs to be specified.")
|
||||
|
||||
def start_instance(module, proxmox, vm, vmid, timeout):
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.start.post()
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s' %
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
if self.module.params['clone_type'] == 'linked':
|
||||
if not clone_is_template:
|
||||
self.module.fail_json(changed=False, msg="'linked' clone type is specified, but cloned container is not a template container.")
|
||||
# Don't need to do more, by default create_full_copy is set to false already
|
||||
elif self.module.params['clone_type'] == 'opportunistic':
|
||||
if not clone_is_template:
|
||||
# Cloned container is not a template, so we need our 'storage' parameter
|
||||
valid_clone_parameters.append('storage')
|
||||
elif self.module.params['clone_type'] == 'full':
|
||||
create_full_copy = True
|
||||
valid_clone_parameters.append('storage')
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
clone_parameters = {}
|
||||
|
||||
if create_full_copy:
|
||||
clone_parameters['full'] = '1'
|
||||
else:
|
||||
clone_parameters['full'] = '0'
|
||||
for param in valid_clone_parameters:
|
||||
if self.module.params[param] is not None:
|
||||
clone_parameters[param] = self.module.params[param]
|
||||
|
||||
def stop_instance(module, proxmox, vm, vmid, timeout, force):
|
||||
if force:
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post(forceStop=1)
|
||||
else:
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.shutdown.post()
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s' %
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
taskid = getattr(proxmox_node, VZ_TYPE)(clone).clone.post(newid=vmid, **clone_parameters)
|
||||
else:
|
||||
taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
while timeout:
|
||||
if (proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s' %
|
||||
proxmox_node.tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def umount_instance(module, proxmox, vm, vmid, timeout):
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.umount.post()
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for unmounting VM. Last line in task before timeout: %s' %
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
def start_instance(self, vm, vmid, timeout):
|
||||
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.start.post()
|
||||
while timeout:
|
||||
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def stop_instance(self, vm, vmid, timeout, force):
|
||||
if force:
|
||||
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.shutdown.post(forceStop=1)
|
||||
else:
|
||||
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.shutdown.post()
|
||||
while timeout:
|
||||
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def umount_instance(self, vm, vmid, timeout):
|
||||
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.umount.post()
|
||||
while timeout:
|
||||
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
self.module.fail_json(msg='Reached timeout while waiting for unmounting VM. Last line in task before timeout: %s' %
|
||||
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
module_args = proxmox_auth_argument_spec()
|
||||
proxmox_args = dict(
|
||||
vmid=dict(type='int', required=False),
|
||||
node=dict(),
|
||||
pool=dict(),
|
||||
password=dict(no_log=True),
|
||||
hostname=dict(),
|
||||
ostemplate=dict(),
|
||||
disk=dict(type='str'),
|
||||
cores=dict(type='int'),
|
||||
cpus=dict(type='int'),
|
||||
memory=dict(type='int'),
|
||||
swap=dict(type='int'),
|
||||
netif=dict(type='dict'),
|
||||
mounts=dict(type='dict'),
|
||||
ip_address=dict(),
|
||||
onboot=dict(type='bool'),
|
||||
features=dict(type='list', elements='str'),
|
||||
storage=dict(default='local'),
|
||||
cpuunits=dict(type='int'),
|
||||
nameserver=dict(),
|
||||
searchdomain=dict(),
|
||||
timeout=dict(type='int', default=30),
|
||||
force=dict(type='bool', default=False),
|
||||
purge=dict(type='bool', default=False),
|
||||
state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']),
|
||||
pubkey=dict(type='str'),
|
||||
unprivileged=dict(type='bool', default=False),
|
||||
description=dict(type='str'),
|
||||
hookscript=dict(type='str'),
|
||||
proxmox_default_behavior=dict(type='str', default='no_defaults', choices=['compatibility', 'no_defaults']),
|
||||
clone=dict(type='int'),
|
||||
clone_type=dict(default='opportunistic', choices=['full', 'linked', 'opportunistic']),
|
||||
)
|
||||
module_args.update(proxmox_args)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
api_host=dict(required=True),
|
||||
api_password=dict(no_log=True, fallback=(env_fallback, ['PROXMOX_PASSWORD'])),
|
||||
api_token_id=dict(no_log=True),
|
||||
api_token_secret=dict(no_log=True),
|
||||
api_user=dict(required=True),
|
||||
vmid=dict(type='int', required=False),
|
||||
validate_certs=dict(type='bool', default=False),
|
||||
node=dict(),
|
||||
pool=dict(),
|
||||
password=dict(no_log=True),
|
||||
hostname=dict(),
|
||||
ostemplate=dict(),
|
||||
disk=dict(type='str'),
|
||||
cores=dict(type='int'),
|
||||
cpus=dict(type='int'),
|
||||
memory=dict(type='int'),
|
||||
swap=dict(type='int'),
|
||||
netif=dict(type='dict'),
|
||||
mounts=dict(type='dict'),
|
||||
ip_address=dict(),
|
||||
onboot=dict(type='bool'),
|
||||
features=dict(type='list', elements='str'),
|
||||
storage=dict(default='local'),
|
||||
cpuunits=dict(type='int'),
|
||||
nameserver=dict(),
|
||||
searchdomain=dict(),
|
||||
timeout=dict(type='int', default=30),
|
||||
force=dict(type='bool', default=False),
|
||||
purge=dict(type='bool', default=False),
|
||||
state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']),
|
||||
pubkey=dict(type='str', default=None),
|
||||
unprivileged=dict(type='bool', default=False),
|
||||
description=dict(type='str'),
|
||||
hookscript=dict(type='str'),
|
||||
proxmox_default_behavior=dict(type='str', default='no_defaults', choices=['compatibility', 'no_defaults']),
|
||||
clone=dict(type='int'),
|
||||
clone_type=dict(default='opportunistic', choices=['full', 'linked', 'opportunistic']),
|
||||
),
|
||||
argument_spec=module_args,
|
||||
required_if=[
|
||||
('state', 'present', ['node', 'hostname']),
|
||||
('state', 'present', ('clone', 'ostemplate'), True), # Require one of clone and ostemplate. Together with mutually_exclusive this ensures that we
|
||||
@@ -627,17 +588,13 @@ def main():
|
||||
mutually_exclusive=[('clone', 'ostemplate')], # Creating a new container is done either by cloning an existing one, or based on a template.
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg='proxmoxer required for this module')
|
||||
proxmox = ProxmoxLxcAnsible(module)
|
||||
|
||||
global VZ_TYPE
|
||||
VZ_TYPE = 'openvz' if proxmox.version() < LooseVersion('4.0') else 'lxc'
|
||||
|
||||
state = module.params['state']
|
||||
api_host = module.params['api_host']
|
||||
api_password = module.params['api_password']
|
||||
api_token_id = module.params['api_token_id']
|
||||
api_token_secret = module.params['api_token_secret']
|
||||
api_user = module.params['api_user']
|
||||
vmid = module.params['vmid']
|
||||
validate_certs = module.params['validate_certs']
|
||||
node = module.params['node']
|
||||
disk = module.params['disk']
|
||||
cpus = module.params['cpus']
|
||||
@@ -664,68 +621,54 @@ def main():
|
||||
if module.params[param] is None:
|
||||
module.params[param] = value
|
||||
|
||||
auth_args = {'user': api_user}
|
||||
if not api_token_id:
|
||||
auth_args['password'] = api_password
|
||||
else:
|
||||
auth_args['token_name'] = api_token_id
|
||||
auth_args['token_value'] = api_token_secret
|
||||
|
||||
try:
|
||||
proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
|
||||
global VZ_TYPE
|
||||
VZ_TYPE = 'openvz' if proxmox_version(proxmox) < LooseVersion('4.0') else 'lxc'
|
||||
except Exception as e:
|
||||
module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
|
||||
|
||||
# If vmid not set get the Next VM id from ProxmoxAPI
|
||||
# If hostname is set get the VM id from ProxmoxAPI
|
||||
if not vmid and state == 'present':
|
||||
vmid = get_nextvmid(module, proxmox)
|
||||
vmid = proxmox.get_nextvmid()
|
||||
elif not vmid and hostname:
|
||||
hosts = get_vmid(proxmox, hostname)
|
||||
if len(hosts) == 0:
|
||||
module.fail_json(msg="Vmid could not be fetched => Hostname doesn't exist (action: %s)" % state)
|
||||
vmid = hosts[0]
|
||||
vmid = proxmox.get_vmid(hostname, choose_first_if_multiple=True)
|
||||
elif not vmid:
|
||||
module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state)
|
||||
|
||||
# Create a new container
|
||||
if state == 'present' and clone is None:
|
||||
try:
|
||||
if get_instance(proxmox, vmid) and not module.params['force']:
|
||||
if proxmox.get_vm(vmid, ignore_missing=True) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
|
||||
# If no vmid was passed, there cannot be another VM named 'hostname'
|
||||
if not module.params['vmid'] and get_vmid(proxmox, hostname) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, get_vmid(proxmox, hostname)[0]))
|
||||
elif not node_check(proxmox, node):
|
||||
if (not module.params['vmid'] and
|
||||
proxmox.get_vmid(hostname, ignore_missing=True, choose_first_if_multiple=True) and
|
||||
not module.params['force']):
|
||||
vmid = proxmox.get_vmid(hostname, choose_first_if_multiple=True)
|
||||
module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, vmid))
|
||||
elif not proxmox.get_node(node):
|
||||
module.fail_json(msg="node '%s' not exists in cluster" % node)
|
||||
elif not content_check(proxmox, node, module.params['ostemplate'], template_store):
|
||||
elif not proxmox.content_check(node, module.params['ostemplate'], template_store):
|
||||
module.fail_json(msg="ostemplate '%s' not exists on node %s and storage %s"
|
||||
% (module.params['ostemplate'], node, template_store))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Pre-creation checks of {VZ_TYPE} VM {vmid} failed with exception: {e}".format(VZ_TYPE=VZ_TYPE, vmid=vmid, e=e))
|
||||
|
||||
try:
|
||||
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone,
|
||||
cores=module.params['cores'],
|
||||
pool=module.params['pool'],
|
||||
password=module.params['password'],
|
||||
hostname=module.params['hostname'],
|
||||
ostemplate=module.params['ostemplate'],
|
||||
netif=module.params['netif'],
|
||||
mounts=module.params['mounts'],
|
||||
ip_address=module.params['ip_address'],
|
||||
onboot=ansible_to_proxmox_bool(module.params['onboot']),
|
||||
cpuunits=module.params['cpuunits'],
|
||||
nameserver=module.params['nameserver'],
|
||||
searchdomain=module.params['searchdomain'],
|
||||
force=ansible_to_proxmox_bool(module.params['force']),
|
||||
pubkey=module.params['pubkey'],
|
||||
features=",".join(module.params['features']) if module.params['features'] is not None else None,
|
||||
unprivileged=ansible_to_proxmox_bool(module.params['unprivileged']),
|
||||
description=module.params['description'],
|
||||
hookscript=module.params['hookscript'])
|
||||
proxmox.create_instance(vmid, node, disk, storage, cpus, memory, swap, timeout, clone,
|
||||
cores=module.params['cores'],
|
||||
pool=module.params['pool'],
|
||||
password=module.params['password'],
|
||||
hostname=module.params['hostname'],
|
||||
ostemplate=module.params['ostemplate'],
|
||||
netif=module.params['netif'],
|
||||
mounts=module.params['mounts'],
|
||||
ip_address=module.params['ip_address'],
|
||||
onboot=ansible_to_proxmox_bool(module.params['onboot']),
|
||||
cpuunits=module.params['cpuunits'],
|
||||
nameserver=module.params['nameserver'],
|
||||
searchdomain=module.params['searchdomain'],
|
||||
force=ansible_to_proxmox_bool(module.params['force']),
|
||||
pubkey=module.params['pubkey'],
|
||||
features=",".join(module.params['features']) if module.params['features'] is not None else None,
|
||||
unprivileged=ansible_to_proxmox_bool(module.params['unprivileged']),
|
||||
description=module.params['description'],
|
||||
hookscript=module.params['hookscript'])
|
||||
|
||||
module.exit_json(changed=True, msg="Deployed VM %s from template %s" % (vmid, module.params['ostemplate']))
|
||||
except Exception as e:
|
||||
@@ -734,18 +677,21 @@ def main():
|
||||
# Clone a container
|
||||
elif state == 'present' and clone is not None:
|
||||
try:
|
||||
if get_instance(proxmox, vmid) and not module.params['force']:
|
||||
if proxmox.get_vm(vmid, ignore_missing=True) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
|
||||
# If no vmid was passed, there cannot be another VM named 'hostname'
|
||||
if not module.params['vmid'] and get_vmid(proxmox, hostname) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, get_vmid(proxmox, hostname)[0]))
|
||||
if not get_instance(proxmox, clone):
|
||||
if (not module.params['vmid'] and
|
||||
proxmox.get_vmid(hostname, ignore_missing=True, choose_first_if_multiple=True) and
|
||||
not module.params['force']):
|
||||
vmid = proxmox.get_vmid(hostname, choose_first_if_multiple=True)
|
||||
module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, vmid))
|
||||
if not proxmox.get_vm(clone, ignore_missing=True):
|
||||
module.exit_json(changed=False, msg="Container to be cloned does not exist")
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Pre-clone checks of {VZ_TYPE} VM {vmid} failed with exception: {e}".format(VZ_TYPE=VZ_TYPE, vmid=vmid, e=e))
|
||||
|
||||
try:
|
||||
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone)
|
||||
proxmox.create_instance(vmid, node, disk, storage, cpus, memory, swap, timeout, clone)
|
||||
|
||||
module.exit_json(changed=True, msg="Cloned VM %s from %s" % (vmid, clone))
|
||||
except Exception as e:
|
||||
@@ -753,64 +699,60 @@ def main():
|
||||
|
||||
elif state == 'started':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
if not vm:
|
||||
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running':
|
||||
vm = proxmox.get_vm(vmid)
|
||||
if getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running':
|
||||
module.exit_json(changed=False, msg="VM %s is already running" % vmid)
|
||||
|
||||
if start_instance(module, proxmox, vm, vmid, timeout):
|
||||
if proxmox.start_instance(vm, vmid, timeout):
|
||||
module.exit_json(changed=True, msg="VM %s started" % vmid)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="starting of VM %s failed with exception: %s" % (vmid, e))
|
||||
|
||||
elif state == 'stopped':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
if not vm:
|
||||
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||
vm = proxmox.get_vm(vmid)
|
||||
|
||||
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
|
||||
if getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
|
||||
if module.params['force']:
|
||||
if umount_instance(module, proxmox, vm, vmid, timeout):
|
||||
if proxmox.umount_instance(vm, vmid, timeout):
|
||||
module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
|
||||
else:
|
||||
module.exit_json(changed=False, msg=("VM %s is already shutdown, but mounted. "
|
||||
"You can use force option to umount it.") % vmid)
|
||||
|
||||
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped':
|
||||
if getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped':
|
||||
module.exit_json(changed=False, msg="VM %s is already shutdown" % vmid)
|
||||
|
||||
if stop_instance(module, proxmox, vm, vmid, timeout, force=module.params['force']):
|
||||
if proxmox.stop_instance(vm, vmid, timeout, force=module.params['force']):
|
||||
module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="stopping of VM %s failed with exception: %s" % (vmid, e))
|
||||
|
||||
elif state == 'restarted':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
if not vm:
|
||||
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||
if (getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'stopped' or
|
||||
getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted'):
|
||||
vm = proxmox.get_vm(vmid)
|
||||
|
||||
vm_status = getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.current.get()['status']
|
||||
if vm_status in ['stopped', 'mounted']:
|
||||
module.exit_json(changed=False, msg="VM %s is not running" % vmid)
|
||||
|
||||
if (stop_instance(module, proxmox, vm, vmid, timeout, force=module.params['force']) and
|
||||
start_instance(module, proxmox, vm, vmid, timeout)):
|
||||
if (proxmox.stop_instance(vm, vmid, timeout, force=module.params['force']) and
|
||||
proxmox.start_instance(vm, vmid, timeout)):
|
||||
module.exit_json(changed=True, msg="VM %s is restarted" % vmid)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="restarting of VM %s failed with exception: %s" % (vmid, e))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
vm = proxmox.get_vm(vmid, ignore_missing=True)
|
||||
if not vm:
|
||||
module.exit_json(changed=False, msg="VM %s does not exist" % vmid)
|
||||
|
||||
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'running':
|
||||
vm_status = getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.current.get()['status']
|
||||
if vm_status == 'running':
|
||||
module.exit_json(changed=False, msg="VM %s is running. Stop it before deletion." % vmid)
|
||||
|
||||
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
|
||||
if vm_status == 'mounted':
|
||||
module.exit_json(changed=False, msg="VM %s is mounted. Stop it with force option before deletion." % vmid)
|
||||
|
||||
delete_params = {}
|
||||
@@ -818,16 +760,16 @@ def main():
|
||||
if module.params['purge']:
|
||||
delete_params['purge'] = 1
|
||||
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid, **delete_params)
|
||||
taskid = getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE).delete(vmid, **delete_params)
|
||||
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
task_status = proxmox.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
|
||||
if (task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK'):
|
||||
module.exit_json(changed=True, msg="VM %s removed" % vmid)
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for removing VM. Last line in task before timeout: %s'
|
||||
% proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
% proxmox.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
|
||||
@@ -76,7 +76,7 @@ proxmox_domains:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxDomainInfoAnsible(ProxmoxAnsible):
|
||||
@@ -114,9 +114,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxDomainInfoAnsible(module)
|
||||
domain = module.params['domain']
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ proxmox_groups:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxGroupInfoAnsible(ProxmoxAnsible):
|
||||
@@ -124,9 +124,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxGroupInfoAnsible(module)
|
||||
group = module.params['group']
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user