summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:05:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-26 04:05:56 +0000
commit67c6a4d1dccb62159b9d9b2dea4e2f487446e276 (patch)
tree9ccbb35137f480bbbdb899accbda52a8135d3416 /ansible_collections/community/general/plugins
parentAdding upstream version 9.4.0+dfsg. (diff)
downloadansible-67c6a4d1dccb62159b9d9b2dea4e2f487446e276.tar.xz
ansible-67c6a4d1dccb62159b9d9b2dea4e2f487446e276.zip
Adding upstream version 9.5.1+dfsg.upstream/9.5.1+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/general/plugins')
-rw-r--r--ansible_collections/community/general/plugins/callback/hipchat.py4
-rw-r--r--ansible_collections/community/general/plugins/callback/loganalytics.py13
-rw-r--r--ansible_collections/community/general/plugins/callback/logstash.py9
-rw-r--r--ansible_collections/community/general/plugins/callback/splunk.py13
-rw-r--r--ansible_collections/community/general/plugins/callback/sumologic.py14
-rw-r--r--ansible_collections/community/general/plugins/filter/from_ini.py2
-rw-r--r--ansible_collections/community/general/plugins/filter/to_ini.py2
-rw-r--r--ansible_collections/community/general/plugins/inventory/cobbler.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/gitlab_runners.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/icinga2.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/linode.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/lxd.py2
-rw-r--r--ansible_collections/community/general/plugins/inventory/nmap.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/online.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/opennebula.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/proxmox.py2
-rw-r--r--ansible_collections/community/general/plugins/inventory/scaleway.py2
-rw-r--r--ansible_collections/community/general/plugins/inventory/stackpath_compute.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/virtualbox.py3
-rw-r--r--ansible_collections/community/general/plugins/inventory/xen_orchestra.py2
-rw-r--r--ansible_collections/community/general/plugins/lookup/bitwarden.py40
-rw-r--r--ansible_collections/community/general/plugins/lookup/bitwarden_secrets_manager.py21
-rw-r--r--ansible_collections/community/general/plugins/lookup/passwordstore.py31
-rw-r--r--ansible_collections/community/general/plugins/module_utils/datetime.py32
-rw-r--r--ansible_collections/community/general/plugins/module_utils/gitlab.py13
-rw-r--r--ansible_collections/community/general/plugins/module_utils/identity/keycloak/keycloak.py102
-rw-r--r--ansible_collections/community/general/plugins/module_utils/ipa.py2
-rw-r--r--ansible_collections/community/general/plugins/module_utils/puppet.py1
-rw-r--r--ansible_collections/community/general/plugins/module_utils/redfish_utils.py50
-rw-r--r--ansible_collections/community/general/plugins/module_utils/scaleway.py8
-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
-rw-r--r--ansible_collections/community/general/plugins/plugin_utils/unsafe.py41
65 files changed, 1209 insertions, 200 deletions
diff --git a/ansible_collections/community/general/plugins/callback/hipchat.py b/ansible_collections/community/general/plugins/callback/hipchat.py
index 3e10b69e7..afd9e2055 100644
--- a/ansible_collections/community/general/plugins/callback/hipchat.py
+++ b/ansible_collections/community/general/plugins/callback/hipchat.py
@@ -18,6 +18,10 @@ DOCUMENTATION = '''
description:
- This callback plugin sends status updates to a HipChat channel during playbook execution.
- Before 2.4 only environment variables were available for configuring this plugin.
+ deprecated:
+ removed_in: 10.0.0
+ why: The hipchat service has been discontinued and the self-hosted variant has been End of Life since 2020.
+ alternative: There is none.
options:
token:
description: HipChat API token for v1 or v2 API.
diff --git a/ansible_collections/community/general/plugins/callback/loganalytics.py b/ansible_collections/community/general/plugins/callback/loganalytics.py
index fbcdc6f89..ed7e47b2e 100644
--- a/ansible_collections/community/general/plugins/callback/loganalytics.py
+++ b/ansible_collections/community/general/plugins/callback/loganalytics.py
@@ -59,13 +59,16 @@ import uuid
import socket
import getpass
-from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
class AzureLogAnalyticsSource(object):
def __init__(self):
@@ -93,7 +96,7 @@ class AzureLogAnalyticsSource(object):
return "https://{0}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01".format(workspace_id)
def __rfc1123date(self):
- return datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
+ return now().strftime('%a, %d %b %Y %H:%M:%S GMT')
def send_event(self, workspace_id, shared_key, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
@@ -167,7 +170,7 @@ class CallbackModule(CallbackBase):
def _seconds_since_start(self, result):
return (
- datetime.utcnow() -
+ now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
@@ -185,10 +188,10 @@ class CallbackModule(CallbackBase):
self.loganalytics.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_playbook_on_handler_task_start(self, task):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_runner_on_ok(self, result, **kwargs):
self.loganalytics.send_event(
diff --git a/ansible_collections/community/general/plugins/callback/logstash.py b/ansible_collections/community/general/plugins/callback/logstash.py
index 144e1f991..f3725e465 100644
--- a/ansible_collections/community/general/plugins/callback/logstash.py
+++ b/ansible_collections/community/general/plugins/callback/logstash.py
@@ -99,7 +99,6 @@ from ansible import context
import socket
import uuid
import logging
-from datetime import datetime
try:
import logstash
@@ -109,6 +108,10 @@ except ImportError:
from ansible.plugins.callback import CallbackBase
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
class CallbackModule(CallbackBase):
@@ -126,7 +129,7 @@ class CallbackModule(CallbackBase):
"pip install python-logstash for Python 2"
"pip install python3-logstash for Python 3")
- self.start_time = datetime.utcnow()
+ self.start_time = now()
def _init_plugin(self):
if not self.disabled:
@@ -185,7 +188,7 @@ class CallbackModule(CallbackBase):
self.logger.info("ansible start", extra=data)
def v2_playbook_on_stats(self, stats):
- end_time = datetime.utcnow()
+ end_time = now()
runtime = end_time - self.start_time
summarize_stat = {}
for host in stats.processed.keys():
diff --git a/ansible_collections/community/general/plugins/callback/splunk.py b/ansible_collections/community/general/plugins/callback/splunk.py
index d15547f44..a3e401bc2 100644
--- a/ansible_collections/community/general/plugins/callback/splunk.py
+++ b/ansible_collections/community/general/plugins/callback/splunk.py
@@ -88,13 +88,16 @@ import uuid
import socket
import getpass
-from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
class SplunkHTTPCollectorSource(object):
def __init__(self):
@@ -134,7 +137,7 @@ class SplunkHTTPCollectorSource(object):
else:
time_format = '%Y-%m-%d %H:%M:%S +0000'
- data['timestamp'] = datetime.utcnow().strftime(time_format)
+ data['timestamp'] = now().strftime(time_format)
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
@@ -181,7 +184,7 @@ class CallbackModule(CallbackBase):
def _runtime(self, result):
return (
- datetime.utcnow() -
+ now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
@@ -220,10 +223,10 @@ class CallbackModule(CallbackBase):
self.splunk.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_playbook_on_handler_task_start(self, task):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_runner_on_ok(self, result, **kwargs):
self.splunk.send_event(
diff --git a/ansible_collections/community/general/plugins/callback/sumologic.py b/ansible_collections/community/general/plugins/callback/sumologic.py
index 46ab3f0f7..0304b9de5 100644
--- a/ansible_collections/community/general/plugins/callback/sumologic.py
+++ b/ansible_collections/community/general/plugins/callback/sumologic.py
@@ -46,13 +46,16 @@ import uuid
import socket
import getpass
-from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
class SumologicHTTPCollectorSource(object):
def __init__(self):
@@ -84,8 +87,7 @@ class SumologicHTTPCollectorSource(object):
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
- data['timestamp'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S '
- '+0000')
+ data['timestamp'] = now().strftime('%Y-%m-%d %H:%M:%S +0000')
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
@@ -123,7 +125,7 @@ class CallbackModule(CallbackBase):
def _runtime(self, result):
return (
- datetime.utcnow() -
+ now() -
self.start_datetimes[result._task._uuid]
).total_seconds()
@@ -144,10 +146,10 @@ class CallbackModule(CallbackBase):
self.sumologic.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_playbook_on_handler_task_start(self, task):
- self.start_datetimes[task._uuid] = datetime.utcnow()
+ self.start_datetimes[task._uuid] = now()
def v2_runner_on_ok(self, result, **kwargs):
self.sumologic.send_event(
diff --git a/ansible_collections/community/general/plugins/filter/from_ini.py b/ansible_collections/community/general/plugins/filter/from_ini.py
index d68b51092..6fe83875e 100644
--- a/ansible_collections/community/general/plugins/filter/from_ini.py
+++ b/ansible_collections/community/general/plugins/filter/from_ini.py
@@ -57,7 +57,7 @@ class IniParser(ConfigParser):
''' Implements a configparser which is able to return a dict '''
def __init__(self):
- super().__init__()
+ super().__init__(interpolation=None)
self.optionxform = str
def as_dict(self):
diff --git a/ansible_collections/community/general/plugins/filter/to_ini.py b/ansible_collections/community/general/plugins/filter/to_ini.py
index 22ef16d72..bdf2dde27 100644
--- a/ansible_collections/community/general/plugins/filter/to_ini.py
+++ b/ansible_collections/community/general/plugins/filter/to_ini.py
@@ -63,7 +63,7 @@ class IniParser(ConfigParser):
''' Implements a configparser which sets the correct optionxform '''
def __init__(self):
- super().__init__()
+ super().__init__(interpolation=None)
self.optionxform = str
diff --git a/ansible_collections/community/general/plugins/inventory/cobbler.py b/ansible_collections/community/general/plugins/inventory/cobbler.py
index 8ca36f426..cdef9944a 100644
--- a/ansible_collections/community/general/plugins/inventory/cobbler.py
+++ b/ansible_collections/community/general/plugins/inventory/cobbler.py
@@ -117,7 +117,8 @@ from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_text
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, to_safe_group_name
from ansible.module_utils.six import text_type
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
# xmlrpc
try:
diff --git a/ansible_collections/community/general/plugins/inventory/gitlab_runners.py b/ansible_collections/community/general/plugins/inventory/gitlab_runners.py
index 536f4bb1b..bd29e8d31 100644
--- a/ansible_collections/community/general/plugins/inventory/gitlab_runners.py
+++ b/ansible_collections/community/general/plugins/inventory/gitlab_runners.py
@@ -83,7 +83,8 @@ keyed_groups:
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.common.text.converters import to_native
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
try:
import gitlab
diff --git a/ansible_collections/community/general/plugins/inventory/icinga2.py b/ansible_collections/community/general/plugins/inventory/icinga2.py
index 6746bb8e0..d1f2bc617 100644
--- a/ansible_collections/community/general/plugins/inventory/icinga2.py
+++ b/ansible_collections/community/general/plugins/inventory/icinga2.py
@@ -102,7 +102,8 @@ from ansible.errors import AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.module_utils.urls import open_url
from ansible.module_utils.six.moves.urllib.error import HTTPError
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
class InventoryModule(BaseInventoryPlugin, Constructable):
diff --git a/ansible_collections/community/general/plugins/inventory/linode.py b/ansible_collections/community/general/plugins/inventory/linode.py
index fc79f12c5..e161e086e 100644
--- a/ansible_collections/community/general/plugins/inventory/linode.py
+++ b/ansible_collections/community/general/plugins/inventory/linode.py
@@ -122,7 +122,8 @@ compose:
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
try:
diff --git a/ansible_collections/community/general/plugins/inventory/lxd.py b/ansible_collections/community/general/plugins/inventory/lxd.py
index c803f47dd..cf64f4ee8 100644
--- a/ansible_collections/community/general/plugins/inventory/lxd.py
+++ b/ansible_collections/community/general/plugins/inventory/lxd.py
@@ -175,7 +175,7 @@ from ansible.module_utils.six import raise_from
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible_collections.community.general.plugins.module_utils.lxd import LXDClient, LXDClientException
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
try:
import ipaddress
diff --git a/ansible_collections/community/general/plugins/inventory/nmap.py b/ansible_collections/community/general/plugins/inventory/nmap.py
index 3a28007a3..2ca474a1f 100644
--- a/ansible_collections/community/general/plugins/inventory/nmap.py
+++ b/ansible_collections/community/general/plugins/inventory/nmap.py
@@ -126,7 +126,8 @@ from ansible.errors import AnsibleParserError
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.module_utils.common.process import get_bin_path
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
diff --git a/ansible_collections/community/general/plugins/inventory/online.py b/ansible_collections/community/general/plugins/inventory/online.py
index b3a9ecd37..9355d9d41 100644
--- a/ansible_collections/community/general/plugins/inventory/online.py
+++ b/ansible_collections/community/general/plugins/inventory/online.py
@@ -68,7 +68,8 @@ from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.ansible_release import __version__ as ansible_version
from ansible.module_utils.six.moves.urllib.parse import urljoin
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
class InventoryModule(BaseInventoryPlugin):
diff --git a/ansible_collections/community/general/plugins/inventory/opennebula.py b/ansible_collections/community/general/plugins/inventory/opennebula.py
index 3babfa232..b097307c3 100644
--- a/ansible_collections/community/general/plugins/inventory/opennebula.py
+++ b/ansible_collections/community/general/plugins/inventory/opennebula.py
@@ -97,7 +97,8 @@ except ImportError:
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.module_utils.common.text.converters import to_native
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
from collections import namedtuple
import os
diff --git a/ansible_collections/community/general/plugins/inventory/proxmox.py b/ansible_collections/community/general/plugins/inventory/proxmox.py
index ed55ef1b6..774833c48 100644
--- a/ansible_collections/community/general/plugins/inventory/proxmox.py
+++ b/ansible_collections/community/general/plugins/inventory/proxmox.py
@@ -226,9 +226,9 @@ from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.utils.display import Display
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
# 3rd party imports
try:
diff --git a/ansible_collections/community/general/plugins/inventory/scaleway.py b/ansible_collections/community/general/plugins/inventory/scaleway.py
index 601129f56..dc24a17da 100644
--- a/ansible_collections/community/general/plugins/inventory/scaleway.py
+++ b/ansible_collections/community/general/plugins/inventory/scaleway.py
@@ -121,10 +121,10 @@ else:
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible_collections.community.general.plugins.module_utils.scaleway import SCALEWAY_LOCATION, parse_pagination_link
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
from ansible.module_utils.urls import open_url
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.six import raise_from
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
import ansible.module_utils.six.moves.urllib.parse as urllib_parse
diff --git a/ansible_collections/community/general/plugins/inventory/stackpath_compute.py b/ansible_collections/community/general/plugins/inventory/stackpath_compute.py
index 9a556d39e..6b48a49f1 100644
--- a/ansible_collections/community/general/plugins/inventory/stackpath_compute.py
+++ b/ansible_collections/community/general/plugins/inventory/stackpath_compute.py
@@ -72,7 +72,8 @@ from ansible.plugins.inventory import (
Cacheable
)
from ansible.utils.display import Display
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
display = Display()
diff --git a/ansible_collections/community/general/plugins/inventory/virtualbox.py b/ansible_collections/community/general/plugins/inventory/virtualbox.py
index 8604808e1..79b04ec72 100644
--- a/ansible_collections/community/general/plugins/inventory/virtualbox.py
+++ b/ansible_collections/community/general/plugins/inventory/virtualbox.py
@@ -62,7 +62,8 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.module_utils.common.process import get_bin_path
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
+
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
diff --git a/ansible_collections/community/general/plugins/inventory/xen_orchestra.py b/ansible_collections/community/general/plugins/inventory/xen_orchestra.py
index 96dd99770..4094af246 100644
--- a/ansible_collections/community/general/plugins/inventory/xen_orchestra.py
+++ b/ansible_collections/community/general/plugins/inventory/xen_orchestra.py
@@ -82,9 +82,9 @@ from time import sleep
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
-from ansible.utils.unsafe_proxy import wrap_var as make_unsafe
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
+from ansible_collections.community.general.plugins.plugin_utils.unsafe import make_unsafe
# 3rd party imports
try:
diff --git a/ansible_collections/community/general/plugins/lookup/bitwarden.py b/ansible_collections/community/general/plugins/lookup/bitwarden.py
index 2cb2d19a1..7584cd98a 100644
--- a/ansible_collections/community/general/plugins/lookup/bitwarden.py
+++ b/ansible_collections/community/general/plugins/lookup/bitwarden.py
@@ -29,7 +29,7 @@ DOCUMENTATION = """
- Field to retrieve, for example V(name) or V(id).
- If set to V(id), only zero or one element can be returned.
Use the Jinja C(first) filter to get the only list element.
- - When O(collection_id) is set, this field can be undefined to retrieve the whole collection records.
+ - If set to V(None) or V(''), or if O(_terms) is empty, records are not filtered by fields.
type: str
default: name
version_added: 5.7.0
@@ -40,6 +40,10 @@ DOCUMENTATION = """
description: Collection ID to filter results by collection. Leave unset to skip filtering.
type: str
version_added: 6.3.0
+ organization_id:
+ description: Organization ID to filter results by organization. Leave unset to skip filtering.
+ type: str
+ version_added: 8.5.0
bw_session:
description: Pass session key instead of reading from env.
type: str
@@ -142,45 +146,44 @@ class Bitwarden(object):
raise BitwardenException(err)
return to_text(out, errors='surrogate_or_strict'), to_text(err, errors='surrogate_or_strict')
- def _get_matches(self, search_value, search_field, collection_id=None):
+ def _get_matches(self, search_value, search_field, collection_id=None, organization_id=None):
"""Return matching records whose search_field is equal to key.
"""
# Prepare set of params for Bitwarden CLI
- if search_value:
- if search_field == 'id':
- params = ['get', 'item', search_value]
- else:
- params = ['list', 'items', '--search', search_value]
- if collection_id:
- params.extend(['--collectionid', collection_id])
+ if search_field == 'id':
+ params = ['get', 'item', search_value]
else:
- if not collection_id:
- raise AnsibleError("search_value is required if collection_id is not set.")
+ params = ['list', 'items']
+ if search_value:
+ params.extend(['--search', search_value])
- params = ['list', 'items', '--collectionid', collection_id]
+ if collection_id:
+ params.extend(['--collectionid', collection_id])
+ if organization_id:
+ params.extend(['--organizationid', organization_id])
out, err = self._run(params)
# This includes things that matched in different fields.
initial_matches = AnsibleJSONDecoder().raw_decode(out)[0]
- if search_field == 'id' or not search_value:
+ if search_field == 'id':
if initial_matches is None:
initial_matches = []
else:
initial_matches = [initial_matches]
# Filter to only include results from the right field.
- return [item for item in initial_matches if item[search_field] == search_value]
+ return [item for item in initial_matches if not search_value or item[search_field] == search_value]
- def get_field(self, field, search_value=None, search_field="name", collection_id=None):
+ def get_field(self, field, search_value, search_field="name", collection_id=None, organization_id=None):
"""Return a list of the specified field for records whose search_field match search_value
and filtered by collection if collection has been provided.
If field is None, return the whole record for each match.
"""
- matches = self._get_matches(search_value, search_field, collection_id)
+ matches = self._get_matches(search_value, search_field, collection_id, organization_id)
if not field:
return matches
field_matches = []
@@ -215,15 +218,16 @@ class LookupModule(LookupBase):
field = self.get_option('field')
search_field = self.get_option('search')
collection_id = self.get_option('collection_id')
+ organization_id = self.get_option('organization_id')
_bitwarden.session = self.get_option('bw_session')
if not _bitwarden.unlocked:
raise AnsibleError("Bitwarden Vault locked. Run 'bw unlock'.")
if not terms:
- return [_bitwarden.get_field(field, None, search_field, collection_id)]
+ terms = [None]
- return [_bitwarden.get_field(field, term, search_field, collection_id) for term in terms]
+ return [_bitwarden.get_field(field, term, search_field, collection_id, organization_id) for term in terms]
_bitwarden = Bitwarden()
diff --git a/ansible_collections/community/general/plugins/lookup/bitwarden_secrets_manager.py b/ansible_collections/community/general/plugins/lookup/bitwarden_secrets_manager.py
index 2d6706bee..8cabc693f 100644
--- a/ansible_collections/community/general/plugins/lookup/bitwarden_secrets_manager.py
+++ b/ansible_collections/community/general/plugins/lookup/bitwarden_secrets_manager.py
@@ -70,6 +70,7 @@ RETURN = """
"""
from subprocess import Popen, PIPE
+from time import sleep
from ansible.errors import AnsibleLookupError
from ansible.module_utils.common.text.converters import to_text
@@ -84,11 +85,29 @@ class BitwardenSecretsManagerException(AnsibleLookupError):
class BitwardenSecretsManager(object):
def __init__(self, path='bws'):
self._cli_path = path
+ self._max_retries = 3
+ self._retry_delay = 1
@property
def cli_path(self):
return self._cli_path
+ def _run_with_retry(self, args, stdin=None, retries=0):
+ out, err, rc = self._run(args, stdin)
+
+ if rc != 0:
+ if retries >= self._max_retries:
+ raise BitwardenSecretsManagerException("Max retries exceeded. Unable to retrieve secret.")
+
+ if "Too many requests" in err:
+ delay = self._retry_delay * (2 ** retries)
+ sleep(delay)
+ return self._run_with_retry(args, stdin, retries + 1)
+ else:
+ raise BitwardenSecretsManagerException(f"Command failed with return code {rc}: {err}")
+
+ return out, err, rc
+
def _run(self, args, stdin=None):
p = Popen([self.cli_path] + args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
out, err = p.communicate(stdin)
@@ -107,7 +126,7 @@ class BitwardenSecretsManager(object):
'get', 'secret', secret_id
]
- out, err, rc = self._run(params)
+ out, err, rc = self._run_with_retry(params)
if rc != 0:
raise BitwardenSecretsManagerException(to_text(err))
diff --git a/ansible_collections/community/general/plugins/lookup/passwordstore.py b/ansible_collections/community/general/plugins/lookup/passwordstore.py
index 7a6fca7a0..9814fe133 100644
--- a/ansible_collections/community/general/plugins/lookup/passwordstore.py
+++ b/ansible_collections/community/general/plugins/lookup/passwordstore.py
@@ -139,6 +139,21 @@ DOCUMENTATION = '''
type: bool
default: true
version_added: 8.1.0
+ missing_subkey:
+ description:
+ - Preference about what to do if the password subkey is missing.
+ - If set to V(error), the lookup will error out if the subkey does not exist.
+ - If set to V(empty) or V(warn), will return a V(none) in case the subkey does not exist.
+ version_added: 8.6.0
+ type: str
+ default: empty
+ choices:
+ - error
+ - warn
+ - empty
+ ini:
+ - section: passwordstore_lookup
+ key: missing_subkey
notes:
- The lookup supports passing all options as lookup parameters since community.general 6.0.0.
'''
@@ -147,6 +162,7 @@ ansible.cfg: |
[passwordstore_lookup]
lock=readwrite
locktimeout=45s
+ missing_subkey=warn
tasks.yml: |
---
@@ -432,6 +448,20 @@ class LookupModule(LookupBase):
if self.paramvals['subkey'] in self.passdict:
return self.passdict[self.paramvals['subkey']]
else:
+ if self.paramvals["missing_subkey"] == "error":
+ raise AnsibleError(
+ "passwordstore: subkey {0} for passname {1} not found and missing_subkey=error is set".format(
+ self.paramvals["subkey"], self.passname
+ )
+ )
+
+ if self.paramvals["missing_subkey"] == "warn":
+ display.warning(
+ "passwordstore: subkey {0} for passname {1} not found".format(
+ self.paramvals["subkey"], self.passname
+ )
+ )
+
return None
@contextmanager
@@ -481,6 +511,7 @@ class LookupModule(LookupBase):
'umask': self.get_option('umask'),
'timestamp': self.get_option('timestamp'),
'preserve': self.get_option('preserve'),
+ "missing_subkey": self.get_option("missing_subkey"),
}
def run(self, terms, variables, **kwargs):
diff --git a/ansible_collections/community/general/plugins/module_utils/datetime.py b/ansible_collections/community/general/plugins/module_utils/datetime.py
new file mode 100644
index 000000000..c7899f68d
--- /dev/null
+++ b/ansible_collections/community/general/plugins/module_utils/datetime.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2023 Felix Fontein <felix@fontein.de>
+# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
+# SPDX-License-Identifier: BSD-2-Clause
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import datetime as _datetime
+import sys
+
+
+_USE_TIMEZONE = sys.version_info >= (3, 6)
+
+
+def ensure_timezone_info(value):
+ if not _USE_TIMEZONE or value.tzinfo is not None:
+ return value
+ return value.astimezone(_datetime.timezone.utc)
+
+
+def fromtimestamp(value):
+ if _USE_TIMEZONE:
+ return _datetime.fromtimestamp(value, tz=_datetime.timezone.utc)
+ return _datetime.utcfromtimestamp(value)
+
+
+def now():
+ if _USE_TIMEZONE:
+ return _datetime.datetime.now(tz=_datetime.timezone.utc)
+ return _datetime.datetime.utcnow()
diff --git a/ansible_collections/community/general/plugins/module_utils/gitlab.py b/ansible_collections/community/general/plugins/module_utils/gitlab.py
index f9872b877..b1354d8a9 100644
--- a/ansible_collections/community/general/plugins/module_utils/gitlab.py
+++ b/ansible_collections/community/general/plugins/module_utils/gitlab.py
@@ -81,16 +81,23 @@ def find_group(gitlab_instance, identifier):
return group
-def ensure_gitlab_package(module):
+def ensure_gitlab_package(module, min_version=None):
if not HAS_GITLAB_PACKAGE:
module.fail_json(
msg=missing_required_lib("python-gitlab", url='https://python-gitlab.readthedocs.io/en/stable/'),
exception=GITLAB_IMP_ERR
)
+ gitlab_version = gitlab.__version__
+ if min_version is not None and LooseVersion(gitlab_version) < LooseVersion(min_version):
+ module.fail_json(
+ msg="This module requires python-gitlab Python module >= %s "
+ "(installed version: %s). Please upgrade python-gitlab to version %s or above."
+ % (min_version, gitlab_version, min_version)
+ )
-def gitlab_authentication(module):
- ensure_gitlab_package(module)
+def gitlab_authentication(module, min_version=None):
+ ensure_gitlab_package(module, min_version=min_version)
gitlab_url = module.params['api_url']
validate_certs = module.params['validate_certs']
diff --git a/ansible_collections/community/general/plugins/module_utils/identity/keycloak/keycloak.py b/ansible_collections/community/general/plugins/module_utils/identity/keycloak/keycloak.py
index 9e1c3f4d9..b2a189250 100644
--- a/ansible_collections/community/general/plugins/module_utils/identity/keycloak/keycloak.py
+++ b/ansible_collections/community/general/plugins/module_utils/identity/keycloak/keycloak.py
@@ -28,6 +28,9 @@ URL_CLIENT_ROLES = "{url}/admin/realms/{realm}/clients/{id}/roles"
URL_CLIENT_ROLE = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}"
URL_CLIENT_ROLE_COMPOSITES = "{url}/admin/realms/{realm}/clients/{id}/roles/{name}/composites"
+URL_CLIENT_ROLE_SCOPE_CLIENTS = "{url}/admin/realms/{realm}/clients/{id}/scope-mappings/clients/{scopeid}"
+URL_CLIENT_ROLE_SCOPE_REALM = "{url}/admin/realms/{realm}/clients/{id}/scope-mappings/realm"
+
URL_REALM_ROLES = "{url}/admin/realms/{realm}/roles"
URL_REALM_ROLE = "{url}/admin/realms/{realm}/roles/{name}"
URL_REALM_ROLEMAPPINGS = "{url}/admin/realms/{realm}/users/{id}/role-mappings/realm"
@@ -3049,6 +3052,105 @@ class KeycloakAPI(object):
except Exception:
return False
+ def get_client_role_scope_from_client(self, clientid, clientscopeid, realm="master"):
+ """ Fetch the roles associated with the client's scope for a specific client on the Keycloak server.
+ :param clientid: ID of the client from which to obtain the associated roles.
+ :param clientscopeid: ID of the client who owns the roles.
+ :param realm: Realm from which to obtain the scope.
+ :return: The client scope of roles from specified client.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid)
+ try:
+ return json.loads(to_native(open_url(client_role_scope_url, method='GET', http_agent=self.http_agent, headers=self.restheaders,
+ timeout=self.connection_timeout,
+ validate_certs=self.validate_certs).read()))
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not fetch roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ def update_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm="master"):
+ """ Update and fetch the roles associated with the client's scope on the Keycloak server.
+ :param payload: List of roles to be added to the scope.
+ :param clientid: ID of the client to update scope.
+ :param clientscopeid: ID of the client who owns the roles.
+ :param realm: Realm from which to obtain the clients.
+ :return: The client scope of roles from specified client.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid)
+ try:
+ open_url(client_role_scope_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout,
+ data=json.dumps(payload), validate_certs=self.validate_certs)
+
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not update roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ return self.get_client_role_scope_from_client(clientid, clientscopeid, realm)
+
+ def delete_client_role_scope_from_client(self, payload, clientid, clientscopeid, realm="master"):
+ """ Delete the roles contains in the payload from the client's scope on the Keycloak server.
+ :param payload: List of roles to be deleted.
+ :param clientid: ID of the client to delete roles from scope.
+ :param clientscopeid: ID of the client who owns the roles.
+ :param realm: Realm from which to obtain the clients.
+ :return: The client scope of roles from specified client.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_CLIENTS.format(url=self.baseurl, realm=realm, id=clientid, scopeid=clientscopeid)
+ try:
+ open_url(client_role_scope_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout,
+ data=json.dumps(payload), validate_certs=self.validate_certs)
+
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not delete roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ return self.get_client_role_scope_from_client(clientid, clientscopeid, realm)
+
+ def get_client_role_scope_from_realm(self, clientid, realm="master"):
+ """ Fetch the realm roles from the client's scope on the Keycloak server.
+ :param clientid: ID of the client from which to obtain the associated realm roles.
+ :param realm: Realm from which to obtain the clients.
+ :return: The client realm roles scope.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid)
+ try:
+ return json.loads(to_native(open_url(client_role_scope_url, method='GET', http_agent=self.http_agent, headers=self.restheaders,
+ timeout=self.connection_timeout,
+ validate_certs=self.validate_certs).read()))
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not fetch roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ def update_client_role_scope_from_realm(self, payload, clientid, realm="master"):
+ """ Update and fetch the realm roles from the client's scope on the Keycloak server.
+ :param payload: List of realm roles to add.
+ :param clientid: ID of the client to update scope.
+ :param realm: Realm from which to obtain the clients.
+ :return: The client realm roles scope.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid)
+ try:
+ open_url(client_role_scope_url, method='POST', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout,
+ data=json.dumps(payload), validate_certs=self.validate_certs)
+
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not update roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ return self.get_client_role_scope_from_realm(clientid, realm)
+
+ def delete_client_role_scope_from_realm(self, payload, clientid, realm="master"):
+ """ Delete the realm roles contains in the payload from the client's scope on the Keycloak server.
+ :param payload: List of realm roles to delete.
+ :param clientid: ID of the client to delete roles from scope.
+ :param realm: Realm from which to obtain the clients.
+ :return: The client realm roles scope.
+ """
+ client_role_scope_url = URL_CLIENT_ROLE_SCOPE_REALM.format(url=self.baseurl, realm=realm, id=clientid)
+ try:
+ open_url(client_role_scope_url, method='DELETE', http_agent=self.http_agent, headers=self.restheaders, timeout=self.connection_timeout,
+ data=json.dumps(payload), validate_certs=self.validate_certs)
+
+ except Exception as e:
+ self.fail_open_url(e, msg='Could not delete roles scope for client %s in realm %s: %s' % (clientid, realm, str(e)))
+
+ return self.get_client_role_scope_from_realm(clientid, realm)
+
def fail_open_url(self, e, msg, **kwargs):
try:
if isinstance(e, HTTPError):
diff --git a/ansible_collections/community/general/plugins/module_utils/ipa.py b/ansible_collections/community/general/plugins/module_utils/ipa.py
index eda9b4132..fb63d5556 100644
--- a/ansible_collections/community/general/plugins/module_utils/ipa.py
+++ b/ansible_collections/community/general/plugins/module_utils/ipa.py
@@ -104,7 +104,7 @@ class IPAClient(object):
def get_ipa_version(self):
response = self.ping()['summary']
- ipa_ver_regex = re.compile(r'IPA server version (\d\.\d\.\d).*')
+ ipa_ver_regex = re.compile(r'IPA server version (\d+\.\d+\.\d+).*')
version_match = ipa_ver_regex.match(response)
ipa_version = None
if version_match:
diff --git a/ansible_collections/community/general/plugins/module_utils/puppet.py b/ansible_collections/community/general/plugins/module_utils/puppet.py
index 8d553a2d2..f05b0673f 100644
--- a/ansible_collections/community/general/plugins/module_utils/puppet.py
+++ b/ansible_collections/community/general/plugins/module_utils/puppet.py
@@ -107,5 +107,6 @@ def puppet_runner(module):
verbose=cmd_runner_fmt.as_bool("--verbose"),
),
check_rc=False,
+ force_lang=module.params["environment_lang"],
)
return runner
diff --git a/ansible_collections/community/general/plugins/module_utils/redfish_utils.py b/ansible_collections/community/general/plugins/module_utils/redfish_utils.py
index 4c2057129..6935573d0 100644
--- a/ansible_collections/community/general/plugins/module_utils/redfish_utils.py
+++ b/ansible_collections/community/general/plugins/module_utils/redfish_utils.py
@@ -1149,6 +1149,54 @@ class RedfishUtils(object):
return response
return {'ret': True, 'changed': True}
+ def manager_reset_to_defaults(self, command):
+ return self.reset_to_defaults(command, self.manager_uri,
+ '#Manager.ResetToDefaults')
+
+ def reset_to_defaults(self, command, resource_uri, action_name):
+ key = "Actions"
+ reset_type_values = ['ResetAll',
+ 'PreserveNetworkAndUsers',
+ 'PreserveNetwork']
+
+ if command not in reset_type_values:
+ return {'ret': False, 'msg': 'Invalid Command (%s)' % command}
+
+ # read the resource and get the current power state
+ response = self.get_request(self.root_uri + resource_uri)
+ if response['ret'] is False:
+ return response
+ data = response['data']
+
+ # get the reset Action and target URI
+ if key not in data or action_name not in data[key]:
+ return {'ret': False, 'msg': 'Action %s not found' % action_name}
+ reset_action = data[key][action_name]
+ if 'target' not in reset_action:
+ return {'ret': False,
+ 'msg': 'target URI missing from Action %s' % action_name}
+ action_uri = reset_action['target']
+
+ # get AllowableValues
+ ai = self._get_all_action_info_values(reset_action)
+ allowable_values = ai.get('ResetType', {}).get('AllowableValues', [])
+
+ # map ResetType to an allowable value if needed
+ if allowable_values and command not in allowable_values:
+ return {'ret': False,
+ 'msg': 'Specified reset type (%s) not supported '
+ 'by service. Supported types: %s' %
+ (command, allowable_values)}
+
+ # define payload
+ payload = {'ResetType': command}
+
+ # POST to Action URI
+ response = self.post_request(self.root_uri + action_uri, payload)
+ if response['ret'] is False:
+ return response
+ return {'ret': True, 'changed': True}
+
def _find_account_uri(self, username=None, acct_id=None):
if not any((username, acct_id)):
return {'ret': False, 'msg':
@@ -1549,6 +1597,8 @@ class RedfishUtils(object):
data = response['data']
+ result['multipart_supported'] = 'MultipartHttpPushUri' in data
+
if "Actions" in data:
actions = data['Actions']
if len(actions) > 0:
diff --git a/ansible_collections/community/general/plugins/module_utils/scaleway.py b/ansible_collections/community/general/plugins/module_utils/scaleway.py
index 67b821103..1310ba560 100644
--- a/ansible_collections/community/general/plugins/module_utils/scaleway.py
+++ b/ansible_collections/community/general/plugins/module_utils/scaleway.py
@@ -17,6 +17,10 @@ from ansible.module_utils.basic import env_fallback, missing_required_lib
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import urlencode
+from ansible_collections.community.general.plugins.module_utils.datetime import (
+ now,
+)
+
SCALEWAY_SECRET_IMP_ERR = None
try:
from passlib.hash import argon2
@@ -306,10 +310,10 @@ class Scaleway(object):
# Prevent requesting the resource status too soon
time.sleep(wait_sleep_time)
- start = datetime.datetime.utcnow()
+ start = now()
end = start + datetime.timedelta(seconds=wait_timeout)
- while datetime.datetime.utcnow() < end:
+ while now() < end:
self.module.debug("We are going to wait for the resource to finish its transition")
state = self.fetch_state(resource)
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
diff --git a/ansible_collections/community/general/plugins/plugin_utils/unsafe.py b/ansible_collections/community/general/plugins/plugin_utils/unsafe.py
new file mode 100644
index 000000000..1eb61bea0
--- /dev/null
+++ b/ansible_collections/community/general/plugins/plugin_utils/unsafe.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
+# 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
+
+import re
+
+from ansible.module_utils.six import binary_type, text_type
+from ansible.module_utils.common._collections_compat import Mapping, Set
+from ansible.module_utils.common.collections import is_sequence
+from ansible.utils.unsafe_proxy import (
+ AnsibleUnsafe,
+ wrap_var as _make_unsafe,
+)
+
+_RE_TEMPLATE_CHARS = re.compile(u'[{}]')
+_RE_TEMPLATE_CHARS_BYTES = re.compile(b'[{}]')
+
+
+def make_unsafe(value):
+ if value is None or isinstance(value, AnsibleUnsafe):
+ return value
+
+ if isinstance(value, Mapping):
+ return dict((make_unsafe(key), make_unsafe(val)) for key, val in value.items())
+ elif isinstance(value, Set):
+ return set(make_unsafe(elt) for elt in value)
+ elif is_sequence(value):
+ return type(value)(make_unsafe(elt) for elt in value)
+ elif isinstance(value, binary_type):
+ if _RE_TEMPLATE_CHARS_BYTES.search(value):
+ value = _make_unsafe(value)
+ return value
+ elif isinstance(value, text_type):
+ if _RE_TEMPLATE_CHARS.search(value):
+ value = _make_unsafe(value)
+ return value
+
+ return value