summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:05:57 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:05:57 +0000
commit0dcbb2c58231264c2f0a0374733b5e9cf8747e1f (patch)
tree7f133117f9ebecefdc96e42e01ee7557247d5d8a /ansible_collections/community/general/plugins/modules
parentAdding debian version 9.4.0+dfsg-1. (diff)
downloadansible-0dcbb2c58231264c2f0a0374733b5e9cf8747e1f.tar.xz
ansible-0dcbb2c58231264c2f0a0374733b5e9cf8747e1f.zip
Merging upstream version 9.5.1+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/general/plugins/modules')
-rw-r--r--ansible_collections/community/general/plugins/modules/aix_filesystem.py2
-rw-r--r--ansible_collections/community/general/plugins/modules/apt_rpm.py33
-rw-r--r--ansible_collections/community/general/plugins/modules/cobbler_sync.py9
-rw-r--r--ansible_collections/community/general/plugins/modules/cobbler_system.py9
-rw-r--r--ansible_collections/community/general/plugins/modules/filesystem.py50
-rw-r--r--ansible_collections/community/general/plugins/modules/flatpak.py83
-rw-r--r--ansible_collections/community/general/plugins/modules/github_key.py10
-rw-r--r--ansible_collections/community/general/plugins/modules/gitlab_issue.py8
-rw-r--r--ansible_collections/community/general/plugins/modules/gitlab_label.py12
-rw-r--r--ansible_collections/community/general/plugins/modules/gitlab_milestone.py12
-rw-r--r--ansible_collections/community/general/plugins/modules/haproxy.py2
-rw-r--r--ansible_collections/community/general/plugins/modules/imc_rest.py13
-rw-r--r--ansible_collections/community/general/plugins/modules/ini_file.py133
-rw-r--r--ansible_collections/community/general/plugins/modules/java_cert.py37
-rw-r--r--ansible_collections/community/general/plugins/modules/keycloak_client.py16
-rw-r--r--ansible_collections/community/general/plugins/modules/keycloak_client_rolescope.py280
-rw-r--r--ansible_collections/community/general/plugins/modules/keycloak_clientscope.py9
-rw-r--r--ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py9
-rw-r--r--ansible_collections/community/general/plugins/modules/keycloak_realm.py29
-rw-r--r--ansible_collections/community/general/plugins/modules/lxd_container.py41
-rw-r--r--ansible_collections/community/general/plugins/modules/nmcli.py47
-rw-r--r--ansible_collections/community/general/plugins/modules/osx_defaults.py18
-rw-r--r--ansible_collections/community/general/plugins/modules/pagerduty.py10
-rw-r--r--ansible_collections/community/general/plugins/modules/pagerduty_change.py8
-rw-r--r--ansible_collections/community/general/plugins/modules/portage.py11
-rw-r--r--ansible_collections/community/general/plugins/modules/puppet.py10
-rw-r--r--ansible_collections/community/general/plugins/modules/redfish_command.py17
-rw-r--r--ansible_collections/community/general/plugins/modules/riak.py15
-rw-r--r--ansible_collections/community/general/plugins/modules/scaleway_compute.py5
-rw-r--r--ansible_collections/community/general/plugins/modules/scaleway_database_backup.py7
-rw-r--r--ansible_collections/community/general/plugins/modules/scaleway_lb.py5
-rw-r--r--ansible_collections/community/general/plugins/modules/ssh_config.py5
-rw-r--r--ansible_collections/community/general/plugins/modules/statusio_maintenance.py12
-rw-r--r--ansible_collections/community/general/plugins/modules/xml.py9
34 files changed, 835 insertions, 141 deletions
diff --git a/ansible_collections/community/general/plugins/modules/aix_filesystem.py b/ansible_collections/community/general/plugins/modules/aix_filesystem.py
index 6abf6317f..4a3775c67 100644
--- a/ansible_collections/community/general/plugins/modules/aix_filesystem.py
+++ b/ansible_collections/community/general/plugins/modules/aix_filesystem.py
@@ -242,7 +242,7 @@ def _validate_vg(module, vg):
if rc != 0:
module.fail_json(msg="Failed executing %s command." % lsvg_cmd)
- rc, current_all_vgs, err = module.run_command([lsvg_cmd, "%s"])
+ rc, current_all_vgs, err = module.run_command([lsvg_cmd])
if rc != 0:
module.fail_json(msg="Failed executing %s command." % lsvg_cmd)
diff --git a/ansible_collections/community/general/plugins/modules/apt_rpm.py b/ansible_collections/community/general/plugins/modules/apt_rpm.py
index de1b57411..03b87e78f 100644
--- a/ansible_collections/community/general/plugins/modules/apt_rpm.py
+++ b/ansible_collections/community/general/plugins/modules/apt_rpm.py
@@ -37,7 +37,17 @@ options:
state:
description:
- Indicates the desired package state.
- choices: [ absent, present, installed, removed ]
+ - Please note that V(present) and V(installed) are equivalent to V(latest) right now.
+ This will change in the future. To simply ensure that a package is installed, without upgrading
+ it, use the V(present_not_latest) state.
+ - The states V(latest) and V(present_not_latest) have been added in community.general 8.6.0.
+ choices:
+ - absent
+ - present
+ - present_not_latest
+ - installed
+ - removed
+ - latest
default: present
type: str
update_cache:
@@ -180,7 +190,7 @@ def check_package_version(module, name):
return False
-def query_package_provides(module, name):
+def query_package_provides(module, name, allow_upgrade=False):
# rpm -q returns 0 if the package is installed,
# 1 if it is not installed
if name.endswith('.rpm'):
@@ -195,10 +205,11 @@ def query_package_provides(module, name):
rc, out, err = module.run_command("%s -q --provides %s" % (RPM_PATH, name))
if rc == 0:
+ if not allow_upgrade:
+ return True
if check_package_version(module, name):
return True
- else:
- return False
+ return False
def update_package_db(module):
@@ -255,14 +266,14 @@ def remove_packages(module, packages):
return (False, "package(s) already absent")
-def install_packages(module, pkgspec):
+def install_packages(module, pkgspec, allow_upgrade=False):
if pkgspec is None:
return (False, "Empty package list")
packages = ""
for package in pkgspec:
- if not query_package_provides(module, package):
+ if not query_package_provides(module, package, allow_upgrade=allow_upgrade):
packages += "'%s' " % package
if len(packages) != 0:
@@ -270,8 +281,8 @@ def install_packages(module, pkgspec):
rc, out, err = module.run_command("%s -y install %s" % (APT_PATH, packages), environ_update={"LANG": "C"})
installed = True
- for packages in pkgspec:
- if not query_package_provides(module, package):
+ for package in pkgspec:
+ if not query_package_provides(module, package, allow_upgrade=False):
installed = False
# apt-rpm always have 0 for exit code if --force is used
@@ -286,7 +297,7 @@ def install_packages(module, pkgspec):
def main():
module = AnsibleModule(
argument_spec=dict(
- state=dict(type='str', default='present', choices=['absent', 'installed', 'present', 'removed']),
+ state=dict(type='str', default='present', choices=['absent', 'installed', 'present', 'removed', 'present_not_latest', 'latest']),
update_cache=dict(type='bool', default=False),
clean=dict(type='bool', default=False),
dist_upgrade=dict(type='bool', default=False),
@@ -320,8 +331,8 @@ def main():
output += out
packages = p['package']
- if p['state'] in ['installed', 'present']:
- (m, out) = install_packages(module, packages)
+ if p['state'] in ['installed', 'present', 'present_not_latest', 'latest']:
+ (m, out) = install_packages(module, packages, allow_upgrade=p['state'] != 'present_not_latest')
modified = modified or m
output += out
diff --git a/ansible_collections/community/general/plugins/modules/cobbler_sync.py b/ansible_collections/community/general/plugins/modules/cobbler_sync.py
index 4ec87c96c..27f57028b 100644
--- a/ansible_collections/community/general/plugins/modules/cobbler_sync.py
+++ b/ansible_collections/community/general/plugins/modules/cobbler_sync.py
@@ -75,13 +75,16 @@ RETURN = r'''
# Default return values
'''
-import datetime
import ssl
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves import xmlrpc_client
from ansible.module_utils.common.text.converters import to_text
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
def main():
module = AnsibleModule(
@@ -110,7 +113,7 @@ def main():
changed=True,
)
- start = datetime.datetime.utcnow()
+ start = now()
ssl_context = None
if not validate_certs:
@@ -142,7 +145,7 @@ def main():
except Exception as e:
module.fail_json(msg="Failed to sync Cobbler. {error}".format(error=to_text(e)))
- elapsed = datetime.datetime.utcnow() - start
+ elapsed = now() - start
module.exit_json(elapsed=elapsed.seconds, **result)
diff --git a/ansible_collections/community/general/plugins/modules/cobbler_system.py b/ansible_collections/community/general/plugins/modules/cobbler_system.py
index cecc02f71..a327ede84 100644
--- a/ansible_collections/community/general/plugins/modules/cobbler_system.py
+++ b/ansible_collections/community/general/plugins/modules/cobbler_system.py
@@ -152,7 +152,6 @@ system:
type: dict
'''
-import datetime
import ssl
from ansible.module_utils.basic import AnsibleModule
@@ -160,6 +159,10 @@ from ansible.module_utils.six import iteritems
from ansible.module_utils.six.moves import xmlrpc_client
from ansible.module_utils.common.text.converters import to_text
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
IFPROPS_MAPPING = dict(
bondingopts='bonding_opts',
bridgeopts='bridge_opts',
@@ -232,7 +235,7 @@ def main():
changed=False,
)
- start = datetime.datetime.utcnow()
+ start = now()
ssl_context = None
if not validate_certs:
@@ -340,7 +343,7 @@ def main():
if module._diff:
result['diff'] = dict(before=system, after=result['system'])
- elapsed = datetime.datetime.utcnow() - start
+ elapsed = now() - start
module.exit_json(elapsed=elapsed.seconds, **result)
diff --git a/ansible_collections/community/general/plugins/modules/filesystem.py b/ansible_collections/community/general/plugins/modules/filesystem.py
index ec361245b..73e8c79c6 100644
--- a/ansible_collections/community/general/plugins/modules/filesystem.py
+++ b/ansible_collections/community/general/plugins/modules/filesystem.py
@@ -40,11 +40,12 @@ options:
default: present
version_added: 1.3.0
fstype:
- choices: [ btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap, ufs ]
+ choices: [ bcachefs, btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap, ufs ]
description:
- Filesystem type to be created. This option is required with
O(state=present) (or if O(state) is omitted).
- ufs support has been added in community.general 3.4.0.
+ - bcachefs support has been added in community.general 8.6.0.
type: str
aliases: [type]
dev:
@@ -67,7 +68,7 @@ options:
resizefs:
description:
- If V(true), if the block device and filesystem size differ, grow the filesystem into the space.
- - Supported for C(btrfs), C(ext2), C(ext3), C(ext4), C(ext4dev), C(f2fs), C(lvm), C(xfs), C(ufs) and C(vfat) filesystems.
+ - Supported for C(bcachefs), C(btrfs), C(ext2), C(ext3), C(ext4), C(ext4dev), C(f2fs), C(lvm), C(xfs), C(ufs) and C(vfat) filesystems.
Attempts to resize other filesystem types will fail.
- XFS Will only grow if mounted. Currently, the module is based on commands
from C(util-linux) package to perform operations, so resizing of XFS is
@@ -86,7 +87,7 @@ options:
- The UUID options specified in O(opts) take precedence over this value.
- See xfs_admin(8) (C(xfs)), tune2fs(8) (C(ext2), C(ext3), C(ext4), C(ext4dev)) for possible values.
- For O(fstype=lvm) the value is ignored, it resets the PV UUID if set.
- - Supported for O(fstype) being one of C(ext2), C(ext3), C(ext4), C(ext4dev), C(lvm), or C(xfs).
+ - Supported for O(fstype) being one of C(bcachefs), C(ext2), C(ext3), C(ext4), C(ext4dev), C(lvm), or C(xfs).
- This is B(not idempotent). Specifying this option will always result in a change.
- Mutually exclusive with O(resizefs).
type: str
@@ -405,6 +406,48 @@ class Reiserfs(Filesystem):
MKFS_FORCE_FLAGS = ['-q']
+class Bcachefs(Filesystem):
+ MKFS = 'mkfs.bcachefs'
+ MKFS_FORCE_FLAGS = ['--force']
+ MKFS_SET_UUID_OPTIONS = ['-U', '--uuid']
+ INFO = 'bcachefs'
+ GROW = 'bcachefs'
+ GROW_MAX_SPACE_FLAGS = ['device', 'resize']
+
+ def get_fs_size(self, dev):
+ """Return size in bytes of filesystem on device (integer)."""
+ dummy, stdout, dummy = self.module.run_command([self.module.get_bin_path(self.INFO),
+ 'show-super', str(dev)], check_rc=True)
+
+ for line in stdout.splitlines():
+ if "Size: " in line:
+ parts = line.split()
+ unit = parts[2]
+
+ base = None
+ exp = None
+
+ units_2 = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
+ units_10 = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
+
+ try:
+ exp = units_2.index(unit)
+ base = 1024
+ except ValueError:
+ exp = units_10.index(unit)
+ base = 1000
+
+ if exp == 0:
+ value = int(parts[1])
+ else:
+ value = float(parts[1])
+
+ if base is not None and exp is not None:
+ return int(value * pow(base, exp))
+
+ raise ValueError(repr(stdout))
+
+
class Btrfs(Filesystem):
MKFS = 'mkfs.btrfs'
INFO = 'btrfs'
@@ -567,6 +610,7 @@ class UFS(Filesystem):
FILESYSTEMS = {
+ 'bcachefs': Bcachefs,
'ext2': Ext2,
'ext3': Ext3,
'ext4': Ext4,
diff --git a/ansible_collections/community/general/plugins/modules/flatpak.py b/ansible_collections/community/general/plugins/modules/flatpak.py
index 80dbabdfa..15e404d45 100644
--- a/ansible_collections/community/general/plugins/modules/flatpak.py
+++ b/ansible_collections/community/general/plugins/modules/flatpak.py
@@ -26,7 +26,9 @@ extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
- support: full
+ support: partial
+ details:
+ - If O(state=latest), the module will always return C(changed=true).
diff_mode:
support: none
options:
@@ -53,12 +55,12 @@ options:
- Both C(https://) and C(http://) URLs are supported.
- When supplying a reverse DNS name, you can use the O(remote) option to specify on what remote
to look for the flatpak. An example for a reverse DNS name is C(org.gnome.gedit).
- - When used with O(state=absent), it is recommended to specify the name in the reverse DNS
- format.
- - When supplying a URL with O(state=absent), the module will try to match the
- installed flatpak based on the name of the flatpakref to remove it. However, there is no
- guarantee that the names of the flatpakref file and the reverse DNS name of the installed
- flatpak do match.
+ - When used with O(state=absent) or O(state=latest), it is recommended to specify the name in
+ the reverse DNS format.
+ - When supplying a URL with O(state=absent) or O(state=latest), the module will try to match the
+ installed flatpak based on the name of the flatpakref to remove or update it. However, there
+ is no guarantee that the names of the flatpakref file and the reverse DNS name of the
+ installed flatpak do match.
type: list
elements: str
required: true
@@ -82,7 +84,8 @@ options:
state:
description:
- Indicates the desired package state.
- choices: [ absent, present ]
+ - The value V(latest) is supported since community.general 8.6.0.
+ choices: [ absent, present, latest ]
type: str
default: present
'''
@@ -118,6 +121,37 @@ EXAMPLES = r'''
- org.inkscape.Inkscape
- org.mozilla.firefox
+- name: Update the spotify flatpak
+ community.general.flatpak:
+ name: https://s3.amazonaws.com/alexlarsson/spotify-repo/spotify.flatpakref
+ state: latest
+
+- name: Update the gedit flatpak package without dependencies (not recommended)
+ community.general.flatpak:
+ name: https://git.gnome.org/browse/gnome-apps-nightly/plain/gedit.flatpakref
+ state: latest
+ no_dependencies: true
+
+- name: Update the gedit package from flathub for current user
+ community.general.flatpak:
+ name: org.gnome.gedit
+ state: latest
+ method: user
+
+- name: Update the Gnome Calendar flatpak from the gnome remote system-wide
+ community.general.flatpak:
+ name: org.gnome.Calendar
+ state: latest
+ remote: gnome
+
+- name: Update multiple packages
+ community.general.flatpak:
+ name:
+ - org.gimp.GIMP
+ - org.inkscape.Inkscape
+ - org.mozilla.firefox
+ state: latest
+
- name: Remove the gedit flatpak
community.general.flatpak:
name: org.gnome.gedit
@@ -195,6 +229,28 @@ def install_flat(module, binary, remote, names, method, no_dependencies):
result['changed'] = True
+def update_flat(module, binary, names, method, no_dependencies):
+ """Update existing flatpaks."""
+ global result # pylint: disable=global-variable-not-assigned
+ installed_flat_names = [
+ _match_installed_flat_name(module, binary, name, method)
+ for name in names
+ ]
+ command = [binary, "update", "--{0}".format(method)]
+ flatpak_version = _flatpak_version(module, binary)
+ if LooseVersion(flatpak_version) < LooseVersion('1.1.3'):
+ command += ["-y"]
+ else:
+ command += ["--noninteractive"]
+ if no_dependencies:
+ command += ["--no-deps"]
+ command += installed_flat_names
+ stdout = _flatpak_command(module, module.check_mode, command)
+ result["changed"] = (
+ True if module.check_mode else stdout.find("Nothing to do.") == -1
+ )
+
+
def uninstall_flat(module, binary, names, method):
"""Remove existing flatpaks."""
global result # pylint: disable=global-variable-not-assigned
@@ -313,7 +369,7 @@ def main():
method=dict(type='str', default='system',
choices=['user', 'system']),
state=dict(type='str', default='present',
- choices=['absent', 'present']),
+ choices=['absent', 'present', 'latest']),
no_dependencies=dict(type='bool', default=False),
executable=dict(type='path', default='flatpak')
),
@@ -338,10 +394,13 @@ def main():
module.fail_json(msg="Executable '%s' was not found on the system." % executable, **result)
installed, not_installed = flatpak_exists(module, binary, name, method)
- if state == 'present' and not_installed:
- install_flat(module, binary, remote, not_installed, method, no_dependencies)
- elif state == 'absent' and installed:
+ if state == 'absent' and installed:
uninstall_flat(module, binary, installed, method)
+ else:
+ if state == 'latest' and installed:
+ update_flat(module, binary, installed, method, no_dependencies)
+ if state in ('present', 'latest') and not_installed:
+ install_flat(module, binary, remote, not_installed, method, no_dependencies)
module.exit_json(**result)
diff --git a/ansible_collections/community/general/plugins/modules/github_key.py b/ansible_collections/community/general/plugins/modules/github_key.py
index fa3a0a01f..a74ead984 100644
--- a/ansible_collections/community/general/plugins/modules/github_key.py
+++ b/ansible_collections/community/general/plugins/modules/github_key.py
@@ -91,12 +91,17 @@ EXAMPLES = '''
pubkey: "{{ lookup('ansible.builtin.file', '/home/foo/.ssh/id_rsa.pub') }}"
'''
+import datetime
import json
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
API_BASE = 'https://api.github.com'
@@ -151,14 +156,13 @@ def get_all_keys(session):
def create_key(session, name, pubkey, check_mode):
if check_mode:
- from datetime import datetime
- now = datetime.utcnow()
+ now_t = now()
return {
'id': 0,
'key': pubkey,
'title': name,
'url': 'http://example.com/CHECK_MODE_GITHUB_KEY',
- 'created_at': datetime.strftime(now, '%Y-%m-%dT%H:%M:%SZ'),
+ 'created_at': datetime.strftime(now_t, '%Y-%m-%dT%H:%M:%SZ'),
'read_only': False,
'verified': False
}
diff --git a/ansible_collections/community/general/plugins/modules/gitlab_issue.py b/ansible_collections/community/general/plugins/modules/gitlab_issue.py
index 6d95bf6cf..3277c4f1a 100644
--- a/ansible_collections/community/general/plugins/modules/gitlab_issue.py
+++ b/ansible_collections/community/general/plugins/modules/gitlab_issue.py
@@ -143,7 +143,6 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
from ansible.module_utils.common.text.converters import to_native, to_text
-from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
auth_argument_spec, gitlab_authentication, gitlab, find_project, find_group
)
@@ -330,13 +329,8 @@ def main():
state_filter = module.params['state_filter']
title = module.params['title']
- gitlab_version = gitlab.__version__
- if LooseVersion(gitlab_version) < LooseVersion('2.3.0'):
- module.fail_json(msg="community.general.gitlab_issue requires python-gitlab Python module >= 2.3.0 (installed version: [%s])."
- " Please upgrade python-gitlab to version 2.3.0 or above." % gitlab_version)
-
# check prerequisites and connect to gitlab server
- gitlab_instance = gitlab_authentication(module)
+ gitlab_instance = gitlab_authentication(module, min_version='2.3.0')
this_project = find_project(gitlab_instance, project)
if this_project is None:
diff --git a/ansible_collections/community/general/plugins/modules/gitlab_label.py b/ansible_collections/community/general/plugins/modules/gitlab_label.py
index f2c8393f2..635033ab6 100644
--- a/ansible_collections/community/general/plugins/modules/gitlab_label.py
+++ b/ansible_collections/community/general/plugins/modules/gitlab_label.py
@@ -222,9 +222,8 @@ labels_obj:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
-from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
- auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
+ auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project
)
@@ -450,14 +449,7 @@ def main():
label_list = module.params['labels']
state = module.params['state']
- gitlab_version = gitlab.__version__
- _min_gitlab = '3.2.0'
- if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
- module.fail_json(msg="community.general.gitlab_label requires python-gitlab Python module >= %s "
- "(installed version: [%s]). Please upgrade "
- "python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
-
- gitlab_instance = gitlab_authentication(module)
+ gitlab_instance = gitlab_authentication(module, min_version='3.2.0')
# find_project can return None, but the other must exist
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
diff --git a/ansible_collections/community/general/plugins/modules/gitlab_milestone.py b/ansible_collections/community/general/plugins/modules/gitlab_milestone.py
index 0a616ea47..4b8b933cc 100644
--- a/ansible_collections/community/general/plugins/modules/gitlab_milestone.py
+++ b/ansible_collections/community/general/plugins/modules/gitlab_milestone.py
@@ -206,9 +206,8 @@ milestones_obj:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
-from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
- auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
+ auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project
)
from datetime import datetime
@@ -452,14 +451,7 @@ def main():
milestone_list = module.params['milestones']
state = module.params['state']
- gitlab_version = gitlab.__version__
- _min_gitlab = '3.2.0'
- if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
- module.fail_json(msg="community.general.gitlab_milestone requires python-gitlab Python module >= %s "
- "(installed version: [%s]). Please upgrade "
- "python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
-
- gitlab_instance = gitlab_authentication(module)
+ gitlab_instance = gitlab_authentication(module, min_version='3.2.0')
# find_project can return None, but the other must exist
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
diff --git a/ansible_collections/community/general/plugins/modules/haproxy.py b/ansible_collections/community/general/plugins/modules/haproxy.py
index 05f52d55c..cbaa43833 100644
--- a/ansible_collections/community/general/plugins/modules/haproxy.py
+++ b/ansible_collections/community/general/plugins/modules/haproxy.py
@@ -343,7 +343,7 @@ class HAProxy(object):
if state is not None:
self.execute(Template(cmd).substitute(pxname=backend, svname=svname))
- if self.wait:
+ if self.wait and not (wait_for_status == "DRAIN" and state == "DOWN"):
self.wait_until_status(backend, svname, wait_for_status)
def get_state_for(self, pxname, svname):
diff --git a/ansible_collections/community/general/plugins/modules/imc_rest.py b/ansible_collections/community/general/plugins/modules/imc_rest.py
index 113d341e8..7f5a5e081 100644
--- a/ansible_collections/community/general/plugins/modules/imc_rest.py
+++ b/ansible_collections/community/general/plugins/modules/imc_rest.py
@@ -268,7 +268,6 @@ output:
errorDescr="XML PARSING ERROR: Element 'computeRackUnit', attribute 'admin_Power': The attribute 'admin_Power' is not allowed.\n"/>
'''
-import datetime
import os
import traceback
@@ -292,6 +291,10 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.six.moves import zip_longest
from ansible.module_utils.urls import fetch_url
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
def imc_response(module, rawoutput, rawinput=''):
''' Handle IMC returned data '''
@@ -375,14 +378,14 @@ def main():
else:
module.fail_json(msg='Cannot find/access path:\n%s' % path)
- start = datetime.datetime.utcnow()
+ start = now()
# Perform login first
url = '%s://%s/nuova' % (protocol, hostname)
data = '<aaaLogin inName="%s" inPassword="%s"/>' % (username, password)
resp, auth = fetch_url(module, url, data=data, method='POST', timeout=timeout)
if resp is None or auth['status'] != 200:
- result['elapsed'] = (datetime.datetime.utcnow() - start).seconds
+ result['elapsed'] = (now() - start).seconds
module.fail_json(msg='Task failed with error %(status)s: %(msg)s' % auth, **result)
result.update(imc_response(module, resp.read()))
@@ -415,7 +418,7 @@ def main():
# Perform actual request
resp, info = fetch_url(module, url, data=data, method='POST', timeout=timeout)
if resp is None or info['status'] != 200:
- result['elapsed'] = (datetime.datetime.utcnow() - start).seconds
+ result['elapsed'] = (now() - start).seconds
module.fail_json(msg='Task failed with error %(status)s: %(msg)s' % info, **result)
# Merge results with previous results
@@ -431,7 +434,7 @@ def main():
result['changed'] = ('modified' in results)
# Report success
- result['elapsed'] = (datetime.datetime.utcnow() - start).seconds
+ result['elapsed'] = (now() - start).seconds
module.exit_json(**result)
finally:
logout(module, url, cookie, timeout)
diff --git a/ansible_collections/community/general/plugins/modules/ini_file.py b/ansible_collections/community/general/plugins/modules/ini_file.py
index ec71a9473..affee2a4f 100644
--- a/ansible_collections/community/general/plugins/modules/ini_file.py
+++ b/ansible_collections/community/general/plugins/modules/ini_file.py
@@ -44,6 +44,30 @@ options:
- If being omitted, the O(option) will be placed before the first O(section).
- Omitting O(section) is also required if the config format does not support sections.
type: str
+ section_has_values:
+ type: list
+ elements: dict
+ required: false
+ suboptions:
+ option:
+ type: str
+ description: Matching O(section) must contain this option.
+ required: true
+ value:
+ type: str
+ description: Matching O(section_has_values[].option) must have this specific value.
+ values:
+ description:
+ - The string value to be associated with an O(section_has_values[].option).
+ - Mutually exclusive with O(section_has_values[].value).
+ - O(section_has_values[].value=v) is equivalent to O(section_has_values[].values=[v]).
+ type: list
+ elements: str
+ description:
+ - Among possibly multiple sections of the same name, select the first one that contains matching options and values.
+ - With O(state=present), if a suitable section is not found, a new section will be added, including the required options.
+ - With O(state=absent), at most one O(section) is removed if it contains the values.
+ version_added: 8.6.0
option:
description:
- If set (required for changing a O(value)), this is the name of the option.
@@ -182,6 +206,57 @@ EXAMPLES = r'''
option: beverage
value: lemon juice
state: present
+
+- name: Remove the peer configuration for 10.128.0.11/32
+ community.general.ini_file:
+ path: /etc/wireguard/wg0.conf
+ section: Peer
+ section_has_values:
+ - option: AllowedIps
+ value: 10.128.0.11/32
+ mode: '0600'
+ state: absent
+
+- name: Add "beverage=lemon juice" outside a section in specified file
+ community.general.ini_file:
+ path: /etc/conf
+ option: beverage
+ value: lemon juice
+ state: present
+
+- name: Update the public key for peer 10.128.0.12/32
+ community.general.ini_file:
+ path: /etc/wireguard/wg0.conf
+ section: Peer
+ section_has_values:
+ - option: AllowedIps
+ value: 10.128.0.12/32
+ option: PublicKey
+ value: xxxxxxxxxxxxxxxxxxxx
+ mode: '0600'
+ state: present
+
+- name: Remove the peer configuration for 10.128.0.11/32
+ community.general.ini_file:
+ path: /etc/wireguard/wg0.conf
+ section: Peer
+ section_has_values:
+ - option: AllowedIps
+ value: 10.4.0.11/32
+ mode: '0600'
+ state: absent
+
+- name: Update the public key for peer 10.128.0.12/32
+ community.general.ini_file:
+ path: /etc/wireguard/wg0.conf
+ section: Peer
+ section_has_values:
+ - option: AllowedIps
+ value: 10.4.0.12/32
+ option: PublicKey
+ value: xxxxxxxxxxxxxxxxxxxx
+ mode: '0600'
+ state: present
'''
import io
@@ -222,7 +297,19 @@ def update_section_line(option, changed, section_lines, index, changed_lines, ig
return (changed, msg)
-def do_ini(module, filename, section=None, option=None, values=None,
+def check_section_has_values(section_has_values, section_lines):
+ if section_has_values is not None:
+ for condition in section_has_values:
+ for line in section_lines:
+ match = match_opt(condition["option"], line)
+ if match and (len(condition["values"]) == 0 or match.group(7) in condition["values"]):
+ break
+ else:
+ return False
+ return True
+
+
+def do_ini(module, filename, section=None, section_has_values=None, option=None, values=None,
state='present', exclusive=True, backup=False, no_extra_spaces=False,
ignore_spaces=False, create=True, allow_no_value=False, modify_inactive_option=True, follow=False):
@@ -307,14 +394,22 @@ def do_ini(module, filename, section=None, option=None, values=None,
section_pattern = re.compile(to_text(r'^\[\s*%s\s*]' % re.escape(section.strip())))
for index, line in enumerate(ini_lines):
+ # end of section:
+ if within_section and line.startswith(u'['):
+ if check_section_has_values(
+ section_has_values, ini_lines[section_start:index]
+ ):
+ section_end = index
+ break
+ else:
+ # look for another section
+ within_section = False
+ section_start = section_end = 0
+
# find start and end of section
if section_pattern.match(line):
within_section = True
section_start = index
- elif line.startswith(u'['):
- if within_section:
- section_end = index
- break
before = ini_lines[0:section_start]
section_lines = ini_lines[section_start:section_end]
@@ -435,6 +530,18 @@ def do_ini(module, filename, section=None, option=None, values=None,
if not within_section and state == 'present':
ini_lines.append(u'[%s]\n' % section)
msg = 'section and option added'
+ if section_has_values:
+ for condition in section_has_values:
+ if condition['option'] != option:
+ if len(condition['values']) > 0:
+ for value in condition['values']:
+ ini_lines.append(assignment_format % (condition['option'], value))
+ elif allow_no_value:
+ ini_lines.append(u'%s\n' % condition['option'])
+ elif not exclusive:
+ for value in condition['values']:
+ if value not in values:
+ values.append(value)
if option and values:
for value in values:
ini_lines.append(assignment_format % (option, value))
@@ -476,6 +583,11 @@ def main():
argument_spec=dict(
path=dict(type='path', required=True, aliases=['dest']),
section=dict(type='str'),
+ section_has_values=dict(type='list', elements='dict', options=dict(
+ option=dict(type='str', required=True),
+ value=dict(type='str'),
+ values=dict(type='list', elements='str')
+ ), default=None, mutually_exclusive=[['value', 'values']]),
option=dict(type='str'),
value=dict(type='str'),
values=dict(type='list', elements='str'),
@@ -498,6 +610,7 @@ def main():
path = module.params['path']
section = module.params['section']
+ section_has_values = module.params['section_has_values']
option = module.params['option']
value = module.params['value']
values = module.params['values']
@@ -519,8 +632,16 @@ def main():
elif values is None:
values = []
+ if section_has_values:
+ for condition in section_has_values:
+ if condition['value'] is not None:
+ condition['values'] = [condition['value']]
+ elif condition['values'] is None:
+ condition['values'] = []
+# raise Exception("section_has_values: {}".format(section_has_values))
+
(changed, backup_file, diff, msg) = do_ini(
- module, path, section, option, values, state, exclusive, backup,
+ module, path, section, section_has_values, option, values, state, exclusive, backup,
no_extra_spaces, ignore_spaces, create, allow_no_value, modify_inactive_option, follow)
if not module.check_mode and os.path.exists(path):
diff --git a/ansible_collections/community/general/plugins/modules/java_cert.py b/ansible_collections/community/general/plugins/modules/java_cert.py
index 72302b12c..e2d04b71e 100644
--- a/ansible_collections/community/general/plugins/modules/java_cert.py
+++ b/ansible_collections/community/general/plugins/modules/java_cert.py
@@ -28,7 +28,7 @@ options:
cert_url:
description:
- Basic URL to fetch SSL certificate from.
- - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate.
+ - Exactly one of O(cert_url), O(cert_path), O(cert_content), or O(pkcs12_path) is required to load certificate.
type: str
cert_port:
description:
@@ -39,8 +39,14 @@ options:
cert_path:
description:
- Local path to load certificate from.
- - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate.
+ - Exactly one of O(cert_url), O(cert_path), O(cert_content), or O(pkcs12_path) is required to load certificate.
type: path
+ cert_content:
+ description:
+ - Content of the certificate used to create the keystore.
+ - Exactly one of O(cert_url), O(cert_path), O(cert_content), or O(pkcs12_path) is required to load certificate.
+ type: str
+ version_added: 8.6.0
cert_alias:
description:
- Imported certificate alias.
@@ -55,10 +61,10 @@ options:
pkcs12_path:
description:
- Local path to load PKCS12 keystore from.
- - Unlike O(cert_url) and O(cert_path), the PKCS12 keystore embeds the private key matching
+ - Unlike O(cert_url), O(cert_path) and O(cert_content), the PKCS12 keystore embeds the private key matching
the certificate, and is used to import both the certificate and its private key into the
java keystore.
- - Exactly one of O(cert_url), O(cert_path), or O(pkcs12_path) is required to load certificate.
+ - Exactly one of O(cert_url), O(cert_path), O(cert_content), or O(pkcs12_path) is required to load certificate.
type: path
pkcs12_password:
description:
@@ -149,6 +155,19 @@ EXAMPLES = r'''
cert_alias: LE_RootCA
trust_cacert: true
+- name: Import trusted CA from the SSL certificate stored in the cert_content variable
+ community.general.java_cert:
+ cert_content: |
+ -----BEGIN CERTIFICATE-----
+ ...
+ -----END CERTIFICATE-----
+ keystore_path: /tmp/cacerts
+ keystore_pass: changeit
+ keystore_create: true
+ state: present
+ cert_alias: LE_RootCA
+ trust_cacert: true
+
- name: Import SSL certificate from google.com to a keystore, create it if it doesn't exist
community.general.java_cert:
cert_url: google.com
@@ -487,6 +506,7 @@ def main():
argument_spec = dict(
cert_url=dict(type='str'),
cert_path=dict(type='path'),
+ cert_content=dict(type='str'),
pkcs12_path=dict(type='path'),
pkcs12_password=dict(type='str', no_log=True),
pkcs12_alias=dict(type='str'),
@@ -503,11 +523,11 @@ def main():
module = AnsibleModule(
argument_spec=argument_spec,
- required_if=[['state', 'present', ('cert_path', 'cert_url', 'pkcs12_path'), True],
+ required_if=[['state', 'present', ('cert_path', 'cert_url', 'cert_content', 'pkcs12_path'), True],
['state', 'absent', ('cert_url', 'cert_alias'), True]],
required_together=[['keystore_path', 'keystore_pass']],
mutually_exclusive=[
- ['cert_url', 'cert_path', 'pkcs12_path']
+ ['cert_url', 'cert_path', 'cert_content', 'pkcs12_path']
],
supports_check_mode=True,
add_file_common_args=True,
@@ -515,6 +535,7 @@ def main():
url = module.params.get('cert_url')
path = module.params.get('cert_path')
+ content = module.params.get('cert_content')
port = module.params.get('cert_port')
pkcs12_path = module.params.get('pkcs12_path')
@@ -582,6 +603,10 @@ def main():
# certificate to stdout so we don't need to do any transformations.
new_certificate = path
+ elif content:
+ with open(new_certificate, "w") as f:
+ f.write(content)
+
elif url:
# Getting the X509 digest from a URL is the same as from a path, we just have
# to download the cert first
diff --git a/ansible_collections/community/general/plugins/modules/keycloak_client.py b/ansible_collections/community/general/plugins/modules/keycloak_client.py
index b151e4541..cd9c60bac 100644
--- a/ansible_collections/community/general/plugins/modules/keycloak_client.py
+++ b/ansible_collections/community/general/plugins/modules/keycloak_client.py
@@ -248,8 +248,9 @@ options:
description:
- Type of client.
- At creation only, default value will be V(openid-connect) if O(protocol) is omitted.
+ - The V(docker-v2) value was added in community.general 8.6.0.
type: str
- choices: ['openid-connect', 'saml']
+ choices: ['openid-connect', 'saml', 'docker-v2']
full_scope_allowed:
description:
@@ -393,7 +394,7 @@ options:
protocol:
description:
- This specifies for which protocol this protocol mapper is active.
- choices: ['openid-connect', 'saml']
+ choices: ['openid-connect', 'saml', 'docker-v2']
type: str
protocolMapper:
@@ -724,6 +725,7 @@ import copy
PROTOCOL_OPENID_CONNECT = 'openid-connect'
PROTOCOL_SAML = 'saml'
+PROTOCOL_DOCKER_V2 = 'docker-v2'
CLIENT_META_DATA = ['authorizationServicesEnabled']
@@ -742,6 +744,12 @@ def normalise_cr(clientrep, remove_ids=False):
if 'attributes' in clientrep:
clientrep['attributes'] = list(sorted(clientrep['attributes']))
+ if 'defaultClientScopes' in clientrep:
+ clientrep['defaultClientScopes'] = list(sorted(clientrep['defaultClientScopes']))
+
+ if 'optionalClientScopes' in clientrep:
+ clientrep['optionalClientScopes'] = list(sorted(clientrep['optionalClientScopes']))
+
if 'redirectUris' in clientrep:
clientrep['redirectUris'] = list(sorted(clientrep['redirectUris']))
@@ -785,7 +793,7 @@ def main():
consentText=dict(type='str'),
id=dict(type='str'),
name=dict(type='str'),
- protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML]),
+ protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
@@ -819,7 +827,7 @@ def main():
authorization_services_enabled=dict(type='bool', aliases=['authorizationServicesEnabled']),
public_client=dict(type='bool', aliases=['publicClient']),
frontchannel_logout=dict(type='bool', aliases=['frontchannelLogout']),
- protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML]),
+ protocol=dict(type='str', choices=[PROTOCOL_OPENID_CONNECT, PROTOCOL_SAML, PROTOCOL_DOCKER_V2]),
attributes=dict(type='dict'),
full_scope_allowed=dict(type='bool', aliases=['fullScopeAllowed']),
node_re_registration_timeout=dict(type='int', aliases=['nodeReRegistrationTimeout']),
diff --git a/ansible_collections/community/general/plugins/modules/keycloak_client_rolescope.py b/ansible_collections/community/general/plugins/modules/keycloak_client_rolescope.py
new file mode 100644
index 000000000..cca72f0dd
--- /dev/null
+++ b/ansible_collections/community/general/plugins/modules/keycloak_client_rolescope.py
@@ -0,0 +1,280 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) Ansible project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: keycloak_client_rolescope
+
+short_description: Allows administration of Keycloak client roles scope to restrict the usage of certain roles to a other specific client applications.
+
+version_added: 8.6.0
+
+description:
+ - This module allows you to add or remove Keycloak roles from clients scope via the Keycloak REST API.
+ It requires access to the REST API via OpenID Connect; the user connecting and the client being
+ used must have the requisite access rights. In a default Keycloak installation, admin-cli
+ and an admin user would work, as would a separate client definition with the scope tailored
+ to your needs and a user having the expected roles.
+
+ - Client O(client_id) must have O(community.general.keycloak_client#module:full_scope_allowed) set to V(false).
+
+ - Attributes are multi-valued in the Keycloak API. All attributes are lists of individual values and will
+ be returned that way by this module. You may pass single values for attributes when calling the module,
+ and this will be translated into a list suitable for the API.
+
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+
+options:
+ state:
+ description:
+ - State of the role mapping.
+ - On V(present), all roles in O(role_names) will be mapped if not exists yet.
+ - On V(absent), all roles mapping in O(role_names) will be removed if it exists.
+ default: 'present'
+ type: str
+ choices:
+ - present
+ - absent
+
+ realm:
+ type: str
+ description:
+ - The Keycloak realm under which clients resides.
+ default: 'master'
+
+ client_id:
+ type: str
+ required: true
+ description:
+ - Roles provided in O(role_names) while be added to this client scope.
+
+ client_scope_id:
+ type: str
+ description:
+ - If the O(role_names) are client role, the client ID under which it resides.
+ - If this parameter is absent, the roles are considered a realm role.
+ role_names:
+ required: true
+ type: list
+ elements: str
+ description:
+ - Names of roles to manipulate.
+ - If O(client_scope_id) is present, all roles must be under this client.
+ - If O(client_scope_id) is absent, all roles must be under the realm.
+
+
+extends_documentation_fragment:
+ - community.general.keycloak
+ - community.general.attributes
+
+author:
+ - Andre Desrosiers (@desand01)
+'''
+
+EXAMPLES = '''
+- name: Add roles to public client scope
+ community.general.keycloak_client_rolescope:
+ auth_keycloak_url: https://auth.example.com/auth
+ auth_realm: master
+ auth_username: USERNAME
+ auth_password: PASSWORD
+ realm: MyCustomRealm
+ client_id: frontend-client-public
+ client_scope_id: backend-client-private
+ role_names:
+ - backend-role-admin
+ - backend-role-user
+
+- name: Remove roles from public client scope
+ community.general.keycloak_client_rolescope:
+ auth_keycloak_url: https://auth.example.com/auth
+ auth_realm: master
+ auth_username: USERNAME
+ auth_password: PASSWORD
+ realm: MyCustomRealm
+ client_id: frontend-client-public
+ client_scope_id: backend-client-private
+ role_names:
+ - backend-role-admin
+ state: absent
+
+- name: Add realm roles to public client scope
+ community.general.keycloak_client_rolescope:
+ auth_keycloak_url: https://auth.example.com/auth
+ auth_realm: master
+ auth_username: USERNAME
+ auth_password: PASSWORD
+ realm: MyCustomRealm
+ client_id: frontend-client-public
+ role_names:
+ - realm-role-admin
+ - realm-role-user
+'''
+
+RETURN = '''
+msg:
+ description: Message as to what action was taken.
+ returned: always
+ type: str
+ sample: "Client role scope for frontend-client-public has been updated"
+
+end_state:
+ description: Representation of role role scope after module execution.
+ returned: on success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "clientRole": false,
+ "composite": false,
+ "containerId": "MyCustomRealm",
+ "id": "47293104-59a6-46f0-b460-2e9e3c9c424c",
+ "name": "backend-role-admin"
+ },
+ {
+ "clientRole": false,
+ "composite": false,
+ "containerId": "MyCustomRealm",
+ "id": "39c62a6d-542c-4715-92d2-41021eb33967",
+ "name": "backend-role-user"
+ }
+ ]
+'''
+
+from ansible_collections.community.general.plugins.module_utils.identity.keycloak.keycloak import KeycloakAPI, \
+ keycloak_argument_spec, get_token, KeycloakError
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ """
+ Module execution
+
+ :return:
+ """
+ argument_spec = keycloak_argument_spec()
+
+ meta_args = dict(
+ client_id=dict(type='str', required=True),
+ client_scope_id=dict(type='str'),
+ realm=dict(type='str', default='master'),
+ role_names=dict(type='list', elements='str', required=True),
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ )
+
+ argument_spec.update(meta_args)
+
+ module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True)
+
+ result = dict(changed=False, msg='', diff={}, end_state={})
+
+ # Obtain access token, initialize API
+ try:
+ connection_header = get_token(module.params)
+ except KeycloakError as e:
+ module.fail_json(msg=str(e))
+
+ kc = KeycloakAPI(module, connection_header)
+
+ realm = module.params.get('realm')
+ clientid = module.params.get('client_id')
+ client_scope_id = module.params.get('client_scope_id')
+ role_names = module.params.get('role_names')
+ state = module.params.get('state')
+
+ objRealm = kc.get_realm_by_id(realm)
+ if not objRealm:
+ module.fail_json(msg="Failed to retrive realm '{realm}'".format(realm=realm))
+
+ objClient = kc.get_client_by_clientid(clientid, realm)
+ if not objClient:
+ module.fail_json(msg="Failed to retrive client '{realm}.{clientid}'".format(realm=realm, clientid=clientid))
+ if objClient["fullScopeAllowed"] and state == "present":
+ module.fail_json(msg="FullScopeAllowed is active for Client '{realm}.{clientid}'".format(realm=realm, clientid=clientid))
+
+ if client_scope_id:
+ objClientScope = kc.get_client_by_clientid(client_scope_id, realm)
+ if not objClientScope:
+ module.fail_json(msg="Failed to retrive client '{realm}.{client_scope_id}'".format(realm=realm, client_scope_id=client_scope_id))
+ before_role_mapping = kc.get_client_role_scope_from_client(objClient["id"], objClientScope["id"], realm)
+ else:
+ before_role_mapping = kc.get_client_role_scope_from_realm(objClient["id"], realm)
+
+ if client_scope_id:
+ # retrive all role from client_scope
+ client_scope_roles_by_name = kc.get_client_roles_by_id(objClientScope["id"], realm)
+ else:
+ # retrive all role from realm
+ client_scope_roles_by_name = kc.get_realm_roles(realm)
+
+ # convert to indexed Dict by name
+ client_scope_roles_by_name = {role["name"]: role for role in client_scope_roles_by_name}
+ role_mapping_by_name = {role["name"]: role for role in before_role_mapping}
+ role_mapping_to_manipulate = []
+
+ if state == "present":
+ # update desired
+ for role_name in role_names:
+ if role_name not in client_scope_roles_by_name:
+ if client_scope_id:
+ module.fail_json(msg="Failed to retrive role '{realm}.{client_scope_id}.{role_name}'"
+ .format(realm=realm, client_scope_id=client_scope_id, role_name=role_name))
+ else:
+ module.fail_json(msg="Failed to retrive role '{realm}.{role_name}'".format(realm=realm, role_name=role_name))
+ if role_name not in role_mapping_by_name:
+ role_mapping_to_manipulate.append(client_scope_roles_by_name[role_name])
+ role_mapping_by_name[role_name] = client_scope_roles_by_name[role_name]
+ else:
+ # remove role if present
+ for role_name in role_names:
+ if role_name in role_mapping_by_name:
+ role_mapping_to_manipulate.append(role_mapping_by_name[role_name])
+ del role_mapping_by_name[role_name]
+
+ before_role_mapping = sorted(before_role_mapping, key=lambda d: d['name'])
+ desired_role_mapping = sorted(role_mapping_by_name.values(), key=lambda d: d['name'])
+
+ result['changed'] = len(role_mapping_to_manipulate) > 0
+
+ if result['changed']:
+ result['diff'] = dict(before=before_role_mapping, after=desired_role_mapping)
+
+ if not result['changed']:
+ # no changes
+ result['end_state'] = before_role_mapping
+ result['msg'] = "No changes required for client role scope {name}.".format(name=clientid)
+ elif state == "present":
+ # doing update
+ if module.check_mode:
+ result['end_state'] = desired_role_mapping
+ elif client_scope_id:
+ result['end_state'] = kc.update_client_role_scope_from_client(role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm)
+ else:
+ result['end_state'] = kc.update_client_role_scope_from_realm(role_mapping_to_manipulate, objClient["id"], realm)
+ result['msg'] = "Client role scope for {name} has been updated".format(name=clientid)
+ else:
+ # doing delete
+ if module.check_mode:
+ result['end_state'] = desired_role_mapping
+ elif client_scope_id:
+ result['end_state'] = kc.delete_client_role_scope_from_client(role_mapping_to_manipulate, objClient["id"], objClientScope["id"], realm)
+ else:
+ result['end_state'] = kc.delete_client_role_scope_from_realm(role_mapping_to_manipulate, objClient["id"], realm)
+ result['msg'] = "Client role scope for {name} has been deleted".format(name=clientid)
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py
index d37af5f0c..d24e0f1f2 100644
--- a/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py
+++ b/ansible_collections/community/general/plugins/modules/keycloak_clientscope.py
@@ -79,7 +79,8 @@ options:
protocol:
description:
- Type of client.
- choices: ['openid-connect', 'saml', 'wsfed']
+ - The V(docker-v2) value was added in community.general 8.6.0.
+ choices: ['openid-connect', 'saml', 'wsfed', 'docker-v2']
type: str
protocol_mappers:
@@ -95,7 +96,7 @@ options:
description:
- This specifies for which protocol this protocol mapper.
- is active.
- choices: ['openid-connect', 'saml', 'wsfed']
+ choices: ['openid-connect', 'saml', 'wsfed', 'docker-v2']
type: str
protocolMapper:
@@ -330,7 +331,7 @@ def main():
protmapper_spec = dict(
id=dict(type='str'),
name=dict(type='str'),
- protocol=dict(type='str', choices=['openid-connect', 'saml', 'wsfed']),
+ protocol=dict(type='str', choices=['openid-connect', 'saml', 'wsfed', 'docker-v2']),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
@@ -341,7 +342,7 @@ def main():
id=dict(type='str'),
name=dict(type='str'),
description=dict(type='str'),
- protocol=dict(type='str', choices=['openid-connect', 'saml', 'wsfed']),
+ protocol=dict(type='str', choices=['openid-connect', 'saml', 'wsfed', 'docker-v2']),
attributes=dict(type='dict'),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
)
diff --git a/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py b/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py
index cd7f6c09b..7bffb5cbb 100644
--- a/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py
+++ b/ansible_collections/community/general/plugins/modules/keycloak_clienttemplate.py
@@ -68,7 +68,8 @@ options:
protocol:
description:
- Type of client template.
- choices: ['openid-connect', 'saml']
+ - The V(docker-v2) value was added in community.general 8.6.0.
+ choices: ['openid-connect', 'saml', 'docker-v2']
type: str
full_scope_allowed:
@@ -107,7 +108,7 @@ options:
protocol:
description:
- This specifies for which protocol this protocol mapper is active.
- choices: ['openid-connect', 'saml']
+ choices: ['openid-connect', 'saml', 'docker-v2']
type: str
protocolMapper:
@@ -292,7 +293,7 @@ def main():
consentText=dict(type='str'),
id=dict(type='str'),
name=dict(type='str'),
- protocol=dict(type='str', choices=['openid-connect', 'saml']),
+ protocol=dict(type='str', choices=['openid-connect', 'saml', 'docker-v2']),
protocolMapper=dict(type='str'),
config=dict(type='dict'),
)
@@ -304,7 +305,7 @@ def main():
id=dict(type='str'),
name=dict(type='str'),
description=dict(type='str'),
- protocol=dict(type='str', choices=['openid-connect', 'saml']),
+ protocol=dict(type='str', choices=['openid-connect', 'saml', 'docker-v2']),
attributes=dict(type='dict'),
full_scope_allowed=dict(type='bool'),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec),
diff --git a/ansible_collections/community/general/plugins/modules/keycloak_realm.py b/ansible_collections/community/general/plugins/modules/keycloak_realm.py
index 9f2e72b52..6128c9e4c 100644
--- a/ansible_collections/community/general/plugins/modules/keycloak_realm.py
+++ b/ansible_collections/community/general/plugins/modules/keycloak_realm.py
@@ -582,6 +582,27 @@ from ansible_collections.community.general.plugins.module_utils.identity.keycloa
from ansible.module_utils.basic import AnsibleModule
+def normalise_cr(realmrep):
+ """ Re-sorts any properties where the order is important so that diff's is minimised and the change detection is more effective.
+
+ :param realmrep: the realmrep dict to be sanitized
+ :return: normalised realmrep dict
+ """
+ # Avoid the dict passed in to be modified
+ realmrep = realmrep.copy()
+
+ if 'enabledEventTypes' in realmrep:
+ realmrep['enabledEventTypes'] = list(sorted(realmrep['enabledEventTypes']))
+
+ if 'otpSupportedApplications' in realmrep:
+ realmrep['otpSupportedApplications'] = list(sorted(realmrep['otpSupportedApplications']))
+
+ if 'supportedLocales' in realmrep:
+ realmrep['supportedLocales'] = list(sorted(realmrep['supportedLocales']))
+
+ return realmrep
+
+
def sanitize_cr(realmrep):
""" Removes probably sensitive details from a realm representation.
@@ -595,7 +616,7 @@ def sanitize_cr(realmrep):
if 'saml.signing.private.key' in result['attributes']:
result['attributes'] = result['attributes'].copy()
result['attributes']['saml.signing.private.key'] = '********'
- return result
+ return normalise_cr(result)
def main():
@@ -777,9 +798,11 @@ def main():
result['changed'] = True
if module.check_mode:
# We can only compare the current realm with the proposed updates we have
+ before_norm = normalise_cr(before_realm)
+ desired_norm = normalise_cr(desired_realm)
if module._diff:
- result['diff'] = dict(before=before_realm_sanitized,
- after=sanitize_cr(desired_realm))
+ result['diff'] = dict(before=sanitize_cr(before_norm),
+ after=sanitize_cr(desired_norm))
result['changed'] = (before_realm != desired_realm)
module.exit_json(**result)
diff --git a/ansible_collections/community/general/plugins/modules/lxd_container.py b/ansible_collections/community/general/plugins/modules/lxd_container.py
index 9fd1b183b..b82e2be9b 100644
--- a/ansible_collections/community/general/plugins/modules/lxd_container.py
+++ b/ansible_collections/community/general/plugins/modules/lxd_container.py
@@ -86,8 +86,8 @@ options:
source:
description:
- 'The source for the instance
- (for example V({ "type": "image", "mode": "pull", "server": "https://images.linuxcontainers.org",
- "protocol": "lxd", "alias": "ubuntu/xenial/amd64" })).'
+ (for example V({ "type": "image", "mode": "pull", "server": "https://cloud-images.ubuntu.com/releases/",
+ "protocol": "simplestreams", "alias": "22.04" })).'
- 'See U(https://documentation.ubuntu.com/lxd/en/latest/api/) for complete API documentation.'
- 'Note that C(protocol) accepts two choices: V(lxd) or V(simplestreams).'
required: false
@@ -205,6 +205,9 @@ notes:
- You can copy a file in the created instance to the localhost
with C(command=lxc file pull instance_name/dir/filename filename).
See the first example below.
+ - linuxcontainers.org has phased out LXC/LXD support with March 2024
+ (U(https://discuss.linuxcontainers.org/t/important-notice-for-lxd-users-image-server/18479)).
+ Currently only Ubuntu is still providing images.
'''
EXAMPLES = '''
@@ -220,9 +223,9 @@ EXAMPLES = '''
source:
type: image
mode: pull
- server: https://images.linuxcontainers.org
- protocol: lxd # if you get a 404, try setting protocol: simplestreams
- alias: ubuntu/xenial/amd64
+ server: https://cloud-images.ubuntu.com/releases/
+ protocol: simplestreams
+ alias: "22.04"
profiles: ["default"]
wait_for_ipv4_addresses: true
timeout: 600
@@ -264,6 +267,26 @@ EXAMPLES = '''
wait_for_ipv4_addresses: true
timeout: 600
+# An example of creating a ubuntu-minial container
+- hosts: localhost
+ connection: local
+ tasks:
+ - name: Create a started container
+ community.general.lxd_container:
+ name: mycontainer
+ ignore_volatile_options: true
+ state: started
+ source:
+ type: image
+ mode: pull
+ # Provides Ubuntu minimal images
+ server: https://cloud-images.ubuntu.com/minimal/releases/
+ protocol: simplestreams
+ alias: "22.04"
+ profiles: ["default"]
+ wait_for_ipv4_addresses: true
+ timeout: 600
+
# An example for creating container in project other than default
- hosts: localhost
connection: local
@@ -278,8 +301,8 @@ EXAMPLES = '''
protocol: simplestreams
type: image
mode: pull
- server: https://images.linuxcontainers.org
- alias: ubuntu/20.04/cloud
+ server: https://cloud-images.ubuntu.com/releases/
+ alias: "22.04"
profiles: ["default"]
wait_for_ipv4_addresses: true
timeout: 600
@@ -347,7 +370,7 @@ EXAMPLES = '''
source:
type: image
mode: pull
- alias: ubuntu/xenial/amd64
+ alias: "22.04"
target: node01
- name: Create container on another node
@@ -358,7 +381,7 @@ EXAMPLES = '''
source:
type: image
mode: pull
- alias: ubuntu/xenial/amd64
+ alias: "22.04"
target: node02
# An example for creating a virtual machine
diff --git a/ansible_collections/community/general/plugins/modules/nmcli.py b/ansible_collections/community/general/plugins/modules/nmcli.py
index 9360ce37d..6f0884da9 100644
--- a/ansible_collections/community/general/plugins/modules/nmcli.py
+++ b/ansible_collections/community/general/plugins/modules/nmcli.py
@@ -64,13 +64,16 @@ options:
- Type V(infiniband) is added in community.general 2.0.0.
- Type V(loopback) is added in community.general 8.1.0.
- Type V(macvlan) is added in community.general 6.6.0.
+ - Type V(ovs-bridge) is added in community.general 8.6.0.
+ - Type V(ovs-interface) is added in community.general 8.6.0.
+ - Type V(ovs-port) is added in community.general 8.6.0.
- Type V(wireguard) is added in community.general 4.3.0.
- Type V(vpn) is added in community.general 5.1.0.
- Using V(bond-slave), V(bridge-slave), or V(team-slave) implies V(ethernet) connection type with corresponding O(slave_type) option.
- If you want to control non-ethernet connection attached to V(bond), V(bridge), or V(team) consider using O(slave_type) option.
type: str
choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, macvlan, sit, team, team-slave, vlan, vxlan,
- wifi, gsm, wireguard, vpn, loopback ]
+ wifi, gsm, wireguard, ovs-bridge, ovs-port, ovs-interface, vpn, loopback ]
mode:
description:
- This is the type of device or network connection that you wish to create for a bond or bridge.
@@ -86,12 +89,13 @@ options:
slave_type:
description:
- Type of the device of this slave's master connection (for example V(bond)).
+ - Type V(ovs-port) is added in community.general 8.6.0.
type: str
- choices: [ 'bond', 'bridge', 'team' ]
+ choices: [ 'bond', 'bridge', 'team', 'ovs-port' ]
version_added: 7.0.0
master:
description:
- - Master <master (ifname, or connection UUID or conn_name) of bridge, team, bond master connection profile.
+ - Master <master (ifname, or connection UUID or conn_name) of bridge, team, bond, ovs-port master connection profile.
- Mandatory if O(slave_type) is defined.
type: str
ip4:
@@ -1505,6 +1509,32 @@ EXAMPLES = r'''
table: "production"
routing_rules4:
- "priority 0 from 192.168.1.50 table 200"
+
+## Creating an OVS bridge and attaching a port
+- name: Create OVS Bridge
+ community.general.nmcli:
+ conn_name: ovs-br-conn
+ ifname: ovs-br
+ type: ovs-bridge
+ state: present
+
+- name: Create OVS Port for OVS Bridge Interface
+ community.general.nmcli:
+ conn_name: ovs-br-interface-port-conn
+ ifname: ovs-br-interface-port
+ master: ovs-br
+ type: ovs-port
+ state: present
+
+## Adding an ethernet interface to an OVS bridge port
+- name: Add Ethernet Interface to OVS Port
+ community.general.nmcli:
+ conn_name: eno1
+ ifname: eno1
+ master: ovs-br-interface-port
+ slave_type: ovs-port
+ type: ethernet
+ state: present
'''
RETURN = r"""#
@@ -1678,7 +1708,8 @@ class Nmcli(object):
}
# IP address options.
- if self.ip_conn_type and not self.master:
+ # The ovs-interface type can be both ip_conn_type and have a master
+ if (self.ip_conn_type and not self.master) or self.type == "ovs-interface":
options.update({
'ipv4.addresses': self.enforce_ipv4_cidr_notation(self.ip4),
'ipv4.dhcp-client-id': self.dhcp_client_id,
@@ -1939,6 +1970,7 @@ class Nmcli(object):
'wireguard',
'vpn',
'loopback',
+ 'ovs-interface',
)
@property
@@ -2005,6 +2037,8 @@ class Nmcli(object):
'team-slave',
'wifi',
'infiniband',
+ 'ovs-port',
+ 'ovs-interface',
)
@property
@@ -2400,7 +2434,7 @@ def main():
state=dict(type='str', required=True, choices=['absent', 'present']),
conn_name=dict(type='str', required=True),
master=dict(type='str'),
- slave_type=dict(type='str', choices=['bond', 'bridge', 'team']),
+ slave_type=dict(type='str', choices=['bond', 'bridge', 'team', 'ovs-port']),
ifname=dict(type='str'),
type=dict(type='str',
choices=[
@@ -2425,6 +2459,9 @@ def main():
'wireguard',
'vpn',
'loopback',
+ 'ovs-interface',
+ 'ovs-bridge',
+ 'ovs-port',
]),
ip4=dict(type='list', elements='str'),
gw4=dict(type='str'),
diff --git a/ansible_collections/community/general/plugins/modules/osx_defaults.py b/ansible_collections/community/general/plugins/modules/osx_defaults.py
index 336e95332..db5d889a3 100644
--- a/ansible_collections/community/general/plugins/modules/osx_defaults.py
+++ b/ansible_collections/community/general/plugins/modules/osx_defaults.py
@@ -50,6 +50,13 @@ options:
type: str
choices: [ array, bool, boolean, date, float, int, integer, string ]
default: string
+ check_type:
+ description:
+ - Checks if the type of the provided O(value) matches the type of an existing default.
+ - If the types do not match, raises an error.
+ type: bool
+ default: true
+ version_added: 8.6.0
array_add:
description:
- Add new elements to the array for a key which has an array as its value.
@@ -158,6 +165,7 @@ class OSXDefaults(object):
self.domain = module.params['domain']
self.host = module.params['host']
self.key = module.params['key']
+ self.check_type = module.params['check_type']
self.type = module.params['type']
self.array_add = module.params['array_add']
self.value = module.params['value']
@@ -349,10 +357,11 @@ class OSXDefaults(object):
self.delete()
return True
- # There is a type mismatch! Given type does not match the type in defaults
- value_type = type(self.value)
- if self.current_value is not None and not isinstance(self.current_value, value_type):
- raise OSXDefaultsException("Type mismatch. Type in defaults: %s" % type(self.current_value).__name__)
+ # Check if there is a type mismatch, e.g. given type does not match the type in defaults
+ if self.check_type:
+ value_type = type(self.value)
+ if self.current_value is not None and not isinstance(self.current_value, value_type):
+ raise OSXDefaultsException("Type mismatch. Type in defaults: %s" % type(self.current_value).__name__)
# Current value matches the given value. Nothing need to be done. Arrays need extra care
if self.type == "array" and self.current_value is not None and not self.array_add and \
@@ -383,6 +392,7 @@ def main():
domain=dict(type='str', default='NSGlobalDomain'),
host=dict(type='str'),
key=dict(type='str', no_log=False),
+ check_type=dict(type='bool', default=True),
type=dict(type='str', default='string', choices=['array', 'bool', 'boolean', 'date', 'float', 'int', 'integer', 'string']),
array_add=dict(type='bool', default=False),
value=dict(type='raw'),
diff --git a/ansible_collections/community/general/plugins/modules/pagerduty.py b/ansible_collections/community/general/plugins/modules/pagerduty.py
index 596c4f4da..853bd6d79 100644
--- a/ansible_collections/community/general/plugins/modules/pagerduty.py
+++ b/ansible_collections/community/general/plugins/modules/pagerduty.py
@@ -151,6 +151,10 @@ import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
class PagerDutyRequest(object):
def __init__(self, module, name, user, token):
@@ -206,9 +210,9 @@ class PagerDutyRequest(object):
return [{'id': service, 'type': 'service_reference'}]
def _compute_start_end_time(self, hours, minutes):
- now = datetime.datetime.utcnow()
- later = now + datetime.timedelta(hours=int(hours), minutes=int(minutes))
- start = now.strftime("%Y-%m-%dT%H:%M:%SZ")
+ now_t = now()
+ later = now_t + datetime.timedelta(hours=int(hours), minutes=int(minutes))
+ start = now_t.strftime("%Y-%m-%dT%H:%M:%SZ")
end = later.strftime("%Y-%m-%dT%H:%M:%SZ")
return start, end
diff --git a/ansible_collections/community/general/plugins/modules/pagerduty_change.py b/ansible_collections/community/general/plugins/modules/pagerduty_change.py
index 1a1e50dcf..acd31fb44 100644
--- a/ansible_collections/community/general/plugins/modules/pagerduty_change.py
+++ b/ansible_collections/community/general/plugins/modules/pagerduty_change.py
@@ -110,7 +110,10 @@ EXAMPLES = '''
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.basic import AnsibleModule
-from datetime import datetime
+
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
def main():
@@ -161,8 +164,7 @@ def main():
if module.params['environment']:
custom_details['environment'] = module.params['environment']
- now = datetime.utcnow()
- timestamp = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+ timestamp = now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
payload = {
'summary': module.params['summary'],
diff --git a/ansible_collections/community/general/plugins/modules/portage.py b/ansible_collections/community/general/plugins/modules/portage.py
index 112f6d2d7..8ae8efb08 100644
--- a/ansible_collections/community/general/plugins/modules/portage.py
+++ b/ansible_collections/community/general/plugins/modules/portage.py
@@ -121,6 +121,14 @@ options:
type: bool
default: false
+ select:
+ description:
+ - If set to V(true), explicitely add the package to the world file.
+ - Please note that this option is not used for idempotency, it is only used
+ when actually installing a package.
+ type: bool
+ version_added: 8.6.0
+
sync:
description:
- Sync package repositories first
@@ -374,6 +382,7 @@ def emerge_packages(module, packages):
'loadavg': '--load-average',
'backtrack': '--backtrack',
'withbdeps': '--with-bdeps',
+ 'select': '--select',
}
for flag, arg in emerge_flags.items():
@@ -523,6 +532,7 @@ def main():
nodeps=dict(default=False, type='bool'),
onlydeps=dict(default=False, type='bool'),
depclean=dict(default=False, type='bool'),
+ select=dict(default=None, type='bool'),
quiet=dict(default=False, type='bool'),
verbose=dict(default=False, type='bool'),
sync=dict(default=None, choices=['yes', 'web', 'no']),
@@ -543,6 +553,7 @@ def main():
['quiet', 'verbose'],
['quietbuild', 'verbose'],
['quietfail', 'verbose'],
+ ['oneshot', 'select'],
],
supports_check_mode=True,
)
diff --git a/ansible_collections/community/general/plugins/modules/puppet.py b/ansible_collections/community/general/plugins/modules/puppet.py
index 86eac062a..b28583fe0 100644
--- a/ansible_collections/community/general/plugins/modules/puppet.py
+++ b/ansible_collections/community/general/plugins/modules/puppet.py
@@ -116,6 +116,15 @@ options:
- Whether to print file changes details
type: bool
default: false
+ environment_lang:
+ description:
+ - The lang environment to use when running the puppet agent.
+ - The default value, V(C), is supported on every system, but can lead to encoding errors if UTF-8 is used in the output
+ - Use V(C.UTF-8) or V(en_US.UTF-8) or similar UTF-8 supporting locales in case of problems. You need to make sure
+ the selected locale is supported on the system the puppet agent runs on.
+ type: str
+ default: C
+ version_added: 8.6.0
requirements:
- puppet
author:
@@ -208,6 +217,7 @@ def main():
debug=dict(type='bool', default=False),
verbose=dict(type='bool', default=False),
use_srv_records=dict(type='bool'),
+ environment_lang=dict(type='str', default='C'),
),
supports_check_mode=True,
mutually_exclusive=[
diff --git a/ansible_collections/community/general/plugins/modules/redfish_command.py b/ansible_collections/community/general/plugins/modules/redfish_command.py
index e66380493..06224235a 100644
--- a/ansible_collections/community/general/plugins/modules/redfish_command.py
+++ b/ansible_collections/community/general/plugins/modules/redfish_command.py
@@ -281,6 +281,12 @@ options:
- BIOS attributes that needs to be verified in the given server.
type: dict
version_added: 6.4.0
+ reset_to_defaults_mode:
+ description:
+ - Mode to apply when reseting to default.
+ type: str
+ choices: [ ResetAll, PreserveNetworkAndUsers, PreserveNetwork ]
+ version_added: 8.6.0
author:
- "Jose Delarosa (@jose-delarosa)"
@@ -714,6 +720,13 @@ EXAMPLES = '''
command: PowerReboot
resource_id: BMC
+ - name: Factory reset manager to defaults
+ community.general.redfish_command:
+ category: Manager
+ command: ResetToDefaults
+ resource_id: BMC
+ reset_to_defaults_mode: ResetAll
+
- name: Verify BIOS attributes
community.general.redfish_command:
category: Systems
@@ -764,6 +777,7 @@ CATEGORY_COMMANDS_ALL = {
"UpdateAccountServiceProperties"],
"Sessions": ["ClearSessions", "CreateSession", "DeleteSession"],
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
+ "ResetToDefaults",
"VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart",
"PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"],
"Update": ["SimpleUpdate", "MultipartHTTPPushUpdate", "PerformRequestedOperations"],
@@ -825,6 +839,7 @@ def main():
)
),
strip_etag_quotes=dict(type='bool', default=False),
+ reset_to_defaults_mode=dict(choices=['ResetAll', 'PreserveNetworkAndUsers', 'PreserveNetwork']),
bios_attributes=dict(type="dict")
),
required_together=[
@@ -1017,6 +1032,8 @@ def main():
result = rf_utils.virtual_media_insert(virtual_media, category)
elif command == 'VirtualMediaEject':
result = rf_utils.virtual_media_eject(virtual_media, category)
+ elif command == 'ResetToDefaults':
+ result = rf_utils.manager_reset_to_defaults(module.params['reset_to_defaults_mode'])
elif category == "Update":
# execute only if we find UpdateService resources
diff --git a/ansible_collections/community/general/plugins/modules/riak.py b/ansible_collections/community/general/plugins/modules/riak.py
index fe295d2d6..438263da2 100644
--- a/ansible_collections/community/general/plugins/modules/riak.py
+++ b/ansible_collections/community/general/plugins/modules/riak.py
@@ -93,7 +93,7 @@ from ansible.module_utils.urls import fetch_url
def ring_check(module, riak_admin_bin):
- cmd = '%s ringready' % riak_admin_bin
+ cmd = riak_admin_bin + ['ringready']
rc, out, err = module.run_command(cmd)
if rc == 0 and 'TRUE All nodes agree on the ring' in out:
return True
@@ -127,6 +127,7 @@ def main():
# make sure riak commands are on the path
riak_bin = module.get_bin_path('riak')
riak_admin_bin = module.get_bin_path('riak-admin')
+ riak_admin_bin = [riak_admin_bin] if riak_admin_bin is not None else [riak_bin, 'admin']
timeout = time.time() + 120
while True:
@@ -164,7 +165,7 @@ def main():
module.fail_json(msg=out)
elif command == 'kv_test':
- cmd = '%s test' % riak_admin_bin
+ cmd = riak_admin_bin + ['test']
rc, out, err = module.run_command(cmd)
if rc == 0:
result['kv_test'] = out
@@ -175,7 +176,7 @@ def main():
if nodes.count(node_name) == 1 and len(nodes) > 1:
result['join'] = 'Node is already in cluster or staged to be in cluster.'
else:
- cmd = '%s cluster join %s' % (riak_admin_bin, target_node)
+ cmd = riak_admin_bin + ['cluster', 'join', target_node]
rc, out, err = module.run_command(cmd)
if rc == 0:
result['join'] = out
@@ -184,7 +185,7 @@ def main():
module.fail_json(msg=out)
elif command == 'plan':
- cmd = '%s cluster plan' % riak_admin_bin
+ cmd = riak_admin_bin + ['cluster', 'plan']
rc, out, err = module.run_command(cmd)
if rc == 0:
result['plan'] = out
@@ -194,7 +195,7 @@ def main():
module.fail_json(msg=out)
elif command == 'commit':
- cmd = '%s cluster commit' % riak_admin_bin
+ cmd = riak_admin_bin + ['cluster', 'commit']
rc, out, err = module.run_command(cmd)
if rc == 0:
result['commit'] = out
@@ -206,7 +207,7 @@ def main():
if wait_for_handoffs:
timeout = time.time() + wait_for_handoffs
while True:
- cmd = '%s transfers' % riak_admin_bin
+ cmd = riak_admin_bin + ['transfers']
rc, out, err = module.run_command(cmd)
if 'No transfers active' in out:
result['handoffs'] = 'No transfers active.'
@@ -216,7 +217,7 @@ def main():
module.fail_json(msg='Timeout waiting for handoffs.')
if wait_for_service:
- cmd = [riak_admin_bin, 'wait_for_service', 'riak_%s' % wait_for_service, node_name]
+ cmd = riak_admin_bin + ['wait_for_service', 'riak_%s' % wait_for_service, node_name]
rc, out, err = module.run_command(cmd)
result['service'] = out
diff --git a/ansible_collections/community/general/plugins/modules/scaleway_compute.py b/ansible_collections/community/general/plugins/modules/scaleway_compute.py
index 7f85bc668..58a321505 100644
--- a/ansible_collections/community/general/plugins/modules/scaleway_compute.py
+++ b/ansible_collections/community/general/plugins/modules/scaleway_compute.py
@@ -183,6 +183,7 @@ import datetime
import time
from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.general.plugins.module_utils.datetime import now
from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_LOCATION, scaleway_argument_spec, Scaleway
SCALEWAY_SERVER_STATES = (
@@ -235,9 +236,9 @@ def wait_to_complete_state_transition(compute_api, server, wait=None):
wait_timeout = compute_api.module.params["wait_timeout"]
wait_sleep_time = compute_api.module.params["wait_sleep_time"]
- start = datetime.datetime.utcnow()
+ start = now()
end = start + datetime.timedelta(seconds=wait_timeout)
- while datetime.datetime.utcnow() < end:
+ while now() < end:
compute_api.module.debug("We are going to wait for the server to finish its transition")
if fetch_state(compute_api, server) not in SCALEWAY_TRANSITIONS_STATES:
compute_api.module.debug("It seems that the server is not in transition anymore.")
diff --git a/ansible_collections/community/general/plugins/modules/scaleway_database_backup.py b/ansible_collections/community/general/plugins/modules/scaleway_database_backup.py
index 592ec0b7f..1d0c17fb6 100644
--- a/ansible_collections/community/general/plugins/modules/scaleway_database_backup.py
+++ b/ansible_collections/community/general/plugins/modules/scaleway_database_backup.py
@@ -170,6 +170,9 @@ import datetime
import time
from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
from ansible_collections.community.general.plugins.module_utils.scaleway import (
Scaleway,
scaleway_argument_spec,
@@ -189,9 +192,9 @@ def wait_to_complete_state_transition(module, account_api, backup=None):
if backup is None or backup['status'] in stable_states:
return backup
- start = datetime.datetime.utcnow()
+ start = now()
end = start + datetime.timedelta(seconds=wait_timeout)
- while datetime.datetime.utcnow() < end:
+ while now() < end:
module.debug('We are going to wait for the backup to finish its transition')
response = account_api.get('/rdb/v1/regions/%s/backups/%s' % (module.params.get('region'), backup['id']))
diff --git a/ansible_collections/community/general/plugins/modules/scaleway_lb.py b/ansible_collections/community/general/plugins/modules/scaleway_lb.py
index 3e43a8ae2..5bd16c3f4 100644
--- a/ansible_collections/community/general/plugins/modules/scaleway_lb.py
+++ b/ansible_collections/community/general/plugins/modules/scaleway_lb.py
@@ -161,6 +161,7 @@ RETURNS = '''
import datetime
import time
from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.general.plugins.module_utils.datetime import now
from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_REGIONS, SCALEWAY_ENDPOINT, scaleway_argument_spec, Scaleway
STABLE_STATES = (
@@ -208,9 +209,9 @@ def wait_to_complete_state_transition(api, lb, force_wait=False):
wait_timeout = api.module.params["wait_timeout"]
wait_sleep_time = api.module.params["wait_sleep_time"]
- start = datetime.datetime.utcnow()
+ start = now()
end = start + datetime.timedelta(seconds=wait_timeout)
- while datetime.datetime.utcnow() < end:
+ while now() < end:
api.module.debug("We are going to wait for the load-balancer to finish its transition")
state = fetch_state(api, lb)
if state in STABLE_STATES:
diff --git a/ansible_collections/community/general/plugins/modules/ssh_config.py b/ansible_collections/community/general/plugins/modules/ssh_config.py
index e89e087b3..d974f4537 100644
--- a/ansible_collections/community/general/plugins/modules/ssh_config.py
+++ b/ansible_collections/community/general/plugins/modules/ssh_config.py
@@ -88,7 +88,8 @@ options:
strict_host_key_checking:
description:
- Whether to strictly check the host key when doing connections to the remote host.
- choices: [ 'yes', 'no', 'ask' ]
+ - The value V(accept-new) is supported since community.general 8.6.0.
+ choices: [ 'yes', 'no', 'ask', 'accept-new' ]
type: str
proxycommand:
description:
@@ -370,7 +371,7 @@ def main():
strict_host_key_checking=dict(
type='str',
default=None,
- choices=['yes', 'no', 'ask']
+ choices=['yes', 'no', 'ask', 'accept-new'],
),
controlmaster=dict(type='str', default=None, choices=['yes', 'no', 'ask', 'auto', 'autoask']),
controlpath=dict(type='str', default=None),
diff --git a/ansible_collections/community/general/plugins/modules/statusio_maintenance.py b/ansible_collections/community/general/plugins/modules/statusio_maintenance.py
index e6b34b709..0a96d0fb4 100644
--- a/ansible_collections/community/general/plugins/modules/statusio_maintenance.py
+++ b/ansible_collections/community/general/plugins/modules/statusio_maintenance.py
@@ -188,6 +188,10 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.urls import open_url
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
def get_api_auth_headers(api_id, api_key, url, statuspage):
@@ -270,11 +274,11 @@ def get_date_time(start_date, start_time, minutes):
except (NameError, ValueError):
return 1, None, "Couldn't work out a valid date"
else:
- now = datetime.datetime.utcnow()
- delta = now + datetime.timedelta(minutes=minutes)
+ now_t = now()
+ delta = now_t + datetime.timedelta(minutes=minutes)
# start_date
- returned_date.append(now.strftime("%m/%d/%Y"))
- returned_date.append(now.strftime("%H:%M"))
+ returned_date.append(now_t.strftime("%m/%d/%Y"))
+ returned_date.append(now_t.strftime("%H:%M"))
# end_date
returned_date.append(delta.strftime("%m/%d/%Y"))
returned_date.append(delta.strftime("%H:%M"))
diff --git a/ansible_collections/community/general/plugins/modules/xml.py b/ansible_collections/community/general/plugins/modules/xml.py
index a3c12b8ee..f5cdbeac3 100644
--- a/ansible_collections/community/general/plugins/modules/xml.py
+++ b/ansible_collections/community/general/plugins/modules/xml.py
@@ -436,11 +436,16 @@ def is_attribute(tree, xpath, namespaces):
""" Test if a given xpath matches and that match is an attribute
An xpath attribute search will only match one item"""
+
+ # lxml 5.1.1 removed etree._ElementStringResult, so we can no longer simply assume it's there
+ # (https://github.com/lxml/lxml/commit/eba79343d0e7ad1ce40169f60460cdd4caa29eb3)
+ ElementStringResult = getattr(etree, '_ElementStringResult', None)
+
if xpath_matches(tree, xpath, namespaces):
match = tree.xpath(xpath, namespaces=namespaces)
- if isinstance(match[0], etree._ElementStringResult):
+ if isinstance(match[0], etree._ElementUnicodeResult):
return True
- elif isinstance(match[0], etree._ElementUnicodeResult):
+ elif ElementStringResult is not None and isinstance(match[0], ElementStringResult):
return True
return False