summaryrefslogtreecommitdiffstats
path: root/ansible_collections/openstack/cloud/plugins/module_utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/openstack/cloud/plugins/module_utils
parentInitial commit. (diff)
downloadansible-66cec45960ce1d9c794e9399de15c138acb18aed.tar.xz
ansible-66cec45960ce1d9c794e9399de15c138acb18aed.zip
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/openstack/cloud/plugins/module_utils')
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/__init__.py0
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/ironic.py68
-rw-r--r--ansible_collections/openstack/cloud/plugins/module_utils/openstack.py470
3 files changed, 538 insertions, 0 deletions
diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py b/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ansible_collections/openstack/cloud/plugins/module_utils/__init__.py
diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py b/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py
new file mode 100644
index 00000000..a7ab19ef
--- /dev/null
+++ b/ansible_collections/openstack/cloud/plugins/module_utils/ironic.py
@@ -0,0 +1,68 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_full_argument_spec
+
+
+def ironic_argument_spec(**kwargs):
+ spec = dict(
+ auth_type=dict(required=False),
+ ironic_url=dict(required=False),
+ )
+ spec.update(kwargs)
+ return openstack_full_argument_spec(**spec)
+
+
+# TODO(dtantsur): inherit the collection's base module
+class IronicModule(AnsibleModule):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._update_ironic_auth()
+
+ def _update_ironic_auth(self):
+ """Validate and update authentication parameters for ironic."""
+ if (
+ self.params['auth_type'] in [None, 'None', 'none']
+ and self.params['ironic_url'] is None
+ and not self.params['cloud']
+ and not (self.params['auth']
+ and self.params['auth'].get('endpoint'))
+ ):
+ self.fail_json(msg=("Authentication appears to be disabled, "
+ "Please define either ironic_url, or cloud, "
+ "or auth.endpoint"))
+
+ if (
+ self.params['ironic_url']
+ and self.params['auth_type'] in [None, 'None', 'none']
+ and not (self.params['auth']
+ and self.params['auth'].get('endpoint'))
+ ):
+ self.params['auth'] = dict(
+ endpoint=self.params['ironic_url']
+ )
diff --git a/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py b/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py
new file mode 100644
index 00000000..8663d2fc
--- /dev/null
+++ b/ansible_collections/openstack/cloud/plugins/module_utils/openstack.py
@@ -0,0 +1,470 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Copyright 2019 Red Hat, Inc.
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import abc
+import copy
+from ansible.module_utils.six import raise_from
+try:
+ from ansible.module_utils.compat.version import StrictVersion
+except ImportError:
+ try:
+ from distutils.version import StrictVersion
+ except ImportError as exc:
+ raise_from(ImportError('To use this plugin or module with ansible-core'
+ ' < 2.11, you need to use Python < 3.12 with '
+ 'distutils.version present'), exc)
+import importlib
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import iteritems
+
+OVERRIDES = {'os_client_config': 'config',
+ 'os_endpoint': 'catalog_endpoint',
+ 'os_flavor': 'compute_flavor',
+ 'os_flavor_info': 'compute_flavor_info',
+ 'os_group': 'identity_group',
+ 'os_group_info': 'identity_group_info',
+ 'os_ironic': 'baremetal_node',
+ 'os_ironic_inspect': 'baremetal_inspect',
+ 'os_ironic_node': 'baremetal_node_action',
+ 'os_keystone_domain': 'identity_domain',
+ 'os_keystone_domain_info': 'identity_domain_info',
+ 'os_keystone_endpoint': 'endpoint',
+ 'os_keystone_identity_provider': 'federation_idp',
+ 'os_keystone_identity_provider_info': 'federation_idp_info',
+ 'os_keystone_mapping': 'federation_mapping',
+ 'os_keystone_mapping_info': 'federation_mapping_info',
+ 'os_keystone_role': 'identity_role',
+ 'os_keystone_service': 'catalog_service',
+ 'os_listener': 'lb_listener',
+ 'os_member': 'lb_member',
+ 'os_nova_flavor': 'compute_flavor',
+ 'os_nova_host_aggregate': 'host_aggregate',
+ 'os_pool': 'lb_pool',
+ 'os_user': 'identity_user',
+ 'os_user_group': 'group_assignment',
+ 'os_user_info': 'identity_user_info',
+ 'os_user_role': 'role_assignment',
+ 'os_zone': 'dns_zone'}
+
+CUSTOM_VAR_PARAMS = ['min_ver', 'max_ver']
+
+MINIMUM_SDK_VERSION = '0.36.0'
+MAXIMUM_SDK_VERSION = '0.98.999'
+
+
+def ensure_compatibility(version, min_version=None, max_version=None):
+ """ Raises ImportError if the specified version does not
+ meet the minimum and maximum version requirements"""
+
+ if min_version and MINIMUM_SDK_VERSION:
+ min_version = max(StrictVersion(MINIMUM_SDK_VERSION),
+ StrictVersion(min_version))
+ elif MINIMUM_SDK_VERSION:
+ min_version = StrictVersion(MINIMUM_SDK_VERSION)
+
+ if max_version and MAXIMUM_SDK_VERSION:
+ max_version = min(StrictVersion(MAXIMUM_SDK_VERSION),
+ StrictVersion(max_version))
+ elif MAXIMUM_SDK_VERSION:
+ max_version = StrictVersion(MAXIMUM_SDK_VERSION)
+
+ if min_version and StrictVersion(version) < min_version:
+ raise ImportError(
+ "Version MUST be >={min_version} and <={max_version}, but"
+ " {version} is smaller than minimum version {min_version}"
+ .format(version=version,
+ min_version=min_version,
+ max_version=max_version))
+
+ if max_version and StrictVersion(version) > max_version:
+ raise ImportError(
+ "Version MUST be >={min_version} and <={max_version}, but"
+ " {version} is larger than maximum version {max_version}"
+ .format(version=version,
+ min_version=min_version,
+ max_version=max_version))
+
+
+def openstack_argument_spec():
+ # DEPRECATED: This argument spec is only used for the deprecated old
+ # OpenStack modules. It turns out that modern OpenStack auth is WAY
+ # more complex than this.
+ # Consume standard OpenStack environment variables.
+ # This is mainly only useful for ad-hoc command line operation as
+ # in playbooks one would assume variables would be used appropriately
+ OS_AUTH_URL = os.environ.get('OS_AUTH_URL', 'http://127.0.0.1:35357/v2.0/')
+ OS_PASSWORD = os.environ.get('OS_PASSWORD', None)
+ OS_REGION_NAME = os.environ.get('OS_REGION_NAME', None)
+ OS_USERNAME = os.environ.get('OS_USERNAME', 'admin')
+ OS_TENANT_NAME = os.environ.get('OS_TENANT_NAME', OS_USERNAME)
+
+ spec = dict(
+ login_username=dict(default=OS_USERNAME),
+ auth_url=dict(default=OS_AUTH_URL),
+ region_name=dict(default=OS_REGION_NAME),
+ availability_zone=dict(),
+ )
+ if OS_PASSWORD:
+ spec['login_password'] = dict(default=OS_PASSWORD)
+ else:
+ spec['login_password'] = dict(required=True)
+ if OS_TENANT_NAME:
+ spec['login_tenant_name'] = dict(default=OS_TENANT_NAME)
+ else:
+ spec['login_tenant_name'] = dict(required=True)
+ return spec
+
+
+def openstack_find_nova_addresses(addresses, ext_tag, key_name=None):
+
+ ret = []
+ for (k, v) in iteritems(addresses):
+ if key_name and k == key_name:
+ ret.extend([addrs['addr'] for addrs in v])
+ else:
+ for interface_spec in v:
+ if 'OS-EXT-IPS:type' in interface_spec and interface_spec['OS-EXT-IPS:type'] == ext_tag:
+ ret.append(interface_spec['addr'])
+ return ret
+
+
+def openstack_full_argument_spec(**kwargs):
+ spec = dict(
+ cloud=dict(default=None, type='raw'),
+ auth_type=dict(default=None),
+ auth=dict(default=None, type='dict', no_log=True),
+ region_name=dict(default=None),
+ availability_zone=dict(default=None),
+ validate_certs=dict(default=None, type='bool', aliases=['verify']),
+ ca_cert=dict(default=None, aliases=['cacert']),
+ client_cert=dict(default=None, aliases=['cert']),
+ client_key=dict(default=None, no_log=True, aliases=['key']),
+ wait=dict(default=True, type='bool'),
+ timeout=dict(default=180, type='int'),
+ api_timeout=dict(default=None, type='int'),
+ interface=dict(
+ default='public', choices=['public', 'internal', 'admin'],
+ aliases=['endpoint_type']),
+ sdk_log_path=dict(default=None, type='str'),
+ sdk_log_level=dict(
+ default='INFO', type='str', choices=['INFO', 'DEBUG']),
+ )
+ # Filter out all our custom parameters before passing to AnsibleModule
+ kwargs_copy = copy.deepcopy(kwargs)
+ for v in kwargs_copy.values():
+ for c in CUSTOM_VAR_PARAMS:
+ v.pop(c, None)
+ spec.update(kwargs_copy)
+ return spec
+
+
+def openstack_module_kwargs(**kwargs):
+ ret = {}
+ for key in ('mutually_exclusive', 'required_together', 'required_one_of'):
+ if key in kwargs:
+ if key in ret:
+ ret[key].extend(kwargs[key])
+ else:
+ ret[key] = kwargs[key]
+ return ret
+
+
+# for compatibility with old versions
+def openstack_cloud_from_module(module, min_version=None, max_version=None):
+ try:
+ # Due to the name shadowing we should import other way
+ sdk = importlib.import_module('openstack')
+ except ImportError:
+ module.fail_json(msg='openstacksdk is required for this module')
+
+ try:
+ ensure_compatibility(sdk.version.__version__,
+ min_version, max_version)
+ except ImportError as e:
+ module.fail_json(
+ msg="Incompatible openstacksdk library found: {error}."
+ .format(error=str(e)))
+
+ cloud_config = module.params.pop('cloud', None)
+ try:
+ if isinstance(cloud_config, dict):
+ fail_message = (
+ "A cloud config dict was provided to the cloud parameter"
+ " but also a value was provided for {param}. If a cloud"
+ " config dict is provided, {param} should be"
+ " excluded.")
+ for param in (
+ 'auth', 'region_name', 'validate_certs',
+ 'ca_cert', 'client_key', 'api_timeout', 'auth_type'):
+ if module.params[param] is not None:
+ module.fail_json(msg=fail_message.format(param=param))
+ # For 'interface' parameter, fail if we receive a non-default value
+ if module.params['interface'] != 'public':
+ module.fail_json(msg=fail_message.format(param='interface'))
+ return sdk, sdk.connect(**cloud_config)
+ else:
+ return sdk, sdk.connect(
+ cloud=cloud_config,
+ auth_type=module.params['auth_type'],
+ auth=module.params['auth'],
+ region_name=module.params['region_name'],
+ verify=module.params['validate_certs'],
+ cacert=module.params['ca_cert'],
+ key=module.params['client_key'],
+ api_timeout=module.params['api_timeout'],
+ interface=module.params['interface'],
+ )
+ except sdk.exceptions.SDKException as e:
+ # Probably a cloud configuration/login error
+ module.fail_json(msg=str(e))
+
+
+class OpenStackModule:
+ """Openstack Module is a base class for all Openstack Module classes.
+
+ The class has `run` function that should be overriden in child classes,
+ the provided methods include:
+
+ Methods:
+ params: Dictionary of Ansible module parameters.
+ module_name: Module name (i.e. server_action)
+ sdk_version: Version of used OpenstackSDK.
+ results: Dictionary for return of Ansible module,
+ must include `changed` keyword.
+ exit, exit_json: Exit module and return data inside, must include
+ changed` keyword in a data.
+ fail, fail_json: Exit module with failure, has `msg` keyword to
+ specify a reason of failure.
+ conn: Connection to SDK object.
+ log: Print message to system log.
+ debug: Print debug message to system log, prints if Ansible Debug is
+ enabled or verbosity is more than 2.
+ check_deprecated_names: Function that checks if module was called with
+ a deprecated name and prints the correct name
+ with deprecation warning.
+ check_versioned: helper function to check that all arguments are known
+ in the current SDK version.
+ run: method that executes and shall be overriden in inherited classes.
+
+ Args:
+ deprecated_names: Should specify deprecated modules names for current
+ module.
+ argument_spec: Used for construction of Openstack common arguments.
+ module_kwargs: Additional arguments for Ansible Module.
+ """
+
+ deprecated_names = ()
+ argument_spec = {}
+ module_kwargs = {}
+ module_min_sdk_version = None
+ module_max_sdk_version = None
+
+ def __init__(self):
+ """Initialize Openstack base class.
+
+ Set up variables, connection to SDK and check if there are
+ deprecated names.
+ """
+ self.ansible = AnsibleModule(
+ openstack_full_argument_spec(**self.argument_spec),
+ **self.module_kwargs)
+ self.params = self.ansible.params
+ self.module_name = self.ansible._name
+ self.check_mode = self.ansible.check_mode
+ self.sdk_version = None
+ self.results = {'changed': False}
+ self.exit = self.exit_json = self.ansible.exit_json
+ self.fail = self.fail_json = self.ansible.fail_json
+ self.warn = self.ansible.warn
+ self.sdk, self.conn = self.openstack_cloud_from_module()
+ self.check_deprecated_names()
+ self.setup_sdk_logging()
+
+ def log(self, msg):
+ """Prints log message to system log.
+
+ Arguments:
+ msg {str} -- Log message
+ """
+ self.ansible.log(msg)
+
+ def debug(self, msg):
+ """Prints debug message to system log
+
+ Arguments:
+ msg {str} -- Debug message.
+ """
+ if self.ansible._debug or self.ansible._verbosity > 2:
+ self.ansible.log(
+ " ".join(['[DEBUG]', msg]))
+
+ def setup_sdk_logging(self):
+ log_path = self.params.get('sdk_log_path')
+ if log_path is not None:
+ log_level = self.params.get('sdk_log_level')
+ self.sdk.enable_logging(
+ debug=True if log_level == 'DEBUG' else False,
+ http_debug=True if log_level == 'DEBUG' else False,
+ path=log_path
+ )
+
+ def check_deprecated_names(self):
+ """Check deprecated module names if `deprecated_names` variable is set.
+ """
+ new_module_name = OVERRIDES.get(self.module_name)
+ if self.module_name in self.deprecated_names and new_module_name:
+ self.ansible.deprecate(
+ "The '%s' module has been renamed to '%s' in openstack "
+ "collection: openstack.cloud.%s" % (
+ self.module_name, new_module_name, new_module_name),
+ version='2.0.0', collection_name='openstack.cloud')
+
+ def openstack_cloud_from_module(self):
+ """Sets up connection to cloud using provided options. Checks if all
+ provided variables are supported for the used SDK version.
+ """
+ try:
+ # Due to the name shadowing we should import other way
+ sdk = importlib.import_module('openstack')
+ self.sdk_version = sdk.version.__version__
+ except ImportError:
+ self.fail_json(msg='openstacksdk is required for this module')
+
+ try:
+ ensure_compatibility(self.sdk_version,
+ self.module_min_sdk_version,
+ self.module_max_sdk_version)
+ except ImportError as e:
+ self.fail_json(
+ msg="Incompatible openstacksdk library found: {error}."
+ .format(error=str(e)))
+
+ # Fail if there are set unsupported for this version parameters
+ # New parameters should NOT use 'default' but rely on SDK defaults
+ for param in self.argument_spec:
+ if (self.params[param] is not None
+ and 'min_ver' in self.argument_spec[param]
+ and StrictVersion(self.sdk_version) < self.argument_spec[param]['min_ver']):
+ self.fail_json(
+ msg="To use parameter '{param}' with module '{module}', the installed version of "
+ "the openstacksdk library MUST be >={min_version}.".format(
+ min_version=self.argument_spec[param]['min_ver'],
+ param=param,
+ module=self.module_name))
+ if (self.params[param] is not None
+ and 'max_ver' in self.argument_spec[param]
+ and StrictVersion(self.sdk_version) > self.argument_spec[param]['max_ver']):
+ self.fail_json(
+ msg="To use parameter '{param}' with module '{module}', the installed version of "
+ "the openstacksdk library MUST be <={max_version}.".format(
+ max_version=self.argument_spec[param]['max_ver'],
+ param=param,
+ module=self.module_name))
+
+ cloud_config = self.params.pop('cloud', None)
+ if isinstance(cloud_config, dict):
+ fail_message = (
+ "A cloud config dict was provided to the cloud parameter"
+ " but also a value was provided for {param}. If a cloud"
+ " config dict is provided, {param} should be"
+ " excluded.")
+ for param in (
+ 'auth', 'region_name', 'validate_certs',
+ 'ca_cert', 'client_key', 'api_timeout', 'auth_type'):
+ if self.params[param] is not None:
+ self.fail_json(msg=fail_message.format(param=param))
+ # For 'interface' parameter, fail if we receive a non-default value
+ if self.params['interface'] != 'public':
+ self.fail_json(msg=fail_message.format(param='interface'))
+ else:
+ cloud_config = dict(
+ cloud=cloud_config,
+ auth_type=self.params['auth_type'],
+ auth=self.params['auth'],
+ region_name=self.params['region_name'],
+ verify=self.params['validate_certs'],
+ cacert=self.params['ca_cert'],
+ key=self.params['client_key'],
+ api_timeout=self.params['api_timeout'],
+ interface=self.params['interface'],
+ )
+ try:
+ return sdk, sdk.connect(**cloud_config)
+ except sdk.exceptions.SDKException as e:
+ # Probably a cloud configuration/login error
+ self.fail_json(msg=str(e))
+
+ # Filter out all arguments that are not from current SDK version
+ def check_versioned(self, **kwargs):
+ """Check that provided arguments are supported by current SDK version
+
+ Returns:
+ versioned_result {dict} dictionary of only arguments that are
+ supported by current SDK version. All others
+ are dropped.
+ """
+ versioned_result = {}
+ for var_name in kwargs:
+ if ('min_ver' in self.argument_spec[var_name]
+ and StrictVersion(self.sdk_version) < self.argument_spec[var_name]['min_ver']):
+ continue
+ if ('max_ver' in self.argument_spec[var_name]
+ and StrictVersion(self.sdk_version) > self.argument_spec[var_name]['max_ver']):
+ continue
+ versioned_result.update({var_name: kwargs[var_name]})
+ return versioned_result
+
+ @abc.abstractmethod
+ def run(self):
+ """Function for overriding in inhetired classes, it's executed by default.
+ """
+ pass
+
+ def __call__(self):
+ """Execute `run` function when calling the class.
+ """
+ try:
+ results = self.run()
+ if results and isinstance(results, dict):
+ self.ansible.exit_json(**results)
+ except self.sdk.exceptions.OpenStackCloudException as e:
+ params = {
+ 'msg': str(e),
+ 'extra_data': {
+ 'data': getattr(e, 'extra_data', 'None'),
+ 'details': getattr(e, 'details', 'None'),
+ 'response': getattr(getattr(e, 'response', ''),
+ 'text', 'None')
+ }
+ }
+ self.ansible.fail_json(**params)
+ # if we got to this place, modules didn't exit
+ self.ansible.exit_json(**self.results)