summaryrefslogtreecommitdiffstats
path: root/ansible_collections/purestorage/fusion/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/purestorage/fusion/plugins')
-rw-r--r--ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py56
-rw-r--r--ansible_collections/purestorage/fusion/plugins/inventory/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/errors.py291
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py183
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/getters.py99
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/networking.py76
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/operations.py42
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py75
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py162
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/startup.py26
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py139
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py265
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py162
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py312
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py88
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py1130
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py244
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py274
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py278
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py187
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py281
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py180
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py255
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py507
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py208
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py169
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py122
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py187
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py450
29 files changed, 6448 insertions, 0 deletions
diff --git a/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py b/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py
new file mode 100644
index 000000000..a2f933161
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Simon Dodsley <simon@purestorage.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ # Standard Pure Storage documentation fragment
+ DOCUMENTATION = r"""
+options:
+ - See separate platform section for more details
+requirements:
+ - See separate platform section for more details
+notes:
+ - Ansible modules are available for the following Pure Storage products: FlashArray, FlashBlade, Pure1, Fusion
+"""
+
+ # Documentation fragment for Fusion
+ FUSION = r"""
+options:
+ private_key_file:
+ aliases: [ key_file ]
+ description:
+ - Path to the private key file
+ - Defaults to the set environment variable under FUSION_PRIVATE_KEY_FILE.
+ type: str
+ private_key_password:
+ description:
+ - Password of the encrypted private key file
+ type: str
+ issuer_id:
+ aliases: [ app_id ]
+ description:
+ - Application ID from Pure1 Registration page
+ - eg. pure1:apikey:dssf2331sd
+ - Defaults to the set environment variable under FUSION_ISSUER_ID
+ type: str
+ access_token:
+ description:
+ - Access token for Fusion Service
+ - Defaults to the set environment variable under FUSION_ACCESS_TOKEN
+ type: str
+notes:
+ - This module requires the I(purefusion) Python library
+ - You must set C(FUSION_ISSUER_ID) and C(FUSION_PRIVATE_KEY_FILE) environment variables
+ if I(issuer_id) and I(private_key_file) arguments are not passed to the module directly
+ - If you want to use access token for authentication, you must use C(FUSION_ACCESS_TOKEN) environment variable
+ if I(access_token) argument is not passed to the module directly
+requirements:
+ - python >= 3.8
+ - purefusion
+"""
diff --git a/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py b/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py b/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py
new file mode 100644
index 000000000..0edf364cf
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py
@@ -0,0 +1,291 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion as purefusion
+ import urllib3
+except ImportError:
+ pass
+
+import sys
+import json
+import re
+import traceback as trace
+
+
+class OperationException(Exception):
+ """Raised if an asynchronous Operation fails."""
+
+ def __init__(self, op, http_error=None):
+ self._op = op
+ self._http_error = http_error
+
+ @property
+ def op(self):
+ return self._op
+
+ @property
+ def http_error(self):
+ return self._http_error
+
+
+def _get_verbosity(module):
+ # verbosity is a private member and Ansible does not really allow
+ # providing extra information only if the user wants it due to ideological
+ # reasons, so extract it as carefully as possible and assume non-verbose
+ # if something fails
+ try:
+ if module._verbosity is not None and isinstance(module._verbosity, int):
+ return module._verbosity
+ except Exception:
+ pass
+ return 0
+
+
+def _extract_rest_call_site(traceback):
+ # extracts first function in traceback that comes from 'fusion.api.*_api*',
+ # converts its name from something like 'get_volume' to 'Get volume' and returns
+ while traceback:
+ try:
+ frame = traceback.tb_frame
+ func_name = (
+ frame.f_code.co_name
+ ) # contains function name, e.g. 'get_volume'
+ mod_path = frame.f_globals[
+ "__name__"
+ ] # contains module path, e.g. 'fusion.api.volumes_api'
+ path_segments = mod_path.split(".")
+ if (
+ path_segments[0] == "fusion"
+ and path_segments[1] == "api"
+ and "_api" in path_segments[2]
+ ):
+ call_site = func_name.replace("_", " ").capitalize()
+ return call_site
+ except Exception:
+ pass
+ traceback = traceback.tb_next
+ return None
+
+
+class DetailsPrinter:
+ def __init__(self, target):
+ self._target = target
+ self._parenthesed = False
+
+ def append(self, what):
+ if not self._parenthesed:
+ self._target += " ("
+ self._parenthesed = True
+ else:
+ self._target += ", "
+
+ self._target += what
+
+ def finish(self):
+ if self._parenthesed:
+ self._target += ")"
+ return self._target
+
+
+def format_fusion_api_exception(exception, traceback=None):
+ """Formats `fusion.rest.ApiException` into a simple short form, suitable
+ for Ansible error output. Returns a (message: str, body: dict) tuple."""
+ message = None
+ code = None
+ resource_name = None
+ request_id = None
+ body = None
+ call_site = _extract_rest_call_site(traceback)
+ try:
+ body = json.loads(exception.body)
+ request_id = body.get("request_id", None)
+ error = body["error"]
+ message = error.get("message")
+ code = error.get("pure_code")
+ if not code:
+ code = exception.status
+ if not code:
+ code = error.get("http_code")
+ resource_name = error["details"]["name"]
+ except Exception:
+ pass
+
+ output = ""
+ if call_site:
+ output += "'{0}' failed".format(call_site)
+ else:
+ output += "request failed"
+
+ if message:
+ output += ", {0}".format(message.replace('"', "'"))
+
+ details = DetailsPrinter(output)
+ if resource_name:
+ details.append("resource: '{0}'".format(resource_name))
+ if code:
+ details.append("code: '{0}'".format(code))
+ if request_id:
+ details.append("request id: '{0}'".format(request_id))
+ output = details.finish()
+
+ return (output, body)
+
+
+def format_failed_fusion_operation_exception(exception):
+ """Formats failed `fusion.Operation` into a simple short form, suitable
+ for Ansible error output. Returns a (message: str, body: dict) tuple."""
+ op = exception.op
+ http_error = exception.http_error
+ if op.status != "Failed" and not http_error:
+ raise ValueError(
+ "BUG: can only format Operation exception with .status == Failed or http_error != None"
+ )
+
+ message = None
+ code = None
+ operation_name = None
+ operation_id = None
+
+ try:
+ if op.status == "Failed":
+ operation_id = op.id
+ error = op.error
+ message = error.message
+ code = error.pure_code
+ if not code:
+ code = error.http_code
+ operation_name = op.request_type
+ except Exception as e:
+ pass
+
+ output = ""
+ if operation_name:
+ # converts e.g. 'CreateVolume' to 'Create volume'
+ operation_name = re.sub("(.)([A-Z][a-z]+)", r"\1 \2", operation_name)
+ operation_name = re.sub(
+ "([a-z0-9])([A-Z])", r"\1 \2", operation_name
+ ).capitalize()
+ output += "{0}: ".format(operation_name)
+ output += "operation failed"
+
+ if message:
+ output += ", {0}".format(message.replace('"', "'"))
+
+ details = DetailsPrinter(output)
+ if code:
+ details.append("code: '{0}'".format(code))
+ if operation_id:
+ details.append("operation id: '{0}'".format(operation_id))
+ if http_error:
+ details.append("HTTP error: '{0}'".format(str(http_error).replace('"', "'")))
+
+ output = details.finish()
+
+ return output
+
+
+def format_http_exception(exception, traceback):
+ """Formats failed `urllib3.exceptions` exceptions into a simple short form,
+ suitable for Ansible error output. Returns a `str`."""
+ # urllib3 exceptions hide all details in a formatted message so all we
+ # can do is append the REST call that caused this
+ output = ""
+ call_site = _extract_rest_call_site(traceback)
+ if call_site:
+ output += "'{0}': ".format(call_site)
+ output += "HTTP request failed via "
+
+ inner = exception
+ while True:
+ try:
+ e = inner.reason
+ if e and isinstance(e, urllib3.exceptions.HTTPError):
+ inner = e
+ continue
+ break
+ except Exception:
+ break
+
+ if inner != exception:
+ output += "'{0}'/'{1}'".format(type(inner).__name__, type(exception).__name__)
+ else:
+ output += "'{0}'".format(type(exception).__name__)
+
+ output += " - {0}".format(str(exception).replace('"', "'"))
+
+ return output
+
+
+def _handle_api_exception(
+ module,
+ exception,
+ traceback,
+ verbosity,
+):
+ (error_message, body) = format_fusion_api_exception(exception, traceback)
+
+ if verbosity > 1:
+ module.fail_json(msg=error_message, call_details=body, traceback=str(traceback))
+ elif verbosity > 0:
+ module.fail_json(msg=error_message, call_details=body)
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _handle_operation_exception(module, exception, traceback, verbosity):
+ op = exception.op
+
+ error_message = format_failed_fusion_operation_exception(exception)
+
+ if verbosity > 1:
+ module.fail_json(
+ msg=error_message, op_details=op.to_dict(), traceback=str(traceback)
+ )
+ elif verbosity > 0:
+ module.fail_json(msg=error_message, op_details=op.to_dict())
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _handle_http_exception(module, exception, traceback, verbosity):
+ error_message = format_http_exception(exception, traceback)
+
+ if verbosity > 1:
+ module.fail_json(msg=error_message, traceback=trace.format_exception(exception))
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _except_hook_callback(module, original_hook, type, value, traceback):
+ verbosity = _get_verbosity(module)
+ if type == purefusion.rest.ApiException:
+ _handle_api_exception(
+ module,
+ value,
+ traceback,
+ verbosity,
+ )
+ elif type == OperationException:
+ _handle_operation_exception(module, value, traceback, verbosity)
+ elif issubclass(type, urllib3.exceptions.HTTPError):
+ _handle_http_exception(module, value, traceback, verbosity)
+
+ # if we bubbled here the handlers were not able to process the exception
+ original_hook(type, value, traceback)
+
+
+def install_fusion_exception_hook(module):
+ """Installs a hook that catches `purefusion.rest.ApiException` and
+ `OperationException` and produces simpler and nicer error messages
+ for Ansible output."""
+ original_hook = sys.excepthook
+ sys.excepthook = lambda type, value, traceback: _except_hook_callback(
+ module, original_hook, type, value, traceback
+ )
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py b/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py
new file mode 100644
index 000000000..74b5f0e91
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+
+# 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 (c), Simon Dodsley <simon@purestorage.com>,2021
+# 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.
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion
+except ImportError:
+ pass
+
+from os import environ
+from urllib.parse import urljoin
+import platform
+
+TOKEN_EXCHANGE_URL = "https://api.pure1.purestorage.com/oauth2/1.0/token"
+VERSION = 1.0
+USER_AGENT_BASE = "Ansible"
+
+PARAM_ISSUER_ID = "issuer_id"
+PARAM_PRIVATE_KEY_FILE = "private_key_file"
+PARAM_PRIVATE_KEY_PASSWORD = "private_key_password"
+PARAM_ACCESS_TOKEN = "access_token"
+ENV_ISSUER_ID = "FUSION_ISSUER_ID"
+ENV_API_HOST = "FUSION_API_HOST"
+ENV_PRIVATE_KEY_FILE = "FUSION_PRIVATE_KEY_FILE"
+ENV_TOKEN_ENDPOINT = "FUSION_TOKEN_ENDPOINT"
+ENV_ACCESS_TOKEN = "FUSION_ACCESS_TOKEN"
+
+# will be deprecated in 2.0.0
+PARAM_APP_ID = "app_id" # replaced by PARAM_ISSUER_ID
+PARAM_KEY_FILE = "key_file" # replaced by PARAM_PRIVATE_KEY_FILE
+ENV_APP_ID = "FUSION_APP_ID" # replaced by ENV_ISSUER_ID
+ENV_HOST = "FUSION_HOST" # replaced by ENV_API_HOST
+DEP_VER = "2.0.0"
+BASE_PATH = "/api/1.1"
+
+
+def _env_deprecation_warning(module, old_env, new_env, vers):
+ if old_env in environ:
+ if new_env in environ:
+ module.warn(
+ f"{old_env} env variable is ignored because {new_env} is specified."
+ f" {old_env} env variable is deprecated and will be removed in version {vers}"
+ f" Please use {new_env} env variable only."
+ )
+ else:
+ module.warn(
+ f"{old_env} env variable is deprecated and will be removed in version {vers}"
+ f" Please use {new_env} env variable instead."
+ )
+
+
+def _param_deprecation_warning(module, old_param, new_param, vers):
+ if old_param in module.params:
+ module.warn(
+ f"{old_param} parameter is deprecated and will be removed in version {vers}"
+ f" Please use {new_param} parameter instead."
+ f" Don't use both parameters simultaneously."
+ )
+
+
+def get_fusion(module):
+ """Return System Object or Fail"""
+ # deprecation warnings
+ _param_deprecation_warning(module, PARAM_APP_ID, PARAM_ISSUER_ID, DEP_VER)
+ _param_deprecation_warning(module, PARAM_KEY_FILE, PARAM_PRIVATE_KEY_FILE, DEP_VER)
+ _env_deprecation_warning(module, ENV_APP_ID, ENV_ISSUER_ID, DEP_VER)
+ _env_deprecation_warning(module, ENV_HOST, ENV_API_HOST, DEP_VER)
+
+ user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
+ "base": USER_AGENT_BASE,
+ "class": __name__,
+ "version": VERSION,
+ "platform": platform.platform(),
+ }
+
+ issuer_id = module.params[PARAM_ISSUER_ID]
+ access_token = module.params[PARAM_ACCESS_TOKEN]
+ private_key_file = module.params[PARAM_PRIVATE_KEY_FILE]
+ private_key_password = module.params[PARAM_PRIVATE_KEY_PASSWORD]
+
+ if private_key_password is not None:
+ module.fail_on_missing_params([PARAM_PRIVATE_KEY_FILE])
+
+ config = fusion.Configuration()
+ if ENV_API_HOST in environ or ENV_HOST in environ:
+ host_url = environ.get(ENV_API_HOST, environ.get(ENV_HOST))
+ config.host = urljoin(host_url, BASE_PATH)
+ config.token_endpoint = environ.get(ENV_TOKEN_ENDPOINT, config.token_endpoint)
+
+ if access_token is not None:
+ config.access_token = access_token
+ elif issuer_id is not None and private_key_file is not None:
+ config.issuer_id = issuer_id
+ config.private_key_file = private_key_file
+ if private_key_password is not None:
+ config.private_key_password = private_key_password
+ elif ENV_ACCESS_TOKEN in environ:
+ config.access_token = environ.get(ENV_ACCESS_TOKEN)
+ elif (
+ ENV_ISSUER_ID in environ or ENV_APP_ID in environ
+ ) and ENV_PRIVATE_KEY_FILE in environ:
+ config.issuer_id = environ.get(ENV_ISSUER_ID, environ.get(ENV_APP_ID))
+ config.private_key_file = environ.get(ENV_PRIVATE_KEY_FILE)
+ else:
+ module.fail_json(
+ msg=f"You must set either {ENV_ISSUER_ID} and {ENV_PRIVATE_KEY_FILE} or {ENV_ACCESS_TOKEN} environment variables. "
+ f"Or module arguments either {PARAM_ISSUER_ID} and {PARAM_PRIVATE_KEY_FILE} or {PARAM_ACCESS_TOKEN}"
+ )
+
+ try:
+ client = fusion.ApiClient(config)
+ client.set_default_header("User-Agent", user_agent)
+ api_instance = fusion.DefaultApi(client)
+ api_instance.get_version()
+ except Exception as err:
+ module.fail_json(msg="Fusion authentication failed: {0}".format(err))
+
+ return client
+
+
+def fusion_argument_spec():
+ """Return standard base dictionary used for the argument_spec argument in AnsibleModule"""
+
+ return {
+ PARAM_ISSUER_ID: {
+ "no_log": True,
+ "aliases": [PARAM_APP_ID],
+ "deprecated_aliases": [
+ {
+ "name": PARAM_APP_ID,
+ "version": DEP_VER,
+ "collection_name": "purefusion.fusion",
+ }
+ ],
+ },
+ PARAM_PRIVATE_KEY_FILE: {
+ "no_log": False,
+ "aliases": [PARAM_KEY_FILE],
+ "deprecated_aliases": [
+ {
+ "name": PARAM_KEY_FILE,
+ "version": DEP_VER,
+ "collection_name": "purefusion.fusion",
+ }
+ ],
+ },
+ PARAM_PRIVATE_KEY_PASSWORD: {
+ "no_log": True,
+ },
+ PARAM_ACCESS_TOKEN: {
+ "no_log": True,
+ },
+ }
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py b/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py
new file mode 100644
index 000000000..535de76ba
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Daniel Turecek (dturecek@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+
+def get_array(module, fusion, array_name=None):
+ """Return Array or None"""
+ array_api_instance = purefusion.ArraysApi(fusion)
+ try:
+ if array_name is None:
+ array_name = module.params["array"]
+
+ return array_api_instance.get_array(
+ array_name=array_name,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_az(module, fusion, availability_zone_name=None):
+ """Get Availability Zone or None"""
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ try:
+ if availability_zone_name is None:
+ availability_zone_name = module.params["availability_zone"]
+
+ return az_api_instance.get_availability_zone(
+ region_name=module.params["region"],
+ availability_zone_name=availability_zone_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_region(module, fusion, region_name=None):
+ """Get Region or None"""
+ region_api_instance = purefusion.RegionsApi(fusion)
+ try:
+ if region_name is None:
+ region_name = module.params["region"]
+
+ return region_api_instance.get_region(
+ region_name=region_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_ss(module, fusion, storage_service_name=None):
+ """Return Storage Service or None"""
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ try:
+ if storage_service_name is None:
+ storage_service_name = module.params["storage_service"]
+
+ return ss_api_instance.get_storage_service(
+ storage_service_name=storage_service_name
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_tenant(module, fusion, tenant_name=None):
+ """Return Tenant or None"""
+ api_instance = purefusion.TenantsApi(fusion)
+ try:
+ if tenant_name is None:
+ tenant_name = module.params["tenant"]
+
+ return api_instance.get_tenant(tenant_name=tenant_name)
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_ts(module, fusion, tenant_space_name=None):
+ """Tenant Space or None"""
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ try:
+ if tenant_space_name is None:
+ tenant_space_name = module.params["tenant_space"]
+
+ return ts_api_instance.get_tenant_space(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=tenant_space_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py b/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py
new file mode 100644
index 000000000..a00d8200a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import ipaddress
+
+# while regexes are hard to maintain, they are used anyways for few reasons:
+# a) REST backend accepts fairly restricted input and we need to match that input instead of all
+# the esoteric extra forms various packages are usually capable of parsing (like dotted-decimal
+# subnet masks, octal octets, hexadecimal octets, zero-extended addresses etc.)
+# b) manually written parsing routines are usually complex to write, verify and think about
+import re
+
+# IPv4 octet regex part, matches only simple decimal 0-255 without leading zeroes
+_octet = (
+ "((?:[0-9])|" # matches 0-9
+ "(?:[1-9][0-9])|" # matches 10-99
+ "(?:1[0-9][0-9])|" # matches 100-199
+ "(?:2[0-4][0-9])|" # matches 200-249
+ "(?:25[0-5]))" # matches 250-255
+)
+
+# IPv4 subnet mask regex part, matches decimal 8-32
+_subnet_mask = (
+ "((?:[8-9])|" # matches 8-9
+ "(?:[1-2][0-9])|" # matches 10-29
+ "(?:3[0-2]))" # matches 30-32
+)
+
+# matches IPv4 addresses
+_addr_pattern = re.compile(r"^{octet}\.{octet}\.{octet}\.{octet}$".format(octet=_octet))
+# matches IPv4 networks in CIDR format, i.e. addresses in the form 'a.b.c.d/e'
+_cidr_pattern = re.compile(
+ r"^{octet}\.{octet}\.{octet}\.{octet}\/{0}$".format(_subnet_mask, octet=_octet)
+)
+
+
+def is_valid_network(addr):
+ """Returns True if `addr` is IPv4 address/submask in bit CIDR notation, False otherwise."""
+ match = re.match(_cidr_pattern, addr)
+ if match is None:
+ return False
+ for i in range(4):
+ if int(match.group(i + 1)) > 255:
+ return False
+ mask = int(match.group(5))
+ if mask < 8 or mask > 32:
+ return False
+ return True
+
+
+def is_valid_address(addr):
+ """Returns True if `addr` is a valid IPv4 address, False otherwise. Does not support
+ octal/hex notations."""
+ match = re.match(_addr_pattern, addr)
+ if match is None:
+ return False
+ for i in range(4):
+ if int(match.group(i + 1)) > 255:
+ return False
+ return True
+
+
+def is_address_in_network(addr, network):
+ """Returns True if `addr` and `network` are a valid IPv4 address and
+ IPv4 network respectively and if `addr` is in `network`, False otherwise."""
+ if not is_valid_address(addr) or not is_valid_network(network):
+ return False
+ parsed_addr = ipaddress.ip_address(addr)
+ parsed_net = ipaddress.ip_network(network)
+ return parsed_addr in parsed_net
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py b/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py
new file mode 100644
index 000000000..dc80aefe3
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import time
+import math
+
+try:
+ import fusion as purefusion
+ from urllib3.exceptions import HTTPError
+except ImportError:
+ pass
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+
+
+def await_operation(fusion, operation, fail_playbook_if_operation_fails=True):
+ """
+ Waits for given operation to finish.
+ Throws an exception by default if the operation fails.
+ """
+ op_api = purefusion.OperationsApi(fusion)
+ operation_get = None
+ while True:
+ try:
+ operation_get = op_api.get_operation(operation.id)
+ if operation_get.status == "Succeeded":
+ return operation_get
+ if operation_get.status == "Failed":
+ if fail_playbook_if_operation_fails:
+ raise OperationException(operation_get)
+ return operation_get
+ except HTTPError as err:
+ raise OperationException(operation, http_error=err)
+ time.sleep(int(math.ceil(operation_get.retry_in / 1000)))
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py b/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py
new file mode 100644
index 000000000..a2cd75245
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+import re
+
+__metaclass__ = type
+
+METRIC_SUFFIXES = ["K", "M", "G", "T", "P"]
+
+duration_pattern = re.compile(
+ r"^((?P<Y>[1-9]\d*)Y)?((?P<W>[1-9]\d*)W)?((?P<D>[1-9]\d*)D)?(((?P<H>[1-9]\d*)H)?((?P<M>[1-9]\d*)M)?)?$"
+)
+duration_transformation = {
+ "Y": 365 * 24 * 60,
+ "W": 7 * 24 * 60,
+ "D": 24 * 60,
+ "H": 60,
+ "M": 1,
+}
+
+
+def parse_number_with_metric_suffix(module, number, factor=1024):
+ """Given a human-readable string (e.g. 2G, 30M, 400),
+ return the resolved integer.
+ Will call `module.fail_json()` for invalid inputs.
+ """
+ try:
+ stripped_num = number.strip()
+ if stripped_num[-1].isdigit():
+ return int(stripped_num)
+ # has unit prefix
+ result = float(stripped_num[:-1])
+ suffix = stripped_num[-1].upper()
+ factor_count = METRIC_SUFFIXES.index(suffix) + 1
+ for _i in range(0, factor_count):
+ result = result * float(factor)
+ return int(result)
+ except Exception:
+ module.fail_json(
+ msg="'{0}' is not a valid number, use '400', '1K', '2M', ...".format(number)
+ )
+ return 0
+
+
+def parse_duration(period):
+ if period.isdigit():
+ return int(period)
+
+ match = duration_pattern.match(period.upper())
+ if not match or period == "":
+ raise ValueError("Invalid format")
+
+ durations = {
+ "Y": int(match.group("Y")) if match.group("Y") else 0,
+ "W": int(match.group("W")) if match.group("W") else 0,
+ "D": int(match.group("D")) if match.group("D") else 0,
+ "H": int(match.group("H")) if match.group("H") else 0,
+ "M": int(match.group("M")) if match.group("M") else 0,
+ }
+ return sum(value * duration_transformation[key] for key, value in durations.items())
+
+
+def parse_minutes(module, period):
+ try:
+ return parse_duration(period)
+ except ValueError:
+ module.fail_json(
+ msg=(
+ "'{0}' is not a valid time period, use combination of data units (Y,W,D,H,M)"
+ "e.g. 4W3D5H, 5D8H5M, 3D, 5W, 1Y5W..."
+ ).format(period)
+ )
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py b/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py
new file mode 100644
index 000000000..a4edaf341
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import re
+import importlib
+import importlib.metadata
+
+# This file exists because Ansible currently cannot declare dependencies on Python modules.
+# see https://github.com/ansible/ansible/issues/62733 for more info about lack of req support
+
+#############################
+
+# 'module_name, package_name, version_requirements' triplets
+DEPENDENCIES = [
+ ("fusion", "purefusion", ">=1.0.11,<2.0"),
+ ("urllib3", "urllib3", None),
+]
+
+#############################
+
+
+def _parse_version(val):
+ """
+ Parse a package version.
+ Takes in either MAJOR.MINOR or MAJOR.MINOR.PATCH form. PATCH
+ can have additional suffixes, e.g. '-prerelease', 'a1', ...
+
+ :param val: a string representation of the package version
+ :returns: tuple of ints (MAJOR, MINOR, PATCH) or None if not parsed
+ """
+ # regexes for this were really ugly
+ try:
+ parts = val.split(".")
+ if len(parts) < 2 or len(parts) > 3:
+ return None
+ major = int(parts[0])
+ minor = int(parts[1])
+ if len(parts) > 2:
+ patch = re.match(r"^\d+", parts[2])
+ patch = int(patch.group(0))
+ else:
+ patch = None
+ return (major, minor, patch)
+ except Exception:
+ return None
+
+
+# returns list of tuples [(COMPARATOR, (MAJOR, MINOR, PATCH)),...]
+def _parse_version_requirements(val):
+ """
+ Parse package requirements.
+
+ :param val: a string in the form ">=1.0.11,<2.0"
+ :returns: list of tuples in the form [(">=", (1, 0, 11)), ("<", (2, 0, None))] or None if not parsed
+ """
+ reqs = []
+ try:
+ parts = val.split(",")
+ for part in parts:
+ match = re.match(r"\s*(>=|<=|==|=|<|>|!=)\s*([^\s]+)", part)
+ op = match.group(1)
+ ver = match.group(2)
+ ver_tuple = _parse_version(ver)
+ if not ver_tuple:
+ raise ValueError("invalid version {0}".format(ver))
+ reqs.append((op, ver_tuple))
+ return reqs
+ except Exception as e:
+ raise ValueError("invalid version requirement '{0}' {1}".format(val, e))
+
+
+def _compare_version(op, ver, req):
+ """
+ Compare two versions.
+
+ :param op: a string, one of comparators ">=", "<=", "=", "==", ">" or "<"
+ :param ver: version tuple in _parse_version() return form
+ :param req: version tuple in _parse_version() return form
+ :returns: True if ver 'op' req; False otherwise
+ """
+
+ def _cmp(a, b):
+ return (a > b) - (a < b)
+
+ major = _cmp(ver[0], req[0])
+ minor = _cmp(ver[1], req[1])
+ patch = None
+ if req[2] is not None:
+ patch = _cmp(ver[2] or 0, req[2])
+ result = {
+ ">=": major > 0 or (major == 0 and (minor > 0 or patch is None or patch >= 0)),
+ "<=": major < 0 or (major == 0 and (minor < 0 or patch is None or patch <= 0)),
+ ">": major > 0
+ or (major == 0 and (minor > 0 or patch is not None and patch > 0)),
+ "<": major < 0
+ or (major == 0 and (minor < 0 or patch is not None and patch < 0)),
+ "=": major == 0 and minor == 0 and (patch is None or patch == 0),
+ "==": major == 0 and minor == 0 and (patch is None or patch == 0),
+ "!=": major != 0 or minor != 0 or (patch is not None and patch != 0),
+ }.get(op)
+ return result
+
+
+def _version_satisfied(version, requirements):
+ """
+ Checks whether version matches given version requirements.
+
+ :param version: a string, in input form to _parse_version()
+ :param requirements: as string, in input form to _parse_version_requirements()
+ :returns: True if 'version' matches 'requirements'; False otherwise
+ """
+
+ version = _parse_version(version)
+ requirements = _parse_version_requirements(requirements)
+ for req in requirements:
+ if not _compare_version(req[0], version, req[1]):
+ return False
+ return True
+
+
+# poor helper to work around the fact Ansible is unable to manage python dependencies
+def _check_import(ansible_module, module, package=None, version_requirements=None):
+ """
+ Tries to import a module and optionally validates its package version.
+ Calls AnsibleModule.fail_json() if not satisfied.
+
+ :param ansible_module: an AnsibleModule instance
+ :param module: a string with module name to try to import
+ :param package: a string, package to check version for; must be specified with 'version_requirements'
+ :param version_requirements: a string, version requirements for 'package'
+ """
+ try:
+ mod = importlib.import_module(module)
+ except ImportError:
+ ansible_module.fail_json(
+ msg="Error: Python package '{0}' required and missing".format(module)
+ )
+
+ if package and version_requirements:
+ # silently ignore version checks and hope for the best if we can't fetch
+ # the package version since we can't know how the user installs packages
+ try:
+ version = importlib.metadata.version(package)
+ if version and not _version_satisfied(version, version_requirements):
+ ansible_module.fail_json(
+ msg="Error: Python package '{0}' version '{1}' does not satisfy requirements '{2}'".format(
+ module, version, version_requirements
+ )
+ )
+ except Exception:
+ pass # ignore package loads
+
+
+def check_dependencies(ansible_module):
+ for module, package, version_requirements in DEPENDENCIES:
+ _check_import(ansible_module, module, package, version_requirements)
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py b/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py
new file mode 100644
index 000000000..55d7f11a2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ install_fusion_exception_hook,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.prerequisites import (
+ check_dependencies,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ get_fusion,
+)
+
+
+def setup_fusion(module):
+ check_dependencies(module)
+ install_fusion_exception_hook(module)
+ return get_fusion(module)
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py
new file mode 100644
index 000000000..39860449d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py
@@ -0,0 +1,139 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_api_client
+version_added: '1.0.0'
+short_description: Manage API clients in Pure Storage Fusion
+description:
+- Create or delete an API Client in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the client.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the client should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ public_key:
+ description:
+ - The API clients PEM formatted (Base64 encoded) RSA public key.
+ - Include the C(—–BEGIN PUBLIC KEY—–) and C(—–END PUBLIC KEY—–) lines.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new API client foo
+ purestorage.fusion.fusion_api_client:
+ name: "foo client"
+ public_key: "{{lookup('file', 'public_pem_file') }}"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_client_id(module, fusion):
+ """Get API Client ID, or None if not available"""
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ try:
+ clients = id_api_instance.list_api_clients()
+ for client in clients:
+ if (
+ client.public_key == module.params["public_key"]
+ and client.display_name == module.params["name"]
+ ):
+ return client.id
+ return None
+ except purefusion.rest.ApiException:
+ return None
+
+
+def delete_client(module, fusion, client_id):
+ """Delete API Client"""
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ id_api_instance.delete_api_client(api_client_id=client_id)
+ module.exit_json(changed=changed)
+
+
+def create_client(module, fusion):
+ """Create API Client"""
+
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ client = purefusion.APIClientPost(
+ public_key=module.params["public_key"],
+ display_name=module.params["name"],
+ )
+ id_api_instance.create_api_client(client)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ public_key=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ client_id = get_client_id(module, fusion)
+ if client_id is None and state == "present":
+ create_client(module, fusion)
+ elif client_id is not None and state == "absent":
+ delete_client(module, fusion, client_id)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py
new file mode 100644
index 000000000..f7933eabe
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py
@@ -0,0 +1,265 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_array
+version_added: '1.0.0'
+short_description: Manage arrays in Pure Storage Fusion
+description:
+- Create or delete an array in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the array.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the array should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the array.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - The region the AZ is in.
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The availability zone the array is located in.
+ type: str
+ required: true
+ hardware_type:
+ description:
+ - Hardware type to which the storage class applies.
+ choices: [ flash-array-x, flash-array-c, flash-array-x-optane, flash-array-xl ]
+ type: str
+ host_name:
+ description:
+ - Management IP address of the array, or FQDN.
+ type: str
+ appliance_id:
+ description:
+ - Appliance ID of the array.
+ type: str
+ maintenance_mode:
+ description:
+ - "Switch the array into maintenance mode or back.
+ Array in maintenance mode can have placement groups migrated out but not in.
+ Intended use cases are for example safe decommissioning or to prevent use
+ of an array that has not yet been fully configured."
+ type: bool
+ unavailable_mode:
+ description:
+ - "Switch the array into unavailable mode or back.
+ Fusion tries to exclude unavailable arrays from virtually any operation it
+ can. This is to prevent stalling operations in case of e.g. a networking
+ failure. As of the moment arrays have to be marked unavailable manually."
+ type: bool
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new array foo
+ purestorage.fusion.fusion_array:
+ name: foo
+ az: zone_1
+ region: region1
+ hardware_type: flash-array-x
+ host_name: foo_array
+ display_name: "foo array"
+ appliance_id: 1227571-198887878-35016350232000707
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_array(module, fusion):
+ """Return Array or None"""
+ return getters.get_array(module, fusion, array_name=module.params["name"])
+
+
+def create_array(module, fusion):
+ """Create Array"""
+
+ array_api_instance = purefusion.ArraysApi(fusion)
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ array = purefusion.ArrayPost(
+ hardware_type=module.params["hardware_type"],
+ display_name=display_name,
+ host_name=module.params["host_name"],
+ name=module.params["name"],
+ appliance_id=module.params["appliance_id"],
+ )
+ res = array_api_instance.create_array(
+ array,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ await_operation(fusion, res)
+ return True
+
+
+def update_array(module, fusion):
+ """Update Array"""
+ array = get_array(module, fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != array.display_name
+ ):
+ patch = purefusion.ArrayPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if module.params["host_name"] and module.params["host_name"] != array.host_name:
+ patch = purefusion.ArrayPatch(
+ host_name=purefusion.NullableString(module.params["host_name"])
+ )
+ patches.append(patch)
+
+ if (
+ module.params["maintenance_mode"] is not None
+ and module.params["maintenance_mode"] != array.maintenance_mode
+ ):
+ patch = purefusion.ArrayPatch(
+ maintenance_mode=purefusion.NullableBoolean(
+ module.params["maintenance_mode"]
+ )
+ )
+ patches.append(patch)
+ if (
+ module.params["unavailable_mode"] is not None
+ and module.params["unavailable_mode"] != array.unavailable_mode
+ ):
+ patch = purefusion.ArrayPatch(
+ unavailable_mode=purefusion.NullableBoolean(
+ module.params["unavailable_mode"]
+ )
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ array_api_instance = purefusion.ArraysApi(fusion)
+ for patch in patches:
+ op = array_api_instance.update_array(
+ patch,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ array_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def delete_array(module, fusion):
+ """Delete Array - not currently available"""
+ array_api_instance = purefusion.ArraysApi(fusion)
+ if not module.check_mode:
+ res = array_api_instance.delete_array(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["name"],
+ )
+ await_operation(fusion, res)
+ return True
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ appliance_id=dict(type="str"),
+ host_name=dict(type="str"),
+ hardware_type=dict(
+ type="str",
+ choices=[
+ "flash-array-x",
+ "flash-array-c",
+ "flash-array-x-optane",
+ "flash-array-xl",
+ ],
+ ),
+ maintenance_mode=dict(type="bool"),
+ unavailable_mode=dict(type="bool"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ array = get_array(module, fusion)
+
+ changed = False
+ if not array and state == "present":
+ module.fail_on_missing_params(["hardware_type", "host_name", "appliance_id"])
+ changed = create_array(module, fusion) | update_array(
+ module, fusion
+ ) # update is run to set properties which cannot be set on creation and instead use defaults
+ elif array and state == "present":
+ changed = changed | update_array(module, fusion)
+ elif array and state == "absent":
+ changed = changed | delete_array(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py
new file mode 100644
index 000000000..02647d397
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py
@@ -0,0 +1,162 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_az
+version_added: '1.0.0'
+short_description: Create Availability Zones in Pure Storage Fusion
+description:
+- Manage an Availability Zone in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the Availability Zone.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the Availability Zone should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the Availability Zone.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - Region within which the AZ is created.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new AZ foo
+ purestorage.fusion.fusion_az:
+ name: foo
+ display_name: "foo AZ"
+ region: region1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete AZ foo
+ purestorage.fusion.fusion_az:
+ name: foo
+ state: absent
+ region: region1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_az(module, fusion):
+ """Get Availability Zone or None"""
+ return getters.get_az(module, fusion, availability_zone_name=module.params["name"])
+
+
+def delete_az(module, fusion):
+ """Delete Availability Zone"""
+
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = az_api_instance.delete_availability_zone(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def create_az(module, fusion):
+ """Create Availability Zone"""
+
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+
+ azone = purefusion.AvailabilityZonePost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = az_api_instance.create_availability_zone(
+ azone, region_name=module.params["region"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ azone = get_az(module, fusion)
+
+ if not azone and state == "present":
+ create_az(module, fusion)
+ elif azone and state == "absent":
+ delete_az(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py
new file mode 100644
index 000000000..3f45ea2dd
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py
@@ -0,0 +1,312 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_hap
+version_added: '1.0.0'
+short_description: Manage host access policies in Pure Storage Fusion
+description:
+- Create or delete host access policies in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+- Setting passwords is not an idempotent action.
+- Only iSCSI transport is currently supported.
+- iSCSI CHAP is not yet supported.
+options:
+ name:
+ description:
+ - The name of the host access policy.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the host access policy.
+ type: str
+ state:
+ description:
+ - Define whether the host access policy should exist or not.
+ - When removing host access policy all connected volumes must
+ have been previously disconnected.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ wwns:
+ type: list
+ elements: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - List of wwns for the host access policy.
+ iqn:
+ type: str
+ description:
+ - IQN for the host access policy.
+ nqn:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - NQN for the host access policy.
+ personality:
+ type: str
+ description:
+ - Define which operating system the host is.
+ default: linux
+ choices: ['linux', 'windows', 'hpux', 'vms', 'aix', 'esxi', 'solaris', 'hitachi-vsp', 'oracle-vm-server']
+ target_user:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the target user name for CHAP authentication.
+ - Required with I(target_password).
+ - To clear the username/password pair use C(clear) as the password.
+ target_password:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the target password for CHAP authentication.
+ - Password length between 12 and 255 characters.
+ - To clear the username/password pair use C(clear) as the password.
+ host_user:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the host user name for CHAP authentication.
+ - Required with I(host_password).
+ - To clear the username/password pair use C(clear) as the password.
+ host_password:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the host password for CHAP authentication.
+ - Password length between 12 and 255 characters.
+ - To clear the username/password pair use C(clear) as the password.
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new AIX host access policy
+ purestorage.fusion.fusion_hap:
+ name: foo
+ personality: aix
+ iqn: "iqn.2005-03.com.RedHat:linux-host1"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete host access policy
+ purestorage.fusion.fusion_hap:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+import re
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def _check_iqn(module, fusion):
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ hosts = hap_api_instance.list_host_access_policies().items
+ for host in hosts:
+ if host.iqn == module.params["iqn"] and host.name != module.params["name"]:
+ module.fail_json(
+ msg="Supplied IQN {0} already used by host access policy {1}".format(
+ module.params["iqn"], host.name
+ )
+ )
+
+
+def get_host(module, fusion):
+ """Return host or None"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ try:
+ return hap_api_instance.get_host_access_policy(
+ host_access_policy_name=module.params["name"]
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_hap(module, fusion):
+ """Create a new host access policy"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+
+ op = hap_api_instance.create_host_access_policy(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module.params["iqn"],
+ personality=module.params["personality"],
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def delete_hap(module, fusion):
+ """Delete a Host Access Policy"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = hap_api_instance.delete_host_access_policy(
+ host_access_policy_name=module.params["name"]
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ nqn=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ iqn=dict(type="str"),
+ wwns=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ host_password=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ no_log=True,
+ ),
+ host_user=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ target_password=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ no_log=True,
+ ),
+ target_user=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ display_name=dict(type="str"),
+ personality=dict(
+ type="str",
+ default="linux",
+ choices=[
+ "linux",
+ "windows",
+ "hpux",
+ "vms",
+ "aix",
+ "esxi",
+ "solaris",
+ "hitachi-vsp",
+ "oracle-vm-server",
+ ],
+ ),
+ )
+ )
+
+ required_if = [["state", "present", ["personality", "iqn"]]]
+
+ module = AnsibleModule(
+ argument_spec,
+ supports_check_mode=True,
+ required_if=required_if,
+ )
+ fusion = setup_fusion(module)
+
+ if module.params["nqn"]:
+ module.warn(
+ "`nqn` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["wwns"]:
+ module.warn(
+ "`wwns` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["host_password"]:
+ module.warn(
+ "`host_password` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["host_user"]:
+ module.warn(
+ "`host_user` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["target_password"]:
+ module.warn(
+ "`target_password` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["target_user"]:
+ module.warn(
+ "`target_user` parameter is deprecated and will be removed in version 2.0.0"
+ )
+
+ hap_pattern = re.compile("^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$")
+ iqn_pattern = re.compile(
+ r"^iqn\.\d{4}-\d{2}((?<!-)\.(?!-)[a-zA-Z0-9\-]+){1,63}(?<!-)(?<!\.)(:(?!:)[^,\s'\"]+)?$"
+ )
+
+ if not hap_pattern.match(module.params["name"]):
+ module.fail_json(
+ msg="Host Access Policy {0} does not conform to naming convention".format(
+ module.params["name"]
+ )
+ )
+
+ if module.params["iqn"] is not None and not iqn_pattern.match(module.params["iqn"]):
+ module.fail_json(
+ msg="IQN {0} is not a valid iSCSI IQN".format(module.params["name"])
+ )
+
+ state = module.params["state"]
+ host = get_host(module, fusion)
+ _check_iqn(module, fusion)
+
+ if host is None and state == "present":
+ create_hap(module, fusion)
+ elif host is not None and state == "absent":
+ delete_hap(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py
new file mode 100644
index 000000000..31d313e9d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_hw
+version_added: '1.0.0'
+deprecated:
+ removed_at_date: "2023-08-09"
+ why: Hardware type cannot be modified in Pure Storage Fusion
+ alternative: there's no alternative as this functionality has never worked before
+short_description: Create hardware types in Pure Storage Fusion
+description:
+- Create a hardware type in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the hardware type.
+ type: str
+ state:
+ description:
+ - Define whether the hardware type should exist or not.
+ - Currently there is no mechanism to delete a hardware type.
+ default: present
+ choices: [ present ]
+ type: str
+ display_name:
+ description:
+ - The human name of the hardware type.
+ - If not provided, defaults to I(name).
+ type: str
+ media_type:
+ description:
+ - Volume size limit in M, G, T or P units.
+ type: str
+ array_type:
+ description:
+ - The array type for the hardware type.
+ choices: [ FA//X, FA//C ]
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+# this module does nothing, thus no example is provided
+EXAMPLES = r"""
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str"),
+ display_name=dict(type="str"),
+ array_type=dict(type="str", choices=["FA//X", "FA//C"]),
+ media_type=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py
new file mode 100644
index 000000000..be019d3d2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py
@@ -0,0 +1,1130 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com), Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_info
+version_added: '1.0.0'
+short_description: Collect information from Pure Fusion
+description:
+ - Collect information from a Pure Fusion environment.
+ - By default, the module will collect basic
+ information including counts for arrays, availability_zones, volumes, snapshots
+ . Fleet capacity and data reduction rates are also provided.
+ - Additional information can be collected based on the configured set of arguments.
+author:
+ - Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will define the information to be collected.
+ Possible values for this include all, minimum, roles, users, arrays, hardware_types,
+ volumes, host_access_policies, storage_classes, protection_policies, placement_groups,
+ network_interfaces, availability_zones, network_interface_groups, storage_endpoints,
+ snapshots, regions, storage_services, tenants, tenant_spaces, network_interface_groups and api_clients.
+ type: list
+ elements: str
+ required: false
+ default: minimum
+extends_documentation_fragment:
+ - purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Collect default set of information
+ purestorage.fusion.fusion_info:
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+ register: fusion_info
+
+- name: Show default information
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info']['default'] }}"
+
+- name: Collect all information
+ purestorage.fusion.fusion_info:
+ gather_subset:
+ - all
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Show all information
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
+"""
+
+RETURN = r"""
+fusion_info:
+ description: Returns the information collected from Fusion
+ returned: always
+ type: dict
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+import time
+import http
+
+
+def _convert_microseconds(micros):
+ seconds = (micros / 1000) % 60
+ minutes = (micros / (1000 * 60)) % 60
+ hours = (micros / (1000 * 60 * 60)) % 24
+ return seconds, minutes, hours
+
+
+def _api_permission_denied_handler(name):
+ """Return decorator which catches #403 errors"""
+
+ def inner(func):
+ def wrapper(module, fusion, *args, **kwargs):
+ try:
+ return func(module, fusion, *args, **kwargs)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ module.warn(f"Cannot get [{name} dict], reason: Permission denied")
+ return None
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ return wrapper
+
+ return inner
+
+
+def generate_default_dict(module, fusion):
+ def warning_api_exception(name):
+ module.warn(f"Cannot get {name} in [default dict], reason: Permission denied")
+
+ def warning_argument_none(name, requirement):
+ module.warn(
+ f"Cannot get {name} in [default dict], reason: Required argument `{requirement}` not available."
+ )
+
+ # All values are independent on each other - if getting one value fails, we will show warning and continue.
+ # That's also the reason why there's so many nested for loops repeating all over again.
+ version = None
+ users_num = None
+ protection_policies_num = None
+ host_access_policies_num = None
+ hardware_types_num = None
+ storage_services = None
+ storage_services_num = None
+ tenants = None
+ tenants_num = None
+ regions = None
+ regions_num = None
+ roles = None
+ roles_num = None
+ storage_classes_num = None
+ role_assignments_num = None
+ tenant_spaces_num = None
+ volumes_num = None
+ placement_groups_num = None
+ snapshots_num = None
+ availability_zones_num = None
+ arrays_num = None
+ network_interfaces_num = None
+ network_interface_groups_num = None
+ storage_endpoints_num = None
+
+ try:
+ version = purefusion.DefaultApi(fusion).get_version().version
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("API version")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ users_num = len(purefusion.IdentityManagerApi(fusion).list_users())
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Users")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ protection_policies_num = len(
+ purefusion.ProtectionPoliciesApi(fusion).list_protection_policies().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Protection Policies")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ host_access_policies_num = len(
+ purefusion.HostAccessPoliciesApi(fusion).list_host_access_policies().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Host Access Policies")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ hardware_types_num = len(
+ purefusion.HardwareTypesApi(fusion).list_hardware_types().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Hardware Types")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ storage_services = purefusion.StorageServicesApi(fusion).list_storage_services()
+ storage_services_num = len(storage_services.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Services")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ tenants = purefusion.TenantsApi(fusion).list_tenants()
+ tenants_num = len(tenants.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Tenants")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ regions = purefusion.RegionsApi(fusion).list_regions()
+ regions_num = len(regions.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Regions")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ roles = purefusion.RolesApi(fusion).list_roles()
+ roles_num = len(roles)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Roles")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ if storage_services is not None:
+ try:
+ storage_class_api_instance = purefusion.StorageClassesApi(fusion)
+ storage_classes_num = sum(
+ len(
+ storage_class_api_instance.list_storage_classes(
+ storage_service_name=storage_service.name
+ ).items
+ )
+ for storage_service in storage_services.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Classes")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Storage Classes", "storage_services")
+
+ if roles is not None:
+ try:
+ role_assign_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ role_assignments_num = sum(
+ len(role_assign_api_instance.list_role_assignments(role_name=role.name))
+ for role in roles
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Role Assignments")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Role Assignments", "roles")
+
+ if tenants is not None:
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ try:
+ tenant_spaces_num = sum(
+ len(
+ tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ for tenant in tenants.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Tenant Spaces")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ vol_api_instance = purefusion.VolumesApi(fusion)
+ volumes_num = sum(
+ len(
+ vol_api_instance.list_volumes(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Volumes")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ plgrp_api_instance = purefusion.PlacementGroupsApi(fusion)
+ placement_groups_num = sum(
+ len(
+ plgrp_api_instance.list_placement_groups(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Placement Groups")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ snapshot_api_instance = purefusion.SnapshotsApi(fusion)
+ snapshots_num = sum(
+ len(
+ snapshot_api_instance.list_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Snapshots")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Tenant Spaces", "tenants")
+ warning_argument_none("Volumes", "tenants")
+ warning_argument_none("Placement Groups", "tenants")
+ warning_argument_none("Snapshots", "tenants")
+
+ if regions is not None:
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ try:
+ availability_zones_num = sum(
+ len(
+ az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ for region in regions.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Availability Zones")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ arrays_api_instance = purefusion.ArraysApi(fusion)
+ arrays_num = sum(
+ len(
+ arrays_api_instance.list_arrays(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Arrays")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ network_interface_groups_num = sum(
+ len(
+ nig_api_instance.list_network_interface_groups(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Network Interface Groups")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ send_api_instance = purefusion.StorageEndpointsApi(fusion)
+ storage_endpoints_num = sum(
+ len(
+ send_api_instance.list_storage_endpoints(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Endpoints")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ nic_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ network_interfaces_num = sum(
+ len(
+ nic_api_instance.list_network_interfaces(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ array_name=array_detail.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ for array_detail in arrays_api_instance.list_arrays(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Network Interfaces")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Availability Zones", "regions")
+ warning_argument_none("Network Interfaces", "regions")
+ warning_argument_none("Network Interface Groups", "regions")
+ warning_argument_none("Storage Endpoints", "regions")
+ warning_argument_none("Arrays", "regions")
+
+ return {
+ "version": version,
+ "users": users_num,
+ "protection_policies": protection_policies_num,
+ "host_access_policies": host_access_policies_num,
+ "hardware_types": hardware_types_num,
+ "storage_services": storage_services_num,
+ "tenants": tenants_num,
+ "regions": regions_num,
+ "storage_classes": storage_classes_num,
+ "roles": roles_num,
+ "role_assignments": role_assignments_num,
+ "tenant_spaces": tenant_spaces_num,
+ "volumes": volumes_num,
+ "placement_groups": placement_groups_num,
+ "snapshots": snapshots_num,
+ "availability_zones": availability_zones_num,
+ "arrays": arrays_num,
+ "network_interfaces": network_interfaces_num,
+ "network_interface_groups": network_interface_groups_num,
+ "storage_endpoints": storage_endpoints_num,
+ }
+
+
+@_api_permission_denied_handler("network_interfaces")
+def generate_nics_dict(module, fusion):
+ nics_info = {}
+ nic_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ arrays_api_instance = purefusion.ArraysApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ array_details = arrays_api_instance.list_arrays(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for array_detail in array_details.items:
+ array_name = az.name + "/" + array_detail.name
+ nics_info[array_name] = {}
+ nics = nic_api_instance.list_network_interfaces(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ array_name=array_detail.name,
+ )
+
+ for nic in nics.items:
+ nics_info[array_name][nic.name] = {
+ "enabled": nic.enabled,
+ "display_name": nic.display_name,
+ "interface_type": nic.interface_type,
+ "services": nic.services,
+ "max_speed": nic.max_speed,
+ "vlan": nic.eth.vlan,
+ "address": nic.eth.address,
+ "mac_address": nic.eth.mac_address,
+ "gateway": nic.eth.gateway,
+ "mtu": nic.eth.mtu,
+ "network_interface_group": nic.network_interface_group.name,
+ "availability_zone": nic.availability_zone.name,
+ }
+ return nics_info
+
+
+@_api_permission_denied_handler("host_access_policies")
+def generate_hap_dict(module, fusion):
+ hap_info = {}
+ api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ hosts = api_instance.list_host_access_policies()
+ for host in hosts.items:
+ name = host.name
+ hap_info[name] = {
+ "personality": host.personality,
+ "display_name": host.display_name,
+ "iqn": host.iqn,
+ }
+ return hap_info
+
+
+@_api_permission_denied_handler("arrays")
+def generate_array_dict(module, fusion):
+ array_info = {}
+ array_api_instance = purefusion.ArraysApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ arrays = array_api_instance.list_arrays(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for array in arrays.items:
+ array_name = array.name
+ array_space = array_api_instance.get_array_space(
+ availability_zone_name=az.name,
+ array_name=array_name,
+ region_name=region.name,
+ )
+ array_perf = array_api_instance.get_array_performance(
+ availability_zone_name=az.name,
+ array_name=array_name,
+ region_name=region.name,
+ )
+ array_info[array_name] = {
+ "region": region.name,
+ "availability_zone": az.name,
+ "host_name": array.host_name,
+ "maintenance_mode": array.maintenance_mode,
+ "unavailable_mode": array.unavailable_mode,
+ "display_name": array.display_name,
+ "hardware_type": array.hardware_type.name,
+ "appliance_id": array.appliance_id,
+ "apartment_id": getattr(array, "apartment_id", None),
+ "space": {
+ "total_physical_space": array_space.total_physical_space,
+ },
+ "performance": {
+ "read_bandwidth": array_perf.read_bandwidth,
+ "read_latency_us": array_perf.read_latency_us,
+ "reads_per_sec": array_perf.reads_per_sec,
+ "write_bandwidth": array_perf.write_bandwidth,
+ "write_latency_us": array_perf.write_latency_us,
+ "writes_per_sec": array_perf.writes_per_sec,
+ },
+ }
+ return array_info
+
+
+@_api_permission_denied_handler("placement_groups")
+def generate_pg_dict(module, fusion):
+ pg_info = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ groups = pg_api_instance.list_placement_groups(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for group in groups.items:
+ group_name = tenant.name + "/" + tenant_space.name + "/" + group.name
+ pg_info[group_name] = {
+ "tenant": group.tenant.name,
+ "display_name": group.display_name,
+ "placement_engine": group.placement_engine,
+ "tenant_space": group.tenant_space.name,
+ "az": group.availability_zone.name,
+ "array": getattr(group.array, "name", None),
+ }
+ return pg_info
+
+
+@_api_permission_denied_handler("tenant_spaces")
+def generate_ts_dict(module, fusion):
+ ts_info = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ ts_name = tenant.name + "/" + tenant_space.name
+ ts_info[ts_name] = {
+ "tenant": tenant.name,
+ "display_name": tenant_space.display_name,
+ }
+ return ts_info
+
+
+@_api_permission_denied_handler("protection_policies")
+def generate_pp_dict(module, fusion):
+ pp_info = {}
+ api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ policies = api_instance.list_protection_policies()
+ for policy in policies.items:
+ policy_name = policy.name
+ pp_info[policy_name] = {
+ "objectives": policy.objectives,
+ }
+ return pp_info
+
+
+@_api_permission_denied_handler("tenants")
+def generate_tenant_dict(module, fusion):
+ tenants_api_instance = purefusion.TenantsApi(fusion)
+ return {
+ tenant.name: {
+ "display_name": tenant.display_name,
+ }
+ for tenant in tenants_api_instance.list_tenants().items
+ }
+
+
+@_api_permission_denied_handler("regions")
+def generate_regions_dict(module, fusion):
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ return {
+ region.name: {
+ "display_name": region.display_name,
+ }
+ for region in regions_api_instance.list_regions().items
+ }
+
+
+@_api_permission_denied_handler("availability_zones")
+def generate_zones_dict(module, fusion):
+ zones_info = {}
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ zones = az_api_instance.list_availability_zones(region_name=region.name)
+ for zone in zones.items:
+ az_name = zone.name
+ zones_info[az_name] = {
+ "display_name": zone.display_name,
+ "region": zone.region.name,
+ }
+ return zones_info
+
+
+@_api_permission_denied_handler("role_assignments")
+def generate_ras_dict(module, fusion):
+ ras_info = {}
+ ras_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ role_api_instance = purefusion.RolesApi(fusion)
+ roles = role_api_instance.list_roles()
+ for role in roles:
+ ras = ras_api_instance.list_role_assignments(role_name=role.name)
+ for assignment in ras:
+ name = assignment.name
+ ras_info[name] = {
+ "display_name": assignment.display_name,
+ "role": assignment.role.name,
+ "scope": assignment.scope.name,
+ }
+ return ras_info
+
+
+@_api_permission_denied_handler("roles")
+def generate_roles_dict(module, fusion):
+ roles_info = {}
+ api_instance = purefusion.RolesApi(fusion)
+ roles = api_instance.list_roles()
+ for role in roles:
+ name = role.name
+ roles_info[name] = {
+ "display_name": role.display_name,
+ "scopes": role.assignable_scopes,
+ }
+ return roles_info
+
+
+@_api_permission_denied_handler("api_clients")
+def generate_api_client_dict(module, fusion):
+ client_info = {}
+ api_instance = purefusion.IdentityManagerApi(fusion)
+ clients = api_instance.list_api_clients()
+ for client in clients:
+ client_info[client.name] = {
+ "display_name": client.display_name,
+ "issuer": client.issuer,
+ "public_key": client.public_key,
+ "creator_id": client.creator_id,
+ "last_key_update": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(client.last_key_update / 1000),
+ ),
+ "last_used": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(client.last_used / 1000),
+ ),
+ }
+ return client_info
+
+
+@_api_permission_denied_handler("users")
+def generate_users_dict(module, fusion):
+ users_info = {}
+ api_instance = purefusion.IdentityManagerApi(fusion)
+ users = api_instance.list_users()
+ for user in users:
+ users_info[user.name] = {
+ "display_name": user.display_name,
+ "email": user.email,
+ "id": user.id,
+ }
+ return users_info
+
+
+@_api_permission_denied_handler("hardware_types")
+def generate_hardware_types_dict(module, fusion):
+ hardware_info = {}
+ api_instance = purefusion.HardwareTypesApi(fusion)
+ hw_types = api_instance.list_hardware_types()
+ for hw_type in hw_types.items:
+ hardware_info[hw_type.name] = {
+ "array_type": hw_type.array_type,
+ "display_name": hw_type.display_name,
+ "media_type": hw_type.media_type,
+ }
+ return hardware_info
+
+
+@_api_permission_denied_handler("storage_classes")
+def generate_sc_dict(module, fusion):
+ sc_info = {}
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ services = ss_api_instance.list_storage_services()
+ for service in services.items:
+ classes = sc_api_instance.list_storage_classes(
+ storage_service_name=service.name,
+ )
+ for s_class in classes.items:
+ sc_info[s_class.name] = {
+ "bandwidth_limit": getattr(s_class, "bandwidth_limit", None),
+ "iops_limit": getattr(s_class, "iops_limit", None),
+ "size_limit": getattr(s_class, "size_limit", None),
+ "display_name": s_class.display_name,
+ "storage_service": service.name,
+ }
+ return sc_info
+
+
+@_api_permission_denied_handler("storage_services")
+def generate_storserv_dict(module, fusion):
+ ss_dict = {}
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ services = ss_api_instance.list_storage_services()
+ for service in services.items:
+ ss_dict[service.name] = {
+ "display_name": service.display_name,
+ "hardware_types": None,
+ }
+ # can be None if we don't have permission to see this
+ if service.hardware_types is not None:
+ ss_dict[service.name]["hardware_types"] = []
+ for hwtype in service.hardware_types:
+ ss_dict[service.name]["hardware_types"].append(hwtype.name)
+ return ss_dict
+
+
+@_api_permission_denied_handler("storage_endpoints")
+def generate_se_dict(module, fusion):
+ se_dict = {}
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ endpoints = se_api_instance.list_storage_endpoints(
+ region_name=region.name,
+ availability_zone_name=az.name,
+ )
+ for endpoint in endpoints.items:
+ name = region.name + "/" + az.name + "/" + endpoint.name
+ se_dict[name] = {
+ "display_name": endpoint.display_name,
+ "endpoint_type": endpoint.endpoint_type,
+ "iscsi_interfaces": [],
+ }
+ for iface in endpoint.iscsi.discovery_interfaces:
+ dct = {
+ "address": iface.address,
+ "gateway": iface.gateway,
+ "mtu": iface.mtu,
+ "network_interface_groups": None,
+ }
+ if iface.network_interface_groups is not None:
+ dct["network_interface_groups"] = [
+ nig.name for nig in iface.network_interface_groups
+ ]
+ se_dict[name]["iscsi_interfaces"].append(dct)
+ return se_dict
+
+
+@_api_permission_denied_handler("network_interface_groups")
+def generate_nigs_dict(module, fusion):
+ nigs_dict = {}
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ nigs = nig_api_instance.list_network_interface_groups(
+ region_name=region.name,
+ availability_zone_name=az.name,
+ )
+ for nig in nigs.items:
+ name = region.name + "/" + az.name + "/" + nig.name
+ nigs_dict[name] = {
+ "display_name": nig.display_name,
+ "gateway": nig.eth.gateway,
+ "prefix": nig.eth.prefix,
+ "mtu": nig.eth.mtu,
+ }
+ return nigs_dict
+
+
+@_api_permission_denied_handler("snapshots")
+def generate_snap_dicts(module, fusion):
+ snap_dict = {}
+ vsnap_dict = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ snap_api_instance = purefusion.SnapshotsApi(fusion)
+ vsnap_api_instance = purefusion.VolumeSnapshotsApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ snaps = snap_api_instance.list_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for snap in snaps.items:
+ snap_name = tenant.name + "/" + tenant_space.name + "/" + snap.name
+ secs, mins, hours = _convert_microseconds(snap.time_remaining)
+ snap_dict[snap_name] = {
+ "display_name": snap.display_name,
+ "protection_policy": snap.protection_policy,
+ "time_remaining": "{0} hours, {1} mins, {2} secs".format(
+ int(hours), int(mins), int(secs)
+ ),
+ "volume_snapshots_link": snap.volume_snapshots_link,
+ }
+ vsnaps = vsnap_api_instance.list_volume_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ snapshot_name=snap.name,
+ )
+ for vsnap in vsnaps.items:
+ vsnap_name = (
+ tenant.name
+ + "/"
+ + tenant_space.name
+ + "/"
+ + snap.name
+ + "/"
+ + vsnap.name
+ )
+ secs, mins, hours = _convert_microseconds(vsnap.time_remaining)
+ vsnap_dict[vsnap_name] = {
+ "size": vsnap.size,
+ "display_name": vsnap.display_name,
+ "protection_policy": vsnap.protection_policy,
+ "serial_number": vsnap.serial_number,
+ "created_at": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(vsnap.created_at / 1000),
+ ),
+ "time_remaining": "{0} hours, {1} mins, {2} secs".format(
+ int(hours), int(mins), int(secs)
+ ),
+ "placement_group": vsnap.placement_group.name,
+ }
+ return snap_dict, vsnap_dict
+
+
+@_api_permission_denied_handler("volumes")
+def generate_volumes_dict(module, fusion):
+ volume_info = {}
+
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ vol_api_instance = purefusion.VolumesApi(fusion)
+ tenant_space_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenant_space_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ volumes = vol_api_instance.list_volumes(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for volume in volumes.items:
+ vol_name = tenant.name + "/" + tenant_space.name + "/" + volume.name
+ volume_info[vol_name] = {
+ "tenant": tenant.name,
+ "tenant_space": tenant_space.name,
+ "name": volume.name,
+ "size": volume.size,
+ "display_name": volume.display_name,
+ "placement_group": volume.placement_group.name,
+ "source_volume_snapshot": getattr(
+ volume.source_volume_snapshot, "name", None
+ ),
+ "protection_policy": getattr(
+ volume.protection_policy, "name", None
+ ),
+ "storage_class": volume.storage_class.name,
+ "serial_number": volume.serial_number,
+ "target": {},
+ "array": getattr(volume.array, "name", None),
+ }
+
+ volume_info[vol_name]["target"] = {
+ "iscsi": {
+ "addresses": volume.target.iscsi.addresses,
+ "iqn": volume.target.iscsi.iqn,
+ },
+ "nvme": {
+ "addresses": None,
+ "nqn": None,
+ },
+ "fc": {
+ "addresses": None,
+ "wwns": None,
+ },
+ }
+ return volume_info
+
+
+def main():
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(gather_subset=dict(default="minimum", type="list", elements="str"))
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ # will handle all errors (except #403 which should be handled in code)
+ fusion = setup_fusion(module)
+
+ subset = [test.lower() for test in module.params["gather_subset"]]
+ valid_subsets = (
+ "all",
+ "minimum",
+ "roles",
+ "users",
+ "placements",
+ "arrays",
+ "hardware_types",
+ "volumes",
+ "hosts",
+ "storage_classes",
+ "protection_policies",
+ "placement_groups",
+ "interfaces",
+ "zones",
+ "nigs",
+ "storage_endpoints",
+ "snapshots",
+ "storage_services",
+ "tenants",
+ "tenant_spaces",
+ "network_interface_groups",
+ "api_clients",
+ "availability_zones",
+ "host_access_policies",
+ "network_interfaces",
+ "regions",
+ )
+ for option in subset:
+ if option not in valid_subsets:
+ module.fail_json(
+ msg=f"value gather_subset must be one or more of: {','.join(valid_subsets)}, got: {','.join(subset)}\nvalue {option} is not allowed"
+ )
+
+ info = {}
+
+ if "minimum" in subset or "all" in subset:
+ info["default"] = generate_default_dict(module, fusion)
+ if "hardware_types" in subset or "all" in subset:
+ info["hardware_types"] = generate_hardware_types_dict(module, fusion)
+ if "users" in subset or "all" in subset:
+ info["users"] = generate_users_dict(module, fusion)
+ if "regions" in subset or "all" in subset:
+ info["regions"] = generate_regions_dict(module, fusion)
+ if "availability_zones" in subset or "all" in subset or "zones" in subset:
+ info["availability_zones"] = generate_zones_dict(module, fusion)
+ if "zones" in subset:
+ module.warn(
+ "The 'zones' subset is deprecated and will be removed in the version 2.0.0\nUse 'availability_zones' subset instead."
+ )
+ if "roles" in subset or "all" in subset:
+ info["roles"] = generate_roles_dict(module, fusion)
+ info["role_assignments"] = generate_ras_dict(module, fusion)
+ if "storage_services" in subset or "all" in subset:
+ info["storage_services"] = generate_storserv_dict(module, fusion)
+ if "volumes" in subset or "all" in subset:
+ info["volumes"] = generate_volumes_dict(module, fusion)
+ if "protection_policies" in subset or "all" in subset:
+ info["protection_policies"] = generate_pp_dict(module, fusion)
+ if "placement_groups" in subset or "all" in subset or "placements" in subset:
+ info["placement_groups"] = generate_pg_dict(module, fusion)
+ if "placements" in subset:
+ module.warn(
+ "The 'placements' subset is deprecated and will be removed in the version 1.7.0"
+ )
+ if "storage_classes" in subset or "all" in subset:
+ info["storage_classes"] = generate_sc_dict(module, fusion)
+ if "network_interfaces" in subset or "all" in subset or "interfaces" in subset:
+ info["network_interfaces"] = generate_nics_dict(module, fusion)
+ if "interfaces" in subset:
+ module.warn(
+ "The 'interfaces' subset is deprecated and will be removed in the version 2.0.0\nUse 'network_interfaces' subset instead."
+ )
+ if "host_access_policies" in subset or "all" in subset or "hosts" in subset:
+ info["host_access_policies"] = generate_hap_dict(module, fusion)
+ if "hosts" in subset:
+ module.warn(
+ "The 'hosts' subset is deprecated and will be removed in the version 2.0.0\nUse 'host_access_policies' subset instead."
+ )
+ if "arrays" in subset or "all" in subset:
+ info["arrays"] = generate_array_dict(module, fusion)
+ if "tenants" in subset or "all" in subset:
+ info["tenants"] = generate_tenant_dict(module, fusion)
+ if "tenant_spaces" in subset or "all" in subset:
+ info["tenant_spaces"] = generate_ts_dict(module, fusion)
+ if "storage_endpoints" in subset or "all" in subset:
+ info["storage_endpoints"] = generate_se_dict(module, fusion)
+ if "api_clients" in subset or "all" in subset:
+ info["api_clients"] = generate_api_client_dict(module, fusion)
+ if "network_interface_groups" in subset or "all" in subset or "nigs" in subset:
+ info["network_interface_groups"] = generate_nigs_dict(module, fusion)
+ if "nigs" in subset:
+ module.warn(
+ "The 'nigs' subset is deprecated and will be removed in the version 1.7.0"
+ )
+ if "snapshots" in subset or "all" in subset:
+ snap_dicts = generate_snap_dicts(module, fusion)
+ if snap_dicts is not None:
+ info["snapshots"], info["volume_snapshots"] = snap_dicts
+ else:
+ info["snapshots"], info["volume_snapshots"] = None, None
+
+ module.exit_json(changed=False, fusion_info=info)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py
new file mode 100644
index 000000000..6816ed841
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ni
+version_added: '1.0.0'
+short_description: Manage network interfaces in Pure Storage Fusion
+description:
+- Update parameters of network interfaces in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the network interface.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the network interface.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - The name of the region the availability zone is in.
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the network interface.
+ type: str
+ required: true
+ array:
+ description:
+ - The name of the array the network interface belongs to.
+ type: str
+ required: true
+ eth:
+ description:
+ - The IP address associated with the network interface.
+ - IP address must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ - Required together with `network_interface_group` parameter.
+ type: str
+ enabled:
+ description:
+ - True if network interface is in use.
+ type: bool
+ network_interface_group:
+ description:
+ - The name of the network interface group this network interface belongs to.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Patch network interface
+ purestorage.fusion.fusion_ni:
+ name: foo
+ region: us-west
+ availability_zone: bar
+ array: array0
+ eth: 10.21.200.124/24
+ enabled: true
+ network_interface_group: subnet-0
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.getters import (
+ get_array,
+ get_az,
+ get_region,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_network,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ni(module, fusion):
+ """Get Network Interface or None"""
+ ni_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ try:
+ return ni_api_instance.get_network_interface(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["array"],
+ net_intf_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def update_ni(module, fusion, ni):
+ """Update Network Interface"""
+ ni_api_instance = purefusion.NetworkInterfacesApi(fusion)
+
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ni.display_name
+ ):
+ patch = purefusion.NetworkInterfacePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if module.params["enabled"] is not None and module.params["enabled"] != ni.enabled:
+ patch = purefusion.NetworkInterfacePatch(
+ enabled=purefusion.NullableBoolean(module.params["enabled"]),
+ )
+ patches.append(patch)
+
+ if (
+ module.params["network_interface_group"]
+ and module.params["network_interface_group"] != ni.network_interface_group
+ ):
+ if module.params["eth"] and module.params["eth"] != ni.eth:
+ patch = purefusion.NetworkInterfacePatch(
+ eth=purefusion.NetworkInterfacePatchEth(
+ purefusion.NullableString(module.params["eth"])
+ ),
+ network_interface_group=purefusion.NullableString(
+ module.params["network_interface_group"]
+ ),
+ )
+ else:
+ patch = purefusion.NetworkInterfacePatch(
+ network_interface_group=purefusion.NullableString(
+ module.params["network_interface_group"]
+ ),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ni_api_instance.update_network_interface(
+ patch,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["array"],
+ net_intf_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ array=dict(type="str", required=True),
+ eth=dict(type="str"),
+ enabled=dict(type="bool"),
+ network_interface_group=dict(type="str"),
+ )
+ )
+
+ required_by = {
+ "eth": "network_interface_group",
+ }
+
+ module = AnsibleModule(
+ argument_spec,
+ supports_check_mode=True,
+ required_by=required_by,
+ )
+
+ fusion = setup_fusion(module)
+
+ if module.params["eth"] and not is_valid_network(module.params["eth"]):
+ module.fail_json(
+ msg="`eth` '{0}' is not a valid address in CIDR notation".format(
+ module.params["eth"]
+ )
+ )
+
+ if not get_region(module, fusion):
+ module.fail_json(
+ msg="Region {0} does not exist.".format(module.params["region"])
+ )
+
+ if not get_az(module, fusion):
+ module.fail_json(
+ msg="Availability Zone {0} does not exist.".format(
+ module.params["availability_zone"]
+ )
+ )
+
+ if not get_array(module, fusion):
+ module.fail_json(msg="Array {0} does not exist.".format(module.params["array"]))
+
+ ni = get_ni(module, fusion)
+ if not ni:
+ module.fail_json(
+ msg="Network Interface {0} does not exist".format(module.params["name"])
+ )
+
+ update_ni(module, fusion, ni)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py
new file mode 100644
index 000000000..d6056fd5a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_nig
+version_added: '1.0.0'
+short_description: Manage Network Interface Groups in Pure Storage Fusion
+description:
+- Create, delete and modify network interface groups in Pure Storage Fusion.
+- Currently this only supports a single tenant subnet per tenant network
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the network interface group.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the network interface group.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the network interface group should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the network interface group.
+ type: str
+ required: true
+ region:
+ description:
+ - Region for the network interface group.
+ type: str
+ required: true
+ gateway:
+ description:
+ - "Address of the subnet gateway.
+ Currently must be a valid IPv4 address."
+ type: str
+ mtu:
+ description:
+ - MTU setting for the subnet.
+ default: 1500
+ type: int
+ group_type:
+ description:
+ - The type of network interface group.
+ type: str
+ default: eth
+ choices: [ eth ]
+ prefix:
+ description:
+ - "Network prefix in CIDR notation.
+ Required to create a new network interface group.
+ Currently only IPv4 addresses with subnet mask are supported."
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new network interface group foo in AZ bar
+ purestorage.fusion.fusion_nig:
+ name: foo
+ availability_zone: bar
+ region: region1
+ mtu: 9000
+ gateway: 10.21.200.1
+ prefix: 10.21.200.0/24
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete network interface group foo in AZ bar
+ purestorage.fusion.fusion_nig:
+ name: foo
+ availability_zone: bar
+ region: region1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_address,
+ is_valid_network,
+ is_address_in_network,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_nig(module, fusion):
+ """Check Network Interface Group"""
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ try:
+ return nig_api_instance.get_network_interface_group(
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_nig(module, fusion):
+ """Create Network Interface Group"""
+
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+
+ changed = False
+ if module.params["gateway"] and not is_address_in_network(
+ module.params["gateway"], module.params["prefix"]
+ ):
+ module.fail_json(msg="`gateway` must be an address in subnet `prefix`")
+
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+ if module.params["group_type"] == "eth":
+ if module.params["gateway"]:
+ eth = purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module.params["prefix"],
+ gateway=module.params["gateway"],
+ mtu=module.params["mtu"],
+ )
+ else:
+ eth = purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module.params["prefix"],
+ mtu=module.params["mtu"],
+ )
+ nig = purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=eth,
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = nig_api_instance.create_network_interface_group(
+ nig,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ await_operation(fusion, op)
+ changed = True
+ else:
+ # to prevent future unintended error
+ module.warn(f"group_type={module.params['group_type']} is not implemented")
+
+ module.exit_json(changed=changed)
+
+
+def delete_nig(module, fusion):
+ """Delete Network Interface Group"""
+ changed = True
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ if not module.check_mode:
+ op = nig_api_instance.delete_network_interface_group(
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def update_nig(module, fusion, nig):
+ """Update Network Interface Group"""
+
+ nifg_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != nig.display_name
+ ):
+ patch = purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = nifg_api_instance.update_network_interface_group(
+ patch,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ region=dict(type="str", required=True),
+ prefix=dict(type="str"),
+ gateway=dict(type="str"),
+ mtu=dict(type="int", default=1500),
+ group_type=dict(type="str", default="eth", choices=["eth"]),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ if module.params["prefix"] and not is_valid_network(module.params["prefix"]):
+ module.fail_json(
+ msg="`prefix` '{0}' is not a valid address in CIDR notation".format(
+ module.params["prefix"]
+ )
+ )
+ if module.params["gateway"] and not is_valid_address(module.params["gateway"]):
+ module.fail_json(
+ msg="`gateway` '{0}' is not a valid address".format(
+ module.params["gateway"]
+ )
+ )
+
+ nig = get_nig(module, fusion)
+
+ if state == "present" and not nig:
+ module.fail_on_missing_params(["prefix"])
+ create_nig(module, fusion)
+ elif state == "present" and nig:
+ update_nig(module, fusion, nig)
+ elif state == "absent" and nig:
+ delete_nig(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py
new file mode 100644
index 000000000..57843d896
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py
@@ -0,0 +1,278 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_pg
+version_added: '1.0.0'
+short_description: Manage placement groups in Pure Storage Fusion
+description:
+- Create, update or delete a placement groups in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the placement group.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the placement group.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the placement group should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ tenant_space:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ region:
+ description:
+ - The name of the region the availability zone is in.
+ type: str
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone the placement group is in.
+ type: str
+ storage_service:
+ description:
+ - The name of the storage service to create the placement group for.
+ type: str
+ array:
+ description:
+ - "Array to place the placement group to. Changing it (i.e. manual migration)
+ is an elevated operation."
+ type: str
+ placement_engine:
+ description:
+ - For workload placement recommendations from Pure1 Meta, use C(pure1meta).
+ - Please note that this might increase volume creation time.
+ type: str
+ choices: [ heuristics, pure1meta ]
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new placement group named foo
+ purestorage.fusion.fusion_pg:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ availability_zone: az1
+ region: region1
+ storage_service: storage_service_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete placement group foo
+ purestorage.fusion.fusion_pg:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_pg(module, fusion):
+ """Return Placement Group or None"""
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ try:
+ return pg_api_instance.get_placement_group(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ placement_group_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_pg(module, fusion):
+ """Create Placement Group"""
+
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ group = purefusion.PlacementGroupPost(
+ availability_zone=module.params["availability_zone"],
+ name=module.params["name"],
+ display_name=display_name,
+ region=module.params["region"],
+ storage_service=module.params["storage_service"],
+ )
+ op = pg_api_instance.create_placement_group(
+ group,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def update_display_name(module, fusion, patches, pg):
+ if not module.params["display_name"]:
+ return
+ if module.params["display_name"] == pg.display_name:
+ return
+ patch = purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+
+def update_array(module, fusion, patches, pg):
+ if not module.params["array"]:
+ return
+ if not pg.array:
+ module.warn(
+ "cannot see placement group array, probably missing required permissions to change it"
+ )
+ return
+ if pg.array.name == module.params["array"]:
+ return
+
+ patch = purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(module.params["array"]),
+ )
+ patches.append(patch)
+
+
+def update_pg(module, fusion, pg):
+ """Update Placement Group"""
+
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ patches = []
+
+ update_display_name(module, fusion, patches, pg)
+ update_array(module, fusion, patches, pg)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = pg_api_instance.update_placement_group(
+ patch,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ placement_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def delete_pg(module, fusion):
+ """Delete Placement Group"""
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ if not module.check_mode:
+ op = pg_api_instance.delete_placement_group(
+ placement_group_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ tenant=dict(type="str", required=True),
+ tenant_space=dict(type="str", required=True),
+ region=dict(type="str"),
+ availability_zone=dict(type="str", aliases=["az"]),
+ storage_service=dict(type="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ array=dict(type="str"),
+ placement_engine=dict(
+ type="str",
+ choices=["heuristics", "pure1meta"],
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ if module.params["placement_engine"]:
+ module.warn("placement_engine parameter will be deprecated in version 2.0.0")
+
+ changed = False
+
+ state = module.params["state"]
+ pgroup = get_pg(module, fusion)
+
+ if state == "present" and not pgroup:
+ module.fail_on_missing_params(
+ ["region", "availability_zone", "storage_service"]
+ )
+ changed = create_pg(module, fusion) or changed
+ if module.params["array"]:
+ # changing placement requires additional update
+ pgroup = get_pg(module, fusion)
+ changed = update_pg(module, fusion, pgroup) or changed
+ elif state == "present" and pgroup:
+ changed = update_pg(module, fusion, pgroup) or changed
+ elif state == "absent" and pgroup:
+ changed = delete_pg(module, fusion) or changed
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py
new file mode 100644
index 000000000..abce9195c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_pp
+version_added: '1.0.0'
+short_description: Manage protection policies in Pure Storage Fusion
+description:
+- Manage protection policies in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the protection policy.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the protection policy should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the protection policy.
+ - If not provided, defaults to I(name).
+ type: str
+ local_rpo:
+ description:
+ - Recovery Point Objective for snapshots.
+ - Value should be specified in minutes.
+ - Minimum value is 10 minutes.
+ type: str
+ local_retention:
+ description:
+ - Retention Duration for periodic snapshots.
+ - Minimum value is 10 minutes.
+ - Value can be provided as m(inutes), h(ours),
+ d(ays), w(eeks), or y(ears).
+ - If no unit is provided, minutes are assumed.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new protection policy foo
+ purestorage.fusion.fusion_pp:
+ name: foo
+ local_rpo: 10
+ local_retention: 4d
+ display_name: "foo pp"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete protection policy foo
+ purestorage.fusion.fusion_pp:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_minutes,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_pp(module, fusion):
+ """Return Protection Policy or None"""
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ try:
+ return pp_api_instance.get_protection_policy(
+ protection_policy_name=module.params["name"]
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_pp(module, fusion):
+ """Create Protection Policy"""
+
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ local_rpo = parse_minutes(module, module.params["local_rpo"])
+ local_retention = parse_minutes(module, module.params["local_retention"])
+ if local_retention < 1:
+ module.fail_json(msg="Local Retention must be a minimum of 1 minutes")
+ if local_rpo < 10:
+ module.fail_json(msg="Local RPO must be a minimum of 10 minutes")
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ op = pp_api_instance.create_protection_policy(
+ purefusion.ProtectionPolicyPost(
+ name=module.params["name"],
+ display_name=display_name,
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT" + str(local_rpo) + "M"),
+ purefusion.Retention(
+ type="Retention", after="PT" + str(local_retention) + "M"
+ ),
+ ],
+ )
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_pp(module, fusion):
+ """Delete Protection Policy"""
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = pp_api_instance.delete_protection_policy(
+ protection_policy_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ local_rpo=dict(type="str"),
+ local_retention=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ policy = get_pp(module, fusion)
+
+ if not policy and state == "present":
+ module.fail_on_missing_params(["local_rpo", "local_retention"])
+ create_pp(module, fusion)
+ elif policy and state == "absent":
+ delete_pp(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py
new file mode 100644
index 000000000..7cfc7d866
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py
@@ -0,0 +1,281 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ra
+version_added: '1.0.0'
+short_description: Manage role assignments in Pure Storage Fusion
+description:
+- Create or delete a storage class in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ role:
+ description:
+ - The name of the role to be assigned/unassigned.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the role assingment should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ user:
+ description:
+ - The username to assign the role to.
+ - Currently this only supports the Pure1 App ID.
+ - This should be provide in the same format as I(issuer_id).
+ type: str
+ principal:
+ description:
+ - The unique ID of the principal (User or API Client) to assign to the role.
+ type: str
+ api_client_key:
+ description:
+ - The key of API client to assign the role to.
+ type: str
+ scope:
+ description:
+ - The level to which the role is assigned.
+ choices: [ organization, tenant, tenant_space ]
+ default: organization
+ type: str
+ tenant:
+ description:
+ - The name of the tenant the user has the role applied to.
+ - Must be provided if I(scope) is set to either C(tenant) or C(tenant_space).
+ type: str
+ tenant_space:
+ description:
+ - The name of the tenant_space the user has the role applied to.
+ - Must be provided if I(scope) is set to C(tenant_space).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Assign role foo to user in tenant bar
+ purestorage.fusion.fusion_ra:
+ name: foo
+ user: key_name
+ tenant: bar
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete role foo from user in tenant bar
+ purestorage.fusion.fusion_ra:
+ name: foo
+ user: key_name
+ tenant: bar
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_principal(module, fusion):
+ if module.params["principal"]:
+ return module.params["principal"]
+ if module.params["user"]:
+ principal = user_to_principal(fusion, module.params["user"])
+ if not principal:
+ module.fail_json(
+ msg="User {0} does not exist".format(module.params["user"])
+ )
+ return principal
+ if module.params["api_client_key"]:
+ principal = apiclient_to_principal(fusion, module.params["api_client_key"])
+ if not principal:
+ module.fail_json(
+ msg="API Client with key {0} does not exist".format(
+ module.params["api_client_key"]
+ )
+ )
+ return principal
+
+
+def user_to_principal(fusion, user_id):
+ """Given a human readable Fusion user, such as a Pure 1 App ID
+ return the associated principal
+ """
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ users = id_api_instance.list_users()
+ for user in users:
+ if user.name == user_id:
+ return user.id
+ return None
+
+
+def apiclient_to_principal(fusion, api_client_key):
+ """Given an API client key, such as "pure1:apikey:123xXxyYyzYzASDF" (also known as issuer_id),
+ return the associated principal
+ """
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ api_clients = id_api_instance.list_users(name=api_client_key)
+ if len(api_clients) > 0:
+ return api_clients[0].id
+ return None
+
+
+def get_scope(params):
+ """Given a scope type and associated tenant
+ and tenant_space, return the scope_link
+ """
+ scope_link = None
+ if params["scope"] == "organization":
+ scope_link = "/"
+ elif params["scope"] == "tenant":
+ scope_link = "/tenants/" + params["tenant"]
+ elif params["scope"] == "tenant_space":
+ scope_link = (
+ "/tenants/" + params["tenant"] + "/tenant-spaces/" + params["tenant_space"]
+ )
+ return scope_link
+
+
+def get_ra(module, fusion):
+ """Return Role Assignment or None"""
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ try:
+ principal = get_principal(module, fusion)
+ assignments = ra_api_instance.list_role_assignments(
+ role_name=module.params["role"],
+ principal=principal,
+ )
+ for assign in assignments:
+ scope = get_scope(module.params)
+ if assign.scope.self_link == scope:
+ return assign
+ return None
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_ra(module, fusion):
+ """Create Role Assignment"""
+
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ principal = get_principal(module, fusion)
+ scope = get_scope(module.params)
+ assignment = purefusion.RoleAssignmentPost(scope=scope, principal=principal)
+ op = ra_api_instance.create_role_assignment(
+ assignment, role_name=module.params["role"]
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def delete_ra(module, fusion):
+ """Delete Role Assignment"""
+ changed = True
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ if not module.check_mode:
+ ra_name = get_ra(module, fusion).name
+ op = ra_api_instance.delete_role_assignment(
+ role_name=module.params["role"], role_assignment_name=ra_name
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ api_client_key=dict(type="str", no_log=True),
+ principal=dict(type="str"),
+ role=dict(
+ type="str",
+ required=True,
+ deprecated_aliases=[
+ dict(
+ name="name",
+ date="2023-07-26",
+ collection_name="purefusion.fusion",
+ )
+ ],
+ ),
+ scope=dict(
+ type="str",
+ default="organization",
+ choices=["organization", "tenant", "tenant_space"],
+ ),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ tenant=dict(type="str"),
+ tenant_space=dict(type="str"),
+ user=dict(type="str"),
+ )
+ )
+
+ required_if = [
+ ["scope", "tenant", ["tenant"]],
+ ["scope", "tenant_space", ["tenant", "tenant_space"]],
+ ]
+ mutually_exclusive = [
+ ("user", "principal", "api_client_key"),
+ ]
+ required_one_of = [
+ ("user", "principal", "api_client_key"),
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_if=required_if,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive,
+ required_one_of=required_one_of,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ role_assignment = get_ra(module, fusion)
+
+ if not role_assignment and state == "present":
+ create_ra(module, fusion)
+ elif role_assignment and state == "absent":
+ delete_ra(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py
new file mode 100644
index 000000000..fbcbff4b0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_region
+version_added: '1.1.0'
+short_description: Manage Regions in Pure Storage Fusion
+description:
+- Manage regions in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the Region.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the Region should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the Region.
+ - If not provided, defaults to I(name).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ display_name: "foo Region"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ display_name: "new foo Region"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+
+
+def get_region(module, fusion):
+ """Get Region or None"""
+ return getters.get_region(module, fusion, module.params["name"])
+
+
+def create_region(module, fusion):
+ """Create Region"""
+
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ region = purefusion.RegionPost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = reg_api_instance.create_region(region)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_region(module, fusion):
+ """Delete Region"""
+
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = reg_api_instance.delete_region(region_name=module.params["name"])
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_region(module, fusion, region):
+ """Update Region settings"""
+ changed = False
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != region.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ reg = purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module.params["display_name"])
+ )
+ op = reg_api_instance.update_region(
+ reg,
+ region_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ region = get_region(module, fusion)
+
+ if not region and state == "present":
+ create_region(module, fusion)
+ elif region and state == "present":
+ update_region(module, fusion, region)
+ elif region and state == "absent":
+ delete_region(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py
new file mode 100644
index 000000000..2327b8d48
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py
@@ -0,0 +1,255 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_sc
+version_added: '1.0.0'
+short_description: Manage storage classes in Pure Storage Fusion
+description:
+- Manage a storage class in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+- It is not currently possible to update bw_limit or
+ iops_limit after a storage class has been created.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the storage class.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the storage class should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the storage class.
+ - If not provided, defaults to I(name).
+ type: str
+ size_limit:
+ description:
+ - Volume size limit in M, G, T or P units.
+ - Must be between 1MB and 4PB.
+ - If not provided at creation, this will default to 4PB.
+ type: str
+ bw_limit:
+ description:
+ - The bandwidth limit in M or G units.
+ M will set MB/s.
+ G will set GB/s.
+ - Must be between 1MB/s and 512GB/s.
+ - If not provided at creation, this will default to 512GB/s.
+ type: str
+ iops_limit:
+ description:
+ - The IOPs limit - use value or K or M.
+ K will mean 1000.
+ M will mean 1000000.
+ - Must be between 100 and 100000000.
+ - If not provided at creation, this will default to 100000000.
+ type: str
+ storage_service:
+ description:
+ - Storage service to which the storage class belongs.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage class foo
+ purestorage.fusion.fusion_sc:
+ name: foo
+ size_limit: 100G
+ iops_limit: 100000
+ bw_limit: 25M
+ storage_service: service1
+ display_name: "test class"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update storage class (only display_name change is supported)
+ purestorage.fusion.fusion_sc:
+ name: foo
+ display_name: "main class"
+ storage_service: service1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete storage class
+ purestorage.fusion.fusion_sc:
+ name: foo
+ storage_service: service1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_number_with_metric_suffix,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_sc(module, fusion):
+ """Return Storage Class or None"""
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ try:
+ return sc_api_instance.get_storage_class(
+ storage_class_name=module.params["name"],
+ storage_service_name=module.params["storage_service"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_sc(module, fusion):
+ """Create Storage Class"""
+
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+
+ if not module.params["size_limit"]:
+ module.params["size_limit"] = "4P"
+ if not module.params["iops_limit"]:
+ module.params["iops_limit"] = "100000000"
+ if not module.params["bw_limit"]:
+ module.params["bw_limit"] = "512G"
+ size_limit = parse_number_with_metric_suffix(module, module.params["size_limit"])
+ iops_limit = int(
+ parse_number_with_metric_suffix(
+ module, module.params["iops_limit"], factor=1000
+ )
+ )
+ bw_limit = parse_number_with_metric_suffix(module, module.params["bw_limit"])
+ if bw_limit < 1048576 or bw_limit > 549755813888: # 1MB/s to 512GB/s
+ module.fail_json(msg="Bandwidth limit is not within the required range")
+ if iops_limit < 100 or iops_limit > 100_000_000:
+ module.fail_json(msg="IOPs limit is not within the required range")
+ if size_limit < 1048576 or size_limit > 4503599627370496: # 1MB to 4PB
+ module.fail_json(msg="Size limit is not within the required range")
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ s_class = purefusion.StorageClassPost(
+ name=module.params["name"],
+ size_limit=size_limit,
+ iops_limit=iops_limit,
+ bandwidth_limit=bw_limit,
+ display_name=display_name,
+ )
+ op = sc_api_instance.create_storage_class(
+ s_class, storage_service_name=module.params["storage_service"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_sc(module, fusion, s_class):
+ """Update Storage Class settings"""
+ changed = False
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != s_class.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ sclass = purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module.params["display_name"])
+ )
+ op = sc_api_instance.update_storage_class(
+ sclass,
+ storage_service_name=module.params["storage_service"],
+ storage_class_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_sc(module, fusion):
+ """Delete Storage Class"""
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = sc_api_instance.delete_storage_class(
+ storage_class_name=module.params["name"],
+ storage_service_name=module.params["storage_service"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ iops_limit=dict(type="str"),
+ bw_limit=dict(type="str"),
+ size_limit=dict(type="str"),
+ storage_service=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ s_class = get_sc(module, fusion)
+
+ if not s_class and state == "present":
+ create_sc(module, fusion)
+ elif s_class and state == "present":
+ update_sc(module, fusion, s_class)
+ elif s_class and state == "absent":
+ delete_sc(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py
new file mode 100644
index 000000000..9eed4bea0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py
@@ -0,0 +1,507 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Simon Dodsley (simon@purestorage.com), Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_se
+version_added: '1.0.0'
+short_description: Manage storage endpoints in Pure Storage Fusion
+description:
+- Create or delete storage endpoints in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the storage endpoint.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the storage endpoint.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the storage endpoint should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ region:
+ description:
+ - The name of the region the availability zone is in
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the storage endpoint.
+ type: str
+ required: true
+ endpoint_type:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - Type of the storage endpoint. Only iSCSI is available at the moment.
+ type: str
+ iscsi:
+ description:
+ - List of discovery interfaces.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description:
+ - IP address to be used in the subnet of the storage endpoint.
+ - IP address must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ type: str
+ gateway:
+ description:
+ - Address of the subnet gateway.
+ type: str
+ network_interface_groups:
+ description:
+ - List of network interface groups to assign to the address.
+ type: list
+ elements: str
+ cbs_azure_iscsi:
+ description:
+ - CBS Azure iSCSI
+ type: dict
+ suboptions:
+ storage_endpoint_collection_identity:
+ description:
+ - The Storage Endpoint Collection Identity which belongs to the Azure entities.
+ type: str
+ load_balancer:
+ description:
+ - The Load Balancer id which gives permissions to CBS array applications to modify the Load Balancer.
+ type: str
+ load_balancer_addresses:
+ description:
+ - The IPv4 addresses of the Load Balancer.
+ type: list
+ elements: str
+ network_interface_groups:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - List of network interface groups to assign to the storage endpoints.
+ type: list
+ elements: str
+ addresses:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - List of IP addresses to be used in the subnet of the storage endpoint.
+ - IP addresses must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ type: list
+ elements: str
+ gateway:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - Address of the subnet gateway.
+ - Currently this must be provided.
+ type: str
+
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ iscsi:
+ - address: 10.21.200.124/24
+ gateway: 10.21.200.1
+ network_interface_groups:
+ - subnet-0
+ - address: 10.21.200.36/24
+ gateway: 10.21.200.2
+ network_interface_groups:
+ - subnet-0
+ - subnet-1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Create new CBS storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ cbs_azure_iscsi:
+ storage_endpoint_collection_identity: "/subscriptions/sub/resourcegroups/sec/providers/ms/userAssignedIdentities/secId"
+ load_balancer: "/subscriptions/sub/resourcegroups/sec/providers/ms/loadBalancers/sec-lb"
+ load_balancer_addresses:
+ - 10.21.200.1
+ - 10.21.200.2
+ state: present
+ app_id: key_name
+ key_file: "az-admin-private-key.pem"
+
+- name: Delete storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: (DEPRECATED) Create new storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ gateway: 10.21.200.1
+ region: us-west
+ addresses:
+ - 10.21.200.124/24
+ - 10.21.200.36/24
+ network_interface_groups:
+ - subnet-0
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_network,
+ is_valid_address,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+#######################################################################
+# DEPRECATED CODE SECTION STARTS
+
+
+def create_se_old(module, fusion):
+ """Create Storage Endpoint"""
+
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+
+ changed = True
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ ifaces = []
+ for address in module.params["addresses"]:
+ if module.params["gateway"]:
+ iface = purefusion.StorageEndpointIscsiDiscoveryInterfacePost(
+ address=address,
+ gateway=module.params["gateway"],
+ network_interface_groups=module.params["network_interface_groups"],
+ )
+ else:
+ iface = purefusion.StorageEndpointIscsiDiscoveryInterfacePost(
+ address=address,
+ network_interface_groups=module.params["network_interface_groups"],
+ )
+ ifaces.append(iface)
+ op = purefusion.StorageEndpointPost(
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=ifaces,
+ ),
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = se_api_instance.create_storage_endpoint(
+ op,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+# DEPRECATED CODE SECTION ENDS
+#######################################################################
+
+
+def get_se(module, fusion):
+ """Storage Endpoint or None"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ try:
+ return se_api_instance.get_storage_endpoint(
+ region_name=module.params["region"],
+ storage_endpoint_name=module.params["name"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_se(module, fusion):
+ """Create Storage Endpoint"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+
+ if not module.check_mode:
+ endpoint_type = None
+
+ iscsi = None
+ if module.params["iscsi"] is not None:
+ iscsi = purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module.params["iscsi"]
+ ]
+ )
+ endpoint_type = "iscsi"
+
+ cbs_azure_iscsi = None
+ if module.params["cbs_azure_iscsi"] is not None:
+ cbs_azure_iscsi = purefusion.StorageEndpointCbsAzureIscsiPost(
+ storage_endpoint_collection_identity=module.params["cbs_azure_iscsi"][
+ "storage_endpoint_collection_identity"
+ ],
+ load_balancer=module.params["cbs_azure_iscsi"]["load_balancer"],
+ load_balancer_addresses=module.params["cbs_azure_iscsi"][
+ "load_balancer_addresses"
+ ],
+ )
+ endpoint_type = "cbs-azure-iscsi"
+
+ op = se_api_instance.create_storage_endpoint(
+ purefusion.StorageEndpointPost(
+ name=module.params["name"],
+ display_name=module.params["display_name"] or module.params["name"],
+ endpoint_type=endpoint_type,
+ iscsi=iscsi,
+ cbs_azure_iscsi=cbs_azure_iscsi,
+ ),
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=True)
+
+
+def delete_se(module, fusion):
+ """Delete Storage Endpoint"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ if not module.check_mode:
+ op = se_api_instance.delete_storage_endpoint(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ storage_endpoint_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=True)
+
+
+def update_se(module, fusion, se):
+ """Update Storage Endpoint"""
+
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != se.display_name
+ ):
+ patch = purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = se_api_instance.update_storage_endpoint(
+ patch,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ storage_endpoint_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ iscsi=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str"),
+ gateway=dict(type="str"),
+ network_interface_groups=dict(type="list", elements="str"),
+ ),
+ ),
+ cbs_azure_iscsi=dict(
+ type="dict",
+ options=dict(
+ storage_endpoint_collection_identity=dict(type="str"),
+ load_balancer=dict(type="str"),
+ load_balancer_addresses=dict(type="list", elements="str"),
+ ),
+ ),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ # deprecated, will be removed in 2.0.0
+ endpoint_type=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ addresses=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ gateway=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ network_interface_groups=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ )
+ )
+
+ mutually_exclusive = [
+ ("iscsi", "cbs_azure_iscsi"),
+ # can not use both deprecated and new fields at the same time
+ ("iscsi", "cbs_azure_iscsi", "addresses"),
+ ("iscsi", "cbs_azure_iscsi", "gateway"),
+ ("iscsi", "cbs_azure_iscsi", "network_interface_groups"),
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+
+ if module.params["endpoint_type"] is not None:
+ module.warn(
+ "'endpoint_type' parameter is deprecated and will be removed in the version 2.0"
+ )
+
+ deprecated_parameters = {"addresses", "gateway", "network_interface_groups"}
+ used_deprecated_parameters = [
+ key
+ for key in list(deprecated_parameters & module.params.keys())
+ if module.params[key] is not None
+ ]
+
+ if len(used_deprecated_parameters) > 0:
+ # user uses deprecated module interface
+ for param_name in used_deprecated_parameters:
+ module.warn(
+ f"'{param_name}' parameter is deprecated and will be removed in the version 2.0"
+ )
+
+ if module.params["addresses"]:
+ for address in module.params["addresses"]:
+ if not is_valid_network(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid address in CIDR notation"
+ )
+
+ sendp = get_se(module, fusion)
+
+ if state == "present" and not sendp:
+ module.fail_on_missing_params(["addresses"])
+ if not (module.params["addresses"]):
+ module.fail_json(
+ msg="At least one entry in 'addresses' is required to create new storage endpoint"
+ )
+ create_se_old(module, fusion)
+ elif state == "present" and sendp:
+ update_se(module, fusion, sendp)
+ elif state == "absent" and sendp:
+ delete_se(module, fusion)
+ else:
+ # user uses new module interface
+ if module.params["iscsi"] is not None:
+ for endpoint in module.params["iscsi"]:
+ address = endpoint["address"]
+ if not is_valid_network(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid address in CIDR notation"
+ )
+ gateway = endpoint["gateway"]
+ if not is_valid_address(gateway):
+ module.fail_json(
+ msg=f"'{gateway}' is not a valid IPv4 address notation"
+ )
+ if module.params["cbs_azure_iscsi"] is not None:
+ for address in module.params["cbs_azure_iscsi"]["load_balancer_addresses"]:
+ if not is_valid_address(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid IPv4 address notation"
+ )
+
+ sendp = get_se(module, fusion)
+
+ if state == "present" and not sendp:
+ if (
+ module.params["iscsi"] is None
+ and module.params["cbs_azure_iscsi"] is None
+ ):
+ module.fail_json(
+ msg="either 'iscsi' or `cbs_azure_iscsi` parameter is required when creating storage endpoint"
+ )
+ create_se(module, fusion)
+ elif state == "present" and sendp:
+ update_se(module, fusion, sendp)
+ elif state == "absent" and sendp:
+ delete_se(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py
new file mode 100644
index 000000000..3fdbb07dd
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ss
+version_added: '1.0.0'
+short_description: Manage storage services in Pure Storage Fusion
+description:
+- Manage a storage services in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the storage service.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the storage service should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the storage service.
+ - If not provided, defaults to I(name).
+ type: str
+ hardware_types:
+ description:
+ - Hardware types to which the storage service applies.
+ type: list
+ elements: str
+ choices: [ flash-array-x, flash-array-c, flash-array-x-optane, flash-array-xl ]
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage service foo
+ purestorage.fusion.fusion_ss:
+ name: foo
+ hardware_types:
+ - flash-array-x
+ - flash-array-x-optane
+ display_name: "test class"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update storage service
+ purestorage.fusion.fusion_ss:
+ name: foo
+ display_name: "main class"
+ hardware_types:
+ - flash-array-c
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete storage service
+ purestorage.fusion.fusion_ss:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ss(module, fusion):
+ """Return Storage Service or None"""
+ return getters.get_ss(module, fusion, storage_service_name=module.params["name"])
+
+
+def create_ss(module, fusion):
+ """Create Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ s_service = purefusion.StorageServicePost(
+ name=module.params["name"],
+ display_name=display_name,
+ hardware_types=module.params["hardware_types"],
+ )
+ op = ss_api_instance.create_storage_service(s_service)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_ss(module, fusion):
+ """Delete Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = ss_api_instance.delete_storage_service(
+ storage_service_name=module.params["name"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_ss(module, fusion, ss):
+ """Update Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ss.display_name
+ ):
+ patch = purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ss_api_instance.update_storage_service(
+ patch,
+ storage_service_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ hardware_types=dict(
+ type="list",
+ elements="str",
+ choices=[
+ "flash-array-x",
+ "flash-array-c",
+ "flash-array-x-optane",
+ "flash-array-xl",
+ ],
+ ),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ s_service = get_ss(module, fusion)
+
+ if not s_service and state == "present":
+ module.fail_on_missing_params(["hardware_types"])
+ create_ss(module, fusion)
+ elif s_service and state == "present":
+ update_ss(module, fusion, s_service)
+ elif s_service and state == "absent":
+ delete_ss(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py
new file mode 100644
index 000000000..96e890a6b
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_tenant
+version_added: '1.0.0'
+short_description: Manage tenants in Pure Storage Fusion
+description:
+- Create,delete or update a tenant in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the tenant should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the tenant.
+ - If not provided, defaults to I(name).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo
+ display_name: "tenant foo"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_tenant(module, fusion):
+ """Return Tenant or None"""
+ return getters.get_tenant(module, fusion, tenant_name=module.params["name"])
+
+
+def create_tenant(module, fusion):
+ """Create Tenant"""
+
+ api_instance = purefusion.TenantsApi(fusion)
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ tenant = purefusion.TenantPost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = api_instance.create_tenant(tenant)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_tenant(module, fusion, tenant):
+ """Update Tenant settings"""
+ changed = False
+ api_instance = purefusion.TenantsApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != tenant.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ new_tenant = purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ op = api_instance.update_tenant(
+ new_tenant,
+ tenant_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_tenant(module, fusion):
+ """Delete Tenant"""
+ changed = True
+ api_instance = purefusion.TenantsApi(fusion)
+ if not module.check_mode:
+ op = api_instance.delete_tenant(tenant_name=module.params["name"])
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ tenant = get_tenant(module, fusion)
+
+ if not tenant and state == "present":
+ create_tenant(module, fusion)
+ elif tenant and state == "present":
+ update_tenant(module, fusion, tenant)
+ elif tenant and state == "absent":
+ delete_tenant(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py
new file mode 100644
index 000000000..717b1e46f
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py
@@ -0,0 +1,122 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_tn
+version_added: '1.0.0'
+deprecated:
+ removed_at_date: "2023-07-26"
+ why: Tenant Networks were removed as a concept in Pure Storage Fusion
+ alternative: most of the functionality can be replicated using M(purestorage.fusion.fusion_se) and M(purestorage.fusion.fusion_nig)
+short_description: Manage tenant networks in Pure Storage Fusion
+description:
+- Create or delete tenant networks in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+- Currently this only supports a single tenant subnet per tenant network.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the tenant network.
+ type: str
+ display_name:
+ description:
+ - The human name of the tenant network.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the tenant network should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ region:
+ description:
+ - The name of the region the availability zone is in
+ type: str
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the tenant network.
+ type: str
+ provider_subnets:
+ description:
+ - List of provider subnets to assign to the tenant networks subnet.
+ type: list
+ elements: str
+ addresses:
+ description:
+ - List of IP addresses to be used in the subnet of the tenant network.
+ - IP addresses must include a CIDR notation.
+ - IPv4 and IPv6 are fully supported.
+ type: list
+ elements: str
+ gateway:
+ description:
+ - Address of the subnet gateway.
+ - Currently this must be provided.
+ type: str
+ mtu:
+ description:
+ - MTU setting for the subnet.
+ default: 1500
+ type: int
+ prefix:
+ description:
+ - Network prefix in CIDR format.
+ - This will be deprecated soon.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+# this module does nothing, thus no example is provided
+EXAMPLES = r"""
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str"),
+ region=dict(type="str"),
+ display_name=dict(type="str"),
+ availability_zone=dict(type="str", aliases=["az"]),
+ prefix=dict(type="str"),
+ gateway=dict(type="str"),
+ mtu=dict(type="int", default=1500),
+ provider_subnets=dict(type="list", elements="str"),
+ addresses=dict(type="list", elements="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ module.warn(
+ "This module is deprecated, doesn't work, and will be removed in the version 2.0."
+ " Please, use purestorage.fusion.fusion_se and purestorage.fusion.fusion_nig instead."
+ )
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py
new file mode 100644
index 000000000..33fb0187a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ts
+version_added: '1.0.0'
+short_description: Manage tenant spaces in Pure Storage Fusion
+description:
+- Create, update or delete a tenant spaces in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the tenant space.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the tenant space should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new tenant space foo for tenant bar
+ purestorage.fusion.fusion_ts:
+ name: foo
+ tenant: bar
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete tenant space foo in tenant bar
+ purestorage.fusion.fusion_ts:
+ name: foo
+ tenant: bar
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ts(module, fusion):
+ """Tenant Space or None"""
+ return getters.get_ts(module, fusion, tenant_space_name=module.params["name"])
+
+
+def create_ts(module, fusion):
+ """Create Tenant Space"""
+
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ tspace = purefusion.TenantSpacePost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = ts_api_instance.create_tenant_space(
+ tspace,
+ tenant_name=module.params["tenant"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_ts(module, fusion, ts):
+ """Update Tenant Space"""
+
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ts.display_name
+ ):
+ patch = purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ts_api_instance.update_tenant_space(
+ patch,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def delete_ts(module, fusion):
+ """Delete Tenant Space"""
+ changed = True
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ if not module.check_mode:
+ op = ts_api_instance.delete_tenant_space(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ tenant=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ tspace = get_ts(module, fusion)
+
+ if state == "present" and not tspace:
+ create_ts(module, fusion)
+ elif state == "present" and tspace:
+ update_ts(module, fusion, tspace)
+ elif state == "absent" and tspace:
+ delete_ts(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py
new file mode 100644
index 000000000..5b19064f5
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py
@@ -0,0 +1,450 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Simon Dodsley (simon@purestorage.com), Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_volume
+version_added: '1.0.0'
+short_description: Manage volumes in Pure Storage Fusion
+description:
+- Create, update or delete a volume in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the volume.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the volume.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the volume should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ tenant_space:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ eradicate:
+ description:
+ - "Wipes the volume instead of a soft delete if true. Must be used with `state: absent`."
+ type: bool
+ default: false
+ size:
+ description:
+ - Volume size in M, G, T or P units.
+ type: str
+ storage_class:
+ description:
+ - The name of the storage class.
+ type: str
+ placement_group:
+ description:
+ - The name of the placement group.
+ type: str
+ protection_policy:
+ description:
+ - The name of the protection policy.
+ type: str
+ host_access_policies:
+ description:
+ - 'A list of host access policies to connect the volume to.
+ To clear, assign empty list: host_access_policies: []'
+ type: list
+ elements: str
+ rename:
+ description:
+ - New name for volume.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new volume named foo in storage_class fred
+ purestorage.fusion.fusion_volume:
+ name: foo
+ storage_class: fred
+ size: 1T
+ tenant: test
+ tenant_space: space_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Extend the size of an existing volume named foo
+ purestorage.fusion.fusion_volume:
+ name: foo
+ size: 2T
+ tenant: test
+ tenant_space: space_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete volume named foo
+ purestorage.fusion.fusion_volume:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_number_with_metric_suffix,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_volume(module, fusion):
+ """Return Volume or None"""
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ try:
+ return volume_api_instance.get_volume(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ volume_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_wanted_haps(module):
+ """Return set of host access policies to assign"""
+ if not module.params["host_access_policies"]:
+ return set()
+ # looks like yaml parsing can leave in some spaces if coma-delimited .so strip() the names
+ return set([hap.strip() for hap in module.params["host_access_policies"]])
+
+
+def extract_current_haps(volume):
+ """Return set of host access policies that volume currently has"""
+ if not volume.host_access_policies:
+ return set()
+ return set([hap.name for hap in volume.host_access_policies])
+
+
+def create_volume(module, fusion):
+ """Create Volume"""
+
+ size = parse_number_with_metric_suffix(module, module.params["size"])
+
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ volume = purefusion.VolumePost(
+ size=size,
+ storage_class=module.params["storage_class"],
+ placement_group=module.params["placement_group"],
+ name=module.params["name"],
+ display_name=display_name,
+ protection_policy=module.params["protection_policy"],
+ )
+ op = volume_api_instance.create_volume(
+ volume,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def update_host_access_policies(module, current, patches):
+ wanted = module.params
+ # 'wanted[...] is not None' to differentiate between empty list and no list
+ if wanted["host_access_policies"] is not None:
+ current_haps = extract_current_haps(current)
+ wanted_haps = get_wanted_haps(module)
+ if wanted_haps != current_haps:
+ patch = purefusion.VolumePatch(
+ host_access_policies=purefusion.NullableString(",".join(wanted_haps))
+ )
+ patches.append(patch)
+
+
+def update_destroyed(module, current, patches):
+ wanted = module.params
+ destroyed = wanted["state"] != "present"
+ if destroyed != current.destroyed:
+ patch = purefusion.VolumePatch(destroyed=purefusion.NullableBoolean(destroyed))
+ patches.append(patch)
+ if destroyed and not module.params["eradicate"]:
+ module.warn(
+ (
+ "Volume '{0}' is being soft deleted to prevent data loss, "
+ "if you want to wipe it immediately to reclaim used space, add 'eradicate: true'"
+ ).format(current.name)
+ )
+
+
+def update_display_name(module, current, patches):
+ wanted = module.params
+ if wanted["display_name"] and wanted["display_name"] != current.display_name:
+ patch = purefusion.VolumePatch(
+ display_name=purefusion.NullableString(wanted["display_name"])
+ )
+ patches.append(patch)
+
+
+def update_storage_class(module, current, patches):
+ wanted = module.params
+ if (
+ wanted["storage_class"]
+ and wanted["storage_class"] != current.storage_class.name
+ ):
+ patch = purefusion.VolumePatch(
+ storage_class=purefusion.NullableString(wanted["storage_class"])
+ )
+ patches.append(patch)
+
+
+def update_placement_group(module, current, patches):
+ wanted = module.params
+ if (
+ wanted["placement_group"]
+ and wanted["placement_group"] != current.placement_group.name
+ ):
+ patch = purefusion.VolumePatch(
+ placement_group=purefusion.NullableString(wanted["placement_group"])
+ )
+ patches.append(patch)
+
+
+def update_size(module, current, patches):
+ wanted = module.params
+ if wanted["size"]:
+ wanted_size = parse_number_with_metric_suffix(module, wanted["size"])
+ if wanted_size != current.size:
+ patch = purefusion.VolumePatch(size=purefusion.NullableSize(wanted_size))
+ patches.append(patch)
+
+
+def update_protection_policy(module, current, patches):
+ wanted = module.params
+ current_policy = current.protection_policy.name if current.protection_policy else ""
+ if (
+ wanted["protection_policy"] is not None
+ and wanted["protection_policy"] != current_policy
+ ):
+ patch = purefusion.VolumePatch(
+ protection_policy=purefusion.NullableString(wanted["protection_policy"])
+ )
+ patches.append(patch)
+
+
+def apply_patches(module, fusion, patches):
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ for patch in patches:
+ op = volume_api_instance.update_volume(
+ patch,
+ volume_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+
+def update_volume(module, fusion):
+ """Update Volume size, placement group, protection policy, storage class, HAPs"""
+ current = get_volume(module, fusion)
+ patches = []
+
+ if not current:
+ # cannot update nonexistent volume
+ # Note for check mode: the reasons this codepath is ran in check mode
+ # is to catch any argument errors and to compute 'changed'. Basically
+ # all argument checks are kept in validate_arguments() to filter the
+ # first part. The second part MAY diverge flow from the real run here if
+ # create_volume() created the volume and update was then run to update
+ # its properties. HOWEVER we don't really care in that case because
+ # create_volume() already sets 'changed' to true, so any 'changed'
+ # result from update_volume() would not change it.
+ return False
+
+ # volumes with 'destroyed' flag are kinda special because we can't change
+ # most of their properties while in this state, so we need to set it last
+ # and unset it first if changed, respectively
+ if module.params["state"] == "present":
+ update_destroyed(module, current, patches)
+ update_size(module, current, patches)
+ update_protection_policy(module, current, patches)
+ update_display_name(module, current, patches)
+ update_storage_class(module, current, patches)
+ update_placement_group(module, current, patches)
+ update_host_access_policies(module, current, patches)
+ elif module.params["state"] == "absent" and not current.destroyed:
+ update_size(module, current, patches)
+ update_protection_policy(module, current, patches)
+ update_display_name(module, current, patches)
+ update_storage_class(module, current, patches)
+ update_placement_group(module, current, patches)
+ update_host_access_policies(module, current, patches)
+ update_destroyed(module, current, patches)
+
+ if not module.check_mode:
+ apply_patches(module, fusion, patches)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def eradicate_volume(module, fusion):
+ """Eradicate Volume"""
+ current = get_volume(module, fusion)
+ if module.check_mode:
+ return current or module.params["state"] == "present"
+ if not current:
+ return False
+
+ # update_volume() should be called before eradicate=True and it should
+ # ensure the volume is destroyed and HAPs are unassigned
+ if not current.destroyed or current.host_access_policies:
+ module.fail_json(
+ msg="BUG: inconsistent state, eradicate_volume() cannot be called with current.destroyed=False or any host_access_policies"
+ )
+
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ op = volume_api_instance.delete_volume(
+ volume_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def validate_arguments(module, volume):
+ """Validates most argument conditions and possible unacceptable argument combinations"""
+ state = module.params["state"]
+
+ if state == "present" and not volume:
+ module.fail_on_missing_params(["placement_group", "storage_class", "size"])
+
+ if module.params["state"] == "absent" and (
+ module.params["host_access_policies"]
+ or (volume and volume.host_access_policies)
+ ):
+ module.fail_json(
+ msg=(
+ "Volume must have no host access policies when destroyed, either revert the delete "
+ "by setting 'state: present' or remove all HAPs by 'host_access_policies: []'"
+ )
+ )
+
+ if state == "present" and module.params["eradicate"]:
+ module.fail_json(
+ msg="'eradicate: true' cannot be used together with 'state: present'"
+ )
+
+ if module.params["size"]:
+ size = parse_number_with_metric_suffix(module, module.params["size"])
+ if size < 1048576 or size > 4503599627370496: # 1MB to 4PB
+ module.fail_json(
+ msg="Size is not within the required range, size must be between 1MB and 4PB"
+ )
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ deprecated_hosts = dict(
+ name="hosts", date="2023-07-26", collection_name="purefusion.fusion"
+ )
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ rename=dict(
+ type="str",
+ removed_at_date="2023-07-26",
+ removed_from_collection="purestorage.fusion",
+ ),
+ tenant=dict(type="str", required=True),
+ tenant_space=dict(type="str", required=True),
+ placement_group=dict(type="str"),
+ storage_class=dict(type="str"),
+ protection_policy=dict(type="str"),
+ host_access_policies=dict(
+ type="list", elements="str", deprecated_aliases=[deprecated_hosts]
+ ),
+ eradicate=dict(type="bool", default=False),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ size=dict(type="str"),
+ )
+ )
+
+ required_by = {
+ "placement_group": "storage_class",
+ }
+
+ module = AnsibleModule(
+ argument_spec,
+ required_by=required_by,
+ supports_check_mode=True,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+
+ volume = get_volume(module, fusion)
+
+ validate_arguments(module, volume)
+
+ if state == "absent" and not volume:
+ module.exit_json(changed=False)
+
+ changed = False
+ if state == "present" and not volume:
+ changed = changed | create_volume(module, fusion)
+ # volume might exist even if soft-deleted, so we still have to update it
+ changed = changed | update_volume(module, fusion)
+ if module.params["eradicate"]:
+ changed = changed | eradicate_volume(module, fusion)
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()