mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-01 19:02:49 +00:00
Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de5f65105d | ||
|
|
607c1ce085 | ||
|
|
ac0c24637a | ||
|
|
d7d7395fa0 | ||
|
|
4689cc307e | ||
|
|
5da7b7d509 | ||
|
|
334f8d3708 | ||
|
|
485b8b9f23 | ||
|
|
865c96e66c | ||
|
|
14498b7ad1 | ||
|
|
76372a8975 | ||
|
|
1fa6d37c0b | ||
|
|
07704d22fa | ||
|
|
9343652086 | ||
|
|
62c026968a | ||
|
|
a15260f85f | ||
|
|
bc13d83a64 | ||
|
|
b9e5db4373 | ||
|
|
2e5d3a0e1f | ||
|
|
dabdc16010 | ||
|
|
bd56572025 | ||
|
|
2b22a3c60b | ||
|
|
4f418ce747 | ||
|
|
fb26dbfa31 | ||
|
|
4bd1bb8c2a | ||
|
|
a742525c89 | ||
|
|
badfef8070 | ||
|
|
33ed69bc93 | ||
|
|
8736ff46f2 | ||
|
|
543a292f4b | ||
|
|
a93ae0c1ae | ||
|
|
b247603961 | ||
|
|
d91ff5c282 | ||
|
|
c66a43aa27 | ||
|
|
30ac14bfc1 | ||
|
|
d9e48f0488 | ||
|
|
80efe01bbf | ||
|
|
7bfdae02e9 | ||
|
|
a552266120 | ||
|
|
2c16874370 | ||
|
|
275961b4ad | ||
|
|
c083b2fa6c | ||
|
|
cc32ee2889 | ||
|
|
e677c46329 | ||
|
|
e90b6d0184 | ||
|
|
c398c6bb96 | ||
|
|
6185f06f64 | ||
|
|
91ba894643 | ||
|
|
5807791c80 | ||
|
|
378c73a2a1 | ||
|
|
a8e60d0358 | ||
|
|
f2d6ac54e9 | ||
|
|
9d7fe2f0ae | ||
|
|
57b3ce9572 | ||
|
|
405435236f | ||
|
|
538a701f89 | ||
|
|
5d3132cfe0 | ||
|
|
41690c84a2 | ||
|
|
5d9f58b69d | ||
|
|
b2f16f184a | ||
|
|
bc61b2d656 | ||
|
|
364e491b7e | ||
|
|
df3898b08c | ||
|
|
aeb672e809 | ||
|
|
3724b36934 | ||
|
|
d9c09095c4 | ||
|
|
43e709b9f2 | ||
|
|
a2042c9b93 | ||
|
|
67bb94ae89 | ||
|
|
4298003ac8 | ||
|
|
1b44e595a3 | ||
|
|
d9f99fdf8d | ||
|
|
542772500b | ||
|
|
b66b26259a | ||
|
|
0567de50d8 | ||
|
|
147ffc6b48 | ||
|
|
4594f7cd18 | ||
|
|
008de3e245 | ||
|
|
943c021446 | ||
|
|
4ef7b7573b | ||
|
|
a0ae0a7c76 | ||
|
|
4408972762 | ||
|
|
dce8b507fd | ||
|
|
40eec12c2c | ||
|
|
cd4a02605e | ||
|
|
0929d24077 | ||
|
|
ac7b95e710 | ||
|
|
cd50836977 | ||
|
|
23cc57c9f6 | ||
|
|
b9119335cd | ||
|
|
e4e33e6824 | ||
|
|
9cb619ff6c | ||
|
|
47e808da51 | ||
|
|
01b25a8236 | ||
|
|
6cf8ce06ca | ||
|
|
31f130a56f | ||
|
|
7ef0705984 | ||
|
|
5e326a25a4 | ||
|
|
6526e0196a | ||
|
|
e757adbfca | ||
|
|
3a2ce4add5 | ||
|
|
3868664046 | ||
|
|
476914013d | ||
|
|
dcfaee08a0 | ||
|
|
78b61cc5cb | ||
|
|
32438bdf80 | ||
|
|
8685d12996 | ||
|
|
f0c18ec730 | ||
|
|
3d4dc21a68 | ||
|
|
49e620cb6a | ||
|
|
e82c2ad80d | ||
|
|
74dfcae673 | ||
|
|
1e01aeacb4 | ||
|
|
3b207ba0fd | ||
|
|
90850b3763 | ||
|
|
5af8d6132d | ||
|
|
bce7efb866 | ||
|
|
edd8981af6 | ||
|
|
c2adcfa51d | ||
|
|
fd72e9b2a3 | ||
|
|
96ed253c79 | ||
|
|
b21e6466c7 | ||
|
|
de8d1760c4 | ||
|
|
a098845a0f | ||
|
|
ff6735f0ce | ||
|
|
657268120c | ||
|
|
e541b5b709 | ||
|
|
9a3fd8fabe | ||
|
|
baf124bc17 | ||
|
|
712bd5194d | ||
|
|
0dfa80d386 | ||
|
|
e1eb88def5 | ||
|
|
bdf0c4e0bf | ||
|
|
0b2f50a3ed | ||
|
|
f9b7938cf6 | ||
|
|
053d0aec28 | ||
|
|
f9baa999a8 | ||
|
|
ab10b6ba36 | ||
|
|
e8a6fabf4c | ||
|
|
5fca1f641b | ||
|
|
88f0a4c770 | ||
|
|
9a565f356c | ||
|
|
3c0a9d7826 | ||
|
|
12cf3dc19a | ||
|
|
6e98e3d3eb | ||
|
|
716a1b924e | ||
|
|
fa4bf56fed | ||
|
|
2c385cfab5 | ||
|
|
e44011ff94 | ||
|
|
25ffe69b51 | ||
|
|
144945894f | ||
|
|
4c85efd807 | ||
|
|
cdadfa979e | ||
|
|
e3a8d238a8 | ||
|
|
93a1aa4e38 | ||
|
|
cf1b02c6b9 | ||
|
|
dda872c3e6 | ||
|
|
d90eb6444a | ||
|
|
93782ffb35 | ||
|
|
dac26d12bd | ||
|
|
22946365fc | ||
|
|
ddc546596e | ||
|
|
a618aa6b0a | ||
|
|
49598ac93a | ||
|
|
de7afabe17 | ||
|
|
fafe6ef87b | ||
|
|
148c133248 | ||
|
|
3bf688be39 | ||
|
|
bc8721c37c | ||
|
|
579bd879c1 | ||
|
|
d83a835a3c | ||
|
|
89e6e6c626 | ||
|
|
510a9228c0 | ||
|
|
8a43df548c | ||
|
|
491ba1b1a3 | ||
|
|
4d6f4c82e2 | ||
|
|
f5ad2cee8d | ||
|
|
5e6a7cab92 | ||
|
|
48c50fa335 | ||
|
|
a1e2ada993 | ||
|
|
d7eb5432f3 | ||
|
|
26f19db2f8 | ||
|
|
8bc0c103ad | ||
|
|
0e495aae75 | ||
|
|
840b1b82ac | ||
|
|
2b08a308bc | ||
|
|
04305e8d9d | ||
|
|
15109a26fd | ||
|
|
d1730adce0 | ||
|
|
69d7cce55c | ||
|
|
d80aca951c | ||
|
|
6f5462fb27 | ||
|
|
1c5c622ae8 | ||
|
|
0b9abdf3de | ||
|
|
f077c1e104 | ||
|
|
ba789d71ec | ||
|
|
897729b507 | ||
|
|
cf8107b628 | ||
|
|
fe922a26f0 | ||
|
|
485a3cc11e | ||
|
|
bdfa91b3df | ||
|
|
0123222ba8 | ||
|
|
3406288644 | ||
|
|
c44fc97d6c | ||
|
|
7901287dd3 | ||
|
|
f2d1099b83 | ||
|
|
438ed7ea0e | ||
|
|
a119ae2833 | ||
|
|
bcf984ec1c | ||
|
|
f28375eeb0 | ||
|
|
682469b9b8 | ||
|
|
3fa1c3ac2c | ||
|
|
0d96b65b4b | ||
|
|
6daf178146 | ||
|
|
f8d0e5448d | ||
|
|
2aa9fc7528 | ||
|
|
0317d506b8 | ||
|
|
274ab506ca | ||
|
|
4bef90fc7e | ||
|
|
438d38ddfe | ||
|
|
8b277cbe61 | ||
|
|
f1e0e590ab | ||
|
|
d65b6edfaf | ||
|
|
e0a86f172f | ||
|
|
81f66feea4 | ||
|
|
124f465819 | ||
|
|
85af92810c | ||
|
|
b8fdfdc644 | ||
|
|
3d6227d1e2 | ||
|
|
423509769d | ||
|
|
cdaf6d9493 | ||
|
|
e1017afe4a | ||
|
|
86dfc731ad | ||
|
|
58037799e4 | ||
|
|
1e5c0d5f42 | ||
|
|
d6a0c914d1 | ||
|
|
f190897687 | ||
|
|
b3995081a2 | ||
|
|
c2083c8034 | ||
|
|
e3e0bf3cfb | ||
|
|
9246704874 | ||
|
|
2394a2dd5b | ||
|
|
c99873bd30 | ||
|
|
9380f9ef7d | ||
|
|
fc2e2db6e4 | ||
|
|
87a213225d | ||
|
|
78a4efae28 | ||
|
|
ccc2fcefdd | ||
|
|
57c523af55 | ||
|
|
e7567e854b | ||
|
|
2dbcfaa650 | ||
|
|
5c5774b7b5 | ||
|
|
402fdcec6a | ||
|
|
302d88b33d | ||
|
|
6ff737dc87 | ||
|
|
b5ea91259b | ||
|
|
5b6bb1776c | ||
|
|
9772fb291c | ||
|
|
6e0c62d3e2 | ||
|
|
a5ae69c701 | ||
|
|
9d8fac08bb | ||
|
|
6dd19450bd | ||
|
|
14432bd760 | ||
|
|
1c31fa1ff3 | ||
|
|
f7aa319704 | ||
|
|
9e91fb0704 | ||
|
|
f8eca8a209 | ||
|
|
6d8ae5d639 | ||
|
|
62e852f421 | ||
|
|
58803e62fe | ||
|
|
5eff6e779a | ||
|
|
e39c887508 | ||
|
|
e660f3e8d3 | ||
|
|
ab7b199af9 | ||
|
|
a34df7dc49 | ||
|
|
953058b518 | ||
|
|
5c7feec6f7 | ||
|
|
eab6f4c6ff | ||
|
|
4999521c11 | ||
|
|
e733b486b8 | ||
|
|
10f1f690e4 | ||
|
|
27377140d0 | ||
|
|
ffa1436f05 | ||
|
|
115f4b5c51 | ||
|
|
d385c47d0b | ||
|
|
e19e69a07e | ||
|
|
1ba0a31328 | ||
|
|
47f3922c51 | ||
|
|
1b6a3efa31 | ||
|
|
4850c3b2b4 | ||
|
|
7bda6f1df7 | ||
|
|
6200dbaedf | ||
|
|
bce01f325a | ||
|
|
ebba59d2ee | ||
|
|
82506a10ba | ||
|
|
5f77312888 | ||
|
|
93cfbaf2a4 | ||
|
|
7773289ceb | ||
|
|
3bd1fa77c2 | ||
|
|
c164c2634c | ||
|
|
0781f49673 | ||
|
|
bd64ddc570 | ||
|
|
0072cb27d4 | ||
|
|
a68a8511c8 | ||
|
|
42181abb51 | ||
|
|
9bf247bb5f | ||
|
|
723665b0d3 | ||
|
|
100cfd1592 | ||
|
|
6b57b2bb74 | ||
|
|
da3874c96d | ||
|
|
1c4556dc4c | ||
|
|
a7ec516be3 | ||
|
|
1d6d8bdf7f | ||
|
|
d7c5b35b32 | ||
|
|
a3e07bd083 | ||
|
|
a4a1f1240e | ||
|
|
ee601d3036 | ||
|
|
3fdb4d4afb | ||
|
|
685bdc0dc7 | ||
|
|
fbe9e5ba3e | ||
|
|
94c04e4a8f | ||
|
|
dc30d33d64 | ||
|
|
ffe55564f0 | ||
|
|
ec87b44816 | ||
|
|
30e2d9f26f | ||
|
|
874b00aebb | ||
|
|
274d7984c7 | ||
|
|
991b3cbb04 | ||
|
|
66112e7c90 | ||
|
|
83a48d9fdc | ||
|
|
9402741c63 | ||
|
|
c44a33e15d | ||
|
|
0b18c977df | ||
|
|
5da06a75aa | ||
|
|
d456d25d6b | ||
|
|
abc2be0bf6 |
@@ -29,6 +29,7 @@ schedules:
|
|||||||
always: true
|
always: true
|
||||||
branches:
|
branches:
|
||||||
include:
|
include:
|
||||||
|
- stable-12
|
||||||
- stable-11
|
- stable-11
|
||||||
- cron: 0 11 * * 0
|
- cron: 0 11 * * 0
|
||||||
displayName: Weekly (old stable branches)
|
displayName: Weekly (old stable branches)
|
||||||
@@ -56,14 +57,14 @@ pool: Standard
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
### Sanity
|
### Sanity
|
||||||
- stage: Sanity_devel
|
- stage: Sanity_2_21
|
||||||
displayName: Sanity devel
|
displayName: Sanity 2.21
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
nameFormat: Test {0}
|
nameFormat: Test {0}
|
||||||
testFormat: devel/sanity/{0}
|
testFormat: 2.21/sanity/{0}
|
||||||
targets:
|
targets:
|
||||||
- test: 1
|
- test: 1
|
||||||
- test: 2
|
- test: 2
|
||||||
@@ -95,28 +96,15 @@ stages:
|
|||||||
- test: 2
|
- test: 2
|
||||||
- test: 3
|
- test: 3
|
||||||
- test: 4
|
- test: 4
|
||||||
- stage: Sanity_2_18
|
|
||||||
displayName: Sanity 2.18
|
|
||||||
dependsOn: []
|
|
||||||
jobs:
|
|
||||||
- template: templates/matrix.yml
|
|
||||||
parameters:
|
|
||||||
nameFormat: Test {0}
|
|
||||||
testFormat: 2.18/sanity/{0}
|
|
||||||
targets:
|
|
||||||
- test: 1
|
|
||||||
- test: 2
|
|
||||||
- test: 3
|
|
||||||
- test: 4
|
|
||||||
### Units
|
### Units
|
||||||
- stage: Units_devel
|
- stage: Units_2_21
|
||||||
displayName: Units devel
|
displayName: Units 2.21
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
nameFormat: Python {0}
|
nameFormat: Python {0}
|
||||||
testFormat: devel/units/{0}/1
|
testFormat: 2.21/units/{0}/1
|
||||||
targets:
|
targets:
|
||||||
- test: 3.9
|
- test: 3.9
|
||||||
- test: '3.10'
|
- test: '3.10'
|
||||||
@@ -148,56 +136,46 @@ stages:
|
|||||||
- test: 3.8
|
- test: 3.8
|
||||||
- test: "3.11"
|
- test: "3.11"
|
||||||
- test: "3.13"
|
- test: "3.13"
|
||||||
- stage: Units_2_18
|
|
||||||
displayName: Units 2.18
|
|
||||||
dependsOn: []
|
|
||||||
jobs:
|
|
||||||
- template: templates/matrix.yml
|
|
||||||
parameters:
|
|
||||||
nameFormat: Python {0}
|
|
||||||
testFormat: 2.18/units/{0}/1
|
|
||||||
targets:
|
|
||||||
- test: 3.8
|
|
||||||
- test: "3.11"
|
|
||||||
- test: "3.13"
|
|
||||||
|
|
||||||
## Remote
|
## Remote
|
||||||
- stage: Remote_devel_extra_vms
|
- stage: Remote_2_21_extra_vms
|
||||||
displayName: Remote devel extra VMs
|
displayName: Remote 2.21 extra VMs
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
testFormat: devel/{0}
|
testFormat: 2.21/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: Alpine 3.22
|
- name: Alpine 3.23
|
||||||
test: alpine/3.22
|
test: alpine/3.23
|
||||||
# - name: Fedora 42
|
# - name: Fedora 43
|
||||||
# test: fedora/42
|
# test: fedora/43
|
||||||
- name: Ubuntu 22.04
|
- name: Ubuntu 22.04
|
||||||
test: ubuntu/22.04
|
test: ubuntu/22.04
|
||||||
- name: Ubuntu 24.04
|
- name: Ubuntu 24.04
|
||||||
test: ubuntu/24.04
|
test: ubuntu/24.04
|
||||||
groups:
|
groups:
|
||||||
- vm
|
- vm
|
||||||
- stage: Remote_devel
|
- stage: Remote_2_21
|
||||||
displayName: Remote devel
|
displayName: Remote 2.21
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
testFormat: devel/{0}
|
testFormat: 2.21/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: macOS 15.3
|
- name: macOS 26.3
|
||||||
test: macos/15.3
|
test: macos/26.3
|
||||||
- name: RHEL 10.0
|
- name: RHEL 10.1
|
||||||
test: rhel/10.0
|
test: rhel/10.1
|
||||||
- name: RHEL 9.6
|
- name: RHEL 9.7
|
||||||
test: rhel/9.6
|
test: rhel/9.7
|
||||||
- name: FreeBSD 14.3
|
# TODO: enable this ASAP!
|
||||||
test: freebsd/14.3
|
# - name: FreeBSD 15.0
|
||||||
- name: FreeBSD 13.5
|
# test: freebsd/15.0
|
||||||
test: freebsd/13.5
|
# TODO: enable this ASAP!
|
||||||
|
# - name: FreeBSD 14.4
|
||||||
|
# test: freebsd/14.4
|
||||||
groups:
|
groups:
|
||||||
- 1
|
- 1
|
||||||
- 2
|
- 2
|
||||||
@@ -210,8 +188,10 @@ stages:
|
|||||||
parameters:
|
parameters:
|
||||||
testFormat: 2.20/{0}
|
testFormat: 2.20/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: RHEL 10.0
|
- name: macOS 15.3
|
||||||
test: rhel/10.0
|
test: macos/15.3
|
||||||
|
- name: RHEL 10.1
|
||||||
|
test: rhel/10.1
|
||||||
- name: FreeBSD 14.3
|
- name: FreeBSD 14.3
|
||||||
test: freebsd/14.3
|
test: freebsd/14.3
|
||||||
groups:
|
groups:
|
||||||
@@ -226,48 +206,28 @@ stages:
|
|||||||
parameters:
|
parameters:
|
||||||
testFormat: 2.19/{0}
|
testFormat: 2.19/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: RHEL 9.5
|
- name: RHEL 10.1
|
||||||
test: rhel/9.5
|
test: rhel/10.1
|
||||||
- name: RHEL 10.0
|
|
||||||
test: rhel/10.0
|
|
||||||
- name: FreeBSD 14.2
|
- name: FreeBSD 14.2
|
||||||
test: freebsd/14.2
|
test: freebsd/14.2
|
||||||
groups:
|
groups:
|
||||||
- 1
|
- 1
|
||||||
- 2
|
- 2
|
||||||
- 3
|
- 3
|
||||||
- stage: Remote_2_18
|
|
||||||
displayName: Remote 2.18
|
|
||||||
dependsOn: []
|
|
||||||
jobs:
|
|
||||||
- template: templates/matrix.yml
|
|
||||||
parameters:
|
|
||||||
testFormat: 2.18/{0}
|
|
||||||
targets:
|
|
||||||
- name: macOS 14.3
|
|
||||||
test: macos/14.3
|
|
||||||
- name: RHEL 9.4
|
|
||||||
test: rhel/9.4
|
|
||||||
- name: FreeBSD 14.1
|
|
||||||
test: freebsd/14.1
|
|
||||||
groups:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
- stage: Docker_devel
|
- stage: Docker_2_21
|
||||||
displayName: Docker devel
|
displayName: Docker 2.21
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
testFormat: devel/linux/{0}
|
testFormat: 2.21/linux/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: Fedora 42
|
- name: Fedora 43
|
||||||
test: fedora42
|
test: fedora43
|
||||||
- name: Alpine 3.22
|
- name: Alpine 3.23
|
||||||
test: alpine322
|
test: alpine323
|
||||||
- name: Ubuntu 22.04
|
- name: Ubuntu 22.04
|
||||||
test: ubuntu2204
|
test: ubuntu2204
|
||||||
- name: Ubuntu 24.04
|
- name: Ubuntu 24.04
|
||||||
@@ -308,33 +268,15 @@ stages:
|
|||||||
- 1
|
- 1
|
||||||
- 2
|
- 2
|
||||||
- 3
|
- 3
|
||||||
- stage: Docker_2_18
|
|
||||||
displayName: Docker 2.18
|
|
||||||
dependsOn: []
|
|
||||||
jobs:
|
|
||||||
- template: templates/matrix.yml
|
|
||||||
parameters:
|
|
||||||
testFormat: 2.18/linux/{0}
|
|
||||||
targets:
|
|
||||||
- name: Fedora 40
|
|
||||||
test: fedora40
|
|
||||||
- name: Alpine 3.20
|
|
||||||
test: alpine320
|
|
||||||
- name: Ubuntu 24.04
|
|
||||||
test: ubuntu2404
|
|
||||||
groups:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
|
|
||||||
### Community Docker
|
### Community Docker
|
||||||
- stage: Docker_community_devel
|
- stage: Docker_community_2_21
|
||||||
displayName: Docker (community images) devel
|
displayName: Docker (community images) 2.21
|
||||||
dependsOn: []
|
dependsOn: []
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/matrix.yml
|
- template: templates/matrix.yml
|
||||||
parameters:
|
parameters:
|
||||||
testFormat: devel/linux-community/{0}
|
testFormat: 2.21/linux-community/{0}
|
||||||
targets:
|
targets:
|
||||||
- name: Debian 11 Bullseye
|
- name: Debian 11 Bullseye
|
||||||
test: debian-bullseye/3.9
|
test: debian-bullseye/3.9
|
||||||
@@ -343,7 +285,7 @@ stages:
|
|||||||
- name: Debian 13 Trixie
|
- name: Debian 13 Trixie
|
||||||
test: debian-13-trixie/3.13
|
test: debian-13-trixie/3.13
|
||||||
- name: ArchLinux
|
- name: ArchLinux
|
||||||
test: archlinux/3.13
|
test: archlinux/3.14
|
||||||
groups:
|
groups:
|
||||||
- 1
|
- 1
|
||||||
- 2
|
- 2
|
||||||
@@ -351,14 +293,14 @@ stages:
|
|||||||
|
|
||||||
### Generic
|
### Generic
|
||||||
# Right now all generic tests are disabled. Uncomment when at least one of them is re-enabled.
|
# Right now all generic tests are disabled. Uncomment when at least one of them is re-enabled.
|
||||||
# - stage: Generic_devel
|
# - stage: Generic_2_21
|
||||||
# displayName: Generic devel
|
# displayName: Generic 2.21
|
||||||
# dependsOn: []
|
# dependsOn: []
|
||||||
# jobs:
|
# jobs:
|
||||||
# - template: templates/matrix.yml
|
# - template: templates/matrix.yml
|
||||||
# parameters:
|
# parameters:
|
||||||
# nameFormat: Python {0}
|
# nameFormat: Python {0}
|
||||||
# testFormat: devel/generic/{0}/1
|
# testFormat: 2.21/generic/{0}/1
|
||||||
# targets:
|
# targets:
|
||||||
# - test: '3.9'
|
# - test: '3.9'
|
||||||
# - test: '3.12'
|
# - test: '3.12'
|
||||||
@@ -384,44 +326,28 @@ stages:
|
|||||||
# testFormat: 2.19/generic/{0}/1
|
# testFormat: 2.19/generic/{0}/1
|
||||||
# targets:
|
# targets:
|
||||||
# - test: '3.9'
|
# - test: '3.9'
|
||||||
# - test: '3.13'
|
|
||||||
# - stage: Generic_2_18
|
|
||||||
# displayName: Generic 2.18
|
|
||||||
# dependsOn: []
|
|
||||||
# jobs:
|
|
||||||
# - template: templates/matrix.yml
|
|
||||||
# parameters:
|
|
||||||
# nameFormat: Python {0}
|
|
||||||
# testFormat: 2.18/generic/{0}/1
|
|
||||||
# targets:
|
|
||||||
# - test: '3.8'
|
|
||||||
# - test: '3.13'
|
# - test: '3.13'
|
||||||
|
|
||||||
- stage: Summary
|
- stage: Summary
|
||||||
condition: succeededOrFailed()
|
condition: succeededOrFailed()
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Sanity_devel
|
- Sanity_2_21
|
||||||
- Sanity_2_20
|
- Sanity_2_20
|
||||||
- Sanity_2_19
|
- Sanity_2_19
|
||||||
- Sanity_2_18
|
- Units_2_21
|
||||||
- Units_devel
|
|
||||||
- Units_2_20
|
- Units_2_20
|
||||||
- Units_2_19
|
- Units_2_19
|
||||||
- Units_2_18
|
- Remote_2_21_extra_vms
|
||||||
- Remote_devel_extra_vms
|
- Remote_2_21
|
||||||
- Remote_devel
|
|
||||||
- Remote_2_20
|
- Remote_2_20
|
||||||
- Remote_2_19
|
- Remote_2_19
|
||||||
- Remote_2_18
|
- Docker_2_21
|
||||||
- Docker_devel
|
|
||||||
- Docker_2_20
|
- Docker_2_20
|
||||||
- Docker_2_19
|
- Docker_2_19
|
||||||
- Docker_2_18
|
- Docker_community_2_21
|
||||||
- Docker_community_devel
|
|
||||||
# Right now all generic tests are disabled. Uncomment when at least one of them is re-enabled.
|
# Right now all generic tests are disabled. Uncomment when at least one of them is re-enabled.
|
||||||
# - Generic_devel
|
# - Generic_2_21
|
||||||
# - Generic_2_20
|
# - Generic_2_20
|
||||||
# - Generic_2_19
|
# - Generic_2_19
|
||||||
# - Generic_2_18
|
|
||||||
jobs:
|
jobs:
|
||||||
- template: templates/coverage.yml
|
- template: templates/coverage.yml
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ Keep in mind that Azure Pipelines does not enforce unique job display names (onl
|
|||||||
It is up to pipeline authors to avoid name collisions when deviating from the recommended format.
|
It is up to pipeline authors to avoid name collisions when deviating from the recommended format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -23,12 +24,12 @@ def main():
|
|||||||
"""Main program entry point."""
|
"""Main program entry point."""
|
||||||
source_directory = sys.argv[1]
|
source_directory = sys.argv[1]
|
||||||
|
|
||||||
if "/ansible_collections/" in os.getcwd():
|
if '/ansible_collections/' in os.getcwd():
|
||||||
output_path = "tests/output"
|
output_path = "tests/output"
|
||||||
else:
|
else:
|
||||||
output_path = "test/results"
|
output_path = "test/results"
|
||||||
|
|
||||||
destination_directory = os.path.join(output_path, "coverage")
|
destination_directory = os.path.join(output_path, 'coverage')
|
||||||
|
|
||||||
if not os.path.exists(destination_directory):
|
if not os.path.exists(destination_directory):
|
||||||
os.makedirs(destination_directory)
|
os.makedirs(destination_directory)
|
||||||
@@ -37,27 +38,27 @@ def main():
|
|||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
for name in os.listdir(source_directory):
|
for name in os.listdir(source_directory):
|
||||||
match = re.search("^Coverage (?P<attempt>[0-9]+) (?P<label>.+)$", name)
|
match = re.search('^Coverage (?P<attempt>[0-9]+) (?P<label>.+)$', name)
|
||||||
label = match.group("label")
|
label = match.group('label')
|
||||||
attempt = int(match.group("attempt"))
|
attempt = int(match.group('attempt'))
|
||||||
jobs[label] = max(attempt, jobs.get(label, 0))
|
jobs[label] = max(attempt, jobs.get(label, 0))
|
||||||
|
|
||||||
for label, attempt in jobs.items():
|
for label, attempt in jobs.items():
|
||||||
name = "Coverage {attempt} {label}".format(label=label, attempt=attempt)
|
name = 'Coverage {attempt} {label}'.format(label=label, attempt=attempt)
|
||||||
source = os.path.join(source_directory, name)
|
source = os.path.join(source_directory, name)
|
||||||
source_files = os.listdir(source)
|
source_files = os.listdir(source)
|
||||||
|
|
||||||
for source_file in source_files:
|
for source_file in source_files:
|
||||||
source_path = os.path.join(source, source_file)
|
source_path = os.path.join(source, source_file)
|
||||||
destination_path = os.path.join(destination_directory, source_file + "." + label)
|
destination_path = os.path.join(destination_directory, source_file + '.' + label)
|
||||||
print('"%s" -> "%s"' % (source_path, destination_path))
|
print('"%s" -> "%s"' % (source_path, destination_path))
|
||||||
shutil.copyfile(source_path, destination_path)
|
shutil.copyfile(source_path, destination_path)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
print("Coverage file count: %d" % count)
|
print('Coverage file count: %d' % count)
|
||||||
print("##vso[task.setVariable variable=coverageFileCount]%d" % count)
|
print('##vso[task.setVariable variable=coverageFileCount]%d' % count)
|
||||||
print("##vso[task.setVariable variable=outputPath]%s" % output_path)
|
print('##vso[task.setVariable variable=outputPath]%s' % output_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class Args:
|
|||||||
|
|
||||||
def parse_args() -> Args:
|
def parse_args() -> Args:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("-n", "--dry-run", action="store_true")
|
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||||
parser.add_argument("path", type=pathlib.Path)
|
parser.add_argument('path', type=pathlib.Path)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -48,14 +48,12 @@ def parse_args() -> Args:
|
|||||||
|
|
||||||
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
||||||
processed = []
|
processed = []
|
||||||
for file in directory.joinpath("reports").glob("coverage*.xml"):
|
for file in directory.joinpath('reports').glob('coverage*.xml'):
|
||||||
name = file.stem.replace("coverage=", "")
|
name = file.stem.replace('coverage=', '')
|
||||||
|
|
||||||
# Get flags from name
|
# Get flags from name
|
||||||
flags = name.replace("-powershell", "").split("=") # Drop '-powershell' suffix
|
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
|
||||||
flags = [
|
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
|
||||||
flag if not flag.startswith("stub") else flag.split("-")[0] for flag in flags
|
|
||||||
] # Remove "-01" from stub files
|
|
||||||
|
|
||||||
processed.append(CoverageFile(name, file, flags))
|
processed.append(CoverageFile(name, file, flags))
|
||||||
|
|
||||||
@@ -66,16 +64,14 @@ def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], d
|
|||||||
for file in files:
|
for file in files:
|
||||||
cmd = [
|
cmd = [
|
||||||
str(codecov_bin),
|
str(codecov_bin),
|
||||||
"--name",
|
'--name', file.name,
|
||||||
file.name,
|
'--file', str(file.path),
|
||||||
"--file",
|
|
||||||
str(file.path),
|
|
||||||
]
|
]
|
||||||
for flag in file.flags:
|
for flag in file.flags:
|
||||||
cmd.extend(["--flags", flag])
|
cmd.extend(['--flags', flag])
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(f"DRY-RUN: Would run command: {cmd}")
|
print(f'DRY-RUN: Would run command: {cmd}')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
@@ -83,11 +79,11 @@ def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], d
|
|||||||
|
|
||||||
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
||||||
if dry_run:
|
if dry_run:
|
||||||
print(f"DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}")
|
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
|
||||||
return
|
return
|
||||||
|
|
||||||
with urllib.request.urlopen(url) as resp:
|
with urllib.request.urlopen(url) as resp:
|
||||||
with dest.open("w+b") as f:
|
with dest.open('w+b') as f:
|
||||||
# Read data in chunks rather than all at once
|
# Read data in chunks rather than all at once
|
||||||
shutil.copyfileobj(resp, f, 64 * 1024)
|
shutil.copyfileobj(resp, f, 64 * 1024)
|
||||||
|
|
||||||
@@ -96,14 +92,14 @@ def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = Fals
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
url = "https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov"
|
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
|
||||||
with tempfile.TemporaryDirectory(prefix="codecov-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
|
||||||
codecov_bin = pathlib.Path(tmpdir) / "codecov"
|
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
|
||||||
download_file(url, codecov_bin, 0o755, args.dry_run)
|
download_file(url, codecov_bin, 0o755, args.dry_run)
|
||||||
|
|
||||||
files = process_files(args.path)
|
files = process_files(args.path)
|
||||||
upload_files(codecov_bin, files, args.dry_run)
|
upload_files(codecov_bin, files, args.dry_run)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
"""Prepends a relative timestamp to each input line from stdin and writes it to stdout."""
|
"""Prepends a relative timestamp to each input line from stdin and writes it to stdout."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -15,14 +16,14 @@ def main():
|
|||||||
"""Main program entry point."""
|
"""Main program entry point."""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
sys.stdin.reconfigure(errors="surrogateescape")
|
sys.stdin.reconfigure(errors='surrogateescape')
|
||||||
sys.stdout.reconfigure(errors="surrogateescape")
|
sys.stdout.reconfigure(errors='surrogateescape')
|
||||||
|
|
||||||
for line in sys.stdin:
|
for line in sys.stdin:
|
||||||
seconds = time.time() - start
|
seconds = time.time() - start
|
||||||
sys.stdout.write("%02d:%02d %s" % (seconds // 60, seconds % 60, line))
|
sys.stdout.write('%02d:%02d %s' % (seconds // 60, seconds % 60, line))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "community.general devcontainer",
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:3.14-bookworm",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
|
||||||
},
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"settings": {
|
|
||||||
"terminal.integrated.shell.linux": "/bin/bash",
|
|
||||||
"python.pythonPath": "/usr/local/bin/python",
|
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"files.autoSave": "afterDelay",
|
|
||||||
"files.eol": "\n",
|
|
||||||
"files.insertFinalNewline": true,
|
|
||||||
"files.trimFinalNewlines": true,
|
|
||||||
"files.trimTrailingWhitespace": true
|
|
||||||
},
|
|
||||||
"extensions": [
|
|
||||||
"charliermarsh.ruff",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"redhat.ansible",
|
|
||||||
"redhat.vscode-yaml",
|
|
||||||
"trond-snekvik.simple-rst",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"remoteUser": "vscode",
|
|
||||||
"postCreateCommand": ".devcontainer/setup.sh",
|
|
||||||
"workspaceFolder": "/workspace/ansible_collections/community/general",
|
|
||||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace/ansible_collections/community/general,type=bind"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https: //www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
SPDX-FileCopyrightText: 2025 Alexei Znamensky <russoz@gmail.com>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Copyright (c) Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
sudo chown -R vscode:vscode /workspace/
|
|
||||||
|
|
||||||
pip install -U pip
|
|
||||||
pip install -r .devcontainer/requirements-dev.txt
|
|
||||||
pip install -r tests/unit/requirements.txt
|
|
||||||
|
|
||||||
export ANSIBLE_COLLECTIONS_PATH=/workspace:${ANSIBLE_COLLECTIONS_PATH}
|
|
||||||
ansible-galaxy collection install -v -r tests/unit/requirements.yml
|
|
||||||
ansible-galaxy collection install -v -r tests/integration/requirements.yml
|
|
||||||
|
|
||||||
pre-commit install
|
|
||||||
@@ -7,7 +7,3 @@ d032de3b16eed11ea3a31cd3d96d78f7c46a2ee0
|
|||||||
e8f965fbf8154ea177c6622da149f2ae8533bd3c
|
e8f965fbf8154ea177c6622da149f2ae8533bd3c
|
||||||
e938ca5f20651abc160ee6aba10014013d04dcc1
|
e938ca5f20651abc160ee6aba10014013d04dcc1
|
||||||
eaa5e07b2866e05b6c7b5628ca92e9cb1142d008
|
eaa5e07b2866e05b6c7b5628ca92e9cb1142d008
|
||||||
|
|
||||||
# Code reformatting
|
|
||||||
340ff8586d4f1cb6a0f3c934eb42589bcc29c0ea
|
|
||||||
e530d2906a1f61df89861286ac57c951a247f32c
|
|
||||||
|
|||||||
37
.github/BOTMETA.yml
vendored
37
.github/BOTMETA.yml
vendored
@@ -99,6 +99,7 @@ files:
|
|||||||
$callbacks/unixy.py:
|
$callbacks/unixy.py:
|
||||||
labels: unixy
|
labels: unixy
|
||||||
maintainers: akatch
|
maintainers: akatch
|
||||||
|
$callbacks/yaml.py: {}
|
||||||
$connections/:
|
$connections/:
|
||||||
labels: connections
|
labels: connections
|
||||||
$connections/chroot.py: {}
|
$connections/chroot.py: {}
|
||||||
@@ -238,9 +239,6 @@ files:
|
|||||||
maintainers: vbotka
|
maintainers: vbotka
|
||||||
$inventories/icinga2.py:
|
$inventories/icinga2.py:
|
||||||
maintainers: BongoEADGC6
|
maintainers: BongoEADGC6
|
||||||
$inventories/incus.py:
|
|
||||||
labels: incus
|
|
||||||
maintainers: stgraber
|
|
||||||
$inventories/linode.py:
|
$inventories/linode.py:
|
||||||
keywords: linode dynamic inventory script
|
keywords: linode dynamic inventory script
|
||||||
labels: cloud linode
|
labels: cloud linode
|
||||||
@@ -368,8 +366,6 @@ files:
|
|||||||
$module_utils/jenkins.py:
|
$module_utils/jenkins.py:
|
||||||
labels: jenkins
|
labels: jenkins
|
||||||
maintainers: russoz
|
maintainers: russoz
|
||||||
$module_utils/_lxc.py:
|
|
||||||
maintainers: russoz
|
|
||||||
$module_utils/manageiq.py:
|
$module_utils/manageiq.py:
|
||||||
labels: manageiq
|
labels: manageiq
|
||||||
maintainers: $team_manageiq
|
maintainers: $team_manageiq
|
||||||
@@ -398,6 +394,9 @@ files:
|
|||||||
$module_utils/puppet.py:
|
$module_utils/puppet.py:
|
||||||
labels: puppet
|
labels: puppet
|
||||||
maintainers: russoz
|
maintainers: russoz
|
||||||
|
$module_utils/pure.py:
|
||||||
|
labels: pure pure_storage
|
||||||
|
maintainers: $team_purestorage
|
||||||
$module_utils/redfish_utils.py:
|
$module_utils/redfish_utils.py:
|
||||||
labels: redfish_utils
|
labels: redfish_utils
|
||||||
maintainers: $team_redfish
|
maintainers: $team_redfish
|
||||||
@@ -481,6 +480,8 @@ files:
|
|||||||
keywords: beadm dladm illumos ipadm nexenta omnios openindiana pfexec smartos solaris sunos zfs zpool
|
keywords: beadm dladm illumos ipadm nexenta omnios openindiana pfexec smartos solaris sunos zfs zpool
|
||||||
labels: beadm solaris
|
labels: beadm solaris
|
||||||
maintainers: $team_solaris
|
maintainers: $team_solaris
|
||||||
|
$modules/bearychat.py:
|
||||||
|
maintainers: tonyseek
|
||||||
$modules/bigpanda.py:
|
$modules/bigpanda.py:
|
||||||
ignore: hkariti
|
ignore: hkariti
|
||||||
$modules/bitbucket_:
|
$modules/bitbucket_:
|
||||||
@@ -586,6 +587,9 @@ files:
|
|||||||
$modules/etcd3.py:
|
$modules/etcd3.py:
|
||||||
ignore: vfauth
|
ignore: vfauth
|
||||||
maintainers: evrardjp
|
maintainers: evrardjp
|
||||||
|
$modules/facter.py:
|
||||||
|
labels: facter
|
||||||
|
maintainers: $team_ansible_core gamethis
|
||||||
$modules/facter_facts.py:
|
$modules/facter_facts.py:
|
||||||
labels: facter
|
labels: facter
|
||||||
maintainers: russoz $team_ansible_core gamethis
|
maintainers: russoz $team_ansible_core gamethis
|
||||||
@@ -594,8 +598,6 @@ files:
|
|||||||
$modules/filesystem.py:
|
$modules/filesystem.py:
|
||||||
labels: filesystem
|
labels: filesystem
|
||||||
maintainers: pilou- abulimov quidame
|
maintainers: pilou- abulimov quidame
|
||||||
$modules/file_remove.py:
|
|
||||||
maintainers: shahargolshani
|
|
||||||
$modules/flatpak.py:
|
$modules/flatpak.py:
|
||||||
maintainers: $team_flatpak
|
maintainers: $team_flatpak
|
||||||
$modules/flatpak_remote.py:
|
$modules/flatpak_remote.py:
|
||||||
@@ -747,8 +749,6 @@ files:
|
|||||||
maintainers: obourdon hryamzik
|
maintainers: obourdon hryamzik
|
||||||
$modules/ip_netns.py:
|
$modules/ip_netns.py:
|
||||||
maintainers: bregman-arie
|
maintainers: bregman-arie
|
||||||
$modules/ip2location_info.py:
|
|
||||||
maintainers: ip2location
|
|
||||||
$modules/ipa_:
|
$modules/ipa_:
|
||||||
maintainers: $team_ipa
|
maintainers: $team_ipa
|
||||||
ignore: fxfitz
|
ignore: fxfitz
|
||||||
@@ -813,8 +813,6 @@ files:
|
|||||||
maintainers: Slezhuk pertoft
|
maintainers: Slezhuk pertoft
|
||||||
$modules/kdeconfig.py:
|
$modules/kdeconfig.py:
|
||||||
maintainers: smeso
|
maintainers: smeso
|
||||||
$modules/kea_command.py:
|
|
||||||
maintainers: mirabilos
|
|
||||||
$modules/kernel_blacklist.py:
|
$modules/kernel_blacklist.py:
|
||||||
maintainers: matze
|
maintainers: matze
|
||||||
$modules/keycloak_:
|
$modules/keycloak_:
|
||||||
@@ -869,8 +867,6 @@ files:
|
|||||||
maintainers: bratwurzt
|
maintainers: bratwurzt
|
||||||
$modules/keycloak_realm_rolemapping.py:
|
$modules/keycloak_realm_rolemapping.py:
|
||||||
maintainers: agross mhuysamen Gaetan2907
|
maintainers: agross mhuysamen Gaetan2907
|
||||||
$modules/keycloak_user_execute_actions_email.py:
|
|
||||||
maintainers: mariusbertram
|
|
||||||
$modules/keyring.py:
|
$modules/keyring.py:
|
||||||
maintainers: ahussey-redhat
|
maintainers: ahussey-redhat
|
||||||
$modules/keyring_info.py:
|
$modules/keyring_info.py:
|
||||||
@@ -935,10 +931,6 @@ files:
|
|||||||
maintainers: conloos
|
maintainers: conloos
|
||||||
$modules/lxd_project.py:
|
$modules/lxd_project.py:
|
||||||
maintainers: we10710aa
|
maintainers: we10710aa
|
||||||
$modules/lxd_storage_pool_info.py:
|
|
||||||
maintainers: smcavoy
|
|
||||||
$modules/lxd_storage_volume_info.py:
|
|
||||||
maintainers: smcavoy
|
|
||||||
$modules/macports.py:
|
$modules/macports.py:
|
||||||
ignore: ryansb
|
ignore: ryansb
|
||||||
keywords: brew cask darwin homebrew macosx macports osx
|
keywords: brew cask darwin homebrew macosx macports osx
|
||||||
@@ -1504,9 +1496,7 @@ files:
|
|||||||
maintainers: vbotka
|
maintainers: vbotka
|
||||||
$tests/fqdn_valid.py:
|
$tests/fqdn_valid.py:
|
||||||
maintainers: vbotka
|
maintainers: vbotka
|
||||||
$modules/sssd_info.py:
|
#########################
|
||||||
maintainers: a-gabidullin
|
|
||||||
#########################
|
|
||||||
docs/docsite/rst/filter_guide.rst: {}
|
docs/docsite/rst/filter_guide.rst: {}
|
||||||
docs/docsite/rst/filter_guide_abstract_informations.rst: {}
|
docs/docsite/rst/filter_guide_abstract_informations.rst: {}
|
||||||
docs/docsite/rst/filter_guide_abstract_informations_counting_elements_in_sequence.rst:
|
docs/docsite/rst/filter_guide_abstract_informations_counting_elements_in_sequence.rst:
|
||||||
@@ -1575,7 +1565,7 @@ files:
|
|||||||
maintainers: russoz
|
maintainers: russoz
|
||||||
docs/docsite/rst/test_guide.rst:
|
docs/docsite/rst/test_guide.rst:
|
||||||
maintainers: felixfontein
|
maintainers: felixfontein
|
||||||
#########################
|
#########################
|
||||||
tests/:
|
tests/:
|
||||||
labels: tests
|
labels: tests
|
||||||
tests/integration:
|
tests/integration:
|
||||||
@@ -1602,7 +1592,7 @@ macros:
|
|||||||
plugin_utils: plugins/plugin_utils
|
plugin_utils: plugins/plugin_utils
|
||||||
tests: plugins/test
|
tests: plugins/test
|
||||||
team_ansible_core:
|
team_ansible_core:
|
||||||
team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross
|
team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister molekuul ramooncamacho wtcross
|
||||||
team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo
|
team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo
|
||||||
team_consul: sgargan apollo13 Ilgmi
|
team_consul: sgargan apollo13 Ilgmi
|
||||||
team_cyberark_conjur: jvanderhoof ryanprior
|
team_cyberark_conjur: jvanderhoof ryanprior
|
||||||
@@ -1620,10 +1610,11 @@ macros:
|
|||||||
team_networking: NilashishC Qalthos danielmellado ganeshrn justjais trishnaguha sganesh-infoblox privateip
|
team_networking: NilashishC Qalthos danielmellado ganeshrn justjais trishnaguha sganesh-infoblox privateip
|
||||||
team_opennebula: ilicmilan meerkampdvv rsmontero xorel nilsding
|
team_opennebula: ilicmilan meerkampdvv rsmontero xorel nilsding
|
||||||
team_oracle: manojmeda mross22 nalsaber
|
team_oracle: manojmeda mross22 nalsaber
|
||||||
|
team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16
|
||||||
team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06 jyundt
|
team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06 jyundt
|
||||||
team_rhsm: cnsnyder ptoscano
|
team_rhsm: cnsnyder ptoscano
|
||||||
team_scaleway: remyleone abarbare
|
team_scaleway: remyleone abarbare
|
||||||
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l
|
team_solaris: bcoca fishman jasperla jpdasma scathatheworm troy2914 xen0l
|
||||||
team_suse: commel evrardjp lrupp AnderEnder alxgu andytom sealor
|
team_suse: commel evrardjp lrupp AnderEnder alxgu andytom sealor
|
||||||
team_virt: joshainglis karmab Thulium-Drake Ajpantuso
|
team_virt: joshainglis karmab Thulium-Drake Ajpantuso
|
||||||
team_wdc: mikemoerk
|
team_wdc: mikemoerk
|
||||||
|
|||||||
122
.github/workflows/ansible-test.yml
vendored
122
.github/workflows/ansible-test.yml
vendored
@@ -29,7 +29,9 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ansible:
|
ansible:
|
||||||
|
- '2.16'
|
||||||
- '2.17'
|
- '2.17'
|
||||||
|
- '2.18'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Perform sanity testing
|
- name: Perform sanity testing
|
||||||
@@ -57,12 +59,24 @@ jobs:
|
|||||||
exclude:
|
exclude:
|
||||||
- ansible: ''
|
- ansible: ''
|
||||||
include:
|
include:
|
||||||
|
- ansible: '2.16'
|
||||||
|
python: '2.7'
|
||||||
|
- ansible: '2.16'
|
||||||
|
python: '3.6'
|
||||||
|
- ansible: '2.16'
|
||||||
|
python: '3.11'
|
||||||
- ansible: '2.17'
|
- ansible: '2.17'
|
||||||
python: '3.7'
|
python: '3.7'
|
||||||
- ansible: '2.17'
|
- ansible: '2.17'
|
||||||
python: '3.10'
|
python: '3.10'
|
||||||
- ansible: '2.17'
|
- ansible: '2.17'
|
||||||
python: '3.12'
|
python: '3.12'
|
||||||
|
- ansible: '2.18'
|
||||||
|
python: '3.8'
|
||||||
|
- ansible: '2.18'
|
||||||
|
python: '3.11'
|
||||||
|
- ansible: '2.18'
|
||||||
|
python: '3.13'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: >-
|
- name: >-
|
||||||
@@ -98,6 +112,44 @@ jobs:
|
|||||||
exclude:
|
exclude:
|
||||||
- ansible: ''
|
- ansible: ''
|
||||||
include:
|
include:
|
||||||
|
# 2.16
|
||||||
|
# CentOS 7 does not work in GHA, that's why it's not listed here.
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: fedora38
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: fedora38
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: fedora38
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: opensuse15
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: opensuse15
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: opensuse15
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.16'
|
||||||
|
docker: alpine3
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
# 2.17
|
# 2.17
|
||||||
- ansible: '2.17'
|
- ansible: '2.17'
|
||||||
docker: fedora39
|
docker: fedora39
|
||||||
@@ -111,18 +163,6 @@ jobs:
|
|||||||
docker: fedora39
|
docker: fedora39
|
||||||
python: ''
|
python: ''
|
||||||
target: azp/posix/3/
|
target: azp/posix/3/
|
||||||
- ansible: '2.17'
|
|
||||||
docker: ubuntu2004
|
|
||||||
python: ''
|
|
||||||
target: azp/posix/1/
|
|
||||||
- ansible: '2.17'
|
|
||||||
docker: ubuntu2004
|
|
||||||
python: ''
|
|
||||||
target: azp/posix/2/
|
|
||||||
- ansible: '2.17'
|
|
||||||
docker: ubuntu2004
|
|
||||||
python: ''
|
|
||||||
target: azp/posix/3/
|
|
||||||
- ansible: '2.17'
|
- ansible: '2.17'
|
||||||
docker: alpine319
|
docker: alpine319
|
||||||
python: ''
|
python: ''
|
||||||
@@ -135,15 +175,55 @@ jobs:
|
|||||||
docker: alpine319
|
docker: alpine319
|
||||||
python: ''
|
python: ''
|
||||||
target: azp/posix/3/
|
target: azp/posix/3/
|
||||||
# Right now all generic tests are disabled. Uncomment when at least one of them is re-enabled.
|
- ansible: '2.17'
|
||||||
# - ansible: '2.17'
|
docker: ubuntu2004
|
||||||
# docker: default
|
python: ''
|
||||||
# python: '3.7'
|
target: azp/posix/1/
|
||||||
# target: azp/generic/1/
|
- ansible: '2.17'
|
||||||
# - ansible: '2.17'
|
docker: ubuntu2004
|
||||||
# docker: default
|
python: ''
|
||||||
# python: '3.12'
|
target: azp/posix/2/
|
||||||
# target: azp/generic/1/
|
- ansible: '2.17'
|
||||||
|
docker: ubuntu2004
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
# 2.18
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: fedora40
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: fedora40
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: fedora40
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: ubuntu2404
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: ubuntu2404
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: ubuntu2404
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: alpine320
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/1/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: alpine320
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/2/
|
||||||
|
- ansible: '2.18'
|
||||||
|
docker: alpine320
|
||||||
|
python: ''
|
||||||
|
target: azp/posix/3/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: >-
|
- name: >-
|
||||||
|
|||||||
34
.github/workflows/docs.yml
vendored
34
.github/workflows/docs.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
# Copyright (c) Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: nox
|
|
||||||
'on':
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- stable-*
|
|
||||||
paths:
|
|
||||||
- docs/**
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- docs/**
|
|
||||||
# Run CI once per day (at 08:00 UTC)
|
|
||||||
schedule:
|
|
||||||
- cron: '0 8 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nox:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Validate generated Ansible output"
|
|
||||||
steps:
|
|
||||||
- name: Check out collection
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- name: Run nox
|
|
||||||
uses: ansible-community/antsibull-nox@main
|
|
||||||
with:
|
|
||||||
sessions: ansible-output
|
|
||||||
216
.mypy.ini
216
.mypy.ini
@@ -1,216 +0,0 @@
|
|||||||
# Copyright (c) Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
[mypy]
|
|
||||||
# check_untyped_defs = True
|
|
||||||
# disallow_untyped_defs = True
|
|
||||||
|
|
||||||
# strict = True -- only try to enable once everything (including dependencies!) is typed
|
|
||||||
strict_equality = True
|
|
||||||
strict_bytes = True
|
|
||||||
|
|
||||||
warn_redundant_casts = True
|
|
||||||
# warn_return_any = True
|
|
||||||
warn_unreachable = True
|
|
||||||
|
|
||||||
[mypy-ansible.*]
|
|
||||||
# ansible-core has partial typing information
|
|
||||||
follow_untyped_imports = True
|
|
||||||
|
|
||||||
# The following imports are Python packages that:
|
|
||||||
# 1. We do not install (we can't install everything!);
|
|
||||||
# 2. That have type stubs, but we don't install them (again, we can't install everything!); or
|
|
||||||
# 3. That have no types and type stubs.
|
|
||||||
[mypy-aerospike.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-boto3.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-bs4.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-cgi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-chef.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-consul.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-credstash.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-crypt.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-datadog.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-dbus.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-delinea.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-dnf.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-dnsimple.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-etcd3.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-flatdict.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-footmark.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-fqdn.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-func.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-gi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-github3.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-hashids.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-heroku3.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-hpe3parclient.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-hpe3par_sdk.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-hpilo.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-hpOneView.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-httmock.*] # TODO!
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-influxdb.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-jc.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-jenkins.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-jmespath.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-jsonpatch.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-kazoo.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-keyring.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-keystoneauth1.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-layman.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-ldap.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-legacycrypt.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-libcloud.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-linode.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-linode_api4.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-lmdb.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-logdna.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-logstash.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-lxc.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-manageiq_client.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-matrix_client.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-memcache.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-nc_dnsapi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-nomad.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-oci.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-oneandone.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-opentelemetry.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-ovh.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-ovirtsdk.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-packet.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-paho.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pam.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pdpyras.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-petname.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pingdom.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-portage.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-potatoes_that_will_never_be_there.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-prettytable.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pubnub_blocks_client.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pushbullet.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pycdlib.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pyghmi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pylxca.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pymssql.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pyodbc.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pyone.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pypureomapi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pysnmp.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-pyxcli.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-rpm.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-salt.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-selinux.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-semantic_version.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-sendgrid.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-seobject.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-sha.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-SoftLayer.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-spotinst_sdk.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-statsd.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-storops.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-taiga.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-thycotic.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-univention.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-vexatapi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-websocket.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-XenAPI.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-xkcdpass.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-xmljson.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-xmltodict.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
[mypy-xmpp.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# SPDX-FileCopyrightText: 2025 Alexei Znamensky <russoz@gmail.com>
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
# Ruff version.
|
|
||||||
rev: v0.14.10
|
|
||||||
hooks:
|
|
||||||
# Run the linter.
|
|
||||||
- id: ruff-check
|
|
||||||
# Run the formatter.
|
|
||||||
- id: ruff-format
|
|
||||||
1854
CHANGELOG.md
1854
CHANGELOG.md
File diff suppressed because one or more lines are too long
1750
CHANGELOG.rst
1750
CHANGELOG.rst
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ Please read our ['Contributing to collections'](https://docs.ansible.com/project
|
|||||||
* Make sure your PR includes a [changelog fragment](https://docs.ansible.com/projects/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment).
|
* Make sure your PR includes a [changelog fragment](https://docs.ansible.com/projects/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment).
|
||||||
* You must not include a fragment for new modules or new 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 :) )
|
* You must not include a fragment for new modules or new 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 :) )
|
||||||
* Please always include a link to the pull request itself, and if the PR is about an issue, also a link to the issue. Also make sure the fragment ends with a period, and begins with a lower-case letter after `-`. (Again, if you don't do this, we'll add suggestions to fix it, so don't worry too much :) )
|
* Please always include a link to the pull request itself, and if the PR is about an issue, also a link to the issue. Also make sure the fragment ends with a period, and begins with a lower-case letter after `-`. (Again, if you don't do this, we'll add suggestions to fix it, so don't worry too much :) )
|
||||||
* Note that we format the code with `ruff format`. If your change does not match the formatters expectations, CI will fail and your PR will not get merged. See below for how to format code with antsibull-nox.
|
* 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.
|
||||||
|
|
||||||
You can also read the Ansible community's [Quick-start development guide](https://docs.ansible.com/projects/ansible/devel/community/create_pr_quick_start.html).
|
You can also read the Ansible community's [Quick-start development guide](https://docs.ansible.com/projects/ansible/devel/community/create_pr_quick_start.html).
|
||||||
|
|
||||||
@@ -49,24 +49,11 @@ If you want to test a PR locally, refer to [our testing guide](https://docs.ansi
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## Format code; and run sanity or unit tests locally (with antsibull-nox)
|
## Run sanity or unit locally (with antsibull-nox)
|
||||||
|
|
||||||
The easiest way to format the code, and to run sanity and unit tests locally is to use [antsibull-nox](https://docs.ansible.com/projects/antsibull-nox/).
|
The easiest way to run sanity and unit tests locally is to use [antsibull-nox](https://docs.ansible.com/projects/antsibull-nox/).
|
||||||
(If you have [nox](https://nox.thea.codes/en/stable/) installed, it will automatically install antsibull-nox in a virtual environment for you.)
|
(If you have [nox](https://nox.thea.codes/en/stable/) installed, it will automatically install antsibull-nox in a virtual environment for you.)
|
||||||
|
|
||||||
### Format code
|
|
||||||
|
|
||||||
The following commands show how to run ansible-test sanity tests:
|
|
||||||
|
|
||||||
```.bash
|
|
||||||
# Run all configured formatters:
|
|
||||||
nox -Re formatters
|
|
||||||
|
|
||||||
# If you notice discrepancies between your local formatter and CI, you might
|
|
||||||
# need to re-generate the virtual environment:
|
|
||||||
nox -e formatters
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sanity tests
|
### Sanity tests
|
||||||
|
|
||||||
The following commands show how to run ansible-test sanity tests:
|
The following commands show how to run ansible-test sanity tests:
|
||||||
@@ -133,7 +120,6 @@ ansible-test sanity --docker -v plugins/modules/system/pids.py tests/integration
|
|||||||
Note that for running unit tests, you need to install required collections in the same folder structure that `community.general` is checked out in.
|
Note that for running unit tests, you need to install required collections in the same folder structure that `community.general` is checked out in.
|
||||||
Right now, you need to install [`community.internal_test_tools`](https://github.com/ansible-collections/community.internal_test_tools).
|
Right now, you need to install [`community.internal_test_tools`](https://github.com/ansible-collections/community.internal_test_tools).
|
||||||
If you want to use the latest version from GitHub, you can run:
|
If you want to use the latest version from GitHub, you can run:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/ansible-collections/community.internal_test_tools.git ~/dev/ansible_collections/community/internal_test_tools
|
git clone https://github.com/ansible-collections/community.internal_test_tools.git ~/dev/ansible_collections/community/internal_test_tools
|
||||||
```
|
```
|
||||||
@@ -156,7 +142,6 @@ ansible-test units --docker -v --python 3.8 tests/unit/plugins/modules/net_tools
|
|||||||
Note that for running integration tests, you need to install required collections in the same folder structure that `community.general` is checked out in.
|
Note that for running integration tests, you need to install required collections in the same folder structure that `community.general` is checked out in.
|
||||||
Right now, depending on the test, you need to install [`ansible.posix`](https://github.com/ansible-collections/ansible.posix), [`community.crypto`](https://github.com/ansible-collections/community.crypto), and [`community.docker`](https://github.com/ansible-collections/community.docker):
|
Right now, depending on the test, you need to install [`ansible.posix`](https://github.com/ansible-collections/ansible.posix), [`community.crypto`](https://github.com/ansible-collections/community.crypto), and [`community.docker`](https://github.com/ansible-collections/community.docker):
|
||||||
If you want to use the latest versions from GitHub, you can run:
|
If you want to use the latest versions from GitHub, you can run:
|
||||||
|
|
||||||
```
|
```
|
||||||
mkdir -p ~/dev/ansible_collections/ansible
|
mkdir -p ~/dev/ansible_collections/ansible
|
||||||
git clone https://github.com/ansible-collections/ansible.posix.git ~/dev/ansible_collections/ansible/posix
|
git clone https://github.com/ansible-collections/ansible.posix.git ~/dev/ansible_collections/ansible/posix
|
||||||
@@ -169,13 +154,11 @@ The following commands show how to run integration tests:
|
|||||||
#### In Docker
|
#### In Docker
|
||||||
|
|
||||||
Integration tests on Docker have the following parameters:
|
Integration tests on Docker have the following parameters:
|
||||||
|
|
||||||
- `image_name` (required): The name of the Docker image. To get the list of supported Docker images, run
|
- `image_name` (required): The name of the Docker image. To get the list of supported Docker images, run
|
||||||
`ansible-test integration --help` and look for _target docker images_.
|
`ansible-test integration --help` and look for _target docker images_.
|
||||||
- `test_name` (optional): The name of the integration test.
|
- `test_name` (optional): The name of the integration test.
|
||||||
For modules, this equals the short name of the module; for example, `pacman` in case of `community.general.pacman`.
|
For modules, this equals the short name of the module; for example, `pacman` in case of `community.general.pacman`.
|
||||||
For plugins, the plugin type is added before the plugin's short name, for example `callback_yaml` for the `community.general.yaml` callback.
|
For plugins, the plugin type is added before the plugin's short name, for example `callback_yaml` for the `community.general.yaml` callback.
|
||||||
|
|
||||||
```.bash
|
```.bash
|
||||||
# Test all plugins/modules on fedora40
|
# Test all plugins/modules on fedora40
|
||||||
ansible-test integration -v --docker fedora40
|
ansible-test integration -v --docker fedora40
|
||||||
@@ -196,31 +179,6 @@ 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.
|
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.
|
||||||
|
|
||||||
## Devcontainer
|
|
||||||
|
|
||||||
Since community.general 12.2.0, the project repository supports [devcontainers](https://containers.dev/). In short, it is a standard mechanism to
|
|
||||||
create a container that is then used during the development cycle. Many tools are pre-installed in the container and will be already available
|
|
||||||
to you as a developer. A number of different IDEs support that configuration, the most prominent ones being VSCode and PyCharm.
|
|
||||||
|
|
||||||
See the files under [.devcontainer](.devcontainer) for details on what is deployed inside that container.
|
|
||||||
|
|
||||||
Beware of:
|
|
||||||
|
|
||||||
- By default, the devcontainer installs the latest version of `ansible-core`.
|
|
||||||
When testing your changes locally, keep in mind that the collection must support older versions of
|
|
||||||
`ansible-core` and, depending on what is being tested, results may vary.
|
|
||||||
- Integration tests executed directly inside the devcontainer without isolation (see above) may fail if
|
|
||||||
they expected to be run in full fledged VMs. On the other hand, the devcontainer setup allows running
|
|
||||||
containers inside the container (the `docker-in-docker` feature).
|
|
||||||
- The devcontainer is built with a directory structure such that
|
|
||||||
`.../ansible_collections/community/general` contains the project repository, so `ansible-test` and
|
|
||||||
other standard tools should work without any additional setup
|
|
||||||
- By default, the devcontainer installs `pre-commit` and configures it to perform `ruff check` and
|
|
||||||
`ruff format` on the Python files, prior to commiting. That configuration is going to be used by
|
|
||||||
`git` even outside the devcontainer. To prevent errors, you have to either install `pre-commit` in
|
|
||||||
your computer, outside the devcontainer, or run `pre-commit uninstall` from within the devcontainer
|
|
||||||
before quitting it.
|
|
||||||
|
|
||||||
## Creating new modules or plugins
|
## Creating new modules or plugins
|
||||||
|
|
||||||
Creating new modules and plugins requires a bit more work than other Pull Requests.
|
Creating new modules and plugins requires a bit more work than other Pull Requests.
|
||||||
@@ -230,7 +188,7 @@ Creating new modules and plugins requires a bit more work than other Pull Reques
|
|||||||
|
|
||||||
2. Please do not add more than one plugin/module in one PR, especially if it is the first plugin/module you are contributing.
|
2. Please do not add more than one plugin/module in one PR, especially if it is the first plugin/module you are contributing.
|
||||||
That makes it easier for reviewers, and increases the chance that your PR will get merged. If you plan to contribute a group
|
That makes it easier for reviewers, and increases the chance that your PR will get merged. If you plan to contribute a group
|
||||||
of plugins/modules (say, more than a module and a corresponding `_info` module), please mention that in the first PR. In
|
of plugins/modules (say, more than a module and a corresponding ``_info`` module), please mention that in the first PR. In
|
||||||
such cases, you also have to think whether it is better to publish the group of plugins/modules in a new collection.
|
such cases, you also have to think whether it is better to publish the group of plugins/modules in a new collection.
|
||||||
|
|
||||||
3. When creating a new module or plugin, please make sure that you follow various guidelines:
|
3. When creating a new module or plugin, please make sure that you follow various guidelines:
|
||||||
|
|||||||
48
LICENSES/PSF-2.0.txt
Normal file
48
LICENSES/PSF-2.0.txt
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||||
|
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||||
|
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||||
|
distribute, and otherwise use Python alone or in any derivative version,
|
||||||
|
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||||
|
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||||
|
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
|
||||||
|
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||||
|
prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
24
README.md
24
README.md
@@ -7,9 +7,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
# Community General Collection
|
# Community General Collection
|
||||||
|
|
||||||
[](https://docs.ansible.com/projects/ansible/devel/collections/community/general/)
|
[](https://docs.ansible.com/projects/ansible/devel/collections/community/general/)
|
||||||
[](https://dev.azure.com/ansible/community.general/_build?definitionId=31)
|
[](https://dev.azure.com/ansible/community.general/_build?definitionId=31)
|
||||||
[](https://github.com/ansible-collections/community.general/actions)
|
[](https://github.com/ansible-collections/community.general/actions)
|
||||||
[](https://github.com/ansible-collections/community.general/actions)
|
[](https://github.com/ansible-collections/community.general/actions)
|
||||||
[](https://codecov.io/gh/ansible-collections/community.general)
|
[](https://codecov.io/gh/ansible-collections/community.general)
|
||||||
[](https://api.reuse.software/info/github.com/ansible-collections/community.general)
|
[](https://api.reuse.software/info/github.com/ansible-collections/community.general)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ For more information about communication, see the [Ansible communication guide](
|
|||||||
|
|
||||||
## Tested with Ansible
|
## Tested with Ansible
|
||||||
|
|
||||||
Tested with the current ansible-core 2.17, ansible-core 2.18, ansible-core 2.19, ansible-core 2.20 releases and the current development version of ansible-core. Ansible-core versions before 2.17.0 are not supported. This includes all ansible-base 2.10 and Ansible 2.9 releases.
|
Tested with the current ansible-core 2.16, ansible-core 2.17, ansible-core 2.18, ansible-core 2.19, ansible-core 2.20, and ansible-core 2.21 releases. Ansible-core versions before 2.16.0 are not supported. This includes all ansible-base 2.10 and Ansible 2.9 releases.
|
||||||
|
|
||||||
## External requirements
|
## External requirements
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ We are actively accepting new contributors.
|
|||||||
|
|
||||||
All types of contributions are very welcome.
|
All types of contributions are very welcome.
|
||||||
|
|
||||||
You don't know how to start? Refer to our [contribution guide](https://github.com/ansible-collections/community.general/blob/stable-12/CONTRIBUTING.md)!
|
You don't know how to start? Refer to our [contribution guide](https://github.com/ansible-collections/community.general/blob/main/CONTRIBUTING.md)!
|
||||||
|
|
||||||
The current maintainers are listed in the [commit-rights.md](https://github.com/ansible-collections/community.general/blob/stable-12/commit-rights.md#people) file. If you have questions or need help, feel free to mention them in the proposals.
|
The current maintainers are listed in the [commit-rights.md](https://github.com/ansible-collections/community.general/blob/main/commit-rights.md#people) file. If you have questions or need help, feel free to mention them in the proposals.
|
||||||
|
|
||||||
You can find more information in the [developer guide for collections](https://docs.ansible.com/projects/ansible/devel/dev_guide/developing_collections.html#contributing-to-collections), and in the [Ansible Community Guide](https://docs.ansible.com/projects/ansible/latest/community/index.html).
|
You can find more information in the [developer guide for collections](https://docs.ansible.com/projects/ansible/devel/dev_guide/developing_collections.html#contributing-to-collections), and in the [Ansible Community Guide](https://docs.ansible.com/projects/ansible/latest/community/index.html).
|
||||||
|
|
||||||
Also for some notes specific to this collection see [our CONTRIBUTING documentation](https://github.com/ansible-collections/community.general/blob/stable-12/CONTRIBUTING.md).
|
Also for some notes specific to this collection see [our CONTRIBUTING documentation](https://github.com/ansible-collections/community.general/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Running tests
|
### Running tests
|
||||||
|
|
||||||
@@ -102,8 +102,8 @@ See [here](https://docs.ansible.com/projects/ansible/devel/dev_guide/developing_
|
|||||||
|
|
||||||
To learn how to maintain / become a maintainer of this collection, refer to:
|
To learn how to maintain / become a maintainer of this collection, refer to:
|
||||||
|
|
||||||
* [Committer guidelines](https://github.com/ansible-collections/community.general/blob/stable-12/commit-rights.md).
|
* [Committer guidelines](https://github.com/ansible-collections/community.general/blob/main/commit-rights.md).
|
||||||
* [Maintainer guidelines](https://github.com/ansible/community-docs/blob/stable-12/maintaining.rst).
|
* [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst).
|
||||||
|
|
||||||
It is necessary for maintainers of this collection to be subscribed to:
|
It is necessary for maintainers of this collection to be subscribed to:
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ See the [Releasing guidelines](https://github.com/ansible/community-docs/blob/ma
|
|||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
See the [changelog](https://github.com/ansible-collections/community.general/blob/stable-12/CHANGELOG.md).
|
See the [changelog](https://github.com/ansible-collections/community.general/blob/stable-11/CHANGELOG.md).
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -137,8 +137,8 @@ See [this issue](https://github.com/ansible-collections/community.general/issues
|
|||||||
|
|
||||||
This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later.
|
This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later.
|
||||||
|
|
||||||
See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.general/blob/stable-12/COPYING) for the full text.
|
See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.general/blob/stable-11/COPYING) for the full text.
|
||||||
|
|
||||||
Parts of the collection are licensed under the [BSD 2-Clause license](https://github.com/ansible-collections/community.general/blob/stable-12/LICENSES/BSD-2-Clause.txt) and the [MIT license](https://github.com/ansible-collections/community.general/blob/stable-12/LICENSES/MIT.txt).
|
Parts of the collection are licensed under the [BSD 2-Clause license](https://github.com/ansible-collections/community.general/blob/stable-11/LICENSES/BSD-2-Clause.txt), the [MIT license](https://github.com/ansible-collections/community.general/blob/stable-11/LICENSES/MIT.txt), and the [PSF 2.0 license](https://github.com/ansible-collections/community.general/blob/stable-11/LICENSES/PSF-2.0.txt).
|
||||||
|
|
||||||
All files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `REUSE.toml`. This conforms to the [REUSE specification](https://reuse.software/spec/).
|
All files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `REUSE.toml`. This conforms to the [REUSE specification](https://reuse.software/spec/).
|
||||||
|
|||||||
@@ -22,30 +22,13 @@ stable_branches = [ "stable-*" ]
|
|||||||
[sessions.lint]
|
[sessions.lint]
|
||||||
run_isort = false
|
run_isort = false
|
||||||
run_black = false
|
run_black = false
|
||||||
run_ruff_check = true
|
|
||||||
ruff_check_config = "ruff.toml"
|
|
||||||
run_ruff_format = true
|
|
||||||
ruff_format_config = "ruff.toml"
|
|
||||||
run_flake8 = false
|
run_flake8 = false
|
||||||
run_pylint = false
|
run_pylint = false
|
||||||
run_yamllint = true
|
run_yamllint = true
|
||||||
yamllint_config = ".yamllint"
|
yamllint_config = ".yamllint"
|
||||||
# yamllint_config_plugins = ".yamllint-docs"
|
# yamllint_config_plugins = ".yamllint-docs"
|
||||||
# yamllint_config_plugins_examples = ".yamllint-examples"
|
# yamllint_config_plugins_examples = ".yamllint-examples"
|
||||||
run_mypy = true
|
run_mypy = false
|
||||||
mypy_ansible_core_package = "ansible-core>=2.19.0"
|
|
||||||
mypy_config = ".mypy.ini"
|
|
||||||
mypy_extra_deps = [
|
|
||||||
"cryptography",
|
|
||||||
"dnspython",
|
|
||||||
"lxml-stubs",
|
|
||||||
"types-mock",
|
|
||||||
"types-paramiko",
|
|
||||||
"types-passlib",
|
|
||||||
"types-psutil",
|
|
||||||
"types-PyYAML",
|
|
||||||
"types-requests",
|
|
||||||
]
|
|
||||||
|
|
||||||
[sessions.docs_check]
|
[sessions.docs_check]
|
||||||
validate_collection_refs="all"
|
validate_collection_refs="all"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
changelogs/fragments/11.4.8.yml
Normal file
1
changelogs/fragments/11.4.8.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
release_summary: Bugfix release.
|
||||||
2
changelogs/fragments/11909-fix-favicon-url.yml
Normal file
2
changelogs/fragments/11909-fix-favicon-url.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
minor_changes:
|
||||||
|
- "mattermost, rocketchat, slack - update default ``icon_url`` to ansible favicon (https://github.com/ansible-collections/community.general/pull/11909)."
|
||||||
@@ -5,13 +5,3 @@
|
|||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
write_changelog: true
|
write_changelog: true
|
||||||
|
|
||||||
ansible_output:
|
|
||||||
global_env:
|
|
||||||
ANSIBLE_STDOUT_CALLBACK: community.general.tasks_only
|
|
||||||
ANSIBLE_COLLECTIONS_TASKS_ONLY_NUMBER_OF_COLUMNS: 90
|
|
||||||
global_postprocessors:
|
|
||||||
reformat-yaml:
|
|
||||||
command:
|
|
||||||
- python
|
|
||||||
- docs/docsite/reformat-yaml.py
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright (c) Ansible Project
|
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from ruamel.yaml import YAML
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
yaml = YAML(typ="rt")
|
|
||||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
||||||
|
|
||||||
# Load
|
|
||||||
data = yaml.load(sys.stdin)
|
|
||||||
|
|
||||||
# Dump
|
|
||||||
sio = StringIO()
|
|
||||||
yaml.dump(data, sio)
|
|
||||||
print(sio.getvalue().rstrip("\n"))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -13,34 +13,6 @@ Use the filter :ansplugin:`community.general.keep_keys#filter` if you have a lis
|
|||||||
|
|
||||||
Let us use the below list in the following examples:
|
Let us use the below list in the following examples:
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
- name: set-template
|
|
||||||
template:
|
|
||||||
env:
|
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT: yaml
|
|
||||||
variables:
|
|
||||||
data:
|
|
||||||
previous_code_block: yaml
|
|
||||||
previous_code_block_index: 0
|
|
||||||
computation:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
postprocessors:
|
|
||||||
- name: reformat-yaml
|
|
||||||
language: yaml
|
|
||||||
skip_first_lines: 2
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
@{{ computation | indent(8) }}@
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
input:
|
input:
|
||||||
@@ -65,48 +37,24 @@ Let us use the below list in the following examples:
|
|||||||
|
|
||||||
gives
|
gives
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k0_x0: A0
|
- {k0_x0: A0, k1_x1: B0}
|
||||||
k1_x1: B0
|
- {k0_x0: A1, k1_x1: B1}
|
||||||
- k0_x0: A1
|
|
||||||
k1_x1: B1
|
|
||||||
|
|
||||||
|
|
||||||
.. versionadded:: 9.1.0
|
.. versionadded:: 9.1.0
|
||||||
|
|
||||||
* The results of the below examples 1-5 are all the same:
|
* The results of the below examples 1-5 are all the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: equal
|
|
||||||
target: ['k0_x0', 'k1_x1']
|
|
||||||
result: "{{ input | community.general.keep_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k0_x0: A0
|
- {k0_x0: A0, k1_x1: B0}
|
||||||
k1_x1: B0
|
- {k0_x0: A1, k1_x1: B1}
|
||||||
- k0_x0: A1
|
|
||||||
k1_x1: B1
|
|
||||||
|
|
||||||
|
|
||||||
1. Match keys that equal any of the items in the target.
|
1. Match keys that equal any of the items in the target.
|
||||||
@@ -157,28 +105,12 @@ gives
|
|||||||
|
|
||||||
* The results of the below examples 6-9 are all the same:
|
* The results of the below examples 6-9 are all the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: equal
|
|
||||||
target: k0_x0
|
|
||||||
result: "{{ input | community.general.keep_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k0_x0: A0
|
- {k0_x0: A0}
|
||||||
- k0_x0: A1
|
- {k0_x0: A1}
|
||||||
|
|
||||||
|
|
||||||
6. Match keys that equal the target.
|
6. Match keys that equal the target.
|
||||||
@@ -216,3 +148,4 @@ gives
|
|||||||
mp: regex
|
mp: regex
|
||||||
target: ^.*0_x.*$
|
target: ^.*0_x.*$
|
||||||
result: "{{ input | community.general.keep_keys(target=target, matching_parameter=mp) }}"
|
result: "{{ input | community.general.keep_keys(target=target, matching_parameter=mp) }}"
|
||||||
|
|
||||||
|
|||||||
@@ -13,34 +13,6 @@ Use the filter :ansplugin:`community.general.remove_keys#filter` if you have a l
|
|||||||
|
|
||||||
Let us use the below list in the following examples:
|
Let us use the below list in the following examples:
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
- name: set-template
|
|
||||||
template:
|
|
||||||
env:
|
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT: yaml
|
|
||||||
variables:
|
|
||||||
data:
|
|
||||||
previous_code_block: yaml
|
|
||||||
previous_code_block_index: 0
|
|
||||||
computation:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
postprocessors:
|
|
||||||
- name: reformat-yaml
|
|
||||||
language: yaml
|
|
||||||
skip_first_lines: 2
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
@{{ computation | indent(8) }}@
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
input:
|
input:
|
||||||
@@ -65,19 +37,13 @@ Let us use the below list in the following examples:
|
|||||||
|
|
||||||
gives
|
gives
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k2_x2:
|
- k2_x2: [C0]
|
||||||
- C0
|
|
||||||
k3_x3: foo
|
k3_x3: foo
|
||||||
- k2_x2:
|
- k2_x2: [C1]
|
||||||
- C1
|
|
||||||
k3_x3: bar
|
k3_x3: bar
|
||||||
|
|
||||||
|
|
||||||
@@ -85,31 +51,13 @@ gives
|
|||||||
|
|
||||||
* The results of the below examples 1-5 are all the same:
|
* The results of the below examples 1-5 are all the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: equal
|
|
||||||
target: ['k0_x0', 'k1_x1']
|
|
||||||
result: "{{ input | community.general.remove_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k2_x2:
|
- k2_x2: [C0]
|
||||||
- C0
|
|
||||||
k3_x3: foo
|
k3_x3: foo
|
||||||
- k2_x2:
|
- k2_x2: [C1]
|
||||||
- C1
|
|
||||||
k3_x3: bar
|
k3_x3: bar
|
||||||
|
|
||||||
|
|
||||||
@@ -161,33 +109,15 @@ gives
|
|||||||
|
|
||||||
* The results of the below examples 6-9 are all the same:
|
* The results of the below examples 6-9 are all the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: equal
|
|
||||||
target: k0_x0
|
|
||||||
result: "{{ input | community.general.remove_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- k1_x1: B0
|
- k1_x1: B0
|
||||||
k2_x2:
|
k2_x2: [C0]
|
||||||
- C0
|
|
||||||
k3_x3: foo
|
k3_x3: foo
|
||||||
- k1_x1: B1
|
- k1_x1: B1
|
||||||
k2_x2:
|
k2_x2: [C1]
|
||||||
- C1
|
|
||||||
k3_x3: bar
|
k3_x3: bar
|
||||||
|
|
||||||
|
|
||||||
@@ -226,3 +156,4 @@ gives
|
|||||||
mp: regex
|
mp: regex
|
||||||
target: ^.*0_x.*$
|
target: ^.*0_x.*$
|
||||||
result: "{{ input | community.general.remove_keys(target=target, matching_parameter=mp) }}"
|
result: "{{ input | community.general.remove_keys(target=target, matching_parameter=mp) }}"
|
||||||
|
|
||||||
|
|||||||
@@ -13,34 +13,6 @@ Use the filter :ansplugin:`community.general.replace_keys#filter` if you have a
|
|||||||
|
|
||||||
Let us use the below list in the following examples:
|
Let us use the below list in the following examples:
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
- name: set-template
|
|
||||||
template:
|
|
||||||
env:
|
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT: yaml
|
|
||||||
variables:
|
|
||||||
data:
|
|
||||||
previous_code_block: yaml
|
|
||||||
previous_code_block_index: 0
|
|
||||||
computation:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
postprocessors:
|
|
||||||
- name: reformat-yaml
|
|
||||||
language: yaml
|
|
||||||
skip_first_lines: 2
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
@{{ computation | indent(8) }}@
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
input:
|
input:
|
||||||
@@ -68,23 +40,17 @@ Let us use the below list in the following examples:
|
|||||||
|
|
||||||
gives
|
gives
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- a0: A0
|
- a0: A0
|
||||||
a1: B0
|
a1: B0
|
||||||
k2_x2:
|
k2_x2: [C0]
|
||||||
- C0
|
|
||||||
k3_x3: foo
|
k3_x3: foo
|
||||||
- a0: A1
|
- a0: A1
|
||||||
a1: B1
|
a1: B1
|
||||||
k2_x2:
|
k2_x2: [C1]
|
||||||
- C1
|
|
||||||
k3_x3: bar
|
k3_x3: bar
|
||||||
|
|
||||||
|
|
||||||
@@ -92,37 +58,17 @@ gives
|
|||||||
|
|
||||||
* The results of the below examples 1-3 are all the same:
|
* The results of the below examples 1-3 are all the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: starts_with
|
|
||||||
target:
|
|
||||||
- {after: a0, before: k0}
|
|
||||||
- {after: a1, before: k1}
|
|
||||||
result: "{{ input | community.general.replace_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- a0: A0
|
- a0: A0
|
||||||
a1: B0
|
a1: B0
|
||||||
k2_x2:
|
k2_x2: [C0]
|
||||||
- C0
|
|
||||||
k3_x3: foo
|
k3_x3: foo
|
||||||
- a0: A1
|
- a0: A1
|
||||||
a1: B1
|
a1: B1
|
||||||
k2_x2:
|
k2_x2: [C1]
|
||||||
- C1
|
|
||||||
k3_x3: bar
|
k3_x3: bar
|
||||||
|
|
||||||
|
|
||||||
@@ -165,29 +111,12 @@ gives
|
|||||||
|
|
||||||
* The results of the below examples 4-5 are the same:
|
* The results of the below examples 4-5 are the same:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
|
|
||||||
# I picked one of the examples
|
|
||||||
mp: regex
|
|
||||||
target:
|
|
||||||
- {after: X, before: ^.*_x.*$}
|
|
||||||
result: "{{ input | community.general.replace_keys(target=target, matching_parameter=mp) }}"
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- X: foo
|
- {X: foo}
|
||||||
- X: bar
|
- {X: bar}
|
||||||
|
|
||||||
|
|
||||||
4. If more keys match the same attribute before the last one will be used.
|
4. If more keys match the same attribute before the last one will be used.
|
||||||
@@ -216,11 +145,6 @@ gives
|
|||||||
|
|
||||||
6. If there are more matches for a key the first one will be used.
|
6. If there are more matches for a key the first one will be used.
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
@@ -241,17 +165,11 @@ gives
|
|||||||
|
|
||||||
gives
|
gives
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
:emphasize-lines: 1-
|
:emphasize-lines: 1-
|
||||||
|
|
||||||
result:
|
result:
|
||||||
- X: A
|
- {X: A, bbb1: B, ccc1: C}
|
||||||
bbb1: B
|
- {X: D, bbb2: E, ccc2: F}
|
||||||
ccc1: C
|
|
||||||
- X: D
|
|
||||||
bbb2: E
|
|
||||||
ccc2: F
|
|
||||||
|
|||||||
@@ -20,17 +20,6 @@ The :ansplugin:`community.general.counter filter plugin <community.general.count
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Count character occurrences in a string] ********************************************
|
TASK [Count character occurrences in a string] ********************************************
|
||||||
@@ -83,20 +72,9 @@ This plugin is useful for selecting resources based on current allocation:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. 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] ***
|
TASK [Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks]
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": "scsi_2"
|
"msg": "scsi_2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,27 +31,16 @@ You can use the :ansplugin:`community.general.dict_kv filter <community.general.
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Create a single-entry dictionary] ***************************************************
|
TASK [Create a single-entry dictionary] **************************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": {
|
"msg": {
|
||||||
"thatsmyvar": "myvalue"
|
"thatsmyvar": "myvalue"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Create a list of dictionaries where the 'server' field is taken from a list] ********
|
TASK [Create a list of dictionaries where the 'server' field is taken from a list] *******
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": [
|
"msg": [
|
||||||
{
|
{
|
||||||
@@ -98,20 +87,9 @@ If you need to convert a list of key-value pairs to a dictionary, you can use th
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Create a dictionary with the dict function] *****************************************
|
TASK [Create a dictionary with the dict function] ****************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": {
|
"msg": {
|
||||||
"1": 2,
|
"1": 2,
|
||||||
@@ -119,7 +97,7 @@ This produces:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Create a dictionary with the community.general.dict filter] *************************
|
TASK [Create a dictionary with the community.general.dict filter] ************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": {
|
"msg": {
|
||||||
"1": 2,
|
"1": 2,
|
||||||
@@ -127,7 +105,7 @@ This produces:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Create a list of dictionaries with map and the community.general.dict filter] *******
|
TASK [Create a list of dictionaries with map and the community.general.dict filter] ******
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": [
|
"msg": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,49 +22,6 @@ One example is ``ansible_facts.mounts``, which is a list of dictionaries where e
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
skip_first_lines: 3 # the set_fact task
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- ansible.builtin.set_fact:
|
|
||||||
ansible_facts:
|
|
||||||
mounts:
|
|
||||||
- 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"
|
|
||||||
- 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 | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Output mount facts grouped by device name] ******************************************
|
TASK [Output mount facts grouped by device name] ******************************************
|
||||||
@@ -122,7 +79,7 @@ This produces:
|
|||||||
"options": "rw,relatime",
|
"options": "rw,relatime",
|
||||||
"size_available": 42143,
|
"size_available": 42143,
|
||||||
"size_total": 543210,
|
"size_total": 543210,
|
||||||
"uuid": "abcdef01-2345-6789-0abc-def012345678"
|
"uuid": "bdf50b7d-4859-40af-8665-c637ee7a7808"
|
||||||
},
|
},
|
||||||
"/boot": {
|
"/boot": {
|
||||||
"block_available": 2000,
|
"block_available": 2000,
|
||||||
|
|||||||
@@ -21,34 +21,6 @@ These filters preserve the item order, eliminate duplicates and are an extended
|
|||||||
|
|
||||||
Let us use the lists below in the following examples:
|
Let us use the lists below in the following examples:
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
- name: set-template
|
|
||||||
template:
|
|
||||||
env:
|
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT: yaml
|
|
||||||
variables:
|
|
||||||
data:
|
|
||||||
previous_code_block: yaml
|
|
||||||
previous_code_block_index: 0
|
|
||||||
computation:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
postprocessors:
|
|
||||||
- name: reformat-yaml
|
|
||||||
language: yaml
|
|
||||||
skip_first_lines: 2
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
@{{ computation | indent(8) }}@
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: result
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
A: [9, 5, 7, 1, 9, 4, 10, 5, 9, 7]
|
A: [9, 5, 7, 1, 9, 4, 10, 5, 9, 7]
|
||||||
@@ -63,22 +35,9 @@ The union of ``A`` and ``B`` can be written as:
|
|||||||
|
|
||||||
This statement produces:
|
This statement produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
result:
|
result: [9, 5, 7, 1, 4, 10, 2, 8, 3]
|
||||||
- 9
|
|
||||||
- 5
|
|
||||||
- 7
|
|
||||||
- 1
|
|
||||||
- 4
|
|
||||||
- 10
|
|
||||||
- 2
|
|
||||||
- 8
|
|
||||||
- 3
|
|
||||||
|
|
||||||
If you want to calculate the intersection of ``A``, ``B`` and ``C``, you can use the following statement:
|
If you want to calculate the intersection of ``A``, ``B`` and ``C``, you can use the following statement:
|
||||||
|
|
||||||
@@ -100,14 +59,9 @@ or
|
|||||||
|
|
||||||
All three statements are equivalent and give:
|
All three statements are equivalent and give:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
result:
|
result: [1]
|
||||||
- 1
|
|
||||||
|
|
||||||
.. note:: Be aware that in most cases, filter calls without any argument require ``flatten=true``, otherwise the input is returned as result. The reason for this is, that the input is considered as a variable argument and is wrapped by an additional outer list. ``flatten=true`` ensures that this list is removed before the input is processed by the filter logic.
|
.. note:: Be aware that in most cases, filter calls without any argument require ``flatten=true``, otherwise the input is returned as result. The reason for this is, that the input is considered as a variable argument and is wrapped by an additional outer list. ``flatten=true`` ensures that this list is removed before the input is processed by the filter logic.
|
||||||
|
|
||||||
@@ -121,14 +75,7 @@ For example, the symmetric difference of ``A``, ``B`` and ``C`` may be written a
|
|||||||
|
|
||||||
This gives:
|
This gives:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
result:
|
result: [5, 8, 3, 1]
|
||||||
- 5
|
|
||||||
- 8
|
|
||||||
- 3
|
|
||||||
- 1
|
|
||||||
|
|||||||
@@ -12,34 +12,6 @@ If you have two or more lists of dictionaries and want to combine them into a li
|
|||||||
|
|
||||||
Let us use the lists below in the following examples:
|
Let us use the lists below in the following examples:
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
- name: set-template
|
|
||||||
template:
|
|
||||||
env:
|
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT: yaml
|
|
||||||
variables:
|
|
||||||
data:
|
|
||||||
previous_code_block: yaml
|
|
||||||
previous_code_block_index: 0
|
|
||||||
computation:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
postprocessors:
|
|
||||||
- name: reformat-yaml
|
|
||||||
language: yaml
|
|
||||||
skip_first_lines: 2
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- vars:
|
|
||||||
@{{ data | indent(8) }}@
|
|
||||||
@{{ computation | indent(8) }}@
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: list3
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list1:
|
list1:
|
||||||
@@ -62,22 +34,13 @@ In the example below the lists are merged by the attribute ``name``:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- extra: false
|
- {name: bar, extra: false}
|
||||||
name: bar
|
- {name: baz, path: /baz}
|
||||||
- name: baz
|
- {name: foo, extra: true, path: /foo}
|
||||||
path: /baz
|
- {name: meh, extra: true}
|
||||||
- extra: true
|
|
||||||
name: foo
|
|
||||||
path: /foo
|
|
||||||
- extra: true
|
|
||||||
name: meh
|
|
||||||
|
|
||||||
|
|
||||||
.. versionadded:: 2.0.0
|
.. versionadded:: 2.0.0
|
||||||
@@ -93,22 +56,13 @@ It is possible to use a list of lists as an input of the filter:
|
|||||||
|
|
||||||
This produces the same result as in the previous example:
|
This produces the same result as in the previous example:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- extra: false
|
- {name: bar, extra: false}
|
||||||
name: bar
|
- {name: baz, path: /baz}
|
||||||
- name: baz
|
- {name: foo, extra: true, path: /foo}
|
||||||
path: /baz
|
- {name: meh, extra: true}
|
||||||
- extra: true
|
|
||||||
name: foo
|
|
||||||
path: /foo
|
|
||||||
- extra: true
|
|
||||||
name: meh
|
|
||||||
|
|
||||||
Single list
|
Single list
|
||||||
"""""""""""
|
"""""""""""
|
||||||
@@ -121,22 +75,13 @@ It is possible to merge single list:
|
|||||||
|
|
||||||
This produces the same result as in the previous example:
|
This produces the same result as in the previous example:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- extra: false
|
- {name: bar, extra: false}
|
||||||
name: bar
|
- {name: baz, path: /baz}
|
||||||
- name: baz
|
- {name: foo, extra: true, path: /foo}
|
||||||
path: /baz
|
- {name: meh, extra: true}
|
||||||
- extra: true
|
|
||||||
name: foo
|
|
||||||
path: /foo
|
|
||||||
- extra: true
|
|
||||||
name: meh
|
|
||||||
|
|
||||||
|
|
||||||
The filter also accepts two optional parameters: :ansopt:`community.general.lists_mergeby#filter:recursive` and :ansopt:`community.general.lists_mergeby#filter:list_merge`. This is available since community.general 4.4.0.
|
The filter also accepts two optional parameters: :ansopt:`community.general.lists_mergeby#filter:recursive` and :ansopt:`community.general.lists_mergeby#filter:list_merge`. This is available since community.general 4.4.0.
|
||||||
@@ -151,11 +96,6 @@ The examples below set :ansopt:`community.general.lists_mergeby#filter:recursive
|
|||||||
|
|
||||||
Let us use the lists below in the following examples
|
Let us use the lists below in the following examples
|
||||||
|
|
||||||
.. ansible-output-meta::
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- name: reset-previous-blocks
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list1:
|
list1:
|
||||||
@@ -188,25 +128,17 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=replace` (def
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- patch_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [patch_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [3, 4, 4]
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
- 4
|
|
||||||
|
|
||||||
list_merge=keep
|
list_merge=keep
|
||||||
"""""""""""""""
|
"""""""""""""""
|
||||||
@@ -221,26 +153,17 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=keep`:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- default_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [default_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [1, 1, 2, 3]
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
|
|
||||||
list_merge=append
|
list_merge=append
|
||||||
"""""""""""""""""
|
"""""""""""""""""
|
||||||
@@ -255,30 +178,17 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append`:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- default_value
|
|
||||||
- patch_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [default_value, patch_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [1, 1, 2, 3, 3, 4, 4]
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
- 4
|
|
||||||
|
|
||||||
list_merge=prepend
|
list_merge=prepend
|
||||||
""""""""""""""""""
|
""""""""""""""""""
|
||||||
@@ -293,30 +203,17 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend`:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- patch_value
|
|
||||||
- default_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [patch_value, default_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [3, 4, 4, 1, 1, 2, 3]
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
- 4
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
|
|
||||||
list_merge=append_rp
|
list_merge=append_rp
|
||||||
""""""""""""""""""""
|
""""""""""""""""""""
|
||||||
@@ -331,29 +228,17 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=append_rp`:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- default_value
|
|
||||||
- patch_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [default_value, patch_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [1, 1, 2, 3, 4, 4]
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
- 4
|
|
||||||
|
|
||||||
list_merge=prepend_rp
|
list_merge=prepend_rp
|
||||||
"""""""""""""""""""""
|
"""""""""""""""""""""
|
||||||
@@ -368,26 +253,15 @@ Example :ansopt:`community.general.lists_mergeby#filter:list_merge=prepend_rp`:
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: ~
|
|
||||||
|
|
||||||
.. code-block:: yaml
|
.. code-block:: yaml
|
||||||
|
|
||||||
list3:
|
list3:
|
||||||
- name: myname01
|
- name: myname01
|
||||||
param01:
|
param01:
|
||||||
list:
|
|
||||||
- patch_value
|
|
||||||
- default_value
|
|
||||||
x: default_value
|
x: default_value
|
||||||
y: patch_value
|
y: patch_value
|
||||||
|
list: [patch_value, default_value]
|
||||||
z: patch_value
|
z: patch_value
|
||||||
- name: myname02
|
- name: myname02
|
||||||
param01:
|
param01: [3, 4, 4, 1, 1, 2]
|
||||||
- 3
|
|
||||||
- 4
|
|
||||||
- 4
|
|
||||||
- 1
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
|
|||||||
@@ -24,17 +24,6 @@ Ansible offers the :ansplugin:`community.general.read_csv module <community.gene
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Parse CSV from string] **************************************************************
|
TASK [Parse CSV from string] **************************************************************
|
||||||
@@ -80,34 +69,6 @@ Converting to JSON
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
skip_first_lines: 3
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- ansible.builtin.set_fact:
|
|
||||||
result_stdout: |-
|
|
||||||
bin
|
|
||||||
boot
|
|
||||||
dev
|
|
||||||
etc
|
|
||||||
home
|
|
||||||
lib
|
|
||||||
proc
|
|
||||||
root
|
|
||||||
run
|
|
||||||
tmp
|
|
||||||
|
|
||||||
- name: Run 'ls' to list files in /
|
|
||||||
command: ls /
|
|
||||||
register: result
|
|
||||||
|
|
||||||
- name: Parse the ls output
|
|
||||||
debug:
|
|
||||||
msg: "{{ result_stdout | community.general.jc('ls') }}"
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Run 'ls' to list files in /] ********************************************************
|
TASK [Run 'ls' to list files in /] ********************************************************
|
||||||
|
|||||||
@@ -25,17 +25,6 @@ Hashids
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Create hashid] **********************************************************************
|
TASK [Create hashid] **********************************************************************
|
||||||
@@ -77,32 +66,16 @@ You can use the :ansplugin:`community.general.random_mac filter <community.gener
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- name: "Create a random MAC starting with ff:"
|
|
||||||
debug:
|
|
||||||
# We're using a seed here to avoid randomness in the output
|
|
||||||
msg: "{{ 'FF' | community.general.random_mac(seed='') }}"
|
|
||||||
|
|
||||||
- name: "Create a random MAC starting with 00:11:22:"
|
|
||||||
debug:
|
|
||||||
# We're using a seed here to avoid randomness in the output
|
|
||||||
msg: "{{ '00:11:22' | community.general.random_mac(seed='') }}"
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Create a random MAC starting with ff:] **********************************************
|
TASK [Create a random MAC starting with ff:] **********************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": "ff:84:f5:d1:59:20"
|
"msg": "ff:69:d3:78:7f:b4"
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": "00:11:22:84:f5:d1"
|
"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:
|
You can also initialize the random number generator from a seed to create random-but-idempotent MAC addresses:
|
||||||
|
|||||||
@@ -69,32 +69,21 @@ Note that months and years are using a simplified representation: a month is 30
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Convert string to seconds] **********************************************************
|
TASK [Convert string to seconds] **********************************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": 109210.123
|
"msg": "109210.123"
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Convert string to hours] ************************************************************
|
TASK [Convert string to hours] ************************************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": 30.336145277778
|
"msg": "30.336145277778"
|
||||||
}
|
}
|
||||||
|
|
||||||
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": 1.096851471595
|
"msg": "1.096851471595"
|
||||||
}
|
}
|
||||||
|
|
||||||
.. versionadded: 0.2.0
|
.. versionadded: 0.2.0
|
||||||
|
|||||||
@@ -21,20 +21,9 @@ You can use the :ansplugin:`community.general.unicode_normalize filter <communit
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Compare Unicode representations] ****************************************************
|
TASK [Compare Unicode representations] ********************************************************
|
||||||
ok: [localhost] => {
|
ok: [localhost] => {
|
||||||
"msg": true
|
"msg": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,6 @@ If you need to sort a list of version numbers, the Jinja ``sort`` filter is prob
|
|||||||
|
|
||||||
This produces:
|
This produces:
|
||||||
|
|
||||||
.. ansible-output-data::
|
|
||||||
|
|
||||||
variables:
|
|
||||||
task:
|
|
||||||
previous_code_block: yaml+jinja
|
|
||||||
playbook: |-
|
|
||||||
- hosts: localhost
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
@{{ task | indent(4) }}@
|
|
||||||
|
|
||||||
.. code-block:: ansible-output
|
.. code-block:: ansible-output
|
||||||
|
|
||||||
TASK [Sort list by version number] ********************************************************
|
TASK [Sort list by version number] ********************************************************
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The inventory plugin :ansplugin:`community.general.iocage#inventory` gets the in
|
|||||||
|
|
||||||
See:
|
See:
|
||||||
|
|
||||||
* `iocage - A FreeBSD Jail Manager <https://iocage.readthedocs.io/en/latest>`_
|
* `iocage - A FreeBSD Jail Manager <https://freebsd.github.io/iocage/>`_
|
||||||
* `man iocage <https://man.freebsd.org/cgi/man.cgi?query=iocage>`_
|
* `man iocage <https://man.freebsd.org/cgi/man.cgi?query=iocage>`_
|
||||||
* `Jails and Containers <https://docs.freebsd.org/en/books/handbook/jails>`_
|
* `Jails and Containers <https://docs.freebsd.org/en/books/handbook/jails>`_
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ As root at the iocage host, create three VNET jails with a DHCP interface from t
|
|||||||
shell> iocage create --template ansible_client --name srv_3 bpf=1 dhcp=1 vnet=1
|
shell> iocage create --template ansible_client --name srv_3 bpf=1 dhcp=1 vnet=1
|
||||||
srv_3 successfully created!
|
srv_3 successfully created!
|
||||||
|
|
||||||
See: `Configuring a VNET Jail <https://iocage.readthedocs.io/en/latest/networking.html#configuring-a-vnet-jail>`_.
|
See: `Configuring VNET <https://freebsd.github.io/iocage/networking.html#vimage-vnet>`_.
|
||||||
|
|
||||||
As admin at the controller, list the jails:
|
As admin at the controller, list the jails:
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ Optionally, create shared IP jails:
|
|||||||
| None | srv_3 | off | down | jail | 14.2-RELEASE-p3 | em0|10.1.0.103/24 | - | ansible_client | no |
|
| None | srv_3 | off | down | jail | 14.2-RELEASE-p3 | em0|10.1.0.103/24 | - | ansible_client | no |
|
||||||
+------+-------+------+-------+------+-----------------+-------------------+-----+----------------+----------+
|
+------+-------+------+-------+------+-----------------+-------------------+-----+----------------+----------+
|
||||||
|
|
||||||
See: `Configuring a Shared IP Jail <https://iocage.readthedocs.io/en/latest/networking.html#configuring-a-shared-ip-jail>`_
|
See: `Configuring a Shared IP Jail <https://freebsd.github.io/iocage/networking.html#shared-ip>`_
|
||||||
|
|
||||||
If iocage needs environment variable(s), use the option :ansopt:`community.general.iocage#inventory:env`. For example,
|
If iocage needs environment variable(s), use the option :ansopt:`community.general.iocage#inventory:env`. For example,
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
namespace: community
|
namespace: community
|
||||||
name: general
|
name: general
|
||||||
version: 12.2.0
|
version: 11.4.8
|
||||||
readme: README.md
|
readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Ansible (https://github.com/ansible)
|
- Ansible (https://github.com/ansible)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
requires_ansible: '>=2.17.0'
|
requires_ansible: '>=2.16.0'
|
||||||
action_groups:
|
action_groups:
|
||||||
consul:
|
consul:
|
||||||
- consul_agent_check
|
- consul_agent_check
|
||||||
@@ -46,7 +46,6 @@ action_groups:
|
|||||||
- keycloak_user_federation
|
- keycloak_user_federation
|
||||||
- keycloak_user_rolemapping
|
- keycloak_user_rolemapping
|
||||||
- keycloak_userprofile
|
- keycloak_userprofile
|
||||||
- keycloak_user_execute_actions_email
|
|
||||||
scaleway:
|
scaleway:
|
||||||
- scaleway_compute
|
- scaleway_compute
|
||||||
- scaleway_compute_private_network
|
- scaleway_compute_private_network
|
||||||
@@ -101,7 +100,7 @@ plugin_routing:
|
|||||||
warning_text: Use the 'default' callback plugin with 'display_failed_stderr
|
warning_text: Use the 'default' callback plugin with 'display_failed_stderr
|
||||||
= yes' option.
|
= yes' option.
|
||||||
yaml:
|
yaml:
|
||||||
tombstone:
|
deprecation:
|
||||||
removal_version: 12.0.0
|
removal_version: 12.0.0
|
||||||
warning_text: >-
|
warning_text: >-
|
||||||
The plugin has been superseded by the option `result_format=yaml` in callback plugin ansible.builtin.default from ansible-core 2.13 onwards.
|
The plugin has been superseded by the option `result_format=yaml` in callback plugin ansible.builtin.default from ansible-core 2.13 onwards.
|
||||||
@@ -154,7 +153,7 @@ plugin_routing:
|
|||||||
removal_version: 13.0.0
|
removal_version: 13.0.0
|
||||||
warning_text: Project Atomic was sunset by the end of 2019.
|
warning_text: Project Atomic was sunset by the end of 2019.
|
||||||
bearychat:
|
bearychat:
|
||||||
tombstone:
|
deprecation:
|
||||||
removal_version: 12.0.0
|
removal_version: 12.0.0
|
||||||
warning_text: Chat service is no longer available.
|
warning_text: Chat service is no longer available.
|
||||||
catapult:
|
catapult:
|
||||||
@@ -203,14 +202,6 @@ plugin_routing:
|
|||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 10.0.0
|
removal_version: 10.0.0
|
||||||
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
|
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
|
||||||
dimensiondata_network:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Service and its endpoints are no longer available.
|
|
||||||
dimensiondata_vlan:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Service and its endpoints are no longer available.
|
|
||||||
docker_compose:
|
docker_compose:
|
||||||
redirect: community.docker.docker_compose
|
redirect: community.docker.docker_compose
|
||||||
docker_config:
|
docker_config:
|
||||||
@@ -266,7 +257,7 @@ plugin_routing:
|
|||||||
docker_volume_info:
|
docker_volume_info:
|
||||||
redirect: community.docker.docker_volume_info
|
redirect: community.docker.docker_volume_info
|
||||||
facter:
|
facter:
|
||||||
tombstone:
|
deprecation:
|
||||||
removal_version: 12.0.0
|
removal_version: 12.0.0
|
||||||
warning_text: Use community.general.facter_facts instead.
|
warning_text: Use community.general.facter_facts instead.
|
||||||
flowdock:
|
flowdock:
|
||||||
@@ -398,10 +389,6 @@ plugin_routing:
|
|||||||
redirect: community.kubevirt.kubevirt_template
|
redirect: community.kubevirt.kubevirt_template
|
||||||
kubevirt_vm:
|
kubevirt_vm:
|
||||||
redirect: community.kubevirt.kubevirt_vm
|
redirect: community.kubevirt.kubevirt_vm
|
||||||
layman:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 14.0.0
|
|
||||||
warning_text: Gentoo deprecated C(layman) in mid-2023.
|
|
||||||
ldap_attr:
|
ldap_attr:
|
||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
@@ -506,30 +493,6 @@ plugin_routing:
|
|||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: Use community.general.one_image_info instead.
|
warning_text: Use community.general.one_image_info instead.
|
||||||
oneandone_firewall_policy:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
oneandone_load_balancer:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
oneandone_monitoring_policy:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
oneandone_private_network:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
oneandone_public_ip:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
oneandone_server:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
onepassword_facts:
|
onepassword_facts:
|
||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
@@ -837,10 +800,6 @@ plugin_routing:
|
|||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: Use purestorage.flashblade.purefb_info instead.
|
warning_text: Use purestorage.flashblade.purefb_info instead.
|
||||||
pushbullet:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Module relies on Python package pushbullet.py which is not maintained and supports only up to Python 3.2.
|
|
||||||
python_requirements_facts:
|
python_requirements_facts:
|
||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
@@ -1037,24 +996,12 @@ plugin_routing:
|
|||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: Use community.general.smartos_image_info instead.
|
warning_text: Use community.general.smartos_image_info instead.
|
||||||
spotinst_aws_elastigroup:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Module relies on unsupported Python package. Use the module spot.cloud_modules.aws_elastigroup instead.
|
|
||||||
stackdriver:
|
stackdriver:
|
||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 9.0.0
|
removal_version: 9.0.0
|
||||||
warning_text: This module relied on HTTPS APIs that do not exist anymore,
|
warning_text: This module relied on HTTPS APIs that do not exist anymore,
|
||||||
and any new development in the direction of providing an alternative should
|
and any new development in the direction of providing an alternative should
|
||||||
happen in the context of the google.cloud collection.
|
happen in the context of the google.cloud collection.
|
||||||
swupd:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 15.0.0
|
|
||||||
warning_text: ClearLinux was made EOL in July 2025. If you think the module is still useful for another distribution, please create an issue in the community.general repository.
|
|
||||||
typetalk:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: The typetalk service will be discontinued on Dec 2025.
|
|
||||||
vertica_facts:
|
vertica_facts:
|
||||||
tombstone:
|
tombstone:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
@@ -1091,14 +1038,6 @@ plugin_routing:
|
|||||||
doc_fragments:
|
doc_fragments:
|
||||||
_gcp:
|
_gcp:
|
||||||
redirect: community.google._gcp
|
redirect: community.google._gcp
|
||||||
dimensiondata:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Service and its endpoints are no longer available.
|
|
||||||
dimensiondata_wait:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Service and its endpoints are no longer available.
|
|
||||||
docker:
|
docker:
|
||||||
redirect: community.docker.docker
|
redirect: community.docker.docker
|
||||||
hetzner:
|
hetzner:
|
||||||
@@ -1141,7 +1080,7 @@ plugin_routing:
|
|||||||
removal_version: 15.0.0
|
removal_version: 15.0.0
|
||||||
warning_text: The proxmox content has been moved to community.proxmox.
|
warning_text: The proxmox content has been moved to community.proxmox.
|
||||||
purestorage:
|
purestorage:
|
||||||
tombstone:
|
deprecation:
|
||||||
removal_version: 12.0.0
|
removal_version: 12.0.0
|
||||||
warning_text: The modules for purestorage were removed in community.general 3.0.0, this document fragment was left behind.
|
warning_text: The modules for purestorage were removed in community.general 3.0.0, this document fragment was left behind.
|
||||||
rackspace:
|
rackspace:
|
||||||
@@ -1150,18 +1089,6 @@ plugin_routing:
|
|||||||
warning_text: This doc fragment was used by rax modules, that relied on the deprecated
|
warning_text: This doc fragment was used by rax modules, that relied on the deprecated
|
||||||
package pyrax.
|
package pyrax.
|
||||||
module_utils:
|
module_utils:
|
||||||
cloud:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: This code is not used by community.general. If you want to use it in another collection, please copy it over.
|
|
||||||
database:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: This code is not used by community.general. If you want to use it in another collection, please copy it over.
|
|
||||||
dimensiondata:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: Service and its endpoints are no longer available.
|
|
||||||
docker.common:
|
docker.common:
|
||||||
redirect: community.docker.common
|
redirect: community.docker.common
|
||||||
docker.swarm:
|
docker.swarm:
|
||||||
@@ -1174,10 +1101,6 @@ plugin_routing:
|
|||||||
redirect: community.google.gcp
|
redirect: community.google.gcp
|
||||||
hetzner:
|
hetzner:
|
||||||
redirect: community.hrobot.robot
|
redirect: community.hrobot.robot
|
||||||
known_hosts:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: This code is not used by community.general. If you want to use it in another collection, please copy it over.
|
|
||||||
kubevirt:
|
kubevirt:
|
||||||
redirect: community.kubevirt.kubevirt
|
redirect: community.kubevirt.kubevirt
|
||||||
net_tools.nios.api:
|
net_tools.nios.api:
|
||||||
@@ -1186,10 +1109,6 @@ plugin_routing:
|
|||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 13.0.0
|
removal_version: 13.0.0
|
||||||
warning_text: Code is unmaintained here and official Oracle collection is available for a number of years.
|
warning_text: Code is unmaintained here and official Oracle collection is available for a number of years.
|
||||||
oneandone:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: DNS fails to resolve the API endpoint used by the module.
|
|
||||||
postgresql:
|
postgresql:
|
||||||
redirect: community.postgresql.postgresql
|
redirect: community.postgresql.postgresql
|
||||||
proxmox:
|
proxmox:
|
||||||
@@ -1198,7 +1117,7 @@ plugin_routing:
|
|||||||
removal_version: 15.0.0
|
removal_version: 15.0.0
|
||||||
warning_text: The proxmox content has been moved to community.proxmox.
|
warning_text: The proxmox content has been moved to community.proxmox.
|
||||||
pure:
|
pure:
|
||||||
tombstone:
|
deprecation:
|
||||||
removal_version: 12.0.0
|
removal_version: 12.0.0
|
||||||
warning_text: The modules for purestorage were removed in community.general 3.0.0, this module util was left behind.
|
warning_text: The modules for purestorage were removed in community.general 3.0.0, this module util was left behind.
|
||||||
rax:
|
rax:
|
||||||
@@ -1209,10 +1128,6 @@ plugin_routing:
|
|||||||
redirect: dellemc.openmanage.dellemc_idrac
|
redirect: dellemc.openmanage.dellemc_idrac
|
||||||
remote_management.dellemc.ome:
|
remote_management.dellemc.ome:
|
||||||
redirect: dellemc.openmanage.ome
|
redirect: dellemc.openmanage.ome
|
||||||
saslprep:
|
|
||||||
deprecation:
|
|
||||||
removal_version: 13.0.0
|
|
||||||
warning_text: This code is not used by community.general. If you want to use it in another collection, please copy it over.
|
|
||||||
inventory:
|
inventory:
|
||||||
docker_machine:
|
docker_machine:
|
||||||
redirect: community.docker.docker_machine
|
redirect: community.docker.docker_machine
|
||||||
|
|||||||
21
noxfile.py
21
noxfile.py
@@ -6,14 +6,10 @@
|
|||||||
# dependencies = ["nox>=2025.02.09", "antsibull-nox"]
|
# dependencies = ["nox>=2025.02.09", "antsibull-nox"]
|
||||||
# ///
|
# ///
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import nox
|
import nox
|
||||||
|
|
||||||
# Whether the noxfile is running in CI:
|
|
||||||
IN_CI = os.environ.get("CI") == "true"
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import antsibull_nox
|
import antsibull_nox
|
||||||
@@ -36,23 +32,6 @@ def botmeta(session: nox.Session) -> None:
|
|||||||
session.run("python", "tests/sanity/extra/botmeta.py")
|
session.run("python", "tests/sanity/extra/botmeta.py")
|
||||||
|
|
||||||
|
|
||||||
@nox.session(name="ansible-output", default=False)
|
|
||||||
def ansible_output(session: nox.Session) -> None:
|
|
||||||
session.install(
|
|
||||||
"ansible-core",
|
|
||||||
"antsibull-docs",
|
|
||||||
# Needed libs for some code blocks:
|
|
||||||
"jc",
|
|
||||||
"hashids",
|
|
||||||
# Tools for post-processing
|
|
||||||
"ruamel.yaml", # used by docs/docsite/reformat-yaml.py
|
|
||||||
)
|
|
||||||
args = []
|
|
||||||
if IN_CI:
|
|
||||||
args.append("--check")
|
|
||||||
session.run("antsibull-docs", "ansible-output", *args, *session.posargs)
|
|
||||||
|
|
||||||
|
|
||||||
# Allow to run the noxfile with `python noxfile.py`, `pipx run noxfile.py`, or similar.
|
# Allow to run the noxfile with `python noxfile.py`, `pipx run noxfile.py`, or similar.
|
||||||
# Requires nox >= 2025.02.09
|
# Requires nox >= 2025.02.09
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2020, quidame <quidame@poivron.org>
|
# Copyright (c) 2020, quidame <quidame@poivron.org>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -5,7 +6,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from ansible.plugins.action import ActionBase
|
from ansible.plugins.action import ActionBase
|
||||||
from ansible.errors import AnsibleActionFail, AnsibleConnectionFailure
|
from ansible.errors import AnsibleActionFail, AnsibleConnectionFailure
|
||||||
@@ -16,85 +16,75 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
class ActionModule(ActionBase):
|
||||||
|
|
||||||
# Keep internal params away from user interactions
|
# Keep internal params away from user interactions
|
||||||
_VALID_ARGS = frozenset(("path", "state", "table", "noflush", "counters", "modprobe", "ip_version", "wait"))
|
_VALID_ARGS = frozenset(('path', 'state', 'table', 'noflush', 'counters', 'modprobe', 'ip_version', 'wait'))
|
||||||
DEFAULT_SUDOABLE = True
|
DEFAULT_SUDOABLE = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def msg_error__async_and_poll_not_zero(task_poll, task_async, max_timeout) -> str:
|
def msg_error__async_and_poll_not_zero(task_poll, task_async, max_timeout):
|
||||||
return (
|
return (
|
||||||
"This module doesn't support async>0 and poll>0 when its 'state' param "
|
"This module doesn't support async>0 and poll>0 when its 'state' param "
|
||||||
"is set to 'restored'. To enable its rollback feature (that needs the "
|
"is set to 'restored'. To enable its rollback feature (that needs the "
|
||||||
"module to run asynchronously on the remote), please set task attribute "
|
"module to run asynchronously on the remote), please set task attribute "
|
||||||
f"'poll' (={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
|
f"'poll' (={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
|
||||||
f"'ansible_timeout' (={max_timeout}) (recommended)."
|
f"'ansible_timeout' (={max_timeout}) (recommended).")
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def msg_warning__no_async_is_no_rollback(task_poll, task_async, max_timeout) -> str:
|
def msg_warning__no_async_is_no_rollback(task_poll, task_async, max_timeout):
|
||||||
return (
|
return (
|
||||||
"Attempts to restore iptables state without rollback in case of mistake "
|
"Attempts to restore iptables state without rollback in case of mistake "
|
||||||
"may lead the ansible controller to loose access to the hosts and never "
|
"may lead the ansible controller to loose access to the hosts and never "
|
||||||
"regain it before fixing firewall rules through a serial console, or any "
|
"regain it before fixing firewall rules through a serial console, or any "
|
||||||
f"other way except SSH. Please set task attribute 'poll' (={task_poll}) to 0, and "
|
f"other way except SSH. Please set task attribute 'poll' (={task_poll}) to 0, and "
|
||||||
f"'async' (={task_async}) to a value >2 and not greater than 'ansible_timeout' (={max_timeout}) "
|
f"'async' (={task_async}) to a value >2 and not greater than 'ansible_timeout' (={max_timeout}) "
|
||||||
"(recommended)."
|
"(recommended).")
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def msg_warning__async_greater_than_timeout(task_poll, task_async, max_timeout) -> str:
|
def msg_warning__async_greater_than_timeout(task_poll, task_async, max_timeout):
|
||||||
return (
|
return (
|
||||||
"You attempt to restore iptables state with rollback in case of mistake, "
|
"You attempt to restore iptables state with rollback in case of mistake, "
|
||||||
"but with settings that will lead this rollback to happen AFTER that the "
|
"but with settings that will lead this rollback to happen AFTER that the "
|
||||||
"controller will reach its own timeout. Please set task attribute 'poll' "
|
"controller will reach its own timeout. Please set task attribute 'poll' "
|
||||||
f"(={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
|
f"(={task_poll}) to 0, and 'async' (={task_async}) to a value >2 and not greater than "
|
||||||
f"'ansible_timeout' (={max_timeout}) (recommended)."
|
f"'ansible_timeout' (={max_timeout}) (recommended).")
|
||||||
)
|
|
||||||
|
|
||||||
def _async_result(
|
def _async_result(self, async_status_args, task_vars, timeout):
|
||||||
self, async_status_args: dict[str, t.Any], task_vars: dict[str, t.Any], timeout: int
|
'''
|
||||||
) -> dict[str, t.Any]:
|
|
||||||
"""
|
|
||||||
Retrieve results of the asynchronous task, and display them in place of
|
Retrieve results of the asynchronous task, and display them in place of
|
||||||
the async wrapper results (those with the ansible_job_id key).
|
the async wrapper results (those with the ansible_job_id key).
|
||||||
"""
|
'''
|
||||||
async_status = self._task.copy()
|
async_status = self._task.copy()
|
||||||
async_status.args = async_status_args
|
async_status.args = async_status_args
|
||||||
async_status.action = "ansible.builtin.async_status"
|
async_status.action = 'ansible.builtin.async_status'
|
||||||
async_status.async_val = 0
|
async_status.async_val = 0
|
||||||
async_action = self._shared_loader_obj.action_loader.get(
|
async_action = self._shared_loader_obj.action_loader.get(
|
||||||
async_status.action,
|
async_status.action, task=async_status, connection=self._connection,
|
||||||
task=async_status,
|
play_context=self._play_context, loader=self._loader, templar=self._templar,
|
||||||
connection=self._connection,
|
shared_loader_obj=self._shared_loader_obj)
|
||||||
play_context=self._play_context,
|
|
||||||
loader=self._loader,
|
|
||||||
templar=self._templar,
|
|
||||||
shared_loader_obj=self._shared_loader_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
if async_status.args["mode"] == "cleanup":
|
if async_status.args['mode'] == 'cleanup':
|
||||||
return async_action.run(task_vars=task_vars)
|
return async_action.run(task_vars=task_vars)
|
||||||
|
|
||||||
# At least one iteration is required, even if timeout is 0.
|
# At least one iteration is required, even if timeout is 0.
|
||||||
for dummy in range(max(1, timeout)):
|
for dummy in range(max(1, timeout)):
|
||||||
async_result = async_action.run(task_vars=task_vars)
|
async_result = async_action.run(task_vars=task_vars)
|
||||||
if async_result.get("finished", 0) == 1:
|
if async_result.get('finished', 0) == 1:
|
||||||
break
|
break
|
||||||
time.sleep(min(1, timeout))
|
time.sleep(min(1, timeout))
|
||||||
|
|
||||||
return async_result
|
return async_result
|
||||||
|
|
||||||
def run(self, tmp: str | None = None, task_vars: dict[str, t.Any] | None = None) -> dict[str, t.Any]:
|
def run(self, tmp=None, task_vars=None):
|
||||||
|
|
||||||
self._supports_check_mode = True
|
self._supports_check_mode = True
|
||||||
self._supports_async = True
|
self._supports_async = True
|
||||||
|
|
||||||
if task_vars is None:
|
result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
task_vars = {}
|
|
||||||
|
|
||||||
result = super().run(tmp, task_vars)
|
|
||||||
del tmp # tmp no longer has any effect
|
del tmp # tmp no longer has any effect
|
||||||
|
|
||||||
if not result.get("skipped"):
|
if not result.get('skipped'):
|
||||||
|
|
||||||
# FUTURE: better to let _execute_module calculate this internally?
|
# FUTURE: better to let _execute_module calculate this internally?
|
||||||
wrap_async = self._task.async_val and not self._connection.has_native_async
|
wrap_async = self._task.async_val and not self._connection.has_native_async
|
||||||
|
|
||||||
@@ -109,38 +99,41 @@ class ActionModule(ActionBase):
|
|||||||
starter_cmd = None
|
starter_cmd = None
|
||||||
confirm_cmd = None
|
confirm_cmd = None
|
||||||
|
|
||||||
if module_args.get("state", None) == "restored":
|
if module_args.get('state', None) == 'restored':
|
||||||
if not wrap_async:
|
if not wrap_async:
|
||||||
if not check_mode:
|
if not check_mode:
|
||||||
display.warning(self.msg_error__async_and_poll_not_zero(task_poll, task_async, max_timeout))
|
display.warning(self.msg_error__async_and_poll_not_zero(
|
||||||
|
task_poll,
|
||||||
|
task_async,
|
||||||
|
max_timeout))
|
||||||
elif task_poll:
|
elif task_poll:
|
||||||
raise AnsibleActionFail(
|
raise AnsibleActionFail(self.msg_warning__no_async_is_no_rollback(
|
||||||
self.msg_warning__no_async_is_no_rollback(task_poll, task_async, max_timeout)
|
task_poll,
|
||||||
)
|
task_async,
|
||||||
|
max_timeout))
|
||||||
else:
|
else:
|
||||||
if task_async > max_timeout and not check_mode:
|
if task_async > max_timeout and not check_mode:
|
||||||
display.warning(
|
display.warning(self.msg_warning__async_greater_than_timeout(
|
||||||
self.msg_warning__async_greater_than_timeout(task_poll, task_async, max_timeout)
|
task_poll,
|
||||||
)
|
task_async,
|
||||||
|
max_timeout))
|
||||||
|
|
||||||
# inject the async directory based on the shell option into the
|
# inject the async directory based on the shell option into the
|
||||||
# module args
|
# module args
|
||||||
async_dir = self.get_shell_option("async_dir", default="~/.ansible_async")
|
async_dir = self.get_shell_option('async_dir', default="~/.ansible_async")
|
||||||
|
|
||||||
# Bind the loop max duration to consistent values on both
|
# Bind the loop max duration to consistent values on both
|
||||||
# remote and local sides (if not the same, make the loop
|
# remote and local sides (if not the same, make the loop
|
||||||
# longer on the controller); and set a backup file path.
|
# longer on the controller); and set a backup file path.
|
||||||
module_args["_timeout"] = task_async
|
module_args['_timeout'] = task_async
|
||||||
module_args["_back"] = f"{async_dir}/iptables.state"
|
module_args['_back'] = f'{async_dir}/iptables.state'
|
||||||
async_status_args = dict(mode="status")
|
async_status_args = dict(mode='status')
|
||||||
confirm_cmd = f"rm -f {module_args['_back']}"
|
confirm_cmd = f"rm -f {module_args['_back']}"
|
||||||
starter_cmd = f"touch {module_args['_back']}.starter"
|
starter_cmd = f"touch {module_args['_back']}.starter"
|
||||||
remaining_time = max(task_async, max_timeout)
|
remaining_time = max(task_async, max_timeout)
|
||||||
|
|
||||||
# do work!
|
# do work!
|
||||||
result = merge_hash(
|
result = merge_hash(result, self._execute_module(module_args=module_args, task_vars=task_vars, wrap_async=wrap_async))
|
||||||
result, self._execute_module(module_args=module_args, task_vars=task_vars, wrap_async=wrap_async)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then the 3-steps "go ahead or rollback":
|
# Then the 3-steps "go ahead or rollback":
|
||||||
# 1. Catch early errors of the module (in asynchronous task) if any.
|
# 1. Catch early errors of the module (in asynchronous task) if any.
|
||||||
@@ -148,9 +141,9 @@ class ActionModule(ActionBase):
|
|||||||
# 2. Reset connection to ensure a persistent one will not be reused.
|
# 2. Reset connection to ensure a persistent one will not be reused.
|
||||||
# 3. Confirm the restored state by removing the backup on the remote.
|
# 3. Confirm the restored state by removing the backup on the remote.
|
||||||
# Retrieve the results of the asynchronous task to return them.
|
# Retrieve the results of the asynchronous task to return them.
|
||||||
if "_back" in module_args:
|
if '_back' in module_args:
|
||||||
async_status_args["jid"] = result.get("ansible_job_id", None)
|
async_status_args['jid'] = result.get('ansible_job_id', None)
|
||||||
if async_status_args["jid"] is None:
|
if async_status_args['jid'] is None:
|
||||||
raise AnsibleActionFail("Unable to get 'ansible_job_id'.")
|
raise AnsibleActionFail("Unable to get 'ansible_job_id'.")
|
||||||
|
|
||||||
# Catch early errors due to missing mandatory option, bad
|
# Catch early errors due to missing mandatory option, bad
|
||||||
@@ -164,7 +157,7 @@ class ActionModule(ActionBase):
|
|||||||
|
|
||||||
# As the main command is not yet executed on the target, here
|
# As the main command is not yet executed on the target, here
|
||||||
# 'finished' means 'failed before main command be executed'.
|
# 'finished' means 'failed before main command be executed'.
|
||||||
if not result["finished"]:
|
if not result['finished']:
|
||||||
try:
|
try:
|
||||||
self._connection.reset()
|
self._connection.reset()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -186,16 +179,16 @@ class ActionModule(ActionBase):
|
|||||||
result = merge_hash(result, self._async_result(async_status_args, task_vars, remaining_time))
|
result = merge_hash(result, self._async_result(async_status_args, task_vars, remaining_time))
|
||||||
|
|
||||||
# Cleanup async related stuff and internal params
|
# Cleanup async related stuff and internal params
|
||||||
for key in ("ansible_job_id", "results_file", "started", "finished"):
|
for key in ('ansible_job_id', 'results_file', 'started', 'finished'):
|
||||||
if result.get(key):
|
if result.get(key):
|
||||||
del result[key]
|
del result[key]
|
||||||
|
|
||||||
if result.get("invocation", {}).get("module_args"):
|
if result.get('invocation', {}).get('module_args'):
|
||||||
for key in ("_back", "_timeout", "_async_dir", "jid"):
|
for key in ('_back', '_timeout', '_async_dir', 'jid'):
|
||||||
if result["invocation"]["module_args"].get(key):
|
if result['invocation']['module_args'].get(key):
|
||||||
del result["invocation"]["module_args"][key]
|
del result['invocation']['module_args'][key]
|
||||||
|
|
||||||
async_status_args["mode"] = "cleanup"
|
async_status_args['mode'] = 'cleanup'
|
||||||
dummy = self._async_result(async_status_args, task_vars, 0)
|
dummy = self._async_result(async_status_args, task_vars, 0)
|
||||||
|
|
||||||
if not wrap_async:
|
if not wrap_async:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2020, Amin Vakil <info@aminvakil.com>
|
# Copyright (c) 2020, Amin Vakil <info@aminvakil.com>
|
||||||
# Copyright (c) 2016-2018, Matt Davis <mdavis@ansible.com>
|
# Copyright (c) 2016-2018, Matt Davis <mdavis@ansible.com>
|
||||||
# Copyright (c) 2018, Sam Doran <sdoran@redhat.com>
|
# Copyright (c) 2018, Sam Doran <sdoran@redhat.com>
|
||||||
@@ -7,117 +8,120 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||||
from ansible.module_utils.common.collections import is_string
|
from ansible.module_utils.common.collections import is_string
|
||||||
from ansible.plugins.action import ActionBase
|
from ansible.plugins.action import ActionBase
|
||||||
from ansible.utils.display import Display
|
from ansible.utils.display import Display
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
|
|
||||||
class Distribution(t.TypedDict):
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
family: str
|
|
||||||
|
|
||||||
|
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(mapping, key):
|
||||||
|
return to_native(mapping[key]).strip()
|
||||||
|
|
||||||
|
|
||||||
class TimedOutException(Exception):
|
class TimedOutException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ActionModule(ActionBase):
|
class ActionModule(ActionBase):
|
||||||
TRANSFERS_FILES = False
|
TRANSFERS_FILES = False
|
||||||
_VALID_ARGS = frozenset(("msg", "delay", "search_paths"))
|
_VALID_ARGS = frozenset((
|
||||||
|
'msg',
|
||||||
|
'delay',
|
||||||
|
'search_paths'
|
||||||
|
))
|
||||||
|
|
||||||
DEFAULT_CONNECT_TIMEOUT = None
|
DEFAULT_CONNECT_TIMEOUT = None
|
||||||
DEFAULT_PRE_SHUTDOWN_DELAY = 0
|
DEFAULT_PRE_SHUTDOWN_DELAY = 0
|
||||||
DEFAULT_SHUTDOWN_MESSAGE = "Shut down initiated by Ansible"
|
DEFAULT_SHUTDOWN_MESSAGE = 'Shut down initiated by Ansible'
|
||||||
DEFAULT_SHUTDOWN_COMMAND = "shutdown"
|
DEFAULT_SHUTDOWN_COMMAND = 'shutdown'
|
||||||
DEFAULT_SHUTDOWN_COMMAND_ARGS = '-h {delay_min} "{message}"'
|
DEFAULT_SHUTDOWN_COMMAND_ARGS = '-h {delay_min} "{message}"'
|
||||||
DEFAULT_SUDOABLE = True
|
DEFAULT_SUDOABLE = True
|
||||||
|
|
||||||
SHUTDOWN_COMMANDS = {
|
SHUTDOWN_COMMANDS = {
|
||||||
"alpine": "poweroff",
|
'alpine': 'poweroff',
|
||||||
"vmkernel": "halt",
|
'vmkernel': 'halt',
|
||||||
}
|
}
|
||||||
|
|
||||||
SHUTDOWN_COMMAND_ARGS = {
|
SHUTDOWN_COMMAND_ARGS = {
|
||||||
"alpine": "",
|
'alpine': '',
|
||||||
"void": '-h +{delay_min} "{message}"',
|
'void': '-h +{delay_min} "{message}"',
|
||||||
"freebsd": '-p +{delay_sec}s "{message}"',
|
'freebsd': '-p +{delay_sec}s "{message}"',
|
||||||
"linux": DEFAULT_SHUTDOWN_COMMAND_ARGS,
|
'linux': DEFAULT_SHUTDOWN_COMMAND_ARGS,
|
||||||
"macosx": '-h +{delay_min} "{message}"',
|
'macosx': '-h +{delay_min} "{message}"',
|
||||||
"openbsd": '-h +{delay_min} "{message}"',
|
'openbsd': '-h +{delay_min} "{message}"',
|
||||||
"solaris": '-y -g {delay_sec} -i 5 "{message}"',
|
'solaris': '-y -g {delay_sec} -i 5 "{message}"',
|
||||||
"sunos": '-y -g {delay_sec} -i 5 "{message}"',
|
'sunos': '-y -g {delay_sec} -i 5 "{message}"',
|
||||||
"vmkernel": "-d {delay_sec}",
|
'vmkernel': '-d {delay_sec}',
|
||||||
"aix": "-Fh",
|
'aix': '-Fh',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(ActionModule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def delay(self):
|
def delay(self):
|
||||||
return self._check_delay("delay", self.DEFAULT_PRE_SHUTDOWN_DELAY)
|
return self._check_delay('delay', self.DEFAULT_PRE_SHUTDOWN_DELAY)
|
||||||
|
|
||||||
def _check_delay(self, key: str, default: int) -> int:
|
def _check_delay(self, key, default):
|
||||||
"""Ensure that the value is positive or zero"""
|
"""Ensure that the value is positive or zero"""
|
||||||
value = int(self._task.args.get(key, default))
|
value = int(self._task.args.get(key, default))
|
||||||
if value < 0:
|
if value < 0:
|
||||||
value = 0
|
value = 0
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
def _get_value_from_facts(self, variable_name, distribution, default_value):
|
||||||
def _get_value_from_facts(data: dict[str, str], distribution: Distribution, default_value: str) -> str:
|
|
||||||
"""Get dist+version specific args first, then distribution, then family, lastly use default"""
|
"""Get dist+version specific args first, then distribution, then family, lastly use default"""
|
||||||
return data.get(
|
attr = getattr(self, variable_name)
|
||||||
distribution["name"] + distribution["version"],
|
value = attr.get(
|
||||||
data.get(distribution["name"], data.get(distribution["family"], default_value)),
|
distribution['name'] + distribution['version'],
|
||||||
)
|
attr.get(
|
||||||
|
distribution['name'],
|
||||||
|
attr.get(
|
||||||
|
distribution['family'],
|
||||||
|
getattr(self, default_value))))
|
||||||
|
return value
|
||||||
|
|
||||||
def get_distribution(self, task_vars: dict[str, t.Any]) -> Distribution:
|
def get_distribution(self, task_vars):
|
||||||
# FIXME: only execute the module if we don't already have the facts we need
|
# FIXME: only execute the module if we don't already have the facts we need
|
||||||
display.debug(f"{self._task.action}: running setup module to get distribution")
|
distribution = {}
|
||||||
|
display.debug(f'{self._task.action}: running setup module to get distribution')
|
||||||
module_output = self._execute_module(
|
module_output = self._execute_module(
|
||||||
task_vars=task_vars, module_name="ansible.legacy.setup", module_args={"gather_subset": "min"}
|
task_vars=task_vars,
|
||||||
)
|
module_name='ansible.legacy.setup',
|
||||||
|
module_args={'gather_subset': 'min'})
|
||||||
try:
|
try:
|
||||||
if module_output.get("failed", False):
|
if module_output.get('failed', False):
|
||||||
raise AnsibleError(
|
raise AnsibleError(f"Failed to determine system distribution. {fmt(module_output, 'module_stdout')}, {fmt(module_output, 'module_stderr')}")
|
||||||
f"Failed to determine system distribution. {to_native(module_output['module_stdout'])}, {to_native(module_output['module_stderr'])}"
|
distribution['name'] = module_output['ansible_facts']['ansible_distribution'].lower()
|
||||||
)
|
distribution['version'] = to_text(
|
||||||
distribution: Distribution = {
|
module_output['ansible_facts']['ansible_distribution_version'].split('.')[0])
|
||||||
"name": module_output["ansible_facts"]["ansible_distribution"].lower(),
|
distribution['family'] = to_text(module_output['ansible_facts']['ansible_os_family'].lower())
|
||||||
"version": to_text(module_output["ansible_facts"]["ansible_distribution_version"].split(".")[0]),
|
|
||||||
"family": to_text(module_output["ansible_facts"]["ansible_os_family"].lower()),
|
|
||||||
}
|
|
||||||
display.debug(f"{self._task.action}: distribution: {distribution}")
|
display.debug(f"{self._task.action}: distribution: {distribution}")
|
||||||
return distribution
|
return distribution
|
||||||
except KeyError as ke:
|
except KeyError as ke:
|
||||||
raise AnsibleError(f'Failed to get distribution information. Missing "{ke.args[0]}" in output.') from ke
|
raise AnsibleError(f'Failed to get distribution information. Missing "{ke.args[0]}" in output.')
|
||||||
|
|
||||||
def get_shutdown_command(self, task_vars: dict[str, t.Any], distribution: Distribution) -> str:
|
def get_shutdown_command(self, task_vars, distribution):
|
||||||
def find_command(command: str, find_search_paths: list[str]) -> list[str]:
|
def find_command(command, find_search_paths):
|
||||||
display.debug(
|
display.debug(f'{self._task.action}: running find module looking in {find_search_paths} to get path for "{command}"')
|
||||||
f'{self._task.action}: running find module looking in {find_search_paths} to get path for "{command}"'
|
|
||||||
)
|
|
||||||
find_result = self._execute_module(
|
find_result = self._execute_module(
|
||||||
task_vars=task_vars,
|
task_vars=task_vars,
|
||||||
# prevent collection search by calling with ansible.legacy (still allows library/ override of find)
|
# prevent collection search by calling with ansible.legacy (still allows library/ override of find)
|
||||||
module_name="ansible.legacy.find",
|
module_name='ansible.legacy.find',
|
||||||
module_args={"paths": find_search_paths, "patterns": [command], "file_type": "any"},
|
module_args={
|
||||||
|
'paths': find_search_paths,
|
||||||
|
'patterns': [command],
|
||||||
|
'file_type': 'any'
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return [x["path"] for x in find_result["files"]]
|
return [x['path'] for x in find_result['files']]
|
||||||
|
|
||||||
shutdown_bin = self._get_value_from_facts(self.SHUTDOWN_COMMANDS, distribution, self.DEFAULT_SHUTDOWN_COMMAND)
|
shutdown_bin = self._get_value_from_facts('SHUTDOWN_COMMANDS', distribution, 'DEFAULT_SHUTDOWN_COMMAND')
|
||||||
default_search_paths = ["/sbin", "/usr/sbin", "/usr/local/sbin"]
|
default_search_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
|
||||||
search_paths = self._task.args.get("search_paths", default_search_paths)
|
search_paths = self._task.args.get('search_paths', default_search_paths)
|
||||||
|
|
||||||
# FIXME: switch all this to user arg spec validation methods when they are available
|
# FIXME: switch all this to user arg spec validation methods when they are available
|
||||||
# Convert bare strings to a list
|
# Convert bare strings to a list
|
||||||
@@ -128,38 +132,36 @@ class ActionModule(ActionBase):
|
|||||||
incorrect_type = any(not is_string(x) for x in search_paths)
|
incorrect_type = any(not is_string(x) for x in search_paths)
|
||||||
if not isinstance(search_paths, list) or incorrect_type:
|
if not isinstance(search_paths, list) or incorrect_type:
|
||||||
raise TypeError
|
raise TypeError
|
||||||
except TypeError as e:
|
except TypeError:
|
||||||
# Error if we didn't get a list
|
# Error if we didn't get a list
|
||||||
err_msg = f"'search_paths' must be a string or flat list of strings, got {search_paths}"
|
err_msg = f"'search_paths' must be a string or flat list of strings, got {search_paths}"
|
||||||
raise AnsibleError(err_msg) from e
|
raise AnsibleError(err_msg)
|
||||||
|
|
||||||
full_path = find_command(shutdown_bin, search_paths) # find the path to the shutdown command
|
full_path = find_command(shutdown_bin, search_paths) # find the path to the shutdown command
|
||||||
if not full_path: # if we could not find the shutdown command
|
if not full_path: # if we could not find the shutdown command
|
||||||
|
|
||||||
# tell the user we will try with systemd
|
# tell the user we will try with systemd
|
||||||
display.vvv(
|
display.vvv(f'Unable to find command "{shutdown_bin}" in search paths: {search_paths}, will attempt a shutdown using systemd directly.')
|
||||||
f'Unable to find command "{shutdown_bin}" in search paths: {search_paths}, will attempt a shutdown using systemd directly.'
|
systemctl_search_paths = ['/bin', '/usr/bin']
|
||||||
)
|
full_path = find_command('systemctl', systemctl_search_paths) # find the path to the systemctl command
|
||||||
systemctl_search_paths = ["/bin", "/usr/bin"]
|
|
||||||
full_path = find_command("systemctl", systemctl_search_paths) # find the path to the systemctl command
|
|
||||||
if not full_path: # if we couldn't find systemctl
|
if not full_path: # if we couldn't find systemctl
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
f'Could not find command "{shutdown_bin}" in search paths: {search_paths} or systemctl'
|
f'Could not find command "{shutdown_bin}" in search paths: {search_paths} or systemctl'
|
||||||
f" command in search paths: {systemctl_search_paths}, unable to shutdown."
|
f' command in search paths: {systemctl_search_paths}, unable to shutdown.') # we give up here
|
||||||
) # we give up here
|
|
||||||
else:
|
else:
|
||||||
return f"{full_path[0]} poweroff" # done, since we cannot use args with systemd shutdown
|
return f"{full_path[0]} poweroff" # done, since we cannot use args with systemd shutdown
|
||||||
|
|
||||||
# systemd case taken care of, here we add args to the command
|
# systemd case taken care of, here we add args to the command
|
||||||
args = self._get_value_from_facts(self.SHUTDOWN_COMMAND_ARGS, distribution, self.DEFAULT_SHUTDOWN_COMMAND_ARGS)
|
args = self._get_value_from_facts('SHUTDOWN_COMMAND_ARGS', distribution, 'DEFAULT_SHUTDOWN_COMMAND_ARGS')
|
||||||
# Convert seconds to minutes. If less that 60, set it to 0.
|
# Convert seconds to minutes. If less that 60, set it to 0.
|
||||||
delay_sec = self.delay
|
delay_sec = self.delay
|
||||||
shutdown_message = self._task.args.get("msg", self.DEFAULT_SHUTDOWN_MESSAGE)
|
shutdown_message = self._task.args.get('msg', self.DEFAULT_SHUTDOWN_MESSAGE)
|
||||||
|
|
||||||
af = args.format(delay_sec=delay_sec, delay_min=delay_sec // 60, message=shutdown_message)
|
af = args.format(delay_sec=delay_sec, delay_min=delay_sec // 60, message=shutdown_message)
|
||||||
return f"{full_path[0]} {af}"
|
return f'{full_path[0]} {af}'
|
||||||
|
|
||||||
def perform_shutdown(self, task_vars, distribution) -> dict[str, t.Any]:
|
def perform_shutdown(self, task_vars, distribution):
|
||||||
result: dict[str, t.Any] = {}
|
result = {}
|
||||||
shutdown_result = {}
|
shutdown_result = {}
|
||||||
shutdown_command_exec = self.get_shutdown_command(task_vars, distribution)
|
shutdown_command_exec = self.get_shutdown_command(task_vars, distribution)
|
||||||
|
|
||||||
@@ -168,41 +170,40 @@ class ActionModule(ActionBase):
|
|||||||
display.vvv(f"{self._task.action}: shutting down server...")
|
display.vvv(f"{self._task.action}: shutting down server...")
|
||||||
display.debug(f"{self._task.action}: shutting down server with command '{shutdown_command_exec}'")
|
display.debug(f"{self._task.action}: shutting down server with command '{shutdown_command_exec}'")
|
||||||
if self._play_context.check_mode:
|
if self._play_context.check_mode:
|
||||||
shutdown_result["rc"] = 0
|
shutdown_result['rc'] = 0
|
||||||
else:
|
else:
|
||||||
shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
|
shutdown_result = self._low_level_execute_command(shutdown_command_exec, sudoable=self.DEFAULT_SUDOABLE)
|
||||||
except AnsibleConnectionFailure as e:
|
except AnsibleConnectionFailure as e:
|
||||||
# If the connection is closed too quickly due to the system being shutdown, carry on
|
# If the connection is closed too quickly due to the system being shutdown, carry on
|
||||||
display.debug(f"{self._task.action}: AnsibleConnectionFailure caught and handled: {e}")
|
display.debug(
|
||||||
shutdown_result["rc"] = 0
|
f'{self._task.action}: AnsibleConnectionFailure caught and handled: {e}')
|
||||||
|
shutdown_result['rc'] = 0
|
||||||
|
|
||||||
if shutdown_result["rc"] != 0:
|
if shutdown_result['rc'] != 0:
|
||||||
result["failed"] = True
|
result['failed'] = True
|
||||||
result["shutdown"] = False
|
result['shutdown'] = False
|
||||||
result["msg"] = (
|
result['msg'] = f"Shutdown command failed. Error was {fmt(shutdown_result, 'stdout')}, {fmt(shutdown_result, 'stderr')}"
|
||||||
f"Shutdown command failed. Error was {to_native(shutdown_result['stdout'])}, {to_native(shutdown_result['stderr'])}"
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result["failed"] = False
|
result['failed'] = False
|
||||||
result["shutdown_command"] = shutdown_command_exec
|
result['shutdown_command'] = shutdown_command_exec
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def run(self, tmp: str | None = None, task_vars: dict[str, t.Any] | None = None) -> dict[str, t.Any]:
|
def run(self, tmp=None, task_vars=None):
|
||||||
self._supports_check_mode = True
|
self._supports_check_mode = True
|
||||||
self._supports_async = True
|
self._supports_async = True
|
||||||
|
|
||||||
# If running with local connection, fail so we don't shutdown ourself
|
# If running with local connection, fail so we don't shutdown ourself
|
||||||
if self._connection.transport == "local" and (not self._play_context.check_mode):
|
if self._connection.transport == 'local' and (not self._play_context.check_mode):
|
||||||
msg = f"Running {self._task.action} with local connection would shutdown the control node."
|
msg = f'Running {self._task.action} with local connection would shutdown the control node.'
|
||||||
return {"changed": False, "elapsed": 0, "shutdown": False, "failed": True, "msg": msg}
|
return {'changed': False, 'elapsed': 0, 'shutdown': False, 'failed': True, 'msg': msg}
|
||||||
|
|
||||||
if task_vars is None:
|
if task_vars is None:
|
||||||
task_vars = {}
|
task_vars = {}
|
||||||
|
|
||||||
result = super().run(tmp, task_vars)
|
result = super(ActionModule, self).run(tmp, task_vars)
|
||||||
|
|
||||||
if result.get("skipped", False) or result.get("failed", False):
|
if result.get('skipped', False) or result.get('failed', False):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
distribution = self.get_distribution(task_vars)
|
distribution = self.get_distribution(task_vars)
|
||||||
@@ -210,12 +211,12 @@ class ActionModule(ActionBase):
|
|||||||
# Initiate shutdown
|
# Initiate shutdown
|
||||||
shutdown_result = self.perform_shutdown(task_vars, distribution)
|
shutdown_result = self.perform_shutdown(task_vars, distribution)
|
||||||
|
|
||||||
if shutdown_result["failed"]:
|
if shutdown_result['failed']:
|
||||||
result = shutdown_result
|
result = shutdown_result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result["shutdown"] = True
|
result['shutdown'] = True
|
||||||
result["changed"] = True
|
result['changed'] = True
|
||||||
result["shutdown_command"] = shutdown_result["shutdown_command"]
|
result['shutdown_command'] = shutdown_result['shutdown_command']
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -94,44 +95,45 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.doas"
|
|
||||||
|
name = 'community.general.doas'
|
||||||
|
|
||||||
# messages for detecting prompted password issues
|
# messages for detecting prompted password issues
|
||||||
fail = ("Permission denied",)
|
fail = ('Permission denied',)
|
||||||
missing = ("Authorization required",)
|
missing = ('Authorization required',)
|
||||||
|
|
||||||
# See https://github.com/ansible-collections/community.general/issues/9977,
|
# See https://github.com/ansible-collections/community.general/issues/9977,
|
||||||
# https://github.com/ansible/ansible/pull/78111
|
# https://github.com/ansible/ansible/pull/78111
|
||||||
pipelining = False
|
pipelining = False
|
||||||
|
|
||||||
def check_password_prompt(self, b_output):
|
def check_password_prompt(self, b_output):
|
||||||
"""checks if the expected password prompt exists in b_output"""
|
''' checks if the expected password prompt exists in b_output '''
|
||||||
|
|
||||||
# FIXME: more accurate would be: 'doas (%s@' % remote_user
|
# FIXME: more accurate would be: 'doas (%s@' % remote_user
|
||||||
# however become plugins don't have that information currently
|
# however become plugins don't have that information currently
|
||||||
b_prompts = [to_bytes(p) for p in self.get_option("prompt_l10n")] or [rb"doas \(", rb"Password:"]
|
b_prompts = [to_bytes(p) for p in self.get_option('prompt_l10n')] or [br'doas \(', br'Password:']
|
||||||
b_prompt = b"|".join(b_prompts)
|
b_prompt = b"|".join(b_prompts)
|
||||||
|
|
||||||
return bool(re.match(b_prompt, b_output))
|
return bool(re.match(b_prompt, b_output))
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
self.prompt = True
|
self.prompt = True
|
||||||
|
|
||||||
become_exe = self.get_option("become_exe")
|
become_exe = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
if not self.get_option("become_pass") and "-n" not in flags:
|
if not self.get_option('become_pass') and '-n' not in flags:
|
||||||
flags += " -n"
|
flags += ' -n'
|
||||||
|
|
||||||
become_user = self.get_option("become_user")
|
become_user = self.get_option('become_user')
|
||||||
user = f"-u {become_user}" if become_user else ""
|
user = f'-u {become_user}' if become_user else ''
|
||||||
|
|
||||||
success_cmd = self._build_success_command(cmd, shell, noexe=True)
|
success_cmd = self._build_success_command(cmd, shell, noexe=True)
|
||||||
executable = getattr(shell, "executable", shell.SHELL_FAMILY)
|
executable = getattr(shell, 'executable', shell.SHELL_FAMILY)
|
||||||
|
|
||||||
return f"{become_exe} {flags} {user} {executable} -c {success_cmd}"
|
return f'{become_exe} {flags} {user} {executable} -c {success_cmd}'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -74,25 +75,26 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.dzdo"
|
|
||||||
|
name = 'community.general.dzdo'
|
||||||
|
|
||||||
# messages for detecting prompted password issues
|
# messages for detecting prompted password issues
|
||||||
fail = ("Sorry, try again.",)
|
fail = ('Sorry, try again.',)
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
becomecmd = self.get_option("become_exe")
|
becomecmd = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
if self.get_option("become_pass"):
|
if self.get_option('become_pass'):
|
||||||
self.prompt = f"[dzdo via ansible, key={self._id}] password:"
|
self.prompt = f'[dzdo via ansible, key={self._id}] password:'
|
||||||
flags = f'{flags.replace("-n", "")} -p "{self.prompt}"'
|
flags = f"{flags.replace('-n', '')} -p \"{self.prompt}\""
|
||||||
|
|
||||||
become_user = self.get_option("become_user")
|
become_user = self.get_option('become_user')
|
||||||
user = f"-u {become_user}" if become_user else ""
|
user = f'-u {become_user}' if become_user else ''
|
||||||
|
|
||||||
return f"{becomecmd} {flags} {user} {self._build_success_command(cmd, shell)}"
|
return f"{becomecmd} {flags} {user} {self._build_success_command(cmd, shell)}"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -92,22 +93,24 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.ksu"
|
|
||||||
|
name = 'community.general.ksu'
|
||||||
|
|
||||||
# messages for detecting prompted password issues
|
# messages for detecting prompted password issues
|
||||||
fail = ("Password incorrect",)
|
fail = ('Password incorrect',)
|
||||||
missing = ("No password given",)
|
missing = ('No password given',)
|
||||||
|
|
||||||
def check_password_prompt(self, b_output):
|
def check_password_prompt(self, b_output):
|
||||||
"""checks if the expected password prompt exists in b_output"""
|
''' checks if the expected password prompt exists in b_output '''
|
||||||
|
|
||||||
prompts = self.get_option("prompt_l10n") or ["Kerberos password for .*@.*:"]
|
prompts = self.get_option('prompt_l10n') or ["Kerberos password for .*@.*:"]
|
||||||
b_prompt = b"|".join(to_bytes(p) for p in prompts)
|
b_prompt = b"|".join(to_bytes(p) for p in prompts)
|
||||||
|
|
||||||
return bool(re.match(b_prompt, b_output))
|
return bool(re.match(b_prompt, b_output))
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
|
||||||
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
# Prompt handling for ``ksu`` is more complicated, this
|
# Prompt handling for ``ksu`` is more complicated, this
|
||||||
# is used to satisfy the connection plugin
|
# is used to satisfy the connection plugin
|
||||||
@@ -116,8 +119,8 @@ class BecomeModule(BecomeBase):
|
|||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
exe = self.get_option("become_exe")
|
exe = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
user = self.get_option("become_user")
|
user = self.get_option('become_user')
|
||||||
return f"{exe} {user} {flags} -e {self._build_success_command(cmd, shell)} "
|
return f'{exe} {user} {flags} -e {self._build_success_command(cmd, shell)} '
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -96,15 +97,16 @@ from ansible.plugins.become import BecomeBase
|
|||||||
from ansible.module_utils.common.text.converters import to_bytes
|
from ansible.module_utils.common.text.converters import to_bytes
|
||||||
|
|
||||||
|
|
||||||
ansi_color_codes = re_compile(to_bytes(r"\x1B\[[0-9;]+m"))
|
ansi_color_codes = re_compile(to_bytes(r'\x1B\[[0-9;]+m'))
|
||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.machinectl"
|
|
||||||
|
|
||||||
prompt = "Password: "
|
name = 'community.general.machinectl'
|
||||||
fail = ("==== AUTHENTICATION FAILED ====",)
|
|
||||||
success = ("==== AUTHENTICATION COMPLETE ====",)
|
prompt = 'Password: '
|
||||||
|
fail = ('==== AUTHENTICATION FAILED ====',)
|
||||||
|
success = ('==== AUTHENTICATION COMPLETE ====',)
|
||||||
require_tty = True # see https://github.com/ansible-collections/community.general/issues/6932
|
require_tty = True # see https://github.com/ansible-collections/community.general/issues/6932
|
||||||
|
|
||||||
# See https://github.com/ansible/ansible/issues/81254,
|
# See https://github.com/ansible/ansible/issues/81254,
|
||||||
@@ -116,16 +118,16 @@ class BecomeModule(BecomeBase):
|
|||||||
return ansi_color_codes.sub(b"", line)
|
return ansi_color_codes.sub(b"", line)
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
become = self.get_option("become_exe")
|
become = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
user = self.get_option("become_user")
|
user = self.get_option('become_user')
|
||||||
return f"{become} -q shell {flags} {user}@ {self._build_success_command(cmd, shell)}"
|
return f'{become} -q shell {flags} {user}@ {self._build_success_command(cmd, shell)}'
|
||||||
|
|
||||||
def check_success(self, b_output):
|
def check_success(self, b_output):
|
||||||
b_output = self.remove_ansi_codes(b_output)
|
b_output = self.remove_ansi_codes(b_output)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -86,21 +87,22 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.pbrun"
|
|
||||||
|
|
||||||
prompt = "Password:"
|
name = 'community.general.pbrun'
|
||||||
|
|
||||||
|
prompt = 'Password:'
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
become_exe = self.get_option("become_exe")
|
become_exe = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
become_user = self.get_option("become_user")
|
become_user = self.get_option('become_user')
|
||||||
user = f"-u {become_user}" if become_user else ""
|
user = f'-u {become_user}' if become_user else ''
|
||||||
noexe = not self.get_option("wrap_exe")
|
noexe = not self.get_option('wrap_exe')
|
||||||
|
|
||||||
return f"{become_exe} {flags} {user} {self._build_success_command(cmd, shell, noexe=noexe)}"
|
return f"{become_exe} {flags} {user} {self._build_success_command(cmd, shell, noexe=noexe)}"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -91,16 +92,17 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.pfexec"
|
|
||||||
|
name = 'community.general.pfexec'
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
exe = self.get_option("become_exe")
|
exe = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
noexe = not self.get_option("wrap_exe")
|
noexe = not self.get_option('wrap_exe')
|
||||||
return f"{exe} {flags} {self._build_success_command(cmd, shell, noexe=noexe)}"
|
return f'{exe} {flags} {self._build_success_command(cmd, shell, noexe=noexe)}'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -63,16 +64,17 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.pmrun"
|
|
||||||
prompt = "Enter UPM user password:"
|
name = 'community.general.pmrun'
|
||||||
|
prompt = 'Enter UPM user password:'
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
become = self.get_option("become_exe")
|
become = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
return f"{become} {flags} {shlex_quote(self._build_success_command(cmd, shell))}"
|
return f'{become} {flags} {shlex_quote(self._build_success_command(cmd, shell))}'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2024, Ansible Project
|
# Copyright (c) 2024, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -85,12 +86,15 @@ ansi_color_codes = re_compile(to_bytes(r"\x1B\[[0-9;]+m"))
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
|
|
||||||
name = "community.general.run0"
|
name = "community.general.run0"
|
||||||
|
|
||||||
prompt = "Password: "
|
prompt = "Password: "
|
||||||
fail = ("==== AUTHENTICATION FAILED ====",)
|
fail = ("==== AUTHENTICATION FAILED ====",)
|
||||||
success = ("==== AUTHENTICATION COMPLETE ====",)
|
success = ("==== AUTHENTICATION COMPLETE ====",)
|
||||||
require_tty = True # see https://github.com/ansible-collections/community.general/issues/6932
|
require_tty = (
|
||||||
|
True # see https://github.com/ansible-collections/community.general/issues/6932
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_ansi_codes(line):
|
def remove_ansi_codes(line):
|
||||||
@@ -106,7 +110,9 @@ class BecomeModule(BecomeBase):
|
|||||||
flags = self.get_option("become_flags")
|
flags = self.get_option("become_flags")
|
||||||
user = self.get_option("become_user")
|
user = self.get_option("become_user")
|
||||||
|
|
||||||
return f"{become} --user={user} {flags} {self._build_success_command(cmd, shell)}"
|
return (
|
||||||
|
f"{become} --user={user} {flags} {self._build_success_command(cmd, shell)}"
|
||||||
|
)
|
||||||
|
|
||||||
def check_success(self, b_output):
|
def check_success(self, b_output):
|
||||||
b_output = self.remove_ansi_codes(b_output)
|
b_output = self.remove_ansi_codes(b_output)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ansible Project
|
# Copyright (c) 2018, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -75,19 +76,20 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.sesu"
|
|
||||||
|
|
||||||
prompt = "Please enter your password:"
|
name = 'community.general.sesu'
|
||||||
fail = missing = ("Sorry, try again with sesu.",)
|
|
||||||
|
prompt = 'Please enter your password:'
|
||||||
|
fail = missing = ('Sorry, try again with sesu.',)
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
become = self.get_option("become_exe")
|
become = self.get_option('become_exe')
|
||||||
|
|
||||||
flags = self.get_option("become_flags")
|
flags = self.get_option('become_flags')
|
||||||
user = self.get_option("become_user")
|
user = self.get_option('become_user')
|
||||||
return f"{become} {flags} {user} -c {self._build_success_command(cmd, shell)}"
|
return f'{become} {flags} {user} -c {self._build_success_command(cmd, shell)}'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2021, Ansible Project
|
# Copyright (c) 2021, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -79,33 +80,34 @@ from ansible.plugins.become import BecomeBase
|
|||||||
|
|
||||||
|
|
||||||
class BecomeModule(BecomeBase):
|
class BecomeModule(BecomeBase):
|
||||||
name = "community.general.sudosu"
|
|
||||||
|
name = 'community.general.sudosu'
|
||||||
|
|
||||||
# messages for detecting prompted password issues
|
# messages for detecting prompted password issues
|
||||||
fail = ("Sorry, try again.",)
|
fail = ('Sorry, try again.',)
|
||||||
missing = ("Sorry, a password is required to run sudo", "sudo: a password is required")
|
missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required')
|
||||||
|
|
||||||
def build_become_command(self, cmd, shell):
|
def build_become_command(self, cmd, shell):
|
||||||
super().build_become_command(cmd, shell)
|
super(BecomeModule, self).build_become_command(cmd, shell)
|
||||||
|
|
||||||
if not cmd:
|
if not cmd:
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
becomecmd = "sudo"
|
becomecmd = 'sudo'
|
||||||
|
|
||||||
flags = self.get_option("become_flags") or ""
|
flags = self.get_option('become_flags') or ''
|
||||||
prompt = ""
|
prompt = ''
|
||||||
if self.get_option("become_pass"):
|
if self.get_option('become_pass'):
|
||||||
self.prompt = f"[sudo via ansible, key={self._id}] password:"
|
self.prompt = f'[sudo via ansible, key={self._id}] password:'
|
||||||
if flags: # this could be simplified, but kept as is for now for backwards string matching
|
if flags: # this could be simplified, but kept as is for now for backwards string matching
|
||||||
flags = flags.replace("-n", "")
|
flags = flags.replace('-n', '')
|
||||||
prompt = f'-p "{self.prompt}"'
|
prompt = f'-p "{self.prompt}"'
|
||||||
|
|
||||||
user = self.get_option("become_user") or ""
|
user = self.get_option('become_user') or ''
|
||||||
if user:
|
if user:
|
||||||
user = f"{user}"
|
user = f'{user}'
|
||||||
|
|
||||||
if self.get_option("alt_method"):
|
if self.get_option('alt_method'):
|
||||||
return f"{becomecmd} {flags} {prompt} su -l {user} -c {self._build_success_command(cmd, shell, True)}"
|
return f"{becomecmd} {flags} {prompt} su -l {user} -c {self._build_success_command(cmd, shell, True)}"
|
||||||
else:
|
else:
|
||||||
return f"{becomecmd} {flags} {prompt} su -l {user} {self._build_success_command(cmd, shell)}"
|
return f"{becomecmd} {flags} {prompt} su -l {user} {self._build_success_command(cmd, shell)}"
|
||||||
|
|||||||
25
plugins/cache/memcached.py
vendored
25
plugins/cache/memcached.py
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2014, Brian Coca, Josh Drake, et al
|
# Copyright (c) 2014, Brian Coca, Josh Drake, et al
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -59,7 +60,6 @@ from ansible.utils.display import Display
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import memcache
|
import memcache
|
||||||
|
|
||||||
HAS_MEMCACHE = True
|
HAS_MEMCACHE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_MEMCACHE = False
|
HAS_MEMCACHE = False
|
||||||
@@ -67,7 +67,7 @@ except ImportError:
|
|||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
class ProxyClientPool:
|
class ProxyClientPool(object):
|
||||||
"""
|
"""
|
||||||
Memcached connection pooling for thread/fork safety. Inspired by py-redis
|
Memcached connection pooling for thread/fork safety. Inspired by py-redis
|
||||||
connection pool.
|
connection pool.
|
||||||
@@ -76,7 +76,7 @@ class ProxyClientPool:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.max_connections = kwargs.pop("max_connections", 1024)
|
self.max_connections = kwargs.pop('max_connections', 1024)
|
||||||
self.connection_args = args
|
self.connection_args = args
|
||||||
self.connection_kwargs = kwargs
|
self.connection_kwargs = kwargs
|
||||||
self.reset()
|
self.reset()
|
||||||
@@ -124,7 +124,6 @@ class ProxyClientPool:
|
|||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
return self._proxy_client(name, *args, **kwargs)
|
return self._proxy_client(name, *args, **kwargs)
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def _proxy_client(self, name, *args, **kwargs):
|
def _proxy_client(self, name, *args, **kwargs):
|
||||||
@@ -141,8 +140,7 @@ class CacheModuleKeys(MutableSet):
|
|||||||
A set subclass that keeps track of insertion time and persists
|
A set subclass that keeps track of insertion time and persists
|
||||||
the set in memcached.
|
the set in memcached.
|
||||||
"""
|
"""
|
||||||
|
PREFIX = 'ansible_cache_keys'
|
||||||
PREFIX = "ansible_cache_keys"
|
|
||||||
|
|
||||||
def __init__(self, cache, *args, **kwargs):
|
def __init__(self, cache, *args, **kwargs):
|
||||||
self._cache = cache
|
self._cache = cache
|
||||||
@@ -174,14 +172,15 @@ class CacheModuleKeys(MutableSet):
|
|||||||
|
|
||||||
|
|
||||||
class CacheModule(BaseCacheModule):
|
class CacheModule(BaseCacheModule):
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
connection = ["127.0.0.1:11211"]
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
def __init__(self, *args, **kwargs):
|
||||||
if self.get_option("_uri"):
|
connection = ['127.0.0.1:11211']
|
||||||
connection = self.get_option("_uri")
|
|
||||||
self._timeout = self.get_option("_timeout")
|
super(CacheModule, self).__init__(*args, **kwargs)
|
||||||
self._prefix = self.get_option("_prefix")
|
if self.get_option('_uri'):
|
||||||
|
connection = self.get_option('_uri')
|
||||||
|
self._timeout = self.get_option('_timeout')
|
||||||
|
self._prefix = self.get_option('_prefix')
|
||||||
|
|
||||||
if not HAS_MEMCACHE:
|
if not HAS_MEMCACHE:
|
||||||
raise AnsibleError("python-memcached is required for the memcached fact cache")
|
raise AnsibleError("python-memcached is required for the memcached fact cache")
|
||||||
|
|||||||
13
plugins/cache/pickle.py
vendored
13
plugins/cache/pickle.py
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2017, Brian Coca
|
# Copyright (c) 2017, Brian Coca
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -42,7 +43,10 @@ options:
|
|||||||
type: float
|
type: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pickle
|
try:
|
||||||
|
import cPickle as pickle
|
||||||
|
except ImportError:
|
||||||
|
import pickle
|
||||||
|
|
||||||
from ansible.plugins.cache import BaseFileCacheModule
|
from ansible.plugins.cache import BaseFileCacheModule
|
||||||
|
|
||||||
@@ -51,15 +55,14 @@ class CacheModule(BaseFileCacheModule):
|
|||||||
"""
|
"""
|
||||||
A caching module backed by pickle files.
|
A caching module backed by pickle files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_persistent = False # prevent unnecessary JSON serialization and key munging
|
_persistent = False # prevent unnecessary JSON serialization and key munging
|
||||||
|
|
||||||
def _load(self, filepath):
|
def _load(self, filepath):
|
||||||
# Pickle is a binary format
|
# Pickle is a binary format
|
||||||
with open(filepath, "rb") as f:
|
with open(filepath, 'rb') as f:
|
||||||
return pickle.load(f, encoding="bytes")
|
return pickle.load(f, encoding='bytes')
|
||||||
|
|
||||||
def _dump(self, value, filepath):
|
def _dump(self, value, filepath):
|
||||||
with open(filepath, "wb") as f:
|
with open(filepath, 'wb') as f:
|
||||||
# Use pickle protocol 2 which is compatible with Python 2.3+.
|
# Use pickle protocol 2 which is compatible with Python 2.3+.
|
||||||
pickle.dump(value, f, protocol=2)
|
pickle.dump(value, f, protocol=2)
|
||||||
|
|||||||
61
plugins/cache/redis.py
vendored
61
plugins/cache/redis.py
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2014, Brian Coca, Josh Drake, et al
|
# Copyright (c) 2014, Brian Coca, Josh Drake, et al
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -77,7 +78,6 @@ from ansible.utils.display import Display
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from redis import StrictRedis, VERSION
|
from redis import StrictRedis, VERSION
|
||||||
|
|
||||||
HAS_REDIS = True
|
HAS_REDIS = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_REDIS = False
|
HAS_REDIS = False
|
||||||
@@ -94,35 +94,32 @@ class CacheModule(BaseCacheModule):
|
|||||||
to expire keys. This mechanism is used or a pattern matched 'scan' for
|
to expire keys. This mechanism is used or a pattern matched 'scan' for
|
||||||
performance.
|
performance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_sentinel_service_name = None
|
_sentinel_service_name = None
|
||||||
re_url_conn = re.compile(r"^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$")
|
re_url_conn = re.compile(r'^([^:]+|\[[^]]+\]):(\d+):(\d+)(?::(.*))?$')
|
||||||
re_sent_conn = re.compile(r"^(.*):(\d+)$")
|
re_sent_conn = re.compile(r'^(.*):(\d+)$')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
uri = ""
|
uri = ''
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super(CacheModule, self).__init__(*args, **kwargs)
|
||||||
if self.get_option("_uri"):
|
if self.get_option('_uri'):
|
||||||
uri = self.get_option("_uri")
|
uri = self.get_option('_uri')
|
||||||
self._timeout = float(self.get_option("_timeout"))
|
self._timeout = float(self.get_option('_timeout'))
|
||||||
self._prefix = self.get_option("_prefix")
|
self._prefix = self.get_option('_prefix')
|
||||||
self._keys_set = self.get_option("_keyset_name")
|
self._keys_set = self.get_option('_keyset_name')
|
||||||
self._sentinel_service_name = self.get_option("_sentinel_service_name")
|
self._sentinel_service_name = self.get_option('_sentinel_service_name')
|
||||||
|
|
||||||
if not HAS_REDIS:
|
if not HAS_REDIS:
|
||||||
raise AnsibleError(
|
raise AnsibleError("The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'")
|
||||||
"The 'redis' python module (version 2.4.5 or newer) is required for the redis fact cache, 'pip install redis'"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
kw = {}
|
kw = {}
|
||||||
|
|
||||||
# tls connection
|
# tls connection
|
||||||
tlsprefix = "tls://"
|
tlsprefix = 'tls://'
|
||||||
if uri.startswith(tlsprefix):
|
if uri.startswith(tlsprefix):
|
||||||
kw["ssl"] = True
|
kw['ssl'] = True
|
||||||
uri = uri[len(tlsprefix) :]
|
uri = uri[len(tlsprefix):]
|
||||||
|
|
||||||
# redis sentinel connection
|
# redis sentinel connection
|
||||||
if self._sentinel_service_name:
|
if self._sentinel_service_name:
|
||||||
@@ -132,7 +129,7 @@ class CacheModule(BaseCacheModule):
|
|||||||
connection = self._parse_connection(self.re_url_conn, uri)
|
connection = self._parse_connection(self.re_url_conn, uri)
|
||||||
self._db = StrictRedis(*connection, **kw)
|
self._db = StrictRedis(*connection, **kw)
|
||||||
|
|
||||||
display.vv(f"Redis connection: {self._db}")
|
display.vv(f'Redis connection: {self._db}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_connection(re_patt, uri):
|
def _parse_connection(re_patt, uri):
|
||||||
@@ -147,37 +144,36 @@ class CacheModule(BaseCacheModule):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from redis.sentinel import Sentinel
|
from redis.sentinel import Sentinel
|
||||||
except ImportError as e:
|
except ImportError:
|
||||||
raise AnsibleError(
|
raise AnsibleError("The 'redis' python module (version 2.9.0 or newer) is required to use redis sentinel.")
|
||||||
"The 'redis' python module (version 2.9.0 or newer) is required to use redis sentinel."
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if ";" not in uri:
|
if ';' not in uri:
|
||||||
raise AnsibleError("_uri does not have sentinel syntax.")
|
raise AnsibleError('_uri does not have sentinel syntax.')
|
||||||
|
|
||||||
# format: "localhost:26379;localhost2:26379;0:changeme"
|
# format: "localhost:26379;localhost2:26379;0:changeme"
|
||||||
connections = uri.split(";")
|
connections = uri.split(';')
|
||||||
connection_args = connections.pop(-1)
|
connection_args = connections.pop(-1)
|
||||||
if len(connection_args) > 0: # handle if no db nr is given
|
if len(connection_args) > 0: # handle if no db nr is given
|
||||||
connection_args = connection_args.split(":")
|
connection_args = connection_args.split(':')
|
||||||
kw["db"] = connection_args.pop(0)
|
kw['db'] = connection_args.pop(0)
|
||||||
try:
|
try:
|
||||||
kw["password"] = connection_args.pop(0)
|
kw['password'] = connection_args.pop(0)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass # password is optional
|
pass # password is optional
|
||||||
|
|
||||||
sentinels = [self._parse_connection(self.re_sent_conn, shost) for shost in connections]
|
sentinels = [self._parse_connection(self.re_sent_conn, shost) for shost in connections]
|
||||||
display.vv(f"\nUsing redis sentinels: {sentinels}")
|
display.vv(f'\nUsing redis sentinels: {sentinels}')
|
||||||
scon = Sentinel(sentinels, **kw)
|
scon = Sentinel(sentinels, **kw)
|
||||||
try:
|
try:
|
||||||
return scon.master_for(self._sentinel_service_name, socket_timeout=0.2)
|
return scon.master_for(self._sentinel_service_name, socket_timeout=0.2)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise AnsibleError(f"Could not connect to redis sentinel: {exc}") from exc
|
raise AnsibleError(f'Could not connect to redis sentinel: {exc}')
|
||||||
|
|
||||||
def _make_key(self, key):
|
def _make_key(self, key):
|
||||||
return self._prefix + key
|
return self._prefix + key
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
|
|
||||||
if key not in self._cache:
|
if key not in self._cache:
|
||||||
value = self._db.get(self._make_key(key))
|
value = self._db.get(self._make_key(key))
|
||||||
# guard against the key not being removed from the zset;
|
# guard against the key not being removed from the zset;
|
||||||
@@ -191,6 +187,7 @@ class CacheModule(BaseCacheModule):
|
|||||||
return self._cache.get(key)
|
return self._cache.get(key)
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
|
|
||||||
value2 = json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)
|
value2 = json.dumps(value, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)
|
||||||
if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
|
if self._timeout > 0: # a timeout of 0 is handled as meaning 'never expire'
|
||||||
self._db.setex(self._make_key(key), int(self._timeout), value2)
|
self._db.setex(self._make_key(key), int(self._timeout), value2)
|
||||||
@@ -214,7 +211,7 @@ class CacheModule(BaseCacheModule):
|
|||||||
|
|
||||||
def contains(self, key):
|
def contains(self, key):
|
||||||
self._expire_keys()
|
self._expire_keys()
|
||||||
return self._db.zrank(self._keys_set, key) is not None
|
return (self._db.zrank(self._keys_set, key) is not None)
|
||||||
|
|
||||||
def delete(self, key):
|
def delete(self, key):
|
||||||
if key in self._cache:
|
if key in self._cache:
|
||||||
|
|||||||
5
plugins/cache/yaml.py
vendored
5
plugins/cache/yaml.py
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2017, Brian Coca
|
# Copyright (c) 2017, Brian Coca
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -58,9 +59,9 @@ class CacheModule(BaseFileCacheModule):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _load(self, filepath):
|
def _load(self, filepath):
|
||||||
with open(os.path.abspath(filepath), "r", encoding="utf-8") as f:
|
with open(os.path.abspath(filepath), 'r', encoding='utf-8') as f:
|
||||||
return AnsibleLoader(f).get_single_data()
|
return AnsibleLoader(f).get_single_data()
|
||||||
|
|
||||||
def _dump(self, value, filepath):
|
def _dump(self, value, filepath):
|
||||||
with open(os.path.abspath(filepath), "w", encoding="utf-8") as f:
|
with open(os.path.abspath(filepath), 'w', encoding='utf-8') as f:
|
||||||
yaml.dump(value, f, Dumper=AnsibleDumper, default_flow_style=False)
|
yaml.dump(value, f, Dumper=AnsibleDumper, default_flow_style=False)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018 Matt Martz <matt@sivel.net>
|
# Copyright (c) 2018 Matt Martz <matt@sivel.net>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -49,7 +50,6 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
class MemProf(threading.Thread):
|
class MemProf(threading.Thread):
|
||||||
"""Python thread for recording memory usage"""
|
"""Python thread for recording memory usage"""
|
||||||
|
|
||||||
def __init__(self, path, obj=None):
|
def __init__(self, path, obj=None):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
@@ -67,25 +67,25 @@ class MemProf(threading.Thread):
|
|||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "aggregate"
|
CALLBACK_TYPE = 'aggregate'
|
||||||
CALLBACK_NAME = "community.general.cgroup_memory_recap"
|
CALLBACK_NAME = 'community.general.cgroup_memory_recap'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display)
|
super(CallbackModule, self).__init__(display)
|
||||||
|
|
||||||
self._task_memprof = None
|
self._task_memprof = None
|
||||||
|
|
||||||
self.task_results = []
|
self.task_results = []
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.cgroup_max_file = self.get_option("max_mem_file")
|
self.cgroup_max_file = self.get_option('max_mem_file')
|
||||||
self.cgroup_current_file = self.get_option("cur_mem_file")
|
self.cgroup_current_file = self.get_option('cur_mem_file')
|
||||||
|
|
||||||
with open(self.cgroup_max_file, "w+") as f:
|
with open(self.cgroup_max_file, 'w+') as f:
|
||||||
f.write("0")
|
f.write('0')
|
||||||
|
|
||||||
def _profile_memory(self, obj=None):
|
def _profile_memory(self, obj=None):
|
||||||
prev_task = None
|
prev_task = None
|
||||||
@@ -113,8 +113,8 @@ class CallbackModule(CallbackBase):
|
|||||||
with open(self.cgroup_max_file) as f:
|
with open(self.cgroup_max_file) as f:
|
||||||
max_results = int(f.read().strip()) / 1024 / 1024
|
max_results = int(f.read().strip()) / 1024 / 1024
|
||||||
|
|
||||||
self._display.banner("CGROUP MEMORY RECAP")
|
self._display.banner('CGROUP MEMORY RECAP')
|
||||||
self._display.display(f"Execution Maximum: {max_results:0.2f}MB\n\n")
|
self._display.display(f'Execution Maximum: {max_results:0.2f}MB\n\n')
|
||||||
|
|
||||||
for task, memory in self.task_results:
|
for task, memory in self.task_results:
|
||||||
self._display.display(f"{task.get_name()} ({task._uuid}): {memory:0.2f}MB")
|
self._display.display(f'{task.get_name()} ({task._uuid}): {memory:0.2f}MB')
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
# Copyright (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -25,14 +26,13 @@ class CallbackModule(CallbackBase):
|
|||||||
This is a very trivial example of how any callback function can get at play and task objects.
|
This is a very trivial example of how any callback function can get at play and task objects.
|
||||||
play will be 'None' for runner invocations, and task will be None for 'setup' invocations.
|
play will be 'None' for runner invocations, and task will be None for 'setup' invocations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "aggregate"
|
CALLBACK_TYPE = 'aggregate'
|
||||||
CALLBACK_NAME = "community.general.context_demo"
|
CALLBACK_NAME = 'community.general.context_demo'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super(CallbackModule, self).__init__(*args, **kwargs)
|
||||||
self.task = None
|
self.task = None
|
||||||
self.play = None
|
self.play = None
|
||||||
|
|
||||||
@@ -41,11 +41,11 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
self._display.display(" --- ARGS ")
|
self._display.display(" --- ARGS ")
|
||||||
for i, a in enumerate(args):
|
for i, a in enumerate(args):
|
||||||
self._display.display(f" {i}: {a}")
|
self._display.display(f' {i}: {a}')
|
||||||
|
|
||||||
self._display.display(" --- KWARGS ")
|
self._display.display(" --- KWARGS ")
|
||||||
for k in kwargs:
|
for k in kwargs:
|
||||||
self._display.display(f" {k}: {kwargs[k]}")
|
self._display.display(f' {k}: {kwargs[k]}')
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
self.play = play
|
self.play = play
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Ivan Aragones Muniesa <ivan.aragones.muniesa@gmail.com>
|
# Copyright (c) 2018, Ivan Aragones Muniesa <ivan.aragones.muniesa@gmail.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
"""
|
'''
|
||||||
Counter enabled Ansible callback plugin (See DOCUMENTATION for more information)
|
Counter enabled Ansible callback plugin (See DOCUMENTATION for more information)
|
||||||
"""
|
'''
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -29,14 +30,15 @@ from ansible.playbook.task_include import TaskInclude
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
"""
|
|
||||||
|
'''
|
||||||
This is the default callback interface, which simply prints messages
|
This is the default callback interface, which simply prints messages
|
||||||
to stdout when new callback events are received.
|
to stdout when new callback events are received.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.counter_enabled"
|
CALLBACK_NAME = 'community.general.counter_enabled'
|
||||||
|
|
||||||
_task_counter = 1
|
_task_counter = 1
|
||||||
_task_total = 0
|
_task_total = 0
|
||||||
@@ -46,7 +48,7 @@ class CallbackModule(CallbackBase):
|
|||||||
_previous_batch_total = 0
|
_previous_batch_total = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
self._playbook = ""
|
self._playbook = ""
|
||||||
self._play = ""
|
self._play = ""
|
||||||
@@ -54,7 +56,11 @@ class CallbackModule(CallbackBase):
|
|||||||
def _all_vars(self, host=None, task=None):
|
def _all_vars(self, host=None, task=None):
|
||||||
# host and task need to be specified in case 'magic variables' (host vars, group vars, etc)
|
# host and task need to be specified in case 'magic variables' (host vars, group vars, etc)
|
||||||
# need to be loaded as well
|
# need to be loaded as well
|
||||||
return self._play.get_variable_manager().get_vars(play=self._play, host=host, task=task)
|
return self._play.get_variable_manager().get_vars(
|
||||||
|
play=self._play,
|
||||||
|
host=host,
|
||||||
|
task=task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self._playbook = playbook
|
self._playbook = playbook
|
||||||
@@ -62,7 +68,7 @@ class CallbackModule(CallbackBase):
|
|||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
name = play.get_name().strip()
|
name = play.get_name().strip()
|
||||||
if not name:
|
if not name:
|
||||||
msg = "play"
|
msg = u"play"
|
||||||
else:
|
else:
|
||||||
msg = f"PLAY [{name}]"
|
msg = f"PLAY [{name}]"
|
||||||
|
|
||||||
@@ -72,8 +78,8 @@ class CallbackModule(CallbackBase):
|
|||||||
self._play = play
|
self._play = play
|
||||||
|
|
||||||
self._previous_batch_total = self._current_batch_total
|
self._previous_batch_total = self._current_batch_total
|
||||||
self._current_batch_total = self._previous_batch_total + len(self._all_vars()["vars"]["ansible_play_batch"])
|
self._current_batch_total = self._previous_batch_total + len(self._all_vars()['vars']['ansible_play_batch'])
|
||||||
self._host_total = len(self._all_vars()["vars"]["ansible_play_hosts_all"])
|
self._host_total = len(self._all_vars()['vars']['ansible_play_hosts_all'])
|
||||||
self._task_total = len(self._play.get_tasks()[0])
|
self._task_total = len(self._play.get_tasks()[0])
|
||||||
self._task_counter = 1
|
self._task_counter = 1
|
||||||
|
|
||||||
@@ -88,39 +94,39 @@ class CallbackModule(CallbackBase):
|
|||||||
f"{hostcolor(host, stat)} : {colorize('ok', stat['ok'], C.COLOR_OK)} {colorize('changed', stat['changed'], C.COLOR_CHANGED)} "
|
f"{hostcolor(host, stat)} : {colorize('ok', stat['ok'], C.COLOR_OK)} {colorize('changed', stat['changed'], C.COLOR_CHANGED)} "
|
||||||
f"{colorize('unreachable', stat['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', stat['failures'], C.COLOR_ERROR)} "
|
f"{colorize('unreachable', stat['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', stat['failures'], C.COLOR_ERROR)} "
|
||||||
f"{colorize('rescued', stat['rescued'], C.COLOR_OK)} {colorize('ignored', stat['ignored'], C.COLOR_WARN)}",
|
f"{colorize('rescued', stat['rescued'], C.COLOR_OK)} {colorize('ignored', stat['ignored'], C.COLOR_WARN)}",
|
||||||
screen_only=True,
|
screen_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self._display.display(
|
self._display.display(
|
||||||
f"{hostcolor(host, stat, False)} : {colorize('ok', stat['ok'], None)} {colorize('changed', stat['changed'], None)} "
|
f"{hostcolor(host, stat, False)} : {colorize('ok', stat['ok'], None)} {colorize('changed', stat['changed'], None)} "
|
||||||
f"{colorize('unreachable', stat['unreachable'], None)} {colorize('failed', stat['failures'], None)} "
|
f"{colorize('unreachable', stat['unreachable'], None)} {colorize('failed', stat['failures'], None)} "
|
||||||
f"{colorize('rescued', stat['rescued'], None)} {colorize('ignored', stat['ignored'], None)}",
|
f"{colorize('rescued', stat['rescued'], None)} {colorize('ignored', stat['ignored'], None)}",
|
||||||
log_only=True,
|
log_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self._display.display("", screen_only=True)
|
self._display.display("", screen_only=True)
|
||||||
|
|
||||||
# print custom stats
|
# print custom stats
|
||||||
if self._plugin_options.get("show_custom_stats", C.SHOW_CUSTOM_STATS) and stats.custom:
|
if self._plugin_options.get('show_custom_stats', C.SHOW_CUSTOM_STATS) and stats.custom:
|
||||||
# fallback on constants for inherited plugins missing docs
|
# fallback on constants for inherited plugins missing docs
|
||||||
self._display.banner("CUSTOM STATS: ")
|
self._display.banner("CUSTOM STATS: ")
|
||||||
# per host
|
# per host
|
||||||
# TODO: come up with 'pretty format'
|
# TODO: come up with 'pretty format'
|
||||||
for k in sorted(stats.custom.keys()):
|
for k in sorted(stats.custom.keys()):
|
||||||
if k == "_run":
|
if k == '_run':
|
||||||
continue
|
continue
|
||||||
_custom_stats = self._dump_results(stats.custom[k], indent=1).replace("\n", "")
|
_custom_stats = self._dump_results(stats.custom[k], indent=1).replace('\n', '')
|
||||||
self._display.display(f"\t{k}: {_custom_stats}")
|
self._display.display(f'\t{k}: {_custom_stats}')
|
||||||
|
|
||||||
# print per run custom stats
|
# print per run custom stats
|
||||||
if "_run" in stats.custom:
|
if '_run' in stats.custom:
|
||||||
self._display.display("", screen_only=True)
|
self._display.display("", screen_only=True)
|
||||||
_custom_stats_run = self._dump_results(stats.custom["_run"], indent=1).replace("\n", "")
|
_custom_stats_run = self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')
|
||||||
self._display.display(f"\tRUN: {_custom_stats_run}")
|
self._display.display(f'\tRUN: {_custom_stats_run}')
|
||||||
self._display.display("", screen_only=True)
|
self._display.display("", screen_only=True)
|
||||||
|
|
||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
args = ""
|
args = ''
|
||||||
# args can be specified as no_log in several places: in the task or in
|
# args can be specified as no_log in several places: in the task or in
|
||||||
# the argument spec. We can check whether the task is no_log but the
|
# the argument spec. We can check whether the task is no_log but the
|
||||||
# argument spec can't be because that is only run on the target
|
# argument spec can't be because that is only run on the target
|
||||||
@@ -130,8 +136,8 @@ class CallbackModule(CallbackBase):
|
|||||||
# that they can secure this if they feel that their stdout is insecure
|
# that they can secure this if they feel that their stdout is insecure
|
||||||
# (shoulder surfing, logging stdout straight to a file, etc).
|
# (shoulder surfing, logging stdout straight to a file, etc).
|
||||||
if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
|
if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
|
||||||
args = ", ".join(("{k}={v}" for k, v in task.args.items()))
|
args = ', '.join(('{k}={v}' for k, v in task.args.items()))
|
||||||
args = f" {args}"
|
args = f' {args}'
|
||||||
self._display.banner(f"TASK {self._task_counter}/{self._task_total} [{task.get_name().strip()}{args}]")
|
self._display.banner(f"TASK {self._task_counter}/{self._task_total} [{task.get_name().strip()}{args}]")
|
||||||
if self._display.verbosity >= 2:
|
if self._display.verbosity >= 2:
|
||||||
path = task.get_path()
|
path = task.get_path()
|
||||||
@@ -141,22 +147,25 @@ class CallbackModule(CallbackBase):
|
|||||||
self._task_counter += 1
|
self._task_counter += 1
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
|
|
||||||
self._host_counter += 1
|
self._host_counter += 1
|
||||||
|
|
||||||
delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
|
|
||||||
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
|
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
|
||||||
self._print_task_banner(result._task)
|
self._print_task_banner(result._task)
|
||||||
|
|
||||||
if isinstance(result._task, TaskInclude):
|
if isinstance(result._task, TaskInclude):
|
||||||
return
|
return
|
||||||
elif result._result.get("changed", False):
|
elif result._result.get('changed', False):
|
||||||
if delegated_vars:
|
if delegated_vars:
|
||||||
msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]"
|
msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]"
|
||||||
else:
|
else:
|
||||||
msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()}]"
|
msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()}]"
|
||||||
color = C.COLOR_CHANGED
|
color = C.COLOR_CHANGED
|
||||||
else:
|
else:
|
||||||
|
if not self._plugin_options.get("display_ok_hosts", True):
|
||||||
|
return
|
||||||
if delegated_vars:
|
if delegated_vars:
|
||||||
msg = f"ok: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]"
|
msg = f"ok: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]"
|
||||||
else:
|
else:
|
||||||
@@ -165,7 +174,7 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
self._handle_warnings(result._result)
|
self._handle_warnings(result._result)
|
||||||
|
|
||||||
if result._task.loop and "results" in result._result:
|
if result._task.loop and 'results' in result._result:
|
||||||
self._process_items(result)
|
self._process_items(result)
|
||||||
else:
|
else:
|
||||||
self._clean_results(result._result, result._task.action)
|
self._clean_results(result._result, result._task.action)
|
||||||
@@ -175,18 +184,19 @@ class CallbackModule(CallbackBase):
|
|||||||
self._display.display(msg, color=color)
|
self._display.display(msg, color=color)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
|
|
||||||
self._host_counter += 1
|
self._host_counter += 1
|
||||||
|
|
||||||
delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
self._clean_results(result._result, result._task.action)
|
self._clean_results(result._result, result._task.action)
|
||||||
|
|
||||||
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
|
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
|
||||||
self._print_task_banner(result._task)
|
self._print_task_banner(result._task)
|
||||||
|
|
||||||
self._handle_exception(result._result)
|
self._handle_exception(result._result)
|
||||||
self._handle_warnings(result._result)
|
self._handle_warnings(result._result)
|
||||||
|
|
||||||
if result._task.loop and "results" in result._result:
|
if result._task.loop and 'results' in result._result:
|
||||||
self._process_items(result)
|
self._process_items(result)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -194,12 +204,12 @@ class CallbackModule(CallbackBase):
|
|||||||
self._display.display(
|
self._display.display(
|
||||||
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
|
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
|
||||||
f"{delegated_vars['ansible_host']}]: FAILED! => {self._dump_results(result._result)}",
|
f"{delegated_vars['ansible_host']}]: FAILED! => {self._dump_results(result._result)}",
|
||||||
color=C.COLOR_ERROR,
|
color=C.COLOR_ERROR
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._display.display(
|
self._display.display(
|
||||||
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: FAILED! => {self._dump_results(result._result)}",
|
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: FAILED! => {self._dump_results(result._result)}",
|
||||||
color=C.COLOR_ERROR,
|
color=C.COLOR_ERROR
|
||||||
)
|
)
|
||||||
|
|
||||||
if ignore_errors:
|
if ignore_errors:
|
||||||
@@ -208,15 +218,14 @@ class CallbackModule(CallbackBase):
|
|||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self._host_counter += 1
|
self._host_counter += 1
|
||||||
|
|
||||||
if self._plugin_options.get(
|
if self._plugin_options.get('show_skipped_hosts', C.DISPLAY_SKIPPED_HOSTS): # fallback on constants for inherited plugins missing docs
|
||||||
"show_skipped_hosts", C.DISPLAY_SKIPPED_HOSTS
|
|
||||||
): # fallback on constants for inherited plugins missing docs
|
|
||||||
self._clean_results(result._result, result._task.action)
|
self._clean_results(result._result, result._task.action)
|
||||||
|
|
||||||
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
|
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
|
||||||
self._print_task_banner(result._task)
|
self._print_task_banner(result._task)
|
||||||
|
|
||||||
if result._task.loop and "results" in result._result:
|
if result._task.loop and 'results' in result._result:
|
||||||
self._process_items(result)
|
self._process_items(result)
|
||||||
else:
|
else:
|
||||||
msg = f"skipping: {self._host_counter}/{self._host_total} [{result._host.get_name()}]"
|
msg = f"skipping: {self._host_counter}/{self._host_total} [{result._host.get_name()}]"
|
||||||
@@ -227,18 +236,18 @@ class CallbackModule(CallbackBase):
|
|||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
self._host_counter += 1
|
self._host_counter += 1
|
||||||
|
|
||||||
if self._play.strategy == "free" and self._last_task_banner != result._task._uuid:
|
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
|
||||||
self._print_task_banner(result._task)
|
self._print_task_banner(result._task)
|
||||||
|
|
||||||
delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
if delegated_vars:
|
if delegated_vars:
|
||||||
self._display.display(
|
self._display.display(
|
||||||
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
|
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> "
|
||||||
f"{delegated_vars['ansible_host']}]: UNREACHABLE! => {self._dump_results(result._result)}",
|
f"{delegated_vars['ansible_host']}]: UNREACHABLE! => {self._dump_results(result._result)}",
|
||||||
color=C.COLOR_UNREACHABLE,
|
color=C.COLOR_UNREACHABLE
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._display.display(
|
self._display.display(
|
||||||
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: UNREACHABLE! => {self._dump_results(result._result)}",
|
f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: UNREACHABLE! => {self._dump_results(result._result)}",
|
||||||
color=C.COLOR_UNREACHABLE,
|
color=C.COLOR_UNREACHABLE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2024, Felix Fontein <felix@fontein.de>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -35,8 +37,8 @@ from ansible.plugins.callback.default import CallbackModule as Default
|
|||||||
|
|
||||||
class CallbackModule(Default):
|
class CallbackModule(Default):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.default_without_diff"
|
CALLBACK_NAME = 'community.general.default_without_diff'
|
||||||
|
|
||||||
def v2_on_file_diff(self, result):
|
def v2_on_file_diff(self, result):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2016, Dag Wieers <dag@wieers.com>
|
# Copyright (c) 2016, Dag Wieers <dag@wieers.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -22,7 +23,6 @@ requirements:
|
|||||||
HAS_OD = False
|
HAS_OD = False
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
HAS_OD = True
|
HAS_OD = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@@ -70,66 +70,66 @@ display = Display()
|
|||||||
# FIXME: Importing constants as C simply does not work, beats me :-/
|
# FIXME: Importing constants as C simply does not work, beats me :-/
|
||||||
# from ansible import constants as C
|
# from ansible import constants as C
|
||||||
class C:
|
class C:
|
||||||
COLOR_HIGHLIGHT = "white"
|
COLOR_HIGHLIGHT = 'white'
|
||||||
COLOR_VERBOSE = "blue"
|
COLOR_VERBOSE = 'blue'
|
||||||
COLOR_WARN = "bright purple"
|
COLOR_WARN = 'bright purple'
|
||||||
COLOR_ERROR = "red"
|
COLOR_ERROR = 'red'
|
||||||
COLOR_DEBUG = "dark gray"
|
COLOR_DEBUG = 'dark gray'
|
||||||
COLOR_DEPRECATE = "purple"
|
COLOR_DEPRECATE = 'purple'
|
||||||
COLOR_SKIP = "cyan"
|
COLOR_SKIP = 'cyan'
|
||||||
COLOR_UNREACHABLE = "bright red"
|
COLOR_UNREACHABLE = 'bright red'
|
||||||
COLOR_OK = "green"
|
COLOR_OK = 'green'
|
||||||
COLOR_CHANGED = "yellow"
|
COLOR_CHANGED = 'yellow'
|
||||||
|
|
||||||
|
|
||||||
# Taken from Dstat
|
# Taken from Dstat
|
||||||
class vt100:
|
class vt100:
|
||||||
black = "\033[0;30m"
|
black = '\033[0;30m'
|
||||||
darkred = "\033[0;31m"
|
darkred = '\033[0;31m'
|
||||||
darkgreen = "\033[0;32m"
|
darkgreen = '\033[0;32m'
|
||||||
darkyellow = "\033[0;33m"
|
darkyellow = '\033[0;33m'
|
||||||
darkblue = "\033[0;34m"
|
darkblue = '\033[0;34m'
|
||||||
darkmagenta = "\033[0;35m"
|
darkmagenta = '\033[0;35m'
|
||||||
darkcyan = "\033[0;36m"
|
darkcyan = '\033[0;36m'
|
||||||
gray = "\033[0;37m"
|
gray = '\033[0;37m'
|
||||||
|
|
||||||
darkgray = "\033[1;30m"
|
darkgray = '\033[1;30m'
|
||||||
red = "\033[1;31m"
|
red = '\033[1;31m'
|
||||||
green = "\033[1;32m"
|
green = '\033[1;32m'
|
||||||
yellow = "\033[1;33m"
|
yellow = '\033[1;33m'
|
||||||
blue = "\033[1;34m"
|
blue = '\033[1;34m'
|
||||||
magenta = "\033[1;35m"
|
magenta = '\033[1;35m'
|
||||||
cyan = "\033[1;36m"
|
cyan = '\033[1;36m'
|
||||||
white = "\033[1;37m"
|
white = '\033[1;37m'
|
||||||
|
|
||||||
blackbg = "\033[40m"
|
blackbg = '\033[40m'
|
||||||
redbg = "\033[41m"
|
redbg = '\033[41m'
|
||||||
greenbg = "\033[42m"
|
greenbg = '\033[42m'
|
||||||
yellowbg = "\033[43m"
|
yellowbg = '\033[43m'
|
||||||
bluebg = "\033[44m"
|
bluebg = '\033[44m'
|
||||||
magentabg = "\033[45m"
|
magentabg = '\033[45m'
|
||||||
cyanbg = "\033[46m"
|
cyanbg = '\033[46m'
|
||||||
whitebg = "\033[47m"
|
whitebg = '\033[47m'
|
||||||
|
|
||||||
reset = "\033[0;0m"
|
reset = '\033[0;0m'
|
||||||
bold = "\033[1m"
|
bold = '\033[1m'
|
||||||
reverse = "\033[2m"
|
reverse = '\033[2m'
|
||||||
underline = "\033[4m"
|
underline = '\033[4m'
|
||||||
|
|
||||||
clear = "\033[2J"
|
clear = '\033[2J'
|
||||||
# clearline = '\033[K'
|
# clearline = '\033[K'
|
||||||
clearline = "\033[2K"
|
clearline = '\033[2K'
|
||||||
save = "\033[s"
|
save = '\033[s'
|
||||||
restore = "\033[u"
|
restore = '\033[u'
|
||||||
save_all = "\0337"
|
save_all = '\0337'
|
||||||
restore_all = "\0338"
|
restore_all = '\0338'
|
||||||
linewrap = "\033[7h"
|
linewrap = '\033[7h'
|
||||||
nolinewrap = "\033[7l"
|
nolinewrap = '\033[7l'
|
||||||
|
|
||||||
up = "\033[1A"
|
up = '\033[1A'
|
||||||
down = "\033[1B"
|
down = '\033[1B'
|
||||||
right = "\033[1C"
|
right = '\033[1C'
|
||||||
left = "\033[1D"
|
left = '\033[1D'
|
||||||
|
|
||||||
|
|
||||||
colors = dict(
|
colors = dict(
|
||||||
@@ -141,38 +141,41 @@ colors = dict(
|
|||||||
unreachable=vt100.red,
|
unreachable=vt100.red,
|
||||||
)
|
)
|
||||||
|
|
||||||
states = ("skipped", "ok", "changed", "failed", "unreachable")
|
states = ('skipped', 'ok', 'changed', 'failed', 'unreachable')
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackModule_default):
|
class CallbackModule(CallbackModule_default):
|
||||||
"""
|
|
||||||
|
'''
|
||||||
This is the dense callback interface, where screen estate is still valued.
|
This is the dense callback interface, where screen estate is still valued.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "dense"
|
CALLBACK_NAME = 'dense'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
# From CallbackModule
|
# From CallbackModule
|
||||||
self._display = display
|
self._display = display
|
||||||
|
|
||||||
if HAS_OD:
|
if HAS_OD:
|
||||||
|
|
||||||
self.disabled = False
|
self.disabled = False
|
||||||
self.super_ref = super()
|
self.super_ref = super(CallbackModule, self)
|
||||||
self.super_ref.__init__()
|
self.super_ref.__init__()
|
||||||
|
|
||||||
# Attributes to remove from results for more density
|
# Attributes to remove from results for more density
|
||||||
self.removed_attributes = (
|
self.removed_attributes = (
|
||||||
# 'changed',
|
# 'changed',
|
||||||
"delta",
|
'delta',
|
||||||
# 'diff',
|
# 'diff',
|
||||||
"end",
|
'end',
|
||||||
"failed",
|
'failed',
|
||||||
"failed_when_result",
|
'failed_when_result',
|
||||||
"invocation",
|
'invocation',
|
||||||
"start",
|
'start',
|
||||||
"stdout_lines",
|
'stdout_lines',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initiate data structures
|
# Initiate data structures
|
||||||
@@ -180,15 +183,13 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self.keep = False
|
self.keep = False
|
||||||
self.shown_title = False
|
self.shown_title = False
|
||||||
self.count = dict(play=0, handler=0, task=0)
|
self.count = dict(play=0, handler=0, task=0)
|
||||||
self.type = "foo"
|
self.type = 'foo'
|
||||||
|
|
||||||
# Start immediately on the first line
|
# Start immediately on the first line
|
||||||
sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
|
sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
else:
|
else:
|
||||||
display.warning(
|
display.warning("The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling.")
|
||||||
"The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling."
|
|
||||||
)
|
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@@ -198,27 +199,27 @@ class CallbackModule(CallbackModule_default):
|
|||||||
name = result._host.get_name()
|
name = result._host.get_name()
|
||||||
|
|
||||||
# Add a new status in case a failed task is ignored
|
# Add a new status in case a failed task is ignored
|
||||||
if status == "failed" and result._task.ignore_errors:
|
if status == 'failed' and result._task.ignore_errors:
|
||||||
status = "ignored"
|
status = 'ignored'
|
||||||
|
|
||||||
# Check if we have to update an existing state (when looping over items)
|
# Check if we have to update an existing state (when looping over items)
|
||||||
if name not in self.hosts:
|
if name not in self.hosts:
|
||||||
self.hosts[name] = dict(state=status)
|
self.hosts[name] = dict(state=status)
|
||||||
elif states.index(self.hosts[name]["state"]) < states.index(status):
|
elif states.index(self.hosts[name]['state']) < states.index(status):
|
||||||
self.hosts[name]["state"] = status
|
self.hosts[name]['state'] = status
|
||||||
|
|
||||||
# Store delegated hostname, if needed
|
# Store delegated hostname, if needed
|
||||||
delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
if delegated_vars:
|
if delegated_vars:
|
||||||
self.hosts[name]["delegate"] = delegated_vars["ansible_host"]
|
self.hosts[name]['delegate'] = delegated_vars['ansible_host']
|
||||||
|
|
||||||
# Print progress bar
|
# Print progress bar
|
||||||
self._display_progress(result)
|
self._display_progress(result)
|
||||||
|
|
||||||
# # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
|
# # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
|
||||||
# if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
|
# if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
|
||||||
# Ensure that tasks with changes/failures stay on-screen
|
# Ensure that tasks with changes/failures stay on-screen
|
||||||
if status in ["changed", "failed", "unreachable"]:
|
if status in ['changed', 'failed', 'unreachable']:
|
||||||
self.keep = True
|
self.keep = True
|
||||||
|
|
||||||
if self._display.verbosity == 1:
|
if self._display.verbosity == 1:
|
||||||
@@ -239,9 +240,9 @@ class CallbackModule(CallbackModule_default):
|
|||||||
del result[attr]
|
del result[attr]
|
||||||
|
|
||||||
def _handle_exceptions(self, result):
|
def _handle_exceptions(self, result):
|
||||||
if "exception" in result:
|
if 'exception' in result:
|
||||||
# Remove the exception from the result so it is not shown every time
|
# Remove the exception from the result so it is not shown every time
|
||||||
del result["exception"]
|
del result['exception']
|
||||||
|
|
||||||
if self._display.verbosity == 1:
|
if self._display.verbosity == 1:
|
||||||
return "An exception occurred during task execution. To see the full traceback, use -vvv."
|
return "An exception occurred during task execution. To see the full traceback, use -vvv."
|
||||||
@@ -249,16 +250,16 @@ class CallbackModule(CallbackModule_default):
|
|||||||
def _display_progress(self, result=None):
|
def _display_progress(self, result=None):
|
||||||
# Always rewrite the complete line
|
# Always rewrite the complete line
|
||||||
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline)
|
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline)
|
||||||
sys.stdout.write(f"{self.type} {self.count[self.type]}:")
|
sys.stdout.write(f'{self.type} {self.count[self.type]}:')
|
||||||
sys.stdout.write(vt100.reset)
|
sys.stdout.write(vt100.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Print out each host in its own status-color
|
# Print out each host in its own status-color
|
||||||
for name in self.hosts:
|
for name in self.hosts:
|
||||||
sys.stdout.write(" ")
|
sys.stdout.write(' ')
|
||||||
if self.hosts[name].get("delegate", None):
|
if self.hosts[name].get('delegate', None):
|
||||||
sys.stdout.write(f"{self.hosts[name]['delegate']}>")
|
sys.stdout.write(f"{self.hosts[name]['delegate']}>")
|
||||||
sys.stdout.write(colors[self.hosts[name]["state"]] + name + vt100.reset)
|
sys.stdout.write(colors[self.hosts[name]['state']] + name + vt100.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
sys.stdout.write(vt100.linewrap)
|
sys.stdout.write(vt100.linewrap)
|
||||||
@@ -267,7 +268,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
if not self.shown_title:
|
if not self.shown_title:
|
||||||
self.shown_title = True
|
self.shown_title = True
|
||||||
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
|
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
|
||||||
sys.stdout.write(f"{self.type} {self.count[self.type]}: {self.task.get_name().strip()}")
|
sys.stdout.write(f'{self.type} {self.count[self.type]}: {self.task.get_name().strip()}')
|
||||||
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
else:
|
else:
|
||||||
@@ -284,31 +285,29 @@ class CallbackModule(CallbackModule_default):
|
|||||||
|
|
||||||
self._clean_results(result._result)
|
self._clean_results(result._result)
|
||||||
|
|
||||||
dump = ""
|
dump = ''
|
||||||
if result._task.action == "include":
|
if result._task.action == 'include':
|
||||||
return
|
return
|
||||||
elif status == "ok":
|
elif status == 'ok':
|
||||||
return
|
return
|
||||||
elif status == "ignored":
|
elif status == 'ignored':
|
||||||
dump = self._handle_exceptions(result._result)
|
dump = self._handle_exceptions(result._result)
|
||||||
elif status == "failed":
|
elif status == 'failed':
|
||||||
dump = self._handle_exceptions(result._result)
|
dump = self._handle_exceptions(result._result)
|
||||||
elif status == "unreachable":
|
elif status == 'unreachable':
|
||||||
dump = result._result["msg"]
|
dump = result._result['msg']
|
||||||
|
|
||||||
if not dump:
|
if not dump:
|
||||||
dump = self._dump_results(result._result)
|
dump = self._dump_results(result._result)
|
||||||
|
|
||||||
if result._task.loop and "results" in result._result:
|
if result._task.loop and 'results' in result._result:
|
||||||
self._process_items(result)
|
self._process_items(result)
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(f"{colors[status] + status}: ")
|
sys.stdout.write(f"{colors[status] + status}: ")
|
||||||
|
|
||||||
delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
if delegated_vars:
|
if delegated_vars:
|
||||||
sys.stdout.write(
|
sys.stdout.write(f"{vt100.reset}{result._host.get_name()}>{colors[status]}{delegated_vars['ansible_host']}")
|
||||||
f"{vt100.reset}{result._host.get_name()}>{colors[status]}{delegated_vars['ansible_host']}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(result._host.get_name())
|
sys.stdout.write(result._host.get_name())
|
||||||
|
|
||||||
@@ -316,7 +315,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
sys.stdout.write(f"{vt100.reset}{vt100.save}{vt100.clearline}")
|
sys.stdout.write(f"{vt100.reset}{vt100.save}{vt100.clearline}")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if status == "changed":
|
if status == 'changed':
|
||||||
self._handle_warnings(result._result)
|
self._handle_warnings(result._result)
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
@@ -329,13 +328,13 @@ class CallbackModule(CallbackModule_default):
|
|||||||
# Reset at the start of each play
|
# Reset at the start of each play
|
||||||
self.keep = False
|
self.keep = False
|
||||||
self.count.update(dict(handler=0, task=0))
|
self.count.update(dict(handler=0, task=0))
|
||||||
self.count["play"] += 1
|
self.count['play'] += 1
|
||||||
self.play = play
|
self.play = play
|
||||||
|
|
||||||
# Write the next play on screen IN UPPERCASE, and make it permanent
|
# Write the next play on screen IN UPPERCASE, and make it permanent
|
||||||
name = play.get_name().strip()
|
name = play.get_name().strip()
|
||||||
if not name:
|
if not name:
|
||||||
name = "unnamed"
|
name = 'unnamed'
|
||||||
sys.stdout.write(f"PLAY {self.count['play']}: {name.upper()}")
|
sys.stdout.write(f"PLAY {self.count['play']}: {name.upper()}")
|
||||||
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
@@ -353,14 +352,14 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self.shown_title = False
|
self.shown_title = False
|
||||||
self.hosts = OrderedDict()
|
self.hosts = OrderedDict()
|
||||||
self.task = task
|
self.task = task
|
||||||
self.type = "task"
|
self.type = 'task'
|
||||||
|
|
||||||
# Enumerate task if not setup (task names are too long for dense output)
|
# Enumerate task if not setup (task names are too long for dense output)
|
||||||
if task.get_name() != "setup":
|
if task.get_name() != 'setup':
|
||||||
self.count["task"] += 1
|
self.count['task'] += 1
|
||||||
|
|
||||||
# Write the next task on screen (behind the prompt is the previous output)
|
# Write the next task on screen (behind the prompt is the previous output)
|
||||||
sys.stdout.write(f"{self.type} {self.count[self.type]}.")
|
sys.stdout.write(f'{self.type} {self.count[self.type]}.')
|
||||||
sys.stdout.write(vt100.reset)
|
sys.stdout.write(vt100.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
@@ -376,36 +375,36 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self.shown_title = False
|
self.shown_title = False
|
||||||
self.hosts = OrderedDict()
|
self.hosts = OrderedDict()
|
||||||
self.task = task
|
self.task = task
|
||||||
self.type = "handler"
|
self.type = 'handler'
|
||||||
|
|
||||||
# Enumerate handler if not setup (handler names may be too long for dense output)
|
# Enumerate handler if not setup (handler names may be too long for dense output)
|
||||||
if task.get_name() != "setup":
|
if task.get_name() != 'setup':
|
||||||
self.count[self.type] += 1
|
self.count[self.type] += 1
|
||||||
|
|
||||||
# Write the next task on screen (behind the prompt is the previous output)
|
# Write the next task on screen (behind the prompt is the previous output)
|
||||||
sys.stdout.write(f"{self.type} {self.count[self.type]}.")
|
sys.stdout.write(f'{self.type} {self.count[self.type]}.')
|
||||||
sys.stdout.write(vt100.reset)
|
sys.stdout.write(vt100.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def v2_playbook_on_cleanup_task_start(self, task):
|
def v2_playbook_on_cleanup_task_start(self, task):
|
||||||
# TBD
|
# TBD
|
||||||
sys.stdout.write("cleanup.")
|
sys.stdout.write('cleanup.')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
self._add_host(result, "failed")
|
self._add_host(result, 'failed')
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
if result._result.get("changed", False):
|
if result._result.get('changed', False):
|
||||||
self._add_host(result, "changed")
|
self._add_host(result, 'changed')
|
||||||
else:
|
else:
|
||||||
self._add_host(result, "ok")
|
self._add_host(result, 'ok')
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self._add_host(result, "skipped")
|
self._add_host(result, 'skipped')
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
self._add_host(result, "unreachable")
|
self._add_host(result, 'unreachable')
|
||||||
|
|
||||||
def v2_runner_on_include(self, included_file):
|
def v2_runner_on_include(self, included_file):
|
||||||
pass
|
pass
|
||||||
@@ -425,24 +424,24 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self.v2_runner_item_on_ok(result)
|
self.v2_runner_item_on_ok(result)
|
||||||
|
|
||||||
def v2_runner_item_on_ok(self, result):
|
def v2_runner_item_on_ok(self, result):
|
||||||
if result._result.get("changed", False):
|
if result._result.get('changed', False):
|
||||||
self._add_host(result, "changed")
|
self._add_host(result, 'changed')
|
||||||
else:
|
else:
|
||||||
self._add_host(result, "ok")
|
self._add_host(result, 'ok')
|
||||||
|
|
||||||
# Old definition in v2.0
|
# Old definition in v2.0
|
||||||
def v2_playbook_item_on_failed(self, result):
|
def v2_playbook_item_on_failed(self, result):
|
||||||
self.v2_runner_item_on_failed(result)
|
self.v2_runner_item_on_failed(result)
|
||||||
|
|
||||||
def v2_runner_item_on_failed(self, result):
|
def v2_runner_item_on_failed(self, result):
|
||||||
self._add_host(result, "failed")
|
self._add_host(result, 'failed')
|
||||||
|
|
||||||
# Old definition in v2.0
|
# Old definition in v2.0
|
||||||
def v2_playbook_item_on_skipped(self, result):
|
def v2_playbook_item_on_skipped(self, result):
|
||||||
self.v2_runner_item_on_skipped(result)
|
self.v2_runner_item_on_skipped(result)
|
||||||
|
|
||||||
def v2_runner_item_on_skipped(self, result):
|
def v2_runner_item_on_skipped(self, result):
|
||||||
self._add_host(result, "skipped")
|
self._add_host(result, 'skipped')
|
||||||
|
|
||||||
def v2_playbook_on_no_hosts_remaining(self):
|
def v2_playbook_on_no_hosts_remaining(self):
|
||||||
if self._display.verbosity == 0 and self.keep:
|
if self._display.verbosity == 0 and self.keep:
|
||||||
@@ -469,7 +468,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
return
|
return
|
||||||
|
|
||||||
sys.stdout.write(vt100.bold + vt100.underline)
|
sys.stdout.write(vt100.bold + vt100.underline)
|
||||||
sys.stdout.write("SUMMARY")
|
sys.stdout.write('SUMMARY')
|
||||||
|
|
||||||
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
sys.stdout.write(f"{vt100.restore}{vt100.reset}\n{vt100.save}{vt100.clearline}")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
@@ -481,10 +480,10 @@ class CallbackModule(CallbackModule_default):
|
|||||||
f"{hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
|
f"{hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
|
||||||
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
|
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
|
||||||
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
|
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
|
||||||
screen_only=True,
|
screen_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# When using -vv or higher, simply do the default action
|
# When using -vv or higher, simply do the default action
|
||||||
if display.verbosity >= 2 or not HAS_OD:
|
if display.verbosity >= 2 or not HAS_OD:
|
||||||
CallbackModule = CallbackModule_default # type: ignore
|
CallbackModule = CallbackModule_default
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2019, Trevor Highfill <trevor.highfill@outlook.com>
|
# Copyright (c) 2019, Trevor Highfill <trevor.highfill@outlook.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -785,13 +787,12 @@ from ansible.module_utils.common.text.converters import to_text
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from ansible.template import trust_as_template # noqa: F401, pylint: disable=unused-import
|
from ansible.template import trust_as_template # noqa: F401, pylint: disable=unused-import
|
||||||
|
|
||||||
SUPPORTS_DATA_TAGGING = True
|
SUPPORTS_DATA_TAGGING = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
SUPPORTS_DATA_TAGGING = False
|
SUPPORTS_DATA_TAGGING = False
|
||||||
|
|
||||||
|
|
||||||
class DummyStdout:
|
class DummyStdout(object):
|
||||||
def flush(self):
|
def flush(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -806,12 +807,11 @@ class CallbackModule(Default):
|
|||||||
"""
|
"""
|
||||||
Callback plugin that allows you to supply your own custom callback templates to be output.
|
Callback plugin that allows you to supply your own custom callback templates to be output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.diy"
|
CALLBACK_NAME = 'community.general.diy'
|
||||||
|
|
||||||
DIY_NS = "ansible_callback_diy"
|
DIY_NS = 'ansible_callback_diy'
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _suppress_stdout(self, enabled):
|
def _suppress_stdout(self, enabled):
|
||||||
@@ -824,48 +824,50 @@ class CallbackModule(Default):
|
|||||||
def _get_output_specification(self, loader, variables):
|
def _get_output_specification(self, loader, variables):
|
||||||
_ret = {}
|
_ret = {}
|
||||||
_calling_method = sys._getframe(1).f_code.co_name
|
_calling_method = sys._getframe(1).f_code.co_name
|
||||||
_callback_type = _calling_method[3:] if _calling_method[:3] == "v2_" else _calling_method
|
_callback_type = (_calling_method[3:] if _calling_method[:3] == "v2_" else _calling_method)
|
||||||
_callback_options = ["msg", "msg_color"]
|
_callback_options = ['msg', 'msg_color']
|
||||||
|
|
||||||
for option in _callback_options:
|
for option in _callback_options:
|
||||||
_option_name = f"{_callback_type}_{option}"
|
_option_name = f'{_callback_type}_{option}'
|
||||||
_option_template = variables.get(f"{self.DIY_NS}_{_option_name}", self.get_option(_option_name))
|
_option_template = variables.get(
|
||||||
_ret.update({option: self._template(loader=loader, template=_option_template, variables=variables)})
|
f"{self.DIY_NS}_{_option_name}",
|
||||||
|
self.get_option(_option_name)
|
||||||
|
)
|
||||||
|
_ret.update({option: self._template(
|
||||||
|
loader=loader,
|
||||||
|
template=_option_template,
|
||||||
|
variables=variables
|
||||||
|
)})
|
||||||
|
|
||||||
_ret.update({"vars": variables})
|
_ret.update({'vars': variables})
|
||||||
|
|
||||||
return _ret
|
return _ret
|
||||||
|
|
||||||
def _using_diy(self, spec):
|
def _using_diy(self, spec):
|
||||||
sentinel = object()
|
sentinel = object()
|
||||||
omit = spec["vars"].get("omit", sentinel)
|
omit = spec['vars'].get('omit', sentinel)
|
||||||
# With Data Tagging, omit is sentinel
|
# With Data Tagging, omit is sentinel
|
||||||
return (spec["msg"] is not None) and (spec["msg"] != omit or omit is sentinel)
|
return (spec['msg'] is not None) and (spec['msg'] != omit or omit is sentinel)
|
||||||
|
|
||||||
def _parent_has_callback(self):
|
def _parent_has_callback(self):
|
||||||
return hasattr(super(), sys._getframe(1).f_code.co_name)
|
return hasattr(super(CallbackModule, self), sys._getframe(1).f_code.co_name)
|
||||||
|
|
||||||
def _template(self, loader, template, variables):
|
def _template(self, loader, template, variables):
|
||||||
_templar = Templar(loader=loader, variables=variables)
|
_templar = Templar(loader=loader, variables=variables)
|
||||||
return _templar.template(template, preserve_trailing_newlines=True, convert_data=False, escape_backslashes=True)
|
return _templar.template(
|
||||||
|
template,
|
||||||
|
preserve_trailing_newlines=True,
|
||||||
|
convert_data=False,
|
||||||
|
escape_backslashes=True
|
||||||
|
)
|
||||||
|
|
||||||
def _output(self, spec, stderr=False):
|
def _output(self, spec, stderr=False):
|
||||||
_msg = to_text(spec["msg"])
|
_msg = to_text(spec['msg'])
|
||||||
if len(_msg) > 0:
|
if len(_msg) > 0:
|
||||||
self._display.display(msg=_msg, color=spec["msg_color"], stderr=stderr)
|
self._display.display(msg=_msg, color=spec['msg_color'], stderr=stderr)
|
||||||
|
|
||||||
def _get_vars(
|
def _get_vars(self, playbook, play=None, host=None, task=None, included_file=None,
|
||||||
self,
|
handler=None, result=None, stats=None, remove_attr_ref_loop=True):
|
||||||
playbook,
|
|
||||||
play=None,
|
|
||||||
host=None,
|
|
||||||
task=None,
|
|
||||||
included_file=None,
|
|
||||||
handler=None,
|
|
||||||
result=None,
|
|
||||||
stats=None,
|
|
||||||
remove_attr_ref_loop=True,
|
|
||||||
):
|
|
||||||
def _get_value(obj, attr=None, method=None):
|
def _get_value(obj, attr=None, method=None):
|
||||||
if attr:
|
if attr:
|
||||||
return getattr(obj, attr, getattr(obj, f"_{attr}", None))
|
return getattr(obj, attr, getattr(obj, f"_{attr}", None))
|
||||||
@@ -875,8 +877,8 @@ class CallbackModule(Default):
|
|||||||
return _method()
|
return _method()
|
||||||
|
|
||||||
def _remove_attr_ref_loop(obj, attributes):
|
def _remove_attr_ref_loop(obj, attributes):
|
||||||
_loop_var = getattr(obj, "loop_control", None)
|
_loop_var = getattr(obj, 'loop_control', None)
|
||||||
_loop_var = _loop_var or "item"
|
_loop_var = (_loop_var or 'item')
|
||||||
|
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
if str(_loop_var) in str(_get_value(obj=obj, attr=attr)):
|
if str(_loop_var) in str(_get_value(obj=obj, attr=attr)):
|
||||||
@@ -895,128 +897,56 @@ class CallbackModule(Default):
|
|||||||
_all = _variable_manager.get_vars()
|
_all = _variable_manager.get_vars()
|
||||||
if play:
|
if play:
|
||||||
_all = play.get_variable_manager().get_vars(
|
_all = play.get_variable_manager().get_vars(
|
||||||
play=play, host=(host if host else getattr(result, "_host", None)), task=(handler if handler else task)
|
play=play,
|
||||||
|
host=(host if host else getattr(result, '_host', None)),
|
||||||
|
task=(handler if handler else task)
|
||||||
)
|
)
|
||||||
_ret.update(_all)
|
_ret.update(_all)
|
||||||
|
|
||||||
_ret.update(_ret.get(self.DIY_NS, {self.DIY_NS: {} if SUPPORTS_DATA_TAGGING else CallbackDIYDict()}))
|
_ret.update(_ret.get(self.DIY_NS, {self.DIY_NS: {} if SUPPORTS_DATA_TAGGING else CallbackDIYDict()}))
|
||||||
|
|
||||||
_ret[self.DIY_NS].update({"playbook": {}})
|
_ret[self.DIY_NS].update({'playbook': {}})
|
||||||
_playbook_attributes = ["entries", "file_name", "basedir"]
|
_playbook_attributes = ['entries', 'file_name', 'basedir']
|
||||||
|
|
||||||
for attr in _playbook_attributes:
|
for attr in _playbook_attributes:
|
||||||
_ret[self.DIY_NS]["playbook"].update({attr: _get_value(obj=playbook, attr=attr)})
|
_ret[self.DIY_NS]['playbook'].update({attr: _get_value(obj=playbook, attr=attr)})
|
||||||
|
|
||||||
if play:
|
if play:
|
||||||
_ret[self.DIY_NS].update({"play": {}})
|
_ret[self.DIY_NS].update({'play': {}})
|
||||||
_play_attributes = [
|
_play_attributes = ['any_errors_fatal', 'become', 'become_flags', 'become_method',
|
||||||
"any_errors_fatal",
|
'become_user', 'check_mode', 'collections', 'connection',
|
||||||
"become",
|
'debugger', 'diff', 'environment', 'fact_path', 'finalized',
|
||||||
"become_flags",
|
'force_handlers', 'gather_facts', 'gather_subset',
|
||||||
"become_method",
|
'gather_timeout', 'handlers', 'hosts', 'ignore_errors',
|
||||||
"become_user",
|
'ignore_unreachable', 'included_conditional', 'included_path',
|
||||||
"check_mode",
|
'max_fail_percentage', 'module_defaults', 'name', 'no_log',
|
||||||
"collections",
|
'only_tags', 'order', 'port', 'post_tasks', 'pre_tasks',
|
||||||
"connection",
|
'remote_user', 'removed_hosts', 'roles', 'run_once', 'serial',
|
||||||
"debugger",
|
'skip_tags', 'squashed', 'strategy', 'tags', 'tasks', 'uuid',
|
||||||
"diff",
|
'validated', 'vars_files', 'vars_prompt']
|
||||||
"environment",
|
|
||||||
"fact_path",
|
|
||||||
"finalized",
|
|
||||||
"force_handlers",
|
|
||||||
"gather_facts",
|
|
||||||
"gather_subset",
|
|
||||||
"gather_timeout",
|
|
||||||
"handlers",
|
|
||||||
"hosts",
|
|
||||||
"ignore_errors",
|
|
||||||
"ignore_unreachable",
|
|
||||||
"included_conditional",
|
|
||||||
"included_path",
|
|
||||||
"max_fail_percentage",
|
|
||||||
"module_defaults",
|
|
||||||
"name",
|
|
||||||
"no_log",
|
|
||||||
"only_tags",
|
|
||||||
"order",
|
|
||||||
"port",
|
|
||||||
"post_tasks",
|
|
||||||
"pre_tasks",
|
|
||||||
"remote_user",
|
|
||||||
"removed_hosts",
|
|
||||||
"roles",
|
|
||||||
"run_once",
|
|
||||||
"serial",
|
|
||||||
"skip_tags",
|
|
||||||
"squashed",
|
|
||||||
"strategy",
|
|
||||||
"tags",
|
|
||||||
"tasks",
|
|
||||||
"uuid",
|
|
||||||
"validated",
|
|
||||||
"vars_files",
|
|
||||||
"vars_prompt",
|
|
||||||
]
|
|
||||||
|
|
||||||
for attr in _play_attributes:
|
for attr in _play_attributes:
|
||||||
_ret[self.DIY_NS]["play"].update({attr: _get_value(obj=play, attr=attr)})
|
_ret[self.DIY_NS]['play'].update({attr: _get_value(obj=play, attr=attr)})
|
||||||
|
|
||||||
if host:
|
if host:
|
||||||
_ret[self.DIY_NS].update({"host": {}})
|
_ret[self.DIY_NS].update({'host': {}})
|
||||||
_host_attributes = ["name", "uuid", "address", "implicit"]
|
_host_attributes = ['name', 'uuid', 'address', 'implicit']
|
||||||
|
|
||||||
for attr in _host_attributes:
|
for attr in _host_attributes:
|
||||||
_ret[self.DIY_NS]["host"].update({attr: _get_value(obj=host, attr=attr)})
|
_ret[self.DIY_NS]['host'].update({attr: _get_value(obj=host, attr=attr)})
|
||||||
|
|
||||||
if task:
|
if task:
|
||||||
_ret[self.DIY_NS].update({"task": {}})
|
_ret[self.DIY_NS].update({'task': {}})
|
||||||
_task_attributes = [
|
_task_attributes = ['action', 'any_errors_fatal', 'args', 'async', 'async_val',
|
||||||
"action",
|
'become', 'become_flags', 'become_method', 'become_user',
|
||||||
"any_errors_fatal",
|
'changed_when', 'check_mode', 'collections', 'connection',
|
||||||
"args",
|
'debugger', 'delay', 'delegate_facts', 'delegate_to', 'diff',
|
||||||
"async",
|
'environment', 'failed_when', 'finalized', 'ignore_errors',
|
||||||
"async_val",
|
'ignore_unreachable', 'loop', 'loop_control', 'loop_with',
|
||||||
"become",
|
'module_defaults', 'name', 'no_log', 'notify', 'parent', 'poll',
|
||||||
"become_flags",
|
'port', 'register', 'remote_user', 'retries', 'role', 'run_once',
|
||||||
"become_method",
|
'squashed', 'tags', 'untagged', 'until', 'uuid', 'validated',
|
||||||
"become_user",
|
'when']
|
||||||
"changed_when",
|
|
||||||
"check_mode",
|
|
||||||
"collections",
|
|
||||||
"connection",
|
|
||||||
"debugger",
|
|
||||||
"delay",
|
|
||||||
"delegate_facts",
|
|
||||||
"delegate_to",
|
|
||||||
"diff",
|
|
||||||
"environment",
|
|
||||||
"failed_when",
|
|
||||||
"finalized",
|
|
||||||
"ignore_errors",
|
|
||||||
"ignore_unreachable",
|
|
||||||
"loop",
|
|
||||||
"loop_control",
|
|
||||||
"loop_with",
|
|
||||||
"module_defaults",
|
|
||||||
"name",
|
|
||||||
"no_log",
|
|
||||||
"notify",
|
|
||||||
"parent",
|
|
||||||
"poll",
|
|
||||||
"port",
|
|
||||||
"register",
|
|
||||||
"remote_user",
|
|
||||||
"retries",
|
|
||||||
"role",
|
|
||||||
"run_once",
|
|
||||||
"squashed",
|
|
||||||
"tags",
|
|
||||||
"untagged",
|
|
||||||
"until",
|
|
||||||
"uuid",
|
|
||||||
"validated",
|
|
||||||
"when",
|
|
||||||
]
|
|
||||||
|
|
||||||
# remove arguments that reference a loop var because they cause templating issues in
|
# remove arguments that reference a loop var because they cause templating issues in
|
||||||
# callbacks that do not have the loop context(e.g. playbook_on_task_start)
|
# callbacks that do not have the loop context(e.g. playbook_on_task_start)
|
||||||
@@ -1024,128 +954,91 @@ class CallbackModule(Default):
|
|||||||
_task_attributes = _remove_attr_ref_loop(obj=task, attributes=_task_attributes)
|
_task_attributes = _remove_attr_ref_loop(obj=task, attributes=_task_attributes)
|
||||||
|
|
||||||
for attr in _task_attributes:
|
for attr in _task_attributes:
|
||||||
_ret[self.DIY_NS]["task"].update({attr: _get_value(obj=task, attr=attr)})
|
_ret[self.DIY_NS]['task'].update({attr: _get_value(obj=task, attr=attr)})
|
||||||
|
|
||||||
if included_file:
|
if included_file:
|
||||||
_ret[self.DIY_NS].update({"included_file": {}})
|
_ret[self.DIY_NS].update({'included_file': {}})
|
||||||
_included_file_attributes = ["args", "filename", "hosts", "is_role", "task"]
|
_included_file_attributes = ['args', 'filename', 'hosts', 'is_role', 'task']
|
||||||
|
|
||||||
for attr in _included_file_attributes:
|
for attr in _included_file_attributes:
|
||||||
_ret[self.DIY_NS]["included_file"].update({attr: _get_value(obj=included_file, attr=attr)})
|
_ret[self.DIY_NS]['included_file'].update({attr: _get_value(
|
||||||
|
obj=included_file,
|
||||||
|
attr=attr
|
||||||
|
)})
|
||||||
|
|
||||||
if handler:
|
if handler:
|
||||||
_ret[self.DIY_NS].update({"handler": {}})
|
_ret[self.DIY_NS].update({'handler': {}})
|
||||||
_handler_attributes = [
|
_handler_attributes = ['action', 'any_errors_fatal', 'args', 'async', 'async_val',
|
||||||
"action",
|
'become', 'become_flags', 'become_method', 'become_user',
|
||||||
"any_errors_fatal",
|
'changed_when', 'check_mode', 'collections', 'connection',
|
||||||
"args",
|
'debugger', 'delay', 'delegate_facts', 'delegate_to', 'diff',
|
||||||
"async",
|
'environment', 'failed_when', 'finalized', 'ignore_errors',
|
||||||
"async_val",
|
'ignore_unreachable', 'listen', 'loop', 'loop_control',
|
||||||
"become",
|
'loop_with', 'module_defaults', 'name', 'no_log',
|
||||||
"become_flags",
|
'notified_hosts', 'notify', 'parent', 'poll', 'port',
|
||||||
"become_method",
|
'register', 'remote_user', 'retries', 'role', 'run_once',
|
||||||
"become_user",
|
'squashed', 'tags', 'untagged', 'until', 'uuid', 'validated',
|
||||||
"changed_when",
|
'when']
|
||||||
"check_mode",
|
|
||||||
"collections",
|
|
||||||
"connection",
|
|
||||||
"debugger",
|
|
||||||
"delay",
|
|
||||||
"delegate_facts",
|
|
||||||
"delegate_to",
|
|
||||||
"diff",
|
|
||||||
"environment",
|
|
||||||
"failed_when",
|
|
||||||
"finalized",
|
|
||||||
"ignore_errors",
|
|
||||||
"ignore_unreachable",
|
|
||||||
"listen",
|
|
||||||
"loop",
|
|
||||||
"loop_control",
|
|
||||||
"loop_with",
|
|
||||||
"module_defaults",
|
|
||||||
"name",
|
|
||||||
"no_log",
|
|
||||||
"notified_hosts",
|
|
||||||
"notify",
|
|
||||||
"parent",
|
|
||||||
"poll",
|
|
||||||
"port",
|
|
||||||
"register",
|
|
||||||
"remote_user",
|
|
||||||
"retries",
|
|
||||||
"role",
|
|
||||||
"run_once",
|
|
||||||
"squashed",
|
|
||||||
"tags",
|
|
||||||
"untagged",
|
|
||||||
"until",
|
|
||||||
"uuid",
|
|
||||||
"validated",
|
|
||||||
"when",
|
|
||||||
]
|
|
||||||
|
|
||||||
if handler.loop and remove_attr_ref_loop:
|
if handler.loop and remove_attr_ref_loop:
|
||||||
_handler_attributes = _remove_attr_ref_loop(obj=handler, attributes=_handler_attributes)
|
_handler_attributes = _remove_attr_ref_loop(obj=handler,
|
||||||
|
attributes=_handler_attributes)
|
||||||
|
|
||||||
for attr in _handler_attributes:
|
for attr in _handler_attributes:
|
||||||
_ret[self.DIY_NS]["handler"].update({attr: _get_value(obj=handler, attr=attr)})
|
_ret[self.DIY_NS]['handler'].update({attr: _get_value(obj=handler, attr=attr)})
|
||||||
|
|
||||||
_ret[self.DIY_NS]["handler"].update({"is_host_notified": handler.is_host_notified(host)})
|
_ret[self.DIY_NS]['handler'].update({'is_host_notified': handler.is_host_notified(host)})
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
_ret[self.DIY_NS].update({"result": {}})
|
_ret[self.DIY_NS].update({'result': {}})
|
||||||
_result_attributes = ["host", "task", "task_name"]
|
_result_attributes = ['host', 'task', 'task_name']
|
||||||
|
|
||||||
for attr in _result_attributes:
|
for attr in _result_attributes:
|
||||||
_ret[self.DIY_NS]["result"].update({attr: _get_value(obj=result, attr=attr)})
|
_ret[self.DIY_NS]['result'].update({attr: _get_value(obj=result, attr=attr)})
|
||||||
|
|
||||||
_result_methods = ["is_changed", "is_failed", "is_skipped", "is_unreachable"]
|
_result_methods = ['is_changed', 'is_failed', 'is_skipped', 'is_unreachable']
|
||||||
|
|
||||||
for method in _result_methods:
|
for method in _result_methods:
|
||||||
_ret[self.DIY_NS]["result"].update({method: _get_value(obj=result, method=method)})
|
_ret[self.DIY_NS]['result'].update({method: _get_value(obj=result, method=method)})
|
||||||
|
|
||||||
_ret[self.DIY_NS]["result"].update({"output": getattr(result, "_result", None)})
|
_ret[self.DIY_NS]['result'].update({'output': getattr(result, '_result', None)})
|
||||||
|
|
||||||
_ret.update(result._result)
|
_ret.update(result._result)
|
||||||
|
|
||||||
if stats:
|
if stats:
|
||||||
_ret[self.DIY_NS].update({"stats": {}})
|
_ret[self.DIY_NS].update({'stats': {}})
|
||||||
_stats_attributes = [
|
_stats_attributes = ['changed', 'custom', 'dark', 'failures', 'ignored',
|
||||||
"changed",
|
'ok', 'processed', 'rescued', 'skipped']
|
||||||
"custom",
|
|
||||||
"dark",
|
|
||||||
"failures",
|
|
||||||
"ignored",
|
|
||||||
"ok",
|
|
||||||
"processed",
|
|
||||||
"rescued",
|
|
||||||
"skipped",
|
|
||||||
]
|
|
||||||
|
|
||||||
for attr in _stats_attributes:
|
for attr in _stats_attributes:
|
||||||
_ret[self.DIY_NS]["stats"].update({attr: _get_value(obj=stats, attr=attr)})
|
_ret[self.DIY_NS]['stats'].update({attr: _get_value(obj=stats, attr=attr)})
|
||||||
|
|
||||||
_ret[self.DIY_NS].update({"top_level_var_names": list(_ret.keys())})
|
_ret[self.DIY_NS].update({'top_level_var_names': list(_ret.keys())})
|
||||||
|
|
||||||
return _ret
|
return _ret
|
||||||
|
|
||||||
def v2_on_any(self, *args, **kwargs):
|
def v2_on_any(self, *args, **kwargs):
|
||||||
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
|
self._diy_spec = self._get_output_specification(
|
||||||
|
loader=self._diy_loader,
|
||||||
|
variables=self._diy_spec['vars']
|
||||||
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
self._output(spec=self._diy_spec)
|
self._output(spec=self._diy_spec)
|
||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_on_any(*args, **kwargs)
|
super(CallbackModule, self).v2_on_any(*args, **kwargs)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1153,14 +1046,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_on_failed(result, ignore_errors)
|
super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1168,14 +1064,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_on_ok(result)
|
super(CallbackModule, self).v2_runner_on_ok(result)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1183,14 +1082,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_on_skipped(result)
|
super(CallbackModule, self).v2_runner_on_skipped(result)
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1198,7 +1100,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_on_unreachable(result)
|
super(CallbackModule, self).v2_runner_on_unreachable(result)
|
||||||
|
|
||||||
# not implemented as the call to this is not implemented yet
|
# not implemented as the call to this is not implemented yet
|
||||||
def v2_runner_on_async_poll(self, result):
|
def v2_runner_on_async_poll(self, result):
|
||||||
@@ -1220,8 +1122,8 @@ class CallbackModule(Default):
|
|||||||
play=self._diy_play,
|
play=self._diy_play,
|
||||||
task=self._diy_task,
|
task=self._diy_task,
|
||||||
result=result,
|
result=result,
|
||||||
remove_attr_ref_loop=False,
|
remove_attr_ref_loop=False
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1229,7 +1131,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_item_on_ok(result)
|
super(CallbackModule, self).v2_runner_item_on_ok(result)
|
||||||
|
|
||||||
def v2_runner_item_on_failed(self, result):
|
def v2_runner_item_on_failed(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
@@ -1239,8 +1141,8 @@ class CallbackModule(Default):
|
|||||||
play=self._diy_play,
|
play=self._diy_play,
|
||||||
task=self._diy_task,
|
task=self._diy_task,
|
||||||
result=result,
|
result=result,
|
||||||
remove_attr_ref_loop=False,
|
remove_attr_ref_loop=False
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1248,7 +1150,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_item_on_failed(result)
|
super(CallbackModule, self).v2_runner_item_on_failed(result)
|
||||||
|
|
||||||
def v2_runner_item_on_skipped(self, result):
|
def v2_runner_item_on_skipped(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
@@ -1258,8 +1160,8 @@ class CallbackModule(Default):
|
|||||||
play=self._diy_play,
|
play=self._diy_play,
|
||||||
task=self._diy_task,
|
task=self._diy_task,
|
||||||
result=result,
|
result=result,
|
||||||
remove_attr_ref_loop=False,
|
remove_attr_ref_loop=False
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1267,14 +1169,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_item_on_skipped(result)
|
super(CallbackModule, self).v2_runner_item_on_skipped(result)
|
||||||
|
|
||||||
def v2_runner_retry(self, result):
|
def v2_runner_retry(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1282,7 +1187,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_retry(result)
|
super(CallbackModule, self).v2_runner_retry(result)
|
||||||
|
|
||||||
def v2_runner_on_start(self, host, task):
|
def v2_runner_on_start(self, host, task):
|
||||||
self._diy_host = host
|
self._diy_host = host
|
||||||
@@ -1291,8 +1196,11 @@ class CallbackModule(Default):
|
|||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, host=self._diy_host, task=self._diy_task
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
host=self._diy_host,
|
||||||
|
task=self._diy_task
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1300,14 +1208,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_runner_on_start(host, task)
|
super(CallbackModule, self).v2_runner_on_start(host, task)
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self._diy_playbook = playbook
|
self._diy_playbook = playbook
|
||||||
self._diy_loader = self._diy_playbook.get_loader()
|
self._diy_loader = self._diy_playbook.get_loader()
|
||||||
|
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader, variables=self._get_vars(playbook=self._diy_playbook)
|
loader=self._diy_loader,
|
||||||
|
variables=self._get_vars(
|
||||||
|
playbook=self._diy_playbook
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1315,7 +1226,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_start(playbook)
|
super(CallbackModule, self).v2_playbook_on_start(playbook)
|
||||||
|
|
||||||
def v2_playbook_on_notify(self, handler, host):
|
def v2_playbook_on_notify(self, handler, host):
|
||||||
self._diy_handler = handler
|
self._diy_handler = handler
|
||||||
@@ -1324,8 +1235,11 @@ class CallbackModule(Default):
|
|||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, host=self._diy_host, handler=self._diy_handler
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
host=self._diy_host,
|
||||||
|
handler=self._diy_handler
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1333,34 +1247,44 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_notify(handler, host)
|
super(CallbackModule, self).v2_playbook_on_notify(handler, host)
|
||||||
|
|
||||||
def v2_playbook_on_no_hosts_matched(self):
|
def v2_playbook_on_no_hosts_matched(self):
|
||||||
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
|
self._diy_spec = self._get_output_specification(
|
||||||
|
loader=self._diy_loader,
|
||||||
|
variables=self._diy_spec['vars']
|
||||||
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
self._output(spec=self._diy_spec)
|
self._output(spec=self._diy_spec)
|
||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_no_hosts_matched()
|
super(CallbackModule, self).v2_playbook_on_no_hosts_matched()
|
||||||
|
|
||||||
def v2_playbook_on_no_hosts_remaining(self):
|
def v2_playbook_on_no_hosts_remaining(self):
|
||||||
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
|
self._diy_spec = self._get_output_specification(
|
||||||
|
loader=self._diy_loader,
|
||||||
|
variables=self._diy_spec['vars']
|
||||||
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
self._output(spec=self._diy_spec)
|
self._output(spec=self._diy_spec)
|
||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_no_hosts_remaining()
|
super(CallbackModule, self).v2_playbook_on_no_hosts_remaining()
|
||||||
|
|
||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
self._diy_task = task
|
self._diy_task = task
|
||||||
|
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task),
|
variables=self._get_vars(
|
||||||
|
playbook=self._diy_playbook,
|
||||||
|
play=self._diy_play,
|
||||||
|
task=self._diy_task
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1368,7 +1292,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_task_start(task, is_conditional)
|
super(CallbackModule, self).v2_playbook_on_task_start(task, is_conditional)
|
||||||
|
|
||||||
# not implemented as the call to this is not implemented yet
|
# not implemented as the call to this is not implemented yet
|
||||||
def v2_playbook_on_cleanup_task_start(self, task):
|
def v2_playbook_on_cleanup_task_start(self, task):
|
||||||
@@ -1379,7 +1303,11 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task),
|
variables=self._get_vars(
|
||||||
|
playbook=self._diy_playbook,
|
||||||
|
play=self._diy_play,
|
||||||
|
task=self._diy_task
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1387,29 +1315,25 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_handler_task_start(task)
|
super(CallbackModule, self).v2_playbook_on_handler_task_start(task)
|
||||||
|
|
||||||
def v2_playbook_on_vars_prompt(
|
def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None,
|
||||||
self,
|
confirm=False, salt_size=None, salt=None, default=None,
|
||||||
varname,
|
unsafe=None):
|
||||||
private=True,
|
self._diy_spec = self._get_output_specification(
|
||||||
prompt=None,
|
loader=self._diy_loader,
|
||||||
encrypt=None,
|
variables=self._diy_spec['vars']
|
||||||
confirm=False,
|
)
|
||||||
salt_size=None,
|
|
||||||
salt=None,
|
|
||||||
default=None,
|
|
||||||
unsafe=None,
|
|
||||||
):
|
|
||||||
self._diy_spec = self._get_output_specification(loader=self._diy_loader, variables=self._diy_spec["vars"])
|
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
self._output(spec=self._diy_spec)
|
self._output(spec=self._diy_spec)
|
||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_vars_prompt(
|
super(CallbackModule, self).v2_playbook_on_vars_prompt(
|
||||||
varname, private, prompt, encrypt, confirm, salt_size, salt, default, unsafe
|
varname, private, prompt, encrypt,
|
||||||
|
confirm, salt_size, salt, default,
|
||||||
|
unsafe
|
||||||
)
|
)
|
||||||
|
|
||||||
# not implemented as the call to this is not implemented yet
|
# not implemented as the call to this is not implemented yet
|
||||||
@@ -1424,7 +1348,11 @@ class CallbackModule(Default):
|
|||||||
self._diy_play = play
|
self._diy_play = play
|
||||||
|
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader, variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play)
|
loader=self._diy_loader,
|
||||||
|
variables=self._get_vars(
|
||||||
|
playbook=self._diy_playbook,
|
||||||
|
play=self._diy_play
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1432,14 +1360,18 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_play_start(play)
|
super(CallbackModule, self).v2_playbook_on_play_start(play)
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats):
|
def v2_playbook_on_stats(self, stats):
|
||||||
self._diy_stats = stats
|
self._diy_stats = stats
|
||||||
|
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(playbook=self._diy_playbook, play=self._diy_play, stats=self._diy_stats),
|
variables=self._get_vars(
|
||||||
|
playbook=self._diy_playbook,
|
||||||
|
play=self._diy_play,
|
||||||
|
stats=self._diy_stats
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1447,7 +1379,7 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_stats(stats)
|
super(CallbackModule, self).v2_playbook_on_stats(stats)
|
||||||
|
|
||||||
def v2_playbook_on_include(self, included_file):
|
def v2_playbook_on_include(self, included_file):
|
||||||
self._diy_included_file = included_file
|
self._diy_included_file = included_file
|
||||||
@@ -1458,8 +1390,8 @@ class CallbackModule(Default):
|
|||||||
playbook=self._diy_playbook,
|
playbook=self._diy_playbook,
|
||||||
play=self._diy_play,
|
play=self._diy_play,
|
||||||
task=self._diy_included_file._task,
|
task=self._diy_included_file._task,
|
||||||
included_file=self._diy_included_file,
|
included_file=self._diy_included_file
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1467,14 +1399,17 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_playbook_on_include(included_file)
|
super(CallbackModule, self).v2_playbook_on_include(included_file)
|
||||||
|
|
||||||
def v2_on_file_diff(self, result):
|
def v2_on_file_diff(self, result):
|
||||||
self._diy_spec = self._get_output_specification(
|
self._diy_spec = self._get_output_specification(
|
||||||
loader=self._diy_loader,
|
loader=self._diy_loader,
|
||||||
variables=self._get_vars(
|
variables=self._get_vars(
|
||||||
playbook=self._diy_playbook, play=self._diy_play, task=self._diy_task, result=result
|
playbook=self._diy_playbook,
|
||||||
),
|
play=self._diy_play,
|
||||||
|
task=self._diy_task,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._using_diy(spec=self._diy_spec):
|
if self._using_diy(spec=self._diy_spec):
|
||||||
@@ -1482,4 +1417,4 @@ class CallbackModule(Default):
|
|||||||
|
|
||||||
if self._parent_has_callback():
|
if self._parent_has_callback():
|
||||||
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
with self._suppress_stdout(enabled=self._using_diy(spec=self._diy_spec)):
|
||||||
super().v2_on_file_diff(result)
|
super(CallbackModule, self).v2_on_file_diff(result)
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ from ansible.errors import AnsibleError, AnsibleRuntimeError
|
|||||||
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
||||||
from ansible.plugins.callback import CallbackBase
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
|
||||||
ELASTIC_LIBRARY_IMPORT_ERROR: ImportError | None
|
|
||||||
try:
|
try:
|
||||||
from elasticapm import Client, capture_span, trace_parent_from_string, instrument, label
|
from elasticapm import Client, capture_span, trace_parent_from_string, instrument, label
|
||||||
except ImportError as imp_exc:
|
except ImportError as imp_exc:
|
||||||
@@ -116,9 +115,9 @@ class TaskData:
|
|||||||
|
|
||||||
def add_host(self, host):
|
def add_host(self, host):
|
||||||
if host.uuid in self.host_data:
|
if host.uuid in self.host_data:
|
||||||
if host.status == "included":
|
if host.status == 'included':
|
||||||
# concatenate task include output from multiple items
|
# concatenate task include output from multiple items
|
||||||
host.result = f"{self.host_data[host.uuid].result}\n{host.result}"
|
host.result = f'{self.host_data[host.uuid].result}\n{host.result}'
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -138,21 +137,21 @@ class HostData:
|
|||||||
self.finish = time.time()
|
self.finish = time.time()
|
||||||
|
|
||||||
|
|
||||||
class ElasticSource:
|
class ElasticSource(object):
|
||||||
def __init__(self, display):
|
def __init__(self, display):
|
||||||
self.ansible_playbook = ""
|
self.ansible_playbook = ""
|
||||||
self.session = str(uuid.uuid4())
|
self.session = str(uuid.uuid4())
|
||||||
self.host = socket.gethostname()
|
self.host = socket.gethostname()
|
||||||
try:
|
try:
|
||||||
self.ip_address = socket.gethostbyname(socket.gethostname())
|
self.ip_address = socket.gethostbyname(socket.gethostname())
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.ip_address = None
|
self.ip_address = None
|
||||||
self.user = getpass.getuser()
|
self.user = getpass.getuser()
|
||||||
|
|
||||||
self._display = display
|
self._display = display
|
||||||
|
|
||||||
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
|
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
|
||||||
"""record the start of a task for one or more hosts"""
|
""" record the start of a task for one or more hosts """
|
||||||
|
|
||||||
uuid = task._uuid
|
uuid = task._uuid
|
||||||
|
|
||||||
@@ -165,50 +164,38 @@ class ElasticSource:
|
|||||||
args = None
|
args = None
|
||||||
|
|
||||||
if not task.no_log and not hide_task_arguments:
|
if not task.no_log and not hide_task_arguments:
|
||||||
args = ", ".join((f"{k}={v}" for k, v in task.args.items()))
|
args = ', '.join((f'{k}={v}' for k, v in task.args.items()))
|
||||||
|
|
||||||
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
|
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
|
||||||
|
|
||||||
def finish_task(self, tasks_data, status, result):
|
def finish_task(self, tasks_data, status, result):
|
||||||
"""record the results of a task for a single host"""
|
""" record the results of a task for a single host """
|
||||||
|
|
||||||
task_uuid = result._task._uuid
|
task_uuid = result._task._uuid
|
||||||
|
|
||||||
if hasattr(result, "_host") and result._host is not None:
|
if hasattr(result, '_host') and result._host is not None:
|
||||||
host_uuid = result._host._uuid
|
host_uuid = result._host._uuid
|
||||||
host_name = result._host.name
|
host_name = result._host.name
|
||||||
else:
|
else:
|
||||||
host_uuid = "include"
|
host_uuid = 'include'
|
||||||
host_name = "include"
|
host_name = 'include'
|
||||||
|
|
||||||
task = tasks_data[task_uuid]
|
task = tasks_data[task_uuid]
|
||||||
|
|
||||||
task.add_host(HostData(host_uuid, host_name, status, result))
|
task.add_host(HostData(host_uuid, host_name, status, result))
|
||||||
|
|
||||||
def generate_distributed_traces(
|
def generate_distributed_traces(self, tasks_data, status, end_time, traceparent, apm_service_name,
|
||||||
self,
|
apm_server_url, apm_verify_server_cert, apm_secret_token, apm_api_key):
|
||||||
tasks_data,
|
""" generate distributed traces from the collected TaskData and HostData """
|
||||||
status,
|
|
||||||
end_time,
|
|
||||||
traceparent,
|
|
||||||
apm_service_name,
|
|
||||||
apm_server_url,
|
|
||||||
apm_verify_server_cert,
|
|
||||||
apm_secret_token,
|
|
||||||
apm_api_key,
|
|
||||||
):
|
|
||||||
"""generate distributed traces from the collected TaskData and HostData"""
|
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
parent_start_time = None
|
parent_start_time = None
|
||||||
for task in tasks_data.values():
|
for task_uuid, task in tasks_data.items():
|
||||||
if parent_start_time is None:
|
if parent_start_time is None:
|
||||||
parent_start_time = task.start
|
parent_start_time = task.start
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
apm_cli = self.init_apm_client(
|
apm_cli = self.init_apm_client(apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key)
|
||||||
apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key
|
|
||||||
)
|
|
||||||
if apm_cli:
|
if apm_cli:
|
||||||
with closing(apm_cli):
|
with closing(apm_cli):
|
||||||
instrument() # Only call this once, as early as possible.
|
instrument() # Only call this once, as early as possible.
|
||||||
@@ -224,86 +211,78 @@ class ElasticSource:
|
|||||||
label(ansible_host_ip=self.ip_address)
|
label(ansible_host_ip=self.ip_address)
|
||||||
|
|
||||||
for task_data in tasks:
|
for task_data in tasks:
|
||||||
for host_data in task_data.host_data.values():
|
for host_uuid, host_data in task_data.host_data.items():
|
||||||
self.create_span_data(apm_cli, task_data, host_data)
|
self.create_span_data(apm_cli, task_data, host_data)
|
||||||
|
|
||||||
apm_cli.end_transaction(name=__name__, result=status, duration=end_time - parent_start_time)
|
apm_cli.end_transaction(name=__name__, result=status, duration=end_time - parent_start_time)
|
||||||
|
|
||||||
def create_span_data(self, apm_cli, task_data, host_data):
|
def create_span_data(self, apm_cli, task_data, host_data):
|
||||||
"""create the span with the given TaskData and HostData"""
|
""" create the span with the given TaskData and HostData """
|
||||||
|
|
||||||
name = f"[{host_data.name}] {task_data.play}: {task_data.name}"
|
name = f'[{host_data.name}] {task_data.play}: {task_data.name}'
|
||||||
|
|
||||||
message = "success"
|
message = "success"
|
||||||
status = "success"
|
status = "success"
|
||||||
enriched_error_message = None
|
enriched_error_message = None
|
||||||
if host_data.status == "included":
|
if host_data.status == 'included':
|
||||||
rc = 0
|
rc = 0
|
||||||
else:
|
else:
|
||||||
res = host_data.result._result
|
res = host_data.result._result
|
||||||
rc = res.get("rc", 0)
|
rc = res.get('rc', 0)
|
||||||
if host_data.status == "failed":
|
if host_data.status == 'failed':
|
||||||
message = self.get_error_message(res)
|
message = self.get_error_message(res)
|
||||||
enriched_error_message = self.enrich_error_message(res)
|
enriched_error_message = self.enrich_error_message(res)
|
||||||
status = "failure"
|
status = "failure"
|
||||||
elif host_data.status == "skipped":
|
elif host_data.status == 'skipped':
|
||||||
if "skip_reason" in res:
|
if 'skip_reason' in res:
|
||||||
message = res["skip_reason"]
|
message = res['skip_reason']
|
||||||
else:
|
else:
|
||||||
message = "skipped"
|
message = 'skipped'
|
||||||
status = "unknown"
|
status = "unknown"
|
||||||
|
|
||||||
with capture_span(
|
with capture_span(task_data.name,
|
||||||
task_data.name,
|
start=task_data.start,
|
||||||
start=task_data.start,
|
span_type="ansible.task.run",
|
||||||
span_type="ansible.task.run",
|
duration=host_data.finish - task_data.start,
|
||||||
duration=host_data.finish - task_data.start,
|
labels={"ansible.task.args": task_data.args,
|
||||||
labels={
|
"ansible.task.message": message,
|
||||||
"ansible.task.args": task_data.args,
|
"ansible.task.module": task_data.action,
|
||||||
"ansible.task.message": message,
|
"ansible.task.name": name,
|
||||||
"ansible.task.module": task_data.action,
|
"ansible.task.result": rc,
|
||||||
"ansible.task.name": name,
|
"ansible.task.host.name": host_data.name,
|
||||||
"ansible.task.result": rc,
|
"ansible.task.host.status": host_data.status}) as span:
|
||||||
"ansible.task.host.name": host_data.name,
|
|
||||||
"ansible.task.host.status": host_data.status,
|
|
||||||
},
|
|
||||||
) as span:
|
|
||||||
span.outcome = status
|
span.outcome = status
|
||||||
if "failure" in status:
|
if 'failure' in status:
|
||||||
exception = AnsibleRuntimeError(
|
exception = AnsibleRuntimeError(message=f"{task_data.action}: {name} failed with error message {enriched_error_message}")
|
||||||
message=f"{task_data.action}: {name} failed with error message {enriched_error_message}"
|
|
||||||
)
|
|
||||||
apm_cli.capture_exception(exc_info=(type(exception), exception, exception.__traceback__), handled=True)
|
apm_cli.capture_exception(exc_info=(type(exception), exception, exception.__traceback__), handled=True)
|
||||||
|
|
||||||
def init_apm_client(self, apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key):
|
def init_apm_client(self, apm_server_url, apm_service_name, apm_verify_server_cert, apm_secret_token, apm_api_key):
|
||||||
if apm_server_url:
|
if apm_server_url:
|
||||||
return Client(
|
return Client(service_name=apm_service_name,
|
||||||
service_name=apm_service_name,
|
server_url=apm_server_url,
|
||||||
server_url=apm_server_url,
|
verify_server_cert=False,
|
||||||
verify_server_cert=False,
|
secret_token=apm_secret_token,
|
||||||
secret_token=apm_secret_token,
|
api_key=apm_api_key,
|
||||||
api_key=apm_api_key,
|
use_elastic_traceparent_header=True,
|
||||||
use_elastic_traceparent_header=True,
|
debug=True)
|
||||||
debug=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_error_message(result):
|
def get_error_message(result):
|
||||||
if result.get("exception") is not None:
|
if result.get('exception') is not None:
|
||||||
return ElasticSource._last_line(result["exception"])
|
return ElasticSource._last_line(result['exception'])
|
||||||
return result.get("msg", "failed")
|
return result.get('msg', 'failed')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _last_line(text):
|
def _last_line(text):
|
||||||
lines = text.strip().split("\n")
|
lines = text.strip().split('\n')
|
||||||
return lines[-1]
|
return lines[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enrich_error_message(result):
|
def enrich_error_message(result):
|
||||||
message = result.get("msg", "failed")
|
message = result.get('msg', 'failed')
|
||||||
exception = result.get("exception")
|
exception = result.get('exception')
|
||||||
stderr = result.get("stderr")
|
stderr = result.get('stderr')
|
||||||
return f'message: "{message}"\nexception: "{exception}"\nstderr: "{stderr}"'
|
return f"message: \"{message}\"\nexception: \"{exception}\"\nstderr: \"{stderr}\""
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
@@ -312,12 +291,12 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.elastic"
|
CALLBACK_NAME = 'community.general.elastic'
|
||||||
CALLBACK_NEEDS_ENABLED = True
|
CALLBACK_NEEDS_ENABLED = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.hide_task_arguments = None
|
self.hide_task_arguments = None
|
||||||
self.apm_service_name = None
|
self.apm_service_name = None
|
||||||
self.ansible_playbook = None
|
self.ansible_playbook = None
|
||||||
@@ -328,28 +307,28 @@ class CallbackModule(CallbackBase):
|
|||||||
self.disabled = False
|
self.disabled = False
|
||||||
|
|
||||||
if ELASTIC_LIBRARY_IMPORT_ERROR:
|
if ELASTIC_LIBRARY_IMPORT_ERROR:
|
||||||
raise AnsibleError(
|
raise AnsibleError('The `elastic-apm` must be installed to use this plugin') from ELASTIC_LIBRARY_IMPORT_ERROR
|
||||||
"The `elastic-apm` must be installed to use this plugin"
|
|
||||||
) from ELASTIC_LIBRARY_IMPORT_ERROR
|
|
||||||
|
|
||||||
self.tasks_data = OrderedDict()
|
self.tasks_data = OrderedDict()
|
||||||
|
|
||||||
self.elastic = ElasticSource(display=self._display)
|
self.elastic = ElasticSource(display=self._display)
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys,
|
||||||
|
var_options=var_options,
|
||||||
|
direct=direct)
|
||||||
|
|
||||||
self.hide_task_arguments = self.get_option("hide_task_arguments")
|
self.hide_task_arguments = self.get_option('hide_task_arguments')
|
||||||
|
|
||||||
self.apm_service_name = self.get_option("apm_service_name")
|
self.apm_service_name = self.get_option('apm_service_name')
|
||||||
if not self.apm_service_name:
|
if not self.apm_service_name:
|
||||||
self.apm_service_name = "ansible"
|
self.apm_service_name = 'ansible'
|
||||||
|
|
||||||
self.apm_server_url = self.get_option("apm_server_url")
|
self.apm_server_url = self.get_option('apm_server_url')
|
||||||
self.apm_secret_token = self.get_option("apm_secret_token")
|
self.apm_secret_token = self.get_option('apm_secret_token')
|
||||||
self.apm_api_key = self.get_option("apm_api_key")
|
self.apm_api_key = self.get_option('apm_api_key')
|
||||||
self.apm_verify_server_cert = self.get_option("apm_verify_server_cert")
|
self.apm_verify_server_cert = self.get_option('apm_verify_server_cert')
|
||||||
self.traceparent = self.get_option("traceparent")
|
self.traceparent = self.get_option('traceparent')
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.ansible_playbook = basename(playbook._file_name)
|
self.ansible_playbook = basename(playbook._file_name)
|
||||||
@@ -358,29 +337,65 @@ class CallbackModule(CallbackBase):
|
|||||||
self.play_name = play.get_name()
|
self.play_name = play.get_name()
|
||||||
|
|
||||||
def v2_runner_on_no_hosts(self, task):
|
def v2_runner_on_no_hosts(self, task):
|
||||||
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.elastic.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.elastic.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_cleanup_task_start(self, task):
|
def v2_playbook_on_cleanup_task_start(self, task):
|
||||||
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.elastic.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_handler_task_start(self, task):
|
def v2_playbook_on_handler_task_start(self, task):
|
||||||
self.elastic.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.elastic.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
self.errors += 1
|
self.errors += 1
|
||||||
self.elastic.finish_task(self.tasks_data, "failed", result)
|
self.elastic.finish_task(
|
||||||
|
self.tasks_data,
|
||||||
|
'failed',
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
self.elastic.finish_task(self.tasks_data, "ok", result)
|
self.elastic.finish_task(
|
||||||
|
self.tasks_data,
|
||||||
|
'ok',
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self.elastic.finish_task(self.tasks_data, "skipped", result)
|
self.elastic.finish_task(
|
||||||
|
self.tasks_data,
|
||||||
|
'skipped',
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_include(self, included_file):
|
def v2_playbook_on_include(self, included_file):
|
||||||
self.elastic.finish_task(self.tasks_data, "included", included_file)
|
self.elastic.finish_task(
|
||||||
|
self.tasks_data,
|
||||||
|
'included',
|
||||||
|
included_file
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats):
|
def v2_playbook_on_stats(self, stats):
|
||||||
if self.errors == 0:
|
if self.errors == 0:
|
||||||
@@ -396,7 +411,7 @@ class CallbackModule(CallbackBase):
|
|||||||
self.apm_server_url,
|
self.apm_server_url,
|
||||||
self.apm_verify_server_cert,
|
self.apm_verify_server_cert,
|
||||||
self.apm_secret_token,
|
self.apm_secret_token,
|
||||||
self.apm_api_key,
|
self.apm_api_key
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_async_failed(self, result, **kwargs):
|
def v2_runner_on_async_failed(self, result, **kwargs):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2016 maxn nikolaev.makc@gmail.com
|
# Copyright (C) 2016 maxn nikolaev.makc@gmail.com
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -54,31 +55,29 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.jabber"
|
CALLBACK_NAME = 'community.general.jabber'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
|
||||||
|
super(CallbackModule, self).__init__(display=display)
|
||||||
|
|
||||||
if not HAS_XMPP:
|
if not HAS_XMPP:
|
||||||
self._display.warning(
|
self._display.warning("The required python xmpp library (xmpppy) is not installed. "
|
||||||
"The required python xmpp library (xmpppy) is not installed. "
|
"pip install git+https://github.com/ArchipelProject/xmpppy")
|
||||||
"pip install git+https://github.com/ArchipelProject/xmpppy"
|
|
||||||
)
|
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
|
|
||||||
self.serv = os.getenv("JABBER_SERV")
|
self.serv = os.getenv('JABBER_SERV')
|
||||||
self.j_user = os.getenv("JABBER_USER")
|
self.j_user = os.getenv('JABBER_USER')
|
||||||
self.j_pass = os.getenv("JABBER_PASS")
|
self.j_pass = os.getenv('JABBER_PASS')
|
||||||
self.j_to = os.getenv("JABBER_TO")
|
self.j_to = os.getenv('JABBER_TO')
|
||||||
|
|
||||||
if (self.j_user or self.j_pass or self.serv or self.j_to) is None:
|
if (self.j_user or self.j_pass or self.serv or self.j_to) is None:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('Jabber CallBack wants the JABBER_SERV, JABBER_USER, JABBER_PASS and JABBER_TO environment variables')
|
||||||
"Jabber CallBack wants the JABBER_SERV, JABBER_USER, JABBER_PASS and JABBER_TO environment variables"
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_msg(self, msg):
|
def send_msg(self, msg):
|
||||||
"""Send message"""
|
"""Send message"""
|
||||||
@@ -87,7 +86,7 @@ class CallbackModule(CallbackBase):
|
|||||||
client.connect(server=(self.serv, 5222))
|
client.connect(server=(self.serv, 5222))
|
||||||
client.auth(jid.getNode(), self.j_pass, resource=jid.getResource())
|
client.auth(jid.getNode(), self.j_pass, resource=jid.getResource())
|
||||||
message = xmpp.Message(self.j_to, msg)
|
message = xmpp.Message(self.j_to, msg)
|
||||||
message.setAttr("type", "chat")
|
message.setAttr('type', 'chat')
|
||||||
client.send(message)
|
client.send(message)
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
|
||||||
@@ -111,9 +110,9 @@ class CallbackModule(CallbackBase):
|
|||||||
unreachable = False
|
unreachable = False
|
||||||
for h in hosts:
|
for h in hosts:
|
||||||
s = stats.summarize(h)
|
s = stats.summarize(h)
|
||||||
if s["failures"] > 0:
|
if s['failures'] > 0:
|
||||||
failures = True
|
failures = True
|
||||||
if s["unreachable"] > 0:
|
if s['unreachable'] > 0:
|
||||||
unreachable = True
|
unreachable = True
|
||||||
|
|
||||||
if failures or unreachable:
|
if failures or unreachable:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
# Copyright (c) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -49,10 +50,9 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
logs playbook results, per host, in /var/log/ansible/hosts
|
logs playbook results, per host, in /var/log/ansible/hosts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.log_plays"
|
CALLBACK_NAME = 'community.general.log_plays'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
TIME_FORMAT = "%b %d %Y %H:%M:%S"
|
TIME_FORMAT = "%b %d %Y %H:%M:%S"
|
||||||
@@ -62,10 +62,11 @@ class CallbackModule(CallbackBase):
|
|||||||
return f"{now} - {playbook} - {task_name} - {task_action} - {category} - {data}\n\n"
|
return f"{now} - {playbook} - {task_name} - {task_action} - {category} - {data}\n\n"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.log_folder = self.get_option("log_folder")
|
self.log_folder = self.get_option("log_folder")
|
||||||
|
|
||||||
@@ -75,12 +76,12 @@ class CallbackModule(CallbackBase):
|
|||||||
def log(self, result, category):
|
def log(self, result, category):
|
||||||
data = result._result
|
data = result._result
|
||||||
if isinstance(data, MutableMapping):
|
if isinstance(data, MutableMapping):
|
||||||
if "_ansible_verbose_override" in data:
|
if '_ansible_verbose_override' in data:
|
||||||
# avoid logging extraneous data
|
# avoid logging extraneous data
|
||||||
data = "omitted"
|
data = 'omitted'
|
||||||
else:
|
else:
|
||||||
data = data.copy()
|
data = data.copy()
|
||||||
invocation = data.pop("invocation", None)
|
invocation = data.pop('invocation', None)
|
||||||
data = json.dumps(data, cls=AnsibleJSONEncoder)
|
data = json.dumps(data, cls=AnsibleJSONEncoder)
|
||||||
if invocation is not None:
|
if invocation is not None:
|
||||||
data = f"{json.dumps(invocation)} => {data} "
|
data = f"{json.dumps(invocation)} => {data} "
|
||||||
@@ -93,25 +94,25 @@ class CallbackModule(CallbackBase):
|
|||||||
fd.write(msg)
|
fd.write(msg)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
self.log(result, "FAILED")
|
self.log(result, 'FAILED')
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
self.log(result, "OK")
|
self.log(result, 'OK')
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self.log(result, "SKIPPED")
|
self.log(result, 'SKIPPED')
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
self.log(result, "UNREACHABLE")
|
self.log(result, 'UNREACHABLE')
|
||||||
|
|
||||||
def v2_runner_on_async_failed(self, result):
|
def v2_runner_on_async_failed(self, result):
|
||||||
self.log(result, "ASYNC_FAILED")
|
self.log(result, 'ASYNC_FAILED')
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.playbook = playbook._file_name
|
self.playbook = playbook._file_name
|
||||||
|
|
||||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||||
self.log(result, "IMPORTED", imported_file)
|
self.log(result, 'IMPORTED', imported_file)
|
||||||
|
|
||||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||||
self.log(result, "NOTIMPORTED", missing_file)
|
self.log(result, 'NOTIMPORTED', missing_file)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) Ansible project
|
# Copyright (c) Ansible project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -71,7 +72,7 @@ from ansible_collections.community.general.plugins.module_utils.datetime import
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AzureLogAnalyticsSource:
|
class AzureLogAnalyticsSource(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ansible_check_mode = False
|
self.ansible_check_mode = False
|
||||||
self.ansible_playbook = ""
|
self.ansible_playbook = ""
|
||||||
@@ -83,10 +84,11 @@ class AzureLogAnalyticsSource:
|
|||||||
def __build_signature(self, date, workspace_id, shared_key, content_length):
|
def __build_signature(self, date, workspace_id, shared_key, content_length):
|
||||||
# Build authorisation signature for Azure log analytics API call
|
# Build authorisation signature for Azure log analytics API call
|
||||||
sigs = f"POST\n{content_length}\napplication/json\nx-ms-date:{date}\n/api/logs"
|
sigs = f"POST\n{content_length}\napplication/json\nx-ms-date:{date}\n/api/logs"
|
||||||
utf8_sigs = sigs.encode("utf-8")
|
utf8_sigs = sigs.encode('utf-8')
|
||||||
decoded_shared_key = base64.b64decode(shared_key)
|
decoded_shared_key = base64.b64decode(shared_key)
|
||||||
hmac_sha256_sigs = hmac.new(decoded_shared_key, utf8_sigs, digestmod=hashlib.sha256).digest()
|
hmac_sha256_sigs = hmac.new(
|
||||||
encoded_hash = base64.b64encode(hmac_sha256_sigs).decode("utf-8")
|
decoded_shared_key, utf8_sigs, digestmod=hashlib.sha256).digest()
|
||||||
|
encoded_hash = base64.b64encode(hmac_sha256_sigs).decode('utf-8')
|
||||||
signature = f"SharedKey {workspace_id}:{encoded_hash}"
|
signature = f"SharedKey {workspace_id}:{encoded_hash}"
|
||||||
return signature
|
return signature
|
||||||
|
|
||||||
@@ -94,10 +96,10 @@ class AzureLogAnalyticsSource:
|
|||||||
return f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
|
return f"https://{workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
|
||||||
|
|
||||||
def __rfc1123date(self):
|
def __rfc1123date(self):
|
||||||
return now().strftime("%a, %d %b %Y %H:%M:%S GMT")
|
return now().strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||||
|
|
||||||
def send_event(self, workspace_id, shared_key, state, result, runtime):
|
def send_event(self, workspace_id, shared_key, state, result, runtime):
|
||||||
if result._task_fields["args"].get("_ansible_check_mode") is True:
|
if result._task_fields['args'].get('_ansible_check_mode') is True:
|
||||||
self.ansible_check_mode = True
|
self.ansible_check_mode = True
|
||||||
|
|
||||||
if result._task._role:
|
if result._task._role:
|
||||||
@@ -106,31 +108,31 @@ class AzureLogAnalyticsSource:
|
|||||||
ansible_role = None
|
ansible_role = None
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
data["uuid"] = result._task._uuid
|
data['uuid'] = result._task._uuid
|
||||||
data["session"] = self.session
|
data['session'] = self.session
|
||||||
data["status"] = state
|
data['status'] = state
|
||||||
data["timestamp"] = self.__rfc1123date()
|
data['timestamp'] = self.__rfc1123date()
|
||||||
data["host"] = self.host
|
data['host'] = self.host
|
||||||
data["user"] = self.user
|
data['user'] = self.user
|
||||||
data["runtime"] = runtime
|
data['runtime'] = runtime
|
||||||
data["ansible_version"] = ansible_version
|
data['ansible_version'] = ansible_version
|
||||||
data["ansible_check_mode"] = self.ansible_check_mode
|
data['ansible_check_mode'] = self.ansible_check_mode
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_playbook"] = self.ansible_playbook
|
data['ansible_playbook'] = self.ansible_playbook
|
||||||
data["ansible_role"] = ansible_role
|
data['ansible_role'] = ansible_role
|
||||||
data["ansible_task"] = result._task_fields
|
data['ansible_task'] = result._task_fields
|
||||||
# Removing args since it can contain sensitive data
|
# Removing args since it can contain sensitive data
|
||||||
if "args" in data["ansible_task"]:
|
if 'args' in data['ansible_task']:
|
||||||
data["ansible_task"].pop("args")
|
data['ansible_task'].pop('args')
|
||||||
data["ansible_result"] = result._result
|
data['ansible_result'] = result._result
|
||||||
if "content" in data["ansible_result"]:
|
if 'content' in data['ansible_result']:
|
||||||
data["ansible_result"].pop("content")
|
data['ansible_result'].pop('content')
|
||||||
|
|
||||||
# Adding extra vars info
|
# Adding extra vars info
|
||||||
data["extra_vars"] = self.extra_vars
|
data['extra_vars'] = self.extra_vars
|
||||||
|
|
||||||
# Preparing the playbook logs as JSON format and send to Azure log analytics
|
# Preparing the playbook logs as JSON format and send to Azure log analytics
|
||||||
jsondata = json.dumps({"event": data}, cls=AnsibleJSONEncoder, sort_keys=True)
|
jsondata = json.dumps({'event': data}, cls=AnsibleJSONEncoder, sort_keys=True)
|
||||||
content_length = len(jsondata)
|
content_length = len(jsondata)
|
||||||
rfc1123date = self.__rfc1123date()
|
rfc1123date = self.__rfc1123date()
|
||||||
signature = self.__build_signature(rfc1123date, workspace_id, shared_key, content_length)
|
signature = self.__build_signature(rfc1123date, workspace_id, shared_key, content_length)
|
||||||
@@ -140,35 +142,38 @@ class AzureLogAnalyticsSource:
|
|||||||
workspace_url,
|
workspace_url,
|
||||||
jsondata,
|
jsondata,
|
||||||
headers={
|
headers={
|
||||||
"content-type": "application/json",
|
'content-type': 'application/json',
|
||||||
"Authorization": signature,
|
'Authorization': signature,
|
||||||
"Log-Type": "ansible_playbook",
|
'Log-Type': 'ansible_playbook',
|
||||||
"x-ms-date": rfc1123date,
|
'x-ms-date': rfc1123date
|
||||||
},
|
},
|
||||||
method="POST",
|
method='POST'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "loganalytics"
|
CALLBACK_NAME = 'loganalytics'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.start_datetimes = {} # Collect task start times
|
self.start_datetimes = {} # Collect task start times
|
||||||
self.workspace_id = None
|
self.workspace_id = None
|
||||||
self.shared_key = None
|
self.shared_key = None
|
||||||
self.loganalytics = AzureLogAnalyticsSource()
|
self.loganalytics = AzureLogAnalyticsSource()
|
||||||
|
|
||||||
def _seconds_since_start(self, result):
|
def _seconds_since_start(self, result):
|
||||||
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
|
return (
|
||||||
|
now() -
|
||||||
|
self.start_datetimes[result._task._uuid]
|
||||||
|
).total_seconds()
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
self.workspace_id = self.get_option("workspace_id")
|
self.workspace_id = self.get_option('workspace_id')
|
||||||
self.shared_key = self.get_option("shared_key")
|
self.shared_key = self.get_option('shared_key')
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
vm = play.get_variable_manager()
|
vm = play.get_variable_manager()
|
||||||
@@ -186,25 +191,45 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
def v2_runner_on_ok(self, result, **kwargs):
|
def v2_runner_on_ok(self, result, **kwargs):
|
||||||
self.loganalytics.send_event(
|
self.loganalytics.send_event(
|
||||||
self.workspace_id, self.shared_key, "OK", result, self._seconds_since_start(result)
|
self.workspace_id,
|
||||||
|
self.shared_key,
|
||||||
|
'OK',
|
||||||
|
result,
|
||||||
|
self._seconds_since_start(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result, **kwargs):
|
def v2_runner_on_skipped(self, result, **kwargs):
|
||||||
self.loganalytics.send_event(
|
self.loganalytics.send_event(
|
||||||
self.workspace_id, self.shared_key, "SKIPPED", result, self._seconds_since_start(result)
|
self.workspace_id,
|
||||||
|
self.shared_key,
|
||||||
|
'SKIPPED',
|
||||||
|
result,
|
||||||
|
self._seconds_since_start(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, **kwargs):
|
def v2_runner_on_failed(self, result, **kwargs):
|
||||||
self.loganalytics.send_event(
|
self.loganalytics.send_event(
|
||||||
self.workspace_id, self.shared_key, "FAILED", result, self._seconds_since_start(result)
|
self.workspace_id,
|
||||||
|
self.shared_key,
|
||||||
|
'FAILED',
|
||||||
|
result,
|
||||||
|
self._seconds_since_start(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def runner_on_async_failed(self, result, **kwargs):
|
def runner_on_async_failed(self, result, **kwargs):
|
||||||
self.loganalytics.send_event(
|
self.loganalytics.send_event(
|
||||||
self.workspace_id, self.shared_key, "FAILED", result, self._seconds_since_start(result)
|
self.workspace_id,
|
||||||
|
self.shared_key,
|
||||||
|
'FAILED',
|
||||||
|
result,
|
||||||
|
self._seconds_since_start(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result, **kwargs):
|
def v2_runner_on_unreachable(self, result, **kwargs):
|
||||||
self.loganalytics.send_event(
|
self.loganalytics.send_event(
|
||||||
self.workspace_id, self.shared_key, "UNREACHABLE", result, self._seconds_since_start(result)
|
self.workspace_id,
|
||||||
|
self.shared_key,
|
||||||
|
'UNREACHABLE',
|
||||||
|
result,
|
||||||
|
self._seconds_since_start(result)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Samir Musali <samir.musali@logdna.com>
|
# Copyright (c) 2018, Samir Musali <samir.musali@logdna.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -64,7 +65,6 @@ from ansible.parsing.ajson import AnsibleJSONEncoder
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from logdna import LogDNAHandler
|
from logdna import LogDNAHandler
|
||||||
|
|
||||||
HAS_LOGDNA = True
|
HAS_LOGDNA = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_LOGDNA = False
|
HAS_LOGDNA = False
|
||||||
@@ -73,12 +73,12 @@ except ImportError:
|
|||||||
# Getting MAC Address of system:
|
# Getting MAC Address of system:
|
||||||
def get_mac():
|
def get_mac():
|
||||||
mac = f"{getnode():012x}"
|
mac = f"{getnode():012x}"
|
||||||
return ":".join(map(lambda index: mac[index : index + 2], range(int(len(mac) / 2))))
|
return ":".join(map(lambda index: mac[index:index + 2], range(int(len(mac) / 2))))
|
||||||
|
|
||||||
|
|
||||||
# Getting hostname of system:
|
# Getting hostname of system:
|
||||||
def get_hostname():
|
def get_hostname():
|
||||||
return str(socket.gethostname()).split(".local", 1)[0]
|
return str(socket.gethostname()).split('.local', 1)[0]
|
||||||
|
|
||||||
|
|
||||||
# Getting IP of system:
|
# Getting IP of system:
|
||||||
@@ -88,10 +88,10 @@ def get_ip():
|
|||||||
except Exception:
|
except Exception:
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
try:
|
try:
|
||||||
s.connect(("10.255.255.255", 1))
|
s.connect(('10.255.255.255', 1))
|
||||||
IP = s.getsockname()[0]
|
IP = s.getsockname()[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
IP = "127.0.0.1"
|
IP = '127.0.0.1'
|
||||||
finally:
|
finally:
|
||||||
s.close()
|
s.close()
|
||||||
return IP
|
return IP
|
||||||
@@ -108,13 +108,14 @@ def isJSONable(obj):
|
|||||||
|
|
||||||
# LogDNA Callback Module:
|
# LogDNA Callback Module:
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
|
|
||||||
CALLBACK_VERSION = 0.1
|
CALLBACK_VERSION = 0.1
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.logdna"
|
CALLBACK_NAME = 'community.general.logdna'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
|
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self.playbook_name = None
|
self.playbook_name = None
|
||||||
@@ -125,29 +126,29 @@ class CallbackModule(CallbackBase):
|
|||||||
self.conf_tags = None
|
self.conf_tags = None
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.conf_key = self.get_option("conf_key")
|
self.conf_key = self.get_option('conf_key')
|
||||||
self.plugin_ignore_errors = self.get_option("plugin_ignore_errors")
|
self.plugin_ignore_errors = self.get_option('plugin_ignore_errors')
|
||||||
self.conf_hostname = self.get_option("conf_hostname")
|
self.conf_hostname = self.get_option('conf_hostname')
|
||||||
self.conf_tags = self.get_option("conf_tags")
|
self.conf_tags = self.get_option('conf_tags')
|
||||||
self.mac = get_mac()
|
self.mac = get_mac()
|
||||||
self.ip = get_ip()
|
self.ip = get_ip()
|
||||||
|
|
||||||
if self.conf_hostname is None:
|
if self.conf_hostname is None:
|
||||||
self.conf_hostname = get_hostname()
|
self.conf_hostname = get_hostname()
|
||||||
|
|
||||||
self.conf_tags = self.conf_tags.split(",")
|
self.conf_tags = self.conf_tags.split(',')
|
||||||
|
|
||||||
if HAS_LOGDNA:
|
if HAS_LOGDNA:
|
||||||
self.log = logging.getLogger("logdna")
|
self.log = logging.getLogger('logdna')
|
||||||
self.log.setLevel(logging.INFO)
|
self.log.setLevel(logging.INFO)
|
||||||
self.options = {"hostname": self.conf_hostname, "mac": self.mac, "index_meta": True}
|
self.options = {'hostname': self.conf_hostname, 'mac': self.mac, 'index_meta': True}
|
||||||
self.log.addHandler(LogDNAHandler(self.conf_key, self.options))
|
self.log.addHandler(LogDNAHandler(self.conf_key, self.options))
|
||||||
self.disabled = False
|
self.disabled = False
|
||||||
else:
|
else:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning("WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`")
|
self._display.warning('WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`')
|
||||||
|
|
||||||
def metaIndexing(self, meta):
|
def metaIndexing(self, meta):
|
||||||
invalidKeys = []
|
invalidKeys = []
|
||||||
@@ -159,25 +160,25 @@ class CallbackModule(CallbackBase):
|
|||||||
if ninvalidKeys > 0:
|
if ninvalidKeys > 0:
|
||||||
for key in invalidKeys:
|
for key in invalidKeys:
|
||||||
del meta[key]
|
del meta[key]
|
||||||
meta["__errors"] = f"These keys have been sanitized: {', '.join(invalidKeys)}"
|
meta['__errors'] = f"These keys have been sanitized: {', '.join(invalidKeys)}"
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
def sanitizeJSON(self, data):
|
def sanitizeJSON(self, data):
|
||||||
try:
|
try:
|
||||||
return json.loads(json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder))
|
return json.loads(json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder))
|
||||||
except Exception:
|
except Exception:
|
||||||
return {"warnings": ["JSON Formatting Issue", json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]}
|
return {'warnings': ['JSON Formatting Issue', json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]}
|
||||||
|
|
||||||
def flush(self, log, options):
|
def flush(self, log, options):
|
||||||
if HAS_LOGDNA:
|
if HAS_LOGDNA:
|
||||||
self.log.info(json.dumps(log), options)
|
self.log.info(json.dumps(log), options)
|
||||||
|
|
||||||
def sendLog(self, host, category, logdata):
|
def sendLog(self, host, category, logdata):
|
||||||
options = {"app": "ansible", "meta": {"playbook": self.playbook_name, "host": host, "category": category}}
|
options = {'app': 'ansible', 'meta': {'playbook': self.playbook_name, 'host': host, 'category': category}}
|
||||||
logdata["info"].pop("invocation", None)
|
logdata['info'].pop('invocation', None)
|
||||||
warnings = logdata["info"].pop("warnings", None)
|
warnings = logdata['info'].pop('warnings', None)
|
||||||
if warnings is not None:
|
if warnings is not None:
|
||||||
self.flush({"warn": warnings}, options)
|
self.flush({'warn': warnings}, options)
|
||||||
self.flush(logdata, options)
|
self.flush(logdata, options)
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
@@ -188,21 +189,21 @@ class CallbackModule(CallbackBase):
|
|||||||
result = dict()
|
result = dict()
|
||||||
for host in stats.processed.keys():
|
for host in stats.processed.keys():
|
||||||
result[host] = stats.summarize(host)
|
result[host] = stats.summarize(host)
|
||||||
self.sendLog(self.conf_hostname, "STATS", {"info": self.sanitizeJSON(result)})
|
self.sendLog(self.conf_hostname, 'STATS', {'info': self.sanitizeJSON(result)})
|
||||||
|
|
||||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
def runner_on_failed(self, host, res, ignore_errors=False):
|
||||||
if self.plugin_ignore_errors:
|
if self.plugin_ignore_errors:
|
||||||
ignore_errors = self.plugin_ignore_errors
|
ignore_errors = self.plugin_ignore_errors
|
||||||
self.sendLog(host, "FAILED", {"info": self.sanitizeJSON(res), "ignore_errors": ignore_errors})
|
self.sendLog(host, 'FAILED', {'info': self.sanitizeJSON(res), 'ignore_errors': ignore_errors})
|
||||||
|
|
||||||
def runner_on_ok(self, host, res):
|
def runner_on_ok(self, host, res):
|
||||||
self.sendLog(host, "OK", {"info": self.sanitizeJSON(res)})
|
self.sendLog(host, 'OK', {'info': self.sanitizeJSON(res)})
|
||||||
|
|
||||||
def runner_on_unreachable(self, host, res):
|
def runner_on_unreachable(self, host, res):
|
||||||
self.sendLog(host, "UNREACHABLE", {"info": self.sanitizeJSON(res)})
|
self.sendLog(host, 'UNREACHABLE', {'info': self.sanitizeJSON(res)})
|
||||||
|
|
||||||
def runner_on_async_failed(self, host, res, jid):
|
def runner_on_async_failed(self, host, res, jid):
|
||||||
self.sendLog(host, "ASYNC_FAILED", {"info": self.sanitizeJSON(res), "job_id": jid})
|
self.sendLog(host, 'ASYNC_FAILED', {'info': self.sanitizeJSON(res), 'job_id': jid})
|
||||||
|
|
||||||
def runner_on_async_ok(self, host, res, jid):
|
def runner_on_async_ok(self, host, res, jid):
|
||||||
self.sendLog(host, "ASYNC_OK", {"info": self.sanitizeJSON(res), "job_id": jid})
|
self.sendLog(host, 'ASYNC_OK', {'info': self.sanitizeJSON(res), 'job_id': jid})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2015, Logentries.com, Jimmy Tang <jimmy.tang@logentries.com>
|
# Copyright (c) 2015, Logentries.com, Jimmy Tang <jimmy.tang@logentries.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -103,14 +104,12 @@ import uuid
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import certifi
|
import certifi
|
||||||
|
|
||||||
HAS_CERTIFI = True
|
HAS_CERTIFI = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_CERTIFI = False
|
HAS_CERTIFI = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import flatdict
|
import flatdict
|
||||||
|
|
||||||
HAS_FLATDICT = True
|
HAS_FLATDICT = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_FLATDICT = False
|
HAS_FLATDICT = False
|
||||||
@@ -122,8 +121,9 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
# * Better formatting of output before sending out to logentries data/api nodes.
|
# * Better formatting of output before sending out to logentries data/api nodes.
|
||||||
|
|
||||||
|
|
||||||
class PlainTextSocketAppender:
|
class PlainTextSocketAppender(object):
|
||||||
def __init__(self, display, LE_API="data.logentries.com", LE_PORT=80, LE_TLS_PORT=443):
|
def __init__(self, display, LE_API='data.logentries.com', LE_PORT=80, LE_TLS_PORT=443):
|
||||||
|
|
||||||
self.LE_API = LE_API
|
self.LE_API = LE_API
|
||||||
self.LE_PORT = LE_PORT
|
self.LE_PORT = LE_PORT
|
||||||
self.LE_TLS_PORT = LE_TLS_PORT
|
self.LE_TLS_PORT = LE_TLS_PORT
|
||||||
@@ -132,7 +132,7 @@ class PlainTextSocketAppender:
|
|||||||
# Error message displayed when an incorrect Token has been detected
|
# Error message displayed when an incorrect Token has been detected
|
||||||
self.INVALID_TOKEN = "\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n"
|
self.INVALID_TOKEN = "\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n"
|
||||||
# Unicode Line separator character \u2028
|
# Unicode Line separator character \u2028
|
||||||
self.LINE_SEP = "\u2028"
|
self.LINE_SEP = '\u2028'
|
||||||
|
|
||||||
self._display = display
|
self._display = display
|
||||||
self._conn = None
|
self._conn = None
|
||||||
@@ -171,13 +171,13 @@ class PlainTextSocketAppender:
|
|||||||
def put(self, data):
|
def put(self, data):
|
||||||
# Replace newlines with Unicode line separator
|
# Replace newlines with Unicode line separator
|
||||||
# for multi-line events
|
# for multi-line events
|
||||||
data = to_text(data, errors="surrogate_or_strict")
|
data = to_text(data, errors='surrogate_or_strict')
|
||||||
multiline = data.replace("\n", self.LINE_SEP)
|
multiline = data.replace('\n', self.LINE_SEP)
|
||||||
multiline += "\n"
|
multiline += "\n"
|
||||||
# Send data, reconnect if needed
|
# Send data, reconnect if needed
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
self._conn.send(to_bytes(multiline, errors="surrogate_or_strict"))
|
self._conn.send(to_bytes(multiline, errors='surrogate_or_strict'))
|
||||||
except socket.error:
|
except socket.error:
|
||||||
self.reopen_connection()
|
self.reopen_connection()
|
||||||
continue
|
continue
|
||||||
@@ -188,7 +188,6 @@ class PlainTextSocketAppender:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
HAS_SSL = True
|
HAS_SSL = True
|
||||||
except ImportError: # for systems without TLS support.
|
except ImportError: # for systems without TLS support.
|
||||||
SocketAppender = PlainTextSocketAppender
|
SocketAppender = PlainTextSocketAppender
|
||||||
@@ -200,28 +199,27 @@ else:
|
|||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
context = ssl.create_default_context(
|
context = ssl.create_default_context(
|
||||||
purpose=ssl.Purpose.SERVER_AUTH,
|
purpose=ssl.Purpose.SERVER_AUTH,
|
||||||
cafile=certifi.where(),
|
cafile=certifi.where(), )
|
||||||
)
|
|
||||||
sock = context.wrap_socket(
|
sock = context.wrap_socket(
|
||||||
sock=sock,
|
sock=sock,
|
||||||
do_handshake_on_connect=True,
|
do_handshake_on_connect=True,
|
||||||
suppress_ragged_eofs=True,
|
suppress_ragged_eofs=True, )
|
||||||
)
|
|
||||||
sock.connect((self.LE_API, self.LE_TLS_PORT))
|
sock.connect((self.LE_API, self.LE_TLS_PORT))
|
||||||
self._conn = sock
|
self._conn = sock
|
||||||
|
|
||||||
SocketAppender = TLSSocketAppender # type: ignore
|
SocketAppender = TLSSocketAppender
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.logentries"
|
CALLBACK_NAME = 'community.general.logentries'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
# TODO: allow for alternate posting methods (REST/UDP/agent/etc)
|
# TODO: allow for alternate posting methods (REST/UDP/agent/etc)
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
# verify dependencies
|
# verify dependencies
|
||||||
if not HAS_SSL:
|
if not HAS_SSL:
|
||||||
@@ -229,9 +227,7 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
if not HAS_CERTIFI:
|
if not HAS_CERTIFI:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.')
|
||||||
"The `certifi` python module is not installed.\nDisabling the Logentries callback plugin."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.le_jobid = str(uuid.uuid4())
|
self.le_jobid = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -239,47 +235,41 @@ class CallbackModule(CallbackBase):
|
|||||||
self.timeout = 10
|
self.timeout = 10
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
|
||||||
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
# get options
|
# get options
|
||||||
try:
|
try:
|
||||||
self.api_url = self.get_option("api")
|
self.api_url = self.get_option('api')
|
||||||
self.api_port = self.get_option("port")
|
self.api_port = self.get_option('port')
|
||||||
self.api_tls_port = self.get_option("tls_port")
|
self.api_tls_port = self.get_option('tls_port')
|
||||||
self.use_tls = self.get_option("use_tls")
|
self.use_tls = self.get_option('use_tls')
|
||||||
self.flatten = self.get_option("flatten")
|
self.flatten = self.get_option('flatten')
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self._display.warning(f"Missing option for Logentries callback plugin: {e}")
|
self._display.warning(f"Missing option for Logentries callback plugin: {e}")
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.token = self.get_option("token")
|
self.token = self.get_option('token')
|
||||||
except KeyError:
|
except KeyError as e:
|
||||||
self._display.warning(
|
self._display.warning('Logentries token was not provided, this is required for this callback to operate, disabling')
|
||||||
"Logentries token was not provided, this is required for this callback to operate, disabling"
|
|
||||||
)
|
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
|
|
||||||
if self.flatten and not HAS_FLATDICT:
|
if self.flatten and not HAS_FLATDICT:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin.')
|
||||||
"You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin."
|
|
||||||
)
|
|
||||||
|
|
||||||
self._initialize_connections()
|
self._initialize_connections()
|
||||||
|
|
||||||
def _initialize_connections(self):
|
def _initialize_connections(self):
|
||||||
|
|
||||||
if not self.disabled:
|
if not self.disabled:
|
||||||
if self.use_tls:
|
if self.use_tls:
|
||||||
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_tls_port} with TLS")
|
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_tls_port} with TLS")
|
||||||
self._appender = TLSSocketAppender(
|
self._appender = TLSSocketAppender(display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port)
|
||||||
display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_port}")
|
self._display.vvvv(f"Connecting to {self.api_url}:{self.api_port}")
|
||||||
self._appender = PlainTextSocketAppender(
|
self._appender = PlainTextSocketAppender(display=self._display, LE_API=self.api_url, LE_PORT=self.api_port)
|
||||||
display=self._display, LE_API=self.api_url, LE_PORT=self.api_port
|
|
||||||
)
|
|
||||||
self._appender.reopen_connection()
|
self._appender.reopen_connection()
|
||||||
|
|
||||||
def emit_formatted(self, record):
|
def emit_formatted(self, record):
|
||||||
@@ -290,50 +280,50 @@ class CallbackModule(CallbackBase):
|
|||||||
self.emit(self._dump_results(record))
|
self.emit(self._dump_results(record))
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
msg = record.rstrip("\n")
|
msg = record.rstrip('\n')
|
||||||
msg = f"{self.token} {msg}"
|
msg = f"{self.token} {msg}"
|
||||||
self._appender.put(msg)
|
self._appender.put(msg)
|
||||||
self._display.vvvv("Sent event to logentries")
|
self._display.vvvv("Sent event to logentries")
|
||||||
|
|
||||||
def _set_info(self, host, res):
|
def _set_info(self, host, res):
|
||||||
return {"le_jobid": self.le_jobid, "hostname": host, "results": res}
|
return {'le_jobid': self.le_jobid, 'hostname': host, 'results': res}
|
||||||
|
|
||||||
def runner_on_ok(self, host, res):
|
def runner_on_ok(self, host, res):
|
||||||
results = self._set_info(host, res)
|
results = self._set_info(host, res)
|
||||||
results["status"] = "OK"
|
results['status'] = 'OK'
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
def runner_on_failed(self, host, res, ignore_errors=False):
|
||||||
results = self._set_info(host, res)
|
results = self._set_info(host, res)
|
||||||
results["status"] = "FAILED"
|
results['status'] = 'FAILED'
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def runner_on_skipped(self, host, item=None):
|
def runner_on_skipped(self, host, item=None):
|
||||||
results = self._set_info(host, item)
|
results = self._set_info(host, item)
|
||||||
del results["results"]
|
del results['results']
|
||||||
results["status"] = "SKIPPED"
|
results['status'] = 'SKIPPED'
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def runner_on_unreachable(self, host, res):
|
def runner_on_unreachable(self, host, res):
|
||||||
results = self._set_info(host, res)
|
results = self._set_info(host, res)
|
||||||
results["status"] = "UNREACHABLE"
|
results['status'] = 'UNREACHABLE'
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def runner_on_async_failed(self, host, res, jid):
|
def runner_on_async_failed(self, host, res, jid):
|
||||||
results = self._set_info(host, res)
|
results = self._set_info(host, res)
|
||||||
results["jid"] = jid
|
results['jid'] = jid
|
||||||
results["status"] = "ASYNC_FAILED"
|
results['status'] = 'ASYNC_FAILED'
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
results = {}
|
results = {}
|
||||||
results["le_jobid"] = self.le_jobid
|
results['le_jobid'] = self.le_jobid
|
||||||
results["started_by"] = os.getlogin()
|
results['started_by'] = os.getlogin()
|
||||||
if play.name:
|
if play.name:
|
||||||
results["play"] = play.name
|
results['play'] = play.name
|
||||||
results["hosts"] = play.hosts
|
results['hosts'] = play.hosts
|
||||||
self.emit_formatted(results)
|
self.emit_formatted(results)
|
||||||
|
|
||||||
def playbook_on_stats(self, stats):
|
def playbook_on_stats(self, stats):
|
||||||
"""close connection"""
|
""" close connection """
|
||||||
self._appender.close_connection()
|
self._appender.close_connection()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2020, Yevhen Khmelenko <ujenmr@gmail.com>
|
# Copyright (c) 2020, Yevhen Khmelenko <ujenmr@gmail.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -103,7 +104,6 @@ import logging
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import logstash
|
import logstash
|
||||||
|
|
||||||
HAS_LOGSTASH = True
|
HAS_LOGSTASH = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_LOGSTASH = False
|
HAS_LOGSTASH = False
|
||||||
@@ -116,13 +116,14 @@ from ansible_collections.community.general.plugins.module_utils.datetime import
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.logstash"
|
CALLBACK_NAME = 'community.general.logstash'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
if not HAS_LOGSTASH:
|
if not HAS_LOGSTASH:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
@@ -132,11 +133,14 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
def _init_plugin(self):
|
def _init_plugin(self):
|
||||||
if not self.disabled:
|
if not self.disabled:
|
||||||
self.logger = logging.getLogger("python-logstash-logger")
|
self.logger = logging.getLogger('python-logstash-logger')
|
||||||
self.logger.setLevel(logging.DEBUG)
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
self.handler = logstash.TCPLogstashHandler(
|
self.handler = logstash.TCPLogstashHandler(
|
||||||
self.ls_server, self.ls_port, version=1, message_type=self.ls_type
|
self.ls_server,
|
||||||
|
self.ls_port,
|
||||||
|
version=1,
|
||||||
|
message_type=self.ls_type
|
||||||
)
|
)
|
||||||
|
|
||||||
self.logger.addHandler(self.handler)
|
self.logger.addHandler(self.handler)
|
||||||
@@ -144,36 +148,42 @@ class CallbackModule(CallbackBase):
|
|||||||
self.session = str(uuid.uuid4())
|
self.session = str(uuid.uuid4())
|
||||||
self.errors = 0
|
self.errors = 0
|
||||||
|
|
||||||
self.base_data = {"session": self.session, "host": self.hostname}
|
self.base_data = {
|
||||||
|
'session': self.session,
|
||||||
|
'host': self.hostname
|
||||||
|
}
|
||||||
|
|
||||||
if self.ls_pre_command is not None:
|
if self.ls_pre_command is not None:
|
||||||
self.base_data["ansible_pre_command_output"] = os.popen(self.ls_pre_command).read()
|
self.base_data['ansible_pre_command_output'] = os.popen(
|
||||||
|
self.ls_pre_command).read()
|
||||||
|
|
||||||
if context.CLIARGS is not None:
|
if context.CLIARGS is not None:
|
||||||
self.base_data["ansible_checkmode"] = context.CLIARGS.get("check")
|
self.base_data['ansible_checkmode'] = context.CLIARGS.get('check')
|
||||||
self.base_data["ansible_tags"] = context.CLIARGS.get("tags")
|
self.base_data['ansible_tags'] = context.CLIARGS.get('tags')
|
||||||
self.base_data["ansible_skip_tags"] = context.CLIARGS.get("skip_tags")
|
self.base_data['ansible_skip_tags'] = context.CLIARGS.get('skip_tags')
|
||||||
self.base_data["inventory"] = context.CLIARGS.get("inventory")
|
self.base_data['inventory'] = context.CLIARGS.get('inventory')
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.ls_server = self.get_option("server")
|
self.ls_server = self.get_option('server')
|
||||||
self.ls_port = int(self.get_option("port"))
|
self.ls_port = int(self.get_option('port'))
|
||||||
self.ls_type = self.get_option("type")
|
self.ls_type = self.get_option('type')
|
||||||
self.ls_pre_command = self.get_option("pre_command")
|
self.ls_pre_command = self.get_option('pre_command')
|
||||||
self.ls_format_version = self.get_option("format_version")
|
self.ls_format_version = self.get_option('format_version')
|
||||||
|
|
||||||
self._init_plugin()
|
self._init_plugin()
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "start"
|
data['ansible_type'] = "start"
|
||||||
data["status"] = "OK"
|
data['status'] = "OK"
|
||||||
data["ansible_playbook"] = playbook._file_name
|
data['ansible_playbook'] = playbook._file_name
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("START PLAYBOOK | %s", data["ansible_playbook"], extra=data)
|
self.logger.info(
|
||||||
|
"START PLAYBOOK | %s", data['ansible_playbook'], extra=data
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info("ansible start", extra=data)
|
self.logger.info("ansible start", extra=data)
|
||||||
|
|
||||||
@@ -190,13 +200,15 @@ class CallbackModule(CallbackBase):
|
|||||||
status = "FAILED"
|
status = "FAILED"
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "finish"
|
data['ansible_type'] = "finish"
|
||||||
data["status"] = status
|
data['status'] = status
|
||||||
data["ansible_playbook_duration"] = runtime.total_seconds()
|
data['ansible_playbook_duration'] = runtime.total_seconds()
|
||||||
data["ansible_result"] = json.dumps(summarize_stat) # deprecated field
|
data['ansible_result'] = json.dumps(summarize_stat) # deprecated field
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("FINISH PLAYBOOK | %s", json.dumps(summarize_stat), extra=data)
|
self.logger.info(
|
||||||
|
"FINISH PLAYBOOK | %s", json.dumps(summarize_stat), extra=data
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info("ansible stats", extra=data)
|
self.logger.info("ansible stats", extra=data)
|
||||||
|
|
||||||
@@ -207,10 +219,10 @@ class CallbackModule(CallbackBase):
|
|||||||
self.play_name = play.name
|
self.play_name = play.name
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "start"
|
data['ansible_type'] = "start"
|
||||||
data["status"] = "OK"
|
data['status'] = "OK"
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("START PLAY | %s", self.play_name, extra=data)
|
self.logger.info("START PLAY | %s", self.play_name, extra=data)
|
||||||
@@ -220,61 +232,64 @@ class CallbackModule(CallbackBase):
|
|||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
self.task_id = str(task._uuid)
|
self.task_id = str(task._uuid)
|
||||||
|
|
||||||
"""
|
'''
|
||||||
Tasks and handler tasks are dealt with here
|
Tasks and handler tasks are dealt with here
|
||||||
"""
|
'''
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result, **kwargs):
|
def v2_runner_on_ok(self, result, **kwargs):
|
||||||
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
|
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
if task_name == "setup":
|
if task_name == 'setup':
|
||||||
data["ansible_type"] = "setup"
|
data['ansible_type'] = "setup"
|
||||||
data["status"] = "OK"
|
data['status'] = "OK"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["ansible_task"] = task_name
|
data['ansible_task'] = task_name
|
||||||
data["ansible_facts"] = self._dump_results(result._result)
|
data['ansible_facts'] = self._dump_results(result._result)
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
|
||||||
self.logger.info("SETUP FACTS | %s", self._dump_results(result._result), extra=data)
|
|
||||||
else:
|
|
||||||
self.logger.info("ansible facts", extra=data)
|
|
||||||
else:
|
|
||||||
if "changed" in result._result.keys():
|
|
||||||
data["ansible_changed"] = result._result["changed"]
|
|
||||||
else:
|
|
||||||
data["ansible_changed"] = False
|
|
||||||
|
|
||||||
data["ansible_type"] = "task"
|
|
||||||
data["status"] = "OK"
|
|
||||||
data["ansible_host"] = result._host.name
|
|
||||||
data["ansible_play_id"] = self.play_id
|
|
||||||
data["ansible_play_name"] = self.play_name
|
|
||||||
data["ansible_task"] = task_name
|
|
||||||
data["ansible_task_id"] = self.task_id
|
|
||||||
data["ansible_result"] = self._dump_results(result._result)
|
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
"TASK OK | %s | RESULT | %s", task_name, self._dump_results(result._result), extra=data
|
"SETUP FACTS | %s", self._dump_results(result._result), extra=data
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.info("ansible facts", extra=data)
|
||||||
|
else:
|
||||||
|
if 'changed' in result._result.keys():
|
||||||
|
data['ansible_changed'] = result._result['changed']
|
||||||
|
else:
|
||||||
|
data['ansible_changed'] = False
|
||||||
|
|
||||||
|
data['ansible_type'] = "task"
|
||||||
|
data['status'] = "OK"
|
||||||
|
data['ansible_host'] = result._host.name
|
||||||
|
data['ansible_play_id'] = self.play_id
|
||||||
|
data['ansible_play_name'] = self.play_name
|
||||||
|
data['ansible_task'] = task_name
|
||||||
|
data['ansible_task_id'] = self.task_id
|
||||||
|
data['ansible_result'] = self._dump_results(result._result)
|
||||||
|
|
||||||
|
if self.ls_format_version == "v2":
|
||||||
|
self.logger.info(
|
||||||
|
"TASK OK | %s | RESULT | %s",
|
||||||
|
task_name, self._dump_results(result._result), extra=data
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.info("ansible ok", extra=data)
|
self.logger.info("ansible ok", extra=data)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result, **kwargs):
|
def v2_runner_on_skipped(self, result, **kwargs):
|
||||||
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
|
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "task"
|
data['ansible_type'] = "task"
|
||||||
data["status"] = "SKIPPED"
|
data['status'] = "SKIPPED"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["ansible_task"] = task_name
|
data['ansible_task'] = task_name
|
||||||
data["ansible_task_id"] = self.task_id
|
data['ansible_task_id'] = self.task_id
|
||||||
data["ansible_result"] = self._dump_results(result._result)
|
data['ansible_result'] = self._dump_results(result._result)
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("TASK SKIPPED | %s", task_name, extra=data)
|
self.logger.info("TASK SKIPPED | %s", task_name, extra=data)
|
||||||
@@ -283,12 +298,12 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "import"
|
data['ansible_type'] = "import"
|
||||||
data["status"] = "IMPORTED"
|
data['status'] = "IMPORTED"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["imported_file"] = imported_file
|
data['imported_file'] = imported_file
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("IMPORT | %s", imported_file, extra=data)
|
self.logger.info("IMPORT | %s", imported_file, extra=data)
|
||||||
@@ -297,12 +312,12 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "import"
|
data['ansible_type'] = "import"
|
||||||
data["status"] = "NOT IMPORTED"
|
data['status'] = "NOT IMPORTED"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["imported_file"] = missing_file
|
data['imported_file'] = missing_file
|
||||||
|
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.info("NOT IMPORTED | %s", missing_file, extra=data)
|
self.logger.info("NOT IMPORTED | %s", missing_file, extra=data)
|
||||||
@@ -310,81 +325,75 @@ class CallbackModule(CallbackBase):
|
|||||||
self.logger.info("ansible import", extra=data)
|
self.logger.info("ansible import", extra=data)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, **kwargs):
|
def v2_runner_on_failed(self, result, **kwargs):
|
||||||
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
|
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
if "changed" in result._result.keys():
|
if 'changed' in result._result.keys():
|
||||||
data["ansible_changed"] = result._result["changed"]
|
data['ansible_changed'] = result._result['changed']
|
||||||
else:
|
else:
|
||||||
data["ansible_changed"] = False
|
data['ansible_changed'] = False
|
||||||
|
|
||||||
data["ansible_type"] = "task"
|
data['ansible_type'] = "task"
|
||||||
data["status"] = "FAILED"
|
data['status'] = "FAILED"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["ansible_task"] = task_name
|
data['ansible_task'] = task_name
|
||||||
data["ansible_task_id"] = self.task_id
|
data['ansible_task_id'] = self.task_id
|
||||||
data["ansible_result"] = self._dump_results(result._result)
|
data['ansible_result'] = self._dump_results(result._result)
|
||||||
|
|
||||||
self.errors += 1
|
self.errors += 1
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"TASK FAILED | %s | HOST | %s | RESULT | %s",
|
"TASK FAILED | %s | HOST | %s | RESULT | %s",
|
||||||
task_name,
|
task_name, self.hostname,
|
||||||
self.hostname,
|
self._dump_results(result._result), extra=data
|
||||||
self._dump_results(result._result),
|
|
||||||
extra=data,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.error("ansible failed", extra=data)
|
self.logger.error("ansible failed", extra=data)
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result, **kwargs):
|
def v2_runner_on_unreachable(self, result, **kwargs):
|
||||||
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
|
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "task"
|
data['ansible_type'] = "task"
|
||||||
data["status"] = "UNREACHABLE"
|
data['status'] = "UNREACHABLE"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["ansible_task"] = task_name
|
data['ansible_task'] = task_name
|
||||||
data["ansible_task_id"] = self.task_id
|
data['ansible_task_id'] = self.task_id
|
||||||
data["ansible_result"] = self._dump_results(result._result)
|
data['ansible_result'] = self._dump_results(result._result)
|
||||||
|
|
||||||
self.errors += 1
|
self.errors += 1
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"UNREACHABLE | %s | HOST | %s | RESULT | %s",
|
"UNREACHABLE | %s | HOST | %s | RESULT | %s",
|
||||||
task_name,
|
task_name, self.hostname,
|
||||||
self.hostname,
|
self._dump_results(result._result), extra=data
|
||||||
self._dump_results(result._result),
|
|
||||||
extra=data,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.error("ansible unreachable", extra=data)
|
self.logger.error("ansible unreachable", extra=data)
|
||||||
|
|
||||||
def v2_runner_on_async_failed(self, result, **kwargs):
|
def v2_runner_on_async_failed(self, result, **kwargs):
|
||||||
task_name = str(result._task).replace("TASK: ", "").replace("HANDLER: ", "")
|
task_name = str(result._task).replace('TASK: ', '').replace('HANDLER: ', '')
|
||||||
|
|
||||||
data = self.base_data.copy()
|
data = self.base_data.copy()
|
||||||
data["ansible_type"] = "task"
|
data['ansible_type'] = "task"
|
||||||
data["status"] = "FAILED"
|
data['status'] = "FAILED"
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_play_id"] = self.play_id
|
data['ansible_play_id'] = self.play_id
|
||||||
data["ansible_play_name"] = self.play_name
|
data['ansible_play_name'] = self.play_name
|
||||||
data["ansible_task"] = task_name
|
data['ansible_task'] = task_name
|
||||||
data["ansible_task_id"] = self.task_id
|
data['ansible_task_id'] = self.task_id
|
||||||
data["ansible_result"] = self._dump_results(result._result)
|
data['ansible_result'] = self._dump_results(result._result)
|
||||||
|
|
||||||
self.errors += 1
|
self.errors += 1
|
||||||
if self.ls_format_version == "v2":
|
if self.ls_format_version == "v2":
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"ASYNC FAILED | %s | HOST | %s | RESULT | %s",
|
"ASYNC FAILED | %s | HOST | %s | RESULT | %s",
|
||||||
task_name,
|
task_name, self.hostname,
|
||||||
self.hostname,
|
self._dump_results(result._result), extra=data
|
||||||
self._dump_results(result._result),
|
|
||||||
extra=data,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.error("ansible async", extra=data)
|
self.logger.error("ansible async", extra=data)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2012, Dag Wieers <dag@wieers.com>
|
# Copyright (c) 2012, Dag Wieers <dag@wieers.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -91,33 +93,33 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
"""This Ansible callback plugin mails errors to interested parties."""
|
''' This Ansible callback plugin mails errors to interested parties. '''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.mail"
|
CALLBACK_NAME = 'community.general.mail'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.sender = None
|
self.sender = None
|
||||||
self.to = "root"
|
self.to = 'root'
|
||||||
self.smtphost = os.getenv("SMTPHOST", "localhost")
|
self.smtphost = os.getenv('SMTPHOST', 'localhost')
|
||||||
self.smtpport = 25
|
self.smtpport = 25
|
||||||
self.cc = None
|
self.cc = None
|
||||||
self.bcc = None
|
self.bcc = None
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
|
||||||
|
|
||||||
self.sender = self.get_option("sender")
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
self.to = self.get_option("to")
|
|
||||||
self.smtphost = self.get_option("mta")
|
|
||||||
self.smtpport = self.get_option("mtaport")
|
|
||||||
self.cc = self.get_option("cc")
|
|
||||||
self.bcc = self.get_option("bcc")
|
|
||||||
|
|
||||||
def mail(self, subject="Ansible error mail", body=None):
|
self.sender = self.get_option('sender')
|
||||||
|
self.to = self.get_option('to')
|
||||||
|
self.smtphost = self.get_option('mta')
|
||||||
|
self.smtpport = self.get_option('mtaport')
|
||||||
|
self.cc = self.get_option('cc')
|
||||||
|
self.bcc = self.get_option('bcc')
|
||||||
|
|
||||||
|
def mail(self, subject='Ansible error mail', body=None):
|
||||||
if body is None:
|
if body is None:
|
||||||
body = subject
|
body = subject
|
||||||
|
|
||||||
@@ -131,14 +133,14 @@ class CallbackModule(CallbackBase):
|
|||||||
if self.bcc:
|
if self.bcc:
|
||||||
bcc_addresses = email.utils.getaddresses(self.bcc)
|
bcc_addresses = email.utils.getaddresses(self.bcc)
|
||||||
|
|
||||||
content = f"Date: {email.utils.formatdate()}\n"
|
content = f'Date: {email.utils.formatdate()}\n'
|
||||||
content += f"From: {email.utils.formataddr(sender_address)}\n"
|
content += f'From: {email.utils.formataddr(sender_address)}\n'
|
||||||
if self.to:
|
if self.to:
|
||||||
content += f"To: {', '.join([email.utils.formataddr(pair) for pair in to_addresses])}\n"
|
content += f"To: {', '.join([email.utils.formataddr(pair) for pair in to_addresses])}\n"
|
||||||
if self.cc:
|
if self.cc:
|
||||||
content += f"Cc: {', '.join([email.utils.formataddr(pair) for pair in cc_addresses])}\n"
|
content += f"Cc: {', '.join([email.utils.formataddr(pair) for pair in cc_addresses])}\n"
|
||||||
content += f"Message-ID: {email.utils.make_msgid(domain=self.get_option('message_id_domain'))}\n"
|
content += f"Message-ID: {email.utils.make_msgid(domain=self.get_option('message_id_domain'))}\n"
|
||||||
content += f"Subject: {subject.strip()}\n\n"
|
content += f'Subject: {subject.strip()}\n\n'
|
||||||
content += body
|
content += body
|
||||||
|
|
||||||
addresses = to_addresses
|
addresses = to_addresses
|
||||||
@@ -148,23 +150,23 @@ class CallbackModule(CallbackBase):
|
|||||||
addresses += bcc_addresses
|
addresses += bcc_addresses
|
||||||
|
|
||||||
if not addresses:
|
if not addresses:
|
||||||
self._display.warning("No receiver has been specified for the mail callback plugin.")
|
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.sendmail(self.sender, [address for name, address in addresses], to_bytes(content))
|
||||||
|
|
||||||
smtp.quit()
|
smtp.quit()
|
||||||
|
|
||||||
def subject_msg(self, multiline, failtype, linenr):
|
def subject_msg(self, multiline, failtype, linenr):
|
||||||
msg = multiline.strip("\r\n").splitlines()[linenr]
|
msg = multiline.strip('\r\n').splitlines()[linenr]
|
||||||
return f"{failtype}: {msg}"
|
return f'{failtype}: {msg}'
|
||||||
|
|
||||||
def indent(self, multiline, indent=8):
|
def indent(self, multiline, indent=8):
|
||||||
return re.sub("^", " " * indent, multiline, flags=re.MULTILINE)
|
return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)
|
||||||
|
|
||||||
def body_blob(self, multiline, texttype):
|
def body_blob(self, multiline, texttype):
|
||||||
"""Turn some text output in a well-indented block for sending in a mail body"""
|
''' Turn some text output in a well-indented block for sending in a mail body '''
|
||||||
intro = f"with the following {texttype}:\n\n"
|
intro = f'with the following {texttype}:\n\n'
|
||||||
blob = "\n".join(multiline.strip("\r\n").splitlines())
|
blob = "\n".join(multiline.strip('\r\n').splitlines())
|
||||||
return f"{intro}{self.indent(blob)}\n"
|
return f"{intro}{self.indent(blob)}\n"
|
||||||
|
|
||||||
def mail_result(self, result, failtype):
|
def mail_result(self, result, failtype):
|
||||||
@@ -175,87 +177,83 @@ class CallbackModule(CallbackBase):
|
|||||||
# Add subject
|
# Add subject
|
||||||
if self.itembody:
|
if self.itembody:
|
||||||
subject = self.itemsubject
|
subject = self.itemsubject
|
||||||
elif result._result.get("failed_when_result") is True:
|
elif result._result.get('failed_when_result') is True:
|
||||||
subject = "Failed due to 'failed_when' condition"
|
subject = "Failed due to 'failed_when' condition"
|
||||||
elif result._result.get("msg"):
|
elif result._result.get('msg'):
|
||||||
subject = self.subject_msg(result._result["msg"], failtype, 0)
|
subject = self.subject_msg(result._result['msg'], failtype, 0)
|
||||||
elif result._result.get("stderr"):
|
elif result._result.get('stderr'):
|
||||||
subject = self.subject_msg(result._result["stderr"], failtype, -1)
|
subject = self.subject_msg(result._result['stderr'], failtype, -1)
|
||||||
elif result._result.get("stdout"):
|
elif result._result.get('stdout'):
|
||||||
subject = self.subject_msg(result._result["stdout"], failtype, -1)
|
subject = self.subject_msg(result._result['stdout'], failtype, -1)
|
||||||
elif result._result.get("exception"): # Unrelated exceptions are added to output :-/
|
elif result._result.get('exception'): # Unrelated exceptions are added to output :-/
|
||||||
subject = self.subject_msg(result._result["exception"], failtype, -1)
|
subject = self.subject_msg(result._result['exception'], failtype, -1)
|
||||||
else:
|
else:
|
||||||
subject = f"{failtype}: {result._task.name or result._task.action}"
|
subject = f'{failtype}: {result._task.name or result._task.action}'
|
||||||
|
|
||||||
# Make playbook name visible (e.g. in Outlook/Gmail condensed view)
|
# Make playbook name visible (e.g. in Outlook/Gmail condensed view)
|
||||||
body = f"Playbook: {os.path.basename(self.playbook._file_name)}\n"
|
body = f'Playbook: {os.path.basename(self.playbook._file_name)}\n'
|
||||||
if result._task.name:
|
if result._task.name:
|
||||||
body += f"Task: {result._task.name}\n"
|
body += f'Task: {result._task.name}\n'
|
||||||
body += f"Module: {result._task.action}\n"
|
body += f'Module: {result._task.action}\n'
|
||||||
body += f"Host: {host}\n"
|
body += f'Host: {host}\n'
|
||||||
body += "\n"
|
body += '\n'
|
||||||
|
|
||||||
# Add task information (as much as possible)
|
# Add task information (as much as possible)
|
||||||
body += "The following task failed:\n\n"
|
body += 'The following task failed:\n\n'
|
||||||
if "invocation" in result._result:
|
if 'invocation' in result._result:
|
||||||
body += self.indent(
|
body += self.indent(f"{result._task.action}: {json.dumps(result._result['invocation']['module_args'], indent=4)}\n")
|
||||||
f"{result._task.action}: {json.dumps(result._result['invocation']['module_args'], indent=4)}\n"
|
|
||||||
)
|
|
||||||
elif result._task.name:
|
elif result._task.name:
|
||||||
body += self.indent(f"{result._task.name} ({result._task.action})\n")
|
body += self.indent(f'{result._task.name} ({result._task.action})\n')
|
||||||
else:
|
else:
|
||||||
body += self.indent(f"{result._task.action}\n")
|
body += self.indent(f'{result._task.action}\n')
|
||||||
body += "\n"
|
body += '\n'
|
||||||
|
|
||||||
# Add item / message
|
# Add item / message
|
||||||
if self.itembody:
|
if self.itembody:
|
||||||
body += self.itembody
|
body += self.itembody
|
||||||
elif result._result.get("failed_when_result") is True:
|
elif result._result.get('failed_when_result') is True:
|
||||||
fail_cond_list = "\n- ".join(result._task.failed_when)
|
fail_cond_list = '\n- '.join(result._task.failed_when)
|
||||||
fail_cond = self.indent(f"failed_when:\n- {fail_cond_list}")
|
fail_cond = self.indent(f"failed_when:\n- {fail_cond_list}")
|
||||||
body += f"due to the following condition:\n\n{fail_cond}\n\n"
|
body += f"due to the following condition:\n\n{fail_cond}\n\n"
|
||||||
elif result._result.get("msg"):
|
elif result._result.get('msg'):
|
||||||
body += self.body_blob(result._result["msg"], "message")
|
body += self.body_blob(result._result['msg'], 'message')
|
||||||
|
|
||||||
# Add stdout / stderr / exception / warnings / deprecations
|
# Add stdout / stderr / exception / warnings / deprecations
|
||||||
if result._result.get("stdout"):
|
if result._result.get('stdout'):
|
||||||
body += self.body_blob(result._result["stdout"], "standard output")
|
body += self.body_blob(result._result['stdout'], 'standard output')
|
||||||
if result._result.get("stderr"):
|
if result._result.get('stderr'):
|
||||||
body += self.body_blob(result._result["stderr"], "error output")
|
body += self.body_blob(result._result['stderr'], 'error output')
|
||||||
if result._result.get("exception"): # Unrelated exceptions are added to output :-/
|
if result._result.get('exception'): # Unrelated exceptions are added to output :-/
|
||||||
body += self.body_blob(result._result["exception"], "exception")
|
body += self.body_blob(result._result['exception'], 'exception')
|
||||||
if result._result.get("warnings"):
|
if result._result.get('warnings'):
|
||||||
for i in range(len(result._result.get("warnings"))):
|
for i in range(len(result._result.get('warnings'))):
|
||||||
body += self.body_blob(result._result["warnings"][i], f"exception {i + 1}")
|
body += self.body_blob(result._result['warnings'][i], f'exception {i + 1}')
|
||||||
if result._result.get("deprecations"):
|
if result._result.get('deprecations'):
|
||||||
for i in range(len(result._result.get("deprecations"))):
|
for i in range(len(result._result.get('deprecations'))):
|
||||||
body += self.body_blob(result._result["deprecations"][i], f"exception {i + 1}")
|
body += self.body_blob(result._result['deprecations'][i], f'exception {i + 1}')
|
||||||
|
|
||||||
body += "and a complete dump of the error:\n\n"
|
body += 'and a complete dump of the error:\n\n'
|
||||||
body += self.indent(f"{failtype}: {json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)}")
|
body += self.indent(f'{failtype}: {json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)}')
|
||||||
|
|
||||||
self.mail(subject=subject, body=body)
|
self.mail(subject=subject, body=body)
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.playbook = playbook
|
self.playbook = playbook
|
||||||
self.itembody = ""
|
self.itembody = ''
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
if ignore_errors:
|
if ignore_errors:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.mail_result(result, "Failed")
|
self.mail_result(result, 'Failed')
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
self.mail_result(result, "Unreachable")
|
self.mail_result(result, 'Unreachable')
|
||||||
|
|
||||||
def v2_runner_on_async_failed(self, result):
|
def v2_runner_on_async_failed(self, result):
|
||||||
self.mail_result(result, "Async failure")
|
self.mail_result(result, 'Async failure')
|
||||||
|
|
||||||
def v2_runner_item_on_failed(self, result):
|
def v2_runner_item_on_failed(self, result):
|
||||||
# Pass item information to task failure
|
# Pass item information to task failure
|
||||||
self.itemsubject = result._result["msg"]
|
self.itemsubject = result._result['msg']
|
||||||
self.itembody += self.body_blob(
|
self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), f"failed item dump '{result._result['item']}'")
|
||||||
json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), f"failed item dump '{result._result['item']}'"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018 Remi Verchere <remi@verchere.fr>
|
# Copyright (c) 2018 Remi Verchere <remi@verchere.fr>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -73,13 +74,13 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
"""
|
'''
|
||||||
send ansible-playbook to Nagios server using nrdp protocol
|
send ansible-playbook to Nagios server using nrdp protocol
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.nrdp"
|
CALLBACK_NAME = 'community.general.nrdp'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
# Nagios states
|
# Nagios states
|
||||||
@@ -89,35 +90,34 @@ class CallbackModule(CallbackBase):
|
|||||||
UNKNOWN = 3
|
UNKNOWN = 3
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
self.printed_playbook = False
|
self.printed_playbook = False
|
||||||
self.playbook_name = None
|
self.playbook_name = None
|
||||||
self.play = None
|
self.play = None
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.url = self.get_option("url")
|
self.url = self.get_option('url')
|
||||||
if not self.url.endswith("/"):
|
if not self.url.endswith('/'):
|
||||||
self.url += "/"
|
self.url += '/'
|
||||||
self.token = self.get_option("token")
|
self.token = self.get_option('token')
|
||||||
self.hostname = self.get_option("hostname")
|
self.hostname = self.get_option('hostname')
|
||||||
self.servicename = self.get_option("servicename")
|
self.servicename = self.get_option('servicename')
|
||||||
self.validate_nrdp_certs = self.get_option("validate_certs")
|
self.validate_nrdp_certs = self.get_option('validate_certs')
|
||||||
|
|
||||||
if (self.url or self.token or self.hostname or self.servicename) is None:
|
if (self.url or self.token or self.hostname or
|
||||||
self._display.warning(
|
self.servicename) is None:
|
||||||
"NRDP callback wants the NRDP_URL,"
|
self._display.warning("NRDP callback wants the NRDP_URL,"
|
||||||
" NRDP_TOKEN, NRDP_HOSTNAME,"
|
" NRDP_TOKEN, NRDP_HOSTNAME,"
|
||||||
" NRDP_SERVICENAME"
|
" NRDP_SERVICENAME"
|
||||||
" environment variables'."
|
" environment variables'."
|
||||||
" The NRDP callback plugin is disabled."
|
" The NRDP callback plugin is disabled.")
|
||||||
)
|
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
|
|
||||||
def _send_nrdp(self, state, msg):
|
def _send_nrdp(self, state, msg):
|
||||||
"""
|
'''
|
||||||
nrpd service check send XMLDATA like this:
|
nrpd service check send XMLDATA like this:
|
||||||
<?xml version='1.0'?>
|
<?xml version='1.0'?>
|
||||||
<checkresults>
|
<checkresults>
|
||||||
@@ -128,7 +128,7 @@ class CallbackModule(CallbackBase):
|
|||||||
<output>WARNING: Danger Will Robinson!|perfdata</output>
|
<output>WARNING: Danger Will Robinson!|perfdata</output>
|
||||||
</checkresult>
|
</checkresult>
|
||||||
</checkresults>
|
</checkresults>
|
||||||
"""
|
'''
|
||||||
xmldata = "<?xml version='1.0'?>\n"
|
xmldata = "<?xml version='1.0'?>\n"
|
||||||
xmldata += "<checkresults>\n"
|
xmldata += "<checkresults>\n"
|
||||||
xmldata += "<checkresult type='service'>\n"
|
xmldata += "<checkresult type='service'>\n"
|
||||||
@@ -139,24 +139,31 @@ class CallbackModule(CallbackBase):
|
|||||||
xmldata += "</checkresult>\n"
|
xmldata += "</checkresult>\n"
|
||||||
xmldata += "</checkresults>\n"
|
xmldata += "</checkresults>\n"
|
||||||
|
|
||||||
body = {"cmd": "submitcheck", "token": self.token, "XMLDATA": to_bytes(xmldata)}
|
body = {
|
||||||
|
'cmd': 'submitcheck',
|
||||||
|
'token': self.token,
|
||||||
|
'XMLDATA': to_bytes(xmldata)
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = open_url(self.url, data=urlencode(body), method="POST", validate_certs=self.validate_nrdp_certs)
|
response = open_url(self.url,
|
||||||
|
data=urlencode(body),
|
||||||
|
method='POST',
|
||||||
|
validate_certs=self.validate_nrdp_certs)
|
||||||
return response.read()
|
return response.read()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self._display.warning(f"NRDP callback cannot send result {ex}")
|
self._display.warning(f"NRDP callback cannot send result {ex}")
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
"""
|
'''
|
||||||
Display Playbook and play start messages
|
Display Playbook and play start messages
|
||||||
"""
|
'''
|
||||||
self.play = play
|
self.play = play
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats):
|
def v2_playbook_on_stats(self, stats):
|
||||||
"""
|
'''
|
||||||
Display info about playbook statistics
|
Display info about playbook statistics
|
||||||
"""
|
'''
|
||||||
name = self.play
|
name = self.play
|
||||||
gstats = ""
|
gstats = ""
|
||||||
hosts = sorted(stats.processed.keys())
|
hosts = sorted(stats.processed.keys())
|
||||||
@@ -164,14 +171,13 @@ class CallbackModule(CallbackBase):
|
|||||||
for host in hosts:
|
for host in hosts:
|
||||||
stat = stats.summarize(host)
|
stat = stats.summarize(host)
|
||||||
gstats += (
|
gstats += (
|
||||||
f"'{host}_ok'={stat['ok']} '{host}_changed'={stat['changed']}"
|
f"'{host}_ok'={stat['ok']} '{host}_changed'={stat['changed']} '{host}_unreachable'={stat['unreachable']} '{host}_failed'={stat['failures']} "
|
||||||
f" '{host}_unreachable'={stat['unreachable']} '{host}_failed'={stat['failures']} "
|
|
||||||
)
|
)
|
||||||
# Critical when failed tasks or unreachable host
|
# Critical when failed tasks or unreachable host
|
||||||
critical += stat["failures"]
|
critical += stat['failures']
|
||||||
critical += stat["unreachable"]
|
critical += stat['unreachable']
|
||||||
# Warning when changed tasks
|
# Warning when changed tasks
|
||||||
warning += stat["changed"]
|
warning += stat['changed']
|
||||||
|
|
||||||
msg = f"{name} | {gstats}"
|
msg = f"{name} | {gstats}"
|
||||||
if critical:
|
if critical:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -20,10 +21,11 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
"""
|
|
||||||
|
'''
|
||||||
This callback won't print messages to stdout when new callback events are received.
|
This callback won't print messages to stdout when new callback events are received.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.null"
|
CALLBACK_NAME = 'community.general.null'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2021, Victor Martinez <VictorMartinezRubio@gmail.com>
|
# Copyright (c) 2021, Victor Martinez <VictorMartinezRubio@gmail.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -145,7 +146,6 @@ from ansible.errors import AnsibleError
|
|||||||
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
from ansible.module_utils.ansible_release import __version__ as ansible_version
|
||||||
from ansible.plugins.callback import CallbackBase
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
|
||||||
OTEL_LIBRARY_IMPORT_ERROR: ImportError | None
|
|
||||||
try:
|
try:
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.trace import SpanKind
|
from opentelemetry.trace import SpanKind
|
||||||
@@ -155,8 +155,13 @@ try:
|
|||||||
from opentelemetry.trace.status import Status, StatusCode
|
from opentelemetry.trace.status import Status, StatusCode
|
||||||
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
||||||
from opentelemetry.sdk.trace import TracerProvider
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
|
from opentelemetry.sdk.trace.export import (
|
||||||
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
BatchSpanProcessor,
|
||||||
|
SimpleSpanProcessor
|
||||||
|
)
|
||||||
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
||||||
|
InMemorySpanExporter
|
||||||
|
)
|
||||||
except ImportError as imp_exc:
|
except ImportError as imp_exc:
|
||||||
OTEL_LIBRARY_IMPORT_ERROR = imp_exc
|
OTEL_LIBRARY_IMPORT_ERROR = imp_exc
|
||||||
else:
|
else:
|
||||||
@@ -181,9 +186,9 @@ class TaskData:
|
|||||||
|
|
||||||
def add_host(self, host):
|
def add_host(self, host):
|
||||||
if host.uuid in self.host_data:
|
if host.uuid in self.host_data:
|
||||||
if host.status == "included":
|
if host.status == 'included':
|
||||||
# concatenate task include output from multiple items
|
# concatenate task include output from multiple items
|
||||||
host.result = f"{self.host_data[host.uuid].result}\n{host.result}"
|
host.result = f'{self.host_data[host.uuid].result}\n{host.result}'
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -203,14 +208,14 @@ class HostData:
|
|||||||
self.finish = time_ns()
|
self.finish = time_ns()
|
||||||
|
|
||||||
|
|
||||||
class OpenTelemetrySource:
|
class OpenTelemetrySource(object):
|
||||||
def __init__(self, display):
|
def __init__(self, display):
|
||||||
self.ansible_playbook = ""
|
self.ansible_playbook = ""
|
||||||
self.session = str(uuid.uuid4())
|
self.session = str(uuid.uuid4())
|
||||||
self.host = socket.gethostname()
|
self.host = socket.gethostname()
|
||||||
try:
|
try:
|
||||||
self.ip_address = socket.gethostbyname(socket.gethostname())
|
self.ip_address = socket.gethostbyname(socket.gethostname())
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.ip_address = None
|
self.ip_address = None
|
||||||
self.user = getpass.getuser()
|
self.user = getpass.getuser()
|
||||||
|
|
||||||
@@ -218,11 +223,11 @@ class OpenTelemetrySource:
|
|||||||
|
|
||||||
def traceparent_context(self, traceparent):
|
def traceparent_context(self, traceparent):
|
||||||
carrier = dict()
|
carrier = dict()
|
||||||
carrier["traceparent"] = traceparent
|
carrier['traceparent'] = traceparent
|
||||||
return TraceContextTextMapPropagator().extract(carrier=carrier)
|
return TraceContextTextMapPropagator().extract(carrier=carrier)
|
||||||
|
|
||||||
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
|
def start_task(self, tasks_data, hide_task_arguments, play_name, task):
|
||||||
"""record the start of a task for one or more hosts"""
|
""" record the start of a task for one or more hosts """
|
||||||
|
|
||||||
uuid = task._uuid
|
uuid = task._uuid
|
||||||
|
|
||||||
@@ -240,51 +245,53 @@ class OpenTelemetrySource:
|
|||||||
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
|
tasks_data[uuid] = TaskData(uuid, name, path, play_name, action, args)
|
||||||
|
|
||||||
def finish_task(self, tasks_data, status, result, dump):
|
def finish_task(self, tasks_data, status, result, dump):
|
||||||
"""record the results of a task for a single host"""
|
""" record the results of a task for a single host """
|
||||||
|
|
||||||
task_uuid = result._task._uuid
|
task_uuid = result._task._uuid
|
||||||
|
|
||||||
if hasattr(result, "_host") and result._host is not None:
|
if hasattr(result, '_host') and result._host is not None:
|
||||||
host_uuid = result._host._uuid
|
host_uuid = result._host._uuid
|
||||||
host_name = result._host.name
|
host_name = result._host.name
|
||||||
else:
|
else:
|
||||||
host_uuid = "include"
|
host_uuid = 'include'
|
||||||
host_name = "include"
|
host_name = 'include'
|
||||||
|
|
||||||
task = tasks_data[task_uuid]
|
task = tasks_data[task_uuid]
|
||||||
|
|
||||||
task.dump = dump
|
task.dump = dump
|
||||||
task.add_host(HostData(host_uuid, host_name, status, result))
|
task.add_host(HostData(host_uuid, host_name, status, result))
|
||||||
|
|
||||||
def generate_distributed_traces(
|
def generate_distributed_traces(self,
|
||||||
self,
|
otel_service_name,
|
||||||
otel_service_name,
|
ansible_playbook,
|
||||||
ansible_playbook,
|
tasks_data,
|
||||||
tasks_data,
|
status,
|
||||||
status,
|
traceparent,
|
||||||
traceparent,
|
disable_logs,
|
||||||
disable_logs,
|
disable_attributes_in_logs,
|
||||||
disable_attributes_in_logs,
|
otel_exporter_otlp_traces_protocol,
|
||||||
otel_exporter_otlp_traces_protocol,
|
store_spans_in_file):
|
||||||
store_spans_in_file,
|
""" generate distributed traces from the collected TaskData and HostData """
|
||||||
):
|
|
||||||
"""generate distributed traces from the collected TaskData and HostData"""
|
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
parent_start_time = None
|
parent_start_time = None
|
||||||
for task in tasks_data.values():
|
for task_uuid, task in tasks_data.items():
|
||||||
if parent_start_time is None:
|
if parent_start_time is None:
|
||||||
parent_start_time = task.start
|
parent_start_time = task.start
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
trace.set_tracer_provider(TracerProvider(resource=Resource.create({SERVICE_NAME: otel_service_name})))
|
trace.set_tracer_provider(
|
||||||
|
TracerProvider(
|
||||||
|
resource=Resource.create({SERVICE_NAME: otel_service_name})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
otel_exporter = None
|
otel_exporter = None
|
||||||
if store_spans_in_file:
|
if store_spans_in_file:
|
||||||
otel_exporter = InMemorySpanExporter()
|
otel_exporter = InMemorySpanExporter()
|
||||||
processor = SimpleSpanProcessor(otel_exporter)
|
processor = SimpleSpanProcessor(otel_exporter)
|
||||||
else:
|
else:
|
||||||
if otel_exporter_otlp_traces_protocol == "grpc":
|
if otel_exporter_otlp_traces_protocol == 'grpc':
|
||||||
otel_exporter = GRPCOTLPSpanExporter()
|
otel_exporter = GRPCOTLPSpanExporter()
|
||||||
else:
|
else:
|
||||||
otel_exporter = HTTPOTLPSpanExporter()
|
otel_exporter = HTTPOTLPSpanExporter()
|
||||||
@@ -294,12 +301,8 @@ class OpenTelemetrySource:
|
|||||||
|
|
||||||
tracer = trace.get_tracer(__name__)
|
tracer = trace.get_tracer(__name__)
|
||||||
|
|
||||||
with tracer.start_as_current_span(
|
with tracer.start_as_current_span(ansible_playbook, context=self.traceparent_context(traceparent),
|
||||||
ansible_playbook,
|
start_time=parent_start_time, kind=SpanKind.SERVER) as parent:
|
||||||
context=self.traceparent_context(traceparent),
|
|
||||||
start_time=parent_start_time,
|
|
||||||
kind=SpanKind.SERVER,
|
|
||||||
) as parent:
|
|
||||||
parent.set_status(status)
|
parent.set_status(status)
|
||||||
# Populate trace metadata attributes
|
# Populate trace metadata attributes
|
||||||
parent.set_attribute("ansible.version", ansible_version)
|
parent.set_attribute("ansible.version", ansible_version)
|
||||||
@@ -309,45 +312,43 @@ class OpenTelemetrySource:
|
|||||||
parent.set_attribute("ansible.host.ip", self.ip_address)
|
parent.set_attribute("ansible.host.ip", self.ip_address)
|
||||||
parent.set_attribute("ansible.host.user", self.user)
|
parent.set_attribute("ansible.host.user", self.user)
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
for host_data in task.host_data.values():
|
for host_uuid, host_data in task.host_data.items():
|
||||||
with tracer.start_as_current_span(task.name, start_time=task.start, end_on_exit=False) as span:
|
with tracer.start_as_current_span(task.name, start_time=task.start, end_on_exit=False) as span:
|
||||||
self.update_span_data(task, host_data, span, disable_logs, disable_attributes_in_logs)
|
self.update_span_data(task, host_data, span, disable_logs, disable_attributes_in_logs)
|
||||||
|
|
||||||
return otel_exporter
|
return otel_exporter
|
||||||
|
|
||||||
def update_span_data(self, task_data, host_data, span, disable_logs, disable_attributes_in_logs):
|
def update_span_data(self, task_data, host_data, span, disable_logs, disable_attributes_in_logs):
|
||||||
"""update the span with the given TaskData and HostData"""
|
""" update the span with the given TaskData and HostData """
|
||||||
|
|
||||||
name = f"[{host_data.name}] {task_data.play}: {task_data.name}"
|
name = f'[{host_data.name}] {task_data.play}: {task_data.name}'
|
||||||
|
|
||||||
message = "success"
|
message = 'success'
|
||||||
res = {}
|
res = {}
|
||||||
rc = 0
|
rc = 0
|
||||||
status = Status(status_code=StatusCode.OK)
|
status = Status(status_code=StatusCode.OK)
|
||||||
if host_data.status != "included":
|
if host_data.status != 'included':
|
||||||
# Support loops
|
# Support loops
|
||||||
enriched_error_message = None
|
enriched_error_message = None
|
||||||
if "results" in host_data.result._result:
|
if 'results' in host_data.result._result:
|
||||||
if host_data.status == "failed":
|
if host_data.status == 'failed':
|
||||||
message = self.get_error_message_from_results(host_data.result._result["results"], task_data.action)
|
message = self.get_error_message_from_results(host_data.result._result['results'], task_data.action)
|
||||||
enriched_error_message = self.enrich_error_message_from_results(
|
enriched_error_message = self.enrich_error_message_from_results(host_data.result._result['results'], task_data.action)
|
||||||
host_data.result._result["results"], task_data.action
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
res = host_data.result._result
|
res = host_data.result._result
|
||||||
rc = res.get("rc", 0)
|
rc = res.get('rc', 0)
|
||||||
if host_data.status == "failed":
|
if host_data.status == 'failed':
|
||||||
message = self.get_error_message(res)
|
message = self.get_error_message(res)
|
||||||
enriched_error_message = self.enrich_error_message(res)
|
enriched_error_message = self.enrich_error_message(res)
|
||||||
|
|
||||||
if host_data.status == "failed":
|
if host_data.status == 'failed':
|
||||||
status = Status(status_code=StatusCode.ERROR, description=message)
|
status = Status(status_code=StatusCode.ERROR, description=message)
|
||||||
# Record an exception with the task message
|
# Record an exception with the task message
|
||||||
span.record_exception(BaseException(enriched_error_message))
|
span.record_exception(BaseException(enriched_error_message))
|
||||||
elif host_data.status == "skipped":
|
elif host_data.status == 'skipped':
|
||||||
message = res["skip_reason"] if "skip_reason" in res else "skipped"
|
message = res['skip_reason'] if 'skip_reason' in res else 'skipped'
|
||||||
status = Status(status_code=StatusCode.UNSET)
|
status = Status(status_code=StatusCode.UNSET)
|
||||||
elif host_data.status == "ignored":
|
elif host_data.status == 'ignored':
|
||||||
status = Status(status_code=StatusCode.UNSET)
|
status = Status(status_code=StatusCode.UNSET)
|
||||||
|
|
||||||
span.set_status(status)
|
span.set_status(status)
|
||||||
@@ -359,7 +360,7 @@ class OpenTelemetrySource:
|
|||||||
"ansible.task.name": name,
|
"ansible.task.name": name,
|
||||||
"ansible.task.result": rc,
|
"ansible.task.result": rc,
|
||||||
"ansible.task.host.name": host_data.name,
|
"ansible.task.host.name": host_data.name,
|
||||||
"ansible.task.host.status": host_data.status,
|
"ansible.task.host.status": host_data.status
|
||||||
}
|
}
|
||||||
if isinstance(task_data.args, dict) and "gather_facts" not in task_data.action:
|
if isinstance(task_data.args, dict) and "gather_facts" not in task_data.action:
|
||||||
names = tuple(self.transform_ansible_unicode_to_str(k) for k in task_data.args.keys())
|
names = tuple(self.transform_ansible_unicode_to_str(k) for k in task_data.args.keys())
|
||||||
@@ -379,10 +380,10 @@ class OpenTelemetrySource:
|
|||||||
span.end(end_time=host_data.finish)
|
span.end(end_time=host_data.finish)
|
||||||
|
|
||||||
def set_span_attributes(self, span, attributes):
|
def set_span_attributes(self, span, attributes):
|
||||||
"""update the span attributes with the given attributes if not None"""
|
""" update the span attributes with the given attributes if not None """
|
||||||
|
|
||||||
if span is None and self._display is not None:
|
if span is None and self._display is not None:
|
||||||
self._display.warning("span object is None. Please double check if that is expected.")
|
self._display.warning('span object is None. Please double check if that is expected.')
|
||||||
else:
|
else:
|
||||||
if attributes is not None:
|
if attributes is not None:
|
||||||
span.set_attributes(attributes)
|
span.set_attributes(attributes)
|
||||||
@@ -410,18 +411,7 @@ class OpenTelemetrySource:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def url_from_args(args):
|
def url_from_args(args):
|
||||||
# the order matters
|
# the order matters
|
||||||
url_args = (
|
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url", "registry_url", "endpoint", "uri", "updates_url")
|
||||||
"url",
|
|
||||||
"api_url",
|
|
||||||
"baseurl",
|
|
||||||
"repo",
|
|
||||||
"server_url",
|
|
||||||
"chart_repo_url",
|
|
||||||
"registry_url",
|
|
||||||
"endpoint",
|
|
||||||
"uri",
|
|
||||||
"updates_url",
|
|
||||||
)
|
|
||||||
for arg in url_args:
|
for arg in url_args:
|
||||||
if args is not None and args.get(arg):
|
if args is not None and args.get(arg):
|
||||||
return args.get(arg)
|
return args.get(arg)
|
||||||
@@ -446,33 +436,33 @@ class OpenTelemetrySource:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_error_message(result):
|
def get_error_message(result):
|
||||||
if result.get("exception") is not None:
|
if result.get('exception') is not None:
|
||||||
return OpenTelemetrySource._last_line(result["exception"])
|
return OpenTelemetrySource._last_line(result['exception'])
|
||||||
return result.get("msg", "failed")
|
return result.get('msg', 'failed')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_error_message_from_results(results, action):
|
def get_error_message_from_results(results, action):
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.get("failed", False):
|
if result.get('failed', False):
|
||||||
return f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.get_error_message(result)}"
|
return f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.get_error_message(result)}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _last_line(text):
|
def _last_line(text):
|
||||||
lines = text.strip().split("\n")
|
lines = text.strip().split('\n')
|
||||||
return lines[-1]
|
return lines[-1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enrich_error_message(result):
|
def enrich_error_message(result):
|
||||||
message = result.get("msg", "failed")
|
message = result.get('msg', 'failed')
|
||||||
exception = result.get("exception")
|
exception = result.get('exception')
|
||||||
stderr = result.get("stderr")
|
stderr = result.get('stderr')
|
||||||
return f'message: "{message}"\nexception: "{exception}"\nstderr: "{stderr}"'
|
return f"message: \"{message}\"\nexception: \"{exception}\"\nstderr: \"{stderr}\""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enrich_error_message_from_results(results, action):
|
def enrich_error_message_from_results(results, action):
|
||||||
message = ""
|
message = ""
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.get("failed", False):
|
if result.get('failed', False):
|
||||||
message = f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.enrich_error_message(result)}\n{message}"
|
message = f"{action}({result.get('item', 'none')}) - {OpenTelemetrySource.enrich_error_message(result)}\n{message}"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@@ -483,12 +473,12 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.opentelemetry"
|
CALLBACK_NAME = 'community.general.opentelemetry'
|
||||||
CALLBACK_NEEDS_ENABLED = True
|
CALLBACK_NEEDS_ENABLED = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.hide_task_arguments = None
|
self.hide_task_arguments = None
|
||||||
self.disable_attributes_in_logs = None
|
self.disable_attributes_in_logs = None
|
||||||
self.disable_logs = None
|
self.disable_logs = None
|
||||||
@@ -504,7 +494,7 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
if OTEL_LIBRARY_IMPORT_ERROR:
|
if OTEL_LIBRARY_IMPORT_ERROR:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
"The `opentelemetry-api`, `opentelemetry-exporter-otlp` or `opentelemetry-sdk` must be installed to use this plugin"
|
'The `opentelemetry-api`, `opentelemetry-exporter-otlp` or `opentelemetry-sdk` must be installed to use this plugin'
|
||||||
) from OTEL_LIBRARY_IMPORT_ERROR
|
) from OTEL_LIBRARY_IMPORT_ERROR
|
||||||
|
|
||||||
self.tasks_data = OrderedDict()
|
self.tasks_data = OrderedDict()
|
||||||
@@ -512,35 +502,37 @@ class CallbackModule(CallbackBase):
|
|||||||
self.opentelemetry = OpenTelemetrySource(display=self._display)
|
self.opentelemetry = OpenTelemetrySource(display=self._display)
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys,
|
||||||
|
var_options=var_options,
|
||||||
|
direct=direct)
|
||||||
|
|
||||||
environment_variable = self.get_option("enable_from_environment")
|
environment_variable = self.get_option('enable_from_environment')
|
||||||
if environment_variable is not None and os.environ.get(environment_variable, "false").lower() != "true":
|
if environment_variable is not None and os.environ.get(environment_variable, 'false').lower() != 'true':
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning(
|
||||||
f"The `enable_from_environment` option has been set and {environment_variable} is not enabled. Disabling the `opentelemetry` callback plugin."
|
f"The `enable_from_environment` option has been set and {environment_variable} is not enabled. Disabling the `opentelemetry` callback plugin."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.hide_task_arguments = self.get_option("hide_task_arguments")
|
self.hide_task_arguments = self.get_option('hide_task_arguments')
|
||||||
|
|
||||||
self.disable_attributes_in_logs = self.get_option("disable_attributes_in_logs")
|
self.disable_attributes_in_logs = self.get_option('disable_attributes_in_logs')
|
||||||
|
|
||||||
self.disable_logs = self.get_option("disable_logs")
|
self.disable_logs = self.get_option('disable_logs')
|
||||||
|
|
||||||
self.store_spans_in_file = self.get_option("store_spans_in_file")
|
self.store_spans_in_file = self.get_option('store_spans_in_file')
|
||||||
|
|
||||||
self.otel_service_name = self.get_option("otel_service_name")
|
self.otel_service_name = self.get_option('otel_service_name')
|
||||||
|
|
||||||
if not self.otel_service_name:
|
if not self.otel_service_name:
|
||||||
self.otel_service_name = "ansible"
|
self.otel_service_name = 'ansible'
|
||||||
|
|
||||||
# See https://github.com/open-telemetry/opentelemetry-specification/issues/740
|
# See https://github.com/open-telemetry/opentelemetry-specification/issues/740
|
||||||
self.traceparent = self.get_option("traceparent")
|
self.traceparent = self.get_option('traceparent')
|
||||||
|
|
||||||
self.otel_exporter_otlp_traces_protocol = self.get_option("otel_exporter_otlp_traces_protocol")
|
self.otel_exporter_otlp_traces_protocol = self.get_option('otel_exporter_otlp_traces_protocol')
|
||||||
|
|
||||||
def dump_results(self, task, result):
|
def dump_results(self, task, result):
|
||||||
"""dump the results if disable_logs is not enabled"""
|
""" dump the results if disable_logs is not enabled """
|
||||||
if self.disable_logs:
|
if self.disable_logs:
|
||||||
return ""
|
return ""
|
||||||
# ansible.builtin.uri contains the response in the json field
|
# ansible.builtin.uri contains the response in the json field
|
||||||
@@ -560,40 +552,74 @@ class CallbackModule(CallbackBase):
|
|||||||
self.play_name = play.get_name()
|
self.play_name = play.get_name()
|
||||||
|
|
||||||
def v2_runner_on_no_hosts(self, task):
|
def v2_runner_on_no_hosts(self, task):
|
||||||
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.opentelemetry.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.opentelemetry.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_cleanup_task_start(self, task):
|
def v2_playbook_on_cleanup_task_start(self, task):
|
||||||
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.opentelemetry.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_handler_task_start(self, task):
|
def v2_playbook_on_handler_task_start(self, task):
|
||||||
self.opentelemetry.start_task(self.tasks_data, self.hide_task_arguments, self.play_name, task)
|
self.opentelemetry.start_task(
|
||||||
|
self.tasks_data,
|
||||||
|
self.hide_task_arguments,
|
||||||
|
self.play_name,
|
||||||
|
task
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
if ignore_errors:
|
if ignore_errors:
|
||||||
status = "ignored"
|
status = 'ignored'
|
||||||
else:
|
else:
|
||||||
status = "failed"
|
status = 'failed'
|
||||||
self.errors += 1
|
self.errors += 1
|
||||||
|
|
||||||
self.opentelemetry.finish_task(
|
self.opentelemetry.finish_task(
|
||||||
self.tasks_data, status, result, self.dump_results(self.tasks_data[result._task._uuid], result)
|
self.tasks_data,
|
||||||
|
status,
|
||||||
|
result,
|
||||||
|
self.dump_results(self.tasks_data[result._task._uuid], result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
self.opentelemetry.finish_task(
|
self.opentelemetry.finish_task(
|
||||||
self.tasks_data, "ok", result, self.dump_results(self.tasks_data[result._task._uuid], result)
|
self.tasks_data,
|
||||||
|
'ok',
|
||||||
|
result,
|
||||||
|
self.dump_results(self.tasks_data[result._task._uuid], result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
self.opentelemetry.finish_task(
|
self.opentelemetry.finish_task(
|
||||||
self.tasks_data, "skipped", result, self.dump_results(self.tasks_data[result._task._uuid], result)
|
self.tasks_data,
|
||||||
|
'skipped',
|
||||||
|
result,
|
||||||
|
self.dump_results(self.tasks_data[result._task._uuid], result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_playbook_on_include(self, included_file):
|
def v2_playbook_on_include(self, included_file):
|
||||||
self.opentelemetry.finish_task(self.tasks_data, "included", included_file, "")
|
self.opentelemetry.finish_task(
|
||||||
|
self.tasks_data,
|
||||||
|
'included',
|
||||||
|
included_file,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats):
|
def v2_playbook_on_stats(self, stats):
|
||||||
if self.errors == 0:
|
if self.errors == 0:
|
||||||
@@ -609,7 +635,7 @@ class CallbackModule(CallbackBase):
|
|||||||
self.disable_logs,
|
self.disable_logs,
|
||||||
self.disable_attributes_in_logs,
|
self.disable_attributes_in_logs,
|
||||||
self.otel_exporter_otlp_traces_protocol,
|
self.otel_exporter_otlp_traces_protocol,
|
||||||
self.store_spans_in_file,
|
self.store_spans_in_file
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.store_spans_in_file:
|
if self.store_spans_in_file:
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2025, Max Mitschke <maxmitschke@fastmail.com>
|
# Copyright (c) 2025, Max Mitschke <maxmitschke@fastmail.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
DOCUMENTATION = r"""
|
DOCUMENTATION = r"""
|
||||||
name: print_task
|
name: print_task
|
||||||
@@ -28,7 +30,7 @@ try:
|
|||||||
from yaml import CSafeDumper as SafeDumper
|
from yaml import CSafeDumper as SafeDumper
|
||||||
from yaml import CSafeLoader as SafeLoader
|
from yaml import CSafeLoader as SafeLoader
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from yaml import SafeDumper, SafeLoader # type: ignore
|
from yaml import SafeDumper, SafeLoader
|
||||||
|
|
||||||
from ansible.plugins.callback import CallbackBase
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
|
||||||
@@ -37,19 +39,18 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
This callback module tells you how long your plays ran for.
|
This callback module tells you how long your plays ran for.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "aggregate"
|
CALLBACK_TYPE = 'aggregate'
|
||||||
CALLBACK_NAME = "community.general.print_task"
|
CALLBACK_NAME = 'community.general.print_task'
|
||||||
|
|
||||||
CALLBACK_NEEDS_ENABLED = True
|
CALLBACK_NEEDS_ENABLED = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
self._printed_message = False
|
self._printed_message = False
|
||||||
|
|
||||||
def _print_task(self, task):
|
def _print_task(self, task):
|
||||||
if hasattr(task, "_ds"):
|
if hasattr(task, '_ds'):
|
||||||
task_snippet = load(str([task._ds.copy()]), Loader=SafeLoader)
|
task_snippet = load(str([task._ds.copy()]), Loader=SafeLoader)
|
||||||
task_yaml = dump(task_snippet, sort_keys=False, Dumper=SafeDumper)
|
task_yaml = dump(task_snippet, sort_keys=False, Dumper=SafeDumper)
|
||||||
self._display.display(f"\n{task_yaml}\n")
|
self._display.display(f"\n{task_yaml}\n")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
# Copyright (c) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -30,14 +31,14 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
makes Ansible much more exciting.
|
makes Ansible much more exciting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.say"
|
CALLBACK_NAME = 'community.general.say'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
self.FAILED_VOICE = None
|
self.FAILED_VOICE = None
|
||||||
self.REGULAR_VOICE = None
|
self.REGULAR_VOICE = None
|
||||||
@@ -45,23 +46,21 @@ class CallbackModule(CallbackBase):
|
|||||||
self.LASER_VOICE = None
|
self.LASER_VOICE = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.synthesizer = get_bin_path("say")
|
self.synthesizer = get_bin_path('say')
|
||||||
if platform.system() != "Darwin":
|
if platform.system() != 'Darwin':
|
||||||
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
|
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
|
||||||
self._display.warning(
|
self._display.warning(f"'say' executable found but system is '{platform.system()}': ignoring voice parameter")
|
||||||
f"'say' executable found but system is '{platform.system()}': ignoring voice parameter"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.FAILED_VOICE = "Zarvox"
|
self.FAILED_VOICE = 'Zarvox'
|
||||||
self.REGULAR_VOICE = "Trinoids"
|
self.REGULAR_VOICE = 'Trinoids'
|
||||||
self.HAPPY_VOICE = "Cellos"
|
self.HAPPY_VOICE = 'Cellos'
|
||||||
self.LASER_VOICE = "Princess"
|
self.LASER_VOICE = 'Princess'
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
self.synthesizer = get_bin_path("espeak")
|
self.synthesizer = get_bin_path('espeak')
|
||||||
self.FAILED_VOICE = "klatt"
|
self.FAILED_VOICE = 'klatt'
|
||||||
self.HAPPY_VOICE = "f5"
|
self.HAPPY_VOICE = 'f5'
|
||||||
self.LASER_VOICE = "whisper"
|
self.LASER_VOICE = 'whisper'
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.synthesizer = None
|
self.synthesizer = None
|
||||||
|
|
||||||
@@ -69,14 +68,12 @@ class CallbackModule(CallbackBase):
|
|||||||
# ansible will not call any callback if disabled is set to True
|
# ansible will not call any callback if disabled is set to True
|
||||||
if not self.synthesizer:
|
if not self.synthesizer:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning(f"Unable to find either 'say' or 'espeak' executable, plugin {os.path.basename(__file__)} disabled")
|
||||||
f"Unable to find either 'say' or 'espeak' executable, plugin {os.path.basename(__file__)} disabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
def say(self, msg, voice):
|
def say(self, msg, voice):
|
||||||
cmd = [self.synthesizer, msg]
|
cmd = [self.synthesizer, msg]
|
||||||
if voice:
|
if voice:
|
||||||
cmd.extend(("-v", voice))
|
cmd.extend(('-v', voice))
|
||||||
subprocess.call(cmd)
|
subprocess.call(cmd)
|
||||||
|
|
||||||
def runner_on_failed(self, host, res, ignore_errors=False):
|
def runner_on_failed(self, host, res, ignore_errors=False):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) Fastly, inc 2016
|
# Copyright (c) Fastly, inc 2016
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -45,14 +46,14 @@ from ansible.module_utils.common.text.converters import to_text
|
|||||||
|
|
||||||
DONT_COLORIZE = False
|
DONT_COLORIZE = False
|
||||||
COLORS = {
|
COLORS = {
|
||||||
"normal": "\033[0m",
|
'normal': '\033[0m',
|
||||||
"ok": f"\x1b[{C.COLOR_CODES[C.COLOR_OK]}m", # type: ignore
|
'ok': f'\x1b[{C.COLOR_CODES[C.COLOR_OK]}m',
|
||||||
"bold": "\033[1m",
|
'bold': '\033[1m',
|
||||||
"not_so_bold": "\033[1m\033[34m",
|
'not_so_bold': '\033[1m\033[34m',
|
||||||
"changed": f"\x1b[{C.COLOR_CODES[C.COLOR_CHANGED]}m", # type: ignore
|
'changed': f'\x1b[{C.COLOR_CODES[C.COLOR_CHANGED]}m',
|
||||||
"failed": f"\x1b[{C.COLOR_CODES[C.COLOR_ERROR]}m", # type: ignore
|
'failed': f'\x1b[{C.COLOR_CODES[C.COLOR_ERROR]}m',
|
||||||
"endc": "\033[0m",
|
'endc': '\033[0m',
|
||||||
"skipped": f"\x1b[{C.COLOR_CODES[C.COLOR_SKIP]}m", # type: ignore
|
'skipped': f'\x1b[{C.COLOR_CODES[C.COLOR_SKIP]}m',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,21 +79,22 @@ class CallbackModule(CallbackBase):
|
|||||||
"""selective.py callback plugin."""
|
"""selective.py callback plugin."""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.selective"
|
CALLBACK_NAME = 'community.general.selective'
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
"""selective.py callback plugin."""
|
"""selective.py callback plugin."""
|
||||||
super().__init__(display)
|
super(CallbackModule, self).__init__(display)
|
||||||
self.last_skipped = False
|
self.last_skipped = False
|
||||||
self.last_task_name = None
|
self.last_task_name = None
|
||||||
self.printed_last_task = False
|
self.printed_last_task = False
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
|
||||||
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
global DONT_COLORIZE
|
global DONT_COLORIZE
|
||||||
DONT_COLORIZE = self.get_option("nocolor")
|
DONT_COLORIZE = self.get_option('nocolor')
|
||||||
|
|
||||||
def _print_task(self, task_name=None):
|
def _print_task(self, task_name=None):
|
||||||
if task_name is None:
|
if task_name is None:
|
||||||
@@ -104,7 +106,7 @@ class CallbackModule(CallbackBase):
|
|||||||
if self.last_skipped:
|
if self.last_skipped:
|
||||||
print()
|
print()
|
||||||
line = f"# {task_name} "
|
line = f"# {task_name} "
|
||||||
msg = colorize(f"{line}{'*' * (line_length - len(line))}", "bold")
|
msg = colorize(f"{line}{'*' * (line_length - len(line))}", 'bold')
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
def _indent_text(self, text, indent_level):
|
def _indent_text(self, text, indent_level):
|
||||||
@@ -112,51 +114,48 @@ class CallbackModule(CallbackBase):
|
|||||||
result_lines = []
|
result_lines = []
|
||||||
for l in lines:
|
for l in lines:
|
||||||
result_lines.append(f"{' ' * indent_level}{l}")
|
result_lines.append(f"{' ' * indent_level}{l}")
|
||||||
return "\n".join(result_lines)
|
return '\n'.join(result_lines)
|
||||||
|
|
||||||
def _print_diff(self, diff, indent_level):
|
def _print_diff(self, diff, indent_level):
|
||||||
if isinstance(diff, dict):
|
if isinstance(diff, dict):
|
||||||
try:
|
try:
|
||||||
diff = "\n".join(
|
diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(),
|
||||||
difflib.unified_diff(
|
diff['after'].splitlines(),
|
||||||
diff["before"].splitlines(),
|
fromfile=diff.get('before_header',
|
||||||
diff["after"].splitlines(),
|
'new_file'),
|
||||||
fromfile=diff.get("before_header", "new_file"),
|
tofile=diff['after_header']))
|
||||||
tofile=diff["after_header"],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
diff = dict_diff(diff["before"], diff["after"])
|
diff = dict_diff(diff['before'], diff['after'])
|
||||||
if diff:
|
if diff:
|
||||||
diff = colorize(str(diff), "changed")
|
diff = colorize(str(diff), 'changed')
|
||||||
print(self._indent_text(diff, indent_level + 4))
|
print(self._indent_text(diff, indent_level + 4))
|
||||||
|
|
||||||
def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr):
|
def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr):
|
||||||
if is_host:
|
if is_host:
|
||||||
indent_level = 0
|
indent_level = 0
|
||||||
name = colorize(host_or_item.name, "not_so_bold")
|
name = colorize(host_or_item.name, 'not_so_bold')
|
||||||
else:
|
else:
|
||||||
indent_level = 4
|
indent_level = 4
|
||||||
if isinstance(host_or_item, dict):
|
if isinstance(host_or_item, dict):
|
||||||
if "key" in host_or_item.keys():
|
if 'key' in host_or_item.keys():
|
||||||
host_or_item = host_or_item["key"]
|
host_or_item = host_or_item['key']
|
||||||
name = colorize(to_text(host_or_item), "bold")
|
name = colorize(to_text(host_or_item), 'bold')
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
color = "failed"
|
color = 'failed'
|
||||||
change_string = colorize("FAILED!!!", color)
|
change_string = colorize('FAILED!!!', color)
|
||||||
else:
|
else:
|
||||||
color = "changed" if changed else "ok"
|
color = 'changed' if changed else 'ok'
|
||||||
change_string = colorize(f"changed={changed}", color)
|
change_string = colorize(f"changed={changed}", color)
|
||||||
|
|
||||||
msg = colorize(msg, color)
|
msg = colorize(msg, color)
|
||||||
|
|
||||||
line_length = 120
|
line_length = 120
|
||||||
spaces = " " * (40 - len(name) - indent_level)
|
spaces = ' ' * (40 - len(name) - indent_level)
|
||||||
line = f"{' ' * indent_level} * {name}{spaces}- {change_string}"
|
line = f"{' ' * indent_level} * {name}{spaces}- {change_string}"
|
||||||
|
|
||||||
if len(msg) < 50:
|
if len(msg) < 50:
|
||||||
line += f" -- {msg}"
|
line += f' -- {msg}'
|
||||||
print(f"{line} {'-' * (line_length - len(line))}---------")
|
print(f"{line} {'-' * (line_length - len(line))}---------")
|
||||||
else:
|
else:
|
||||||
print(f"{line} {'-' * (line_length - len(line))}")
|
print(f"{line} {'-' * (line_length - len(line))}")
|
||||||
@@ -165,10 +164,10 @@ class CallbackModule(CallbackBase):
|
|||||||
if diff:
|
if diff:
|
||||||
self._print_diff(diff, indent_level)
|
self._print_diff(diff, indent_level)
|
||||||
if stdout:
|
if stdout:
|
||||||
stdout = colorize(stdout, "failed")
|
stdout = colorize(stdout, 'failed')
|
||||||
print(self._indent_text(stdout, indent_level + 4))
|
print(self._indent_text(stdout, indent_level + 4))
|
||||||
if stderr:
|
if stderr:
|
||||||
stderr = colorize(stderr, "failed")
|
stderr = colorize(stderr, 'failed')
|
||||||
print(self._indent_text(stderr, indent_level + 4))
|
print(self._indent_text(stderr, indent_level + 4))
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
@@ -183,61 +182,61 @@ class CallbackModule(CallbackBase):
|
|||||||
def _print_task_result(self, result, error=False, **kwargs):
|
def _print_task_result(self, result, error=False, **kwargs):
|
||||||
"""Run when a task finishes correctly."""
|
"""Run when a task finishes correctly."""
|
||||||
|
|
||||||
if "print_action" in result._task.tags or error or self._display.verbosity > 1:
|
if 'print_action' in result._task.tags or error or self._display.verbosity > 1:
|
||||||
self._print_task()
|
self._print_task()
|
||||||
self.last_skipped = False
|
self.last_skipped = False
|
||||||
msg = to_text(result._result.get("msg", "")) or to_text(result._result.get("reason", ""))
|
msg = to_text(result._result.get('msg', '')) or\
|
||||||
|
to_text(result._result.get('reason', ''))
|
||||||
|
|
||||||
stderr = [result._result.get("exception", None), result._result.get("module_stderr", None)]
|
stderr = [result._result.get('exception', None),
|
||||||
|
result._result.get('module_stderr', None)]
|
||||||
stderr = "\n".join([e for e in stderr if e]).strip()
|
stderr = "\n".join([e for e in stderr if e]).strip()
|
||||||
|
|
||||||
self._print_host_or_item(
|
self._print_host_or_item(result._host,
|
||||||
result._host,
|
result._result.get('changed', False),
|
||||||
result._result.get("changed", False),
|
msg,
|
||||||
msg,
|
result._result.get('diff', None),
|
||||||
result._result.get("diff", None),
|
is_host=True,
|
||||||
is_host=True,
|
error=error,
|
||||||
error=error,
|
stdout=result._result.get('module_stdout', None),
|
||||||
stdout=result._result.get("module_stdout", None),
|
stderr=stderr.strip(),
|
||||||
stderr=stderr.strip(),
|
)
|
||||||
)
|
if 'results' in result._result:
|
||||||
if "results" in result._result:
|
for r in result._result['results']:
|
||||||
for r in result._result["results"]:
|
failed = 'failed' in r and r['failed']
|
||||||
failed = "failed" in r and r["failed"]
|
|
||||||
|
|
||||||
stderr = [r.get("exception", None), r.get("module_stderr", None)]
|
stderr = [r.get('exception', None), r.get('module_stderr', None)]
|
||||||
stderr = "\n".join([e for e in stderr if e]).strip()
|
stderr = "\n".join([e for e in stderr if e]).strip()
|
||||||
|
|
||||||
self._print_host_or_item(
|
self._print_host_or_item(r[r['ansible_loop_var']],
|
||||||
r[r["ansible_loop_var"]],
|
r.get('changed', False),
|
||||||
r.get("changed", False),
|
to_text(r.get('msg', '')),
|
||||||
to_text(r.get("msg", "")),
|
r.get('diff', None),
|
||||||
r.get("diff", None),
|
is_host=False,
|
||||||
is_host=False,
|
error=failed,
|
||||||
error=failed,
|
stdout=r.get('module_stdout', None),
|
||||||
stdout=r.get("module_stdout", None),
|
stderr=stderr.strip(),
|
||||||
stderr=stderr.strip(),
|
)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.last_skipped = True
|
self.last_skipped = True
|
||||||
print(".", end="")
|
print('.', end="")
|
||||||
|
|
||||||
def v2_playbook_on_stats(self, stats):
|
def v2_playbook_on_stats(self, stats):
|
||||||
"""Display info about playbook statistics."""
|
"""Display info about playbook statistics."""
|
||||||
print()
|
print()
|
||||||
self.printed_last_task = False
|
self.printed_last_task = False
|
||||||
self._print_task("STATS")
|
self._print_task('STATS')
|
||||||
|
|
||||||
hosts = sorted(stats.processed.keys())
|
hosts = sorted(stats.processed.keys())
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
s = stats.summarize(host)
|
s = stats.summarize(host)
|
||||||
|
|
||||||
if s["failures"] or s["unreachable"]:
|
if s['failures'] or s['unreachable']:
|
||||||
color = "failed"
|
color = 'failed'
|
||||||
elif s["changed"]:
|
elif s['changed']:
|
||||||
color = "changed"
|
color = 'changed'
|
||||||
else:
|
else:
|
||||||
color = "ok"
|
color = 'ok'
|
||||||
|
|
||||||
msg = (
|
msg = (
|
||||||
f"{host} : ok={s['ok']}\tchanged={s['changed']}\tfailed={s['failures']}\tunreachable="
|
f"{host} : ok={s['ok']}\tchanged={s['changed']}\tfailed={s['failures']}\tunreachable="
|
||||||
@@ -252,13 +251,14 @@ class CallbackModule(CallbackBase):
|
|||||||
self.last_skipped = False
|
self.last_skipped = False
|
||||||
|
|
||||||
line_length = 120
|
line_length = 120
|
||||||
spaces = " " * (31 - len(result._host.name) - 4)
|
spaces = ' ' * (31 - len(result._host.name) - 4)
|
||||||
|
|
||||||
line = f" * {colorize(result._host.name, 'not_so_bold')}{spaces}- {colorize('skipped', 'skipped')}"
|
line = f" * {colorize(result._host.name, 'not_so_bold')}{spaces}- {colorize('skipped', 'skipped')}"
|
||||||
|
|
||||||
reason = result._result.get("skipped_reason", "") or result._result.get("skip_reason", "")
|
reason = result._result.get('skipped_reason', '') or \
|
||||||
|
result._result.get('skip_reason', '')
|
||||||
if len(reason) < 50:
|
if len(reason) < 50:
|
||||||
line += f" -- {reason}"
|
line += f' -- {reason}'
|
||||||
print(f"{line} {'-' * (line_length - len(line))}---------")
|
print(f"{line} {'-' * (line_length - len(line))}---------")
|
||||||
else:
|
else:
|
||||||
print(f"{line} {'-' * (line_length - len(line))}")
|
print(f"{line} {'-' * (line_length - len(line))}")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2014-2015, Matt Martz <matt@sivel.net>
|
# Copyright (c) 2014-2015, Matt Martz <matt@sivel.net>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -70,7 +71,6 @@ from ansible.plugins.callback import CallbackBase
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import prettytable
|
import prettytable
|
||||||
|
|
||||||
HAS_PRETTYTABLE = True
|
HAS_PRETTYTABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_PRETTYTABLE = False
|
HAS_PRETTYTABLE = False
|
||||||
@@ -80,20 +80,20 @@ class CallbackModule(CallbackBase):
|
|||||||
"""This is an ansible callback plugin that sends status
|
"""This is an ansible callback plugin that sends status
|
||||||
updates to a Slack channel during playbook execution.
|
updates to a Slack channel during playbook execution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.slack"
|
CALLBACK_NAME = 'community.general.slack'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
|
||||||
|
super(CallbackModule, self).__init__(display=display)
|
||||||
|
|
||||||
if not HAS_PRETTYTABLE:
|
if not HAS_PRETTYTABLE:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('The `prettytable` python module is not '
|
||||||
"The `prettytable` python module is not installed. Disabling the Slack callback plugin."
|
'installed. Disabling the Slack callback '
|
||||||
)
|
'plugin.')
|
||||||
|
|
||||||
self.playbook_name = None
|
self.playbook_name = None
|
||||||
|
|
||||||
@@ -103,34 +103,34 @@ class CallbackModule(CallbackBase):
|
|||||||
self.guid = uuid.uuid4().hex[:6]
|
self.guid = uuid.uuid4().hex[:6]
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
|
||||||
|
|
||||||
self.webhook_url = self.get_option("webhook_url")
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
self.channel = self.get_option("channel")
|
|
||||||
self.username = self.get_option("username")
|
self.webhook_url = self.get_option('webhook_url')
|
||||||
self.show_invocation = self._display.verbosity > 1
|
self.channel = self.get_option('channel')
|
||||||
self.validate_certs = self.get_option("validate_certs")
|
self.username = self.get_option('username')
|
||||||
self.http_agent = self.get_option("http_agent")
|
self.show_invocation = (self._display.verbosity > 1)
|
||||||
|
self.validate_certs = self.get_option('validate_certs')
|
||||||
|
self.http_agent = self.get_option('http_agent')
|
||||||
if self.webhook_url is None:
|
if self.webhook_url is None:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('Slack Webhook URL was not provided. The '
|
||||||
"Slack Webhook URL was not provided. The "
|
'Slack Webhook URL can be provided using '
|
||||||
"Slack Webhook URL can be provided using "
|
'the `SLACK_WEBHOOK_URL` environment '
|
||||||
"the `SLACK_WEBHOOK_URL` environment "
|
'variable.')
|
||||||
"variable."
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_msg(self, attachments):
|
def send_msg(self, attachments):
|
||||||
headers = {
|
headers = {
|
||||||
"Content-type": "application/json",
|
'Content-type': 'application/json',
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"channel": self.channel,
|
'channel': self.channel,
|
||||||
"username": self.username,
|
'username': self.username,
|
||||||
"attachments": attachments,
|
'attachments': attachments,
|
||||||
"parse": "none",
|
'parse': 'none',
|
||||||
"icon_url": ("https://cdn2.hubspot.net/hub/330046/file-449187601-png/ansible_badge.png"),
|
'icon_url': ('https://cdn2.hubspot.net/hub/330046/'
|
||||||
|
'file-449187601-png/ansible_badge.png'),
|
||||||
}
|
}
|
||||||
|
|
||||||
data = json.dumps(payload)
|
data = json.dumps(payload)
|
||||||
@@ -146,63 +146,67 @@ class CallbackModule(CallbackBase):
|
|||||||
)
|
)
|
||||||
return response.read()
|
return response.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._display.warning(f"Could not submit message to Slack: {e}")
|
self._display.warning(f'Could not submit message to Slack: {e}')
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.playbook_name = os.path.basename(playbook._file_name)
|
self.playbook_name = os.path.basename(playbook._file_name)
|
||||||
|
|
||||||
title = [f"*Playbook initiated* (_{self.guid}_)"]
|
title = [
|
||||||
|
f'*Playbook initiated* (_{self.guid}_)'
|
||||||
|
]
|
||||||
|
|
||||||
invocation_items = []
|
invocation_items = []
|
||||||
if context.CLIARGS and self.show_invocation:
|
if context.CLIARGS and self.show_invocation:
|
||||||
tags = context.CLIARGS["tags"]
|
tags = context.CLIARGS['tags']
|
||||||
skip_tags = context.CLIARGS["skip_tags"]
|
skip_tags = context.CLIARGS['skip_tags']
|
||||||
extra_vars = context.CLIARGS["extra_vars"]
|
extra_vars = context.CLIARGS['extra_vars']
|
||||||
subset = context.CLIARGS["subset"]
|
subset = context.CLIARGS['subset']
|
||||||
inventory = [os.path.abspath(i) for i in context.CLIARGS["inventory"]]
|
inventory = [os.path.abspath(i) for i in context.CLIARGS['inventory']]
|
||||||
|
|
||||||
invocation_items.append(f"Inventory: {', '.join(inventory)}")
|
invocation_items.append(f"Inventory: {', '.join(inventory)}")
|
||||||
if tags and tags != ["all"]:
|
if tags and tags != ['all']:
|
||||||
invocation_items.append(f"Tags: {', '.join(tags)}")
|
invocation_items.append(f"Tags: {', '.join(tags)}")
|
||||||
if skip_tags:
|
if skip_tags:
|
||||||
invocation_items.append(f"Skip Tags: {', '.join(skip_tags)}")
|
invocation_items.append(f"Skip Tags: {', '.join(skip_tags)}")
|
||||||
if subset:
|
if subset:
|
||||||
invocation_items.append(f"Limit: {subset}")
|
invocation_items.append(f'Limit: {subset}')
|
||||||
if extra_vars:
|
if extra_vars:
|
||||||
invocation_items.append(f"Extra Vars: {' '.join(extra_vars)}")
|
invocation_items.append(f"Extra Vars: {' '.join(extra_vars)}")
|
||||||
|
|
||||||
title.append(f"by *{context.CLIARGS['remote_user']}*")
|
title.append(f"by *{context.CLIARGS['remote_user']}*")
|
||||||
|
|
||||||
title.append(f"\n\n*{self.playbook_name}*")
|
title.append(f'\n\n*{self.playbook_name}*')
|
||||||
msg_items = [" ".join(title)]
|
msg_items = [' '.join(title)]
|
||||||
if invocation_items:
|
if invocation_items:
|
||||||
_inv_item = "\n".join(invocation_items)
|
_inv_item = '\n'.join(invocation_items)
|
||||||
msg_items.append(f"```\n{_inv_item}\n```")
|
msg_items.append(f'```\n{_inv_item}\n```')
|
||||||
|
|
||||||
msg = "\n".join(msg_items)
|
msg = '\n'.join(msg_items)
|
||||||
|
|
||||||
attachments = [
|
attachments = [{
|
||||||
{
|
'fallback': msg,
|
||||||
"fallback": msg,
|
'fields': [
|
||||||
"fields": [{"value": msg}],
|
{
|
||||||
"color": "warning",
|
'value': msg
|
||||||
"mrkdwn_in": ["text", "fallback", "fields"],
|
}
|
||||||
}
|
],
|
||||||
]
|
'color': 'warning',
|
||||||
|
'mrkdwn_in': ['text', 'fallback', 'fields'],
|
||||||
|
}]
|
||||||
|
|
||||||
self.send_msg(attachments=attachments)
|
self.send_msg(attachments=attachments)
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
"""Display Play start messages"""
|
"""Display Play start messages"""
|
||||||
|
|
||||||
name = play.name or f"Play name not specified ({play._uuid})"
|
name = play.name or f'Play name not specified ({play._uuid})'
|
||||||
msg = f"*Starting play* (_{self.guid}_)\n\n*{name}*"
|
msg = f'*Starting play* (_{self.guid}_)\n\n*{name}*'
|
||||||
attachments = [
|
attachments = [
|
||||||
{
|
{
|
||||||
"fallback": msg,
|
'fallback': msg,
|
||||||
"text": msg,
|
'text': msg,
|
||||||
"color": "warning",
|
'color': 'warning',
|
||||||
"mrkdwn_in": ["text", "fallback", "fields"],
|
'mrkdwn_in': ['text', 'fallback', 'fields'],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
self.send_msg(attachments=attachments)
|
self.send_msg(attachments=attachments)
|
||||||
@@ -212,7 +216,8 @@ class CallbackModule(CallbackBase):
|
|||||||
|
|
||||||
hosts = sorted(stats.processed.keys())
|
hosts = sorted(stats.processed.keys())
|
||||||
|
|
||||||
t = prettytable.PrettyTable(["Host", "Ok", "Changed", "Unreachable", "Failures", "Rescued", "Ignored"])
|
t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
|
||||||
|
'Failures', 'Rescued', 'Ignored'])
|
||||||
|
|
||||||
failures = False
|
failures = False
|
||||||
unreachable = False
|
unreachable = False
|
||||||
@@ -220,28 +225,38 @@ class CallbackModule(CallbackBase):
|
|||||||
for h in hosts:
|
for h in hosts:
|
||||||
s = stats.summarize(h)
|
s = stats.summarize(h)
|
||||||
|
|
||||||
if s["failures"] > 0:
|
if s['failures'] > 0:
|
||||||
failures = True
|
failures = True
|
||||||
if s["unreachable"] > 0:
|
if s['unreachable'] > 0:
|
||||||
unreachable = True
|
unreachable = True
|
||||||
|
|
||||||
t.add_row([h] + [s[k] for k in ["ok", "changed", "unreachable", "failures", "rescued", "ignored"]])
|
t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
|
||||||
|
'failures', 'rescued', 'ignored']])
|
||||||
|
|
||||||
attachments = []
|
attachments = []
|
||||||
msg_items = [f"*Playbook Complete* (_{self.guid}_)"]
|
msg_items = [
|
||||||
|
f'*Playbook Complete* (_{self.guid}_)'
|
||||||
|
]
|
||||||
if failures or unreachable:
|
if failures or unreachable:
|
||||||
color = "danger"
|
color = 'danger'
|
||||||
msg_items.append("\n*Failed!*")
|
msg_items.append('\n*Failed!*')
|
||||||
else:
|
else:
|
||||||
color = "good"
|
color = 'good'
|
||||||
msg_items.append("\n*Success!*")
|
msg_items.append('\n*Success!*')
|
||||||
|
|
||||||
msg_items.append(f"```\n{t}\n```")
|
msg_items.append(f'```\n{t}\n```')
|
||||||
|
|
||||||
msg = "\n".join(msg_items)
|
msg = '\n'.join(msg_items)
|
||||||
|
|
||||||
attachments.append(
|
attachments.append({
|
||||||
{"fallback": msg, "fields": [{"value": msg}], "color": color, "mrkdwn_in": ["text", "fallback", "fields"]}
|
'fallback': msg,
|
||||||
)
|
'fields': [
|
||||||
|
{
|
||||||
|
'value': msg
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'color': color,
|
||||||
|
'mrkdwn_in': ['text', 'fallback', 'fields']
|
||||||
|
})
|
||||||
|
|
||||||
self.send_msg(attachments=attachments)
|
self.send_msg(attachments=attachments)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) Ansible Project
|
# Copyright (c) Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -100,7 +101,7 @@ from ansible_collections.community.general.plugins.module_utils.datetime import
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SplunkHTTPCollectorSource:
|
class SplunkHTTPCollectorSource(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ansible_check_mode = False
|
self.ansible_check_mode = False
|
||||||
self.ansible_playbook = ""
|
self.ansible_playbook = ""
|
||||||
@@ -110,7 +111,7 @@ class SplunkHTTPCollectorSource:
|
|||||||
self.user = getpass.getuser()
|
self.user = getpass.getuser()
|
||||||
|
|
||||||
def send_event(self, url, authtoken, validate_certs, include_milliseconds, batch, state, result, runtime):
|
def send_event(self, url, authtoken, validate_certs, include_milliseconds, batch, state, result, runtime):
|
||||||
if result._task_fields["args"].get("_ansible_check_mode") is True:
|
if result._task_fields['args'].get('_ansible_check_mode') is True:
|
||||||
self.ansible_check_mode = True
|
self.ansible_check_mode = True
|
||||||
|
|
||||||
if result._task._role:
|
if result._task._role:
|
||||||
@@ -118,33 +119,33 @@ class SplunkHTTPCollectorSource:
|
|||||||
else:
|
else:
|
||||||
ansible_role = None
|
ansible_role = None
|
||||||
|
|
||||||
if "args" in result._task_fields:
|
if 'args' in result._task_fields:
|
||||||
del result._task_fields["args"]
|
del result._task_fields['args']
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
data["uuid"] = result._task._uuid
|
data['uuid'] = result._task._uuid
|
||||||
data["session"] = self.session
|
data['session'] = self.session
|
||||||
if batch is not None:
|
if batch is not None:
|
||||||
data["batch"] = batch
|
data['batch'] = batch
|
||||||
data["status"] = state
|
data['status'] = state
|
||||||
|
|
||||||
if include_milliseconds:
|
if include_milliseconds:
|
||||||
time_format = "%Y-%m-%d %H:%M:%S.%f +0000"
|
time_format = '%Y-%m-%d %H:%M:%S.%f +0000'
|
||||||
else:
|
else:
|
||||||
time_format = "%Y-%m-%d %H:%M:%S +0000"
|
time_format = '%Y-%m-%d %H:%M:%S +0000'
|
||||||
|
|
||||||
data["timestamp"] = now().strftime(time_format)
|
data['timestamp'] = now().strftime(time_format)
|
||||||
data["host"] = self.host
|
data['host'] = self.host
|
||||||
data["ip_address"] = self.ip_address
|
data['ip_address'] = self.ip_address
|
||||||
data["user"] = self.user
|
data['user'] = self.user
|
||||||
data["runtime"] = runtime
|
data['runtime'] = runtime
|
||||||
data["ansible_version"] = ansible_version
|
data['ansible_version'] = ansible_version
|
||||||
data["ansible_check_mode"] = self.ansible_check_mode
|
data['ansible_check_mode'] = self.ansible_check_mode
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_playbook"] = self.ansible_playbook
|
data['ansible_playbook'] = self.ansible_playbook
|
||||||
data["ansible_role"] = ansible_role
|
data['ansible_role'] = ansible_role
|
||||||
data["ansible_task"] = result._task_fields
|
data['ansible_task'] = result._task_fields
|
||||||
data["ansible_result"] = result._result
|
data['ansible_result'] = result._result
|
||||||
|
|
||||||
# This wraps the json payload in and outer json event needed by Splunk
|
# This wraps the json payload in and outer json event needed by Splunk
|
||||||
jsondata = json.dumps({"event": data}, cls=AnsibleJSONEncoder, sort_keys=True)
|
jsondata = json.dumps({"event": data}, cls=AnsibleJSONEncoder, sort_keys=True)
|
||||||
@@ -152,20 +153,23 @@ class SplunkHTTPCollectorSource:
|
|||||||
open_url(
|
open_url(
|
||||||
url,
|
url,
|
||||||
jsondata,
|
jsondata,
|
||||||
headers={"Content-type": "application/json", "Authorization": f"Splunk {authtoken}"},
|
headers={
|
||||||
method="POST",
|
'Content-type': 'application/json',
|
||||||
validate_certs=validate_certs,
|
'Authorization': f"Splunk {authtoken}"
|
||||||
|
},
|
||||||
|
method='POST',
|
||||||
|
validate_certs=validate_certs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.splunk"
|
CALLBACK_NAME = 'community.general.splunk'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.start_datetimes = {} # Collect task start times
|
self.start_datetimes = {} # Collect task start times
|
||||||
self.url = None
|
self.url = None
|
||||||
self.authtoken = None
|
self.authtoken = None
|
||||||
@@ -175,40 +179,41 @@ class CallbackModule(CallbackBase):
|
|||||||
self.splunk = SplunkHTTPCollectorSource()
|
self.splunk = SplunkHTTPCollectorSource()
|
||||||
|
|
||||||
def _runtime(self, result):
|
def _runtime(self, result):
|
||||||
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
|
return (
|
||||||
|
now() -
|
||||||
|
self.start_datetimes[result._task._uuid]
|
||||||
|
).total_seconds()
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys,
|
||||||
|
var_options=var_options,
|
||||||
|
direct=direct)
|
||||||
|
|
||||||
self.url = self.get_option("url")
|
self.url = self.get_option('url')
|
||||||
|
|
||||||
if self.url is None:
|
if self.url is None:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('Splunk HTTP collector source URL was '
|
||||||
"Splunk HTTP collector source URL was "
|
'not provided. The Splunk HTTP collector '
|
||||||
"not provided. The Splunk HTTP collector "
|
'source URL can be provided using the '
|
||||||
"source URL can be provided using the "
|
'`SPLUNK_URL` environment variable or '
|
||||||
"`SPLUNK_URL` environment variable or "
|
'in the ansible.cfg file.')
|
||||||
"in the ansible.cfg file."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.authtoken = self.get_option("authtoken")
|
self.authtoken = self.get_option('authtoken')
|
||||||
|
|
||||||
if self.authtoken is None:
|
if self.authtoken is None:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('Splunk HTTP collector requires an authentication'
|
||||||
"Splunk HTTP collector requires an authentication"
|
'token. The Splunk HTTP collector '
|
||||||
"token. The Splunk HTTP collector "
|
'authentication token can be provided using the '
|
||||||
"authentication token can be provided using the "
|
'`SPLUNK_AUTHTOKEN` environment variable or '
|
||||||
"`SPLUNK_AUTHTOKEN` environment variable or "
|
'in the ansible.cfg file.')
|
||||||
"in the ansible.cfg file."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.validate_certs = self.get_option("validate_certs")
|
self.validate_certs = self.get_option('validate_certs')
|
||||||
|
|
||||||
self.include_milliseconds = self.get_option("include_milliseconds")
|
self.include_milliseconds = self.get_option('include_milliseconds')
|
||||||
|
|
||||||
self.batch = self.get_option("batch")
|
self.batch = self.get_option('batch')
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.splunk.ansible_playbook = basename(playbook._file_name)
|
self.splunk.ansible_playbook = basename(playbook._file_name)
|
||||||
@@ -226,9 +231,9 @@ class CallbackModule(CallbackBase):
|
|||||||
self.validate_certs,
|
self.validate_certs,
|
||||||
self.include_milliseconds,
|
self.include_milliseconds,
|
||||||
self.batch,
|
self.batch,
|
||||||
"OK",
|
'OK',
|
||||||
result,
|
result,
|
||||||
self._runtime(result),
|
self._runtime(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result, **kwargs):
|
def v2_runner_on_skipped(self, result, **kwargs):
|
||||||
@@ -238,9 +243,9 @@ class CallbackModule(CallbackBase):
|
|||||||
self.validate_certs,
|
self.validate_certs,
|
||||||
self.include_milliseconds,
|
self.include_milliseconds,
|
||||||
self.batch,
|
self.batch,
|
||||||
"SKIPPED",
|
'SKIPPED',
|
||||||
result,
|
result,
|
||||||
self._runtime(result),
|
self._runtime(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, **kwargs):
|
def v2_runner_on_failed(self, result, **kwargs):
|
||||||
@@ -250,9 +255,9 @@ class CallbackModule(CallbackBase):
|
|||||||
self.validate_certs,
|
self.validate_certs,
|
||||||
self.include_milliseconds,
|
self.include_milliseconds,
|
||||||
self.batch,
|
self.batch,
|
||||||
"FAILED",
|
'FAILED',
|
||||||
result,
|
result,
|
||||||
self._runtime(result),
|
self._runtime(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def runner_on_async_failed(self, result, **kwargs):
|
def runner_on_async_failed(self, result, **kwargs):
|
||||||
@@ -262,9 +267,9 @@ class CallbackModule(CallbackBase):
|
|||||||
self.validate_certs,
|
self.validate_certs,
|
||||||
self.include_milliseconds,
|
self.include_milliseconds,
|
||||||
self.batch,
|
self.batch,
|
||||||
"FAILED",
|
'FAILED',
|
||||||
result,
|
result,
|
||||||
self._runtime(result),
|
self._runtime(result)
|
||||||
)
|
)
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result, **kwargs):
|
def v2_runner_on_unreachable(self, result, **kwargs):
|
||||||
@@ -274,7 +279,7 @@ class CallbackModule(CallbackBase):
|
|||||||
self.validate_certs,
|
self.validate_certs,
|
||||||
self.include_milliseconds,
|
self.include_milliseconds,
|
||||||
self.batch,
|
self.batch,
|
||||||
"UNREACHABLE",
|
'UNREACHABLE',
|
||||||
result,
|
result,
|
||||||
self._runtime(result),
|
self._runtime(result)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) Ansible Project
|
# Copyright (c) Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -57,7 +58,7 @@ from ansible_collections.community.general.plugins.module_utils.datetime import
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SumologicHTTPCollectorSource:
|
class SumologicHTTPCollectorSource(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ansible_check_mode = False
|
self.ansible_check_mode = False
|
||||||
self.ansible_playbook = ""
|
self.ansible_playbook = ""
|
||||||
@@ -67,7 +68,7 @@ class SumologicHTTPCollectorSource:
|
|||||||
self.user = getpass.getuser()
|
self.user = getpass.getuser()
|
||||||
|
|
||||||
def send_event(self, url, state, result, runtime):
|
def send_event(self, url, state, result, runtime):
|
||||||
if result._task_fields["args"].get("_ansible_check_mode") is True:
|
if result._task_fields['args'].get('_ansible_check_mode') is True:
|
||||||
self.ansible_check_mode = True
|
self.ansible_check_mode = True
|
||||||
|
|
||||||
if result._task._role:
|
if result._task._role:
|
||||||
@@ -75,63 +76,67 @@ class SumologicHTTPCollectorSource:
|
|||||||
else:
|
else:
|
||||||
ansible_role = None
|
ansible_role = None
|
||||||
|
|
||||||
if "args" in result._task_fields:
|
if 'args' in result._task_fields:
|
||||||
del result._task_fields["args"]
|
del result._task_fields['args']
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
data["uuid"] = result._task._uuid
|
data['uuid'] = result._task._uuid
|
||||||
data["session"] = self.session
|
data['session'] = self.session
|
||||||
data["status"] = state
|
data['status'] = state
|
||||||
data["timestamp"] = now().strftime("%Y-%m-%d %H:%M:%S +0000")
|
data['timestamp'] = now().strftime('%Y-%m-%d %H:%M:%S +0000')
|
||||||
data["host"] = self.host
|
data['host'] = self.host
|
||||||
data["ip_address"] = self.ip_address
|
data['ip_address'] = self.ip_address
|
||||||
data["user"] = self.user
|
data['user'] = self.user
|
||||||
data["runtime"] = runtime
|
data['runtime'] = runtime
|
||||||
data["ansible_version"] = ansible_version
|
data['ansible_version'] = ansible_version
|
||||||
data["ansible_check_mode"] = self.ansible_check_mode
|
data['ansible_check_mode'] = self.ansible_check_mode
|
||||||
data["ansible_host"] = result._host.name
|
data['ansible_host'] = result._host.name
|
||||||
data["ansible_playbook"] = self.ansible_playbook
|
data['ansible_playbook'] = self.ansible_playbook
|
||||||
data["ansible_role"] = ansible_role
|
data['ansible_role'] = ansible_role
|
||||||
data["ansible_task"] = result._task_fields
|
data['ansible_task'] = result._task_fields
|
||||||
data["ansible_result"] = result._result
|
data['ansible_result'] = result._result
|
||||||
|
|
||||||
open_url(
|
open_url(
|
||||||
url,
|
url,
|
||||||
data=json.dumps(data, cls=AnsibleJSONEncoder, sort_keys=True),
|
data=json.dumps(data, cls=AnsibleJSONEncoder, sort_keys=True),
|
||||||
headers={"Content-type": "application/json", "X-Sumo-Host": data["ansible_host"]},
|
headers={
|
||||||
method="POST",
|
'Content-type': 'application/json',
|
||||||
|
'X-Sumo-Host': data['ansible_host']
|
||||||
|
},
|
||||||
|
method='POST'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackBase):
|
class CallbackModule(CallbackBase):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.sumologic"
|
CALLBACK_NAME = 'community.general.sumologic'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self, display=None):
|
def __init__(self, display=None):
|
||||||
super().__init__(display=display)
|
super(CallbackModule, self).__init__(display=display)
|
||||||
self.start_datetimes = {} # Collect task start times
|
self.start_datetimes = {} # Collect task start times
|
||||||
self.url = None
|
self.url = None
|
||||||
self.sumologic = SumologicHTTPCollectorSource()
|
self.sumologic = SumologicHTTPCollectorSource()
|
||||||
|
|
||||||
def _runtime(self, result):
|
def _runtime(self, result):
|
||||||
return (now() - self.start_datetimes[result._task._uuid]).total_seconds()
|
return (
|
||||||
|
now() -
|
||||||
|
self.start_datetimes[result._task._uuid]
|
||||||
|
).total_seconds()
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
self.url = self.get_option("url")
|
self.url = self.get_option('url')
|
||||||
|
|
||||||
if self.url is None:
|
if self.url is None:
|
||||||
self.disabled = True
|
self.disabled = True
|
||||||
self._display.warning(
|
self._display.warning('Sumologic HTTP collector source URL was '
|
||||||
"Sumologic HTTP collector source URL was "
|
'not provided. The Sumologic HTTP collector '
|
||||||
"not provided. The Sumologic HTTP collector "
|
'source URL can be provided using the '
|
||||||
"source URL can be provided using the "
|
'`SUMOLOGIC_URL` environment variable or '
|
||||||
"`SUMOLOGIC_URL` environment variable or "
|
'in the ansible.cfg file.')
|
||||||
"in the ansible.cfg file."
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
self.sumologic.ansible_playbook = basename(playbook._file_name)
|
self.sumologic.ansible_playbook = basename(playbook._file_name)
|
||||||
@@ -143,16 +148,41 @@ class CallbackModule(CallbackBase):
|
|||||||
self.start_datetimes[task._uuid] = now()
|
self.start_datetimes[task._uuid] = now()
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result, **kwargs):
|
def v2_runner_on_ok(self, result, **kwargs):
|
||||||
self.sumologic.send_event(self.url, "OK", result, self._runtime(result))
|
self.sumologic.send_event(
|
||||||
|
self.url,
|
||||||
|
'OK',
|
||||||
|
result,
|
||||||
|
self._runtime(result)
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result, **kwargs):
|
def v2_runner_on_skipped(self, result, **kwargs):
|
||||||
self.sumologic.send_event(self.url, "SKIPPED", result, self._runtime(result))
|
self.sumologic.send_event(
|
||||||
|
self.url,
|
||||||
|
'SKIPPED',
|
||||||
|
result,
|
||||||
|
self._runtime(result)
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, **kwargs):
|
def v2_runner_on_failed(self, result, **kwargs):
|
||||||
self.sumologic.send_event(self.url, "FAILED", result, self._runtime(result))
|
self.sumologic.send_event(
|
||||||
|
self.url,
|
||||||
|
'FAILED',
|
||||||
|
result,
|
||||||
|
self._runtime(result)
|
||||||
|
)
|
||||||
|
|
||||||
def runner_on_async_failed(self, result, **kwargs):
|
def runner_on_async_failed(self, result, **kwargs):
|
||||||
self.sumologic.send_event(self.url, "FAILED", result, self._runtime(result))
|
self.sumologic.send_event(
|
||||||
|
self.url,
|
||||||
|
'FAILED',
|
||||||
|
result,
|
||||||
|
self._runtime(result)
|
||||||
|
)
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result, **kwargs):
|
def v2_runner_on_unreachable(self, result, **kwargs):
|
||||||
self.sumologic.send_event(self.url, "UNREACHABLE", result, self._runtime(result))
|
self.sumologic.send_event(
|
||||||
|
self.url,
|
||||||
|
'UNREACHABLE',
|
||||||
|
result,
|
||||||
|
self._runtime(result)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -68,89 +69,62 @@ class CallbackModule(CallbackBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "notification"
|
CALLBACK_TYPE = 'notification'
|
||||||
CALLBACK_NAME = "community.general.syslog_json"
|
CALLBACK_NAME = 'community.general.syslog_json'
|
||||||
CALLBACK_NEEDS_WHITELIST = True
|
CALLBACK_NEEDS_WHITELIST = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
|
||||||
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
|
||||||
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
syslog_host = self.get_option("server")
|
syslog_host = self.get_option("server")
|
||||||
syslog_port = int(self.get_option("port"))
|
syslog_port = int(self.get_option("port"))
|
||||||
syslog_facility = self.get_option("facility")
|
syslog_facility = self.get_option("facility")
|
||||||
|
|
||||||
self.logger = logging.getLogger("ansible logger")
|
self.logger = logging.getLogger('ansible logger')
|
||||||
self.logger.setLevel(logging.DEBUG)
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
self.handler = logging.handlers.SysLogHandler(address=(syslog_host, syslog_port), facility=syslog_facility)
|
self.handler = logging.handlers.SysLogHandler(
|
||||||
|
address=(syslog_host, syslog_port),
|
||||||
|
facility=syslog_facility
|
||||||
|
)
|
||||||
self.logger.addHandler(self.handler)
|
self.logger.addHandler(self.handler)
|
||||||
self.hostname = socket.gethostname()
|
self.hostname = socket.gethostname()
|
||||||
|
|
||||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||||
res = result._result
|
res = result._result
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
self.logger.error(
|
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||||
"%s ansible-command: task execution FAILED; host: %s; message: %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
self._dump_results(res),
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result):
|
def v2_runner_on_ok(self, result):
|
||||||
res = result._result
|
res = result._result
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
if result._task.action != "gather_facts" or self.get_option("setup"):
|
if result._task.action != "gather_facts" or self.get_option("setup"):
|
||||||
self.logger.info(
|
self.logger.info('%s ansible-command: task execution OK; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||||
"%s ansible-command: task execution OK; host: %s; message: %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
self._dump_results(res),
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result):
|
def v2_runner_on_skipped(self, result):
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
self.logger.info(
|
self.logger.info('%s ansible-command: task execution SKIPPED; host: %s; message: %s', self.hostname, host, 'skipped')
|
||||||
"%s ansible-command: task execution SKIPPED; host: %s; message: %s", self.hostname, host, "skipped"
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_runner_on_unreachable(self, result):
|
def v2_runner_on_unreachable(self, result):
|
||||||
res = result._result
|
res = result._result
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
self.logger.error(
|
self.logger.error('%s ansible-command: task execution UNREACHABLE; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||||
"%s ansible-command: task execution UNREACHABLE; host: %s; message: %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
self._dump_results(res),
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_runner_on_async_failed(self, result):
|
def v2_runner_on_async_failed(self, result):
|
||||||
res = result._result
|
res = result._result
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
# jid = result._result.get("ansible_job_id")
|
jid = result._result.get('ansible_job_id')
|
||||||
self.logger.error(
|
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
|
||||||
"%s ansible-command: task execution FAILED; host: %s; message: %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
self._dump_results(res),
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_playbook_on_import_for_host(self, result, imported_file):
|
def v2_playbook_on_import_for_host(self, result, imported_file):
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
self.logger.info(
|
self.logger.info('%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s', self.hostname, host, imported_file)
|
||||||
"%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
imported_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
def v2_playbook_on_not_import_for_host(self, result, missing_file):
|
||||||
host = result._host.get_name()
|
host = result._host.get_name()
|
||||||
self.logger.info(
|
self.logger.info('%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s', self.hostname, host, missing_file)
|
||||||
"%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s",
|
|
||||||
self.hostname,
|
|
||||||
host,
|
|
||||||
missing_file,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2025, Felix Fontein <felix@fontein.de>
|
# Copyright (c) 2025, Felix Fontein <felix@fontein.de>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -50,8 +52,8 @@ from ansible.plugins.callback.default import CallbackModule as Default
|
|||||||
|
|
||||||
class CallbackModule(Default):
|
class CallbackModule(Default):
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.tasks_only"
|
CALLBACK_NAME = 'community.general.tasks_only'
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
pass
|
pass
|
||||||
@@ -60,7 +62,7 @@ class CallbackModule(Default):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def set_options(self, *args, **kwargs):
|
def set_options(self, *args, **kwargs):
|
||||||
result = super().set_options(*args, **kwargs)
|
result = super(CallbackModule, self).set_options(*args, **kwargs)
|
||||||
self.number_of_columns = self.get_option("number_of_columns")
|
self.number_of_columns = self.get_option("number_of_columns")
|
||||||
if self.number_of_columns is not None:
|
if self.number_of_columns is not None:
|
||||||
self._display.columns = self.number_of_columns
|
self._display.columns = self.number_of_columns
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2024, kurokobo <kurokobo@protonmail.com>
|
# Copyright (c) 2024, kurokobo <kurokobo@protonmail.com>
|
||||||
# Copyright (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
|
# Copyright (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -103,13 +105,13 @@ class CallbackModule(Default):
|
|||||||
CALLBACK_NAME = "community.general.timestamp"
|
CALLBACK_NAME = "community.general.timestamp"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
# Replace the banner method of the display object with the custom one
|
# Replace the banner method of the display object with the custom one
|
||||||
self._display.banner = types.MethodType(banner, self._display)
|
self._display.banner = types.MethodType(banner, self._display)
|
||||||
|
|
||||||
def set_options(self, task_keys=None, var_options=None, direct=None):
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
super().set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||||
|
|
||||||
# Store zoneinfo for specified timezone if available
|
# Store zoneinfo for specified timezone if available
|
||||||
tzinfo = None
|
tzinfo = None
|
||||||
@@ -119,5 +121,5 @@ class CallbackModule(Default):
|
|||||||
tzinfo = ZoneInfo(self.get_option("timezone"))
|
tzinfo = ZoneInfo(self.get_option("timezone"))
|
||||||
|
|
||||||
# Inject options into the display object
|
# Inject options into the display object
|
||||||
self._display.timestamp_tzinfo = tzinfo
|
setattr(self._display, "timestamp_tzinfo", tzinfo)
|
||||||
self._display.timestamp_format_string = self.get_option("format_string")
|
setattr(self._display, "timestamp_format_string", self.get_option("format_string"))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2023, Al Bowles <@akatch>
|
# Copyright (c) 2023, Al Bowles <@akatch>
|
||||||
# Copyright (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
# Copyright (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -28,7 +29,8 @@ from ansible.plugins.callback.default import CallbackModule as CallbackModule_de
|
|||||||
|
|
||||||
|
|
||||||
class CallbackModule(CallbackModule_default):
|
class CallbackModule(CallbackModule_default):
|
||||||
"""
|
|
||||||
|
'''
|
||||||
Design goals:
|
Design goals:
|
||||||
- Print consolidated output that looks like a *NIX startup log
|
- Print consolidated output that looks like a *NIX startup log
|
||||||
- Defaults should avoid displaying unnecessary information wherever possible
|
- Defaults should avoid displaying unnecessary information wherever possible
|
||||||
@@ -38,16 +40,14 @@ class CallbackModule(CallbackModule_default):
|
|||||||
- Add option to display all hostnames on a single line in the appropriate result color (failures may have a separate line)
|
- Add option to display all hostnames on a single line in the appropriate result color (failures may have a separate line)
|
||||||
- Consolidate stats display
|
- Consolidate stats display
|
||||||
- Don't show play name if no hosts found
|
- Don't show play name if no hosts found
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CALLBACK_VERSION = 2.0
|
CALLBACK_VERSION = 2.0
|
||||||
CALLBACK_TYPE = "stdout"
|
CALLBACK_TYPE = 'stdout'
|
||||||
CALLBACK_NAME = "community.general.unixy"
|
CALLBACK_NAME = 'community.general.unixy'
|
||||||
|
|
||||||
def _run_is_verbose(self, result):
|
def _run_is_verbose(self, result):
|
||||||
return (
|
return ((self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result)
|
||||||
self._display.verbosity > 0 or "_ansible_verbose_always" in result._result
|
|
||||||
) and "_ansible_verbose_override" not in result._result
|
|
||||||
|
|
||||||
def _get_task_display_name(self, task):
|
def _get_task_display_name(self, task):
|
||||||
self.task_display_name = None
|
self.task_display_name = None
|
||||||
@@ -60,8 +60,8 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self.task_display_name = task_display_name
|
self.task_display_name = task_display_name
|
||||||
|
|
||||||
def _preprocess_result(self, result):
|
def _preprocess_result(self, result):
|
||||||
self.delegated_vars = result._result.get("_ansible_delegated_vars", None)
|
self.delegated_vars = result._result.get('_ansible_delegated_vars', None)
|
||||||
self._handle_exception(result._result, use_stderr=self.get_option("display_failed_stderr"))
|
self._handle_exception(result._result, use_stderr=self.get_option('display_failed_stderr'))
|
||||||
self._handle_warnings(result._result)
|
self._handle_warnings(result._result)
|
||||||
|
|
||||||
def _process_result_output(self, result, msg):
|
def _process_result_output(self, result, msg):
|
||||||
@@ -73,16 +73,16 @@ class CallbackModule(CallbackModule_default):
|
|||||||
return task_result
|
return task_result
|
||||||
|
|
||||||
if self.delegated_vars:
|
if self.delegated_vars:
|
||||||
task_delegate_host = self.delegated_vars["ansible_host"]
|
task_delegate_host = self.delegated_vars['ansible_host']
|
||||||
task_result = f"{task_host} -> {task_delegate_host} {msg}"
|
task_result = f"{task_host} -> {task_delegate_host} {msg}"
|
||||||
|
|
||||||
if result._result.get("msg") and result._result.get("msg") != "All items completed":
|
if result._result.get('msg') and result._result.get('msg') != "All items completed":
|
||||||
task_result += f" | msg: {to_text(result._result.get('msg'))}"
|
task_result += f" | msg: {to_text(result._result.get('msg'))}"
|
||||||
|
|
||||||
if result._result.get("stdout"):
|
if result._result.get('stdout'):
|
||||||
task_result += f" | stdout: {result._result.get('stdout')}"
|
task_result += f" | stdout: {result._result.get('stdout')}"
|
||||||
|
|
||||||
if result._result.get("stderr"):
|
if result._result.get('stderr'):
|
||||||
task_result += f" | stderr: {result._result.get('stderr')}"
|
task_result += f" | stderr: {result._result.get('stderr')}"
|
||||||
|
|
||||||
return task_result
|
return task_result
|
||||||
@@ -90,7 +90,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
def v2_playbook_on_task_start(self, task, is_conditional):
|
def v2_playbook_on_task_start(self, task, is_conditional):
|
||||||
self._get_task_display_name(task)
|
self._get_task_display_name(task)
|
||||||
if self.task_display_name is not None:
|
if self.task_display_name is not None:
|
||||||
if task.check_mode and self.get_option("check_mode_markers"):
|
if task.check_mode and self.get_option('check_mode_markers'):
|
||||||
self._display.display(f"{self.task_display_name} (check mode)...")
|
self._display.display(f"{self.task_display_name} (check mode)...")
|
||||||
else:
|
else:
|
||||||
self._display.display(f"{self.task_display_name}...")
|
self._display.display(f"{self.task_display_name}...")
|
||||||
@@ -98,14 +98,14 @@ class CallbackModule(CallbackModule_default):
|
|||||||
def v2_playbook_on_handler_task_start(self, task):
|
def v2_playbook_on_handler_task_start(self, task):
|
||||||
self._get_task_display_name(task)
|
self._get_task_display_name(task)
|
||||||
if self.task_display_name is not None:
|
if self.task_display_name is not None:
|
||||||
if task.check_mode and self.get_option("check_mode_markers"):
|
if task.check_mode and self.get_option('check_mode_markers'):
|
||||||
self._display.display(f"{self.task_display_name} (via handler in check mode)... ")
|
self._display.display(f"{self.task_display_name} (via handler in check mode)... ")
|
||||||
else:
|
else:
|
||||||
self._display.display(f"{self.task_display_name} (via handler)... ")
|
self._display.display(f"{self.task_display_name} (via handler)... ")
|
||||||
|
|
||||||
def v2_playbook_on_play_start(self, play):
|
def v2_playbook_on_play_start(self, play):
|
||||||
name = play.get_name().strip()
|
name = play.get_name().strip()
|
||||||
if play.check_mode and self.get_option("check_mode_markers"):
|
if play.check_mode and self.get_option('check_mode_markers'):
|
||||||
if name and play.hosts:
|
if name and play.hosts:
|
||||||
msg = f"\n- {name} (in check mode) on hosts: {','.join(play.hosts)} -"
|
msg = f"\n- {name} (in check mode) on hosts: {','.join(play.hosts)} -"
|
||||||
else:
|
else:
|
||||||
@@ -119,7 +119,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self._display.display(msg)
|
self._display.display(msg)
|
||||||
|
|
||||||
def v2_runner_on_skipped(self, result, ignore_errors=False):
|
def v2_runner_on_skipped(self, result, ignore_errors=False):
|
||||||
if self.get_option("display_skipped_hosts"):
|
if self.get_option('display_skipped_hosts'):
|
||||||
self._preprocess_result(result)
|
self._preprocess_result(result)
|
||||||
display_color = C.COLOR_SKIP
|
display_color = C.COLOR_SKIP
|
||||||
msg = "skipped"
|
msg = "skipped"
|
||||||
@@ -138,12 +138,12 @@ class CallbackModule(CallbackModule_default):
|
|||||||
msg += f" | item: {item_value}"
|
msg += f" | item: {item_value}"
|
||||||
|
|
||||||
task_result = self._process_result_output(result, msg)
|
task_result = self._process_result_output(result, msg)
|
||||||
self._display.display(f" {task_result}", display_color, stderr=self.get_option("display_failed_stderr"))
|
self._display.display(f" {task_result}", display_color, stderr=self.get_option('display_failed_stderr'))
|
||||||
|
|
||||||
def v2_runner_on_ok(self, result, msg="ok", display_color=C.COLOR_OK):
|
def v2_runner_on_ok(self, result, msg="ok", display_color=C.COLOR_OK):
|
||||||
self._preprocess_result(result)
|
self._preprocess_result(result)
|
||||||
|
|
||||||
result_was_changed = "changed" in result._result and result._result["changed"]
|
result_was_changed = ('changed' in result._result and result._result['changed'])
|
||||||
if result_was_changed:
|
if result_was_changed:
|
||||||
msg = "done"
|
msg = "done"
|
||||||
item_value = self._get_item_label(result._result)
|
item_value = self._get_item_label(result._result)
|
||||||
@@ -152,7 +152,7 @@ class CallbackModule(CallbackModule_default):
|
|||||||
display_color = C.COLOR_CHANGED
|
display_color = C.COLOR_CHANGED
|
||||||
task_result = self._process_result_output(result, msg)
|
task_result = self._process_result_output(result, msg)
|
||||||
self._display.display(f" {task_result}", display_color)
|
self._display.display(f" {task_result}", display_color)
|
||||||
elif self.get_option("display_ok_hosts"):
|
elif self.get_option('display_ok_hosts'):
|
||||||
task_result = self._process_result_output(result, msg)
|
task_result = self._process_result_output(result, msg)
|
||||||
self._display.display(f" {task_result}", display_color)
|
self._display.display(f" {task_result}", display_color)
|
||||||
|
|
||||||
@@ -172,17 +172,17 @@ class CallbackModule(CallbackModule_default):
|
|||||||
display_color = C.COLOR_UNREACHABLE
|
display_color = C.COLOR_UNREACHABLE
|
||||||
task_result = self._process_result_output(result, msg)
|
task_result = self._process_result_output(result, msg)
|
||||||
|
|
||||||
self._display.display(f" {task_result}", display_color, stderr=self.get_option("display_failed_stderr"))
|
self._display.display(f" {task_result}", display_color, stderr=self.get_option('display_failed_stderr'))
|
||||||
|
|
||||||
def v2_on_file_diff(self, result):
|
def v2_on_file_diff(self, result):
|
||||||
if result._task.loop and "results" in result._result:
|
if result._task.loop and 'results' in result._result:
|
||||||
for res in result._result["results"]:
|
for res in result._result['results']:
|
||||||
if "diff" in res and res["diff"] and res.get("changed", False):
|
if 'diff' in res and res['diff'] and res.get('changed', False):
|
||||||
diff = self._get_diff(res["diff"])
|
diff = self._get_diff(res['diff'])
|
||||||
if diff:
|
if diff:
|
||||||
self._display.display(diff)
|
self._display.display(diff)
|
||||||
elif "diff" in result._result and result._result["diff"] and result._result.get("changed", False):
|
elif 'diff' in result._result and result._result['diff'] and result._result.get('changed', False):
|
||||||
diff = self._get_diff(result._result["diff"])
|
diff = self._get_diff(result._result['diff'])
|
||||||
if diff:
|
if diff:
|
||||||
self._display.display(diff)
|
self._display.display(diff)
|
||||||
|
|
||||||
@@ -198,30 +198,30 @@ class CallbackModule(CallbackModule_default):
|
|||||||
f" {hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
|
f" {hostcolor(h, t)} : {colorize('ok', t['ok'], C.COLOR_OK)} {colorize('changed', t['changed'], C.COLOR_CHANGED)} "
|
||||||
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
|
f"{colorize('unreachable', t['unreachable'], C.COLOR_UNREACHABLE)} {colorize('failed', t['failures'], C.COLOR_ERROR)} "
|
||||||
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
|
f"{colorize('rescued', t['rescued'], C.COLOR_OK)} {colorize('ignored', t['ignored'], C.COLOR_WARN)}",
|
||||||
screen_only=True,
|
screen_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self._display.display(
|
self._display.display(
|
||||||
f" {hostcolor(h, t, False)} : {colorize('ok', t['ok'], None)} {colorize('changed', t['changed'], None)} "
|
f" {hostcolor(h, t, False)} : {colorize('ok', t['ok'], None)} {colorize('changed', t['changed'], None)} "
|
||||||
f"{colorize('unreachable', t['unreachable'], None)} {colorize('failed', t['failures'], None)} {colorize('rescued', t['rescued'], None)} "
|
f"{colorize('unreachable', t['unreachable'], None)} {colorize('failed', t['failures'], None)} {colorize('rescued', t['rescued'], None)} "
|
||||||
f"{colorize('ignored', t['ignored'], None)}",
|
f"{colorize('ignored', t['ignored'], None)}",
|
||||||
log_only=True,
|
log_only=True
|
||||||
)
|
)
|
||||||
if stats.custom and self.get_option("show_custom_stats"):
|
if stats.custom and self.get_option('show_custom_stats'):
|
||||||
self._display.banner("CUSTOM STATS: ")
|
self._display.banner("CUSTOM STATS: ")
|
||||||
# per host
|
# per host
|
||||||
# TODO: come up with 'pretty format'
|
# TODO: come up with 'pretty format'
|
||||||
for k in sorted(stats.custom.keys()):
|
for k in sorted(stats.custom.keys()):
|
||||||
if k == "_run":
|
if k == '_run':
|
||||||
continue
|
continue
|
||||||
stat_val = self._dump_results(stats.custom[k], indent=1).replace("\n", "")
|
stat_val = self._dump_results(stats.custom[k], indent=1).replace('\n', '')
|
||||||
self._display.display(f"\t{k}: {stat_val}")
|
self._display.display(f'\t{k}: {stat_val}')
|
||||||
|
|
||||||
# print per run custom stats
|
# print per run custom stats
|
||||||
if "_run" in stats.custom:
|
if '_run' in stats.custom:
|
||||||
self._display.display("", screen_only=True)
|
self._display.display("", screen_only=True)
|
||||||
stat_val_run = self._dump_results(stats.custom["_run"], indent=1).replace("\n", "")
|
stat_val_run = self._dump_results(stats.custom['_run'], indent=1).replace('\n', '')
|
||||||
self._display.display(f"\tRUN: {stat_val_run}")
|
self._display.display(f'\tRUN: {stat_val_run}')
|
||||||
self._display.display("", screen_only=True)
|
self._display.display("", screen_only=True)
|
||||||
|
|
||||||
def v2_playbook_on_no_hosts_matched(self):
|
def v2_playbook_on_no_hosts_matched(self):
|
||||||
@@ -231,24 +231,21 @@ class CallbackModule(CallbackModule_default):
|
|||||||
self._display.display(" Ran out of hosts!", color=C.COLOR_ERROR)
|
self._display.display(" Ran out of hosts!", color=C.COLOR_ERROR)
|
||||||
|
|
||||||
def v2_playbook_on_start(self, playbook):
|
def v2_playbook_on_start(self, playbook):
|
||||||
if context.CLIARGS["check"] and self.get_option("check_mode_markers"):
|
if context.CLIARGS['check'] and self.get_option('check_mode_markers'):
|
||||||
self._display.display(f"Executing playbook {basename(playbook._file_name)} in check mode")
|
self._display.display(f"Executing playbook {basename(playbook._file_name)} in check mode")
|
||||||
else:
|
else:
|
||||||
self._display.display(f"Executing playbook {basename(playbook._file_name)}")
|
self._display.display(f"Executing playbook {basename(playbook._file_name)}")
|
||||||
|
|
||||||
# show CLI arguments
|
# show CLI arguments
|
||||||
if self._display.verbosity > 3:
|
if self._display.verbosity > 3:
|
||||||
if context.CLIARGS.get("args"):
|
if context.CLIARGS.get('args'):
|
||||||
self._display.display(
|
self._display.display(f"Positional arguments: {' '.join(context.CLIARGS['args'])}",
|
||||||
f"Positional arguments: {' '.join(context.CLIARGS['args'])}",
|
color=C.COLOR_VERBOSE, screen_only=True)
|
||||||
color=C.COLOR_VERBOSE,
|
|
||||||
screen_only=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for argument in (a for a in context.CLIARGS if a != "args"):
|
for argument in (a for a in context.CLIARGS if a != 'args'):
|
||||||
val = context.CLIARGS[argument]
|
val = context.CLIARGS[argument]
|
||||||
if val:
|
if val:
|
||||||
self._display.vvvv(f"{argument}: {val}")
|
self._display.vvvv(f'{argument}: {val}')
|
||||||
|
|
||||||
def v2_runner_retry(self, result):
|
def v2_runner_retry(self, result):
|
||||||
msg = f" Retrying... ({result._result['attempts']} of {result._result['retries']})"
|
msg = f" Retrying... ({result._result['attempts']} of {result._result['retries']})"
|
||||||
|
|||||||
195
plugins/callback/yaml.py
Normal file
195
plugins/callback/yaml.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Make coding more python3-ish
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
author: Unknown (!UNKNOWN)
|
||||||
|
name: yaml
|
||||||
|
type: stdout
|
||||||
|
short_description: YAML-ized Ansible screen output
|
||||||
|
deprecated:
|
||||||
|
removed_in: 12.0.0
|
||||||
|
why: Starting in ansible-core 2.13, the P(ansible.builtin.default#callback) callback has support for printing output in
|
||||||
|
YAML format.
|
||||||
|
alternative: Use O(ansible.builtin.default#callback:result_format=yaml).
|
||||||
|
description:
|
||||||
|
- Ansible output that can be quite a bit easier to read than the default JSON formatting.
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- default_callback
|
||||||
|
requirements:
|
||||||
|
- set as stdout in configuration
|
||||||
|
seealso:
|
||||||
|
- plugin: ansible.builtin.default
|
||||||
|
plugin_type: callback
|
||||||
|
description: >-
|
||||||
|
There is a parameter O(ansible.builtin.default#callback:result_format) in P(ansible.builtin.default#callback) that allows
|
||||||
|
you to change the output format to YAML.
|
||||||
|
notes:
|
||||||
|
- With ansible-core 2.13 or newer, you can instead specify V(yaml) for the parameter O(ansible.builtin.default#callback:result_format)
|
||||||
|
in P(ansible.builtin.default#callback).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
|
from ansible.module_utils.common.text.converters import to_text
|
||||||
|
from ansible.plugins.callback import strip_internal_keys, module_response_deepcopy
|
||||||
|
from ansible.plugins.callback.default import CallbackModule as Default
|
||||||
|
|
||||||
|
|
||||||
|
# from http://stackoverflow.com/a/15423007/115478
|
||||||
|
def should_use_block(value):
|
||||||
|
"""Returns true if string should be in block format"""
|
||||||
|
for c in "\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029":
|
||||||
|
if c in value:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def adjust_str_value_for_block(value):
|
||||||
|
# we care more about readable than accuracy, so...
|
||||||
|
# ...no trailing space
|
||||||
|
value = value.rstrip()
|
||||||
|
# ...and non-printable characters
|
||||||
|
value = ''.join(x for x in value if x in string.printable or ord(x) >= 0xA0)
|
||||||
|
# ...tabs prevent blocks from expanding
|
||||||
|
value = value.expandtabs()
|
||||||
|
# ...and odd bits of whitespace
|
||||||
|
value = re.sub(r'[\x0b\x0c\r]', '', value)
|
||||||
|
# ...as does trailing space
|
||||||
|
value = re.sub(r' +\n', '\n', value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def create_string_node(tag, value, style, default_style):
|
||||||
|
if style is None:
|
||||||
|
if should_use_block(value):
|
||||||
|
style = '|'
|
||||||
|
value = adjust_str_value_for_block(value)
|
||||||
|
else:
|
||||||
|
style = default_style
|
||||||
|
return yaml.representer.ScalarNode(tag, value, style=style)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ansible.module_utils.common.yaml import HAS_LIBYAML
|
||||||
|
# import below was added in https://github.com/ansible/ansible/pull/85039,
|
||||||
|
# first contained in ansible-core 2.19.0b2:
|
||||||
|
from ansible.utils.vars import transform_to_native_types
|
||||||
|
|
||||||
|
if HAS_LIBYAML:
|
||||||
|
from yaml.cyaml import CSafeDumper as SafeDumper
|
||||||
|
else:
|
||||||
|
from yaml import SafeDumper
|
||||||
|
|
||||||
|
class MyDumper(SafeDumper):
|
||||||
|
def represent_scalar(self, tag, value, style=None):
|
||||||
|
"""Uses block style for multi-line strings"""
|
||||||
|
node = create_string_node(tag, value, style, self.default_style)
|
||||||
|
if self.alias_key is not None:
|
||||||
|
self.represented_objects[self.alias_key] = node
|
||||||
|
return node
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# In case transform_to_native_types cannot be imported, we either have ansible-core 2.19.0b1
|
||||||
|
# (or some random commit from the devel or stable-2.19 branch after merging the DT changes
|
||||||
|
# and before transform_to_native_types was added), or we have a version without the DT changes.
|
||||||
|
|
||||||
|
# Here we simply assume we have a version without the DT changes, and thus can continue as
|
||||||
|
# with ansible-core 2.18 and before.
|
||||||
|
|
||||||
|
transform_to_native_types = None
|
||||||
|
|
||||||
|
from ansible.parsing.yaml.dumper import AnsibleDumper
|
||||||
|
|
||||||
|
class MyDumper(AnsibleDumper): # pylint: disable=inherit-non-class
|
||||||
|
def represent_scalar(self, tag, value, style=None):
|
||||||
|
"""Uses block style for multi-line strings"""
|
||||||
|
node = create_string_node(tag, value, style, self.default_style)
|
||||||
|
if self.alias_key is not None:
|
||||||
|
self.represented_objects[self.alias_key] = node
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def transform_recursively(value, transform):
|
||||||
|
# Since 2.19.0b7, this should no longer be needed:
|
||||||
|
# https://github.com/ansible/ansible/issues/85325
|
||||||
|
# https://github.com/ansible/ansible/pull/85389
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
return {transform(k): transform(v) for k, v in value.items()}
|
||||||
|
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||||
|
return [transform(e) for e in value]
|
||||||
|
return transform(value)
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackModule(Default):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Variation of the Default output which uses nicely readable YAML instead
|
||||||
|
of JSON for printing results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CALLBACK_VERSION = 2.0
|
||||||
|
CALLBACK_TYPE = 'stdout'
|
||||||
|
CALLBACK_NAME = 'community.general.yaml'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(CallbackModule, self).__init__()
|
||||||
|
|
||||||
|
def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False):
|
||||||
|
if result.get('_ansible_no_log', False):
|
||||||
|
return json.dumps(dict(censored="The output has been hidden due to the fact that 'no_log: true' was specified for this result"))
|
||||||
|
|
||||||
|
# All result keys stating with _ansible_ are internal, so remove them from the result before we output anything.
|
||||||
|
abridged_result = strip_internal_keys(module_response_deepcopy(result))
|
||||||
|
|
||||||
|
# remove invocation unless specifically wanting it
|
||||||
|
if not keep_invocation and self._display.verbosity < 3 and 'invocation' in result:
|
||||||
|
del abridged_result['invocation']
|
||||||
|
|
||||||
|
# remove diff information from screen output
|
||||||
|
if self._display.verbosity < 3 and 'diff' in result:
|
||||||
|
del abridged_result['diff']
|
||||||
|
|
||||||
|
# remove exception from screen output
|
||||||
|
if 'exception' in abridged_result:
|
||||||
|
del abridged_result['exception']
|
||||||
|
|
||||||
|
dumped = ''
|
||||||
|
|
||||||
|
# put changed and skipped into a header line
|
||||||
|
if 'changed' in abridged_result:
|
||||||
|
dumped += f"changed={str(abridged_result['changed']).lower()} "
|
||||||
|
del abridged_result['changed']
|
||||||
|
|
||||||
|
if 'skipped' in abridged_result:
|
||||||
|
dumped += f"skipped={str(abridged_result['skipped']).lower()} "
|
||||||
|
del abridged_result['skipped']
|
||||||
|
|
||||||
|
# if we already have stdout, we don't need stdout_lines
|
||||||
|
if 'stdout' in abridged_result and 'stdout_lines' in abridged_result:
|
||||||
|
abridged_result['stdout_lines'] = '<omitted>'
|
||||||
|
|
||||||
|
# if we already have stderr, we don't need stderr_lines
|
||||||
|
if 'stderr' in abridged_result and 'stderr_lines' in abridged_result:
|
||||||
|
abridged_result['stderr_lines'] = '<omitted>'
|
||||||
|
|
||||||
|
if abridged_result:
|
||||||
|
dumped += '\n'
|
||||||
|
if transform_to_native_types is not None:
|
||||||
|
abridged_result = transform_recursively(abridged_result, lambda v: transform_to_native_types(v, redact=False))
|
||||||
|
dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=MyDumper, default_flow_style=False))
|
||||||
|
|
||||||
|
# indent by a couple of spaces
|
||||||
|
dumped = '\n '.join(dumped.split('\n')).rstrip()
|
||||||
|
return dumped
|
||||||
|
|
||||||
|
def _serialize_diff(self, diff):
|
||||||
|
return to_text(yaml.dump(diff, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
#
|
#
|
||||||
# (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
# (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||||
@@ -87,19 +88,19 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Local chroot based connections"""
|
""" Local chroot based connections """
|
||||||
|
|
||||||
transport = "community.general.chroot"
|
transport = 'community.general.chroot'
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
# su currently has an undiagnosed issue with calculating the file
|
# su currently has an undiagnosed issue with calculating the file
|
||||||
# checksums (so copy, for instance, doesn't work right)
|
# checksums (so copy, for instance, doesn't work right)
|
||||||
# Have to look into that before re-enabling this
|
# Have to look into that before re-enabling this
|
||||||
has_tty = False
|
has_tty = False
|
||||||
|
|
||||||
default_user = "root"
|
default_user = 'root'
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self.chroot = self._play_context.remote_addr
|
self.chroot = self._play_context.remote_addr
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class Connection(ConnectionBase):
|
|||||||
if not os.path.isdir(self.chroot):
|
if not os.path.isdir(self.chroot):
|
||||||
raise AnsibleError(f"{self.chroot} is not a directory")
|
raise AnsibleError(f"{self.chroot} is not a directory")
|
||||||
|
|
||||||
chrootsh = os.path.join(self.chroot, "bin/sh")
|
chrootsh = os.path.join(self.chroot, 'bin/sh')
|
||||||
# Want to check for a usable bourne shell inside the chroot.
|
# Want to check for a usable bourne shell inside the chroot.
|
||||||
# is_executable() == True is sufficient. For symlinks it
|
# is_executable() == True is sufficient. For symlinks it
|
||||||
# gets really complicated really fast. So we punt on finding that
|
# gets really complicated really fast. So we punt on finding that
|
||||||
@@ -116,46 +117,46 @@ class Connection(ConnectionBase):
|
|||||||
raise AnsibleError(f"{self.chroot} does not look like a chrootable dir (/bin/sh missing)")
|
raise AnsibleError(f"{self.chroot} does not look like a chrootable dir (/bin/sh missing)")
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to the chroot"""
|
""" connect to the chroot """
|
||||||
if not self.get_option("disable_root_check") and os.geteuid() != 0:
|
if not self.get_option('disable_root_check') and os.geteuid() != 0:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
"chroot connection requires running as root. "
|
"chroot connection requires running as root. "
|
||||||
"You can override this check with the `disable_root_check` option."
|
"You can override this check with the `disable_root_check` option.")
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.isabs(self.get_option("chroot_exe")):
|
if os.path.isabs(self.get_option('chroot_exe')):
|
||||||
self.chroot_cmd = self.get_option("chroot_exe")
|
self.chroot_cmd = self.get_option('chroot_exe')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.chroot_cmd = get_bin_path(self.get_option("chroot_exe"))
|
self.chroot_cmd = get_bin_path(self.get_option('chroot_exe'))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise AnsibleError(str(e)) from e
|
raise AnsibleError(str(e))
|
||||||
|
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
display.vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
|
display.vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
||||||
"""run a command on the chroot. This is only needed for implementing
|
""" run a command on the chroot. This is only needed for implementing
|
||||||
put_file() get_file() so that we don't have to read the whole file
|
put_file() get_file() so that we don't have to read the whole file
|
||||||
into memory.
|
into memory.
|
||||||
|
|
||||||
compared to exec_command() it looses some niceties like being able to
|
compared to exec_command() it looses some niceties like being able to
|
||||||
return the process's exit code immediately.
|
return the process's exit code immediately.
|
||||||
"""
|
"""
|
||||||
executable = self.get_option("executable")
|
executable = self.get_option('executable')
|
||||||
local_cmd = [self.chroot_cmd, self.chroot, executable, "-c", cmd]
|
local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
|
||||||
|
|
||||||
display.vvv(f"EXEC {local_cmd}", host=self.chroot)
|
display.vvv(f"EXEC {local_cmd}", host=self.chroot)
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""run a command on the chroot"""
|
""" run a command on the chroot """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
p = self._buffered_exec_command(cmd)
|
p = self._buffered_exec_command(cmd)
|
||||||
|
|
||||||
@@ -164,70 +165,70 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prefix_login_path(remote_path):
|
def _prefix_login_path(remote_path):
|
||||||
"""Make sure that we put files into a standard path
|
""" Make sure that we put files into a standard path
|
||||||
|
|
||||||
If a path is relative, then we need to choose where to put it.
|
If a path is relative, then we need to choose where to put it.
|
||||||
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
||||||
exist in any given chroot. So for now we're choosing "/" instead.
|
exist in any given chroot. So for now we're choosing "/" instead.
|
||||||
This also happens to be the former default.
|
This also happens to be the former default.
|
||||||
|
|
||||||
Can revisit using $HOME instead if it is a problem
|
Can revisit using $HOME instead if it is a problem
|
||||||
"""
|
"""
|
||||||
if not remote_path.startswith(os.path.sep):
|
if not remote_path.startswith(os.path.sep):
|
||||||
remote_path = os.path.join(os.path.sep, remote_path)
|
remote_path = os.path.join(os.path.sep, remote_path)
|
||||||
return os.path.normpath(remote_path)
|
return os.path.normpath(remote_path)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to chroot"""
|
""" transfer a file from local to chroot """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.chroot)
|
display.vvv(f"PUT {in_path} TO {out_path}", host=self.chroot)
|
||||||
|
|
||||||
out_path = shlex_quote(self._prefix_login_path(out_path))
|
out_path = shlex_quote(self._prefix_login_path(out_path))
|
||||||
try:
|
try:
|
||||||
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
|
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
|
||||||
if not os.fstat(in_file.fileno()).st_size:
|
if not os.fstat(in_file.fileno()).st_size:
|
||||||
count = " count=0"
|
count = ' count=0'
|
||||||
else:
|
else:
|
||||||
count = ""
|
count = ''
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
|
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("chroot connection requires dd command in the chroot") from e
|
raise AnsibleError("chroot connection requires dd command in the chroot")
|
||||||
try:
|
try:
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
||||||
except IOError as e:
|
except IOError:
|
||||||
raise AnsibleError(f"file or module does not exist at: {in_path}") from e
|
raise AnsibleError(f"file or module does not exist at: {in_path}")
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from chroot to local"""
|
""" fetch a file from chroot to local """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.chroot)
|
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.chroot)
|
||||||
|
|
||||||
in_path = shlex_quote(self._prefix_login_path(in_path))
|
in_path = shlex_quote(self._prefix_login_path(in_path))
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
|
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("chroot connection requires dd command in the chroot") from e
|
raise AnsibleError("chroot connection requires dd command in the chroot")
|
||||||
|
|
||||||
with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb+") as out_file:
|
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file:
|
||||||
try:
|
try:
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
while chunk:
|
while chunk:
|
||||||
out_file.write(chunk)
|
out_file.write(chunk)
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
""" terminate the connection; nothing to do here """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||||
# Copyright (c) 2013, Michael Scherer <misc@zarb.org>
|
# Copyright (c) 2013, Michael Scherer <misc@zarb.org>
|
||||||
@@ -29,7 +30,6 @@ options:
|
|||||||
HAVE_FUNC = False
|
HAVE_FUNC = False
|
||||||
try:
|
try:
|
||||||
import func.overlord.client as fc
|
import func.overlord.client as fc
|
||||||
|
|
||||||
HAVE_FUNC = True
|
HAVE_FUNC = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@@ -46,7 +46,7 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Func-based connections"""
|
""" Func-based connections """
|
||||||
|
|
||||||
has_pipelining = False
|
has_pipelining = False
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class Connection(ConnectionBase):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||||
"""run a command on the remote minion"""
|
""" run a command on the remote minion """
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
@@ -83,16 +83,16 @@ class Connection(ConnectionBase):
|
|||||||
return os.path.join(prefix, normpath[1:])
|
return os.path.join(prefix, normpath[1:])
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to remote"""
|
""" transfer a file from local to remote """
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, "/")
|
out_path = self._normalize_path(out_path, '/')
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
|
display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
|
||||||
self.client.local.copyfile.send(in_path, out_path)
|
self.client.local.copyfile.send(in_path, out_path)
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from remote to local"""
|
""" fetch a file from remote to local """
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, "/")
|
in_path = self._normalize_path(in_path, '/')
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
|
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
|
||||||
# need to use a tmp dir due to difference of semantic for getfile
|
# need to use a tmp dir due to difference of semantic for getfile
|
||||||
# ( who take a # directory as destination) and fetch_file, who
|
# ( who take a # directory as destination) and fetch_file, who
|
||||||
@@ -103,5 +103,5 @@ class Connection(ConnectionBase):
|
|||||||
shutil.rmtree(tmpdir)
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
""" terminate the connection; nothing to do here """
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on lxd.py (c) 2016, Matt Clay <matt@mystile.com>
|
# Based on lxd.py (c) 2016, Matt Clay <matt@mystile.com>
|
||||||
# (c) 2023, Stephane Graber <stgraber@stgraber.org>
|
# (c) 2023, Stephane Graber <stgraber@stgraber.org>
|
||||||
# Copyright (c) 2023 Ansible Project
|
# Copyright (c) 2023 Ansible Project
|
||||||
@@ -13,9 +14,6 @@ short_description: Run tasks in Incus instances using the Incus CLI
|
|||||||
description:
|
description:
|
||||||
- Run commands or put/fetch files to an existing Incus instance using Incus CLI.
|
- Run commands or put/fetch files to an existing Incus instance using Incus CLI.
|
||||||
version_added: "8.2.0"
|
version_added: "8.2.0"
|
||||||
notes:
|
|
||||||
- When using this collection for Windows virtual machines, set C(ansible_shell_type) to C(powershell) or C(cmd) as a variable to the host in
|
|
||||||
the inventory.
|
|
||||||
options:
|
options:
|
||||||
remote_addr:
|
remote_addr:
|
||||||
description:
|
description:
|
||||||
@@ -78,7 +76,6 @@ options:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from subprocess import call, Popen, PIPE
|
from subprocess import call, Popen, PIPE
|
||||||
|
|
||||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||||
@@ -88,116 +85,69 @@ from ansible.plugins.connection import ConnectionBase
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Incus based connections"""
|
""" Incus based connections """
|
||||||
|
|
||||||
transport = "incus"
|
transport = "incus"
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self._incus_cmd = get_bin_path("incus")
|
self._incus_cmd = get_bin_path("incus")
|
||||||
|
|
||||||
if not self._incus_cmd:
|
if not self._incus_cmd:
|
||||||
raise AnsibleError("incus command not found in PATH")
|
raise AnsibleError("incus command not found in PATH")
|
||||||
|
|
||||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
|
||||||
# Initializing regular expression patterns to match on a PowerShell or cmd command line.
|
|
||||||
self.powershell_regex_pattern = re.compile(
|
|
||||||
r"^(?P<executable>(\"?([a-z]:)?[a-z0-9 ()\\.]+)?powershell(\.exe)?\"?|(([a-z]:)?[a-z0-9()\\.]+)?powershell(\.exe)?)\s+.*(?P<command>-c(ommand)?)\s+", # noqa: E501
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
self.cmd_regex_pattern = re.compile(
|
|
||||||
r"^(?P<executable>(\"?([a-z]:)?[a-z0-9 ()\\.]+)?cmd(\.exe)?\"?|(([a-z]:)?[a-z0-9()\\.]+)?cmd(\.exe)?)\s+.*(?P<command>/c)\s+",
|
|
||||||
re.IGNORECASE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Basic setup for a Windows host.
|
|
||||||
self.has_native_async = True
|
|
||||||
self.always_pipeline_modules = True
|
|
||||||
self.module_implementation_preferences = (".ps1", ".exe", "")
|
|
||||||
self.allow_executable = False
|
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to Incus (nothing to do here)"""
|
"""connect to Incus (nothing to do here) """
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
|
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
self._display.vvv(
|
self._display.vvv(f"ESTABLISH Incus CONNECTION FOR USER: {self.get_option('remote_user')}",
|
||||||
f"ESTABLISH Incus CONNECTION FOR USER: {self.get_option('remote_user')}", host=self._instance()
|
host=self._instance())
|
||||||
)
|
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def _build_command(self, cmd) -> list[str]:
|
def _build_command(self, cmd) -> str:
|
||||||
"""build the command to execute on the incus host"""
|
"""build the command to execute on the incus host"""
|
||||||
|
|
||||||
exec_cmd: list[str] = [
|
exec_cmd = [
|
||||||
self._incus_cmd,
|
self._incus_cmd,
|
||||||
"--project",
|
"--project", self.get_option("project"),
|
||||||
self.get_option("project"),
|
|
||||||
"exec",
|
"exec",
|
||||||
*(["-T"] if getattr(self._shell, "_IS_WINDOWS", False) else []),
|
|
||||||
f"{self.get_option('remote')}:{self._instance()}",
|
f"{self.get_option('remote')}:{self._instance()}",
|
||||||
"--",
|
"--"]
|
||||||
]
|
|
||||||
|
|
||||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
if self.get_option("remote_user") != "root":
|
||||||
if (
|
self._display.vvv(
|
||||||
(regex_match := self.powershell_regex_pattern.match(cmd))
|
f"INFO: Running as non-root user: {self.get_option('remote_user')}, \
|
||||||
and (regex_pattern := self.powershell_regex_pattern)
|
trying to run 'incus exec' with become method: {self.get_option('incus_become_method')}",
|
||||||
) or ((regex_match := self.cmd_regex_pattern.match(cmd)) and (regex_pattern := self.cmd_regex_pattern)):
|
host=self._instance(),
|
||||||
self._display.vvvvvv(
|
)
|
||||||
f'Found keyword: "{regex_match.group("command")}" based on regex: {regex_pattern.pattern}',
|
exec_cmd.extend(
|
||||||
host=self._instance(),
|
[self.get_option("incus_become_method"), self.get_option("remote_user"), "-c"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Split the command on the argument -c(ommand) for PowerShell or /c for cmd.
|
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
|
||||||
before_command_argument, after_command_argument = cmd.split(regex_match.group("command"), 1)
|
|
||||||
|
|
||||||
exec_cmd.extend(
|
|
||||||
[
|
|
||||||
# To avoid splitting on a space contained in the path, set the executable as the first argument.
|
|
||||||
regex_match.group("executable").strip('"'),
|
|
||||||
# Remove the executable path and split the rest by space.
|
|
||||||
*(before_command_argument[len(regex_match.group("executable")) :].lstrip().split(" ")),
|
|
||||||
# Set the command argument depending on cmd or powershell.
|
|
||||||
regex_match.group("command"),
|
|
||||||
# Add the rest of the command at the end.
|
|
||||||
after_command_argument,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# For anything else using -EncodedCommand or else, just split on space.
|
|
||||||
exec_cmd.extend(cmd.split(" "))
|
|
||||||
else:
|
|
||||||
if self.get_option("remote_user") != "root":
|
|
||||||
self._display.vvv(
|
|
||||||
f"INFO: Running as non-root user: {self.get_option('remote_user')}, \
|
|
||||||
trying to run 'incus exec' with become method: {self.get_option('incus_become_method')}",
|
|
||||||
host=self._instance(),
|
|
||||||
)
|
|
||||||
exec_cmd.extend([self.get_option("incus_become_method"), self.get_option("remote_user"), "-c"])
|
|
||||||
|
|
||||||
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
|
|
||||||
|
|
||||||
return exec_cmd
|
return exec_cmd
|
||||||
|
|
||||||
def _instance(self):
|
def _instance(self):
|
||||||
# Return only the leading part of the FQDN as the instance name
|
# Return only the leading part of the FQDN as the instance name
|
||||||
# as Incus instance names cannot be a FQDN.
|
# as Incus instance names cannot be a FQDN.
|
||||||
return self.get_option("remote_addr").split(".")[0]
|
return self.get_option('remote_addr').split(".")[0]
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||||
"""execute a command on the Incus host"""
|
""" execute a command on the Incus host """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
self._display.vvv(f"EXEC {cmd}", host=self._instance())
|
self._display.vvv(f"EXEC {cmd}",
|
||||||
|
host=self._instance())
|
||||||
|
|
||||||
local_cmd = self._build_command(cmd)
|
local_cmd = self._build_command(cmd)
|
||||||
self._display.vvvvv(f"EXEC {local_cmd}", host=self._instance())
|
self._display.vvvvv(f"EXEC {local_cmd}", host=self._instance())
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
in_data = to_bytes(in_data, errors="surrogate_or_strict", nonstring="passthru")
|
in_data = to_bytes(in_data, errors='surrogate_or_strict', nonstring='passthru')
|
||||||
|
|
||||||
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
stdout, stderr = process.communicate(in_data)
|
stdout, stderr = process.communicate(in_data)
|
||||||
@@ -205,22 +155,32 @@ class Connection(ConnectionBase):
|
|||||||
stdout = to_text(stdout)
|
stdout = to_text(stdout)
|
||||||
stderr = to_text(stderr)
|
stderr = to_text(stderr)
|
||||||
|
|
||||||
if stderr.startswith("Error: ") and stderr.rstrip().endswith(": Instance is not running"):
|
if stderr.startswith("Error: ") and stderr.rstrip().endswith(
|
||||||
|
": Instance is not running"
|
||||||
|
):
|
||||||
raise AnsibleConnectionFailure(
|
raise AnsibleConnectionFailure(
|
||||||
f"instance not running: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
f"instance not running: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
||||||
)
|
)
|
||||||
|
|
||||||
if stderr.startswith("Error: ") and stderr.rstrip().endswith(": Instance not found"):
|
if stderr.startswith("Error: ") and stderr.rstrip().endswith(
|
||||||
|
": Instance not found"
|
||||||
|
):
|
||||||
raise AnsibleConnectionFailure(
|
raise AnsibleConnectionFailure(
|
||||||
f"instance not found: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
f"instance not found: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
||||||
)
|
)
|
||||||
|
|
||||||
if stderr.startswith("Error: ") and ": User does not have permission " in stderr:
|
if (
|
||||||
|
stderr.startswith("Error: ")
|
||||||
|
and ": User does not have permission " in stderr
|
||||||
|
):
|
||||||
raise AnsibleConnectionFailure(
|
raise AnsibleConnectionFailure(
|
||||||
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
||||||
)
|
)
|
||||||
|
|
||||||
if stderr.startswith("Error: ") and ": User does not have entitlement " in stderr:
|
if (
|
||||||
|
stderr.startswith("Error: ")
|
||||||
|
and ": User does not have entitlement " in stderr
|
||||||
|
):
|
||||||
raise AnsibleConnectionFailure(
|
raise AnsibleConnectionFailure(
|
||||||
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
f"instance access denied: {self._instance()} (remote={self.get_option('remote')}, project={self.get_option('project')})"
|
||||||
)
|
)
|
||||||
@@ -232,26 +192,31 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
rc, uid_out, err = self.exec_command("/bin/id -u")
|
rc, uid_out, err = self.exec_command("/bin/id -u")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise AnsibleError(f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}")
|
raise AnsibleError(
|
||||||
|
f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}"
|
||||||
|
)
|
||||||
uid = uid_out.strip()
|
uid = uid_out.strip()
|
||||||
|
|
||||||
rc, gid_out, err = self.exec_command("/bin/id -g")
|
rc, gid_out, err = self.exec_command("/bin/id -g")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise AnsibleError(f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}")
|
raise AnsibleError(
|
||||||
|
f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}"
|
||||||
|
)
|
||||||
gid = gid_out.strip()
|
gid = gid_out.strip()
|
||||||
|
|
||||||
return int(uid), int(gid)
|
return int(uid), int(gid)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""put a file from local to Incus"""
|
""" put a file from local to Incus """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
|
|
||||||
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self._instance())
|
self._display.vvv(f"PUT {in_path} TO {out_path}",
|
||||||
|
host=self._instance())
|
||||||
|
|
||||||
if not os.path.isfile(to_bytes(in_path, errors="surrogate_or_strict")):
|
if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
|
||||||
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
|
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
|
||||||
|
|
||||||
if not getattr(self._shell, "_IS_WINDOWS", False) and self.get_option("remote_user") != "root":
|
if self.get_option("remote_user") != "root":
|
||||||
uid, gid = self._get_remote_uid_gid()
|
uid, gid = self._get_remote_uid_gid()
|
||||||
local_cmd = [
|
local_cmd = [
|
||||||
self._incus_cmd,
|
self._incus_cmd,
|
||||||
@@ -281,33 +246,30 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
self._display.vvvvv(f"PUT {local_cmd}", host=self._instance())
|
self._display.vvvvv(f"PUT {local_cmd}", host=self._instance())
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
|
|
||||||
call(local_cmd)
|
call(local_cmd)
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from Incus to local"""
|
""" fetch a file from Incus to local """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
|
|
||||||
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self._instance())
|
self._display.vvv(f"FETCH {in_path} TO {out_path}",
|
||||||
|
host=self._instance())
|
||||||
|
|
||||||
local_cmd = [
|
local_cmd = [
|
||||||
self._incus_cmd,
|
self._incus_cmd,
|
||||||
"--project",
|
"--project", self.get_option("project"),
|
||||||
self.get_option("project"),
|
"file", "pull", "--quiet",
|
||||||
"file",
|
|
||||||
"pull",
|
|
||||||
"--quiet",
|
|
||||||
f"{self.get_option('remote')}:{self._instance()}/{in_path}",
|
f"{self.get_option('remote')}:{self._instance()}/{in_path}",
|
||||||
out_path,
|
out_path]
|
||||||
]
|
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
|
|
||||||
call(local_cmd)
|
call(local_cmd)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""close the connection (nothing to do here)"""
|
""" close the connection (nothing to do here) """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
|
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on jail.py
|
# Based on jail.py
|
||||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||||
# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
|
# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
|
||||||
@@ -42,33 +43,31 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class Connection(Jail):
|
class Connection(Jail):
|
||||||
"""Local iocage based connections"""
|
""" Local iocage based connections """
|
||||||
|
|
||||||
transport = "community.general.iocage"
|
transport = 'community.general.iocage'
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
self.ioc_jail = play_context.remote_addr
|
self.ioc_jail = play_context.remote_addr
|
||||||
|
|
||||||
self.iocage_cmd = Jail._search_executable("iocage")
|
self.iocage_cmd = Jail._search_executable('iocage')
|
||||||
|
|
||||||
jail_uuid = self.get_jail_uuid()
|
jail_uuid = self.get_jail_uuid()
|
||||||
|
|
||||||
kwargs[Jail.modified_jailname_key] = f"ioc-{jail_uuid}"
|
kwargs[Jail.modified_jailname_key] = f'ioc-{jail_uuid}'
|
||||||
|
|
||||||
display.vvv(
|
display.vvv(
|
||||||
f"Jail {self.ioc_jail} has been translated to {kwargs[Jail.modified_jailname_key]}",
|
f"Jail {self.ioc_jail} has been translated to {kwargs[Jail.modified_jailname_key]}",
|
||||||
host=kwargs[Jail.modified_jailname_key],
|
host=kwargs[Jail.modified_jailname_key]
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
def get_jail_uuid(self):
|
def get_jail_uuid(self):
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen([self.iocage_cmd, 'get', 'host_hostuuid', self.ioc_jail],
|
||||||
[self.iocage_cmd, "get", "host_hostuuid", self.ioc_jail],
|
stdin=subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stderr=subprocess.STDOUT)
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
)
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
|
|
||||||
@@ -84,4 +83,4 @@ class Connection(Jail):
|
|||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(f"iocage returned an error: {stdout}")
|
raise AnsibleError(f"iocage returned an error: {stdout}")
|
||||||
|
|
||||||
return stdout.strip("\n")
|
return stdout.strip('\n')
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on local.py by Michael DeHaan <michael.dehaan@gmail.com>
|
# Based on local.py by Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# and chroot.py by Maykel Moya <mmoya@speedyrails.com>
|
# and chroot.py by Maykel Moya <mmoya@speedyrails.com>
|
||||||
# Copyright (c) 2013, Michael Scherer <misc@zarb.org>
|
# Copyright (c) 2013, Michael Scherer <misc@zarb.org>
|
||||||
@@ -49,18 +50,18 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Local BSD Jail based connections"""
|
""" Local BSD Jail based connections """
|
||||||
|
|
||||||
modified_jailname_key = "conn_jail_name"
|
modified_jailname_key = 'conn_jail_name'
|
||||||
|
|
||||||
transport = "community.general.jail"
|
transport = 'community.general.jail'
|
||||||
# Pipelining may work. Someone needs to test by setting this to True and
|
# Pipelining may work. Someone needs to test by setting this to True and
|
||||||
# having pipelining=True in their ansible.cfg
|
# having pipelining=True in their ansible.cfg
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
has_tty = False
|
has_tty = False
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self.jail = self._play_context.remote_addr
|
self.jail = self._play_context.remote_addr
|
||||||
if self.modified_jailname_key in kwargs:
|
if self.modified_jailname_key in kwargs:
|
||||||
@@ -69,8 +70,8 @@ class Connection(ConnectionBase):
|
|||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
raise AnsibleError("jail connection requires running as root")
|
raise AnsibleError("jail connection requires running as root")
|
||||||
|
|
||||||
self.jls_cmd = self._search_executable("jls")
|
self.jls_cmd = self._search_executable('jls')
|
||||||
self.jexec_cmd = self._search_executable("jexec")
|
self.jexec_cmd = self._search_executable('jexec')
|
||||||
|
|
||||||
if self.jail not in self.list_jails():
|
if self.jail not in self.list_jails():
|
||||||
raise AnsibleError(f"incorrect jail name {self.jail}")
|
raise AnsibleError(f"incorrect jail name {self.jail}")
|
||||||
@@ -79,27 +80,27 @@ class Connection(ConnectionBase):
|
|||||||
def _search_executable(executable):
|
def _search_executable(executable):
|
||||||
try:
|
try:
|
||||||
return get_bin_path(executable)
|
return get_bin_path(executable)
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raise AnsibleError(f"{executable} command not found in PATH") from e
|
raise AnsibleError(f"{executable} command not found in PATH")
|
||||||
|
|
||||||
def list_jails(self):
|
def list_jails(self):
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
|
||||||
[self.jls_cmd, "-q", "name"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
stdin=subprocess.PIPE,
|
||||||
)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
|
|
||||||
return to_text(stdout, errors="surrogate_or_strict").split()
|
return to_text(stdout, errors='surrogate_or_strict').split()
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to the jail; nothing to do here"""
|
""" connect to the jail; nothing to do here """
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
display.vvv(f"ESTABLISH JAIL CONNECTION FOR USER: {self._play_context.remote_user}", host=self.jail)
|
display.vvv(f"ESTABLISH JAIL CONNECTION FOR USER: {self._play_context.remote_user}", host=self.jail)
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
||||||
"""run a command on the jail. This is only needed for implementing
|
""" run a command on the jail. This is only needed for implementing
|
||||||
put_file() get_file() so that we don't have to read the whole file
|
put_file() get_file() so that we don't have to read the whole file
|
||||||
into memory.
|
into memory.
|
||||||
|
|
||||||
@@ -108,24 +109,25 @@ class Connection(ConnectionBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
local_cmd = [self.jexec_cmd]
|
local_cmd = [self.jexec_cmd]
|
||||||
set_env = ""
|
set_env = ''
|
||||||
|
|
||||||
if self._play_context.remote_user is not None:
|
if self._play_context.remote_user is not None:
|
||||||
local_cmd += ["-U", self._play_context.remote_user]
|
local_cmd += ['-U', self._play_context.remote_user]
|
||||||
# update HOME since -U does not update the jail environment
|
# update HOME since -U does not update the jail environment
|
||||||
set_env = f"HOME=~{self._play_context.remote_user} "
|
set_env = f"HOME=~{self._play_context.remote_user} "
|
||||||
|
|
||||||
local_cmd += [self.jail, self._play_context.executable, "-c", set_env + cmd]
|
local_cmd += [self.jail, self._play_context.executable, '-c', set_env + cmd]
|
||||||
|
|
||||||
display.vvv(f"EXEC {local_cmd}", host=self.jail)
|
display.vvv(f"EXEC {local_cmd}", host=self.jail)
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""run a command on the jail"""
|
""" run a command on the jail """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
p = self._buffered_exec_command(cmd)
|
p = self._buffered_exec_command(cmd)
|
||||||
|
|
||||||
@@ -134,74 +136,70 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prefix_login_path(remote_path):
|
def _prefix_login_path(remote_path):
|
||||||
"""Make sure that we put files into a standard path
|
""" Make sure that we put files into a standard path
|
||||||
|
|
||||||
If a path is relative, then we need to choose where to put it.
|
If a path is relative, then we need to choose where to put it.
|
||||||
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
||||||
exist in any given chroot. So for now we're choosing "/" instead.
|
exist in any given chroot. So for now we're choosing "/" instead.
|
||||||
This also happens to be the former default.
|
This also happens to be the former default.
|
||||||
|
|
||||||
Can revisit using $HOME instead if it is a problem
|
Can revisit using $HOME instead if it is a problem
|
||||||
"""
|
"""
|
||||||
if not remote_path.startswith(os.path.sep):
|
if not remote_path.startswith(os.path.sep):
|
||||||
remote_path = os.path.join(os.path.sep, remote_path)
|
remote_path = os.path.join(os.path.sep, remote_path)
|
||||||
return os.path.normpath(remote_path)
|
return os.path.normpath(remote_path)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to jail"""
|
""" transfer a file from local to jail """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.jail)
|
display.vvv(f"PUT {in_path} TO {out_path}", host=self.jail)
|
||||||
|
|
||||||
out_path = shlex_quote(self._prefix_login_path(out_path))
|
out_path = shlex_quote(self._prefix_login_path(out_path))
|
||||||
try:
|
try:
|
||||||
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
|
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
|
||||||
if not os.fstat(in_file.fileno()).st_size:
|
if not os.fstat(in_file.fileno()).st_size:
|
||||||
count = " count=0"
|
count = ' count=0'
|
||||||
else:
|
else:
|
||||||
count = ""
|
count = ''
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
|
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("jail connection requires dd command in the jail") from e
|
raise AnsibleError("jail connection requires dd command in the jail")
|
||||||
try:
|
try:
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}")
|
||||||
f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}"
|
except IOError:
|
||||||
)
|
raise AnsibleError(f"file or module does not exist at: {in_path}")
|
||||||
except IOError as e:
|
|
||||||
raise AnsibleError(f"file or module does not exist at: {in_path}") from e
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from jail to local"""
|
""" fetch a file from jail to local """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.jail)
|
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.jail)
|
||||||
|
|
||||||
in_path = shlex_quote(self._prefix_login_path(in_path))
|
in_path = shlex_quote(self._prefix_login_path(in_path))
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
|
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("jail connection requires dd command in the jail") from e
|
raise AnsibleError("jail connection requires dd command in the jail")
|
||||||
|
|
||||||
with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb+") as out_file:
|
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb+') as out_file:
|
||||||
try:
|
try:
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
while chunk:
|
while chunk:
|
||||||
out_file.write(chunk)
|
out_file.write(chunk)
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}")
|
||||||
f"failed to transfer file {in_path} to {out_path}:\n{to_native(stdout)}\n{to_native(stderr)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
""" terminate the connection; nothing to do here """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# (c) 2015, Joerg Thalheim <joerg@higgsboson.tk>
|
# (c) 2015, Joerg Thalheim <joerg@higgsboson.tk>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -41,7 +42,6 @@ import errno
|
|||||||
HAS_LIBLXC = False
|
HAS_LIBLXC = False
|
||||||
try:
|
try:
|
||||||
import lxc as _lxc
|
import lxc as _lxc
|
||||||
|
|
||||||
HAS_LIBLXC = True
|
HAS_LIBLXC = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@@ -52,27 +52,27 @@ from ansible.plugins.connection import ConnectionBase
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Local lxc based connections"""
|
""" Local lxc based connections """
|
||||||
|
|
||||||
transport = "community.general.lxc"
|
transport = 'community.general.lxc'
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
default_user = "root"
|
default_user = 'root'
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self.container_name = None
|
self.container_name = None
|
||||||
self.container = None
|
self.container = None
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to the lxc; nothing to do here"""
|
""" connect to the lxc; nothing to do here """
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
|
|
||||||
if not HAS_LIBLXC:
|
if not HAS_LIBLXC:
|
||||||
msg = "lxc python bindings are not installed"
|
msg = "lxc python bindings are not installed"
|
||||||
raise errors.AnsibleError(msg)
|
raise errors.AnsibleError(msg)
|
||||||
|
|
||||||
container_name = self.get_option("remote_addr")
|
container_name = self.get_option('remote_addr')
|
||||||
if self.container and self.container_name == container_name:
|
if self.container and self.container_name == container_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class Connection(ConnectionBase):
|
|||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
for fd in ready_writes:
|
for fd in ready_writes:
|
||||||
in_data = in_data[os.write(fd, in_data) :]
|
in_data = in_data[os.write(fd, in_data):]
|
||||||
if len(in_data) == 0:
|
if len(in_data) == 0:
|
||||||
write_fds.remove(fd)
|
write_fds.remove(fd)
|
||||||
for fd in ready_reads:
|
for fd in ready_reads:
|
||||||
@@ -118,12 +118,12 @@ class Connection(ConnectionBase):
|
|||||||
return fd
|
return fd
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""run a command on the chroot"""
|
""" run a command on the chroot """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
# python2-lxc needs bytes. python3-lxc needs text.
|
# python2-lxc needs bytes. python3-lxc needs text.
|
||||||
executable = to_native(self.get_option("executable"), errors="surrogate_or_strict")
|
executable = to_native(self.get_option('executable'), errors='surrogate_or_strict')
|
||||||
local_cmd = [executable, "-c", to_native(cmd, errors="surrogate_or_strict")]
|
local_cmd = [executable, '-c', to_native(cmd, errors='surrogate_or_strict')]
|
||||||
|
|
||||||
read_stdout, write_stdout = None, None
|
read_stdout, write_stdout = None, None
|
||||||
read_stderr, write_stderr = None, None
|
read_stderr, write_stderr = None, None
|
||||||
@@ -134,14 +134,14 @@ class Connection(ConnectionBase):
|
|||||||
read_stderr, write_stderr = os.pipe()
|
read_stderr, write_stderr = os.pipe()
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"stdout": self._set_nonblocking(write_stdout),
|
'stdout': self._set_nonblocking(write_stdout),
|
||||||
"stderr": self._set_nonblocking(write_stderr),
|
'stderr': self._set_nonblocking(write_stderr),
|
||||||
"env_policy": _lxc.LXC_ATTACH_CLEAR_ENV,
|
'env_policy': _lxc.LXC_ATTACH_CLEAR_ENV
|
||||||
}
|
}
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
read_stdin, write_stdin = os.pipe()
|
read_stdin, write_stdin = os.pipe()
|
||||||
kwargs["stdin"] = self._set_nonblocking(read_stdin)
|
kwargs['stdin'] = self._set_nonblocking(read_stdin)
|
||||||
|
|
||||||
self._display.vvv(f"EXEC {local_cmd}", host=self.container_name)
|
self._display.vvv(f"EXEC {local_cmd}", host=self.container_name)
|
||||||
pid = self.container.attach(_lxc.attach_run_command, local_cmd, **kwargs)
|
pid = self.container.attach(_lxc.attach_run_command, local_cmd, **kwargs)
|
||||||
@@ -154,77 +154,82 @@ class Connection(ConnectionBase):
|
|||||||
if read_stdin:
|
if read_stdin:
|
||||||
read_stdin = os.close(read_stdin)
|
read_stdin = os.close(read_stdin)
|
||||||
|
|
||||||
return self._communicate(pid, in_data, write_stdin, read_stdout, read_stderr)
|
return self._communicate(pid,
|
||||||
|
in_data,
|
||||||
|
write_stdin,
|
||||||
|
read_stdout,
|
||||||
|
read_stderr)
|
||||||
finally:
|
finally:
|
||||||
fds = [read_stdout, write_stdout, read_stderr, write_stderr, read_stdin, write_stdin]
|
fds = [read_stdout,
|
||||||
|
write_stdout,
|
||||||
|
read_stderr,
|
||||||
|
write_stderr,
|
||||||
|
read_stdin,
|
||||||
|
write_stdin]
|
||||||
for fd in fds:
|
for fd in fds:
|
||||||
if fd:
|
if fd:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to lxc"""
|
''' transfer a file from local to lxc '''
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.container_name)
|
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.container_name)
|
||||||
in_path = to_bytes(in_path, errors="surrogate_or_strict")
|
in_path = to_bytes(in_path, errors='surrogate_or_strict')
|
||||||
out_path = to_bytes(out_path, errors="surrogate_or_strict")
|
out_path = to_bytes(out_path, errors='surrogate_or_strict')
|
||||||
|
|
||||||
if not os.path.exists(in_path):
|
if not os.path.exists(in_path):
|
||||||
msg = f"file or module does not exist: {in_path}"
|
msg = f"file or module does not exist: {in_path}"
|
||||||
raise errors.AnsibleFileNotFound(msg)
|
raise errors.AnsibleFileNotFound(msg)
|
||||||
try:
|
try:
|
||||||
src_file = open(in_path, "rb")
|
src_file = open(in_path, "rb")
|
||||||
except IOError as e:
|
except IOError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise errors.AnsibleError(f"failed to open input file to {in_path}") from e
|
raise errors.AnsibleError(f"failed to open input file to {in_path}")
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def write_file(args):
|
def write_file(args):
|
||||||
with open(out_path, "wb+") as dst_file:
|
with open(out_path, 'wb+') as dst_file:
|
||||||
shutil.copyfileobj(src_file, dst_file)
|
shutil.copyfileobj(src_file, dst_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.container.attach_wait(write_file, None)
|
self.container.attach_wait(write_file, None)
|
||||||
except IOError as e:
|
except IOError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
msg = f"failed to transfer file to {out_path}"
|
msg = f"failed to transfer file to {out_path}"
|
||||||
raise errors.AnsibleError(msg) from e
|
raise errors.AnsibleError(msg)
|
||||||
finally:
|
finally:
|
||||||
src_file.close()
|
src_file.close()
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from lxc to local"""
|
''' fetch a file from lxc to local '''
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.container_name)
|
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.container_name)
|
||||||
in_path = to_bytes(in_path, errors="surrogate_or_strict")
|
in_path = to_bytes(in_path, errors='surrogate_or_strict')
|
||||||
out_path = to_bytes(out_path, errors="surrogate_or_strict")
|
out_path = to_bytes(out_path, errors='surrogate_or_strict')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dst_file = open(out_path, "wb")
|
dst_file = open(out_path, "wb")
|
||||||
except IOError as e:
|
except IOError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
msg = f"failed to open output file {out_path}"
|
msg = f"failed to open output file {out_path}"
|
||||||
raise errors.AnsibleError(msg) from e
|
raise errors.AnsibleError(msg)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def write_file(args):
|
def write_file(args):
|
||||||
try:
|
try:
|
||||||
with open(in_path, "rb") as src_file:
|
with open(in_path, 'rb') as src_file:
|
||||||
shutil.copyfileobj(src_file, dst_file)
|
shutil.copyfileobj(src_file, dst_file)
|
||||||
finally:
|
finally:
|
||||||
# this is needed in the lxc child process
|
# this is needed in the lxc child process
|
||||||
# to flush internal python buffers
|
# to flush internal python buffers
|
||||||
dst_file.close()
|
dst_file.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.container.attach_wait(write_file, None)
|
self.container.attach_wait(write_file, None)
|
||||||
except IOError as e:
|
except IOError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
msg = f"failed to transfer file from {in_path} to {out_path}"
|
msg = f"failed to transfer file from {in_path} to {out_path}"
|
||||||
raise errors.AnsibleError(msg) from e
|
raise errors.AnsibleError(msg)
|
||||||
finally:
|
finally:
|
||||||
dst_file.close()
|
dst_file.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
''' terminate the connection; nothing to do here '''
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2016 Matt Clay <matt@mystile.com>
|
# Copyright (c) 2016 Matt Clay <matt@mystile.com>
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
@@ -83,35 +84,35 @@ from ansible.plugins.connection import ConnectionBase
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""lxd based connections"""
|
""" lxd based connections """
|
||||||
|
|
||||||
transport = "community.general.lxd"
|
transport = 'community.general.lxd'
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._lxc_cmd = get_bin_path("lxc")
|
self._lxc_cmd = get_bin_path("lxc")
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raise AnsibleError("lxc command not found in PATH") from e
|
raise AnsibleError("lxc command not found in PATH")
|
||||||
|
|
||||||
def _host(self):
|
def _host(self):
|
||||||
"""translate remote_addr to lxd (short) hostname"""
|
""" translate remote_addr to lxd (short) hostname """
|
||||||
return self.get_option("remote_addr").split(".", 1)[0]
|
return self.get_option("remote_addr").split(".", 1)[0]
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to lxd (nothing to do here)"""
|
"""connect to lxd (nothing to do here) """
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
|
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
self._display.vvv(f"ESTABLISH LXD CONNECTION FOR USER: {self.get_option('remote_user')}", host=self._host())
|
self._display.vvv(f"ESTABLISH LXD CONNECTION FOR USER: {self.get_option('remote_user')}", host=self._host())
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def _build_command(self, cmd) -> list[str]:
|
def _build_command(self, cmd) -> str:
|
||||||
"""build the command to execute on the lxd host"""
|
"""build the command to execute on the lxd host"""
|
||||||
|
|
||||||
exec_cmd: list[str] = [self._lxc_cmd]
|
exec_cmd = [self._lxc_cmd]
|
||||||
|
|
||||||
if self.get_option("project"):
|
if self.get_option("project"):
|
||||||
exec_cmd.extend(["--project", self.get_option("project")])
|
exec_cmd.extend(["--project", self.get_option("project")])
|
||||||
@@ -124,23 +125,25 @@ class Connection(ConnectionBase):
|
|||||||
trying to run 'lxc exec' with become method: {self.get_option('lxd_become_method')}",
|
trying to run 'lxc exec' with become method: {self.get_option('lxd_become_method')}",
|
||||||
host=self._host(),
|
host=self._host(),
|
||||||
)
|
)
|
||||||
exec_cmd.extend([self.get_option("lxd_become_method"), self.get_option("remote_user"), "-c"])
|
exec_cmd.extend(
|
||||||
|
[self.get_option("lxd_become_method"), self.get_option("remote_user"), "-c"]
|
||||||
|
)
|
||||||
|
|
||||||
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
|
exec_cmd.extend([self.get_option("executable"), "-c", cmd])
|
||||||
|
|
||||||
return exec_cmd
|
return exec_cmd
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||||
"""execute a command on the lxd host"""
|
""" execute a command on the lxd host """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
self._display.vvv(f"EXEC {cmd}", host=self._host())
|
self._display.vvv(f"EXEC {cmd}", host=self._host())
|
||||||
|
|
||||||
local_cmd = self._build_command(cmd)
|
local_cmd = self._build_command(cmd)
|
||||||
self._display.vvvvv(f"EXEC {local_cmd}", host=self._host())
|
self._display.vvvvv(f"EXEC {local_cmd}", host=self._host())
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
in_data = to_bytes(in_data, errors="surrogate_or_strict", nonstring="passthru")
|
in_data = to_bytes(in_data, errors='surrogate_or_strict', nonstring='passthru')
|
||||||
|
|
||||||
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
stdout, stderr = process.communicate(in_data)
|
stdout, stderr = process.communicate(in_data)
|
||||||
@@ -163,23 +166,27 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
rc, uid_out, err = self.exec_command("/bin/id -u")
|
rc, uid_out, err = self.exec_command("/bin/id -u")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise AnsibleError(f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}")
|
raise AnsibleError(
|
||||||
|
f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}"
|
||||||
|
)
|
||||||
uid = uid_out.strip()
|
uid = uid_out.strip()
|
||||||
|
|
||||||
rc, gid_out, err = self.exec_command("/bin/id -g")
|
rc, gid_out, err = self.exec_command("/bin/id -g")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise AnsibleError(f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}")
|
raise AnsibleError(
|
||||||
|
f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}"
|
||||||
|
)
|
||||||
gid = gid_out.strip()
|
gid = gid_out.strip()
|
||||||
|
|
||||||
return int(uid), int(gid)
|
return int(uid), int(gid)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""put a file from local to lxd"""
|
""" put a file from local to lxd """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
|
|
||||||
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self._host())
|
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self._host())
|
||||||
|
|
||||||
if not os.path.isfile(to_bytes(in_path, errors="surrogate_or_strict")):
|
if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
|
||||||
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
|
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")
|
||||||
|
|
||||||
local_cmd = [self._lxc_cmd]
|
local_cmd = [self._lxc_cmd]
|
||||||
@@ -212,29 +219,33 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
self._display.vvvvv(f"PUT {local_cmd}", host=self._host())
|
self._display.vvvvv(f"PUT {local_cmd}", host=self._host())
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
|
|
||||||
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from lxd to local"""
|
""" fetch a file from lxd to local """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
|
|
||||||
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self._host())
|
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self._host())
|
||||||
|
|
||||||
local_cmd = [self._lxc_cmd]
|
local_cmd = [self._lxc_cmd]
|
||||||
if self.get_option("project"):
|
if self.get_option("project"):
|
||||||
local_cmd.extend(["--project", self.get_option("project")])
|
local_cmd.extend(["--project", self.get_option("project")])
|
||||||
local_cmd.extend(["file", "pull", f"{self.get_option('remote')}:{self._host()}/{in_path}", out_path])
|
local_cmd.extend([
|
||||||
|
"file", "pull",
|
||||||
|
f"{self.get_option('remote')}:{self._host()}/{in_path}",
|
||||||
|
out_path
|
||||||
|
])
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
|
|
||||||
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""close the connection (nothing to do here)"""
|
""" close the connection (nothing to do here) """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
|
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on the buildah connection plugin
|
# Based on the buildah connection plugin
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# 2018 Kushal Das
|
# 2018 Kushal Das
|
||||||
@@ -53,11 +54,11 @@ class Connection(ConnectionBase):
|
|||||||
"""This is a connection plugin for qubes: it uses qubes-run-vm binary to interact with the containers."""
|
"""This is a connection plugin for qubes: it uses qubes-run-vm binary to interact with the containers."""
|
||||||
|
|
||||||
# String used to identify this Connection class from other classes
|
# String used to identify this Connection class from other classes
|
||||||
transport = "community.general.qubes"
|
transport = 'community.general.qubes'
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self._remote_vmname = self._play_context.remote_addr
|
self._remote_vmname = self._play_context.remote_addr
|
||||||
self._connected = False
|
self._connected = False
|
||||||
@@ -88,29 +89,28 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
local_cmd.append(shell)
|
local_cmd.append(shell)
|
||||||
|
|
||||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||||
|
|
||||||
display.vvvv("Local cmd: ", local_cmd)
|
display.vvvv("Local cmd: ", local_cmd)
|
||||||
|
|
||||||
display.vvv(f"RUN {local_cmd}", host=self._remote_vmname)
|
display.vvv(f"RUN {local_cmd}", host=self._remote_vmname)
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
|
||||||
local_cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
)
|
|
||||||
|
|
||||||
# Here we are writing the actual command to the remote bash
|
# Here we are writing the actual command to the remote bash
|
||||||
p.stdin.write(to_bytes(cmd, errors="surrogate_or_strict"))
|
p.stdin.write(to_bytes(cmd, errors='surrogate_or_strict'))
|
||||||
stdout, stderr = p.communicate(input=in_data)
|
stdout, stderr = p.communicate(input=in_data)
|
||||||
return p.returncode, stdout, stderr
|
return p.returncode, stdout, stderr
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""No persistent connection is being maintained."""
|
"""No persistent connection is being maintained."""
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
@ensure_connect # type: ignore # TODO: for some reason, the type infos for ensure_connect suck...
|
@ensure_connect
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""Run specified command in a running QubesVM"""
|
"""Run specified command in a running QubesVM """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
display.vvvv(f"CMD IS: {cmd}")
|
display.vvvv(f"CMD IS: {cmd}")
|
||||||
|
|
||||||
@@ -120,25 +120,25 @@ class Connection(ConnectionBase):
|
|||||||
return rc, stdout, stderr
|
return rc, stdout, stderr
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""Place a local file located in 'in_path' inside VM at 'out_path'"""
|
""" Place a local file located in 'in_path' inside VM at 'out_path' """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self._remote_vmname)
|
display.vvv(f"PUT {in_path} TO {out_path}", host=self._remote_vmname)
|
||||||
|
|
||||||
with open(in_path, "rb") as fobj:
|
with open(in_path, "rb") as fobj:
|
||||||
source_data = fobj.read()
|
source_data = fobj.read()
|
||||||
|
|
||||||
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}"\n', source_data, "qubes.VMRootShell")
|
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}\"\n', source_data, "qubes.VMRootShell")
|
||||||
# if qubes.VMRootShell service not supported, fallback to qubes.VMShell and
|
# if qubes.VMRootShell service not supported, fallback to qubes.VMShell and
|
||||||
# hope it will have appropriate permissions
|
# hope it will have appropriate permissions
|
||||||
if retcode == 127:
|
if retcode == 127:
|
||||||
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}"\n', source_data)
|
retcode, dummy, dummy = self._qubes(f'cat > "{out_path}\"\n', source_data)
|
||||||
|
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
raise AnsibleConnectionFailure(f"Failed to put_file to {out_path}")
|
raise AnsibleConnectionFailure(f'Failed to put_file to {out_path}')
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""Obtain file specified via 'in_path' from the container and place it at 'out_path'"""
|
"""Obtain file specified via 'in_path' from the container and place it at 'out_path' """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self._remote_vmname)
|
display.vvv(f"FETCH {in_path} TO {out_path}", host=self._remote_vmname)
|
||||||
|
|
||||||
# We are running in dom0
|
# We are running in dom0
|
||||||
@@ -147,9 +147,9 @@ class Connection(ConnectionBase):
|
|||||||
p = subprocess.Popen(cmd_args_list, shell=False, stdout=fobj)
|
p = subprocess.Popen(cmd_args_list, shell=False, stdout=fobj)
|
||||||
p.communicate()
|
p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleConnectionFailure(f"Failed to fetch file to {out_path}")
|
raise AnsibleConnectionFailure(f'Failed to fetch file to {out_path}')
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closing the connection"""
|
""" Closing the connection """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||||
# Based on func.py
|
# Based on func.py
|
||||||
@@ -25,22 +26,21 @@ from ansible.plugins.connection import ConnectionBase
|
|||||||
HAVE_SALTSTACK = False
|
HAVE_SALTSTACK = False
|
||||||
try:
|
try:
|
||||||
import salt.client as sc
|
import salt.client as sc
|
||||||
|
|
||||||
HAVE_SALTSTACK = True
|
HAVE_SALTSTACK = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Salt-based connections"""
|
""" Salt-based connections """
|
||||||
|
|
||||||
has_pipelining = False
|
has_pipelining = False
|
||||||
# while the name of the product is salt, naming that module salt cause
|
# while the name of the product is salt, naming that module salt cause
|
||||||
# trouble with module import
|
# trouble with module import
|
||||||
transport = "community.general.saltstack"
|
transport = 'community.general.saltstack'
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
self.host = self._play_context.remote_addr
|
self.host = self._play_context.remote_addr
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
@@ -52,22 +52,20 @@ class Connection(ConnectionBase):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""run a command on the remote minion"""
|
""" run a command on the remote minion """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
|
||||||
|
|
||||||
self._display.vvv(f"EXEC {cmd}", host=self.host)
|
self._display.vvv(f"EXEC {cmd}", host=self.host)
|
||||||
# need to add 'true;' to work around https://github.com/saltstack/salt/issues/28077
|
# need to add 'true;' to work around https://github.com/saltstack/salt/issues/28077
|
||||||
res = self.client.cmd(self.host, "cmd.exec_code_all", ["bash", f"true;{cmd}"])
|
res = self.client.cmd(self.host, 'cmd.exec_code_all', ['bash', f"true;{cmd}"])
|
||||||
if self.host not in res:
|
if self.host not in res:
|
||||||
raise errors.AnsibleError(
|
raise errors.AnsibleError(f"Minion {self.host} didn't answer, check if salt-minion is running and the name is correct")
|
||||||
f"Minion {self.host} didn't answer, check if salt-minion is running and the name is correct"
|
|
||||||
)
|
|
||||||
|
|
||||||
p = res[self.host]
|
p = res[self.host]
|
||||||
return p["retcode"], p["stdout"], p["stderr"]
|
return p['retcode'], p['stdout'], p['stderr']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_path(path, prefix):
|
def _normalize_path(path, prefix):
|
||||||
@@ -77,27 +75,27 @@ class Connection(ConnectionBase):
|
|||||||
return os.path.join(prefix, normpath[1:])
|
return os.path.join(prefix, normpath[1:])
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to remote"""
|
""" transfer a file from local to remote """
|
||||||
|
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
|
|
||||||
out_path = self._normalize_path(out_path, "/")
|
out_path = self._normalize_path(out_path, '/')
|
||||||
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
|
self._display.vvv(f"PUT {in_path} TO {out_path}", host=self.host)
|
||||||
with open(in_path, "rb") as in_fh:
|
with open(in_path, 'rb') as in_fh:
|
||||||
content = in_fh.read()
|
content = in_fh.read()
|
||||||
self.client.cmd(self.host, "hashutil.base64_decodefile", [base64.b64encode(content), out_path])
|
self.client.cmd(self.host, 'hashutil.base64_decodefile', [base64.b64encode(content), out_path])
|
||||||
|
|
||||||
# TODO test it
|
# TODO test it
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from remote to local"""
|
""" fetch a file from remote to local """
|
||||||
|
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
|
|
||||||
in_path = self._normalize_path(in_path, "/")
|
in_path = self._normalize_path(in_path, '/')
|
||||||
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
|
self._display.vvv(f"FETCH {in_path} TO {out_path}", host=self.host)
|
||||||
content = self.client.cmd(self.host, "cp.get_file_str", [in_path])[self.host]
|
content = self.client.cmd(self.host, 'cp.get_file_str', [in_path])[self.host]
|
||||||
open(out_path, "wb").write(content)
|
open(out_path, 'wb').write(content)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
""" terminate the connection; nothing to do here """
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
# Derived from ansible/plugins/connection/proxmox_pct_remote.py (c) 2024 Nils Stein (@mietzen) <github.nstein@mailbox.org>
|
||||||
# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
# Derived from ansible/plugins/connection/paramiko_ssh.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
|
# Copyright (c) 2025 Rui Lopes (@rgl) <ruilopes.com>
|
||||||
@@ -332,24 +333,31 @@ from ansible.utils.path import makedirs_safe
|
|||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from subprocess import list2cmdline
|
from subprocess import list2cmdline
|
||||||
|
|
||||||
PARAMIKO_IMPORT_ERR: str | None
|
|
||||||
try:
|
try:
|
||||||
import paramiko
|
import paramiko
|
||||||
from paramiko import MissingHostKeyPolicy
|
|
||||||
|
|
||||||
PARAMIKO_IMPORT_ERR = None
|
PARAMIKO_IMPORT_ERR = None
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
paramiko = None
|
||||||
PARAMIKO_IMPORT_ERR = traceback.format_exc()
|
PARAMIKO_IMPORT_ERR = traceback.format_exc()
|
||||||
MissingHostKeyPolicy = object # type: ignore
|
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING and PARAMIKO_IMPORT_ERR is None:
|
||||||
|
from paramiko import MissingHostKeyPolicy
|
||||||
|
from paramiko.client import SSHClient
|
||||||
|
from paramiko.pkey import PKey
|
||||||
|
else:
|
||||||
|
MissingHostKeyPolicy: type = object
|
||||||
|
SSHClient: type = object
|
||||||
|
PKey: type = object
|
||||||
|
|
||||||
|
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
|
||||||
def authenticity_msg(hostname: str, ktype: str, fingerprint: bytes) -> str:
|
def authenticity_msg(hostname: str, ktype: str, fingerprint: str) -> str:
|
||||||
msg = f"""
|
msg = f"""
|
||||||
paramiko: The authenticity of host '{hostname}' can't be established.
|
paramiko: The authenticity of host '{hostname}' can't be established.
|
||||||
The {ktype} key fingerprint is {to_text(fingerprint)}.
|
The {ktype} key fingerprint is {fingerprint}.
|
||||||
Are you sure you want to continue connecting (yes/no)?
|
Are you sure you want to continue connecting (yes/no)?
|
||||||
"""
|
"""
|
||||||
return msg
|
return msg
|
||||||
@@ -369,124 +377,118 @@ class MyAddPolicy(MissingHostKeyPolicy):
|
|||||||
self.connection = connection
|
self.connection = connection
|
||||||
self._options = connection._options
|
self._options = connection._options
|
||||||
|
|
||||||
def missing_host_key(self, client: paramiko.SSHClient, hostname: str, key: paramiko.PKey) -> None:
|
def missing_host_key(self, client: SSHClient, hostname: str, key: PKey) -> None:
|
||||||
if all((self.connection.get_option("host_key_checking"), not self.connection.get_option("host_key_auto_add"))):
|
|
||||||
|
if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))):
|
||||||
|
|
||||||
fingerprint = hexlify(key.get_fingerprint())
|
fingerprint = hexlify(key.get_fingerprint())
|
||||||
ktype = key.get_name()
|
ktype = key.get_name()
|
||||||
|
|
||||||
if self.connection.get_option("use_persistent_connections") or self.connection.force_persistence:
|
if self.connection.get_option('use_persistent_connections') or self.connection.force_persistence:
|
||||||
# don't print the prompt string since the user cannot respond
|
# don't print the prompt string since the user cannot respond
|
||||||
# to the question anyway
|
# to the question anyway
|
||||||
raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
|
raise AnsibleError(authenticity_msg(hostname, ktype, fingerprint)[1:92])
|
||||||
|
|
||||||
inp = to_text(
|
inp = to_text(
|
||||||
display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
|
display.prompt_until(authenticity_msg(hostname, ktype, fingerprint), private=False),
|
||||||
errors="surrogate_or_strict",
|
errors='surrogate_or_strict'
|
||||||
)
|
)
|
||||||
|
|
||||||
if inp.lower() not in ["yes", "y", ""]:
|
if inp.lower() not in ['yes', 'y', '']:
|
||||||
raise AnsibleError("host connection rejected by user")
|
raise AnsibleError('host connection rejected by user')
|
||||||
|
|
||||||
key._added_by_ansible_this_time = True # type: ignore
|
key._added_by_ansible_this_time = True
|
||||||
|
|
||||||
# existing implementation below:
|
# existing implementation below:
|
||||||
client._host_keys.add(hostname, key.get_name(), key) # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
|
client._host_keys.add(hostname, key.get_name(), key)
|
||||||
|
|
||||||
# host keys are actually saved in close() function below
|
# host keys are actually saved in close() function below
|
||||||
# in order to control ordering.
|
# in order to control ordering.
|
||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""SSH based connections (paramiko) to WSL"""
|
""" SSH based connections (paramiko) to WSL """
|
||||||
|
|
||||||
transport = "community.general.wsl"
|
transport = 'community.general.wsl'
|
||||||
_log_channel: str | None = None
|
_log_channel: str | None = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, play_context: PlayContext, new_stdin: io.TextIOWrapper | None = None, *args: t.Any, **kwargs: t.Any):
|
||||||
self, play_context: PlayContext, new_stdin: io.TextIOWrapper | None = None, *args: t.Any, **kwargs: t.Any
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
):
|
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
|
||||||
|
|
||||||
def _set_log_channel(self, name: str) -> None:
|
def _set_log_channel(self, name: str) -> None:
|
||||||
"""Mimic paramiko.SSHClient.set_log_channel"""
|
""" Mimic paramiko.SSHClient.set_log_channel """
|
||||||
self._log_channel = name
|
self._log_channel = name
|
||||||
|
|
||||||
def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
|
def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]:
|
||||||
proxy_command = self.get_option("proxy_command") or None
|
proxy_command = self.get_option('proxy_command') or None
|
||||||
|
|
||||||
sock_kwarg = {}
|
sock_kwarg = {}
|
||||||
if proxy_command:
|
if proxy_command:
|
||||||
replacers: dict[str, str] = {
|
replacers: t.Dict[str, str] = {
|
||||||
"%h": self.get_option("remote_addr"),
|
'%h': self.get_option('remote_addr'),
|
||||||
"%p": str(port),
|
'%p': str(port),
|
||||||
"%r": self.get_option("remote_user"),
|
'%r': self.get_option('remote_user')
|
||||||
}
|
}
|
||||||
for find, replace in replacers.items():
|
for find, replace in replacers.items():
|
||||||
proxy_command = proxy_command.replace(find, replace)
|
proxy_command = proxy_command.replace(find, replace)
|
||||||
try:
|
try:
|
||||||
sock_kwarg = {"sock": paramiko.ProxyCommand(proxy_command)}
|
sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
|
||||||
display.vvv(
|
display.vvv(f'CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}', host=self.get_option('remote_addr'))
|
||||||
f"CONFIGURE PROXY COMMAND FOR CONNECTION: {proxy_command}", host=self.get_option("remote_addr")
|
|
||||||
)
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
display.warning(
|
display.warning('Paramiko ProxyCommand support unavailable. '
|
||||||
"Paramiko ProxyCommand support unavailable. "
|
'Please upgrade to Paramiko 1.9.0 or newer. '
|
||||||
"Please upgrade to Paramiko 1.9.0 or newer. "
|
'Not using configured ProxyCommand')
|
||||||
"Not using configured ProxyCommand"
|
|
||||||
)
|
|
||||||
|
|
||||||
return sock_kwarg
|
return sock_kwarg
|
||||||
|
|
||||||
def _connect(self) -> Connection:
|
def _connect(self) -> Connection:
|
||||||
"""activates the connection object"""
|
""" activates the connection object """
|
||||||
|
|
||||||
if PARAMIKO_IMPORT_ERR is not None:
|
if PARAMIKO_IMPORT_ERR is not None:
|
||||||
raise AnsibleError(f"paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}")
|
raise AnsibleError(f'paramiko is not installed: {to_native(PARAMIKO_IMPORT_ERR)}')
|
||||||
|
|
||||||
port = self.get_option("port")
|
port = self.get_option('port')
|
||||||
display.vvv(
|
display.vvv(f'ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option("remote_user")} on PORT {to_text(port)} TO {self.get_option("remote_addr")}',
|
||||||
f"ESTABLISH PARAMIKO SSH CONNECTION FOR USER: {self.get_option('remote_user')} on PORT {to_text(port)} TO {self.get_option('remote_addr')}",
|
host=self.get_option('remote_addr'))
|
||||||
host=self.get_option("remote_addr"),
|
|
||||||
)
|
|
||||||
|
|
||||||
ssh = paramiko.SSHClient()
|
ssh = paramiko.SSHClient()
|
||||||
|
|
||||||
# Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
|
# Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently
|
||||||
# is keeping or omitting rsa-sha2 algorithms
|
# is keeping or omitting rsa-sha2 algorithms
|
||||||
# default_keys: t.Tuple[str] = ()
|
# default_keys: t.Tuple[str] = ()
|
||||||
paramiko_preferred_pubkeys = getattr(paramiko.Transport, "_preferred_pubkeys", ())
|
paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ())
|
||||||
paramiko_preferred_hostkeys = getattr(paramiko.Transport, "_preferred_keys", ())
|
paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ())
|
||||||
use_rsa_sha2_algorithms = self.get_option("use_rsa_sha2_algorithms")
|
use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms')
|
||||||
disabled_algorithms: dict[str, t.Iterable[str]] = {}
|
disabled_algorithms: t.Dict[str, t.Iterable[str]] = {}
|
||||||
if not use_rsa_sha2_algorithms:
|
if not use_rsa_sha2_algorithms:
|
||||||
if paramiko_preferred_pubkeys:
|
if paramiko_preferred_pubkeys:
|
||||||
disabled_algorithms["pubkeys"] = tuple(a for a in paramiko_preferred_pubkeys if "rsa-sha2" in a)
|
disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a)
|
||||||
if paramiko_preferred_hostkeys:
|
if paramiko_preferred_hostkeys:
|
||||||
disabled_algorithms["keys"] = tuple(a for a in paramiko_preferred_hostkeys if "rsa-sha2" in a)
|
disabled_algorithms['keys'] = tuple(a for a in paramiko_preferred_hostkeys if 'rsa-sha2' in a)
|
||||||
|
|
||||||
# override paramiko's default logger name
|
# override paramiko's default logger name
|
||||||
if self._log_channel is not None:
|
if self._log_channel is not None:
|
||||||
ssh.set_log_channel(self._log_channel)
|
ssh.set_log_channel(self._log_channel)
|
||||||
|
|
||||||
self.keyfile = os.path.expanduser(self.get_option("user_known_hosts_file"))
|
self.keyfile = os.path.expanduser(self.get_option('user_known_hosts_file'))
|
||||||
|
|
||||||
if self.get_option("host_key_checking"):
|
if self.get_option('host_key_checking'):
|
||||||
for ssh_known_hosts in ("/etc/ssh/ssh_known_hosts", "/etc/openssh/ssh_known_hosts", self.keyfile):
|
for ssh_known_hosts in ('/etc/ssh/ssh_known_hosts', '/etc/openssh/ssh_known_hosts', self.keyfile):
|
||||||
try:
|
try:
|
||||||
ssh.load_system_host_keys(ssh_known_hosts)
|
ssh.load_system_host_keys(ssh_known_hosts)
|
||||||
break
|
break
|
||||||
except IOError:
|
except IOError:
|
||||||
pass # file was not found, but not required to function
|
pass # file was not found, but not required to function
|
||||||
except paramiko.hostkeys.InvalidHostKey as e:
|
except paramiko.hostkeys.InvalidHostKey as e:
|
||||||
raise AnsibleConnectionFailure(f"Invalid host key: {to_text(e.line)}") from e
|
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
|
||||||
try:
|
try:
|
||||||
ssh.load_system_host_keys()
|
ssh.load_system_host_keys()
|
||||||
except paramiko.hostkeys.InvalidHostKey as e:
|
except paramiko.hostkeys.InvalidHostKey as e:
|
||||||
raise AnsibleConnectionFailure(f"Invalid host key: {to_text(e.line)}") from e
|
raise AnsibleConnectionFailure(f'Invalid host key: {to_text(e.line)}')
|
||||||
|
|
||||||
ssh_connect_kwargs = self._parse_proxy_command(port)
|
ssh_connect_kwargs = self._parse_proxy_command(port)
|
||||||
ssh.set_missing_host_key_policy(MyAddPolicy(self))
|
ssh.set_missing_host_key_policy(MyAddPolicy(self))
|
||||||
conn_password = self.get_option("password")
|
conn_password = self.get_option('password')
|
||||||
allow_agent = True
|
allow_agent = True
|
||||||
|
|
||||||
if conn_password is not None:
|
if conn_password is not None:
|
||||||
@@ -494,56 +496,54 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
key_filename = None
|
key_filename = None
|
||||||
if self.get_option("private_key_file"):
|
if self.get_option('private_key_file'):
|
||||||
key_filename = os.path.expanduser(self.get_option("private_key_file"))
|
key_filename = os.path.expanduser(self.get_option('private_key_file'))
|
||||||
|
|
||||||
# paramiko 2.2 introduced auth_timeout parameter
|
# paramiko 2.2 introduced auth_timeout parameter
|
||||||
if LooseVersion(paramiko.__version__) >= LooseVersion("2.2.0"):
|
if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'):
|
||||||
ssh_connect_kwargs["auth_timeout"] = self.get_option("timeout")
|
ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout')
|
||||||
|
|
||||||
# paramiko 1.15 introduced banner timeout parameter
|
# paramiko 1.15 introduced banner timeout parameter
|
||||||
if LooseVersion(paramiko.__version__) >= LooseVersion("1.15.0"):
|
if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'):
|
||||||
ssh_connect_kwargs["banner_timeout"] = self.get_option("banner_timeout")
|
ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout')
|
||||||
|
|
||||||
ssh.connect(
|
ssh.connect(
|
||||||
self.get_option("remote_addr").lower(),
|
self.get_option('remote_addr').lower(),
|
||||||
username=self.get_option("remote_user"),
|
username=self.get_option('remote_user'),
|
||||||
allow_agent=allow_agent,
|
allow_agent=allow_agent,
|
||||||
look_for_keys=self.get_option("look_for_keys"),
|
look_for_keys=self.get_option('look_for_keys'),
|
||||||
key_filename=key_filename,
|
key_filename=key_filename,
|
||||||
password=conn_password,
|
password=conn_password,
|
||||||
timeout=self.get_option("timeout"),
|
timeout=self.get_option('timeout'),
|
||||||
port=port,
|
port=port,
|
||||||
disabled_algorithms=disabled_algorithms,
|
disabled_algorithms=disabled_algorithms,
|
||||||
**ssh_connect_kwargs,
|
**ssh_connect_kwargs,
|
||||||
)
|
)
|
||||||
except paramiko.ssh_exception.BadHostKeyException as e:
|
except paramiko.ssh_exception.BadHostKeyException as e:
|
||||||
raise AnsibleConnectionFailure(f"host key mismatch for {to_text(e.hostname)}") from e
|
raise AnsibleConnectionFailure(f'host key mismatch for {to_text(e.hostname)}')
|
||||||
except paramiko.ssh_exception.AuthenticationException as e:
|
except paramiko.ssh_exception.AuthenticationException as e:
|
||||||
msg = f"Failed to authenticate: {e}"
|
msg = f'Failed to authenticate: {e}'
|
||||||
raise AnsibleAuthenticationFailure(msg) from e
|
raise AnsibleAuthenticationFailure(msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = to_text(e)
|
msg = to_text(e)
|
||||||
if "PID check failed" in msg:
|
if u'PID check failed' in msg:
|
||||||
raise AnsibleError(
|
raise AnsibleError('paramiko version issue, please upgrade paramiko on the machine running ansible')
|
||||||
"paramiko version issue, please upgrade paramiko on the machine running ansible"
|
elif u'Private key file is encrypted' in msg:
|
||||||
) from e
|
|
||||||
elif "Private key file is encrypted" in msg:
|
|
||||||
msg = (
|
msg = (
|
||||||
f"ssh {self.get_option('remote_user')}@{self.get_options('remote_addr')}:{port} : "
|
f'ssh {self.get_option("remote_user")}@{self.get_options("remote_addr")}:{port} : '
|
||||||
f"{msg}\nTo connect as a different user, use -u <username>."
|
f'{msg}\nTo connect as a different user, use -u <username>.'
|
||||||
)
|
)
|
||||||
raise AnsibleConnectionFailure(msg) from e
|
raise AnsibleConnectionFailure(msg)
|
||||||
else:
|
else:
|
||||||
raise AnsibleConnectionFailure(msg) from e
|
raise AnsibleConnectionFailure(msg)
|
||||||
self.ssh = ssh
|
self.ssh = ssh
|
||||||
self._connected = True
|
self._connected = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _any_keys_added(self) -> bool:
|
def _any_keys_added(self) -> bool:
|
||||||
for host_keys in self.ssh._host_keys.values(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
|
for hostname, keys in self.ssh._host_keys.items():
|
||||||
for key in host_keys.values():
|
for keytype, key in keys.items():
|
||||||
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
|
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||||
if added_this_time:
|
if added_this_time:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -557,84 +557,81 @@ class Connection(ConnectionBase):
|
|||||||
if not self._any_keys_added():
|
if not self._any_keys_added():
|
||||||
return
|
return
|
||||||
|
|
||||||
path = os.path.expanduser("~/.ssh")
|
path = os.path.expanduser('~/.ssh')
|
||||||
makedirs_safe(path)
|
makedirs_safe(path)
|
||||||
|
|
||||||
with open(filename, "w") as f:
|
with open(filename, 'w') as f:
|
||||||
for hostname, keys in self.ssh._host_keys.items(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
|
for hostname, keys in self.ssh._host_keys.items():
|
||||||
for keytype, key in keys.items():
|
for keytype, key in keys.items():
|
||||||
# was f.write
|
# was f.write
|
||||||
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
|
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||||
if not added_this_time:
|
if not added_this_time:
|
||||||
f.write(f"{hostname} {keytype} {key.get_base64()}\n")
|
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
|
||||||
|
|
||||||
for hostname, keys in self.ssh._host_keys.items(): # type: ignore[attr-defined] # TODO: figure out what _host_keys is!
|
for hostname, keys in self.ssh._host_keys.items():
|
||||||
for keytype, key in keys.items():
|
for keytype, key in keys.items():
|
||||||
added_this_time = getattr(key, "_added_by_ansible_this_time", False)
|
added_this_time = getattr(key, '_added_by_ansible_this_time', False)
|
||||||
if added_this_time:
|
if added_this_time:
|
||||||
f.write(f"{hostname} {keytype} {key.get_base64()}\n")
|
f.write(f'{hostname} {keytype} {key.get_base64()}\n')
|
||||||
|
|
||||||
def _build_wsl_command(self, cmd: str) -> str:
|
def _build_wsl_command(self, cmd: str) -> str:
|
||||||
wsl_distribution = self.get_option("wsl_distribution")
|
wsl_distribution = self.get_option('wsl_distribution')
|
||||||
become = self.get_option("become")
|
become = self.get_option('become')
|
||||||
become_user = self.get_option("become_user")
|
become_user = self.get_option('become_user')
|
||||||
if become and become_user:
|
if become and become_user:
|
||||||
wsl_user = become_user
|
wsl_user = become_user
|
||||||
else:
|
else:
|
||||||
wsl_user = self.get_option("wsl_user")
|
wsl_user = self.get_option('wsl_user')
|
||||||
args = ["wsl.exe", "--distribution", wsl_distribution]
|
args = ['wsl.exe', '--distribution', wsl_distribution]
|
||||||
if wsl_user:
|
if wsl_user:
|
||||||
args.extend(["--user", wsl_user])
|
args.extend(['--user', wsl_user])
|
||||||
args.extend(["--"])
|
args.extend(['--'])
|
||||||
args.extend(shlex.split(cmd))
|
args.extend(shlex.split(cmd))
|
||||||
if os.getenv("_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_WAERI5TEPHEESHA2FAE8"):
|
if os.getenv('_ANSIBLE_TEST_WSL_CONNECTION_PLUGIN_Waeri5tepheeSha2fae8'):
|
||||||
return shlex.join(args)
|
return shlex.join(args)
|
||||||
return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576
|
return list2cmdline(args) # see https://github.com/python/cpython/blob/3.11/Lib/subprocess.py#L576
|
||||||
|
|
||||||
def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
|
def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]:
|
||||||
"""run a command on inside a WSL distribution"""
|
""" run a command on inside a WSL distribution """
|
||||||
|
|
||||||
cmd = self._build_wsl_command(cmd)
|
cmd = self._build_wsl_command(cmd)
|
||||||
|
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable) # type: ignore[safe-super]
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
bufsize = 4096
|
bufsize = 4096
|
||||||
|
|
||||||
try:
|
try:
|
||||||
transport = self.ssh.get_transport()
|
self.ssh.get_transport().set_keepalive(5)
|
||||||
if transport is None:
|
chan = self.ssh.get_transport().open_session()
|
||||||
raise ValueError("Transport not available")
|
|
||||||
transport.set_keepalive(5)
|
|
||||||
chan = transport.open_session()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
text_e = to_text(e)
|
text_e = to_text(e)
|
||||||
msg = "Failed to open session"
|
msg = 'Failed to open session'
|
||||||
if text_e:
|
if text_e:
|
||||||
msg += f": {text_e}"
|
msg += f': {text_e}'
|
||||||
raise AnsibleConnectionFailure(to_native(msg)) from e
|
raise AnsibleConnectionFailure(to_native(msg))
|
||||||
|
|
||||||
display.vvv(f"EXEC {cmd}", host=self.get_option("remote_addr"))
|
display.vvv(f'EXEC {cmd}', host=self.get_option('remote_addr'))
|
||||||
|
|
||||||
cmd_b = to_bytes(cmd, errors="surrogate_or_strict")
|
cmd = to_bytes(cmd, errors='surrogate_or_strict')
|
||||||
|
|
||||||
no_prompt_out = b""
|
no_prompt_out = b''
|
||||||
no_prompt_err = b""
|
no_prompt_err = b''
|
||||||
become_output = b""
|
become_output = b''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chan.exec_command(cmd_b)
|
chan.exec_command(cmd)
|
||||||
if self.become and self.become.expect_prompt():
|
if self.become and self.become.expect_prompt():
|
||||||
password_prompt = False
|
password_prompt = False
|
||||||
become_success = False
|
become_success = False
|
||||||
while not (become_success or password_prompt):
|
while not (become_success or password_prompt):
|
||||||
display.debug("Waiting for Privilege Escalation input")
|
display.debug('Waiting for Privilege Escalation input')
|
||||||
|
|
||||||
chunk = chan.recv(bufsize)
|
chunk = chan.recv(bufsize)
|
||||||
display.debug(f"chunk is: {to_text(chunk)}")
|
display.debug(f'chunk is: {to_text(chunk)}')
|
||||||
if not chunk:
|
if not chunk:
|
||||||
if b"unknown user" in become_output:
|
if b'unknown user' in become_output:
|
||||||
n_become_user = to_native(self.become.get_option("become_user"))
|
n_become_user = to_native(self.become.get_option('become_user'))
|
||||||
raise AnsibleError(f"user {n_become_user} does not exist")
|
raise AnsibleError(f'user {n_become_user} does not exist')
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
# raise AnsibleError('ssh connection closed waiting for password prompt')
|
# raise AnsibleError('ssh connection closed waiting for password prompt')
|
||||||
@@ -652,78 +649,84 @@ class Connection(ConnectionBase):
|
|||||||
|
|
||||||
if password_prompt:
|
if password_prompt:
|
||||||
if self.become:
|
if self.become:
|
||||||
become_pass = self.become.get_option("become_pass")
|
become_pass = self.become.get_option('become_pass')
|
||||||
chan.sendall(to_bytes(f"{become_pass}\n", errors="surrogate_or_strict"))
|
chan.sendall(to_bytes(become_pass + '\n', errors='surrogate_or_strict'))
|
||||||
else:
|
else:
|
||||||
raise AnsibleError("A password is required but none was supplied")
|
raise AnsibleError('A password is required but none was supplied')
|
||||||
else:
|
else:
|
||||||
no_prompt_out += become_output
|
no_prompt_out += become_output
|
||||||
no_prompt_err += become_output
|
no_prompt_err += become_output
|
||||||
|
|
||||||
if in_data:
|
if in_data:
|
||||||
for i in range(0, len(in_data), bufsize):
|
for i in range(0, len(in_data), bufsize):
|
||||||
chan.send(in_data[i : i + bufsize])
|
chan.send(in_data[i:i + bufsize])
|
||||||
chan.shutdown_write()
|
chan.shutdown_write()
|
||||||
elif in_data == b"":
|
elif in_data == b'':
|
||||||
chan.shutdown_write()
|
chan.shutdown_write()
|
||||||
|
|
||||||
except socket.timeout as e:
|
except socket.timeout:
|
||||||
raise AnsibleError(f"ssh timed out waiting for privilege escalation.\n{to_text(become_output)}") from e
|
raise AnsibleError(f'ssh timed out waiting for privilege escalation.\n{to_text(become_output)}')
|
||||||
|
|
||||||
stdout = b"".join(chan.makefile("rb", bufsize))
|
stdout = b''.join(chan.makefile('rb', bufsize))
|
||||||
stderr = b"".join(chan.makefile_stderr("rb", bufsize))
|
stderr = b''.join(chan.makefile_stderr('rb', bufsize))
|
||||||
returncode = chan.recv_exit_status()
|
returncode = chan.recv_exit_status()
|
||||||
|
|
||||||
# NB the full english error message is:
|
# NB the full english error message is:
|
||||||
# 'wsl.exe' is not recognized as an internal or external command,
|
# 'wsl.exe' is not recognized as an internal or external command,
|
||||||
# operable program or batch file.
|
# operable program or batch file.
|
||||||
if "'wsl.exe' is not recognized" in stderr.decode("utf-8"):
|
if "'wsl.exe' is not recognized" in stderr.decode('utf-8'):
|
||||||
raise AnsibleError(f"wsl.exe not found in path of host: {to_text(self.get_option('remote_addr'))}")
|
raise AnsibleError(
|
||||||
|
f'wsl.exe not found in path of host: {to_text(self.get_option("remote_addr"))}')
|
||||||
|
|
||||||
return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
|
return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
|
||||||
|
|
||||||
def put_file(self, in_path: str, out_path: str) -> None:
|
def put_file(self, in_path: str, out_path: str) -> None:
|
||||||
"""transfer a file from local to remote"""
|
""" transfer a file from local to remote """
|
||||||
|
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
|
display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
|
||||||
try:
|
try:
|
||||||
with open(in_path, "rb") as f:
|
with open(in_path, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
returncode, stdout, stderr = self.exec_command(
|
returncode, stdout, stderr = self.exec_command(
|
||||||
f"{self._shell.executable} -c {self._shell.quote(f'cat > {out_path}')}",
|
' '.join([
|
||||||
|
self._shell.executable, '-c',
|
||||||
|
self._shell.quote(f'cat > {out_path}')]),
|
||||||
in_data=data,
|
in_data=data,
|
||||||
sudoable=False,
|
sudoable=False)
|
||||||
)
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
if "cat: not found" in stderr.decode("utf-8"):
|
if 'cat: not found' in stderr.decode('utf-8'):
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
f"cat not found in path of WSL distribution: {to_text(self.get_option('wsl_distribution'))}"
|
f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
|
||||||
)
|
raise AnsibleError(
|
||||||
raise AnsibleError(f"{to_text(stdout)}\n{to_text(stderr)}")
|
f'{to_text(stdout)}\n{to_text(stderr)}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise AnsibleError(f"error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}") from e
|
raise AnsibleError(
|
||||||
|
f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
|
||||||
|
|
||||||
def fetch_file(self, in_path: str, out_path: str) -> None:
|
def fetch_file(self, in_path: str, out_path: str) -> None:
|
||||||
"""save a remote file to the specified path"""
|
""" save a remote file to the specified path """
|
||||||
|
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr"))
|
display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
|
||||||
try:
|
try:
|
||||||
returncode, stdout, stderr = self.exec_command(
|
returncode, stdout, stderr = self.exec_command(
|
||||||
f"{self._shell.executable} -c {self._shell.quote(f'cat {in_path}')}", sudoable=False
|
' '.join([
|
||||||
)
|
self._shell.executable, '-c',
|
||||||
|
self._shell.quote(f'cat {in_path}')]),
|
||||||
|
sudoable=False)
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
if "cat: not found" in stderr.decode("utf-8"):
|
if 'cat: not found' in stderr.decode('utf-8'):
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
f"cat not found in path of WSL distribution: {to_text(self.get_option('wsl_distribution'))}"
|
f'cat not found in path of WSL distribution: {to_text(self.get_option("wsl_distribution"))}')
|
||||||
)
|
raise AnsibleError(
|
||||||
raise AnsibleError(f"{to_text(stdout)}\n{to_text(stderr)}")
|
f'{to_text(stdout)}\n{to_text(stderr)}')
|
||||||
with open(out_path, "wb") as f:
|
with open(out_path, 'wb') as f:
|
||||||
f.write(stdout)
|
f.write(stdout)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise AnsibleError(f"error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}") from e
|
raise AnsibleError(
|
||||||
|
f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""reset the connection"""
|
""" reset the connection """
|
||||||
|
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
return
|
return
|
||||||
@@ -731,9 +734,9 @@ class Connection(ConnectionBase):
|
|||||||
self._connect()
|
self._connect()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""terminate the connection"""
|
""" terminate the connection """
|
||||||
|
|
||||||
if self.get_option("host_key_checking") and self.get_option("record_host_keys") and self._any_keys_added():
|
if self.get_option('host_key_checking') and self.get_option('record_host_keys') and self._any_keys_added():
|
||||||
# add any new SSH host keys -- warning -- this could be slow
|
# add any new SSH host keys -- warning -- this could be slow
|
||||||
# (This doesn't acquire the connection lock because it needs
|
# (This doesn't acquire the connection lock because it needs
|
||||||
# to exclude only other known_hosts writers, not connections
|
# to exclude only other known_hosts writers, not connections
|
||||||
@@ -743,11 +746,11 @@ class Connection(ConnectionBase):
|
|||||||
makedirs_safe(dirname)
|
makedirs_safe(dirname)
|
||||||
tmp_keyfile_name = None
|
tmp_keyfile_name = None
|
||||||
try:
|
try:
|
||||||
with FileLock().lock_file(lockfile, dirname, self.get_option("lock_file_timeout")):
|
with FileLock().lock_file(lockfile, dirname, self.get_option('lock_file_timeout')):
|
||||||
# just in case any were added recently
|
# just in case any were added recently
|
||||||
|
|
||||||
self.ssh.load_system_host_keys()
|
self.ssh.load_system_host_keys()
|
||||||
self.ssh._host_keys.update(self.ssh._system_host_keys) # type: ignore[attr-defined] # TODO this is a HACK!
|
self.ssh._host_keys.update(self.ssh._system_host_keys)
|
||||||
|
|
||||||
# gather information about the current key file, so
|
# gather information about the current key file, so
|
||||||
# we can ensure the new file has the correct mode/owner
|
# we can ensure the new file has the correct mode/owner
|
||||||
@@ -774,16 +777,16 @@ class Connection(ConnectionBase):
|
|||||||
self._save_ssh_host_keys(tmp_keyfile_name)
|
self._save_ssh_host_keys(tmp_keyfile_name)
|
||||||
|
|
||||||
os.rename(tmp_keyfile_name, self.keyfile)
|
os.rename(tmp_keyfile_name, self.keyfile)
|
||||||
except LockTimeout as e:
|
except LockTimeout:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
f"writing lock file for {self.keyfile} ran in to the timeout of {self.get_option('lock_file_timeout')}s"
|
f'writing lock file for {self.keyfile} ran in to the timeout of {self.get_option("lock_file_timeout")}s')
|
||||||
) from e
|
|
||||||
except paramiko.hostkeys.InvalidHostKey as e:
|
except paramiko.hostkeys.InvalidHostKey as e:
|
||||||
raise AnsibleConnectionFailure(f"Invalid host key: {e.line}") from e
|
raise AnsibleConnectionFailure(f'Invalid host key: {e.line}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# unable to save keys, including scenario when key was invalid
|
# unable to save keys, including scenario when key was invalid
|
||||||
# and caught earlier
|
# and caught earlier
|
||||||
raise AnsibleError(f"error occurred while writing SSH host keys!\n{to_text(e)}") from e
|
raise AnsibleError(
|
||||||
|
f'error occurred while writing SSH host keys!\n{to_text(e)}')
|
||||||
finally:
|
finally:
|
||||||
if tmp_keyfile_name is not None:
|
if tmp_keyfile_name is not None:
|
||||||
pathlib.Path(tmp_keyfile_name).unlink(missing_ok=True)
|
pathlib.Path(tmp_keyfile_name).unlink(missing_ok=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
# and chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com>
|
||||||
# and jail.py (c) 2013, Michael Scherer <misc@zarb.org>
|
# and jail.py (c) 2013, Michael Scherer <misc@zarb.org>
|
||||||
@@ -42,22 +43,22 @@ display = Display()
|
|||||||
|
|
||||||
|
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
"""Local zone based connections"""
|
""" Local zone based connections """
|
||||||
|
|
||||||
transport = "community.general.zone"
|
transport = 'community.general.zone'
|
||||||
has_pipelining = True
|
has_pipelining = True
|
||||||
has_tty = False
|
has_tty = False
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||||
|
|
||||||
self.zone = self._play_context.remote_addr
|
self.zone = self._play_context.remote_addr
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
raise AnsibleError("zone connection requires running as root")
|
raise AnsibleError("zone connection requires running as root")
|
||||||
|
|
||||||
self.zoneadm_cmd = to_bytes(self._search_executable("zoneadm"))
|
self.zoneadm_cmd = to_bytes(self._search_executable('zoneadm'))
|
||||||
self.zlogin_cmd = to_bytes(self._search_executable("zlogin"))
|
self.zlogin_cmd = to_bytes(self._search_executable('zlogin'))
|
||||||
|
|
||||||
if self.zone not in self.list_zones():
|
if self.zone not in self.list_zones():
|
||||||
raise AnsibleError(f"incorrect zone name {self.zone}")
|
raise AnsibleError(f"incorrect zone name {self.zone}")
|
||||||
@@ -66,19 +67,19 @@ class Connection(ConnectionBase):
|
|||||||
def _search_executable(executable):
|
def _search_executable(executable):
|
||||||
try:
|
try:
|
||||||
return get_bin_path(executable)
|
return get_bin_path(executable)
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raise AnsibleError(f"{executable} command not found in PATH") from e
|
raise AnsibleError(f"{executable} command not found in PATH")
|
||||||
|
|
||||||
def list_zones(self):
|
def list_zones(self):
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
|
||||||
[self.zoneadm_cmd, "list", "-ip"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
stdin=subprocess.PIPE,
|
||||||
)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
zones = []
|
zones = []
|
||||||
for line in process.stdout.readlines():
|
for line in process.stdout.readlines():
|
||||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
||||||
s = line.split(":")
|
s = line.split(':')
|
||||||
if s[1] != "global":
|
if s[1] != 'global':
|
||||||
zones.append(s[1])
|
zones.append(s[1])
|
||||||
|
|
||||||
return zones
|
return zones
|
||||||
@@ -86,26 +87,23 @@ class Connection(ConnectionBase):
|
|||||||
def get_zone_path(self):
|
def get_zone_path(self):
|
||||||
# solaris10vm# zoneadm -z cswbuild list -p
|
# solaris10vm# zoneadm -z cswbuild list -p
|
||||||
# -:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
|
# -:cswbuild:installed:/zones/cswbuild:479f3c4b-d0c6-e97b-cd04-fd58f2c0238e:native:shared
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen([self.zoneadm_cmd, '-z', to_bytes(self.zone), 'list', '-p'],
|
||||||
[self.zoneadm_cmd, "-z", to_bytes(self.zone), "list", "-p"],
|
stdin=subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# stdout, stderr = p.communicate()
|
# stdout, stderr = p.communicate()
|
||||||
path = process.stdout.readlines()[0].split(":")[3]
|
path = process.stdout.readlines()[0].split(':')[3]
|
||||||
return f"{path}/root"
|
return f"{path}/root"
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""connect to the zone; nothing to do here"""
|
""" connect to the zone; nothing to do here """
|
||||||
super()._connect()
|
super(Connection, self)._connect()
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
display.vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
|
display.vvv("THIS IS A LOCAL ZONE DIR", host=self.zone)
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
def _buffered_exec_command(self, cmd, stdin=subprocess.PIPE):
|
||||||
"""run a command on the zone. This is only needed for implementing
|
""" run a command on the zone. This is only needed for implementing
|
||||||
put_file() get_file() so that we don't have to read the whole file
|
put_file() get_file() so that we don't have to read the whole file
|
||||||
into memory.
|
into memory.
|
||||||
|
|
||||||
@@ -119,13 +117,14 @@ class Connection(ConnectionBase):
|
|||||||
local_cmd = map(to_bytes, local_cmd)
|
local_cmd = map(to_bytes, local_cmd)
|
||||||
|
|
||||||
display.vvv(f"EXEC {local_cmd}", host=self.zone)
|
display.vvv(f"EXEC {local_cmd}", host=self.zone)
|
||||||
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(local_cmd, shell=False, stdin=stdin,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||||
"""run a command on the zone"""
|
""" run a command on the zone """
|
||||||
super().exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
p = self._buffered_exec_command(cmd)
|
p = self._buffered_exec_command(cmd)
|
||||||
|
|
||||||
@@ -133,70 +132,70 @@ class Connection(ConnectionBase):
|
|||||||
return p.returncode, stdout, stderr
|
return p.returncode, stdout, stderr
|
||||||
|
|
||||||
def _prefix_login_path(self, remote_path):
|
def _prefix_login_path(self, remote_path):
|
||||||
"""Make sure that we put files into a standard path
|
""" Make sure that we put files into a standard path
|
||||||
|
|
||||||
If a path is relative, then we need to choose where to put it.
|
If a path is relative, then we need to choose where to put it.
|
||||||
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
||||||
exist in any given chroot. So for now we're choosing "/" instead.
|
exist in any given chroot. So for now we're choosing "/" instead.
|
||||||
This also happens to be the former default.
|
This also happens to be the former default.
|
||||||
|
|
||||||
Can revisit using $HOME instead if it is a problem
|
Can revisit using $HOME instead if it is a problem
|
||||||
"""
|
"""
|
||||||
if not remote_path.startswith(os.path.sep):
|
if not remote_path.startswith(os.path.sep):
|
||||||
remote_path = os.path.join(os.path.sep, remote_path)
|
remote_path = os.path.join(os.path.sep, remote_path)
|
||||||
return os.path.normpath(remote_path)
|
return os.path.normpath(remote_path)
|
||||||
|
|
||||||
def put_file(self, in_path, out_path):
|
def put_file(self, in_path, out_path):
|
||||||
"""transfer a file from local to zone"""
|
""" transfer a file from local to zone """
|
||||||
super().put_file(in_path, out_path)
|
super(Connection, self).put_file(in_path, out_path)
|
||||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.zone)
|
display.vvv(f"PUT {in_path} TO {out_path}", host=self.zone)
|
||||||
|
|
||||||
out_path = shlex_quote(self._prefix_login_path(out_path))
|
out_path = shlex_quote(self._prefix_login_path(out_path))
|
||||||
try:
|
try:
|
||||||
with open(in_path, "rb") as in_file:
|
with open(in_path, 'rb') as in_file:
|
||||||
if not os.fstat(in_file.fileno()).st_size:
|
if not os.fstat(in_file.fileno()).st_size:
|
||||||
count = " count=0"
|
count = ' count=0'
|
||||||
else:
|
else:
|
||||||
count = ""
|
count = ''
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd of={out_path} bs={BUFSIZE}{count}", stdin=in_file)
|
p = self._buffered_exec_command(f'dd of={out_path} bs={BUFSIZE}{count}', stdin=in_file)
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("jail connection requires dd command in the jail") from e
|
raise AnsibleError("jail connection requires dd command in the jail")
|
||||||
try:
|
try:
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
||||||
except IOError as e:
|
except IOError:
|
||||||
raise AnsibleError(f"file or module does not exist at: {in_path}") from e
|
raise AnsibleError(f"file or module does not exist at: {in_path}")
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
"""fetch a file from zone to local"""
|
""" fetch a file from zone to local """
|
||||||
super().fetch_file(in_path, out_path)
|
super(Connection, self).fetch_file(in_path, out_path)
|
||||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.zone)
|
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.zone)
|
||||||
|
|
||||||
in_path = shlex_quote(self._prefix_login_path(in_path))
|
in_path = shlex_quote(self._prefix_login_path(in_path))
|
||||||
try:
|
try:
|
||||||
p = self._buffered_exec_command(f"dd if={in_path} bs={BUFSIZE}")
|
p = self._buffered_exec_command(f'dd if={in_path} bs={BUFSIZE}')
|
||||||
except OSError as e:
|
except OSError:
|
||||||
raise AnsibleError("zone connection requires dd command in the zone") from e
|
raise AnsibleError("zone connection requires dd command in the zone")
|
||||||
|
|
||||||
with open(out_path, "wb+") as out_file:
|
with open(out_path, 'wb+') as out_file:
|
||||||
try:
|
try:
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
while chunk:
|
while chunk:
|
||||||
out_file.write(chunk)
|
out_file.write(chunk)
|
||||||
chunk = p.stdout.read(BUFSIZE)
|
chunk = p.stdout.read(BUFSIZE)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}") from e
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}")
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
raise AnsibleError(f"failed to transfer file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""terminate the connection; nothing to do here"""
|
""" terminate the connection; nothing to do here """
|
||||||
super().close()
|
super(Connection, self).close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
|
# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com>
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment:
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Alicloud only documentation fragment
|
# Alicloud only documentation fragment
|
||||||
DOCUMENTATION = r"""
|
DOCUMENTATION = r"""
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) Ansible Project
|
# Copyright (c) Ansible Project
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment:
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Standard documentation fragment
|
# Standard documentation fragment
|
||||||
DOCUMENTATION = r"""
|
DOCUMENTATION = r"""
|
||||||
options: {}
|
options: {}
|
||||||
@@ -25,7 +29,7 @@ attributes:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Should be used together with the standard fragment
|
# Should be used together with the standard fragment
|
||||||
INFO_MODULE = r"""
|
INFO_MODULE = r'''
|
||||||
options: {}
|
options: {}
|
||||||
attributes:
|
attributes:
|
||||||
check_mode:
|
check_mode:
|
||||||
@@ -36,7 +40,7 @@ attributes:
|
|||||||
support: N/A
|
support: N/A
|
||||||
details:
|
details:
|
||||||
- This action does not modify state.
|
- This action does not modify state.
|
||||||
"""
|
'''
|
||||||
|
|
||||||
CONN = r"""
|
CONN = r"""
|
||||||
options: {}
|
options: {}
|
||||||
@@ -57,7 +61,7 @@ attributes:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Should be used together with the standard fragment and the FACTS fragment
|
# Should be used together with the standard fragment and the FACTS fragment
|
||||||
FACTS_MODULE = r"""
|
FACTS_MODULE = r'''
|
||||||
options: {}
|
options: {}
|
||||||
attributes:
|
attributes:
|
||||||
check_mode:
|
check_mode:
|
||||||
@@ -70,7 +74,7 @@ attributes:
|
|||||||
- This action does not modify state.
|
- This action does not modify state.
|
||||||
facts:
|
facts:
|
||||||
support: full
|
support: full
|
||||||
"""
|
'''
|
||||||
|
|
||||||
FILES = r"""
|
FILES = r"""
|
||||||
options: {}
|
options: {}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) Ansible project
|
# Copyright (c) Ansible project
|
||||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import (absolute_import, division, print_function)
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
|
||||||
class ModuleDocFragment:
|
class ModuleDocFragment(object):
|
||||||
|
|
||||||
# Standard files documentation fragment
|
# Standard files documentation fragment
|
||||||
DOCUMENTATION = r"""
|
DOCUMENTATION = r"""
|
||||||
options:
|
options:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user