mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-01 02:43:16 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba3903e6e0 | ||
|
|
4b6b00d249 | ||
|
|
0a0b0cb42d | ||
|
|
d0b39271b3 | ||
|
|
f07cb76b09 | ||
|
|
09031fc9e6 | ||
|
|
4481d0a4a9 | ||
|
|
5861388f11 | ||
|
|
c581daa48a | ||
|
|
75e2de3581 | ||
|
|
6c7bee1225 | ||
|
|
eafcdfbceb | ||
|
|
82a764446b | ||
|
|
a0032f3513 |
8
.github/BOTMETA.yml
vendored
8
.github/BOTMETA.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, 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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}"
|
||||
@@ -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'
|
||||
|
||||
@@ -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']
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user