mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-29 09:56:53 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3903e6e0 | ||
|
|
4b6b00d249 | ||
|
|
0a0b0cb42d | ||
|
|
d0b39271b3 | ||
|
|
f07cb76b09 | ||
|
|
09031fc9e6 | ||
|
|
4481d0a4a9 | ||
|
|
5861388f11 | ||
|
|
c581daa48a | ||
|
|
75e2de3581 | ||
|
|
6c7bee1225 | ||
|
|
eafcdfbceb | ||
|
|
82a764446b | ||
|
|
a0032f3513 | ||
|
|
8444367cd0 | ||
|
|
de5fbe457f | ||
|
|
40b35acee2 | ||
|
|
9835deb17f | ||
|
|
6fe9cf11f1 | ||
|
|
d3ebdd2874 | ||
|
|
4275bfe87b | ||
|
|
2f87b8c63f | ||
|
|
100fffb4c1 | ||
|
|
1206900488 | ||
|
|
c28ae26636 | ||
|
|
e1e626cdcb | ||
|
|
f8d35eeb14 | ||
|
|
c44298c437 | ||
|
|
1b580476a8 | ||
|
|
44d2d62d38 | ||
|
|
82b2d294b7 | ||
|
|
812fbef786 | ||
|
|
9d795c334b | ||
|
|
512d412eb4 | ||
|
|
8f0ee6966f | ||
|
|
3af9e39043 | ||
|
|
7b78512c59 | ||
|
|
9f0913bf73 | ||
|
|
aea851018b | ||
|
|
69c79f618e | ||
|
|
6a51ba5169 | ||
|
|
52e8e7e928 |
@@ -48,7 +48,7 @@ variables:
|
||||
resources:
|
||||
containers:
|
||||
- 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
|
||||
|
||||
|
||||
19
.github/BOTMETA.yml
vendored
19
.github/BOTMETA.yml
vendored
@@ -254,6 +254,9 @@ files:
|
||||
maintainers: amigus endlesstrax
|
||||
$module_utils/:
|
||||
labels: module_utils
|
||||
$module_utils/gconftool2.py:
|
||||
maintainers: russoz
|
||||
labels: gconftool2
|
||||
$module_utils/gitlab.py:
|
||||
notify: jlozadad
|
||||
maintainers: $team_gitlab
|
||||
@@ -304,6 +307,9 @@ files:
|
||||
$module_utils/xenserver.py:
|
||||
maintainers: bvitnik
|
||||
labels: xenserver
|
||||
$module_utils/xfconf.py:
|
||||
maintainers: russoz
|
||||
labels: xfconf
|
||||
$modules/cloud/alicloud/:
|
||||
maintainers: xiaozhu36
|
||||
$modules/cloud/atomic/atomic_container.py:
|
||||
@@ -434,6 +440,8 @@ files:
|
||||
maintainers: claco
|
||||
$modules/cloud/scaleway/:
|
||||
maintainers: $team_scaleway
|
||||
$modules/cloud/scaleway/scaleway_compute_private_network.py:
|
||||
maintainers: pastral
|
||||
$modules/cloud/scaleway/scaleway_database_backup.py:
|
||||
maintainers: guillaume_ro_fr
|
||||
$modules/cloud/scaleway/scaleway_image_info.py:
|
||||
@@ -1024,7 +1032,7 @@ files:
|
||||
$modules/system/alternatives.py:
|
||||
maintainers: mulby
|
||||
labels: alternatives
|
||||
ignore: DavidWittman
|
||||
ignore: DavidWittman jiuka
|
||||
$modules/system/aix_lvol.py:
|
||||
maintainers: adejoux
|
||||
$modules/system/awall.py:
|
||||
@@ -1052,6 +1060,9 @@ files:
|
||||
$modules/system/gconftool2.py:
|
||||
maintainers: Akasurde kevensen
|
||||
labels: gconftool2
|
||||
$modules/system/gconftool2_info.py:
|
||||
maintainers: russoz
|
||||
labels: gconftool2
|
||||
$modules/system/homectl.py:
|
||||
maintainers: jameslivulpi
|
||||
$modules/system/interfaces_file.py:
|
||||
@@ -1059,6 +1070,10 @@ files:
|
||||
labels: interfaces_file
|
||||
$modules/system/iptables_state.py:
|
||||
maintainers: quidame
|
||||
$modules/system/keyring.py:
|
||||
maintainers: ahussey-redhat
|
||||
$modules/system/keyring_info.py:
|
||||
maintainers: ahussey-redhat
|
||||
$modules/system/shutdown.py:
|
||||
maintainers: nitzmahone samdoran aminvakil
|
||||
$modules/system/java_cert.py:
|
||||
@@ -1281,5 +1296,5 @@ macros:
|
||||
team_rhn: FlossWare alikins barnabycourt vritant
|
||||
team_scaleway: remyleone abarbare
|
||||
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
|
||||
|
||||
@@ -6,6 +6,101 @@ Community General Release Notes
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
|
||||
48
PSF-license.txt
Normal file
48
PSF-license.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
|
||||
All Rights Reserved" are retained in Python alone or in any derivative version
|
||||
prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
@@ -801,3 +801,115 @@ releases:
|
||||
- 5.0.2.yml
|
||||
- simplified-bsd-license.yml
|
||||
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'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: general
|
||||
version: 5.0.2
|
||||
version: 5.2.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
||||
@@ -292,6 +292,8 @@ plugin_routing:
|
||||
redirect: community.google.gce_tag
|
||||
gconftool2:
|
||||
redirect: community.general.system.gconftool2
|
||||
gconftool2_info:
|
||||
redirect: community.general.system.gconftool2_info
|
||||
gcp_backend_service:
|
||||
tombstone:
|
||||
removal_version: 2.0.0
|
||||
@@ -606,6 +608,10 @@ plugin_routing:
|
||||
redirect: community.general.identity.keycloak.keycloak_role
|
||||
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:
|
||||
redirect: community.general.database.misc.kibana_plugin
|
||||
kubevirt_cdi_upload:
|
||||
@@ -1355,6 +1361,8 @@ plugin_routing:
|
||||
redirect: community.general.notification.say
|
||||
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:
|
||||
redirect: community.general.cloud.scaleway.scaleway_database_backup
|
||||
scaleway_image_facts:
|
||||
|
||||
@@ -92,9 +92,21 @@ DOCUMENTATION = '''
|
||||
default: proxmox_
|
||||
type: str
|
||||
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
|
||||
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:
|
||||
version_added: 3.0.0
|
||||
description:
|
||||
@@ -431,6 +443,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
def _get_vm_status(self, properties, node, vmid, vmtype, name):
|
||||
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/status/current" % (self.proxmox_url, node, vmtype, vmid))
|
||||
properties[self._fact('status')] = ret['status']
|
||||
if vmtype == 'qemu':
|
||||
properties[self._fact('qmpstatus')] = ret['qmpstatus']
|
||||
|
||||
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))
|
||||
@@ -489,7 +503,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
name, vmid = item['name'], item['vmid']
|
||||
|
||||
# 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_config(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))
|
||||
self.inventory.add_child(self._group('all_' + ittype), name)
|
||||
self.inventory.add_child(node_type_group, name)
|
||||
if item['status'] == 'stopped':
|
||||
self.inventory.add_child(self._group('all_stopped'), name)
|
||||
elif item['status'] == 'running':
|
||||
self.inventory.add_child(self._group('all_running'), name)
|
||||
|
||||
item_status = item['status']
|
||||
if item_status == 'running':
|
||||
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
|
||||
|
||||
@@ -528,10 +546,14 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
def _populate(self):
|
||||
|
||||
# create common groups
|
||||
self.inventory.add_group(self._group('all_lxc'))
|
||||
self.inventory.add_group(self._group('all_qemu'))
|
||||
self.inventory.add_group(self._group('all_running'))
|
||||
self.inventory.add_group(self._group('all_stopped'))
|
||||
default_groups = ['lxc', 'qemu', 'running', 'stopped']
|
||||
|
||||
if self.get_option('qemu_extended_statuses'):
|
||||
default_groups.extend(['prelaunch', 'paused'])
|
||||
|
||||
for group in default_groups:
|
||||
self.inventory.add_group(self._group('all_%s' % (group)))
|
||||
|
||||
nodes_group = self._group('nodes')
|
||||
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):
|
||||
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.use_cache = cache and self.get_option('cache')
|
||||
self.host_filters = self.get_option('filters')
|
||||
|
||||
@@ -106,6 +106,22 @@ DOCUMENTATION = '''
|
||||
type: str
|
||||
default: 15m
|
||||
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 = """
|
||||
ansible.cfg: |
|
||||
@@ -231,6 +247,24 @@ def check_output2(*popenargs, **kwargs):
|
||||
|
||||
|
||||
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):
|
||||
# I went with the "traditional" param followed with space separated KV pairs.
|
||||
# Waiting for final implementation of lookup parameter parsing.
|
||||
@@ -270,10 +304,12 @@ class LookupModule(LookupBase):
|
||||
self.env = os.environ.copy()
|
||||
self.env['LANGUAGE'] = 'C' # make sure to get errors in English as required by check_output2
|
||||
|
||||
# Set PASSWORD_STORE_DIR
|
||||
if os.path.isdir(self.paramvals['directory']):
|
||||
if self.backend == 'gopass':
|
||||
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']
|
||||
else:
|
||||
elif self.is_real_pass():
|
||||
raise AnsibleError('Passwordstore directory \'{0}\' does not exist'.format(self.paramvals['directory']))
|
||||
|
||||
# Set PASSWORD_STORE_UMASK if umask is set
|
||||
@@ -288,7 +324,9 @@ class LookupModule(LookupBase):
|
||||
def check_pass(self):
|
||||
try:
|
||||
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'
|
||||
).splitlines()
|
||||
self.password = self.passoutput[0]
|
||||
@@ -302,8 +340,10 @@ class LookupModule(LookupBase):
|
||||
if ':' in line:
|
||||
name, value = line.split(':', 1)
|
||||
self.passdict[name.strip()] = value.strip()
|
||||
if os.path.isfile(os.path.join(self.paramvals['directory'], self.passname + ".gpg")):
|
||||
# Only accept password as found, if there a .gpg file for it (might be a tree node otherwise)
|
||||
if (self.backend == 'gopass' or
|
||||
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
|
||||
except (subprocess.CalledProcessError) as e:
|
||||
# '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']:
|
||||
msg += "lookup_pass: old password was {0} (Updated on {1})\n".format(self.password, datetime)
|
||||
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:
|
||||
raise AnsibleError(e)
|
||||
return newpass
|
||||
@@ -351,7 +391,7 @@ class LookupModule(LookupBase):
|
||||
datetime = time.strftime("%d/%m/%Y %H:%M:%S")
|
||||
msg = newpass + '\n' + "lookup_pass: First generated by ansible on {0}\n".format(datetime)
|
||||
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:
|
||||
raise AnsibleError(e)
|
||||
return newpass
|
||||
@@ -380,6 +420,8 @@ class LookupModule(LookupBase):
|
||||
yield
|
||||
|
||||
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
|
||||
timeout = self.get_option('locktimeout')
|
||||
if not re.match('^[0-9]+[smh]$', timeout):
|
||||
@@ -402,6 +444,7 @@ class LookupModule(LookupBase):
|
||||
}
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
self.setup(variables)
|
||||
result = []
|
||||
|
||||
|
||||
@@ -3,51 +3,7 @@
|
||||
# This particular file snippet, and this file snippet only, is based on
|
||||
# Lib/posixpath.py of cpython
|
||||
# It is licensed under the 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 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.
|
||||
# (See PSF-license.txt in this collection)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ class CmdRunner(object):
|
||||
if mod_param_name not in self.arg_formats:
|
||||
self.arg_formats[mod_param_name] = _Format.as_default_type(spec['type'], mod_param_name)
|
||||
|
||||
def context(self, args_order=None, output_process=None, ignore_value_none=True, **kwargs):
|
||||
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:
|
||||
output_process = _process_as_is
|
||||
if args_order is None:
|
||||
@@ -209,18 +209,25 @@ class CmdRunner(object):
|
||||
return _CmdRunnerContext(runner=self,
|
||||
args_order=args_order,
|
||||
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):
|
||||
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):
|
||||
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.args_order = tuple(args_order)
|
||||
self.output_process = output_process
|
||||
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.environ_update = runner.environ_update
|
||||
@@ -260,6 +267,8 @@ class _CmdRunnerContext(object):
|
||||
except Exception as 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)
|
||||
self.results_rc, self.results_out, self.results_err = results
|
||||
self.results_processed = self.output_process(*results)
|
||||
@@ -288,4 +297,12 @@ class _CmdRunnerContext(object):
|
||||
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
|
||||
|
||||
26
plugins/module_utils/gconftool2.py
Normal file
26
plugins/module_utils/gconftool2.py
Normal 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
|
||||
)
|
||||
@@ -6,17 +6,13 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
|
||||
class ModuleHelperException(Exception):
|
||||
@staticmethod
|
||||
def _get_remove(key, kwargs):
|
||||
if key in kwargs:
|
||||
result = kwargs[key]
|
||||
del kwargs[key]
|
||||
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 {}
|
||||
def __init__(self, msg, update_output=None, *args, **kwargs):
|
||||
self.msg = to_native(msg or "Module failed with exception: {0}".format(self))
|
||||
if update_output is None:
|
||||
update_output = {}
|
||||
self.update_output = update_output
|
||||
super(ModuleHelperException, self).__init__(*args)
|
||||
|
||||
@@ -2187,9 +2187,8 @@ class RedfishUtils(object):
|
||||
else:
|
||||
if media_match_strict:
|
||||
continue
|
||||
# if ejected, 'Inserted' should be False and 'ImageName' cleared
|
||||
if (not data.get('Inserted', False) and
|
||||
not data.get('ImageName')):
|
||||
# if ejected, 'Inserted' should be False
|
||||
if (not data.get('Inserted', False)):
|
||||
return uri, data
|
||||
return None, None
|
||||
|
||||
@@ -2225,7 +2224,7 @@ class RedfishUtils(object):
|
||||
return resources, headers
|
||||
|
||||
@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 = {
|
||||
'Image': options.get('image_url')
|
||||
}
|
||||
@@ -2239,6 +2238,12 @@ class RedfishUtils(object):
|
||||
options.get(option), option,
|
||||
allowable)}
|
||||
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
|
||||
|
||||
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()
|
||||
if k.endswith('@Redfish.AllowableValues'))
|
||||
# construct payload
|
||||
payload = self._insert_virt_media_payload(options, param_map, data, ai)
|
||||
if 'Inserted' not in payload:
|
||||
payload = self._insert_virt_media_payload(options, param_map, data, ai, image_only)
|
||||
if 'Inserted' not in payload and not image_only:
|
||||
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
|
||||
response = self.patch_request(self.root_uri + uri, payload)
|
||||
if response['ret'] is False:
|
||||
@@ -2292,6 +2291,13 @@ class RedfishUtils(object):
|
||||
if data["FirmwareVersion"].startswith("iLO 4"):
|
||||
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"]
|
||||
response = self.get_request(self.root_uri + virt_media_uri)
|
||||
if response['ret'] is False:
|
||||
@@ -2346,7 +2352,7 @@ class RedfishUtils(object):
|
||||
# get ActionInfo or AllowableValues
|
||||
ai = self._get_all_action_info_values(action)
|
||||
# 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
|
||||
response = self.post_request(self.root_uri + action_uri, payload)
|
||||
if response['ret'] is False:
|
||||
@@ -2392,6 +2398,9 @@ class RedfishUtils(object):
|
||||
if data["FirmwareVersion"].startswith("iLO 4"):
|
||||
image_only = True
|
||||
|
||||
if 'Supermicro' in data['Oem']:
|
||||
image_only = True
|
||||
|
||||
virt_media_uri = data["VirtualMedia"]["@odata.id"]
|
||||
response = self.get_request(self.root_uri + virt_media_uri)
|
||||
if response['ret'] is False:
|
||||
|
||||
37
plugins/module_utils/xfconf.py
Normal file
37
plugins/module_utils/xfconf.py
Normal 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
|
||||
@@ -83,7 +83,7 @@ options:
|
||||
version_added: 1.3.0
|
||||
clone:
|
||||
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
|
||||
cores:
|
||||
description:
|
||||
|
||||
@@ -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()
|
||||
@@ -45,8 +45,8 @@ options:
|
||||
- The interface to bind the connection to.
|
||||
- The connection will only be applicable to this interface name.
|
||||
- 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.
|
||||
- This parameter defaults to C(conn_name) when left unset.
|
||||
- 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 for all connection types except vpn that removes it.
|
||||
type: str
|
||||
type:
|
||||
description:
|
||||
@@ -55,10 +55,11 @@ options:
|
||||
- Type C(generic) is added in Ansible 2.5.
|
||||
- Type C(infiniband) is added in community.general 2.0.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
|
||||
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:
|
||||
description:
|
||||
- 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.
|
||||
type: int
|
||||
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'''
|
||||
@@ -1288,6 +1341,23 @@ EXAMPLES = r'''
|
||||
autoconnect: true
|
||||
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"""#
|
||||
@@ -1404,6 +1474,7 @@ class Nmcli(object):
|
||||
self.wifi_sec = module.params['wifi_sec']
|
||||
self.gsm = module.params['gsm']
|
||||
self.wireguard = module.params['wireguard']
|
||||
self.vpn = module.params['vpn']
|
||||
|
||||
if self.method4:
|
||||
self.ipv4_method = self.method4
|
||||
@@ -1592,6 +1663,29 @@ class Nmcli(object):
|
||||
options.update({
|
||||
'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.
|
||||
for setting, value in options.items():
|
||||
setting_type = self.settings_type(setting)
|
||||
@@ -1742,7 +1836,10 @@ class Nmcli(object):
|
||||
|
||||
@staticmethod
|
||||
def list_to_string(lst):
|
||||
return ",".join(lst or [""])
|
||||
if lst is None:
|
||||
return None
|
||||
else:
|
||||
return ",".join(lst)
|
||||
|
||||
@staticmethod
|
||||
def settings_type(setting):
|
||||
@@ -1832,6 +1929,10 @@ class Nmcli(object):
|
||||
'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())
|
||||
|
||||
# Constructing the command.
|
||||
@@ -1997,6 +2098,9 @@ class Nmcli(object):
|
||||
current_value = current_value.strip('"')
|
||||
if key == self.mtu_setting and self.mtu is None:
|
||||
self.mtu = 0
|
||||
if key == 'vpn.data':
|
||||
current_value = list(map(str.strip, current_value.split(',')))
|
||||
value = list(map(str.strip, value.split(',')))
|
||||
else:
|
||||
# parameter does not exist
|
||||
current_value = None
|
||||
@@ -2025,6 +2129,10 @@ class Nmcli(object):
|
||||
'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:
|
||||
current_con_type = self.show_connection().get('connection.type')
|
||||
if current_con_type:
|
||||
@@ -2064,6 +2172,7 @@ def main():
|
||||
'wifi',
|
||||
'gsm',
|
||||
'wireguard',
|
||||
'vpn',
|
||||
]),
|
||||
ip4=dict(type='list', elements='str'),
|
||||
gw4=dict(type='str'),
|
||||
@@ -2163,6 +2272,7 @@ def main():
|
||||
wifi_sec=dict(type='dict', no_log=True),
|
||||
gsm=dict(type='dict'),
|
||||
wireguard=dict(type='dict'),
|
||||
vpn=dict(type='dict'),
|
||||
),
|
||||
mutually_exclusive=[['never_default4', 'gw4'],
|
||||
['routes4_extended', 'routes4'],
|
||||
|
||||
@@ -226,7 +226,7 @@ class AnsibleGalaxyInstall(CmdModuleHelper):
|
||||
check_rc = True
|
||||
|
||||
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)
|
||||
line = out.splitlines()[0]
|
||||
match = self._RE_GALAXY_VERSION.match(line)
|
||||
@@ -302,9 +302,9 @@ class AnsibleGalaxyInstall(CmdModuleHelper):
|
||||
self.vars.set("new_roles", {})
|
||||
self.vars.set("ansible29_change", False, change=True, output=False)
|
||||
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':
|
||||
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):
|
||||
self.vars.set("new_collections", {}, change=True)
|
||||
|
||||
@@ -468,7 +468,7 @@ class Rhsm(RegistrationBase):
|
||||
items = ["--all"]
|
||||
|
||||
if items:
|
||||
args = [SUBMAN_CMD, 'unsubscribe'] + items
|
||||
args = [SUBMAN_CMD, 'remove'] + items
|
||||
rc, stderr, stdout = self.module.run_command(args, check_rc=True)
|
||||
return serials
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# Copyright: (c) 2014, Gabe Mulley <gabe.mulley@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)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
@@ -17,6 +18,7 @@ description:
|
||||
- Manages symbolic links using the 'update-alternatives' tool.
|
||||
- Useful when multiple programs are installed but provide similar functionality (e.g. different editors).
|
||||
author:
|
||||
- Marius Rieder (@jiuka)
|
||||
- David Wittman (@DavidWittman)
|
||||
- Gabe Mulley (@mulby)
|
||||
options:
|
||||
@@ -38,19 +40,45 @@ options:
|
||||
type: path
|
||||
priority:
|
||||
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
|
||||
default: 50
|
||||
state:
|
||||
description:
|
||||
- C(present) - install the alternative (if not already installed), but do
|
||||
not set it as the currently selected alternative for the group.
|
||||
- C(selected) - install the alternative (if not already installed), and
|
||||
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
|
||||
type: str
|
||||
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 ]
|
||||
'''
|
||||
|
||||
@@ -78,6 +106,23 @@ EXAMPLES = r'''
|
||||
path: /usr/bin/python3.5
|
||||
link: /usr/bin/python
|
||||
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
|
||||
@@ -90,10 +135,238 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
class AlternativeState:
|
||||
PRESENT = "present"
|
||||
SELECTED = "selected"
|
||||
ABSENT = "absent"
|
||||
AUTO = "auto"
|
||||
|
||||
@classmethod
|
||||
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():
|
||||
@@ -103,115 +376,22 @@ def main():
|
||||
name=dict(type='str', required=True),
|
||||
path=dict(type='path', required=True),
|
||||
link=dict(type='path'),
|
||||
priority=dict(type='int', default=50),
|
||||
priority=dict(type='int'),
|
||||
state=dict(
|
||||
type='str',
|
||||
choices=AlternativeState.to_list(),
|
||||
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,
|
||||
)
|
||||
|
||||
params = module.params
|
||||
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)
|
||||
AlternativesModule(module)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
74
plugins/modules/system/gconftool2_info.py
Normal file
74
plugins/modules/system/gconftool2_info.py
Normal 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()
|
||||
270
plugins/modules/system/keyring.py
Normal file
270
plugins/modules/system/keyring.py
Normal 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()
|
||||
149
plugins/modules/system/keyring_info.py
Normal file
149
plugins/modules/system/keyring_info.py
Normal 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()
|
||||
@@ -51,6 +51,11 @@ options:
|
||||
description:
|
||||
- Puppet environment to be used.
|
||||
type: str
|
||||
confdir:
|
||||
description:
|
||||
- Path to the directory containing the puppet.conf file.
|
||||
type: str
|
||||
version_added: 5.1.0
|
||||
logdest:
|
||||
description:
|
||||
- Where the puppet logs should go, if puppet apply is being used.
|
||||
@@ -179,6 +184,7 @@ def main():
|
||||
puppetmaster=dict(type='str'),
|
||||
modulepath=dict(type='str'),
|
||||
manifest=dict(type='str'),
|
||||
confdir=dict(type='str'),
|
||||
noop=dict(type='bool'),
|
||||
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
|
||||
@@ -255,6 +261,8 @@ def main():
|
||||
cmd += " --server %s" % shlex_quote(p['puppetmaster'])
|
||||
if p['show_diff']:
|
||||
cmd += " --show_diff"
|
||||
if p['confdir']:
|
||||
cmd += " --confdir %s" % shlex_quote(p['confdir'])
|
||||
if p['environment']:
|
||||
cmd += " --environment '%s'" % p['environment']
|
||||
if p['tags']:
|
||||
|
||||
@@ -65,6 +65,15 @@ options:
|
||||
- The name of the user for the sudoers rule.
|
||||
- This option cannot be used in conjunction with I(group).
|
||||
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 = '''
|
||||
@@ -115,7 +124,11 @@ from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
class Sudoers(object):
|
||||
|
||||
FILE_MODE = 0o440
|
||||
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
|
||||
self.check_mode = module.check_mode
|
||||
self.name = module.params['name']
|
||||
self.user = module.params['user']
|
||||
@@ -126,6 +139,7 @@ class Sudoers(object):
|
||||
self.sudoers_path = module.params['sudoers_path']
|
||||
self.file = os.path.join(self.sudoers_path, self.name)
|
||||
self.commands = module.params['commands']
|
||||
self.validation = module.params['validation']
|
||||
|
||||
def write(self):
|
||||
if self.check_mode:
|
||||
@@ -134,6 +148,8 @@ class Sudoers(object):
|
||||
with open(self.file, 'w') as f:
|
||||
f.write(self.content())
|
||||
|
||||
os.chmod(self.file, self.FILE_MODE)
|
||||
|
||||
def delete(self):
|
||||
if self.check_mode:
|
||||
return
|
||||
@@ -145,7 +161,12 @@ class Sudoers(object):
|
||||
|
||||
def matches(self):
|
||||
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):
|
||||
if self.user:
|
||||
@@ -158,10 +179,29 @@ class Sudoers(object):
|
||||
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)
|
||||
|
||||
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):
|
||||
if self.state == 'absent' and self.exists():
|
||||
self.delete()
|
||||
return True
|
||||
if self.state == 'absent':
|
||||
if self.exists():
|
||||
self.delete()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
self.validate()
|
||||
|
||||
if self.exists() and self.matches():
|
||||
return False
|
||||
@@ -197,6 +237,10 @@ def main():
|
||||
'choices': ['present', 'absent'],
|
||||
},
|
||||
'user': {},
|
||||
'validation': {
|
||||
'default': 'detect',
|
||||
'choices': ['absent', 'detect', 'required']
|
||||
},
|
||||
}
|
||||
|
||||
module = AnsibleModule(
|
||||
|
||||
@@ -145,31 +145,15 @@ RETURN = '''
|
||||
sample: '"96" or ["red", "blue", "green"]'
|
||||
'''
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.module_helper import (
|
||||
CmdStateModuleHelper, ArgFormat
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper
|
||||
from ansible_collections.community.general.plugins.module_utils.xfconf import xfconf_runner
|
||||
|
||||
|
||||
class XFConfException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class XFConfProperty(CmdStateModuleHelper):
|
||||
class XFConfProperty(StateModuleHelper):
|
||||
change_params = 'value',
|
||||
diff_params = 'value',
|
||||
output_params = ('property', 'channel', 'value')
|
||||
@@ -191,27 +175,19 @@ class XFConfProperty(CmdStateModuleHelper):
|
||||
)
|
||||
|
||||
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):
|
||||
self.update_vars(meta={"output": True, "fact": True}, **kwargs)
|
||||
|
||||
def __init_module__(self):
|
||||
self.does_not = 'Property "{0}" does not exist on channel "{1}".'.format(self.module.params['property'],
|
||||
self.module.params['channel'])
|
||||
self.runner = xfconf_runner(self.module)
|
||||
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('type', self.vars.value_type, fact=True)
|
||||
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.')
|
||||
|
||||
def process_command_output(self, rc, out, err):
|
||||
@@ -229,11 +205,12 @@ class XFConfProperty(CmdStateModuleHelper):
|
||||
return result
|
||||
|
||||
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):
|
||||
if not self.module.check_mode:
|
||||
self.run_command(params=('channel', 'property', {'reset': True}))
|
||||
with self.runner('channel property reset', check_mode_skip=True) as ctx:
|
||||
ctx.run(reset=True)
|
||||
self.vars.value = None
|
||||
|
||||
def state_present(self):
|
||||
@@ -252,22 +229,14 @@ class XFConfProperty(CmdStateModuleHelper):
|
||||
# or complain if lists' lengths are different
|
||||
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
|
||||
self.vars.is_array = \
|
||||
bool(self.vars.force_array) or \
|
||||
isinstance(self.vars.previous_value, list) or \
|
||||
values_len > 1
|
||||
|
||||
params = ['channel', 'property', {'create': True}]
|
||||
if self.vars.is_array:
|
||||
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)
|
||||
with self.runner('channel property create force_array values_and_types', check_mode_skip=True) as ctx:
|
||||
ctx.run(create=True, force_array=self.vars.is_array, values_and_types=(self.vars.value, value_type))
|
||||
|
||||
if not self.vars.is_array:
|
||||
self.vars.value = self.vars.value[0]
|
||||
|
||||
@@ -74,7 +74,7 @@ RETURN = '''
|
||||
properties:
|
||||
description:
|
||||
- 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
|
||||
type: list
|
||||
elements: str
|
||||
@@ -116,14 +116,15 @@ RETURN = '''
|
||||
- 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):
|
||||
pass
|
||||
|
||||
|
||||
class XFConfInfo(CmdModuleHelper):
|
||||
class XFConfInfo(ModuleHelper):
|
||||
module = dict(
|
||||
argument_spec=dict(
|
||||
channel=dict(type='str'),
|
||||
@@ -135,16 +136,9 @@ class XFConfInfo(CmdModuleHelper):
|
||||
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):
|
||||
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)
|
||||
|
||||
def process_command_output(self, rc, out, err):
|
||||
@@ -167,7 +161,7 @@ class XFConfInfo(CmdModuleHelper):
|
||||
return lines
|
||||
|
||||
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'
|
||||
proc = self.process_command_output
|
||||
if self.vars.channel is None:
|
||||
@@ -176,15 +170,15 @@ class XFConfInfo(CmdModuleHelper):
|
||||
elif self.vars.property is None:
|
||||
output = 'properties'
|
||||
proc = self._process_list_properties
|
||||
result = self.run_command(params=('_list_arg', 'channel', 'property'), process_output=proc)
|
||||
if not self.vars._list_arg and self.vars.is_array:
|
||||
with self.runner.context('list_arg channel property', output_process=proc) as ctx:
|
||||
result = ctx.run(**self.vars)
|
||||
if not self.vars.list_arg and self.vars.is_array:
|
||||
output = "value_array"
|
||||
self.vars.set(output, result)
|
||||
|
||||
|
||||
def main():
|
||||
xfconf = XFConfInfo()
|
||||
xfconf.run()
|
||||
XFConfInfo.execute()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
# Test that path is checked: alternatives must fail when path is nonexistent
|
||||
- import_tasks: path_is_checked.yml
|
||||
|
||||
# Test that subcommands commands work
|
||||
- import_tasks: subcommands.yml
|
||||
|
||||
# Test operation of the 'state' parameter
|
||||
- block:
|
||||
- include_tasks: remove_links.yml
|
||||
@@ -63,6 +66,8 @@
|
||||
state: absent
|
||||
with_items:
|
||||
- '{{ alternatives_dir }}/dummy'
|
||||
- '{{ alternatives_dir }}/dummymain'
|
||||
- '{{ alternatives_dir }}/dummysubcmd'
|
||||
|
||||
- file:
|
||||
path: '/usr/bin/dummy{{ item }}'
|
||||
|
||||
217
tests/integration/targets/alternatives/tasks/subcommands.yml
Normal file
217
tests/integration/targets/alternatives/tasks/subcommands.yml
Normal 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
|
||||
@@ -49,5 +49,3 @@
|
||||
|
||||
- name: check that alternative has been updated
|
||||
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'"
|
||||
|
||||
@@ -21,3 +21,29 @@
|
||||
|
||||
- name: check that alternative has been updated
|
||||
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'"
|
||||
|
||||
@@ -49,6 +49,28 @@
|
||||
- cmd.stdout == "dummy4"
|
||||
|
||||
# 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'
|
||||
# mode, and the highest priority alternative is selected.
|
||||
- name: Set current selected dummy to state = present
|
||||
@@ -56,7 +78,7 @@
|
||||
name: dummy
|
||||
path: /usr/bin/dummy4
|
||||
link: /usr/bin/dummy
|
||||
state: present
|
||||
state: auto
|
||||
|
||||
- name: Ensure that the link group is in auto mode
|
||||
shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^auto$"'
|
||||
@@ -69,3 +91,25 @@
|
||||
assert:
|
||||
that:
|
||||
- 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"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
---
|
||||
alternatives_dir: /var/lib/dpkg/alternatives/
|
||||
alternatives_command: update-alternatives
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
---
|
||||
alternatives_dir: /var/lib/rpm/alternatives/
|
||||
alternatives_command: update-alternatives
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
---
|
||||
alternatives_dir: /var/lib/alternatives/
|
||||
alternatives_command: update-alternatives
|
||||
|
||||
@@ -6,43 +6,15 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import sys
|
||||
|
||||
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
|
||||
'''
|
||||
DOCUMENTATION = ""
|
||||
|
||||
EXAMPLES = ""
|
||||
|
||||
RETURN = ""
|
||||
|
||||
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():
|
||||
@@ -51,11 +23,15 @@ def main():
|
||||
arg_formats=dict(type="dict", default={}),
|
||||
arg_order=dict(type="raw", required=True),
|
||||
arg_values=dict(type="dict", default={}),
|
||||
check_mode_skip=dict(type="bool", default=False),
|
||||
aa=dict(type="raw"),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
p = module.params
|
||||
|
||||
info = None
|
||||
|
||||
arg_formats = {}
|
||||
for arg, fmt_spec in p['arg_formats'].items():
|
||||
func = getattr(fmt, fmt_spec['func'])
|
||||
@@ -65,11 +41,11 @@ def main():
|
||||
|
||||
runner = CmdRunner(module, ['echo', '--'], arg_formats=arg_formats)
|
||||
|
||||
info = None
|
||||
with runner.context(p['arg_order']) as ctx:
|
||||
with runner.context(p['arg_order'], check_mode_skip=p['check_mode_skip']) as ctx:
|
||||
result = ctx.run(**p['arg_values'])
|
||||
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)
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
arg_formats: "{{ item.arg_formats|default(omit) }}"
|
||||
arg_order: "{{ item.arg_order }}"
|
||||
arg_values: "{{ item.arg_values|default(omit) }}"
|
||||
check_mode_skip: "{{ item.check_mode_skip|default(omit) }}"
|
||||
aa: "{{ item.aa|default(omit) }}"
|
||||
register: test_result
|
||||
check_mode: "{{ item.check_mode|default(omit) }}"
|
||||
ignore_errors: "{{ item.expect_error|default(omit) }}"
|
||||
|
||||
- name: check results [{{ item.name }}]
|
||||
|
||||
@@ -82,3 +82,41 @@ cmd_echo_tests:
|
||||
- >-
|
||||
"MissingArgumentValue: Cannot find value for parameter bb"
|
||||
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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies:
|
||||
- setup_pkg_mgr
|
||||
- setup_remote_tmp_dir
|
||||
- setup_remote_constraints
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
pip:
|
||||
name: pycdlib
|
||||
# state: latest
|
||||
extra_args: "-c {{ remote_constraints }}"
|
||||
register: install_pycdlib
|
||||
- debug: var=install_pycdlib
|
||||
|
||||
|
||||
1
tests/integration/targets/keyring/aliases
Normal file
1
tests/integration/targets/keyring/aliases
Normal file
@@ -0,0 +1 @@
|
||||
unsupported
|
||||
95
tests/integration/targets/keyring/tasks/main.yml
Normal file
95
tests/integration/targets/keyring/tasks/main.yml
Normal 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
|
||||
3
tests/integration/targets/keyring/vars/main.yml
Normal file
3
tests/integration/targets/keyring/vars/main.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
keyring_password: Password123
|
||||
user_password: Test123
|
||||
@@ -19,6 +19,44 @@
|
||||
- "~/.gnupg"
|
||||
- "~/.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:
|
||||
# gpg2 --batch --gen-key input # See templates/input
|
||||
# gpg2 --list-secret-keys --keyid-format LONG
|
||||
@@ -151,3 +189,163 @@
|
||||
assert:
|
||||
that:
|
||||
- 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
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
---
|
||||
# Initialise environment
|
||||
|
||||
- name: Register sudoers.d directory
|
||||
- name: Register variables
|
||||
set_fact:
|
||||
sudoers_path: /etc/sudoers.d
|
||||
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
|
||||
ansible.builtin.file:
|
||||
path: "{{ sudoers_path }}"
|
||||
@@ -29,6 +34,11 @@
|
||||
commands: /usr/local/bin/command
|
||||
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
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ sudoers_path }}/my-sudo-rule-1"
|
||||
@@ -130,8 +140,73 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
@@ -139,6 +214,7 @@
|
||||
- rule_1_again is not changed
|
||||
- rule_5 is changed
|
||||
- revoke_rule_1 is changed
|
||||
- revoke_non_existing_rule is not changed
|
||||
|
||||
- name: Check contents
|
||||
ansible.builtin.assert:
|
||||
@@ -150,7 +226,22 @@
|
||||
- "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'"
|
||||
|
||||
- name: Check stats
|
||||
- name: Check revocation stat
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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'
|
||||
|
||||
@@ -207,50 +207,53 @@
|
||||
that:
|
||||
- remove_repo is changed
|
||||
|
||||
- name: add new repository via url to .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
|
||||
# For now, the URL does not work for 15.4
|
||||
- when: ansible_distribution_version is version('15.4', '<')
|
||||
block:
|
||||
- name: add new repository via url to .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
|
||||
command: zypper lr systemsmanagement_Uyuni_Stable
|
||||
register: get_repository_details_from_zypper
|
||||
- name: get repository details from zypper
|
||||
command: zypper lr systemsmanagement_Uyuni_Stable
|
||||
register: get_repository_details_from_zypper
|
||||
|
||||
- name: verify adding via .repo file was successful
|
||||
assert:
|
||||
that:
|
||||
- "added_by_repo_file is changed"
|
||||
- "get_repository_details_from_zypper.rc == 0"
|
||||
- "'/systemsmanagement:/Uyuni:/Stable/' in get_repository_details_from_zypper.stdout"
|
||||
- name: verify adding via .repo file was successful
|
||||
assert:
|
||||
that:
|
||||
- "added_by_repo_file is changed"
|
||||
- "get_repository_details_from_zypper.rc == 0"
|
||||
- "'/systemsmanagement:/Uyuni:/Stable/' in get_repository_details_from_zypper.stdout"
|
||||
|
||||
- name: add same repository via url to .repo file again to verify idempotency
|
||||
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_again_by_repo_file
|
||||
- name: add same repository via url to .repo file again to verify idempotency
|
||||
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_again_by_repo_file
|
||||
|
||||
- name: verify nothing was changed adding a repo with the same .repo file
|
||||
assert:
|
||||
that:
|
||||
- added_again_by_repo_file is not changed
|
||||
- name: verify nothing was changed adding a repo with the same .repo file
|
||||
assert:
|
||||
that:
|
||||
- added_again_by_repo_file is not changed
|
||||
|
||||
- name: remove repository via url to .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: absent
|
||||
register: removed_by_repo_file
|
||||
- name: remove repository via url to .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: absent
|
||||
register: removed_by_repo_file
|
||||
|
||||
- name: get list of files in /etc/zypp/repos.d/
|
||||
command: ls /etc/zypp/repos.d/
|
||||
changed_when: false
|
||||
register: etc_zypp_reposd
|
||||
- name: get list of files in /etc/zypp/repos.d/
|
||||
command: ls /etc/zypp/repos.d/
|
||||
changed_when: false
|
||||
register: etc_zypp_reposd
|
||||
|
||||
- name: verify removal via .repo file was successful, including cleanup of local .repo file in /etc/zypp/repos.d/
|
||||
assert:
|
||||
that:
|
||||
- "removed_by_repo_file"
|
||||
- "'/systemsmanagement:/Uyuni:/Stable/' not in etc_zypp_reposd.stdout"
|
||||
- name: verify removal via .repo file was successful, including cleanup of local .repo file in /etc/zypp/repos.d/
|
||||
assert:
|
||||
that:
|
||||
- "removed_by_repo_file"
|
||||
- "'/systemsmanagement:/Uyuni:/Stable/' not in etc_zypp_reposd.stdout"
|
||||
|
||||
- name: Copy test .repo file
|
||||
copy:
|
||||
|
||||
@@ -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):
|
||||
@@ -537,21 +630,11 @@ def get_vm_snapshots(node, properties, vmtype, vmid, name):
|
||||
}]
|
||||
|
||||
|
||||
def get_vm_status(properties, node, vmtype, vmid, name):
|
||||
return True
|
||||
|
||||
|
||||
def get_option(option):
|
||||
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 get_option(opts):
|
||||
def fn(option):
|
||||
default = opts.get('default', False)
|
||||
return opts.get(option, default)
|
||||
return fn
|
||||
|
||||
|
||||
def test_populate(inventory, mocker):
|
||||
@@ -563,12 +646,19 @@ def test_populate(inventory, mocker):
|
||||
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': True
|
||||
}
|
||||
|
||||
# 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_status = mocker.MagicMock(side_effect=get_vm_status)
|
||||
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._populate()
|
||||
|
||||
@@ -610,3 +700,45 @@ def test_populate(inventory, mocker):
|
||||
|
||||
# check that offline node is in inventory
|
||||
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
|
||||
|
||||
@@ -246,7 +246,7 @@ TC_RUNNER_IDS = sorted(TC_RUNNER.keys())
|
||||
@pytest.mark.parametrize('runner_input, cmd_execution, expected',
|
||||
(TC_RUNNER[tc] for tc in 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 = {}
|
||||
params = {}
|
||||
arg_formats = {}
|
||||
@@ -304,3 +304,66 @@ def test_runner(runner_input, cmd_execution, expected):
|
||||
with runner.context(**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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@@ -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']
|
||||
@@ -98,6 +98,12 @@ TESTCASE_CONNECTION = [
|
||||
'state': 'absent',
|
||||
'_ansible_check_mode': True,
|
||||
},
|
||||
{
|
||||
'type': 'vpn',
|
||||
'conn_name': 'non_existent_nw_device',
|
||||
'state': 'absent',
|
||||
'_ansible_check_mode': True,
|
||||
},
|
||||
]
|
||||
|
||||
TESTCASE_GENERIC = [
|
||||
@@ -449,6 +455,17 @@ ipv6.ignore-auto-dns: 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 = [
|
||||
{
|
||||
'type': 'bond',
|
||||
@@ -1177,6 +1194,69 @@ wireguard.ip4-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,
|
||||
connection_exists=False,
|
||||
@@ -1547,6 +1627,20 @@ def mocked_wireguard_connection_unchanged(mocker):
|
||||
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'])
|
||||
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']
|
||||
|
||||
|
||||
@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'])
|
||||
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)
|
||||
assert not results.get('failed')
|
||||
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']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: Jiri Hnidek (jhnidek@redhat.com)
|
||||
#
|
||||
# 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', '')
|
||||
),
|
||||
(
|
||||
['/testbin/subscription-manager', 'unsubscribe', '--all'],
|
||||
['/testbin/subscription-manager', 'remove', '--all'],
|
||||
{'check_rc': True},
|
||||
(0, '', '')
|
||||
),
|
||||
@@ -755,7 +756,7 @@ Entitlement Type: Physical
|
||||
(
|
||||
[
|
||||
'/testbin/subscription-manager',
|
||||
'unsubscribe',
|
||||
'remove',
|
||||
'--serial=7807912223970164816',
|
||||
],
|
||||
{'check_rc': True},
|
||||
|
||||
102
tests/unit/plugins/modules/system/test_gconftool2_info.py
Normal file
102
tests/unit/plugins/modules/system/test_gconftool2_info.py
Normal 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
|
||||
@@ -29,10 +29,11 @@ dnsimple >= 2 ; python_version >= '3.6'
|
||||
dataclasses ; python_version == '3.6'
|
||||
|
||||
# requirement for the opentelemetry callback plugin
|
||||
# WARNING: these libraries depend on grpcio, which takes 7 minutes (!) to build in CI on Python 3.10
|
||||
opentelemetry-api ; 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'
|
||||
# WARNING: these libraries rely on Protobuf for Python, which regularly stops installing.
|
||||
# That's why they are disabled for now.
|
||||
# opentelemetry-api ; 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
|
||||
elastic-apm ; python_version >= '3.6'
|
||||
|
||||
@@ -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+)
|
||||
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.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-credssp >= 0.1.0 # message encryption support
|
||||
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 < 4.0.0 ; python_version >= '2.7' and 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
|
||||
astroid == 2.2.5
|
||||
|
||||
Reference in New Issue
Block a user