diff --git a/meta/runtime.yml b/meta/runtime.yml index 4ab2374cd..702ca6c04 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -95,3 +95,5 @@ action_groups: - ntnx_ndb_maintenance_window - ntnx_ndb_maintenance_windows_info - ntnx_ndb_slas + - ntnx_volume_groups + - ntnx_volume_groups_info diff --git a/plugins/module_utils/prism/iscsi_clients.py b/plugins/module_utils/prism/iscsi_clients.py new file mode 100644 index 000000000..cd18aa21e --- /dev/null +++ b/plugins/module_utils/prism/iscsi_clients.py @@ -0,0 +1,91 @@ +# This file is part of Ansible +# 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 + +from copy import deepcopy + +from .prism import Prism + + +class Clients(Prism): + __BASEURL__ = "/api/storage/v4.0.a2/config" + + def __init__(self, module): + resource_type = "/iscsi-clients" + super(Clients, self).__init__(module, resource_type=resource_type) + self.build_spec_methods = {} + + def update( + self, + data=None, + uuid=None, + endpoint=None, + query=None, + raise_error=True, + no_response=False, + timeout=30, + method="PATCH", + ): + resp = super(Clients, self).update( + data, + uuid, + endpoint, + query, + raise_error, + no_response, + timeout, + method, + ) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp + + def get_client_spec(self, iscsi_client, authentication_is_enabled=False): + payload = self._get_default_spec() + if self.module.params.get("CHAP_auth") == "enable" or authentication_is_enabled: + chap_auth = True + else: + chap_auth = False + + spec, error = self._build_spec_iscsi_client(payload, iscsi_client, chap_auth) + if error: + return None, error + return spec, None + + @staticmethod + def _get_default_spec(): + return deepcopy({"enabledAuthentications": "NONE"}) + + @staticmethod + def _build_spec_iscsi_client(payload, iscsi_client, chap_auth): + + if iscsi_client.get("uuid"): + payload["extId"] = iscsi_client["uuid"] + elif iscsi_client.get("iscsi_iqn"): + payload["iscsiInitiatorName"] = iscsi_client["iscsi_iqn"] + elif iscsi_client.get("iscsi_ip"): + payload["iscsiInitiatorNetworkId"] = { + "$objectType": "common.v1.config.IPAddressOrFQDN", + "$reserved": { + "$fqObjectType": "common.v1.r0.a3.config.IPAddressOrFQDN" + }, + "$unknownFields": {}, + "ipv4": { + "$objectType": "common.v1.config.IPv4Address", + "$reserved": { + "$fqObjectType": "common.v1.r0.a3.config.IPv4Address" + }, + "$unknownFields": {}, + "value": iscsi_client["iscsi_ip"], + }, + } + if iscsi_client.get("client_password"): + if chap_auth: + payload["clientSecret"] = iscsi_client["client_password"] + + payload["enabledAuthentications"] = "CHAP" + else: + error = "parameters are required together: CHAP_auth, client_password" + return None, error + return payload, None diff --git a/plugins/module_utils/prism/vdisks.py b/plugins/module_utils/prism/vdisks.py new file mode 100644 index 000000000..f87e7f4ce --- /dev/null +++ b/plugins/module_utils/prism/vdisks.py @@ -0,0 +1,58 @@ +# This file is part of Ansible +# 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 + +from copy import deepcopy + +from .groups import get_entity_uuid + + +class VDisks: + @classmethod + def get_spec(cls, module, vdisk): + payload = cls._get_default_spec() + spec, error = cls._build_spec_vdisk(module, payload, vdisk) + if error: + return None, error + return spec, None + + @staticmethod + def _get_default_spec(): + return deepcopy( + { + "diskSizeBytes": None, + } + ) + + @staticmethod + def _build_spec_vdisk(module, payload, vdisk): + + disk_size_bytes = vdisk["size_gb"] * 1024 * 1024 * 1024 + + payload["diskSizeBytes"] = disk_size_bytes + + if vdisk.get("storage_container"): + uuid, error = get_entity_uuid( + vdisk["storage_container"], + module, + key="container_name", + entity_type="storage_container", + ) + if error: + return None, error + + payload["diskDataSourceReference"] = { + "$objectType": "common.v1.config.EntityReference", + "$reserved": { + "$fqObjectType": "common.v1.r0.a3.config.EntityReference" + }, + "$unknownFields": {}, + "extId": uuid, + "entityType": "STORAGE_CONTAINER", + } + elif vdisk.get("uuid"): + payload["extId"] = vdisk["uuid"] + + return payload, None diff --git a/plugins/module_utils/prism/volume_groups.py b/plugins/module_utils/prism/volume_groups.py new file mode 100644 index 000000000..9723a79b2 --- /dev/null +++ b/plugins/module_utils/prism/volume_groups.py @@ -0,0 +1,229 @@ +# This file is part of Ansible +# 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 + +from copy import deepcopy + +from .clusters import get_cluster_uuid +from .prism import Prism +from .vms import get_vm_uuid + + +class VolumeGroup(Prism): + __BASEURL__ = "/api/storage/v4.0.a2/config" + + def __init__(self, module): + resource_type = "/volume-groups" + super(VolumeGroup, self).__init__(module, resource_type=resource_type) + self.build_spec_methods = { + "name": self._build_spec_name, + "desc": self._build_spec_desc, + "cluster": self._build_spec_cluster, + "target_prefix": self._build_spec_target_prefix, + "CHAP_auth": self._build_spec_chap_auth, + "target_password": self._build_spec_target_password, + "load_balance": self._build_spec_load_balance, + "flash_mode": self._build_spec_flash_mode, + } + + def get_vdisks(self, volume_group_uuid, disk_uuid=None): + if disk_uuid: + endpoint = "/disks/{0}".format(disk_uuid) + else: + endpoint = "/disks" + return self.read(volume_group_uuid, endpoint=endpoint) + + def get_vms(self, volume_group_uuid): + endpoint = "/vm-attachments" + return self.read(volume_group_uuid, endpoint=endpoint) + + def get_clients(self, volume_group_uuid): + endpoint = "/iscsi-client-attachments" + return self.read(volume_group_uuid, endpoint=endpoint) + + def create_vdisk(self, spec, volume_group_uuid): + endpoint = "disks" + resp = self.update( + spec, volume_group_uuid, method="POST", endpoint=endpoint, raise_error=False + ) + if resp.get("data") and resp["data"].get("error"): + err = resp["data"]["error"] + if isinstance(err, list): + err = err[0] + if err.get("message"): + err = err["message"] + return None, err + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp, None + + def attach_vm(self, spec, volume_group_uuid): + endpoint = "$actions/attach-vm" + + resp = self.update( + spec, volume_group_uuid, method="POST", endpoint=endpoint, raise_error=False + ) + if resp.get("data") and resp["data"].get("error"): + err = resp["data"]["error"] + if isinstance(err, list): + err = err[0] + if err.get("message"): + err = err["message"] + return None, err + + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp, None + + def attach_iscsi_client(self, spec, volume_group_uuid): + endpoint = "/$actions/attach-iscsi-client" + resp = self.update( + spec, volume_group_uuid, method="POST", endpoint=endpoint, raise_error=False + ) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp + + def update_disk(self, spec, volume_group_uuid, disk_uuid): + endpoint = "disks/{0}".format(disk_uuid) + resp = self.update( + spec, + uuid=volume_group_uuid, + method="PATCH", + endpoint=endpoint, + raise_error=False, + ) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp + + def delete_disk(self, volume_group_uuid, disk_uuid): + endpoint = "disks/{0}".format(disk_uuid) + resp = self.delete(uuid=volume_group_uuid, endpoint=endpoint, raise_error=False) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp + + def detach_vm(self, volume_group_uuid, vm): + if not vm.get("extId"): + vm_uuid, err = get_vm_uuid(vm, self.module) + if err: + if isinstance(err, list): + err = err[0] + if err.get("message"): + err = err["message"] + return None, err + else: + vm_uuid = vm["extId"] + endpoint = "$actions/detach-vm/{0}".format(vm_uuid) + resp = self.update( + uuid=volume_group_uuid, method="POST", endpoint=endpoint, raise_error=False + ) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp, None + + def detach_iscsi_client(self, volume_group_uuid, client_uuid): + endpoint = "$actions/detach-iscsi-client/{0}".format(client_uuid) + resp = self.update( + uuid=volume_group_uuid, method="POST", endpoint=endpoint, raise_error=False + ) + resp["task_uuid"] = resp["data"]["extId"].split(":")[1] + return resp + + def _get_default_spec(self): + return deepcopy( + { + "name": "", + "sharingStatus": "SHARED", + "usageType": "USER", + } + ) + + def get_update_spec(self): + + return self.get_spec( + { + "extId": self.module.params["volume_group_uuid"], + "sharingStatus": "SHARED", + } + ) + + def _build_spec_name(self, payload, value): + payload["name"] = value + return payload, None + + def _build_spec_desc(self, payload, value): + payload["description"] = value + return payload, None + + def _build_spec_target_prefix(self, payload, value): + payload["iscsiTargetPrefix"] = value + return payload, None + + def _build_spec_target_password(self, payload, value): + payload["targetSecret"] = value + return payload, None + + def _build_spec_chap_auth(self, payload, value): + if value == "enable": + payload["enabledAuthentications"] = "CHAP" + else: + payload["enabledAuthentications"] = "NONE" + return payload, None + + def _build_spec_load_balance(self, payload, value): + payload["loadBalanceVmAttachments"] = value + return payload, None + + def _build_spec_flash_mode(self, payload, value): + + if value: + payload["storageFeatures"] = { + "$objectType": "storage.v4.config.StorageFeatures", + "$reserved": { + "$fqObjectType": "storage.v4.r0.a2.config.StorageFeatures" + }, + "$unknownFields": {}, + "flashMode": { + "$objectType": "storage.v4.config.FlashMode", + "$reserved": {"$fqObjectType": "storage.v4.r0.a2.config.FlashMode"}, + "$unknownFields": {}, + "isEnabled": True, + }, + } + return payload, None + + def _build_spec_cluster(self, payload, param): + uuid, err = get_cluster_uuid(param, self.module) + if err: + return None, err + payload["clusterReference"] = uuid + return payload, None + + def get_vm_spec(self, vm): + uuid, error = get_vm_uuid(vm, self.module) + if error: + return None, error + spec = {"extId": uuid} + return spec, None + + def get_client_spec(self, client): + spec = {"extId": client["uuid"]} + return spec, None + + +# Helper functions + + +def get_volume_group_uuid(config, module): + if "name" in config: + service_group = VolumeGroup(module) + name = config["name"] + uuid = service_group.get_uuid(name) + if not uuid: + error = "Volume Group {0} not found.".format(name) + return None, error + elif "uuid" in config: + uuid = config["uuid"] + else: + error = "Config {0} doesn't have name or uuid key".format(config) + return None, error + + return uuid, None diff --git a/plugins/modules/ntnx_volume_groups.py b/plugins/modules/ntnx_volume_groups.py new file mode 100644 index 000000000..1f271a6b6 --- /dev/null +++ b/plugins/modules/ntnx_volume_groups.py @@ -0,0 +1,724 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Prem Karat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: ntnx_volume_groups +short_description: volume_groups module which suports volume_groups CRUD operations +version_added: 1.9.0 +description: 'Create, Update, Delete volume_group' +options: + state: + description: + - Specify state of volume_groups + - If C(state) is set to C(present) then volume_groups is created. + - >- + If C(state) is set to C(absent) and if the volume_groups exists, then + volume_groups is removed. + choices: + - present + - absent + type: str + default: present + wait: + description: Wait for volume_groups CRUD operation to complete. + type: bool + required: false + default: True + name: + description: volume_groups Name + required: False + type: str + volume_group_uuid: + description: volume_group UUID + type: str + desc: + description: volume_groups description + type: str + cluster: + description: Name or UUID of the cluster on which the volume group will be placed + type: dict + suboptions: + name: + description: + - Cluster Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Cluster UUID + - Mutually exclusive with C(name) + type: str + target_prefix: + description: iSCSI target prefix-name. + type: str + flash_mode: + description: if enabled all volume disks of the VG will be pinned to SSD tier. + type: bool + default: false + disks: + description: Volume group disk specification. + type: list + elements: dict + suboptions: + size_gb: + description: The Disk Size in GB. + type: int + state: + description: write + type: str + choices: ["absent"] + uuid: + description: write + type: str + storage_container: + description: Container on which to create the disk. + type: dict + suboptions: + name: + description: + - Storage containter Name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - Storage container UUID + - Mutually exclusive with C(name) + type: str + vms: + description: write + type: list + elements: dict + suboptions: + name: + description: + - VM name + - Mutually exclusive with C(uuid) + type: str + uuid: + description: + - VM UUID + - Mutually exclusive with C(name) + type: str + state: + description: + - write + type: str + choices: ["absent"] + load_balance: + description: write + type: bool + default: false + clients: + description: write + type: list + elements: dict + suboptions: + state: + description: + - write + type: str + choices: ["absent"] + iscsi_iqn: + description: + - write + type: str + uuid: + description: + - write + type: str + iscsi_ip: + description: + - write + type: str + client_password: + description: + - write + type: str + CHAP_auth: + description: Use Challenge-Handshake Authentication Protocol + type: str + choices: ["enable", "disable"] + target_password: + description: CHAP secret + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations +author: + - Prem Karat (@premkarat) + - Gevorg Khachatryan (@Gevorg-Khachatryan-97) + - Alaa Bishtawi (@alaa-bish) +""" + +EXAMPLES = r""" + +""" + +RETURN = r""" + +""" + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.prism.iscsi_clients import Clients # noqa: E402 +from ..module_utils.prism.tasks import Task # noqa: E402 +from ..module_utils.prism.vdisks import VDisks # noqa: E402 +from ..module_utils.prism.volume_groups import VolumeGroup # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + mutually_exclusive = [("name", "uuid")] + + entity_by_spec = dict(name=dict(type="str"), uuid=dict(type="str")) + + disk_spec = dict( + state=dict(type="str", choices=["absent"]), + uuid=dict(type="str"), + size_gb=dict(type="int"), + storage_container=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + ) + + vm_spec = dict( + state=dict(type="str", choices=["absent"]), + name=dict(type="str"), + uuid=dict(type="str"), + ) + + client_spec = dict( + state=dict(type="str", choices=["absent"]), + uuid=dict(type="str"), + iscsi_iqn=dict(type="str"), + iscsi_ip=dict(type="str"), + client_password=dict(type="str", no_log=True), + ) + + module_args = dict( + name=dict(type="str"), + desc=dict(type="str"), + cluster=dict( + type="dict", options=entity_by_spec, mutually_exclusive=mutually_exclusive + ), + target_prefix=dict(type="str"), + volume_group_uuid=dict(type="str"), + flash_mode=dict(type="bool", default=False), + disks=dict( + type="list", + elements="dict", + options=disk_spec, + mutually_exclusive=[ + ("uuid", "storage_container"), + ("state", "size_gb"), + ], + required_if=[("state", "absent", ("uuid",))], + ), + vms=dict( + type="list", + elements="dict", + options=vm_spec, + mutually_exclusive=mutually_exclusive, + ), + load_balance=dict(type="bool", default=False), + clients=dict( + type="list", + elements="dict", + options=client_spec, + mutually_exclusive=[("uuid", "iscsi_iqn", "iscsi_ip")], + required_if=[("state", "absent", ("uuid",))], + ), + CHAP_auth=dict(type="str", choices=["enable", "disable"]), + target_password=dict(type="str", no_log=True), + ) + + return module_args + + +def create_volume_group(module, result): + volume_group = VolumeGroup(module) + vg_disks = module.params.get("disks") + vg_vms = module.params.get("vms") + vg_clients = module.params.get("clients") + + spec, error = volume_group.get_spec() + if error: + result["error"] = error + module.fail_json(msg="Failed generating volume_groups spec", **result) + + if module.check_mode: + result["response"] = spec + return + + resp = volume_group.create(spec) + task_uuid = resp["data"]["extId"].split(":")[1] + result["response"] = resp + result["task_uuid"] = task_uuid + resp, err = wait_for_task_completion(module, result) + + result["changed"] = True + volume_group_uuid = resp["entity_reference_list"][0]["uuid"] + result["volume_group_uuid"] = volume_group_uuid + resp = volume_group.read(volume_group_uuid) + result["response"] = resp.get("data") + + # create disks + if vg_disks: + for disk in vg_disks: + spec, err = VDisks.get_spec(module, disk) + if err: + result["warning"].append("Disk is not created. Error: {0}".format(err)) + result["skipped"] = True + continue + + vdisk_resp, err = volume_group.create_vdisk(spec, volume_group_uuid) + if err: + result["warning"].append("Disk is not created. Error: {0}".format(err)) + result["skipped"] = True + continue + task_uuid = vdisk_resp["task_uuid"] + vdisk_resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "Disk creating task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + disks_resp = volume_group.get_vdisks(volume_group_uuid) + result["response"]["disks"] = disks_resp.get("data") + + # attach vms + if vg_vms: + for vm in vg_vms: + + spec, err = volume_group.get_vm_spec(vm) + if err: + result["warning"].append("VM is not attached. Error: {0}".format(err)) + result["skipped"] = True + continue + + attach_resp, err = volume_group.attach_vm(spec, volume_group_uuid) + if err: + result["warning"].append("VM is not attached. Error: {0}".format(err)) + result["skipped"] = True + continue + + task_uuid = attach_resp["task_uuid"] + attach_resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "VM attaching task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + + vms_resp = volume_group.get_vms(volume_group_uuid) + result["response"]["vms"] = vms_resp.get("data") + + # attach clients + if vg_clients: + client = Clients(module) + for vg_client in vg_clients: + + spec, err = client.get_client_spec(vg_client) + if err: + result["warning"].append("Client is not attached. Error: {0}".format(err)) + result["skipped"] = True + continue + + attach_resp = volume_group.attach_iscsi_client(spec, volume_group_uuid) + + task_uuid = attach_resp["task_uuid"] + attach_resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "Client attaching task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + + clients_resp = volume_group.get_clients(volume_group_uuid) + result["response"]["clients"] = clients_resp.get("data") + + +def update_volume_group(module, result): + volume_group = VolumeGroup(module) + volume_group_uuid = module.params.get("volume_group_uuid") + vg_disks = module.params.get("disks") + vg_vms = module.params.get("vms") + vg_clients = module.params.get("clients") + result["nothing_to_change"] = True + if not volume_group_uuid: + result["error"] = "Missing parameter volume_group_uuid in playbook" + module.fail_json(msg="Failed updating volume_group", **result) + result["volume_group_uuid"] = volume_group_uuid + + # read the current state of volume_group + resp = volume_group.read(volume_group_uuid) + resp = resp.get("data") + + # new spec for updating volume_group + update_spec, error = volume_group.get_update_spec() + if error: + result["error"] = error + module.fail_json(msg="Failed generating volume_group update spec", **result) + + if module.check_mode: + result["response"] = update_spec + return + + # check for idempotency + if not check_for_idempotency(resp, update_spec): + + # update volume_group + result["nothing_to_change"] = False + volume_group.update(update_spec, uuid=volume_group_uuid, method="PATCH") + + result["changed"] = True + + resp = volume_group.read(volume_group_uuid) + resp = resp.get("data") + result["response"] = resp + + # update disks + if vg_disks: + update_volume_group_disks( + module, volume_group, vg_disks, volume_group_uuid, result + ) + + disks_resp = volume_group.get_vdisks(volume_group_uuid) + result["response"]["disks"] = disks_resp.get("data") + + # update vms + if vg_vms: + detach_volume_group_clients(module, volume_group, volume_group_uuid, result) + update_volume_group_vms(module, volume_group, vg_vms, volume_group_uuid, result) + + vms_resp = volume_group.get_vms(volume_group_uuid) + result["response"]["vms"] = vms_resp.get("data") + + # update clients + if vg_clients: + authentication_is_enabled = ( + True if resp.get("enabledAuthentications") else False + ) + detach_volume_group_vms(module, volume_group, volume_group_uuid, result) + update_volume_group_clients( + module, + volume_group, + vg_clients, + volume_group_uuid, + result, + authentication_is_enabled, + ) + + clients_resp = volume_group.get_clients(volume_group_uuid) + result["response"]["clients"] = clients_resp.get("data") + + if result.pop("nothing_to_change"): + module.exit_json( + msg="Nothing to change. Refer docs to check for fields which can be updated" + ) + + +def delete_volume_group(module, result): + volume_group_uuid = module.params["volume_group_uuid"] + if not volume_group_uuid: + result["error"] = "Missing parameter volume_group_uuid in playbook" + module.fail_json(msg="Failed deleting volume_groups", **result) + + volume_group = VolumeGroup(module) + + # detach iscsi_clients + detach_volume_group_clients(module, volume_group, volume_group_uuid, result) + + # detach vms + detach_volume_group_vms(module, volume_group, volume_group_uuid, result) + + resp = volume_group.delete(volume_group_uuid) + resp.pop("metadata") + result["changed"] = True + result["response"] = resp + result["volume_group_uuid"] = volume_group_uuid + + +def update_volume_group_disks( + module, volume_group, vg_disks, volume_group_uuid, result +): + for disk in vg_disks: + if disk.get("uuid"): + disk_uuid = disk["uuid"] + if disk.get("state") == "absent": + result["nothing_to_change"] = False + vdisk_resp = volume_group.delete_disk(volume_group_uuid, disk_uuid) + result["changed"] = True + + else: + vdisk = volume_group.get_vdisks(volume_group_uuid, disk_uuid).get( + "data" + ) + spec, err = VDisks.get_spec(module, disk) + if err: + result["nothing_to_change"] = False + result["warning"].append( + "Disk is not updated. Error: {0}".format(err) + ) + result["skipped"] = True + continue + elif check_for_idempotency(vdisk, spec): + result["warning"].append( + "Nothing to change. Disk: {0}".format(disk_uuid) + ) + result["skipped"] = True + continue + result["nothing_to_change"] = False + vdisk_resp = volume_group.update_disk( + spec, volume_group_uuid, disk_uuid + ) + result["changed"] = True + else: + result["nothing_to_change"] = False + spec, err = VDisks.get_spec(module, disk) + if err: + result["warning"].append("Disk is not created. Error: {0}".format(err)) + result["skipped"] = True + continue + vdisk_resp, err = volume_group.create_vdisk(spec, volume_group_uuid) + if err: + result["warning"].append("Disk is not created. Error: {0}".format(err)) + result["skipped"] = True + continue + result["changed"] = True + + task_uuid = vdisk_resp["task_uuid"] + wait_for_task_completion(module, {"task_uuid": task_uuid}) + + disks_resp = volume_group.get_vdisks(volume_group_uuid) + result["response"]["disks"] = disks_resp.get("data") + + +def update_volume_group_vms(module, volume_group, vg_vms, volume_group_uuid, result): + for vm in vg_vms: + if vm.get("state") == "absent": + result["nothing_to_change"] = False + vm_resp, err = volume_group.detach_vm(volume_group_uuid, vm) + if err: + result["warning"].append("VM is not detached. Error: {0}".format(err)) + result["skipped"] = True + continue + result["changed"] = True + + else: + spec, err = volume_group.get_vm_spec(vm) + if err: + result["warning"].append("VM is not attached. Error: {0}".format(err)) + result["skipped"] = True + continue + + result["nothing_to_change"] = False + vm_resp, err = volume_group.attach_vm(spec, volume_group_uuid) + if err: + result["warning"].append("VM is not attached. Error: {0}".format(err)) + result["skipped"] = True + continue + + task_uuid = vm_resp["task_uuid"] + resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + + if err: + result["warning"].append("VM update task is failed. Error: {0}".format(err)) + result["skipped"] = True + continue + + result["changed"] = True + + +def update_volume_group_clients( + module, + volume_group, + vg_clients, + volume_group_uuid, + result, + authentication_is_enabled, +): + client = Clients(module) + for vg_client in vg_clients: + + if vg_client.get("uuid") and ( + vg_client.get("state") or vg_client.get("client_password") + ): + client_uuid = vg_client["uuid"] + if vg_client.get("state") == "absent": + result["nothing_to_change"] = False + client_resp = volume_group.detach_iscsi_client( + volume_group_uuid, client_uuid + ) + + result["changed"] = True + + else: + resp = client.read(client_uuid, raise_error=False).get("data") + spec, err = client.get_client_spec(vg_client, authentication_is_enabled) + if err or resp.get("error"): + result["nothing_to_change"] = False + result["warning"].append( + "Client is not updated. Error: {0}".format( + resp.get("error") or err + ) + ) + result["skipped"] = True + continue + elif check_for_idempotency(resp, spec): + result["warning"].append( + "Nothing to change. Client: {0}".format(client_uuid) + ) + result["skipped"] = True + continue + result["nothing_to_change"] = False + client_resp = client.update(spec, client_uuid, method="PATCH") + + result["changed"] = True + + else: + result["nothing_to_change"] = False + spec, err = client.get_client_spec(vg_client, authentication_is_enabled) + if err: + result["warning"].append( + "Client is not created. Error: {0}".format(err) + ) + result["skipped"] = True + continue + client_resp = volume_group.attach_iscsi_client(spec, volume_group_uuid) + + result["changed"] = True + + task_uuid = client_resp["task_uuid"] + resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "Client update task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + + +def detach_volume_group_clients(module, volume_group, volume_group_uuid, result): + clients_resp = volume_group.get_clients(volume_group_uuid) + detached_clients = [] + for client in clients_resp.get("data", []): + client_uuid = client["extId"] + detach_resp = volume_group.detach_iscsi_client(volume_group_uuid, client_uuid) + + task_uuid = detach_resp["task_uuid"] + resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "Client detaching task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + detached_clients.append(client_uuid) + + result["detached_clients"] = detached_clients + + +def detach_volume_group_vms(module, volume_group, volume_group_uuid, result): + vms_resp = volume_group.get_vms(volume_group_uuid) + detached_vms = [] + for vm in vms_resp.get("data", []): + detach_resp, err = volume_group.detach_vm(volume_group_uuid, vm) + if err: + result["warning"].append("VM is not detached. Error: {0}".format(err)) + result["skipped"] = True + continue + task_uuid = detach_resp["task_uuid"] + resp, err = wait_for_task_completion( + module, {"task_uuid": task_uuid}, raise_error=False + ) + if err: + result["warning"].append( + "VM detaching task is failed. Error: {0}".format(err) + ) + result["skipped"] = True + continue + detached_vms.append(vm["extId"]) + + result["detached_vms"] = detached_vms + + +def check_for_idempotency(old_spec, update_spec): + + for key, value in update_spec.items(): + if key == "enabledAuthentications" and value != "NONE": + if old_spec.get(key): + return False + elif old_spec.get(key) != value: + return False + + return True + + +def wait_for_task_completion(module, result, raise_error=True): + task = Task(module) + task_uuid = result["task_uuid"] + resp = task.wait_for_completion(task_uuid, raise_error=raise_error) + if resp.get("status") == "FAILED": + err = resp.get("error_detail") + return None, err + return resp, None + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_if=[ + ("state", "present", ("name", "volume_group_uuid"), True), + ("state", "absent", ("volume_group_uuid",)), + ("CHAP_auth", "enable", ("target_password",)), + ], + mutually_exclusive=[("vms", "clients")], + ) + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "volume_group_uuid": None, + "warning": [], + "nothing_to_change": False, + } + state = module.params["state"] + if state == "absent": + delete_volume_group(module, result) + elif module.params.get("volume_group_uuid"): + update_volume_group(module, result) + else: + create_volume_group(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_volume_groups_info.py b/plugins/modules/ntnx_volume_groups_info.py new file mode 100644 index 000000000..dcc6ef660 --- /dev/null +++ b/plugins/modules/ntnx_volume_groups_info.py @@ -0,0 +1,281 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Prem Karat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: ntnx_volume_groups_info +short_description: volume_group info module +version_added: 1.9.0 +description: 'Get volume_group info' +options: + volume_group_uuid: + description: + - volume_group UUID + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + # - nutanix.ncp.ntnx_info +author: + - Prem Karat (@premkarat) + - Gevorg Khachatryan (@Gevorg-Khachatryan-97) + - Alaa Bishtawi (@alaa-bish) +""" +EXAMPLES = r""" + - name: List volume_group using name filter criteria + ntnx_volume_groups_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + filter: + name: "{{ volume_group.name }}" + kind: volume_group + register: result + + - name: List volume_group using length, offset, sort order and name sort attribute + ntnx_volume_groups_info: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: False + length: 10 + offset: 1 + sort_order: "ASCENDING" + sort_attribute: "name" + register: result +""" +RETURN = r""" +metadata: + description: Metadata for volume_group list output + returned: always + type: dict + sample: {} +entities: + description: volume_group intent response + returned: always + type: list + sample: { + "entities": [ + { + "status": { + "description": "string", + "state": "string", + "message_list": [ + { + "message": "string", + "reason": "string", + "details": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + } + } + ], + "cluster_reference": { + "kind": "cluster", + "name": "string", + "uuid": "string" + }, + "resources": { + "flash_mode": "string", + "iscsi_target_name": "string", + "enabled_authentications": "string", + "attachment_list": [ + { + "iscsi_initiator_network_id": "string", + "enabled_authentications": "string", + "vm_reference": { + "kind": "vm", + "name": "string", + "uuid": "string" + }, + "iscsi_initiator_name": "string" + } + ], + "created_by": "string", + "parent_reference": { + "url": "string", + "kind": "string", + "uuid": "string", + "name": "string" + }, + "sharing_status": "string", + "disk_list": [ + { + "index": 0, + "storage_container_uuid": "string", + "disk_size_mib": 0, + "disk_size_bytes": 0, + "uuid": "string" + } + ], + "size_bytes": 0, + "usage_type": "string", + "load_balance_vm_attachments": true, + "is_hidden": true, + "size_mib": 0, + "iscsi_target_prefix": "string" + }, + "name": "string" + }, + "spec": { + "name": "string", + "description": "string", + "resources": { + "flash_mode": "string", + "load_balance_vm_attachments": true, + "created_by": "string", + "iscsi_target_prefix": "string", + "parent_reference": { + "url": "string", + "kind": "string", + "uuid": "string", + "name": "string" + }, + "sharing_status": "string", + "attachment_list": [ + { + "iscsi_initiator_network_id": "string", + "client_secret": "string", + "vm_reference": { + "kind": "vm", + "name": "string", + "uuid": "string" + }, + "iscsi_initiator_name": "string" + } + ], + "usage_type": "string", + "target_secret": "string", + "is_hidden": true, + "disk_list": [ + { + "index": 16383, + "data_source_reference": { + "url": "string", + "kind": "string", + "uuid": "string", + "name": "string" + }, + "disk_size_mib": 0, + "disk_size_bytes": 0, + "storage_container_uuid": "string" + } + ] + }, + "cluster_reference": { + "kind": "cluster", + "name": "string", + "uuid": "string" + } + }, + "api_version": "3.1.0", + "metadata": { + "last_update_time": "2023-03-13T11:41:16.626Z", + "use_categories_mapping": false, + "kind": "volume_group", + "uuid": "string", + "project_reference": { + "kind": "project", + "name": "string", + "uuid": "string" + }, + "creation_time": "2023-03-13T11:41:16.626Z", + "spec_version": 0, + "spec_hash": "string", + "categories_mapping": { + "additionalProp1": [ + "string" + ], + "additionalProp2": [ + "string" + ], + "additionalProp3": [ + "string" + ] + }, + "should_force_translate": true, + "entity_version": "string", + "owner_reference": { + "kind": "user", + "name": "string", + "uuid": "string" + }, + "categories": { + "additionalProp1": "string", + "additionalProp2": "string", + "additionalProp3": "string" + }, + "name": "string" + } + } + ], + "api_version": "3.1.0", + "metadata": { + "kind": "volume_group", + "total_matches": 0, + "sort_attribute": "string", + "filter": "string", + "length": 0, + "sort_order": "string", + "offset": 0 + } +} +""" + +from ..module_utils.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.prism.volume_groups import VolumeGroup # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 + + +def get_module_spec(): + + module_args = dict( + volume_group_uuid=dict(type="str"), + ) + + return module_args + + +def get_volume_group(module, result): + volume_group = VolumeGroup(module) + volume_group_uuid = module.params.get("volume_group_uuid") + resp = volume_group.read(volume_group_uuid) + + result["response"] = resp + + +def get_volume_groups(module, result): + volume_group = VolumeGroup(module) + + resp = volume_group.read() + + result["response"] = resp + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + if module.params.get("volume_group_uuid"): + get_volume_group(module, result) + else: + get_volume_groups(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main()