Compare commits

...

42 Commits
5.0.2 ... 5.2.0

Author SHA1 Message Date
patchback[bot]
ba3903e6e0 Disable opentelemetry installation for unit tests. (#4871) (#4873)
(cherry picked from commit 1eee35dffb)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-21 21:39:38 +02:00
Felix Fontein
4b6b00d249 Release 5.2.0. 2022-06-21 21:24:42 +02:00
patchback[bot]
0a0b0cb42d Fix CI due to pycdlib dropping Python 2 support. (#4865) (#4869)
(cherry picked from commit 297de3011c)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-21 15:03:51 +02:00
patchback[bot]
d0b39271b3 Sudoers validate (#4794) (#4866)
* Use visudo to validate sudoers rules before use

* Replace use of subprocess.Popen with module.run_command

* Switch out apt for package

* Check file mode when verifying file to determine whether something needs to change

* Only install sudo package for debian and redhat environments (when testing)

* Attempt to install sudo on FreeBSD too

* Try just installing sudo for non-darwin machines

* Don't validate file ownership

* Attempt to install sudo on all platforms

* Revert "Attempt to install sudo on all platforms"

This reverts commit b9562a8916.

* Remove file permissions changes from this PR

* Add changelog fragment for 4794 sudoers validation

* Add option to control when sudoers validation is used

* Update changelog fragment

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add version_added to validation property

Co-authored-by: Felix Fontein <felix@fontein.de>

* Also validate failed sudoers validation error message

Co-authored-by: Felix Fontein <felix@fontein.de>

* Make visudo not executable instead of trying to delete it

* Update edge case validation

* Write invalid sudoers file to alternative path to avoid breaking sudo

* Don't try to remove or otherwise modify visudo on Darwin

* Update plugins/modules/system/sudoers.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Remove trailing extra empty line to appease sanity checker

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 97c72f88b7)

Co-authored-by: Jon Ellis <ellis.jp@gmail.com>
2022-06-21 12:52:21 +02:00
Felix Fontein
f07cb76b09 Prepare 5.2.0 release. 2022-06-20 20:29:04 +02:00
patchback[bot]
09031fc9e6 Add keyring and keyring_info modules (#4764) (#4864)
(cherry picked from commit 45362d39a2)

Co-authored-by: ahussey-redhat <93101976+ahussey-redhat@users.noreply.github.com>
2022-06-20 18:27:10 +00:00
patchback[bot]
4481d0a4a9 redfish_command: VirtualMediaInsert does not work with Supermicro (#4839) (#4863)
* bugfix virtual media support for supermicro hardware

* Added Changelog for PR4839

(cherry picked from commit 5e57d2af0a)

Co-authored-by: FRUCHTiii <57792137+FRUCHTiii@users.noreply.github.com>
2022-06-20 19:29:52 +02:00
patchback[bot]
5861388f11 Remove myself from team_suse (#4860) (#4862)
I do not use `zypper` anymore and can thus not help with issues regarding the zypper module.

(cherry picked from commit 652392be27)

Co-authored-by: Dan Čermák <45594031+dcermak@users.noreply.github.com>
2022-06-20 19:04:32 +02:00
patchback[bot]
c581daa48a sudoers: fix handling of state: absent (#4852) (#4853) (#4858)
* sudoers: fix handling of state: absent (#4852)

* typo fixes

(cherry picked from commit 44e21dd407)

Co-authored-by: s-hamann <10639154+s-hamann@users.noreply.github.com>
2022-06-19 15:48:50 +02:00
patchback[bot]
75e2de3581 Add PSF-license.txt for plugins/module_utils/_mount.py (#4847) (#4848)
* Add PSF-license.txt for plugins/module_utils/_mount.py.

* Move other licenses to licenses/.

* Revert "Move other licenses to licenses/."

This reverts commit eab4209889.

(cherry picked from commit dcdfc9c413)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-17 12:49:08 +02:00
patchback[bot]
6c7bee1225 Add scw_compute_private_network (#4727) (#4845)
* Add scw_compute_private_network

* fix argument required and BOTMETA

* little fix in commentary/doc

* test with link for ansible-doc check

* remove unwanted file

* fix entry missing in  meta/runtime.yml

* scaleway_compute_private_network add some check in test and  some fic in doc

* a=add missing  del os.environ

* fix whitespace

* test_scaleway_compute_private_network : fix test

* test_scaleway_compute_private_network : fix pep8

* scaleway_compute_private_network

add . in description

* scaleway_compute_private_network: fix var name

* [scaleway_compute_private_network] add name for the example's task

* Update plugins/modules/cloud/scaleway/scaleway_compute_private_network.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/cloud/scaleway/scaleway_compute_private_network.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 7f4c11cd64)

Co-authored-by: pastral <52627592+pastral@users.noreply.github.com>
2022-06-15 10:58:13 +02:00
patchback[bot]
eafcdfbceb cmd_runner: add __call__ method to invoke context (#4791) (#4844)
* cmd_runner: add __call__ method to invoke context

* change xfconf to use the callable form

* add changelog fragment

* Update changelogs/fragments/4791-cmd-runner-callable.yaml

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 739ca737f1)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-15 10:58:04 +02:00
patchback[bot]
82a764446b passwordstore: Make compatible with shims (#4780) (#4846)
* passwordstore: Make compatible with shims, add backend config

This allows using the passwordstore plugin with scripts that wrap other
password managers. Also adds an explicit configuration (`backend` in
`ini` and `passwordstore_backend` in `vars`) to set the backend to `pass`
(the default) or `gopass`, which allows using gopass as the backend
without the need of a wrapper script. Please be aware that gopass
support is currently limited, but will work for basic operations.

Includes integrations tests.

Resolves #4766

* Apply suggestions from code review

(cherry picked from commit 006f3bfa89)

Co-authored-by: grembo <freebsd@grem.de>
2022-06-15 10:57:52 +02:00
Felix Fontein
a0032f3513 Next expected release is 5.2.0. 2022-06-14 18:15:59 +02:00
Felix Fontein
8444367cd0 Release 5.1.1. 2022-06-14 17:52:01 +02:00
patchback[bot]
de5fbe457f Fix alternatives module (#4836) (#4840)
* Only pass subcommands when they are specified as module arguments.

* When 'subcommands' is specified, 'link' must be given for every subcommand.

* Extend subcommand tests.

(cherry picked from commit 84d8ca9234)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-14 16:30:42 +02:00
patchback[bot]
40b35acee2 redhat_subscription: call 'remove' instead of 'unsubscribe' (#4809) (#4838)
The 'unsubscribe' command of 'subscription-manager' was deprecated
already in subscription-manager 1.11.3, shipped with RHEL 5.11.
As it was removed in subscription-manager 1.29.x, unsubscribing from
pools was thus broken.

The simple fix is to call the proper command, 'remove'.

(cherry picked from commit a45b90e93f)

Co-authored-by: Pino Toscano <ptoscano@redhat.com>
2022-06-14 07:53:49 +02:00
Felix Fontein
9835deb17f Revert "Print debug output during tests."
This reverts commit 6fe9cf11f1.
2022-06-14 07:43:38 +02:00
Felix Fontein
6fe9cf11f1 Print debug output during tests. 2022-06-14 07:35:29 +02:00
Felix Fontein
d3ebdd2874 Prepare 5.1.1 release. 2022-06-13 22:28:45 +02:00
patchback[bot]
4275bfe87b alternatives: Fix bug with priority default (#4810) (#4835)
* alternatives: Fix bug with priority default

If neigther the priority nor the subcommands where specified the module decided to update the priority with the default value anyway. This resulted in bug #4803 and #4804

* Add changelog fragment.

* Distinguish None from 0.

* Address review comments.

* Update plugins/modules/system/alternatives.py

Co-authored-by: Pilou <pierre-louis@libregerbil.fr>

* Remove unrelated issues from changelog.

Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Pilou <pierre-louis@libregerbil.fr>
(cherry picked from commit 57e83ac80b)

Co-authored-by: Marius Rieder <marius.rieder@durchmesser.ch>
2022-06-13 21:51:08 +02:00
patchback[bot]
2f87b8c63f proxmox_kvm: fix typos (#4798) (#4832)
* Typofix

* Update plugins/modules/cloud/misc/proxmox_kvm.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit e51221896b)

Co-authored-by: Wouter Schoot <wouter@schoot.org>
2022-06-13 12:13:16 +02:00
patchback[bot]
100fffb4c1 nmcli: do not convert undefined lists to empty strings (#4813) (#4834)
* do not convert undefined lists to empty strings

* add changelog fragment (#4813)

(cherry picked from commit 72faebffc6)

Co-authored-by: geichelberger <35195803+geichelberger@users.noreply.github.com>
2022-06-13 12:13:05 +02:00
patchback[bot]
1206900488 Ensure managed sudoers config files have 0440 permissions (#4814) (#4828)
* Ensure sudoers config files are created with 0440 permissions to appease visudo validation

* Remove change not required by the bugfix

* Add changelog fragment for 4814 sudoers file permissions

* Update changelogs/fragments/4814-sudoers-file-permissions.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Have less oct casting

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 2d1e58663c)

Co-authored-by: Jon Ellis <ellis.jp@gmail.com>
2022-06-12 08:59:53 +02:00
patchback[bot]
c28ae26636 Bump AZP container version. (#4819) (#4826)
(cherry picked from commit 42c5024b0b)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-12 08:59:34 +02:00
patchback[bot]
e1e626cdcb requests drops support for older Python (#4818) (#4822)
* requests drops support for older Python.

* Work around CentOS 6 pip bugs.

(cherry picked from commit c8a2c5d375)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-12 08:17:04 +02:00
patchback[bot]
f8d35eeb14 Added conditional to only collect qmpstatus on qemu VMs (#4816) (#4817)
* Added conditional to only collect qmpstatus on qemu VMs

* Processed feedback, added changelog

* Initial change to unit tests

* Made Sanity tests happy again

* Missed a function call, removed superfluous function

* Derp, no need to mock get_vm_status anymore

* Added detail checks whether hosts are mapped to the paused/prelaunch groups

* Fix sanity check

* Processed feedback

* Processed feedback - noqa

(cherry picked from commit 71745b8024)

Co-authored-by: Jeffrey van Pelt <jeff@vanpelt.one>
2022-06-11 13:55:15 +02:00
patchback[bot]
c44298c437 CI: Disable repo URL test for OpenSuSE 15.4 (#4805) (#4808)
* Disable repo URL test for OpenSuSE 15.4.

* Forgot some places.

(cherry picked from commit dd24c98fe5)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-06-08 22:21:47 +02:00
Felix Fontein
1b580476a8 Next expected release is 5.2.0. 2022-06-07 13:02:20 +02:00
Felix Fontein
44d2d62d38 Release 5.1.0. 2022-06-07 12:43:11 +02:00
patchback[bot]
82b2d294b7 add support to create L2TP and PPTP VPN connection (#4746) (#4793)
* add support to create L2TP and PPTP VPN connection

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* apply changes pointed on tests and review

- add changelog fragment
- change example code to use jinja2 in place of shell command

* removes trailing whitespace

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* removes linux command from examples

* remove unnecessary brakets

Co-authored-by: Felix Fontein <felix@fontein.de>

* remove unnecessary brakets

Co-authored-by: Felix Fontein <felix@fontein.de>

* simplify psk encoding on example

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* add unit tests

- test unchenged l2tp and pptp vpn connections
- test create l2tp and pptp vpn connections
- fix is_connection_changed to remove default ifname attribuition

* improve tests on vpn.data param

- fix _compare_conn_params to handle vpn.data as lists

* removes block and set_fact from example

Co-authored-by: Felix Fontein <felix@fontein.de>

* makes line shortter to better reading

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/net_tools/nmcli.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit e5e485390d)

Co-authored-by: José Roberto Emerich Junior <jremerich@gmail.com>
2022-06-06 21:57:53 +02:00
patchback[bot]
812fbef786 xfconf module utils: providing a cmd_runner object (#4776) (#4789)
* xfconf: changed implementation to use cmd_runner

* added module_utils/xfconf.py

* xfconf_info: using cmd_runner

* added module_utils to BOTMETA.yml

* added changelog fragment

* use cmd_runner_fmt instead of deprecated form

(cherry picked from commit 8ba3d94740)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 11:11:25 +02:00
patchback[bot]
9d795c334b ModuleHelperException module utils - improved exception initialization (#4755) (#4786)
* ModuleHelperException module utils - improved exception initialization

* added changelog fragment

* Update plugins/module_utils/mh/exceptions.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit b87edda3c7)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 11:11:15 +02:00
patchback[bot]
512d412eb4 Add subcommands parameter for module alternatives. (#4654) (#4788)
* Add slaves parameter for module alternatives.

* alternatives: Improve documentation abous slaves parameter

* alternatives: Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* alternatives: Add schangelog for slaves parameter

* alernatives: Add integration tests

* alternatives: Improv tests

* alternatives: Update tests/integration/targets/alternatives/tasks/slaves.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* alternatives: Rework logic to support updating priority and subcommands

* alternatives: Use more inclusive naming

* alternatives: Fix linter warnings

* alternatives: Dont fail if link is absent

* alternatives: Update changelog fragment

* alternatives: Add tests for prio change and removing

* alternatives: Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* alternatives: Add `state=auto`to reset mode to auto

* alternatives: Fix linter warnings

* alternatives: Fix documentation.

* alternatives: Combine multiple messages.

* alternatives: Set command env for all commands.

* alternatives: Do not update subcommands if parameter is omited

* alternatives: Fix a bug with python 2.7 var scoping

* alternatives: Improce diff before generation

* alternatives: Fix linter warnings

* alternatives: Fix test names

* alternatives: Simplify subcommands handling and improve diffs

* aliases: Only test for subcommand changes if subcommands parameter is set.

* Update plugins/modules/system/alternatives.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 373da56b5b)

Co-authored-by: Marius Rieder <marius.rieder@durchmesser.ch>
2022-06-06 10:57:41 +02:00
patchback[bot]
8f0ee6966f Add puppet confdir option (#4740) (#4787)
* Add puppet confdir option

* Add puppet confdir option change fragment

* Improve quoting in plugins/modules/system/puppet.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add version_added to plugins/modules/system/puppet.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Georg Vogt <georg.vogt@tngtech.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 62ff263ac1)

Co-authored-by: Ge0rgi0s <34042518+Ge0rgi0s@users.noreply.github.com>
2022-06-06 10:57:37 +02:00
patchback[bot]
3af9e39043 cmd_runner: deprecate fmt as the name for the format class (#4777) (#4784)
* cmd_runner: deprecate fmt as the name for the format class

* added changelog fragment

* fixing the deprecation comment

(cherry picked from commit 2d38c8d892)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 10:57:28 +02:00
Felix Fontein
7b78512c59 Forgot to bump version. 2022-06-06 10:40:44 +02:00
patchback[bot]
9f0913bf73 cmd_runner: added flag check_mode_skip to context (#4736) (#4772)
* cmd_runner: added flag skip_if_check_mode to context

* added changelog fragment

* adjusted param name and added new one

(cherry picked from commit be69f95f63)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 10:30:32 +02:00
patchback[bot]
aea851018b gconftool2_info: new module (#4743) (#4773)
* gconftool2_info: new module

* fixed imports

* fixed docs for gconftool2_info

* fixed docs for gconftool2_info

* minor adjustment in docs

* added tests

* adjustments

(cherry picked from commit 49836bb484)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 10:30:24 +02:00
patchback[bot]
69c79f618e ansible_galaxy_install: minor improvements based on MH updates (#4752) (#4774)
* ansible_galaxy_install: minor improvements based on MH updates

* added changelog fragment

(cherry picked from commit d019e22e7d)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-06-06 10:30:15 +02:00
patchback[bot]
6a51ba5169 Proxmox Inventory: added new statuses for qemu (#4723) (#4775)
* added new statuses for qemu

* added document fragment

* lint fixes

* replaced f strings with %

* move the qmpstatus for qemu to a dedicated group

* added documentation to explain the new addition

* update changelog fragment to reflect the change correctly

* update changelog fragment to reflect the change correctly

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* added a switch to get the qemu extended status

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* groups created when qemu_extended_statuses is true and added tests to make sure they are there

* added test to make sure the groups are not present when qemu_extended_statuses is false

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit b556b142ec)

Co-authored-by: Ilija Matoski <ilijamt@gmail.com>
2022-06-06 10:30:08 +02:00
Felix Fontein
52e8e7e928 Prepare 5.1.0 release. 2022-06-06 10:28:30 +02:00
55 changed files with 3175 additions and 373 deletions

View File

@@ -48,7 +48,7 @@ variables:
resources: resources:
containers: containers:
- container: default - container: default
image: quay.io/ansible/azure-pipelines-test-container:1.9.0 image: quay.io/ansible/azure-pipelines-test-container:3.0.0
pool: Standard pool: Standard

19
.github/BOTMETA.yml vendored
View File

@@ -254,6 +254,9 @@ files:
maintainers: amigus endlesstrax maintainers: amigus endlesstrax
$module_utils/: $module_utils/:
labels: module_utils labels: module_utils
$module_utils/gconftool2.py:
maintainers: russoz
labels: gconftool2
$module_utils/gitlab.py: $module_utils/gitlab.py:
notify: jlozadad notify: jlozadad
maintainers: $team_gitlab maintainers: $team_gitlab
@@ -304,6 +307,9 @@ files:
$module_utils/xenserver.py: $module_utils/xenserver.py:
maintainers: bvitnik maintainers: bvitnik
labels: xenserver labels: xenserver
$module_utils/xfconf.py:
maintainers: russoz
labels: xfconf
$modules/cloud/alicloud/: $modules/cloud/alicloud/:
maintainers: xiaozhu36 maintainers: xiaozhu36
$modules/cloud/atomic/atomic_container.py: $modules/cloud/atomic/atomic_container.py:
@@ -434,6 +440,8 @@ files:
maintainers: claco maintainers: claco
$modules/cloud/scaleway/: $modules/cloud/scaleway/:
maintainers: $team_scaleway maintainers: $team_scaleway
$modules/cloud/scaleway/scaleway_compute_private_network.py:
maintainers: pastral
$modules/cloud/scaleway/scaleway_database_backup.py: $modules/cloud/scaleway/scaleway_database_backup.py:
maintainers: guillaume_ro_fr maintainers: guillaume_ro_fr
$modules/cloud/scaleway/scaleway_image_info.py: $modules/cloud/scaleway/scaleway_image_info.py:
@@ -1024,7 +1032,7 @@ files:
$modules/system/alternatives.py: $modules/system/alternatives.py:
maintainers: mulby maintainers: mulby
labels: alternatives labels: alternatives
ignore: DavidWittman ignore: DavidWittman jiuka
$modules/system/aix_lvol.py: $modules/system/aix_lvol.py:
maintainers: adejoux maintainers: adejoux
$modules/system/awall.py: $modules/system/awall.py:
@@ -1052,6 +1060,9 @@ files:
$modules/system/gconftool2.py: $modules/system/gconftool2.py:
maintainers: Akasurde kevensen maintainers: Akasurde kevensen
labels: gconftool2 labels: gconftool2
$modules/system/gconftool2_info.py:
maintainers: russoz
labels: gconftool2
$modules/system/homectl.py: $modules/system/homectl.py:
maintainers: jameslivulpi maintainers: jameslivulpi
$modules/system/interfaces_file.py: $modules/system/interfaces_file.py:
@@ -1059,6 +1070,10 @@ files:
labels: interfaces_file labels: interfaces_file
$modules/system/iptables_state.py: $modules/system/iptables_state.py:
maintainers: quidame maintainers: quidame
$modules/system/keyring.py:
maintainers: ahussey-redhat
$modules/system/keyring_info.py:
maintainers: ahussey-redhat
$modules/system/shutdown.py: $modules/system/shutdown.py:
maintainers: nitzmahone samdoran aminvakil maintainers: nitzmahone samdoran aminvakil
$modules/system/java_cert.py: $modules/system/java_cert.py:
@@ -1281,5 +1296,5 @@ macros:
team_rhn: FlossWare alikins barnabycourt vritant team_rhn: FlossWare alikins barnabycourt vritant
team_scaleway: remyleone abarbare team_scaleway: remyleone abarbare
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l
team_suse: commel dcermak evrardjp lrupp toabctl AnderEnder alxgu andytom sealor team_suse: commel evrardjp lrupp toabctl AnderEnder alxgu andytom sealor
team_virt: joshainglis karmab tleguern Thulium-Drake Ajpantuso team_virt: joshainglis karmab tleguern Thulium-Drake Ajpantuso

View File

@@ -6,6 +6,101 @@ Community General Release Notes
This changelog describes changes after version 4.0.0. This changelog describes changes after version 4.0.0.
v5.2.0
======
Release Summary
---------------
Regular bugfix and feature release.
Minor Changes
-------------
- cmd_runner module utils - add ``__call__`` method to invoke context (https://github.com/ansible-collections/community.general/pull/4791).
- passwordstore lookup plugin - allow using alternative password managers by detecting wrapper scripts, allow explicit configuration of pass and gopass backends (https://github.com/ansible-collections/community.general/issues/4766).
- sudoers - will attempt to validate the proposed sudoers rule using visudo if available, optionally skipped, or required (https://github.com/ansible-collections/community.general/pull/4794, https://github.com/ansible-collections/community.general/issues/4745).
Bugfixes
--------
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_mount.py``.
- redfish_command - fix the check if a virtual media is unmounted to just check for ``instered= false`` caused by Supermicro hardware that does not clear the ``ImageName`` (https://github.com/ansible-collections/community.general/pull/4839).
- redfish_command - the Supermicro Redfish implementation only supports the ``image_url`` parameter in the underlying API calls to ``VirtualMediaInsert`` and ``VirtualMediaEject``. Any values set (or the defaults) for ``write_protected`` or ``inserted`` will be ignored (https://github.com/ansible-collections/community.general/pull/4839).
- sudoers - fix incorrect handling of ``state: absent`` (https://github.com/ansible-collections/community.general/issues/4852).
New Modules
-----------
Cloud
~~~~~
scaleway
^^^^^^^^
- scaleway_compute_private_network - Scaleway compute - private network management
System
~~~~~~
- keyring - Set or delete a passphrase using the Operating System's native keyring
- keyring_info - Get a passphrase using the Operating System's native keyring
v5.1.1
======
Release Summary
---------------
Bugfix release.
Bugfixes
--------
- alternatives - do not set the priority if the priority was not set by the user (https://github.com/ansible-collections/community.general/pull/4810).
- alternatives - only pass subcommands when they are specified as module arguments (https://github.com/ansible-collections/community.general/issues/4803, https://github.com/ansible-collections/community.general/issues/4804, https://github.com/ansible-collections/community.general/pull/4836).
- alternatives - when ``subcommands`` is specified, ``link`` must be given for every subcommand. This was already mentioned in the documentation, but not enforced by the code (https://github.com/ansible-collections/community.general/pull/4836).
- nmcli - fix error caused by adding undefined module arguments for list options (https://github.com/ansible-collections/community.general/issues/4373, https://github.com/ansible-collections/community.general/pull/4813).
- proxmox inventory plugin - fixed extended status detection for qemu (https://github.com/ansible-collections/community.general/pull/4816).
- redhat_subscription - fix unsubscribing on RHEL 9 (https://github.com/ansible-collections/community.general/issues/4741).
- sudoers - ensure sudoers config files are created with the permissions requested by sudoers (0440) (https://github.com/ansible-collections/community.general/pull/4814).
v5.1.0
======
Release Summary
---------------
Regular bugfix and feature release.
Minor Changes
-------------
- ModuleHelper module utils - improved ``ModuleHelperException``, using ``to_native()`` for the exception message (https://github.com/ansible-collections/community.general/pull/4755).
- alternatives - add ``state=absent`` to be able to remove an alternative (https://github.com/ansible-collections/community.general/pull/4654).
- alternatives - add ``subcommands`` parameter (https://github.com/ansible-collections/community.general/pull/4654).
- ansible_galaxy_install - minor refactoring using latest ``ModuleHelper`` updates (https://github.com/ansible-collections/community.general/pull/4752).
- cmd_runner module util - added parameters ``check_mode_skip`` and ``check_mode_return`` to ``CmdRunner.context()``, so that the command is not executed when ``check_mode=True`` (https://github.com/ansible-collections/community.general/pull/4736).
- nmcli - adds ``vpn`` type and parameter for supporting VPN with service type L2TP and PPTP (https://github.com/ansible-collections/community.general/pull/4746).
- proxmox inventory plugin - added new flag ``qemu_extended_statuses`` and new groups ``<group_prefix>prelaunch``, ``<group_prefix>paused``. They will be populated only when ``want_facts=true``, ``qemu_extended_statuses=true`` and only for ``QEMU`` machines (https://github.com/ansible-collections/community.general/pull/4723).
- puppet - adds ``confdir`` parameter to configure a custom confir location (https://github.com/ansible-collections/community.general/pull/4740).
- xfconf - changed implementation to use ``cmd_runner`` (https://github.com/ansible-collections/community.general/pull/4776).
- xfconf module utils - created new module util ``xfconf`` providing a ``cmd_runner`` specific for ``xfconf`` modules (https://github.com/ansible-collections/community.general/pull/4776).
- xfconf_info - changed implementation to use ``cmd_runner`` (https://github.com/ansible-collections/community.general/pull/4776).
Deprecated Features
-------------------
- cmd_runner module utils - deprecated ``fmt`` in favour of ``cmd_runner_fmt`` as the parameter format object (https://github.com/ansible-collections/community.general/pull/4777).
New Modules
-----------
System
~~~~~~
- gconftool2_info - Retrieve GConf configurations
v5.0.2 v5.0.2
====== ======

48
PSF-license.txt Normal file
View 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.

View File

@@ -801,3 +801,115 @@ releases:
- 5.0.2.yml - 5.0.2.yml
- simplified-bsd-license.yml - simplified-bsd-license.yml
release_date: '2022-06-06' release_date: '2022-06-06'
5.1.0:
changes:
deprecated_features:
- cmd_runner module utils - deprecated ``fmt`` in favour of ``cmd_runner_fmt``
as the parameter format object (https://github.com/ansible-collections/community.general/pull/4777).
minor_changes:
- ModuleHelper module utils - improved ``ModuleHelperException``, using ``to_native()``
for the exception message (https://github.com/ansible-collections/community.general/pull/4755).
- alternatives - add ``state=absent`` to be able to remove an alternative (https://github.com/ansible-collections/community.general/pull/4654).
- alternatives - add ``subcommands`` parameter (https://github.com/ansible-collections/community.general/pull/4654).
- ansible_galaxy_install - minor refactoring using latest ``ModuleHelper`` updates
(https://github.com/ansible-collections/community.general/pull/4752).
- cmd_runner module util - added parameters ``check_mode_skip`` and ``check_mode_return``
to ``CmdRunner.context()``, so that the command is not executed when ``check_mode=True``
(https://github.com/ansible-collections/community.general/pull/4736).
- nmcli - adds ``vpn`` type and parameter for supporting VPN with service type
L2TP and PPTP (https://github.com/ansible-collections/community.general/pull/4746).
- proxmox inventory plugin - added new flag ``qemu_extended_statuses`` and new
groups ``<group_prefix>prelaunch``, ``<group_prefix>paused``. They will be
populated only when ``want_facts=true``, ``qemu_extended_statuses=true`` and
only for ``QEMU`` machines (https://github.com/ansible-collections/community.general/pull/4723).
- puppet - adds ``confdir`` parameter to configure a custom confir location
(https://github.com/ansible-collections/community.general/pull/4740).
- xfconf - changed implementation to use ``cmd_runner`` (https://github.com/ansible-collections/community.general/pull/4776).
- xfconf module utils - created new module util ``xfconf`` providing a ``cmd_runner``
specific for ``xfconf`` modules (https://github.com/ansible-collections/community.general/pull/4776).
- xfconf_info - changed implementation to use ``cmd_runner`` (https://github.com/ansible-collections/community.general/pull/4776).
release_summary: Regular bugfix and feature release.
fragments:
- 4654-alternatives-add-subcommands.yml
- 4724-proxmox-qemu-extend.yaml
- 4736-cmd-runner-skip-if-check.yml
- 4740-puppet-feature.yaml
- 4746-add-vpn-support-nmcli.yaml
- 4752-ansible-galaxy-install-mh-updates.yml
- 4755-mhexception-improvement.yml
- 4776-xfconf-cmd-runner.yaml
- 4777-cmd-runner-deprecate-fmt.yaml
- 5.1.0.yml
modules:
- description: Retrieve GConf configurations
name: gconftool2_info
namespace: system
release_date: '2022-06-07'
5.1.1:
changes:
bugfixes:
- alternatives - do not set the priority if the priority was not set by the
user (https://github.com/ansible-collections/community.general/pull/4810).
- alternatives - only pass subcommands when they are specified as module arguments
(https://github.com/ansible-collections/community.general/issues/4803, https://github.com/ansible-collections/community.general/issues/4804,
https://github.com/ansible-collections/community.general/pull/4836).
- alternatives - when ``subcommands`` is specified, ``link`` must be given for
every subcommand. This was already mentioned in the documentation, but not
enforced by the code (https://github.com/ansible-collections/community.general/pull/4836).
- nmcli - fix error caused by adding undefined module arguments for list options
(https://github.com/ansible-collections/community.general/issues/4373, https://github.com/ansible-collections/community.general/pull/4813).
- proxmox inventory plugin - fixed extended status detection for qemu (https://github.com/ansible-collections/community.general/pull/4816).
- redhat_subscription - fix unsubscribing on RHEL 9 (https://github.com/ansible-collections/community.general/issues/4741).
- sudoers - ensure sudoers config files are created with the permissions requested
by sudoers (0440) (https://github.com/ansible-collections/community.general/pull/4814).
release_summary: Bugfix release.
fragments:
- 4809-redhat_subscription-unsubscribe.yaml
- 4810-alternatives-bug.yml
- 4813-fix-nmcli-convert-list.yaml
- 4814-sudoers-file-permissions.yml
- 4816-proxmox-fix-extended-status.yaml
- 4836-alternatives.yml
- 5.1.1.yml
release_date: '2022-06-14'
5.2.0:
changes:
bugfixes:
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_mount.py``.
- redfish_command - fix the check if a virtual media is unmounted to just check
for ``instered= false`` caused by Supermicro hardware that does not clear
the ``ImageName`` (https://github.com/ansible-collections/community.general/pull/4839).
- redfish_command - the Supermicro Redfish implementation only supports the
``image_url`` parameter in the underlying API calls to ``VirtualMediaInsert``
and ``VirtualMediaEject``. Any values set (or the defaults) for ``write_protected``
or ``inserted`` will be ignored (https://github.com/ansible-collections/community.general/pull/4839).
- 'sudoers - fix incorrect handling of ``state: absent`` (https://github.com/ansible-collections/community.general/issues/4852).'
minor_changes:
- cmd_runner module utils - add ``__call__`` method to invoke context (https://github.com/ansible-collections/community.general/pull/4791).
- passwordstore lookup plugin - allow using alternative password managers by
detecting wrapper scripts, allow explicit configuration of pass and gopass
backends (https://github.com/ansible-collections/community.general/issues/4766).
- sudoers - will attempt to validate the proposed sudoers rule using visudo
if available, optionally skipped, or required (https://github.com/ansible-collections/community.general/pull/4794,
https://github.com/ansible-collections/community.general/issues/4745).
release_summary: Regular bugfix and feature release.
fragments:
- 4780-passwordstore-wrapper-compat.yml
- 4791-cmd-runner-callable.yaml
- 4794-sudoers-validation.yml
- 4839-fix-VirtualMediaInsert-Supermicro.yml
- 4852-sudoers-state-absent.yml
- 5.2.0.yml
- psf-license.yml
modules:
- description: Set or delete a passphrase using the Operating System's native
keyring
name: keyring
namespace: system
- description: Get a passphrase using the Operating System's native keyring
name: keyring_info
namespace: system
- description: Scaleway compute - private network management
name: scaleway_compute_private_network
namespace: cloud.scaleway
release_date: '2022-06-21'

View File

@@ -1,6 +1,6 @@
namespace: community namespace: community
name: general name: general
version: 5.0.2 version: 5.2.0
readme: README.md readme: README.md
authors: authors:
- Ansible (https://github.com/ansible) - Ansible (https://github.com/ansible)

View File

@@ -292,6 +292,8 @@ plugin_routing:
redirect: community.google.gce_tag redirect: community.google.gce_tag
gconftool2: gconftool2:
redirect: community.general.system.gconftool2 redirect: community.general.system.gconftool2
gconftool2_info:
redirect: community.general.system.gconftool2_info
gcp_backend_service: gcp_backend_service:
tombstone: tombstone:
removal_version: 2.0.0 removal_version: 2.0.0
@@ -606,6 +608,10 @@ plugin_routing:
redirect: community.general.identity.keycloak.keycloak_role redirect: community.general.identity.keycloak.keycloak_role
keycloak_user_federation: keycloak_user_federation:
redirect: community.general.identity.keycloak.keycloak_user_federation redirect: community.general.identity.keycloak.keycloak_user_federation
keyring:
redirect: community.general.system.keyring
keyring_info:
redirect: community.general.system.keyring_info
kibana_plugin: kibana_plugin:
redirect: community.general.database.misc.kibana_plugin redirect: community.general.database.misc.kibana_plugin
kubevirt_cdi_upload: kubevirt_cdi_upload:
@@ -1355,6 +1361,8 @@ plugin_routing:
redirect: community.general.notification.say redirect: community.general.notification.say
scaleway_compute: scaleway_compute:
redirect: community.general.cloud.scaleway.scaleway_compute redirect: community.general.cloud.scaleway.scaleway_compute
scaleway_compute_private_network:
redirect: community.general.cloud.scaleway.scaleway_compute_private_network
scaleway_database_backup: scaleway_database_backup:
redirect: community.general.cloud.scaleway.scaleway_database_backup redirect: community.general.cloud.scaleway.scaleway_database_backup
scaleway_image_facts: scaleway_image_facts:

View File

@@ -92,9 +92,21 @@ DOCUMENTATION = '''
default: proxmox_ default: proxmox_
type: str type: str
want_facts: want_facts:
description: Gather LXC/QEMU configuration facts. description:
- Gather LXC/QEMU configuration facts.
- When I(want_facts) is set to C(true) more details about QEMU VM status are possible, besides the running and stopped states.
Currently if the VM is running and it is suspended, the status will be running and the machine will be in C(running) group,
but its actual state will be paused. See I(qemu_extended_statuses) for how to retrieve the real status.
default: no default: no
type: bool type: bool
qemu_extended_statuses:
description:
- Requires I(want_facts) to be set to C(true) to function. This will allow you to differentiate betweend C(paused) and C(prelaunch)
statuses of the QEMU VMs.
- This introduces multiple groups [prefixed with I(group_prefix)] C(prelaunch) and C(paused).
default: no
type: bool
version_added: 5.1.0
want_proxmox_nodes_ansible_host: want_proxmox_nodes_ansible_host:
version_added: 3.0.0 version_added: 3.0.0
description: description:
@@ -431,6 +443,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _get_vm_status(self, properties, node, vmid, vmtype, name): def _get_vm_status(self, properties, node, vmid, vmtype, name):
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/status/current" % (self.proxmox_url, node, vmtype, vmid)) ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/status/current" % (self.proxmox_url, node, vmtype, vmid))
properties[self._fact('status')] = ret['status'] properties[self._fact('status')] = ret['status']
if vmtype == 'qemu':
properties[self._fact('qmpstatus')] = ret['qmpstatus']
def _get_vm_snapshots(self, properties, node, vmid, vmtype, name): def _get_vm_snapshots(self, properties, node, vmid, vmtype, name):
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/snapshot" % (self.proxmox_url, node, vmtype, vmid)) ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/snapshot" % (self.proxmox_url, node, vmtype, vmid))
@@ -489,7 +503,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
name, vmid = item['name'], item['vmid'] name, vmid = item['name'], item['vmid']
# get status, config and snapshots if want_facts == True # get status, config and snapshots if want_facts == True
if self.get_option('want_facts'): want_facts = self.get_option('want_facts')
if want_facts:
self._get_vm_status(properties, node, vmid, ittype, name) self._get_vm_status(properties, node, vmid, ittype, name)
self._get_vm_config(properties, node, vmid, ittype, name) self._get_vm_config(properties, node, vmid, ittype, name)
self._get_vm_snapshots(properties, node, vmid, ittype, name) self._get_vm_snapshots(properties, node, vmid, ittype, name)
@@ -503,10 +518,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
node_type_group = self._group('%s_%s' % (node, ittype)) node_type_group = self._group('%s_%s' % (node, ittype))
self.inventory.add_child(self._group('all_' + ittype), name) self.inventory.add_child(self._group('all_' + ittype), name)
self.inventory.add_child(node_type_group, name) self.inventory.add_child(node_type_group, name)
if item['status'] == 'stopped':
self.inventory.add_child(self._group('all_stopped'), name) item_status = item['status']
elif item['status'] == 'running': if item_status == 'running':
self.inventory.add_child(self._group('all_running'), name) if want_facts and ittype == 'qemu' and self.get_option('qemu_extended_statuses'):
# get more details about the status of the qemu VM
item_status = properties.get(self._fact('qmpstatus'), item_status)
self.inventory.add_child(self._group('all_%s' % (item_status, )), name)
return name return name
@@ -528,10 +546,14 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
def _populate(self): def _populate(self):
# create common groups # create common groups
self.inventory.add_group(self._group('all_lxc')) default_groups = ['lxc', 'qemu', 'running', 'stopped']
self.inventory.add_group(self._group('all_qemu'))
self.inventory.add_group(self._group('all_running')) if self.get_option('qemu_extended_statuses'):
self.inventory.add_group(self._group('all_stopped')) default_groups.extend(['prelaunch', 'paused'])
for group in default_groups:
self.inventory.add_group(self._group('all_%s' % (group)))
nodes_group = self._group('nodes') nodes_group = self._group('nodes')
self.inventory.add_group(nodes_group) self.inventory.add_group(nodes_group)
@@ -621,6 +643,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
if proxmox_password is None and (proxmox_token_id is None or proxmox_token_secret is None): if proxmox_password is None and (proxmox_token_id is None or proxmox_token_secret is None):
raise AnsibleError('You must specify either a password or both token_id and token_secret.') raise AnsibleError('You must specify either a password or both token_id and token_secret.')
if self.get_option('qemu_extended_statuses') and not self.get_option('want_facts'):
raise AnsibleError('You must set want_facts to True if you want to use qemu_extended_statuses.')
self.cache_key = self.get_cache_key(path) self.cache_key = self.get_cache_key(path)
self.use_cache = cache and self.get_option('cache') self.use_cache = cache and self.get_option('cache')
self.host_filters = self.get_option('filters') self.host_filters = self.get_option('filters')

View File

@@ -106,6 +106,22 @@ DOCUMENTATION = '''
type: str type: str
default: 15m default: 15m
version_added: 4.5.0 version_added: 4.5.0
backend:
description:
- Specify which backend to use.
- Defaults to C(pass), passwordstore.org's original pass utility.
- C(gopass) support is incomplete.
ini:
- section: passwordstore_lookup
key: backend
vars:
- name: passwordstore_backend
type: str
default: pass
choices:
- pass
- gopass
version_added: 5.2.0
''' '''
EXAMPLES = """ EXAMPLES = """
ansible.cfg: | ansible.cfg: |
@@ -231,6 +247,24 @@ def check_output2(*popenargs, **kwargs):
class LookupModule(LookupBase): class LookupModule(LookupBase):
def __init__(self, loader=None, templar=None, **kwargs):
super(LookupModule, self).__init__(loader, templar, **kwargs)
self.realpass = None
def is_real_pass(self):
if self.realpass is None:
try:
self.passoutput = to_text(
check_output2([self.pass_cmd, "--version"], env=self.env),
errors='surrogate_or_strict'
)
self.realpass = 'pass: the standard unix password manager' in self.passoutput
except (subprocess.CalledProcessError) as e:
raise AnsibleError(e)
return self.realpass
def parse_params(self, term): def parse_params(self, term):
# I went with the "traditional" param followed with space separated KV pairs. # I went with the "traditional" param followed with space separated KV pairs.
# Waiting for final implementation of lookup parameter parsing. # Waiting for final implementation of lookup parameter parsing.
@@ -270,10 +304,12 @@ class LookupModule(LookupBase):
self.env = os.environ.copy() self.env = os.environ.copy()
self.env['LANGUAGE'] = 'C' # make sure to get errors in English as required by check_output2 self.env['LANGUAGE'] = 'C' # make sure to get errors in English as required by check_output2
# Set PASSWORD_STORE_DIR if self.backend == 'gopass':
if os.path.isdir(self.paramvals['directory']): self.env['GOPASS_NO_REMINDER'] = "YES"
elif os.path.isdir(self.paramvals['directory']):
# Set PASSWORD_STORE_DIR
self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory'] self.env['PASSWORD_STORE_DIR'] = self.paramvals['directory']
else: elif self.is_real_pass():
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory'])) raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
# Set PASSWORD_STORE_UMASK if umask is set # Set PASSWORD_STORE_UMASK if umask is set
@@ -288,7 +324,9 @@ class LookupModule(LookupBase):
def check_pass(self): def check_pass(self):
try: try:
self.passoutput = to_text( self.passoutput = to_text(
check_output2(["pass", "show", self.passname], env=self.env), check_output2([self.pass_cmd, 'show'] +
(['--password'] if self.backend == 'gopass' else []) +
[self.passname], env=self.env),
errors='surrogate_or_strict' errors='surrogate_or_strict'
).splitlines() ).splitlines()
self.password = self.passoutput[0] self.password = self.passoutput[0]
@@ -302,8 +340,10 @@ class LookupModule(LookupBase):
if ':' in line: if ':' in line:
name, value = line.split(':', 1) name, value = line.split(':', 1)
self.passdict[name.strip()] = value.strip() self.passdict[name.strip()] = value.strip()
if os.path.isfile(os.path.join(self.paramvals['directory'], self.passname + ".gpg")): if (self.backend == 'gopass' or
# Only accept password as found, if there a .gpg file for it (might be a tree node otherwise) os.path.isfile(os.path.join(self.paramvals['directory'], self.passname + ".gpg"))
or not self.is_real_pass()):
# When using real pass, only accept password as found if there is a .gpg file for it (might be a tree node otherwise)
return True return True
except (subprocess.CalledProcessError) as e: except (subprocess.CalledProcessError) as e:
# 'not in password store' is the expected error if a password wasn't found # 'not in password store' is the expected error if a password wasn't found
@@ -339,7 +379,7 @@ class LookupModule(LookupBase):
if self.paramvals['backup']: if self.paramvals['backup']:
msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime) msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime)
try: try:
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg, env=self.env) check_output2([self.pass_cmd, 'insert', '-f', '-m', self.passname], input=msg, env=self.env)
except (subprocess.CalledProcessError) as e: except (subprocess.CalledProcessError) as e:
raise AnsibleError(e) raise AnsibleError(e)
return newpass return newpass
@@ -351,7 +391,7 @@ class LookupModule(LookupBase):
datetime = time.strftime("%d/%m/%Y %H:%M:%S") datetime = time.strftime("%d/%m/%Y %H:%M:%S")
msg = newpass + '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime) msg = newpass + '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime)
try: try:
check_output2(['pass', 'insert', '-f', '-m', self.passname], input=msg, env=self.env) check_output2([self.pass_cmd, 'insert', '-f', '-m', self.passname], input=msg, env=self.env)
except (subprocess.CalledProcessError) as e: except (subprocess.CalledProcessError) as e:
raise AnsibleError(e) raise AnsibleError(e)
return newpass return newpass
@@ -380,6 +420,8 @@ class LookupModule(LookupBase):
yield yield
def setup(self, variables): def setup(self, variables):
self.backend = self.get_option('backend')
self.pass_cmd = self.backend # pass and gopass are commands as well
self.locked = None self.locked = None
timeout = self.get_option('locktimeout') timeout = self.get_option('locktimeout')
if not re.match('^[0-9]+[smh]$', timeout): if not re.match('^[0-9]+[smh]$', timeout):
@@ -402,6 +444,7 @@ class LookupModule(LookupBase):
} }
def run(self, terms, variables, **kwargs): def run(self, terms, variables, **kwargs):
self.set_options(var_options=variables, direct=kwargs)
self.setup(variables) self.setup(variables)
result = [] result = []

View File

@@ -3,51 +3,7 @@
# This particular file snippet, and this file snippet only, is based on # This particular file snippet, and this file snippet only, is based on
# Lib/posixpath.py of cpython # Lib/posixpath.py of cpython
# It is licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 # It is licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
# # (See PSF-license.txt in this collection)
# 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 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.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function

View File

@@ -197,7 +197,7 @@ class CmdRunner(object):
if mod_param_name not in self.arg_formats: if mod_param_name not in self.arg_formats:
self.arg_formats[mod_param_name] = _Format.as_default_type(spec['type'], mod_param_name) self.arg_formats[mod_param_name] = _Format.as_default_type(spec['type'], mod_param_name)
def context(self, args_order=None, output_process=None, ignore_value_none=True, **kwargs): def __call__(self, args_order=None, output_process=None, ignore_value_none=True, check_mode_skip=False, check_mode_return=None, **kwargs):
if output_process is None: if output_process is None:
output_process = _process_as_is output_process = _process_as_is
if args_order is None: if args_order is None:
@@ -209,18 +209,25 @@ class CmdRunner(object):
return _CmdRunnerContext(runner=self, return _CmdRunnerContext(runner=self,
args_order=args_order, args_order=args_order,
output_process=output_process, output_process=output_process,
ignore_value_none=ignore_value_none, **kwargs) ignore_value_none=ignore_value_none,
check_mode_skip=check_mode_skip,
check_mode_return=check_mode_return, **kwargs)
def has_arg_format(self, arg): def has_arg_format(self, arg):
return arg in self.arg_formats return arg in self.arg_formats
# not decided whether to keep it or not, but if deprecating it will happen in a farther future.
context = __call__
class _CmdRunnerContext(object): class _CmdRunnerContext(object):
def __init__(self, runner, args_order, output_process, ignore_value_none, **kwargs): def __init__(self, runner, args_order, output_process, ignore_value_none, check_mode_skip, check_mode_return, **kwargs):
self.runner = runner self.runner = runner
self.args_order = tuple(args_order) self.args_order = tuple(args_order)
self.output_process = output_process self.output_process = output_process
self.ignore_value_none = ignore_value_none self.ignore_value_none = ignore_value_none
self.check_mode_skip = check_mode_skip
self.check_mode_return = check_mode_return
self.run_command_args = dict(kwargs) self.run_command_args = dict(kwargs)
self.environ_update = runner.environ_update self.environ_update = runner.environ_update
@@ -260,6 +267,8 @@ class _CmdRunnerContext(object):
except Exception as e: except Exception as e:
raise FormatError(arg_name, value, runner.arg_formats[arg_name], e) raise FormatError(arg_name, value, runner.arg_formats[arg_name], e)
if self.check_mode_skip and module.check_mode:
return self.check_mode_return
results = module.run_command(self.cmd, **self.run_command_args) results = module.run_command(self.cmd, **self.run_command_args)
self.results_rc, self.results_out, self.results_err = results self.results_rc, self.results_out, self.results_err = results
self.results_processed = self.output_process(*results) self.results_processed = self.output_process(*results)
@@ -288,4 +297,12 @@ class _CmdRunnerContext(object):
return False return False
fmt = _Format() cmd_runner_fmt = _Format()
#
# The fmt form is deprecated and will be removed in community.general 7.0.0
# Please use:
# cmd_runner_fmt
# Or, to retain the same effect, use:
# from ansible_collections.community.general.plugins.module_utils.cmd_runner import cmd_runner_fmt as fmt
fmt = cmd_runner_fmt

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# (c) 2022, Alexei Znamensky <russoz@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt as fmt
def gconftool2_runner(module, **kwargs):
return CmdRunner(
module,
command='gconftool-2',
arg_formats=dict(
key=fmt.as_list(),
value_type=fmt.as_opt_val("--type"),
value=fmt.as_list(),
direct=fmt.as_bool("--direct"),
config_source=fmt.as_opt_val("--config-source"),
get=fmt.as_bool("--get"),
set_arg=fmt.as_bool("--set"),
unset=fmt.as_bool("--unset"),
),
**kwargs
)

View File

@@ -6,17 +6,13 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from ansible.module_utils.common.text.converters import to_native
class ModuleHelperException(Exception): class ModuleHelperException(Exception):
@staticmethod def __init__(self, msg, update_output=None, *args, **kwargs):
def _get_remove(key, kwargs): self.msg = to_native(msg or "Module failed with exception: {0}".format(self))
if key in kwargs: if update_output is None:
result = kwargs[key] update_output = {}
del kwargs[key] self.update_output = update_output
return result
return None
def __init__(self, *args, **kwargs):
self.msg = self._get_remove('msg', kwargs) or "Module failed with exception: {0}".format(self)
self.update_output = self._get_remove('update_output', kwargs) or {}
super(ModuleHelperException, self).__init__(*args) super(ModuleHelperException, self).__init__(*args)

View File

@@ -2187,9 +2187,8 @@ class RedfishUtils(object):
else: else:
if media_match_strict: if media_match_strict:
continue continue
# if ejected, 'Inserted' should be False and 'ImageName' cleared # if ejected, 'Inserted' should be False
if (not data.get('Inserted', False) and if (not data.get('Inserted', False)):
not data.get('ImageName')):
return uri, data return uri, data
return None, None return None, None
@@ -2225,7 +2224,7 @@ class RedfishUtils(object):
return resources, headers return resources, headers
@staticmethod @staticmethod
def _insert_virt_media_payload(options, param_map, data, ai): def _insert_virt_media_payload(options, param_map, data, ai, image_only=False):
payload = { payload = {
'Image': options.get('image_url') 'Image': options.get('image_url')
} }
@@ -2239,6 +2238,12 @@ class RedfishUtils(object):
options.get(option), option, options.get(option), option,
allowable)} allowable)}
payload[param] = options.get(option) payload[param] = options.get(option)
# Some hardware (such as iLO 4 or Supermicro) only supports the Image property
# Inserted and WriteProtected are not writable
if image_only:
del payload['Inserted']
del payload['WriteProtected']
return payload return payload
def virtual_media_insert_via_patch(self, options, param_map, uri, data, image_only=False): def virtual_media_insert_via_patch(self, options, param_map, uri, data, image_only=False):
@@ -2247,16 +2252,10 @@ class RedfishUtils(object):
{'AllowableValues': v}) for k, v in data.items() {'AllowableValues': v}) for k, v in data.items()
if k.endswith('@Redfish.AllowableValues')) if k.endswith('@Redfish.AllowableValues'))
# construct payload # construct payload
payload = self._insert_virt_media_payload(options, param_map, data, ai) payload = self._insert_virt_media_payload(options, param_map, data, ai, image_only)
if 'Inserted' not in payload: if 'Inserted' not in payload and not image_only:
payload['Inserted'] = True payload['Inserted'] = True
# Some hardware (such as iLO 4) only supports the Image property on the PATCH operation
# Inserted and WriteProtected are not writable
if image_only:
del payload['Inserted']
del payload['WriteProtected']
# PATCH the resource # PATCH the resource
response = self.patch_request(self.root_uri + uri, payload) response = self.patch_request(self.root_uri + uri, payload)
if response['ret'] is False: if response['ret'] is False:
@@ -2292,6 +2291,13 @@ class RedfishUtils(object):
if data["FirmwareVersion"].startswith("iLO 4"): if data["FirmwareVersion"].startswith("iLO 4"):
image_only = True image_only = True
# Supermicro does also not support Inserted and WriteProtected
# Supermicro uses as firmware version only a number so we can't check for it because we
# can't be sure that this firmware version is nut used by another vendor
# Tested with Supermicro Firmware 01.74.02
if 'Supermicro' in data['Oem']:
image_only = True
virt_media_uri = data["VirtualMedia"]["@odata.id"] virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri) response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False: if response['ret'] is False:
@@ -2346,7 +2352,7 @@ class RedfishUtils(object):
# get ActionInfo or AllowableValues # get ActionInfo or AllowableValues
ai = self._get_all_action_info_values(action) ai = self._get_all_action_info_values(action)
# construct payload # construct payload
payload = self._insert_virt_media_payload(options, param_map, data, ai) payload = self._insert_virt_media_payload(options, param_map, data, ai, image_only)
# POST to action # POST to action
response = self.post_request(self.root_uri + action_uri, payload) response = self.post_request(self.root_uri + action_uri, payload)
if response['ret'] is False: if response['ret'] is False:
@@ -2392,6 +2398,9 @@ class RedfishUtils(object):
if data["FirmwareVersion"].startswith("iLO 4"): if data["FirmwareVersion"].startswith("iLO 4"):
image_only = True image_only = True
if 'Supermicro' in data['Oem']:
image_only = True
virt_media_uri = data["VirtualMedia"]["@odata.id"] virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri) response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False: if response['ret'] is False:

View File

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# (c) 2022, Alexei Znamensky <russoz@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.parsing.convert_bool import boolean
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt as fmt
@fmt.unpack_args
def _values_fmt(values, value_types):
result = []
for value, value_type in zip(values, value_types):
if value_type == 'bool':
value = boolean(value)
result.extend(['--type', '{0}'.format(value_type), '--set', '{0}'.format(value)])
return result
def xfconf_runner(module, **kwargs):
runner = CmdRunner(
module,
command='xfconf-query',
arg_formats=dict(
channel=fmt.as_opt_val("--channel"),
property=fmt.as_opt_val("--property"),
force_array=fmt.as_bool("--force-array"),
reset=fmt.as_bool("--reset"),
create=fmt.as_bool("--create"),
list_arg=fmt.as_bool("--list"),
values_and_types=fmt.as_func(_values_fmt),
),
**kwargs
)
return runner

View File

@@ -83,7 +83,7 @@ options:
version_added: 1.3.0 version_added: 1.3.0
clone: clone:
description: description:
- Name of VM to be cloned. If C(vmid) is setted, C(clone) can take arbitrary value but required for initiating the clone. - Name of VM to be cloned. If I(vmid) is set, I(clone) can take an arbitrary value but is required for initiating the clone.
type: str type: str
cores: cores:
description: description:

View File

@@ -0,0 +1,209 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Scaleway VPC management module
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: scaleway_compute_private_network
short_description: Scaleway compute - private network management
version_added: 5.2.0
author: Pascal MANGIN (@pastral)
description:
- This module add or remove a private network to a compute instance
(U(https://developer.scaleway.com)).
extends_documentation_fragment:
- community.general.scaleway
options:
state:
type: str
description:
- Indicate desired state of the VPC.
default: present
choices:
- present
- absent
project:
type: str
description:
- Project identifier.
required: true
region:
type: str
description:
- Scaleway region to use (for example C(par1)).
required: true
choices:
- ams1
- EMEA-NL-EVS
- par1
- EMEA-FR-PAR1
- par2
- EMEA-FR-PAR2
- waw1
- EMEA-PL-WAW1
compute_id:
type: str
description:
- ID of the compute instance (see M(community.general.scaleway_compute)).
required: true
private_network_id:
type: str
description:
- ID of the private network (see M(community.general.scaleway_private_network)).
required: true
'''
EXAMPLES = '''
- name: Plug a VM to a private network
community.general.scaleway_compute_private_network:
project: '{{ scw_project }}'
state: present
region: par1
compute_id: "12345678-f1e6-40ec-83e5-12345d67ed89"
private_network_id: "22345678-f1e6-40ec-83e5-12345d67ed89"
register: nicsvpc_creation_task
- name: Unplug a VM from a private network
community.general.scaleway_compute_private_network:
project: '{{ scw_project }}'
state: absent
region: par1
compute_id: "12345678-f1e6-40ec-83e5-12345d67ed89"
private_network_id: "22345678-f1e6-40ec-83e5-12345d67ed89"
'''
RETURN = '''
scaleway_compute_private_network:
description: Information on the VPC.
returned: success when C(state=present)
type: dict
sample:
{
"created_at": "2022-01-15T11:11:12.676445Z",
"id": "12345678-f1e6-40ec-83e5-12345d67ed89",
"name": "network",
"organization_id": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"project_id": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"tags": [
"tag1",
"tag2",
"tag3",
"tag4",
"tag5"
],
"updated_at": "2022-01-15T11:12:04.624837Z",
"zone": "fr-par-2"
}
'''
from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_LOCATION, scaleway_argument_spec, Scaleway
from ansible.module_utils.basic import AnsibleModule
def get_nics_info(api, compute_id, private_network_id):
response = api.get('servers/' + compute_id + '/private_nics')
if not response.ok:
msg = "Error during get servers information: %s: '%s' (%s)" % (response.info['msg'], response.json['message'], response.json)
api.module.fail_json(msg=msg)
i = 0
list_nics = response.json['private_nics']
while i < len(list_nics):
if list_nics[i]['private_network_id'] == private_network_id:
return list_nics[i]
i += 1
return None
def present_strategy(api, compute_id, private_network_id):
changed = False
nic = get_nics_info(api, compute_id, private_network_id)
if nic is not None:
return changed, nic
data = {"private_network_id": private_network_id}
changed = True
if api.module.check_mode:
return changed, {"status": "a private network would be add to a server"}
response = api.post(path='servers/' + compute_id + '/private_nics', data=data)
if not response.ok:
api.module.fail_json(msg='Error when adding a private network to a server [{0}: {1}]'.format(response.status_code, response.json))
return changed, response.json
def absent_strategy(api, compute_id, private_network_id):
changed = False
nic = get_nics_info(api, compute_id, private_network_id)
if nic is None:
return changed, {}
changed = True
if api.module.check_mode:
return changed, {"status": "private network would be destroyed"}
response = api.delete('servers/' + compute_id + '/private_nics/' + nic['id'])
if not response.ok:
api.module.fail_json(msg='Error deleting private network from server [{0}: {1}]'.format(
response.status_code, response.json))
return changed, response.json
def core(module):
compute_id = module.params['compute_id']
pn_id = module.params['private_network_id']
region = module.params["region"]
module.params['api_url'] = SCALEWAY_LOCATION[region]["api_endpoint"]
api = Scaleway(module=module)
if module.params["state"] == "absent":
changed, summary = absent_strategy(api=api, compute_id=compute_id, private_network_id=pn_id)
else:
changed, summary = present_strategy(api=api, compute_id=compute_id, private_network_id=pn_id)
module.exit_json(changed=changed, scaleway_compute_private_network=summary)
def main():
argument_spec = scaleway_argument_spec()
argument_spec.update(dict(
state=dict(default='present', choices=['absent', 'present']),
project=dict(required=True),
region=dict(required=True, choices=list(SCALEWAY_LOCATION.keys())),
compute_id=dict(required=True),
private_network_id=dict(required=True)
))
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
core(module)
if __name__ == '__main__':
main()

View File

@@ -45,8 +45,8 @@ options:
- The interface to bind the connection to. - The interface to bind the connection to.
- The connection will only be applicable to this interface name. - The connection will only be applicable to this interface name.
- A special value of C('*') can be used for interface-independent connections. - A special value of C('*') can be used for interface-independent connections.
- The ifname argument is mandatory for all connection types except bond, team, bridge and vlan. - The ifname argument is mandatory for all connection types except bond, team, bridge, vlan and vpn.
- This parameter defaults to C(conn_name) when left unset. - This parameter defaults to C(conn_name) when left unset for all connection types except vpn that removes it.
type: str type: str
type: type:
description: description:
@@ -55,10 +55,11 @@ options:
- Type C(generic) is added in Ansible 2.5. - Type C(generic) is added in Ansible 2.5.
- Type C(infiniband) is added in community.general 2.0.0. - Type C(infiniband) is added in community.general 2.0.0.
- Type C(gsm) is added in community.general 3.7.0. - Type C(gsm) is added in community.general 3.7.0.
- Type C(wireguard) is added in community.general 4.3.0 - Type C(wireguard) is added in community.general 4.3.0.
- Type C(vpn) is added in community.general 5.1.0.
type: str type: str
choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm, choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm,
wireguard ] wireguard, vpn ]
mode: mode:
description: description:
- This is the type of device or network connection that you wish to create for a bond or bridge. - This is the type of device or network connection that you wish to create for a bond or bridge.
@@ -905,6 +906,58 @@ options:
description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property. description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property.
type: int type: int
choices: [ 0, 1, 2 ] choices: [ 0, 1, 2 ]
vpn:
description:
- Configuration of a VPN connection (PPTP and L2TP).
- In order to use L2TP you need to be sure that C(network-manager-l2tp) - and C(network-manager-l2tp-gnome)
if host has UI - are installed on the host.
type: dict
version_added: 5.1.0
suboptions:
permissions:
description: User that will have permission to use the connection.
type: str
required: true
service-type:
description: This defines the service type of connection.
type: str
required: true
choices: [ pptp, l2tp ]
gateway:
description: The gateway to connection. It can be an IP address (for example C(192.0.2.1))
or a FQDN address (for example C(vpn.example.com)).
type: str
required: true
password-flags:
description:
- NMSettingSecretFlags indicating how to handle the I(password) property.
- 'Following choices are allowed:
C(0) B(NONE): The system is responsible for providing and storing this secret (default);
C(1) B(AGENT_OWNED): A user secret agent is responsible for providing and storing this secret; when it is required agents will be
asked to retrieve it;
C(2) B(NOT_SAVED): This secret should not be saved, but should be requested from the user each time it is needed;
C(4) B(NOT_REQUIRED): In situations where it cannot be automatically determined that the secret is required
(some VPNs and PPP providers do not require all secrets) this flag indicates that the specific secret is not required.'
type: int
choices: [ 0, 1, 2 , 4 ]
default: 0
user:
description: Username provided by VPN administrator.
type: str
required: true
ipsec-enabled:
description:
- Enable or disable IPSec tunnel to L2TP host.
- This option is need when C(service-type) is C(l2tp).
type: bool
choices: [ yes, no ]
ipsec-psk:
description:
- The pre-shared key in base64 encoding.
- >
You can encode using this Ansible jinja2 expression: C("0s{{ '[YOUR PRE-SHARED KEY]' | ansible.builtin.b64encode }}").
- This is only used when I(ipsec-enabled=true).
type: str
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@@ -1288,6 +1341,23 @@ EXAMPLES = r'''
autoconnect: true autoconnect: true
state: present state: present
- name: >-
Create a VPN L2TP connection for ansible_user to connect on vpn.example.com
authenticating with user 'brittany' and pre-shared key as 'Brittany123'
community.general.nmcli:
type: vpn
conn_name: my-vpn-connection
vpn:
permissions: "{{ ansible_user }}"
service-type: l2tp
gateway: vpn.example.com
password-flags: 2
user: brittany
ipsec-enabled: true
ipsec-psk: "0s{{ 'Brittany123' | ansible.builtin.b64encode }}"
autoconnect: false
state: present
''' '''
RETURN = r"""# RETURN = r"""#
@@ -1404,6 +1474,7 @@ class Nmcli(object):
self.wifi_sec = module.params['wifi_sec'] self.wifi_sec = module.params['wifi_sec']
self.gsm = module.params['gsm'] self.gsm = module.params['gsm']
self.wireguard = module.params['wireguard'] self.wireguard = module.params['wireguard']
self.vpn = module.params['vpn']
if self.method4: if self.method4:
self.ipv4_method = self.method4 self.ipv4_method = self.method4
@@ -1592,6 +1663,29 @@ class Nmcli(object):
options.update({ options.update({
'wireguard.%s' % name: value, 'wireguard.%s' % name: value,
}) })
elif self.type == 'vpn':
if self.vpn:
vpn_data_values = ''
for name, value in self.vpn.items():
if name == 'service-type':
options.update({
'vpn-type': value,
})
elif name == 'permissions':
options.update({
'connection.permissions': value,
})
else:
if vpn_data_values != '':
vpn_data_values += ', '
if isinstance(value, bool):
value = self.bool_to_string(value)
vpn_data_values += '%s=%s' % (name, value)
options.update({
'vpn.data': vpn_data_values,
})
# Convert settings values based on the situation. # Convert settings values based on the situation.
for setting, value in options.items(): for setting, value in options.items():
setting_type = self.settings_type(setting) setting_type = self.settings_type(setting)
@@ -1742,7 +1836,10 @@ class Nmcli(object):
@staticmethod @staticmethod
def list_to_string(lst): def list_to_string(lst):
return ",".join(lst or [""]) if lst is None:
return None
else:
return ",".join(lst)
@staticmethod @staticmethod
def settings_type(setting): def settings_type(setting):
@@ -1832,6 +1929,10 @@ class Nmcli(object):
'connection.interface-name': ifname, 'connection.interface-name': ifname,
} }
# VPN doesn't need an interface but if sended it must be a valid interface.
if self.type == 'vpn' and self.ifname is None:
del options['connection.interface-name']
options.update(self.connection_options()) options.update(self.connection_options())
# Constructing the command. # Constructing the command.
@@ -1997,6 +2098,9 @@ class Nmcli(object):
current_value = current_value.strip('"') current_value = current_value.strip('"')
if key == self.mtu_setting and self.mtu is None: if key == self.mtu_setting and self.mtu is None:
self.mtu = 0 self.mtu = 0
if key == 'vpn.data':
current_value = list(map(str.strip, current_value.split(',')))
value = list(map(str.strip, value.split(',')))
else: else:
# parameter does not exist # parameter does not exist
current_value = None current_value = None
@@ -2025,6 +2129,10 @@ class Nmcli(object):
'connection.interface-name': self.ifname, 'connection.interface-name': self.ifname,
} }
# VPN doesn't need an interface but if sended it must be a valid interface.
if self.type == 'vpn' and self.ifname is None:
del options['connection.interface-name']
if not self.type: if not self.type:
current_con_type = self.show_connection().get('connection.type') current_con_type = self.show_connection().get('connection.type')
if current_con_type: if current_con_type:
@@ -2064,6 +2172,7 @@ def main():
'wifi', 'wifi',
'gsm', 'gsm',
'wireguard', 'wireguard',
'vpn',
]), ]),
ip4=dict(type='list', elements='str'), ip4=dict(type='list', elements='str'),
gw4=dict(type='str'), gw4=dict(type='str'),
@@ -2163,6 +2272,7 @@ def main():
wifi_sec=dict(type='dict', no_log=True), wifi_sec=dict(type='dict', no_log=True),
gsm=dict(type='dict'), gsm=dict(type='dict'),
wireguard=dict(type='dict'), wireguard=dict(type='dict'),
vpn=dict(type='dict'),
), ),
mutually_exclusive=[['never_default4', 'gw4'], mutually_exclusive=[['never_default4', 'gw4'],
['routes4_extended', 'routes4'], ['routes4_extended', 'routes4'],

View File

@@ -226,7 +226,7 @@ class AnsibleGalaxyInstall(CmdModuleHelper):
check_rc = True check_rc = True
def _get_ansible_galaxy_version(self): def _get_ansible_galaxy_version(self):
ansible_galaxy = self.module.get_bin_path("ansible-galaxy", required=True) ansible_galaxy = self.get_bin_path("ansible-galaxy", required=True)
dummy, out, dummy = self.module.run_command([ansible_galaxy, "--version"], check_rc=True) dummy, out, dummy = self.module.run_command([ansible_galaxy, "--version"], check_rc=True)
line = out.splitlines()[0] line = out.splitlines()[0]
match = self._RE_GALAXY_VERSION.match(line) match = self._RE_GALAXY_VERSION.match(line)
@@ -302,9 +302,9 @@ class AnsibleGalaxyInstall(CmdModuleHelper):
self.vars.set("new_roles", {}) self.vars.set("new_roles", {})
self.vars.set("ansible29_change", False, change=True, output=False) self.vars.set("ansible29_change", False, change=True, output=False)
if not (self.vars.ack_ansible29 or self.vars.ack_min_ansiblecore211): if not (self.vars.ack_ansible29 or self.vars.ack_min_ansiblecore211):
self.module.warn("Ansible 2.9 or older: unable to retrieve lists of roles and collections already installed") self.warn("Ansible 2.9 or older: unable to retrieve lists of roles and collections already installed")
if self.vars.requirements_file is not None and self.vars.type == 'both': if self.vars.requirements_file is not None and self.vars.type == 'both':
self.module.warn("Ansible 2.9 or older: will install only roles from requirement files") self.warn("Ansible 2.9 or older: will install only roles from requirement files")
def _setup210plus(self): def _setup210plus(self):
self.vars.set("new_collections", {}, change=True) self.vars.set("new_collections", {}, change=True)

View File

@@ -468,7 +468,7 @@ class Rhsm(RegistrationBase):
items = ["--all"] items = ["--all"]
if items: if items:
args = [SUBMAN_CMD, 'unsubscribe'] + items args = [SUBMAN_CMD, 'remove'] + items
rc, stderr, stdout = self.module.run_command(args, check_rc=True) rc, stderr, stdout = self.module.run_command(args, check_rc=True)
return serials return serials

View File

@@ -3,6 +3,7 @@
# Copyright: (c) 2014, Gabe Mulley <gabe.mulley@gmail.com> # Copyright: (c) 2014, Gabe Mulley <gabe.mulley@gmail.com>
# Copyright: (c) 2015, David Wittman <dwittman@gmail.com> # Copyright: (c) 2015, David Wittman <dwittman@gmail.com>
# Copyright: (c) 2022, Marius Rieder <marius.rieder@scs.com>
# 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)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
@@ -17,6 +18,7 @@ description:
- Manages symbolic links using the 'update-alternatives' tool. - Manages symbolic links using the 'update-alternatives' tool.
- Useful when multiple programs are installed but provide similar functionality (e.g. different editors). - Useful when multiple programs are installed but provide similar functionality (e.g. different editors).
author: author:
- Marius Rieder (@jiuka)
- David Wittman (@DavidWittman) - David Wittman (@DavidWittman)
- Gabe Mulley (@mulby) - Gabe Mulley (@mulby)
options: options:
@@ -38,19 +40,45 @@ options:
type: path type: path
priority: priority:
description: description:
- The priority of the alternative. - The priority of the alternative. If no priority is given for creation C(50) is used as a fallback.
type: int type: int
default: 50
state: state:
description: description:
- C(present) - install the alternative (if not already installed), but do - C(present) - install the alternative (if not already installed), but do
not set it as the currently selected alternative for the group. not set it as the currently selected alternative for the group.
- C(selected) - install the alternative (if not already installed), and - C(selected) - install the alternative (if not already installed), and
set it as the currently selected alternative for the group. set it as the currently selected alternative for the group.
choices: [ present, selected ] - C(auto) - install the alternative (if not already installed), and
set the group to auto mode. Added in community.general 5.1.0.
- C(absent) - removes the alternative. Added in community.general 5.1.0.
choices: [ present, selected, auto, absent ]
default: selected default: selected
type: str type: str
version_added: 4.8.0 version_added: 4.8.0
subcommands:
description:
- A list of subcommands.
- Each subcommand needs a name, a link and a path parameter.
type: list
elements: dict
aliases: ['slaves']
suboptions:
name:
description:
- The generic name of the subcommand.
type: str
required: true
path:
description:
- The path to the real executable that the subcommand should point to.
type: path
required: true
link:
description:
- The path to the symbolic link that should point to the real subcommand executable.
type: path
required: true
version_added: 5.1.0
requirements: [ update-alternatives ] requirements: [ update-alternatives ]
''' '''
@@ -78,6 +106,23 @@ EXAMPLES = r'''
path: /usr/bin/python3.5 path: /usr/bin/python3.5
link: /usr/bin/python link: /usr/bin/python
state: present state: present
- name: Install Python 3.5 and reset selection to auto
community.general.alternatives:
name: python
path: /usr/bin/python3.5
link: /usr/bin/python
state: auto
- name: keytool is a subcommand of java
community.general.alternatives:
name: java
link: /usr/bin/java
path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
subcommands:
- name: keytool
link: /usr/bin/keytool
path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/keytool
''' '''
import os import os
@@ -90,10 +135,238 @@ from ansible.module_utils.basic import AnsibleModule
class AlternativeState: class AlternativeState:
PRESENT = "present" PRESENT = "present"
SELECTED = "selected" SELECTED = "selected"
ABSENT = "absent"
AUTO = "auto"
@classmethod @classmethod
def to_list(cls): def to_list(cls):
return [cls.PRESENT, cls.SELECTED] return [cls.PRESENT, cls.SELECTED, cls.ABSENT, cls.AUTO]
class AlternativesModule(object):
_UPDATE_ALTERNATIVES = None
def __init__(self, module):
self.module = module
self.result = dict(changed=False, diff=dict(before=dict(), after=dict()))
self.module.run_command_environ_update = {'LC_ALL': 'C'}
self.messages = []
self.run()
@property
def mode_present(self):
return self.module.params.get('state') in [AlternativeState.PRESENT, AlternativeState.SELECTED, AlternativeState.AUTO]
@property
def mode_selected(self):
return self.module.params.get('state') == AlternativeState.SELECTED
@property
def mode_auto(self):
return self.module.params.get('state') == AlternativeState.AUTO
def run(self):
self.parse()
if self.mode_present:
# Check if we need to (re)install
subcommands_parameter = self.module.params['subcommands']
priority_parameter = self.module.params['priority']
if (
self.path not in self.current_alternatives or
(priority_parameter is not None and self.current_alternatives[self.path].get('priority') != priority_parameter) or
(subcommands_parameter is not None and (
not all(s in subcommands_parameter for s in self.current_alternatives[self.path].get('subcommands')) or
not all(s in self.current_alternatives[self.path].get('subcommands') for s in subcommands_parameter)
))
):
self.install()
# Check if we need to set the preference
if self.mode_selected and self.current_path != self.path:
self.set()
# Check if we need to reset to auto
if self.mode_auto and self.current_mode == 'manual':
self.auto()
else:
# Check if we need to uninstall
if self.path in self.current_alternatives:
self.remove()
self.result['msg'] = ' '.join(self.messages)
self.module.exit_json(**self.result)
def install(self):
if not os.path.exists(self.path):
self.module.fail_json(msg="Specified path %s does not exist" % self.path)
if not self.link:
self.module.fail_json(msg='Needed to install the alternative, but unable to do so as we are missing the link')
cmd = [self.UPDATE_ALTERNATIVES, '--install', self.link, self.name, self.path, str(self.priority)]
if self.module.params['subcommands'] is not None:
subcommands = [['--slave', subcmd['link'], subcmd['name'], subcmd['path']] for subcmd in self.subcommands]
cmd += [item for sublist in subcommands for item in sublist]
self.result['changed'] = True
self.messages.append("Install alternative '%s' for '%s'." % (self.path, self.name))
if not self.module.check_mode:
self.module.run_command(cmd, check_rc=True)
if self.module._diff:
self.result['diff']['after'] = dict(
state=AlternativeState.PRESENT,
path=self.path,
priority=self.priority,
link=self.link,
)
if self.subcommands:
self.result['diff']['after'].update(dict(
subcommands=self.subcommands
))
def remove(self):
cmd = [self.UPDATE_ALTERNATIVES, '--remove', self.name, self.path]
self.result['changed'] = True
self.messages.append("Remove alternative '%s' from '%s'." % (self.path, self.name))
if not self.module.check_mode:
self.module.run_command(cmd, check_rc=True)
if self.module._diff:
self.result['diff']['after'] = dict(state=AlternativeState.ABSENT)
def set(self):
cmd = [self.UPDATE_ALTERNATIVES, '--set', self.name, self.path]
self.result['changed'] = True
self.messages.append("Set alternative '%s' for '%s'." % (self.path, self.name))
if not self.module.check_mode:
self.module.run_command(cmd, check_rc=True)
if self.module._diff:
self.result['diff']['after']['state'] = AlternativeState.SELECTED
def auto(self):
cmd = [self.UPDATE_ALTERNATIVES, '--auto', self.name]
self.messages.append("Set alternative to auto for '%s'." % (self.name))
self.result['changed'] = True
if not self.module.check_mode:
self.module.run_command(cmd, check_rc=True)
if self.module._diff:
self.result['diff']['after']['state'] = AlternativeState.PRESENT
@property
def name(self):
return self.module.params.get('name')
@property
def path(self):
return self.module.params.get('path')
@property
def link(self):
return self.module.params.get('link') or self.current_link
@property
def priority(self):
if self.module.params.get('priority') is not None:
return self.module.params.get('priority')
return self.current_alternatives.get(self.path, {}).get('priority', 50)
@property
def subcommands(self):
if self.module.params.get('subcommands') is not None:
return self.module.params.get('subcommands')
elif self.path in self.current_alternatives and self.current_alternatives[self.path].get('subcommands'):
return self.current_alternatives[self.path].get('subcommands')
return None
@property
def UPDATE_ALTERNATIVES(self):
if self._UPDATE_ALTERNATIVES is None:
self._UPDATE_ALTERNATIVES = self.module.get_bin_path('update-alternatives', True)
return self._UPDATE_ALTERNATIVES
def parse(self):
self.current_mode = None
self.current_path = None
self.current_link = None
self.current_alternatives = {}
# Run `update-alternatives --display <name>` to find existing alternatives
(rc, display_output, dummy) = self.module.run_command(
[self.UPDATE_ALTERNATIVES, '--display', self.name]
)
if rc != 0:
self.module.debug("No current alternative found. '%s' exited with %s" % (self.UPDATE_ALTERNATIVES, rc))
return
current_mode_regex = re.compile(r'\s-\s(?:status\sis\s)?(\w*)(?:\smode|.)$', re.MULTILINE)
current_path_regex = re.compile(r'^\s*link currently points to (.*)$', re.MULTILINE)
current_link_regex = re.compile(r'^\s*link \w+ is (.*)$', re.MULTILINE)
subcmd_path_link_regex = re.compile(r'^\s*slave (\S+) is (.*)$', re.MULTILINE)
alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s\S+\s)?priority\s(\d+)((?:\s+slave.*)*)', re.MULTILINE)
subcmd_regex = re.compile(r'^\s+slave (.*): (.*)$', re.MULTILINE)
match = current_mode_regex.search(display_output)
if not match:
self.module.debug("No current mode found in output")
return
self.current_mode = match.group(1)
match = current_path_regex.search(display_output)
if not match:
self.module.debug("No current path found in output")
else:
self.current_path = match.group(1)
match = current_link_regex.search(display_output)
if not match:
self.module.debug("No current link found in output")
else:
self.current_link = match.group(1)
subcmd_path_map = dict(subcmd_path_link_regex.findall(display_output))
if not subcmd_path_map and self.subcommands:
subcmd_path_map = dict((s['name'], s['link']) for s in self.subcommands)
for path, prio, subcmd in alternative_regex.findall(display_output):
self.current_alternatives[path] = dict(
priority=int(prio),
subcommands=[dict(
name=name,
path=spath,
link=subcmd_path_map.get(name)
) for name, spath in subcmd_regex.findall(subcmd) if spath != '(null)']
)
if self.module._diff:
if self.path in self.current_alternatives:
self.result['diff']['before'].update(dict(
state=AlternativeState.PRESENT,
path=self.path,
priority=self.current_alternatives[self.path].get('priority'),
link=self.current_link,
))
if self.current_alternatives[self.path].get('subcommands'):
self.result['diff']['before'].update(dict(
subcommands=self.current_alternatives[self.path].get('subcommands')
))
if self.current_mode == 'manual' and self.current_path != self.path:
self.result['diff']['before'].update(dict(
state=AlternativeState.SELECTED
))
else:
self.result['diff']['before'].update(dict(
state=AlternativeState.ABSENT
))
def main(): def main():
@@ -103,115 +376,22 @@ def main():
name=dict(type='str', required=True), name=dict(type='str', required=True),
path=dict(type='path', required=True), path=dict(type='path', required=True),
link=dict(type='path'), link=dict(type='path'),
priority=dict(type='int', default=50), priority=dict(type='int'),
state=dict( state=dict(
type='str', type='str',
choices=AlternativeState.to_list(), choices=AlternativeState.to_list(),
default=AlternativeState.SELECTED, default=AlternativeState.SELECTED,
), ),
subcommands=dict(type='list', elements='dict', aliases=['slaves'], options=dict(
name=dict(type='str', required=True),
path=dict(type='path', required=True),
link=dict(type='path', required=True),
)),
), ),
supports_check_mode=True, supports_check_mode=True,
) )
params = module.params AlternativesModule(module)
name = params['name']
path = params['path']
link = params['link']
priority = params['priority']
state = params['state']
UPDATE_ALTERNATIVES = module.get_bin_path('update-alternatives', True)
current_path = None
all_alternatives = []
# Run `update-alternatives --display <name>` to find existing alternatives
(rc, display_output, dummy) = module.run_command(
['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--display', name]
)
if rc == 0:
# Alternatives already exist for this link group
# Parse the output to determine the current path of the symlink and
# available alternatives
current_path_regex = re.compile(r'^\s*link currently points to (.*)$',
re.MULTILINE)
alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s\S+\s)?priority', re.MULTILINE)
match = current_path_regex.search(display_output)
if match:
current_path = match.group(1)
all_alternatives = alternative_regex.findall(display_output)
if not link:
# Read the current symlink target from `update-alternatives --query`
# in case we need to install the new alternative before setting it.
#
# This is only compatible on Debian-based systems, as the other
# alternatives don't have --query available
rc, query_output, dummy = module.run_command(
['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--query', name]
)
if rc == 0:
for line in query_output.splitlines():
if line.startswith('Link:'):
link = line.split()[1]
break
changed = False
if current_path != path:
# Check mode: expect a change if this alternative is not already
# installed, or if it is to be set as the current selection.
if module.check_mode:
module.exit_json(
changed=(
path not in all_alternatives or
state == AlternativeState.SELECTED
),
current_path=current_path,
)
try:
# install the requested path if necessary
if path not in all_alternatives:
if not os.path.exists(path):
module.fail_json(msg="Specified path %s does not exist" % path)
if not link:
module.fail_json(msg="Needed to install the alternative, but unable to do so as we are missing the link")
module.run_command(
[UPDATE_ALTERNATIVES, '--install', link, name, path, str(priority)],
check_rc=True
)
changed = True
# set the current selection to this path (if requested)
if state == AlternativeState.SELECTED:
module.run_command(
[UPDATE_ALTERNATIVES, '--set', name, path],
check_rc=True
)
changed = True
except subprocess.CalledProcessError as cpe:
module.fail_json(msg=str(dir(cpe)))
elif current_path == path and state == AlternativeState.PRESENT:
# Case where alternative is currently selected, but state is set
# to 'present'. In this case, we set to auto mode.
if module.check_mode:
module.exit_json(changed=True, current_path=current_path)
changed = True
try:
module.run_command(
[UPDATE_ALTERNATIVES, '--auto', name],
check_rc=True,
)
except subprocess.CalledProcessError as cpe:
module.fail_json(msg=str(dir(cpe)))
module.exit_json(changed=changed)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -0,0 +1,74 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2022, Alexei Znamensky <russoz@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: gconftool2_info
author:
- "Alexei Znamensky (@russoz)"
short_description: Retrieve GConf configurations
version_added: 5.1.0
description:
- This module allows retrieving application preferences from the GConf database, with the help of C(gconftool-2).
options:
key:
description:
- The key name for an element in the GConf database.
type: str
required: true
notes:
- See man gconftool-2(1) for more details.
seealso:
- name: gconf repository (archived)
description: Git repository for the project. It is an archived project, so the repository is read-only.
link: https://gitlab.gnome.org/Archive/gconf
'''
EXAMPLES = """
- name: Get value for a certain key in the database.
community.general.gconftool2_info:
key: /desktop/gnome/background/picture_filename
register: result
"""
RETURN = '''
value:
description:
- The value of the property.
returned: success
type: str
sample: Monospace 10
'''
from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper
from ansible_collections.community.general.plugins.module_utils.gconftool2 import gconftool2_runner
class GConftoolInfo(ModuleHelper):
output_params = ['key']
module = dict(
argument_spec=dict(
key=dict(type='str', required=True, no_log=False),
),
supports_check_mode=True,
)
def __init_module__(self):
self.runner = gconftool2_runner(self.module, check_rc=True)
def __run__(self):
with self.runner.context(args_order=["get", "key"]) as ctx:
rc, out, err = ctx.run(get=True)
self.vars.value = None if err and not out else out.rstrip()
def main():
GConftoolInfo.execute()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,270 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Alexander Hussey <ahussey@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
Ansible Module - community.general.keyring
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
---
module: keyring
version_added: 5.2.0
author:
- Alexander Hussey (@ahussey-redhat)
short_description: Set or delete a passphrase using the Operating System's native keyring
description: >-
This module uses the L(keyring Python library, https://pypi.org/project/keyring/)
to set or delete passphrases for a given service and username from the OS' native keyring.
requirements:
- keyring (Python library)
- gnome-keyring (application - required for headless Gnome keyring access)
- dbus-run-session (application - required for headless Gnome keyring access)
options:
service:
description: The name of the service.
required: true
type: str
username:
description: The user belonging to the service.
required: true
type: str
user_password:
description: The password to set.
required: false
type: str
aliases:
- password
keyring_password:
description: Password to unlock keyring.
required: true
type: str
state:
description: Whether the password should exist.
required: false
default: present
type: str
choices:
- present
- absent
"""
EXAMPLES = r"""
- name: Set a password for test/test1
community.general.keyring:
service: test
username: test1
user_password: "{{ user_password }}"
keyring_password: "{{ keyring_password }}"
- name: Delete the password for test/test1
community.general.keyring:
service: test
username: test1
user_password: "{{ user_password }}"
keyring_password: "{{ keyring_password }}"
state: absent
"""
try:
from shlex import quote
except ImportError:
from pipes import quote
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
try:
import keyring
HAS_KEYRING = True
except ImportError:
HAS_KEYRING = False
KEYRING_IMP_ERR = traceback.format_exc()
def del_passphrase(module):
"""
Attempt to delete a passphrase in the keyring using the Python API and fallback to using a shell.
"""
if module.check_mode:
return None
try:
keyring.delete_password(module.params["service"], module.params["username"])
return None
except keyring.errors.KeyringLocked as keyring_locked_err: # pylint: disable=unused-variable
delete_argument = (
'echo "%s" | gnome-keyring-daemon --unlock\nkeyring del %s %s\n'
% (
quote(module.params["keyring_password"]),
quote(module.params["service"]),
quote(module.params["username"]),
)
)
dummy, dummy, stderr = module.run_command(
"dbus-run-session -- /bin/bash",
use_unsafe_shell=True,
data=delete_argument,
encoding=None,
)
if not stderr.decode("UTF-8"):
return None
return stderr.decode("UTF-8")
def set_passphrase(module):
"""
Attempt to set passphrase in the keyring using the Python API and fallback to using a shell.
"""
if module.check_mode:
return None
try:
keyring.set_password(
module.params["service"],
module.params["username"],
module.params["user_password"],
)
return None
except keyring.errors.KeyringLocked as keyring_locked_err: # pylint: disable=unused-variable
set_argument = (
'echo "%s" | gnome-keyring-daemon --unlock\nkeyring set %s %s\n%s\n'
% (
quote(module.params["keyring_password"]),
quote(module.params["service"]),
quote(module.params["username"]),
quote(module.params["user_password"]),
)
)
dummy, dummy, stderr = module.run_command(
"dbus-run-session -- /bin/bash",
use_unsafe_shell=True,
data=set_argument,
encoding=None,
)
if not stderr.decode("UTF-8"):
return None
return stderr.decode("UTF-8")
def get_passphrase(module):
"""
Attempt to retrieve passphrase from keyring using the Python API and fallback to using a shell.
"""
try:
passphrase = keyring.get_password(
module.params["service"], module.params["username"]
)
return passphrase
except keyring.errors.KeyringLocked:
pass
except keyring.errors.InitError:
pass
except AttributeError:
pass
get_argument = 'echo "%s" | gnome-keyring-daemon --unlock\nkeyring get %s %s\n' % (
quote(module.params["keyring_password"]),
quote(module.params["service"]),
quote(module.params["username"]),
)
dummy, stdout, dummy = module.run_command(
"dbus-run-session -- /bin/bash",
use_unsafe_shell=True,
data=get_argument,
encoding=None,
)
try:
return stdout.decode("UTF-8").splitlines()[1] # Only return the line containing the password
except IndexError:
return None
def run_module():
"""
Attempts to retrieve a passphrase from a keyring.
"""
result = dict(
changed=False,
msg="",
)
module_args = dict(
service=dict(type="str", required=True),
username=dict(type="str", required=True),
keyring_password=dict(type="str", required=True, no_log=True),
user_password=dict(
type="str", required=False, no_log=True, aliases=["password"]
),
state=dict(
type="str", required=False, default="present", choices=["absent", "present"]
),
)
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
if not HAS_KEYRING:
module.fail_json(msg=missing_required_lib("keyring"), exception=KEYRING_IMP_ERR)
passphrase = get_passphrase(module)
if module.params["state"] == "present":
if passphrase is not None:
if passphrase == module.params["user_password"]:
result["msg"] = "Passphrase already set for %s@%s" % (
module.params["service"],
module.params["username"],
)
if passphrase != module.params["user_password"]:
set_result = set_passphrase(module)
if set_result is None:
result["changed"] = True
result["msg"] = "Passphrase has been updated for %s@%s" % (
module.params["service"],
module.params["username"],
)
if set_result is not None:
module.fail_json(msg=set_result)
if passphrase is None:
set_result = set_passphrase(module)
if set_result is None:
result["changed"] = True
result["msg"] = "Passphrase has been updated for %s@%s" % (
module.params["service"],
module.params["username"],
)
if set_result is not None:
module.fail_json(msg=set_result)
if module.params["state"] == "absent":
if not passphrase:
result["result"] = "Passphrase already absent for %s@%s" % (
module.params["service"],
module.params["username"],
)
if passphrase:
del_result = del_passphrase(module)
if del_result is None:
result["changed"] = True
result["msg"] = "Passphrase has been removed for %s@%s" % (
module.params["service"],
module.params["username"],
)
if del_result is not None:
module.fail_json(msg=del_result)
module.exit_json(**result)
def main():
"""
main module loop
"""
run_module()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,149 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Alexander Hussey <ahussey@redhat.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
Ansible Module - community.general.keyring_info
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
---
module: keyring_info
version_added: 5.2.0
author:
- Alexander Hussey (@ahussey-redhat)
short_description: Get a passphrase using the Operating System's native keyring
description: >-
This module uses the L(keyring Python library, https://pypi.org/project/keyring/)
to retrieve passphrases for a given service and username from the OS' native keyring.
requirements:
- keyring (Python library)
- gnome-keyring (application - required for headless Linux keyring access)
- dbus-run-session (application - required for headless Linux keyring access)
options:
service:
description: The name of the service.
required: true
type: str
username:
description: The user belonging to the service.
required: true
type: str
keyring_password:
description: Password to unlock keyring.
required: true
type: str
"""
EXAMPLES = r"""
- name: Retrieve password for service_name/user_name
community.general.keyring_info:
service: test
username: test1
keyring_password: "{{ keyring_password }}"
register: test_password
- name: Display password
ansible.builtin.debug:
msg: "{{ test_password.passphrase }}"
"""
RETURN = r"""
passphrase:
description: A string containing the password.
returned: success and the password exists
type: str
sample: Password123
"""
try:
from shlex import quote
except ImportError:
from pipes import quote
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
try:
import keyring
HAS_KEYRING = True
except ImportError:
HAS_KEYRING = False
KEYRING_IMP_ERR = traceback.format_exc()
def _alternate_retrieval_method(module):
get_argument = 'echo "%s" | gnome-keyring-daemon --unlock\nkeyring get %s %s\n' % (
quote(module.params["keyring_password"]),
quote(module.params["service"]),
quote(module.params["username"]),
)
dummy, stdout, dummy = module.run_command(
"dbus-run-session -- /bin/bash",
use_unsafe_shell=True,
data=get_argument,
encoding=None,
)
try:
return stdout.decode("UTF-8").splitlines()[1]
except IndexError:
return None
def run_module():
"""
Attempts to retrieve a passphrase from a keyring.
"""
result = dict(changed=False, msg="")
module_args = dict(
service=dict(type="str", required=True),
username=dict(type="str", required=True),
keyring_password=dict(type="str", required=True, no_log=True),
)
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
if not HAS_KEYRING:
module.fail_json(msg=missing_required_lib("keyring"), exception=KEYRING_IMP_ERR)
try:
passphrase = keyring.get_password(
module.params["service"], module.params["username"]
)
except keyring.errors.KeyringLocked:
pass
except keyring.errors.InitError:
pass
except AttributeError:
pass
passphrase = _alternate_retrieval_method(module)
if passphrase is not None:
result["msg"] = "Successfully retrieved password for %s@%s" % (
module.params["service"],
module.params["username"],
)
result["passphrase"] = passphrase
if passphrase is None:
result["msg"] = "Password for %s@%s does not exist." % (
module.params["service"],
module.params["username"],
)
module.exit_json(**result)
def main():
"""
main module loop
"""
run_module()
if __name__ == "__main__":
main()

View File

@@ -51,6 +51,11 @@ options:
description: description:
- Puppet environment to be used. - Puppet environment to be used.
type: str type: str
confdir:
description:
- Path to the directory containing the puppet.conf file.
type: str
version_added: 5.1.0
logdest: logdest:
description: description:
- Where the puppet logs should go, if puppet apply is being used. - Where the puppet logs should go, if puppet apply is being used.
@@ -179,6 +184,7 @@ def main():
puppetmaster=dict(type='str'), puppetmaster=dict(type='str'),
modulepath=dict(type='str'), modulepath=dict(type='str'),
manifest=dict(type='str'), manifest=dict(type='str'),
confdir=dict(type='str'),
noop=dict(type='bool'), noop=dict(type='bool'),
logdest=dict(type='str', default='stdout', choices=['all', 'stdout', 'syslog']), logdest=dict(type='str', default='stdout', choices=['all', 'stdout', 'syslog']),
# The following is not related to Ansible's diff; see https://github.com/ansible-collections/community.general/pull/3980#issuecomment-1005666154 # The following is not related to Ansible's diff; see https://github.com/ansible-collections/community.general/pull/3980#issuecomment-1005666154
@@ -255,6 +261,8 @@ def main():
cmd += " --server %s" % shlex_quote(p['puppetmaster']) cmd += " --server %s" % shlex_quote(p['puppetmaster'])
if p['show_diff']: if p['show_diff']:
cmd += " --show_diff" cmd += " --show_diff"
if p['confdir']:
cmd += " --confdir %s" % shlex_quote(p['confdir'])
if p['environment']: if p['environment']:
cmd += " --environment '%s'" % p['environment'] cmd += " --environment '%s'" % p['environment']
if p['tags']: if p['tags']:

View File

@@ -65,6 +65,15 @@ options:
- The name of the user for the sudoers rule. - The name of the user for the sudoers rule.
- This option cannot be used in conjunction with I(group). - This option cannot be used in conjunction with I(group).
type: str type: str
validation:
description:
- If C(absent), the sudoers rule will be added without validation.
- If C(detect) and visudo is available, then the sudoers rule will be validated by visudo.
- If C(required), visudo must be available to validate the sudoers rule.
type: str
default: detect
choices: [ absent, detect, required ]
version_added: 5.2.0
''' '''
EXAMPLES = ''' EXAMPLES = '''
@@ -115,7 +124,11 @@ from ansible.module_utils.common.text.converters import to_native
class Sudoers(object): class Sudoers(object):
FILE_MODE = 0o440
def __init__(self, module): def __init__(self, module):
self.module = module
self.check_mode = module.check_mode self.check_mode = module.check_mode
self.name = module.params['name'] self.name = module.params['name']
self.user = module.params['user'] self.user = module.params['user']
@@ -126,6 +139,7 @@ class Sudoers(object):
self.sudoers_path = module.params['sudoers_path'] self.sudoers_path = module.params['sudoers_path']
self.file = os.path.join(self.sudoers_path, self.name) self.file = os.path.join(self.sudoers_path, self.name)
self.commands = module.params['commands'] self.commands = module.params['commands']
self.validation = module.params['validation']
def write(self): def write(self):
if self.check_mode: if self.check_mode:
@@ -134,6 +148,8 @@ class Sudoers(object):
with open(self.file, 'w') as f: with open(self.file, 'w') as f:
f.write(self.content()) f.write(self.content())
os.chmod(self.file, self.FILE_MODE)
def delete(self): def delete(self):
if self.check_mode: if self.check_mode:
return return
@@ -145,7 +161,12 @@ class Sudoers(object):
def matches(self): def matches(self):
with open(self.file, 'r') as f: with open(self.file, 'r') as f:
return f.read() == self.content() content_matches = f.read() == self.content()
current_mode = os.stat(self.file).st_mode & 0o777
mode_matches = current_mode == self.FILE_MODE
return content_matches and mode_matches
def content(self): def content(self):
if self.user: if self.user:
@@ -158,10 +179,29 @@ class Sudoers(object):
runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else '' runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else ''
return "{owner} ALL={runas}{nopasswd} {commands}\n".format(owner=owner, runas=runas_str, nopasswd=nopasswd_str, commands=commands_str) return "{owner} ALL={runas}{nopasswd} {commands}\n".format(owner=owner, runas=runas_str, nopasswd=nopasswd_str, commands=commands_str)
def validate(self):
if self.validation == 'absent':
return
visudo_path = self.module.get_bin_path('visudo', required=self.validation == 'required')
if visudo_path is None:
return
check_command = [visudo_path, '-c', '-f', '-']
rc, stdout, stderr = self.module.run_command(check_command, data=self.content())
if rc != 0:
raise Exception('Failed to validate sudoers rule:\n{stdout}'.format(stdout=stdout))
def run(self): def run(self):
if self.state == 'absent' and self.exists(): if self.state == 'absent':
self.delete() if self.exists():
return True self.delete()
return True
else:
return False
self.validate()
if self.exists() and self.matches(): if self.exists() and self.matches():
return False return False
@@ -197,6 +237,10 @@ def main():
'choices': ['present', 'absent'], 'choices': ['present', 'absent'],
}, },
'user': {}, 'user': {},
'validation': {
'default': 'detect',
'choices': ['absent', 'detect', 'required']
},
} }
module = AnsibleModule( module = AnsibleModule(

View File

@@ -145,31 +145,15 @@ RETURN = '''
sample: '"96" or ["red", "blue", "green"]' sample: '"96" or ["red", "blue", "green"]'
''' '''
from ansible_collections.community.general.plugins.module_utils.module_helper import ( from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper
CmdStateModuleHelper, ArgFormat from ansible_collections.community.general.plugins.module_utils.xfconf import xfconf_runner
)
def fix_bool(value):
vl = value.lower()
return vl if vl in ("true", "false") else value
@ArgFormat.stars_deco(1)
def values_fmt(values, value_types):
result = []
for value, value_type in zip(values, value_types):
if value_type == 'bool':
value = fix_bool(value)
result.extend(['--type', '{0}'.format(value_type), '--set', '{0}'.format(value)])
return result
class XFConfException(Exception): class XFConfException(Exception):
pass pass
class XFConfProperty(CmdStateModuleHelper): class XFConfProperty(StateModuleHelper):
change_params = 'value', change_params = 'value',
diff_params = 'value', diff_params = 'value',
output_params = ('property', 'channel', 'value') output_params = ('property', 'channel', 'value')
@@ -191,27 +175,19 @@ class XFConfProperty(CmdStateModuleHelper):
) )
default_state = 'present' default_state = 'present'
command = 'xfconf-query'
command_args_formats = dict(
channel=dict(fmt=('--channel', '{0}'),),
property=dict(fmt=('--property', '{0}'),),
is_array=dict(fmt="--force-array", style=ArgFormat.BOOLEAN),
reset=dict(fmt="--reset", style=ArgFormat.BOOLEAN),
create=dict(fmt="--create", style=ArgFormat.BOOLEAN),
values_and_types=dict(fmt=values_fmt)
)
def update_xfconf_output(self, **kwargs): def update_xfconf_output(self, **kwargs):
self.update_vars(meta={"output": True, "fact": True}, **kwargs) self.update_vars(meta={"output": True, "fact": True}, **kwargs)
def __init_module__(self): def __init_module__(self):
self.does_not = 'Property "{0}" does not exist on channel "{1}".'.format(self.module.params['property'], self.runner = xfconf_runner(self.module)
self.module.params['channel']) self.does_not = 'Property "{0}" does not exist on channel "{1}".'.format(self.vars.property,
self.vars.channel)
self.vars.set('previous_value', self._get(), fact=True) self.vars.set('previous_value', self._get(), fact=True)
self.vars.set('type', self.vars.value_type, fact=True) self.vars.set('type', self.vars.value_type, fact=True)
self.vars.meta('value').set(initial_value=self.vars.previous_value) self.vars.meta('value').set(initial_value=self.vars.previous_value)
if self.module.params['disable_facts'] is False: if self.vars.disable_facts is False:
self.do_raise('Returning results as facts has been removed. Stop using disable_facts=false.') self.do_raise('Returning results as facts has been removed. Stop using disable_facts=false.')
def process_command_output(self, rc, out, err): def process_command_output(self, rc, out, err):
@@ -229,11 +205,12 @@ class XFConfProperty(CmdStateModuleHelper):
return result return result
def _get(self): def _get(self):
return self.run_command(params=('channel', 'property')) with self.runner('channel property', output_process=self.process_command_output) as ctx:
return ctx.run()
def state_absent(self): def state_absent(self):
if not self.module.check_mode: with self.runner('channel property reset', check_mode_skip=True) as ctx:
self.run_command(params=('channel', 'property', {'reset': True})) ctx.run(reset=True)
self.vars.value = None self.vars.value = None
def state_present(self): def state_present(self):
@@ -252,22 +229,14 @@ class XFConfProperty(CmdStateModuleHelper):
# or complain if lists' lengths are different # or complain if lists' lengths are different
raise XFConfException('Number of elements in "value" and "value_type" must be the same') raise XFConfException('Number of elements in "value" and "value_type" must be the same')
# fix boolean values
self.vars.value = [fix_bool(v[0]) if v[1] == 'bool' else v[0] for v in zip(self.vars.value, value_type)]
# calculates if it is an array # calculates if it is an array
self.vars.is_array = \ self.vars.is_array = \
bool(self.vars.force_array) or \ bool(self.vars.force_array) or \
isinstance(self.vars.previous_value, list) or \ isinstance(self.vars.previous_value, list) or \
values_len > 1 values_len > 1
params = ['channel', 'property', {'create': True}] with self.runner('channel property create force_array values_and_types', check_mode_skip=True) as ctx:
if self.vars.is_array: ctx.run(create=True, force_array=self.vars.is_array, values_and_types=(self.vars.value, value_type))
params.append('is_array')
params.append({'values_and_types': (self.vars.value, value_type)})
if not self.module.check_mode:
self.run_command(params=params)
if not self.vars.is_array: if not self.vars.is_array:
self.vars.value = self.vars.value[0] self.vars.value = self.vars.value[0]

View File

@@ -74,7 +74,7 @@ RETURN = '''
properties: properties:
description: description:
- List of available properties for a specific channel. - List of available properties for a specific channel.
- Returned by passed only the I(channel) parameter to the module. - Returned by passing only the I(channel) parameter to the module.
returned: success returned: success
type: list type: list
elements: str elements: str
@@ -116,14 +116,15 @@ RETURN = '''
- Tmp - Tmp
''' '''
from ansible_collections.community.general.plugins.module_utils.module_helper import CmdModuleHelper, ArgFormat from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper
from ansible_collections.community.general.plugins.module_utils.xfconf import xfconf_runner
class XFConfException(Exception): class XFConfException(Exception):
pass pass
class XFConfInfo(CmdModuleHelper): class XFConfInfo(ModuleHelper):
module = dict( module = dict(
argument_spec=dict( argument_spec=dict(
channel=dict(type='str'), channel=dict(type='str'),
@@ -135,16 +136,9 @@ class XFConfInfo(CmdModuleHelper):
supports_check_mode=True, supports_check_mode=True,
) )
command = 'xfconf-query'
command_args_formats = dict(
channel=dict(fmt=['--channel', '{0}']),
property=dict(fmt=['--property', '{0}']),
_list_arg=dict(fmt="--list", style=ArgFormat.BOOLEAN),
)
check_rc = True
def __init_module__(self): def __init_module__(self):
self.vars.set("_list_arg", False, output=False) self.runner = xfconf_runner(self.module, check_rc=True)
self.vars.set("list_arg", False, output=False)
self.vars.set("is_array", False) self.vars.set("is_array", False)
def process_command_output(self, rc, out, err): def process_command_output(self, rc, out, err):
@@ -167,7 +161,7 @@ class XFConfInfo(CmdModuleHelper):
return lines return lines
def __run__(self): def __run__(self):
self.vars._list_arg = not (bool(self.vars.channel) and bool(self.vars.property)) self.vars.list_arg = not (bool(self.vars.channel) and bool(self.vars.property))
output = 'value' output = 'value'
proc = self.process_command_output proc = self.process_command_output
if self.vars.channel is None: if self.vars.channel is None:
@@ -176,15 +170,15 @@ class XFConfInfo(CmdModuleHelper):
elif self.vars.property is None: elif self.vars.property is None:
output = 'properties' output = 'properties'
proc = self._process_list_properties proc = self._process_list_properties
result = self.run_command(params=('_list_arg', 'channel', 'property'), process_output=proc) with self.runner.context('list_arg channel property', output_process=proc) as ctx:
if not self.vars._list_arg and self.vars.is_array: result = ctx.run(**self.vars)
if not self.vars.list_arg and self.vars.is_array:
output = "value_array" output = "value_array"
self.vars.set(output, result) self.vars.set(output, result)
def main(): def main():
xfconf = XFConfInfo() XFConfInfo.execute()
xfconf.run()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -49,6 +49,9 @@
# Test that path is checked: alternatives must fail when path is nonexistent # Test that path is checked: alternatives must fail when path is nonexistent
- import_tasks: path_is_checked.yml - import_tasks: path_is_checked.yml
# Test that subcommands commands work
- import_tasks: subcommands.yml
# Test operation of the 'state' parameter # Test operation of the 'state' parameter
- block: - block:
- include_tasks: remove_links.yml - include_tasks: remove_links.yml
@@ -63,6 +66,8 @@
state: absent state: absent
with_items: with_items:
- '{{ alternatives_dir }}/dummy' - '{{ alternatives_dir }}/dummy'
- '{{ alternatives_dir }}/dummymain'
- '{{ alternatives_dir }}/dummysubcmd'
- file: - file:
path: '/usr/bin/dummy{{ item }}' path: '/usr/bin/dummy{{ item }}'

View File

@@ -0,0 +1,217 @@
- name: Try with subcommands
alternatives:
name: dummymain
path: '/usr/bin/dummy1'
link: '/usr/bin/dummymain'
subcommands:
- name: dummysubcmd
path: '/usr/bin/dummy2'
link: '/usr/bin/dummysubcmd'
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is changed'
- name: Execute the current dummymain command
command: dummymain
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy1"
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy2"
- name: Get dummymain alternatives output
command:
cmd: '{{ alternatives_command }} --display dummymain'
register: result
- name: Print result
debug:
var: result.stdout_lines
- name: Subcommands are not removed if not specified
alternatives:
name: dummymain
path: '/usr/bin/dummy1'
link: '/usr/bin/dummymain'
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is not changed'
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy2"
- name: Subcommands are removed if set to an empty list
alternatives:
name: dummymain
path: '/usr/bin/dummy1'
link: '/usr/bin/dummymain'
subcommands: []
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is changed'
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
ignore_errors: True
- name: Ensure that the subcommand is gone
assert:
that:
- cmd.rc == 2
- '"No such file" in cmd.msg'
- name: Get dummymain alternatives output
command:
cmd: '{{ alternatives_command }} --display dummymain'
register: result
- name: Print result
debug:
var: result.stdout_lines
- name: Install other alternative with subcommands
alternatives:
name: dummymain
path: '/usr/bin/dummy3'
link: '/usr/bin/dummymain'
subcommands:
- name: dummysubcmd
path: '/usr/bin/dummy4'
link: '/usr/bin/dummysubcmd'
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is changed'
- name: Execute the current dummymain command
command: dummymain
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy3"
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy4"
- name: Get dummymain alternatives output
command:
cmd: '{{ alternatives_command }} --display dummymain'
register: result
- name: Print result
debug:
var: result.stdout_lines
- name: Switch to first alternative
alternatives:
name: dummymain
path: '/usr/bin/dummy1'
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is changed'
- name: Execute the current dummymain command
command: dummymain
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy1"
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
ignore_errors: True
- name: Ensure that the subcommand is gone
assert:
that:
- cmd.rc == 2
- '"No such file" in cmd.msg'
- name: Get dummymain alternatives output
command:
cmd: '{{ alternatives_command }} --display dummymain'
register: result
- name: Print result
debug:
var: result.stdout_lines
- name: Switch to second alternative
alternatives:
name: dummymain
path: '/usr/bin/dummy3'
register: alternative
- name: Check expected command was executed
assert:
that:
- 'alternative is changed'
- name: Execute the current dummymain command
command: dummymain
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy3"
- name: Execute the current dummysubcmd command
command: dummysubcmd
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy4"
- name: Get dummymain alternatives output
command:
cmd: '{{ alternatives_command }} --display dummymain'
register: result
- name: Print result
debug:
var: result.stdout_lines

View File

@@ -49,5 +49,3 @@
- name: check that alternative has been updated - name: check that alternative has been updated
command: "grep -Pzq '/bin/dummy{{ item }}\\n' '{{ alternatives_dir }}/dummy'" command: "grep -Pzq '/bin/dummy{{ item }}\\n' '{{ alternatives_dir }}/dummy'"
# priority doesn't seem updated
#command: "grep -Pzq '/bin/dummy{{ item }}\\n50' '{{ alternatives_dir }}/dummy'"

View File

@@ -21,3 +21,29 @@
- name: check that alternative has been updated - name: check that alternative has been updated
command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 60 + item|int }}' '{{ alternatives_dir }}/dummy'" command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 60 + item|int }}' '{{ alternatives_dir }}/dummy'"
- name: update dummy priority
alternatives:
name: dummy
path: '/usr/bin/dummy{{ item }}'
link: /usr/bin/dummy
priority: '{{ 70 + item|int }}'
register: alternative
- name: check that alternative priority has been updated
command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 70 + item|int }}' '{{ alternatives_dir }}/dummy'"
- name: no change without priority
alternatives:
name: dummy
path: '/usr/bin/dummy{{ item }}'
link: /usr/bin/dummy
register: alternative
- name: check no change was triggered without priority
assert:
that:
- 'alternative is not changed'
- name: check that alternative priority has not been changed
command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 70 + item|int }}' '{{ alternatives_dir }}/dummy'"

View File

@@ -49,6 +49,28 @@
- cmd.stdout == "dummy4" - cmd.stdout == "dummy4"
# Set the currently selected alternative to state = 'present' (was previously # Set the currently selected alternative to state = 'present' (was previously
# selected), and ensure that this results in the group not being set to 'auto'
# mode, and the alternative is still selected.
- name: Set current selected dummy to state = present
alternatives:
name: dummy
path: /usr/bin/dummy4
link: /usr/bin/dummy
state: present
- name: Ensure that the link group is in auto mode
shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^manual$"'
- name: Execute the current dummy command
shell: dummy
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy4"
# Set the currently selected alternative to state = 'auto' (was previously
# selected), and ensure that this results in the group being set to 'auto' # selected), and ensure that this results in the group being set to 'auto'
# mode, and the highest priority alternative is selected. # mode, and the highest priority alternative is selected.
- name: Set current selected dummy to state = present - name: Set current selected dummy to state = present
@@ -56,7 +78,7 @@
name: dummy name: dummy
path: /usr/bin/dummy4 path: /usr/bin/dummy4
link: /usr/bin/dummy link: /usr/bin/dummy
state: present state: auto
- name: Ensure that the link group is in auto mode - name: Ensure that the link group is in auto mode
shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^auto$"' shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^auto$"'
@@ -69,3 +91,25 @@
assert: assert:
that: that:
- cmd.stdout == "dummy2" - cmd.stdout == "dummy2"
# Remove an alternative with state = 'absent' and make sure that
# this change results in the alternative being removed.
- name: Remove best dummy alternative with state = absent
alternatives:
name: dummy
path: /usr/bin/dummy2
state: absent
- name: Ensure that the link group is in auto mode
shell: 'grep "/usr/bin/dummy2" {{ alternatives_dir }}/dummy'
register: cmd
failed_when: cmd.rc == 0
- name: Execute the current dummy command
shell: dummy
register: cmd
- name: Ensure that the expected command was executed
assert:
that:
- cmd.stdout == "dummy1"

View File

@@ -1,2 +1,3 @@
--- ---
alternatives_dir: /var/lib/dpkg/alternatives/ alternatives_dir: /var/lib/dpkg/alternatives/
alternatives_command: update-alternatives

View File

@@ -1,2 +1,3 @@
--- ---
alternatives_dir: /var/lib/rpm/alternatives/ alternatives_dir: /var/lib/rpm/alternatives/
alternatives_command: update-alternatives

View File

@@ -1,2 +1,3 @@
--- ---
alternatives_dir: /var/lib/alternatives/ alternatives_dir: /var/lib/alternatives/
alternatives_command: update-alternatives

View File

@@ -6,43 +6,15 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import sys
DOCUMENTATION = ''' DOCUMENTATION = ""
module: cmd_echo
author: "Alexei Znamensky (@russoz)"
short_description: Simple module for testing
description:
- Simple module test description.
options:
command:
description: aaa
type: list
elements: str
required: true
arg_formats:
description: bbb
type: dict
required: true
arg_order:
description: ccc
type: raw
required: true
arg_values:
description: ddd
type: list
required: true
aa:
description: eee
type: raw
'''
EXAMPLES = "" EXAMPLES = ""
RETURN = "" RETURN = ""
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, fmt from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt as fmt
def main(): def main():
@@ -51,11 +23,15 @@ def main():
arg_formats=dict(type="dict", default={}), arg_formats=dict(type="dict", default={}),
arg_order=dict(type="raw", required=True), arg_order=dict(type="raw", required=True),
arg_values=dict(type="dict", default={}), arg_values=dict(type="dict", default={}),
check_mode_skip=dict(type="bool", default=False),
aa=dict(type="raw"), aa=dict(type="raw"),
), ),
supports_check_mode=True,
) )
p = module.params p = module.params
info = None
arg_formats = {} arg_formats = {}
for arg, fmt_spec in p['arg_formats'].items(): for arg, fmt_spec in p['arg_formats'].items():
func = getattr(fmt, fmt_spec['func']) func = getattr(fmt, fmt_spec['func'])
@@ -65,11 +41,11 @@ def main():
runner = CmdRunner(module, ['echo', '--'], arg_formats=arg_formats) runner = CmdRunner(module, ['echo', '--'], arg_formats=arg_formats)
info = None with runner.context(p['arg_order'], check_mode_skip=p['check_mode_skip']) as ctx:
with runner.context(p['arg_order']) as ctx:
result = ctx.run(**p['arg_values']) result = ctx.run(**p['arg_values'])
info = ctx.run_info info = ctx.run_info
rc, out, err = result check = "check"
rc, out, err = result if result is not None else (None, None, None)
module.exit_json(rc=rc, out=out, err=err, info=info) module.exit_json(rc=rc, out=out, err=err, info=info)

View File

@@ -4,8 +4,10 @@
arg_formats: "{{ item.arg_formats|default(omit) }}" arg_formats: "{{ item.arg_formats|default(omit) }}"
arg_order: "{{ item.arg_order }}" arg_order: "{{ item.arg_order }}"
arg_values: "{{ item.arg_values|default(omit) }}" arg_values: "{{ item.arg_values|default(omit) }}"
check_mode_skip: "{{ item.check_mode_skip|default(omit) }}"
aa: "{{ item.aa|default(omit) }}" aa: "{{ item.aa|default(omit) }}"
register: test_result register: test_result
check_mode: "{{ item.check_mode|default(omit) }}"
ignore_errors: "{{ item.expect_error|default(omit) }}" ignore_errors: "{{ item.expect_error|default(omit) }}"
- name: check results [{{ item.name }}] - name: check results [{{ item.name }}]

View File

@@ -82,3 +82,41 @@ cmd_echo_tests:
- >- - >-
"MissingArgumentValue: Cannot find value for parameter bb" "MissingArgumentValue: Cannot find value for parameter bb"
in test_result.module_stderr in test_result.module_stderr
- name: set aa and bb value with check_mode on
arg_formats:
aa:
func: as_opt_eq_val
args: [--answer]
bb:
func: as_bool
args: [--bb-here]
arg_order: 'aa bb'
arg_values:
bb: true
aa: 11
check_mode: true
assertions:
- test_result.rc == 0
- test_result.out == "-- --answer=11 --bb-here\n"
- test_result.err == ""
- name: set aa and bb value with check_mode and check_mode_skip on
arg_formats:
aa:
func: as_opt_eq_val
args: [--answer]
bb:
func: as_bool
args: [--bb-here]
arg_order: 'aa bb'
arg_values:
bb: true
check_mode_skip: true
aa: 11
check_mode: true
expect_error: true # because if result contains rc != 0, ansible assumes error
assertions:
- test_result.rc == None
- test_result.out == None
- test_result.err == None

View File

@@ -1,3 +1,4 @@
dependencies: dependencies:
- setup_pkg_mgr - setup_pkg_mgr
- setup_remote_tmp_dir - setup_remote_tmp_dir
- setup_remote_constraints

View File

@@ -10,6 +10,7 @@
pip: pip:
name: pycdlib name: pycdlib
# state: latest # state: latest
extra_args: "-c {{ remote_constraints }}"
register: install_pycdlib register: install_pycdlib
- debug: var=install_pycdlib - debug: var=install_pycdlib

View File

@@ -0,0 +1 @@
unsupported

View File

@@ -0,0 +1,95 @@
---
- name: Ensure required packages for headless keyring access are installed (RPM)
ansible.builtin.package:
name: gnome-keyring
become: true
when: "'localhost' not in inventory_hostname"
- name: Ensure keyring is installed (RPM)
ansible.builtin.dnf:
name: python3-keyring
state: present
become: true
when: ansible_facts['os_family'] == 'RedHat'
- name: Ensure keyring is installed (pip)
ansible.builtin.pip:
name: keyring
state: present
become: true
when: ansible_facts['os_family'] != 'RedHat'
# Set password for new account
# Expected result: success
- name: Set password for test/test1
community.general.keyring:
service: test
username: test1
user_password: "{{ user_password }}"
keyring_password: "{{ keyring_password }}"
register: set_password
- name: Assert that the password has been set
ansible.builtin.assert:
that:
- set_password.msg == "Passphrase has been updated for test@test1"
# Print out password to confirm it has been set
# Expected result: success
- name: Retrieve password for test/test1
community.general.keyring_info:
service: test
username: test1
keyring_password: "{{ keyring_password }}"
register: test_set_password
- name: Assert that the password exists
ansible.builtin.assert:
that:
- test_set_password.passphrase == user_password
# Attempt to set password again
# Expected result: success - nothing should happen
- name: Attempt to re-set password for test/test1
community.general.keyring:
service: test
username: test1
user_password: "{{ user_password }}"
keyring_password: "{{ keyring_password }}"
register: second_set_password
- name: Assert that the password has not been changed
ansible.builtin.assert:
that:
- second_set_password.msg == "Passphrase already set for test@test1"
# Delete account
# Expected result: success
- name: Delete password for test/test1
community.general.keyring:
service: test
username: test1
user_password: "{{ user_password }}"
keyring_password: "{{ keyring_password }}"
state: absent
register: del_password
- name: Assert that the password has been deleted
ansible.builtin.assert:
that:
- del_password.msg == "Passphrase has been removed for test@test1"
# Attempt to get deleted account (to confirm it has been deleted).
# Don't use `no_log` as run completes due to failed task.
# Expected result: fail
- name: Retrieve password for test/test1
community.general.keyring_info:
service: test
username: test1
keyring_password: "{{ keyring_password }}"
register: test_del_password
- name: Assert that the password no longer exists
ansible.builtin.assert:
that:
- test_del_password.passphrase is not defined

View File

@@ -0,0 +1,3 @@
---
keyring_password: Password123
user_password: Test123

View File

@@ -19,6 +19,44 @@
- "~/.gnupg" - "~/.gnupg"
- "~/.password-store" - "~/.password-store"
- name: Get path of pass executable
command: which pass
register: result
- name: Store path of pass executable
set_fact:
passpath: "{{ result.stdout }}"
- name: Move original pass into place if there was a leftover
command:
argv:
- mv
- "{{ passpath }}.testorig"
- "{{ passpath }}"
args:
removes: "{{ passpath }}.testorig"
# having gopass is not required for this test, but we store
# its path in case it is installed, so we can restore it
- name: Try to find gopass in path
command: which gopass
register: result
ignore_errors: yes
- name: Store path of gopass executable
set_fact:
gopasspath: "{{ (result.rc == 0) |
ternary(result.stdout, (passpath | dirname, 'gopass') | path_join) }}"
- name: Move original gopass into place if there was a leftover
command:
argv:
- mv
- "{{ gopasspath }}.testorig"
- "{{ gopasspath }}"
args:
removes: "{{ gopasspath }}.testorig"
# How to generate a new GPG key: # How to generate a new GPG key:
# gpg2 --batch --gen-key input # See templates/input # gpg2 --batch --gen-key input # See templates/input
# gpg2 --list-secret-keys --keyid-format LONG # gpg2 --list-secret-keys --keyid-format LONG
@@ -151,3 +189,163 @@
assert: assert:
that: that:
- readyamlpass == 'testpassword\nrandom additional line' - readyamlpass == 'testpassword\nrandom additional line'
- name: Create a password in a folder
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'folder/test-pass length=8 create=yes') }}"
- name: Fetch password from folder
set_fact:
readpass: "{{ lookup('community.general.passwordstore', 'folder/test-pass') }}"
- name: Verify password from folder
assert:
that:
- readpass == newpass
- name: Try to read folder as passname
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'folder') }}"
ignore_errors: true
register: eval_error
- name: Make sure reading folder as passname failed
assert:
that:
- eval_error is failed
- '"passname folder not found" in eval_error.msg'
- name: Change passwordstore location explicitly
set_fact:
passwordstore: "{{ lookup('env','HOME') }}/.password-store"
- name: Make sure password store still works with explicit location set
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'test-pass') }}"
- name: Change passwordstore location to a non-existent place
set_fact:
passwordstore: "somenonexistentplace"
- name: Try reading from non-existent passwordstore location
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'test-pass') }}"
ignore_errors: true
register: eval_error
- name: Make sure reading from non-existent passwordstore location failed
assert:
that:
- eval_error is failed
- >-
"Passwordstore directory 'somenonexistentplace' does not exist" in eval_error.msg
- name: Test pass compatibility shim detection
block:
- name: Move original pass out of the way
command:
argv:
- mv
- "{{ passpath }}"
- "{{ passpath }}.testorig"
args:
creates: "{{ passpath }}.testorig"
- name: Create dummy pass script
ansible.builtin.copy:
content: |
#!/bin/sh
echo "shim_ok"
dest: "{{ passpath }}"
mode: '0755'
- name: Try reading from non-existent passwordstore location with different pass utility
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'test-pass') }}"
environment:
PATH: "/tmp"
- name: Verify password received from shim
assert:
that:
- newpass == "shim_ok"
- name: Try to read folder as passname with a different pass utility
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'folder') }}"
- name: Verify password received from shim
assert:
that:
- newpass == "shim_ok"
always:
- name: Move original pass back into place
command:
argv:
- mv
- "{{ passpath }}.testorig"
- "{{ passpath }}"
args:
removes: "{{ passpath }}.testorig"
- name: Very basic gopass compatibility test
vars:
passwordstore_backend: "gopass"
block:
- name: check if gopass executable exists
stat:
path: "{{ gopasspath }}"
register: gopass_check
- name: Move original gopass out of the way
command:
argv:
- mv
- "{{ gopasspath }}"
- "{{ gopasspath }}.testorig"
args:
creates: "{{ gopasspath }}.testorig"
when: gopass_check.stat.exists == true
- name: Create mocked gopass script
ansible.builtin.copy:
content: |
#!/bin/sh
if [ "$GOPASS_NO_REMINDER" != "YES" ]; then
exit 1
fi
if [ "$1" = "--version" ]; then
exit 2
fi
if [ "$1" = "show" ] && [ "$2" != "--password" ]; then
exit 3
fi
echo "gopass_ok"
dest: "{{ gopasspath }}"
mode: '0755'
- name: Try to read folder as passname using gopass
set_fact:
newpass: "{{ lookup('community.general.passwordstore', 'folder') }}"
- name: Verify password received from gopass
assert:
that:
- newpass == "gopass_ok"
always:
- name: Remove mocked gopass
ansible.builtin.file:
path: "{{ gopasspath }}"
state: absent
- name: Move original gopass back into place
command:
argv:
- mv
- "{{ gopasspath }}.testorig"
- "{{ gopasspath }}"
args:
removes: "{{ gopasspath }}.testorig"
when: gopass_check.stat.exists == true

View File

@@ -1,11 +1,16 @@
--- ---
# Initialise environment # Initialise environment
- name: Register sudoers.d directory - name: Register variables
set_fact: set_fact:
sudoers_path: /etc/sudoers.d sudoers_path: /etc/sudoers.d
alt_sudoers_path: /etc/sudoers_alt alt_sudoers_path: /etc/sudoers_alt
- name: Install sudo package
ansible.builtin.package:
name: sudo
when: ansible_os_family != 'Darwin'
- name: Ensure sudoers directory exists - name: Ensure sudoers directory exists
ansible.builtin.file: ansible.builtin.file:
path: "{{ sudoers_path }}" path: "{{ sudoers_path }}"
@@ -29,6 +34,11 @@
commands: /usr/local/bin/command commands: /usr/local/bin/command
register: rule_1 register: rule_1
- name: Stat my-sudo-rule-1 file
ansible.builtin.stat:
path: "{{ sudoers_path }}/my-sudo-rule-1"
register: rule_1_stat
- name: Grab contents of my-sudo-rule-1 - name: Grab contents of my-sudo-rule-1
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ sudoers_path }}/my-sudo-rule-1" src: "{{ sudoers_path }}/my-sudo-rule-1"
@@ -130,8 +140,73 @@
register: revoke_rule_1_stat register: revoke_rule_1_stat
# Validation testing
- name: Attempt command without full path to executable
community.general.sudoers:
name: edge-case-1
state: present
user: alice
commands: systemctl
ignore_errors: true
register: edge_case_1
- name: Attempt command without full path to executable, but disabling validation
community.general.sudoers:
name: edge-case-2
state: present
user: alice
commands: systemctl
validation: absent
sudoers_path: "{{ alt_sudoers_path }}"
register: edge_case_2
- name: find visudo
command:
cmd: which visudo
register: which_visudo
when: ansible_os_family != 'Darwin'
- name: Prevent visudo being executed
file:
path: "{{ which_visudo.stdout }}"
mode: '-x'
when: ansible_os_family != 'Darwin'
- name: Attempt command without full path to executable, but enforcing validation with no visudo present
community.general.sudoers:
name: edge-case-3
state: present
user: alice
commands: systemctl
validation: required
ignore_errors: true
when: ansible_os_family != 'Darwin'
register: edge_case_3
- name: Revoke non-existing rule
community.general.sudoers:
name: non-existing-rule
state: absent
register: revoke_non_existing_rule
- name: Stat non-existing rule
ansible.builtin.stat:
path: "{{ sudoers_path }}/non-existing-rule"
register: revoke_non_existing_rule_stat
# Run assertions # Run assertions
- name: Check rule 1 file stat
ansible.builtin.assert:
that:
- rule_1_stat.stat.exists
- rule_1_stat.stat.isreg
- rule_1_stat.stat.mode == '0440'
- name: Check changed status - name: Check changed status
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -139,6 +214,7 @@
- rule_1_again is not changed - rule_1_again is not changed
- rule_5 is changed - rule_5 is changed
- revoke_rule_1 is changed - revoke_rule_1 is changed
- revoke_non_existing_rule is not changed
- name: Check contents - name: Check contents
ansible.builtin.assert: ansible.builtin.assert:
@@ -150,7 +226,22 @@
- "rule_5_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command\n'" - "rule_5_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command\n'"
- "rule_6_contents['content'] | b64decode == 'alice ALL=(bob)NOPASSWD: /usr/local/bin/command\n'" - "rule_6_contents['content'] | b64decode == 'alice ALL=(bob)NOPASSWD: /usr/local/bin/command\n'"
- name: Check stats - name: Check revocation stat
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- not revoke_rule_1_stat.stat.exists - not revoke_rule_1_stat.stat.exists
- not revoke_non_existing_rule_stat.stat.exists
- name: Check edge case responses
ansible.builtin.assert:
that:
- edge_case_1 is failed
- "'Failed to validate sudoers rule' in edge_case_1.msg"
- edge_case_2 is not failed
- name: Check missing validation edge case
ansible.builtin.assert:
that:
- edge_case_3 is failed
- "'Failed to find required executable' in edge_case_3.msg"
when: ansible_os_family != 'Darwin'

View File

@@ -207,50 +207,53 @@
that: that:
- remove_repo is changed - remove_repo is changed
- name: add new repository via url to .repo file # For now, the URL does not work for 15.4
community.general.zypper_repository: - when: ansible_distribution_version is version('15.4', '<')
repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo block:
state: present - name: add new repository via url to .repo file
register: added_by_repo_file community.general.zypper_repository:
repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo
state: present
register: added_by_repo_file
- name: get repository details from zypper - name: get repository details from zypper
command: zypper lr systemsmanagement_Uyuni_Stable command: zypper lr systemsmanagement_Uyuni_Stable
register: get_repository_details_from_zypper register: get_repository_details_from_zypper
- name: verify adding via .repo file was successful - name: verify adding via .repo file was successful
assert: assert:
that: that:
- "added_by_repo_file is changed" - "added_by_repo_file is changed"
- "get_repository_details_from_zypper.rc == 0" - "get_repository_details_from_zypper.rc == 0"
- "'/systemsmanagement:/Uyuni:/Stable/' in get_repository_details_from_zypper.stdout" - "'/systemsmanagement:/Uyuni:/Stable/' in get_repository_details_from_zypper.stdout"
- name: add same repository via url to .repo file again to verify idempotency - name: add same repository via url to .repo file again to verify idempotency
community.general.zypper_repository: community.general.zypper_repository:
repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo
state: present state: present
register: added_again_by_repo_file register: added_again_by_repo_file
- name: verify nothing was changed adding a repo with the same .repo file - name: verify nothing was changed adding a repo with the same .repo file
assert: assert:
that: that:
- added_again_by_repo_file is not changed - added_again_by_repo_file is not changed
- name: remove repository via url to .repo file - name: remove repository via url to .repo file
community.general.zypper_repository: community.general.zypper_repository:
repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo repo: http://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/openSUSE_Leap_{{ ansible_distribution_version }}/systemsmanagement:Uyuni:Stable.repo
state: absent state: absent
register: removed_by_repo_file register: removed_by_repo_file
- name: get list of files in /etc/zypp/repos.d/ - name: get list of files in /etc/zypp/repos.d/
command: ls /etc/zypp/repos.d/ command: ls /etc/zypp/repos.d/
changed_when: false changed_when: false
register: etc_zypp_reposd register: etc_zypp_reposd
- name: verify removal via .repo file was successful, including cleanup of local .repo file in /etc/zypp/repos.d/ - name: verify removal via .repo file was successful, including cleanup of local .repo file in /etc/zypp/repos.d/
assert: assert:
that: that:
- "removed_by_repo_file" - "removed_by_repo_file"
- "'/systemsmanagement:/Uyuni:/Stable/' not in etc_zypp_reposd.stdout" - "'/systemsmanagement:/Uyuni:/Stable/' not in etc_zypp_reposd.stdout"
- name: Copy test .repo file - name: Copy test .repo file
copy: copy:

View File

@@ -520,6 +520,99 @@ def get_json(url):
} }
] ]
} }
elif url == "https://localhost:8006/api2/json/nodes/testnode/lxc/100/status/current":
# _get_vm_status (lxc)
return {
"swap": 0,
"name": "test-lxc",
"diskread": 0,
"vmid": 100,
"diskwrite": 0,
"pid": 9000,
"mem": 89980928,
"netin": 1950776396424,
"disk": 4998168576,
"cpu": 0.00163430613110039,
"type": "lxc",
"uptime": 6793736,
"maxmem": 1073741824,
"status": "running",
"cpus": "1",
"ha": {
"group": 'null',
"state": "started",
"managed": 1
},
"maxdisk": 3348329267200,
"netout": 1947793356037,
"maxswap": 1073741824
}
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/status/current":
# _get_vm_status (qemu)
return {
"status": "stopped",
"uptime": 0,
"maxmem": 5364514816,
"maxdisk": 34359738368,
"netout": 0,
"cpus": 2,
"ha": {
"managed": 0
},
"diskread": 0,
"vmid": 101,
"diskwrite": 0,
"name": "test-qemu",
"cpu": 0,
"disk": 0,
"netin": 0,
"mem": 0,
"qmpstatus": "stopped"
}
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/102/status/current":
# _get_vm_status (qemu)
return {
"status": "stopped",
"uptime": 0,
"maxmem": 5364514816,
"maxdisk": 34359738368,
"netout": 0,
"cpus": 2,
"ha": {
"managed": 0
},
"diskread": 0,
"vmid": 102,
"diskwrite": 0,
"name": "test-qemu-windows",
"cpu": 0,
"disk": 0,
"netin": 0,
"mem": 0,
"qmpstatus": "prelaunch"
}
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/103/status/current":
# _get_vm_status (qemu)
return {
"status": "stopped",
"uptime": 0,
"maxmem": 5364514816,
"maxdisk": 34359738368,
"netout": 0,
"cpus": 2,
"ha": {
"managed": 0
},
"diskread": 0,
"vmid": 103,
"diskwrite": 0,
"name": "test-qemu-multi-nic",
"cpu": 0,
"disk": 0,
"netin": 0,
"mem": 0,
"qmpstatus": "paused"
}
def get_vm_snapshots(node, properties, vmtype, vmid, name): def get_vm_snapshots(node, properties, vmtype, vmid, name):
@@ -537,21 +630,11 @@ def get_vm_snapshots(node, properties, vmtype, vmid, name):
}] }]
def get_vm_status(properties, node, vmtype, vmid, name): def get_option(opts):
return True def fn(option):
default = opts.get('default', False)
return opts.get(option, default)
def get_option(option): return fn
if option == 'group_prefix':
return 'proxmox_'
if option == 'facts_prefix':
return 'proxmox_'
elif option == 'want_facts':
return True
elif option == 'want_proxmox_nodes_ansible_host':
return True
else:
return False
def test_populate(inventory, mocker): def test_populate(inventory, mocker):
@@ -563,12 +646,19 @@ def test_populate(inventory, mocker):
inventory.facts_prefix = 'proxmox_' inventory.facts_prefix = 'proxmox_'
inventory.strict = False inventory.strict = False
opts = {
'group_prefix': 'proxmox_',
'facts_prefix': 'proxmox_',
'want_facts': True,
'want_proxmox_nodes_ansible_host': True,
'qemu_extended_statuses': True
}
# bypass authentication and API fetch calls # bypass authentication and API fetch calls
inventory._get_auth = mocker.MagicMock(side_effect=get_auth) inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
inventory._get_json = mocker.MagicMock(side_effect=get_json) inventory._get_json = mocker.MagicMock(side_effect=get_json)
inventory._get_vm_status = mocker.MagicMock(side_effect=get_vm_status)
inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots) inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
inventory.get_option = mocker.MagicMock(side_effect=get_option) inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
inventory._can_add_host = mocker.MagicMock(return_value=True) inventory._can_add_host = mocker.MagicMock(return_value=True)
inventory._populate() inventory._populate()
@@ -610,3 +700,45 @@ def test_populate(inventory, mocker):
# check that offline node is in inventory # check that offline node is in inventory
assert inventory.inventory.get_host('testnode2') assert inventory.inventory.get_host('testnode2')
# make sure that ['prelaunch', 'paused'] are in the group list
for group in ['paused', 'prelaunch']:
assert ('%sall_%s' % (inventory.group_prefix, group)) in inventory.inventory.groups
# check if qemu-windows is in the prelaunch group
group_prelaunch = inventory.inventory.groups['proxmox_all_prelaunch']
assert group_prelaunch.hosts == [host_qemu_windows]
# check if qemu-multi-nic is in the paused group
group_paused = inventory.inventory.groups['proxmox_all_paused']
assert group_paused.hosts == [host_qemu_multi_nic]
def test_populate_missing_qemu_extended_groups(inventory, mocker):
# module settings
inventory.proxmox_user = 'root@pam'
inventory.proxmox_password = 'password'
inventory.proxmox_url = 'https://localhost:8006'
inventory.group_prefix = 'proxmox_'
inventory.facts_prefix = 'proxmox_'
inventory.strict = False
opts = {
'group_prefix': 'proxmox_',
'facts_prefix': 'proxmox_',
'want_facts': True,
'want_proxmox_nodes_ansible_host': True,
'qemu_extended_statuses': False
}
# bypass authentication and API fetch calls
inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
inventory._get_json = mocker.MagicMock(side_effect=get_json)
inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
inventory._can_add_host = mocker.MagicMock(return_value=True)
inventory._populate()
# make sure that ['prelaunch', 'paused'] are not in the group list
for group in ['paused', 'prelaunch']:
assert ('%sall_%s' % (inventory.group_prefix, group)) not in inventory.inventory.groups

View File

@@ -246,7 +246,7 @@ TC_RUNNER_IDS = sorted(TC_RUNNER.keys())
@pytest.mark.parametrize('runner_input, cmd_execution, expected', @pytest.mark.parametrize('runner_input, cmd_execution, expected',
(TC_RUNNER[tc] for tc in TC_RUNNER_IDS), (TC_RUNNER[tc] for tc in TC_RUNNER_IDS),
ids=TC_RUNNER_IDS) ids=TC_RUNNER_IDS)
def test_runner(runner_input, cmd_execution, expected): def test_runner_context(runner_input, cmd_execution, expected):
arg_spec = {} arg_spec = {}
params = {} params = {}
arg_formats = {} arg_formats = {}
@@ -304,3 +304,66 @@ def test_runner(runner_input, cmd_execution, expected):
with runner.context(**runner_input['runner_ctx_args']) as ctx: with runner.context(**runner_input['runner_ctx_args']) as ctx:
results = ctx.run(**cmd_execution['runner_ctx_run_args']) results = ctx.run(**cmd_execution['runner_ctx_run_args'])
_assert_run(runner_input, cmd_execution, expected, ctx, results) _assert_run(runner_input, cmd_execution, expected, ctx, results)
@pytest.mark.parametrize('runner_input, cmd_execution, expected',
(TC_RUNNER[tc] for tc in TC_RUNNER_IDS),
ids=TC_RUNNER_IDS)
def test_runner_callable(runner_input, cmd_execution, expected):
arg_spec = {}
params = {}
arg_formats = {}
for k, v in runner_input['args_bundle'].items():
try:
arg_spec[k] = {'type': v['type']}
except KeyError:
pass
try:
params[k] = v['value']
except KeyError:
pass
try:
arg_formats[k] = v['fmt_func'](v['fmt_arg'])
except KeyError:
pass
orig_results = tuple(cmd_execution[x] for x in ('rc', 'out', 'err'))
print("arg_spec={0}\nparams={1}\narg_formats={2}\n".format(
arg_spec,
params,
arg_formats,
))
module = MagicMock()
type(module).argument_spec = PropertyMock(return_value=arg_spec)
type(module).params = PropertyMock(return_value=params)
module.get_bin_path.return_value = '/mock/bin/testing'
module.run_command.return_value = orig_results
runner = CmdRunner(
module=module,
command="testing",
arg_formats=arg_formats,
**runner_input['runner_init_args']
)
def _assert_run_info(actual, expected):
reduced = dict((k, actual[k]) for k in expected.keys())
assert reduced == expected, "{0}".format(reduced)
def _assert_run(runner_input, cmd_execution, expected, ctx, results):
_assert_run_info(ctx.run_info, expected['run_info'])
assert results == expected.get('results', orig_results)
exc = expected.get("exc")
if exc:
with pytest.raises(exc):
with runner(**runner_input['runner_ctx_args']) as ctx:
results = ctx.run(**cmd_execution['runner_ctx_run_args'])
_assert_run(runner_input, cmd_execution, expected, ctx, results)
else:
with runner(**runner_input['runner_ctx_args']) as ctx:
results = ctx.run(**cmd_execution['runner_ctx_run_args'])
_assert_run(runner_input, cmd_execution, expected, ctx, results)

View File

@@ -0,0 +1,179 @@
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import pytest
from ansible_collections.community.general.plugins.modules.cloud.scaleway import scaleway_compute_private_network
from ansible_collections.community.general.plugins.module_utils.scaleway import Scaleway, Response
from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args
from ansible_collections.community.general.tests.unit.compat.mock import patch
def response_without_nics():
info = {"status": 200,
"body": '{ "private_nics": []}'
}
return Response(None, info)
def response_with_nics():
info = {"status": 200,
"body": ('{ "private_nics": [{'
'"id": "c123b4cd-ef5g-678h-90i1-jk2345678l90",'
'"private_network_id": "b589b4cd-ef5g-678h-90i1-jk2345678l90",'
'"server_id": "c004b4cd-ef5g-678h-90i1-jk2345678l90",'
'"mac_address": "02:00:00:00:12:23",'
'"state": "available",'
'"creation_date": "2022-03-30T06:25:28.155973+00:00",'
'"modification_date": "2022-03-30T06:25:28.155973+00:00",'
'"zone": "fr-par-1"'
'}]}'
)
}
return Response(None, info)
def response_when_add_nics():
info = {"status": 200,
"body": ('{ "private_nics": {'
'"id": "c123b4cd-ef5g-678h-90i1-jk2345678l90",'
'"private_network_id": "b589b4cd-ef5g-678h-90i1-jk2345678l90",'
'"server_id": "c004b4cd-ef5g-678h-90i1-jk2345678l90",'
'"mac_address": "02:00:00:00:12:23",'
'"state": "available",'
'"creation_date": "2022-03-30T06:25:28.155973+00:00",'
'"modification_date": "2022-03-30T06:25:28.155973+00:00",'
'"zone": "fr-par-1"'
'}}'
)
}
return Response(None, info)
def response_remove_nics():
info = {"status": 200}
return Response(None, info)
def test_scaleway_private_network_without_arguments(capfd):
set_module_args({})
with pytest.raises(SystemExit) as results:
scaleway_compute_private_network.main()
out, err = capfd.readouterr()
assert not err
assert json.loads(out)['failed']
def test_scaleway_add_nic(capfd):
os.environ['SCW_API_TOKEN'] = 'notrealtoken'
pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90'
cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90'
url = 'servers/' + cid + '/private_nics'
set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"state": "present",
"region": "par1",
"compute_id": cid,
"private_network_id": pnid
})
with patch.object(Scaleway, 'get') as mock_scw_get:
mock_scw_get.return_value = response_without_nics()
with patch.object(Scaleway, 'post') as mock_scw_post:
mock_scw_post.return_value = response_when_add_nics()
with pytest.raises(SystemExit) as results:
scaleway_compute_private_network.main()
mock_scw_post.assert_any_call(path=url, data={"private_network_id": pnid})
mock_scw_get.assert_any_call(url)
out, err = capfd.readouterr()
del os.environ['SCW_API_TOKEN']
assert not err
assert json.loads(out)['changed']
def test_scaleway_add_existing_nic(capfd):
os.environ['SCW_API_TOKEN'] = 'notrealtoken'
pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90'
cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90'
url = 'servers/' + cid + '/private_nics'
set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"state": "present",
"region": "par1",
"compute_id": cid,
"private_network_id": pnid
})
with patch.object(Scaleway, 'get') as mock_scw_get:
mock_scw_get.return_value = response_with_nics()
with pytest.raises(SystemExit) as results:
scaleway_compute_private_network.main()
mock_scw_get.assert_any_call(url)
out, err = capfd.readouterr()
del os.environ['SCW_API_TOKEN']
assert not err
assert not json.loads(out)['changed']
def test_scaleway_remove_existing_nic(capfd):
os.environ['SCW_API_TOKEN'] = 'notrealtoken'
pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90'
cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90'
nicid = 'c123b4cd-ef5g-678h-90i1-jk2345678l90'
url = 'servers/' + cid + '/private_nics'
urlremove = 'servers/' + cid + '/private_nics/' + nicid
set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"state": "absent",
"region": "par1",
"compute_id": cid,
"private_network_id": pnid
})
with patch.object(Scaleway, 'get') as mock_scw_get:
mock_scw_get.return_value = response_with_nics()
with patch.object(Scaleway, 'delete') as mock_scw_delete:
mock_scw_delete.return_value = response_remove_nics()
with pytest.raises(SystemExit) as results:
scaleway_compute_private_network.main()
mock_scw_delete.assert_any_call(urlremove)
mock_scw_get.assert_any_call(url)
out, err = capfd.readouterr()
del os.environ['SCW_API_TOKEN']
assert not err
assert json.loads(out)['changed']
def test_scaleway_remove_absent_nic(capfd):
os.environ['SCW_API_TOKEN'] = 'notrealtoken'
pnid = 'b589b4cd-ef5g-678h-90i1-jk2345678l90'
cid = 'c004b4cd-ef5g-678h-90i1-jk2345678l90'
url = 'servers/' + cid + '/private_nics'
set_module_args({"project": "a123b4cd-ef5g-678h-90i1-jk2345678l90",
"state": "absent",
"region": "par1",
"compute_id": cid,
"private_network_id": pnid
})
with patch.object(Scaleway, 'get') as mock_scw_get:
mock_scw_get.return_value = response_without_nics()
with pytest.raises(SystemExit) as results:
scaleway_compute_private_network.main()
mock_scw_get.assert_any_call(url)
out, err = capfd.readouterr()
del os.environ['SCW_API_TOKEN']
assert not err
assert not json.loads(out)['changed']

View File

@@ -98,6 +98,12 @@ TESTCASE_CONNECTION = [
'state': 'absent', 'state': 'absent',
'_ansible_check_mode': True, '_ansible_check_mode': True,
}, },
{
'type': 'vpn',
'conn_name': 'non_existent_nw_device',
'state': 'absent',
'_ansible_check_mode': True,
},
] ]
TESTCASE_GENERIC = [ TESTCASE_GENERIC = [
@@ -449,6 +455,17 @@ ipv6.ignore-auto-dns: no
ipv6.ignore-auto-routes: no ipv6.ignore-auto-routes: no
""" """
TESTCASE_GENERIC_ZONE_ONLY = [
{
'type': 'generic',
'conn_name': 'non_existent_nw_device',
'ifname': 'generic_non_existant',
'state': 'present',
'zone': 'public',
'_ansible_check_mode': False,
}
]
TESTCASE_BOND = [ TESTCASE_BOND = [
{ {
'type': 'bond', 'type': 'bond',
@@ -1177,6 +1194,69 @@ wireguard.ip4-auto-default-route: -1 (default)
wireguard.ip6-auto-default-route: -1 (default) wireguard.ip6-auto-default-route: -1 (default)
""" """
TESTCASE_VPN_L2TP = [
{
'type': 'vpn',
'conn_name': 'vpn_l2tp',
'vpn': {
'permissions': 'brittany',
'service-type': 'l2tp',
'gateway': 'vpn.example.com',
'password-flags': '2',
'user': 'brittany',
'ipsec-enabled': 'true',
'ipsec-psk': 'QnJpdHRhbnkxMjM=',
},
'autoconnect': 'false',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_VPN_L2TP_SHOW_OUTPUT = """\
connection.id: vpn_l2tp
connection.type: vpn
connection.autoconnect: no
connection.permissions: brittany
ipv4.method: auto
ipv6.method: auto
vpn-type: l2tp
vpn.service-type: org.freedesktop.NetworkManager.l2tp
vpn.data: gateway=vpn.example.com, password-flags=2, user=brittany, ipsec-enabled=true, ipsec-psk=QnJpdHRhbnkxMjM=
vpn.secrets: ipsec-psk = QnJpdHRhbnkxMjM=
vpn.persistent: no
vpn.timeout: 0
"""
TESTCASE_VPN_PPTP = [
{
'type': 'vpn',
'conn_name': 'vpn_pptp',
'vpn': {
'permissions': 'brittany',
'service-type': 'pptp',
'gateway': 'vpn.example.com',
'password-flags': '2',
'user': 'brittany',
},
'autoconnect': 'false',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_VPN_PPTP_SHOW_OUTPUT = """\
connection.id: vpn_pptp
connection.type: vpn
connection.autoconnect: no
connection.permissions: brittany
ipv4.method: auto
ipv6.method: auto
vpn-type: pptp
vpn.service-type: org.freedesktop.NetworkManager.pptp
vpn.data: password-flags=2, gateway=vpn.example.com, user=brittany
"""
def mocker_set(mocker, def mocker_set(mocker,
connection_exists=False, connection_exists=False,
@@ -1547,6 +1627,20 @@ def mocked_wireguard_connection_unchanged(mocker):
execute_return=(0, TESTCASE_WIREGUARD_SHOW_OUTPUT, "")) execute_return=(0, TESTCASE_WIREGUARD_SHOW_OUTPUT, ""))
@pytest.fixture
def mocked_vpn_l2tp_connection_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_VPN_L2TP_SHOW_OUTPUT, ""))
@pytest.fixture
def mocked_vpn_pptp_connection_unchanged(mocker):
mocker_set(mocker,
connection_exists=True,
execute_return=(0, TESTCASE_VPN_PPTP_SHOW_OUTPUT, ""))
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module']) @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module'])
def test_bond_connection_create(mocked_generic_connection_create, capfd): def test_bond_connection_create(mocked_generic_connection_create, capfd):
""" """
@@ -1805,6 +1899,30 @@ def test_generic_connection_zone_unchanged(mocked_generic_connection_zone_unchan
assert not results['changed'] assert not results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_GENERIC_ZONE_ONLY, indirect=['patch_ansible_module'])
def test_generic_connection_modify_zone_only(mocked_generic_connection_modify, capfd):
"""
Test : Generic connection modified with zone only
"""
with pytest.raises(SystemExit):
nmcli.main()
assert nmcli.Nmcli.execute_command.call_count == 1
arg_list = nmcli.Nmcli.execute_command.call_args_list
args, kwargs = arg_list[0]
assert 'connection.zone' in args[0]
assert 'ipv4.addresses' not in args[0]
assert 'ipv4.gateway' not in args[0]
assert 'ipv6.addresses' not in args[0]
assert 'ipv6.gateway' not in args[0]
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_CONNECTION, indirect=['patch_ansible_module']) @pytest.mark.parametrize('patch_ansible_module', TESTCASE_CONNECTION, indirect=['patch_ansible_module'])
def test_zone_none(mocked_connection_exists, capfd): def test_zone_none(mocked_connection_exists, capfd):
""" """
@@ -3456,3 +3574,111 @@ def test_wireguard_mod(mocked_generic_connection_modify, capfd):
results = json.loads(out) results = json.loads(out)
assert not results.get('failed') assert not results.get('failed')
assert results['changed'] assert results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_L2TP, indirect=['patch_ansible_module'])
def test_vpn_l2tp_connection_unchanged(mocked_vpn_l2tp_connection_unchanged, capfd):
"""
Test : L2TP VPN connection unchanged
"""
with pytest.raises(SystemExit):
nmcli.main()
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert not results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_PPTP, indirect=['patch_ansible_module'])
def test_vpn_pptp_connection_unchanged(mocked_vpn_pptp_connection_unchanged, capfd):
"""
Test : PPTP VPN connection unchanged
"""
with pytest.raises(SystemExit):
nmcli.main()
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert not results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_L2TP, indirect=['patch_ansible_module'])
def test_create_vpn_l2tp(mocked_generic_connection_create, capfd):
"""
Test : Create L2TP VPN connection
"""
with pytest.raises(SystemExit):
nmcli.main()
assert nmcli.Nmcli.execute_command.call_count == 1
arg_list = nmcli.Nmcli.execute_command.call_args_list
add_args, add_kw = arg_list[0]
assert add_args[0][0] == '/usr/bin/nmcli'
assert add_args[0][1] == 'con'
assert add_args[0][2] == 'add'
assert add_args[0][3] == 'type'
assert add_args[0][4] == 'vpn'
assert add_args[0][5] == 'con-name'
assert add_args[0][6] == 'vpn_l2tp'
add_args_text = list(map(to_text, add_args[0]))
for param in ['connection.autoconnect', 'no',
'connection.permissions', 'brittany',
'vpn.data', 'vpn-type', 'l2tp',
]:
assert param in add_args_text
vpn_data_index = add_args_text.index('vpn.data') + 1
args_vpn_data = add_args_text[vpn_data_index]
for vpn_data in ['gateway=vpn.example.com', 'password-flags=2', 'user=brittany', 'ipsec-enabled=true', 'ipsec-psk=QnJpdHRhbnkxMjM=']:
assert vpn_data in args_vpn_data
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']
@pytest.mark.parametrize('patch_ansible_module', TESTCASE_VPN_PPTP, indirect=['patch_ansible_module'])
def test_create_vpn_pptp(mocked_generic_connection_create, capfd):
"""
Test : Create PPTP VPN connection
"""
with pytest.raises(SystemExit):
nmcli.main()
assert nmcli.Nmcli.execute_command.call_count == 1
arg_list = nmcli.Nmcli.execute_command.call_args_list
add_args, add_kw = arg_list[0]
assert add_args[0][0] == '/usr/bin/nmcli'
assert add_args[0][1] == 'con'
assert add_args[0][2] == 'add'
assert add_args[0][3] == 'type'
assert add_args[0][4] == 'vpn'
assert add_args[0][5] == 'con-name'
assert add_args[0][6] == 'vpn_pptp'
add_args_text = list(map(to_text, add_args[0]))
for param in ['connection.autoconnect', 'no',
'connection.permissions', 'brittany',
'vpn.data', 'vpn-type', 'pptp',
]:
assert param in add_args_text
vpn_data_index = add_args_text.index('vpn.data') + 1
args_vpn_data = add_args_text[vpn_data_index]
for vpn_data in ['password-flags=2', 'gateway=vpn.example.com', 'user=brittany']:
assert vpn_data in args_vpn_data
out, err = capfd.readouterr()
results = json.loads(out)
assert not results.get('failed')
assert results['changed']

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Author: Jiri Hnidek (jhnidek@redhat.com) # Author: Jiri Hnidek (jhnidek@redhat.com)
# #
# 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)
@@ -118,7 +119,7 @@ TEST_CASES = [
(0, 'system identity: b26df632-25ed-4452-8f89-0308bfd167cb', '') (0, 'system identity: b26df632-25ed-4452-8f89-0308bfd167cb', '')
), ),
( (
['/testbin/subscription-manager', 'unsubscribe', '--all'], ['/testbin/subscription-manager', 'remove', '--all'],
{'check_rc': True}, {'check_rc': True},
(0, '', '') (0, '', '')
), ),
@@ -755,7 +756,7 @@ Entitlement Type: Physical
( (
[ [
'/testbin/subscription-manager', '/testbin/subscription-manager',
'unsubscribe', 'remove',
'--serial=7807912223970164816', '--serial=7807912223970164816',
], ],
{'check_rc': True}, {'check_rc': True},

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Author: Alexei Znamensky (russoz@gmail.com)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
from ansible_collections.community.general.plugins.modules.system import gconftool2_info
import pytest
TESTED_MODULE = gconftool2_info.__name__
@pytest.fixture
def patch_gconftool2_info(mocker):
"""
Function used for mocking some parts of redhat_subscribtion module
"""
mocker.patch('ansible_collections.community.general.plugins.module_utils.mh.module_helper.AnsibleModule.get_bin_path',
return_value='/testbin/gconftool-2')
TEST_CASES = [
[
{'key': '/desktop/gnome/background/picture_filename'},
{
'id': 'test_simple_element_get',
'run_command.calls': [
(
# Calling of following command will be asserted
['/testbin/gconftool-2', '--get', '/desktop/gnome/background/picture_filename'],
# Was return code checked?
{'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True},
# Mock of returned code, stdout and stderr
(0, '100\n', '',),
),
],
'value': '100',
}
],
[
{'key': '/desktop/gnome/background/picture_filename'},
{
'id': 'test_simple_element_get_not_found',
'run_command.calls': [
(
# Calling of following command will be asserted
['/testbin/gconftool-2', '--get', '/desktop/gnome/background/picture_filename'],
# Was return code checked?
{'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True},
# Mock of returned code, stdout and stderr
(0, '', "No value set for `/desktop/gnome/background/picture_filename'\n",),
),
],
'value': None,
}
],
]
TEST_CASES_IDS = [item[1]['id'] for item in TEST_CASES]
@pytest.mark.parametrize('patch_ansible_module, testcase',
TEST_CASES,
ids=TEST_CASES_IDS,
indirect=['patch_ansible_module'])
@pytest.mark.usefixtures('patch_ansible_module')
def test_gconftool2_info(mocker, capfd, patch_gconftool2_info, testcase):
"""
Run unit tests for test cases listen in TEST_CASES
"""
# Mock function used for running commands first
call_results = [item[2] for item in testcase['run_command.calls']]
mock_run_command = mocker.patch(
'ansible_collections.community.general.plugins.module_utils.mh.module_helper.AnsibleModule.run_command',
side_effect=call_results)
# Try to run test case
with pytest.raises(SystemExit):
gconftool2_info.main()
out, err = capfd.readouterr()
results = json.loads(out)
print("testcase =\n%s" % testcase)
print("results =\n%s" % results)
for conditional_test_result in ('value',):
if conditional_test_result in testcase:
assert conditional_test_result in results, "'{0}' not found in {1}".format(conditional_test_result, results)
assert results[conditional_test_result] == testcase[conditional_test_result], \
"'{0}': '{1}' != '{2}'".format(conditional_test_result, results[conditional_test_result], testcase[conditional_test_result])
assert mock_run_command.call_count == len(testcase['run_command.calls'])
if mock_run_command.call_count:
call_args_list = [(item[0][0], item[1]) for item in mock_run_command.call_args_list]
expected_call_args_list = [(item[0], item[1]) for item in testcase['run_command.calls']]
print("call args list =\n%s" % call_args_list)
print("expected args list =\n%s" % expected_call_args_list)
assert call_args_list == expected_call_args_list

View File

@@ -29,10 +29,11 @@ dnsimple >= 2 ; python_version >= '3.6'
dataclasses ; python_version == '3.6' dataclasses ; python_version == '3.6'
# requirement for the opentelemetry callback plugin # requirement for the opentelemetry callback plugin
# WARNING: these libraries depend on grpcio, which takes 7 minutes (!) to build in CI on Python 3.10 # WARNING: these libraries rely on Protobuf for Python, which regularly stops installing.
opentelemetry-api ; python_version >= '3.6' and python_version < '3.10' # That's why they are disabled for now.
opentelemetry-exporter-otlp ; python_version >= '3.6' and python_version < '3.10' # opentelemetry-api ; python_version >= '3.6' and python_version < '3.10'
opentelemetry-sdk ; python_version >= '3.6' and python_version < '3.10' # opentelemetry-exporter-otlp ; python_version >= '3.6' and python_version < '3.10'
# opentelemetry-sdk ; python_version >= '3.6' and python_version < '3.10'
# requirement for the elastic callback plugin # requirement for the elastic callback plugin
elastic-apm ; python_version >= '3.6' elastic-apm ; python_version >= '3.6'

View File

@@ -23,6 +23,7 @@ pytest-forked < 1.0.2 ; python_version < '2.7' # pytest-forked 1.0.2 and later r
pytest-forked >= 1.0.2 ; python_version >= '2.7' # pytest-forked before 1.0.2 does not work with pytest 4.2.0+ (which requires python 2.7+) pytest-forked >= 1.0.2 ; python_version >= '2.7' # pytest-forked before 1.0.2 does not work with pytest 4.2.0+ (which requires python 2.7+)
ntlm-auth >= 1.3.0 # message encryption support using cryptography ntlm-auth >= 1.3.0 # message encryption support using cryptography
requests < 2.20.0 ; python_version < '2.7' # requests 2.20.0 drops support for python 2.6 requests < 2.20.0 ; python_version < '2.7' # requests 2.20.0 drops support for python 2.6
requests < 2.28 ; python_version >= '2.7' and python_version < '3.7' # requests 2.28.0 drops support for python 3.6 and before
requests-ntlm >= 1.1.0 # message encryption support requests-ntlm >= 1.1.0 # message encryption support
requests-credssp >= 0.1.0 # message encryption support requests-credssp >= 0.1.0 # message encryption support
voluptuous >= 0.11.0 # Schema recursion via Self voluptuous >= 0.11.0 # Schema recursion via Self
@@ -48,6 +49,7 @@ cffi >= 1.14.2, != 1.14.3 # Yanked version which older versions of pip will stil
redis == 2.10.6 ; python_version < '2.7' redis == 2.10.6 ; python_version < '2.7'
redis < 4.0.0 ; python_version >= '2.7' and python_version < '3.6' redis < 4.0.0 ; python_version >= '2.7' and python_version < '3.6'
redis ; python_version >= '3.6' redis ; python_version >= '3.6'
pycdlib < 1.13.0 ; python_version < '3' # 1.13.0 does not work with Python 2, while not declaring that
# freeze pylint and its requirements for consistent test results # freeze pylint and its requirements for consistent test results
astroid == 2.2.5 astroid == 2.2.5