Compare commits

..

14 Commits
5.1.1 ... 5.2.0

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

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

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

* Replace use of subprocess.Popen with module.run_command

* Switch out apt for package

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

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

* Attempt to install sudo on FreeBSD too

* Try just installing sudo for non-darwin machines

* Don't validate file ownership

* Attempt to install sudo on all platforms

* Revert "Attempt to install sudo on all platforms"

This reverts commit b9562a8916.

* Remove file permissions changes from this PR

* Add changelog fragment for 4794 sudoers validation

* Add option to control when sudoers validation is used

* Update changelog fragment

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

* Add version_added to validation property

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

* Also validate failed sudoers validation error message

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

* Make visudo not executable instead of trying to delete it

* Update edge case validation

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

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

* Update plugins/modules/system/sudoers.py

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

* Remove trailing extra empty line to appease sanity checker

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

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

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

* Added Changelog for PR4839

(cherry picked from commit 5e57d2af0a)

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

(cherry picked from commit 652392be27)

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

* typo fixes

(cherry picked from commit 44e21dd407)

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

* Move other licenses to licenses/.

* Revert "Move other licenses to licenses/."

This reverts commit eab4209889.

(cherry picked from commit dcdfc9c413)

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

* fix argument required and BOTMETA

* little fix in commentary/doc

* test with link for ansible-doc check

* remove unwanted file

* fix entry missing in  meta/runtime.yml

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

* a=add missing  del os.environ

* fix whitespace

* test_scaleway_compute_private_network : fix test

* test_scaleway_compute_private_network : fix pep8

* scaleway_compute_private_network

add . in description

* scaleway_compute_private_network: fix var name

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

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

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

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

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

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

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

* change xfconf to use the callable form

* add changelog fragment

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

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

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

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

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

Includes integrations tests.

Resolves #4766

* Apply suggestions from code review

(cherry picked from commit 006f3bfa89)

Co-authored-by: grembo <freebsd@grem.de>
2022-06-15 10:57:52 +02:00
Felix Fontein
a0032f3513 Next expected release is 5.2.0. 2022-06-14 18:15:59 +02:00
26 changed files with 1519 additions and 82 deletions

8
.github/BOTMETA.yml vendored
View File

@@ -440,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:
@@ -1068,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:
@@ -1290,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

View File

@@ -6,6 +6,46 @@ 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
======

48
PSF-license.txt Normal file
View File

@@ -0,0 +1,48 @@
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

View File

@@ -872,3 +872,44 @@ releases:
- 4836-alternatives.yml
- 5.1.1.yml
release_date: '2022-06-14'
5.2.0:
changes:
bugfixes:
- Include ``PSF-license.txt`` file for ``plugins/module_utils/_mount.py``.
- redfish_command - fix the check if a virtual media is unmounted to just check
for ``instered= false`` caused by Supermicro hardware that does not clear
the ``ImageName`` (https://github.com/ansible-collections/community.general/pull/4839).
- redfish_command - the Supermicro Redfish implementation only supports the
``image_url`` parameter in the underlying API calls to ``VirtualMediaInsert``
and ``VirtualMediaEject``. Any values set (or the defaults) for ``write_protected``
or ``inserted`` will be ignored (https://github.com/ansible-collections/community.general/pull/4839).
- 'sudoers - fix incorrect handling of ``state: absent`` (https://github.com/ansible-collections/community.general/issues/4852).'
minor_changes:
- cmd_runner module utils - add ``__call__`` method to invoke context (https://github.com/ansible-collections/community.general/pull/4791).
- passwordstore lookup plugin - allow using alternative password managers by
detecting wrapper scripts, allow explicit configuration of pass and gopass
backends (https://github.com/ansible-collections/community.general/issues/4766).
- sudoers - will attempt to validate the proposed sudoers rule using visudo
if available, optionally skipped, or required (https://github.com/ansible-collections/community.general/pull/4794,
https://github.com/ansible-collections/community.general/issues/4745).
release_summary: Regular bugfix and feature release.
fragments:
- 4780-passwordstore-wrapper-compat.yml
- 4791-cmd-runner-callable.yaml
- 4794-sudoers-validation.yml
- 4839-fix-VirtualMediaInsert-Supermicro.yml
- 4852-sudoers-state-absent.yml
- 5.2.0.yml
- psf-license.yml
modules:
- description: Set or delete a passphrase using the Operating System's native
keyring
name: keyring
namespace: system
- description: Get a passphrase using the Operating System's native keyring
name: keyring_info
namespace: system
- description: Scaleway compute - private network management
name: scaleway_compute_private_network
namespace: cloud.scaleway
release_date: '2022-06-21'

View File

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

View File

@@ -608,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:
@@ -1357,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:

View File

@@ -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 = []

View File

@@ -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

View File

@@ -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, check_mode_skip=False, check_mode_return=None, **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:
@@ -216,6 +216,9 @@ class CmdRunner(object):
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, check_mode_skip, check_mode_return, **kwargs):

View File

@@ -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:

View File

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

View File

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

View File

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

View File

@@ -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 = '''
@@ -118,6 +127,8 @@ 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']
@@ -128,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:
@@ -167,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
@@ -206,6 +237,10 @@ def main():
'choices': ['present', 'absent'],
},
'user': {},
'validation': {
'default': 'detect',
'choices': ['absent', 'detect', 'required']
},
}
module = AnsibleModule(

View File

@@ -205,11 +205,11 @@ class XFConfProperty(StateModuleHelper):
return result
def _get(self):
with self.runner.context('channel property', output_process=self.process_command_output) as ctx:
with self.runner('channel property', output_process=self.process_command_output) as ctx:
return ctx.run()
def state_absent(self):
with self.runner.context('channel property reset', check_mode_skip=True) as ctx:
with self.runner('channel property reset', check_mode_skip=True) as ctx:
ctx.run(reset=True)
self.vars.value = None
@@ -235,7 +235,7 @@ class XFConfProperty(StateModuleHelper):
isinstance(self.vars.previous_value, list) or \
values_len > 1
with self.runner.context('channel property create force_array values_and_types', check_mode_skip=True) as ctx:
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:

View File

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

View File

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

View File

@@ -0,0 +1 @@
unsupported

View File

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

View File

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

View File

@@ -19,6 +19,44 @@
- "~/.gnupg"
- "~/.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

View File

@@ -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 }}"
@@ -135,6 +140,64 @@
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
@@ -151,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:
@@ -162,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'

View File

@@ -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)

View File

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

View File

@@ -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'

View File

@@ -49,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