diff --git a/examples/prism_v2/prism.yml b/examples/prism_v2/prism.yml new file mode 100644 index 000000000..6e70fd0b5 --- /dev/null +++ b/examples/prism_v2/prism.yml @@ -0,0 +1,138 @@ +--- +- name: Prism playbook + hosts: localhost + gather_facts: false + module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: + nutanix_username: + nutanix_password: + validate_certs: false + tasks: + - name: Setting Variables + ansible.builtin.set_fact: + cluster: + uuid: "00095bb3-1234-1122-5312-ac1f6b6f97e2" + ip_pe: "10.0.0.1" + + - name: List all clusters to get prism central external ID + nutanix.ncp.ntnx_clusters_info_v2: + filter: "config/clusterFunction/any(t:t eq Clustermgmt.Config.ClusterFunctionRef'PRISM_CENTRAL')" + register: result + ignore_errors: true + + - name: Get prism central external ID + ansible.builtin.set_fact: + domain_manager_ext_id: "{{ result.response[0].ext_id }}" + + - name: Create backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + + - name: List all backup targets and set backup target external ID + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + + - name: Set backup target cluster external ID + ansible.builtin.set_fact: + backup_target_ext_id: "{{ result.response[0].ext_id }}" + + - name: Create restore source cluster + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + + - name: Get restore source cluster + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ result.response.ext_id }}" + register: result + ignore_errors: true + + - name: Set restore source cluster external ID + ansible.builtin.set_fact: + restore_source_ext_id: "{{ result.response.ext_id }}" + + - name: List all backup targets + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + + - name: List all backup targets with filter + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + filter: location/clusterLocation/config/ext_id eq '{{ cluster.uuid }}' + register: result + ignore_errors: true + + - name: List all backup targets with limit + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + limit: 1 + register: result + ignore_errors: true + + - name: Fetch backup target details using external ID + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + ext_id: "{{ backup_target_ext_id }}" + register: result + ignore_errors: true + + - name: Delete restore source cluster + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_source_ext_id }}" + state: absent + register: result + ignore_errors: true + + - name: Delete backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + state: absent + register: result + ignore_errors: true + + - name: List all PCs + nutanix.ncp.ntnx_pc_config_info_v2: + register: result + ignore_errors: true + + - name: Set PC external ID and name + ansible.builtin.set_fact: + pc_external_id: "{{ result.response[0].ext_id }}" + pc_name: "{{ result.response[0].config.name }}" + + - name: List all PCs with filter + nutanix.ncp.ntnx_pc_config_info_v2: + filter: name eq '{{ pc_name }}' + register: result + ignore_errors: true + + - name: List all PCs with limit + nutanix.ncp.ntnx_pc_config_info_v2: + limit: 1 + register: result + ignore_errors: true + + - name: Fetch PC details using external ID + nutanix.ncp.ntnx_pc_config_info_v2: + ext_id: "{{ pc_external_id }}" + register: result + ignore_errors: true diff --git a/meta/runtime.yml b/meta/runtime.yml index 7bbd5369b..2edaff3cd 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -189,3 +189,13 @@ action_groups: - ntnx_storage_containers_stats_v2 - ntnx_storage_containers_info_v2 - ntnx_storage_containers_v2 + - ntnx_pc_unregistration_v2 + - ntnx_pc_backup_target_info_v2 + - ntnx_pc_backup_target_v2 + - ntnx_pc_config_info_v2 + - ntnx_pc_deploy_v2 + - ntnx_pc_restore_v2 + - ntnx_pc_restore_source_info_v2 + - ntnx_pc_restore_source_v2 + - ntnx_pc_restorable_domain_managers_info_v2 + - ntnx_pc_restore_points_info_v2 diff --git a/plugins/module_utils/v4/pe/base_info_module.py b/plugins/module_utils/v4/pe/base_info_module.py new file mode 100644 index 000000000..f671f44a6 --- /dev/null +++ b/plugins/module_utils/v4/pe/base_info_module.py @@ -0,0 +1,31 @@ +# Copyright: 2021, Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause ) +from __future__ import absolute_import, division, print_function + +from copy import deepcopy + +from .base_module import BasePEModule + +__metaclass__ = type + + +class BaseInfoModule(BasePEModule): + """ + Base Info module class for Nutanix PC v4 list APIs based modules + """ + + info_argument_spec = dict( + filter=dict(type="str"), + page=dict(type="int"), + limit=dict(type="int"), + orderby=dict(type="str"), + select=dict(type="str"), + ) + + def __init__(self, skip_info_args=False, **kwargs): + self.argument_spec = deepcopy(BasePEModule.argument_spec) + self.argument_spec.pop("state") + self.argument_spec.pop("wait") + if not skip_info_args: + self.argument_spec.update(self.info_argument_spec) + super(BaseInfoModule, self).__init__(**kwargs) diff --git a/plugins/module_utils/v4/pe/base_module.py b/plugins/module_utils/v4/pe/base_module.py new file mode 100644 index 000000000..3b9eab52d --- /dev/null +++ b/plugins/module_utils/v4/pe/base_module.py @@ -0,0 +1,60 @@ +# Copyright: 2021, Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause ) +from __future__ import absolute_import, division, print_function + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule, env_fallback + +__metaclass__ = type + + +class BasePEModule(AnsibleModule): + """Basic module with common arguments for PE""" + + unsupported_spec_keys = ["obj"] + argument_spec = dict( + nutanix_host_pe=dict( + type="str", fallback=(env_fallback, ["NUTANIX_PE_HOST"]), required=True + ), + nutanix_port=dict( + default="9440", type="str", fallback=(env_fallback, ["NUTANIX_PORT"]) + ), + nutanix_username=dict( + type="str", fallback=(env_fallback, ["NUTANIX_USERNAME"]), required=True + ), + nutanix_password=dict( + type="str", + no_log=True, + fallback=(env_fallback, ["NUTANIX_PASSWORD"]), + required=True, + ), + validate_certs=dict( + type="bool", default=True, fallback=(env_fallback, ["VALIDATE_CERTS"]) + ), + state=dict(type="str", choices=["present", "absent"], default="present"), + wait=dict(type="bool", default=True), + ) + + def __init__(self, **kwargs): + argument_spec = deepcopy(self.argument_spec) + if kwargs.get("argument_spec"): + argument_spec.update(deepcopy(kwargs["argument_spec"])) + self.argument_spec_with_extra_keys = deepcopy(argument_spec) + self.strip_extra_attributes(argument_spec) + kwargs["argument_spec"] = argument_spec + + if not kwargs.get("supports_check_mode"): + kwargs["supports_check_mode"] = True + + super(BasePEModule, self).__init__(**kwargs) + + def strip_extra_attributes(self, argument_spec): + """ + This recursive method checks argument spec and remove extra spec definations which are not allowed in ansible + """ + for spec in argument_spec.values(): + for k in self.unsupported_spec_keys: + spec.pop(k, None) + if spec.get("options"): + self.strip_extra_attributes(spec["options"]) diff --git a/plugins/module_utils/v4/prism/helpers.py b/plugins/module_utils/v4/prism/helpers.py new file mode 100644 index 000000000..ff0a58af9 --- /dev/null +++ b/plugins/module_utils/v4/prism/helpers.py @@ -0,0 +1,98 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ..utils import raise_api_exception # noqa: E402 + + +def get_restore_source(module, api_instance, ext_id): + """ + This method will return restore source info using external ID. + Args: + module: Ansible module + api_instance: DomainManagerBackupApi instance from ntnx_prism_py_client sdk + ext_id (str): restore source info external ID + return: + restore_source_info (object): restore source info + """ + try: + return api_instance.get_restore_source_by_id(extId=ext_id).data + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching restore source info using ext_id", + ) + + +def get_backup_target(module, api_instance, ext_id): + """ + This method will return backup target info using external ID. + Args: + module: Ansible module + api_instance: DomainManagerBackupApi instance from ntnx_prism_py_client sdk + ext_id (str): backup target info external ID + return: + backup_target_info (object): backup target info + """ + domain_manager_ext_id = module.params.get("domain_manager_ext_id") + try: + return api_instance.get_backup_target_by_id( + extId=ext_id, domainManagerExtId=domain_manager_ext_id + ).data + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching backup target info using ext_id and domain_manager_ext_id", + ) + + +def get_pc_config(module, api_instance, ext_id): + """ + This method will return pc config info using external ID. + Args: + module: Ansible module + api_instance: DomainManagerBackupApi instance from ntnx_prism_py_client sdk + ext_id (str): pc external ID + return: + pc_config_info (object): pc config info + """ + try: + return api_instance.get_domain_manager_by_id(extId=ext_id).data + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching pc config info using ext_id", + ) + + +def get_restore_point( + module, + api_instance, + ext_id, + restore_source_ext_id, + restorable_domain_manager_ext_id, +): + """ + This method will return restore point info using external ID. + Args: + module: Ansible module + api_instance: DomainManagerBackupApi instance from ntnx_prism_py_client sdk + ext_id (str): restore point info external ID + return: + restore_point_info (object): restore point info + """ + try: + return api_instance.get_restore_point_by_id( + restoreSourceExtId=restore_source_ext_id, + restorableDomainManagerExtId=restorable_domain_manager_ext_id, + extId=ext_id, + ).data + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching restore point info using ext_id", + ) diff --git a/plugins/module_utils/v4/prism/pc_api_client.py b/plugins/module_utils/v4/prism/pc_api_client.py index 9da763af1..4e3de003c 100644 --- a/plugins/module_utils/v4/prism/pc_api_client.py +++ b/plugins/module_utils/v4/prism/pc_api_client.py @@ -67,3 +67,15 @@ def get_domain_manager_api_instance(module): """ api_client = get_pc_api_client(module) return ntnx_prism_py_client.DomainManagerApi(api_client=api_client) + + +def get_domain_manager_backup_api_instance(module): + """ + This method will return domain manager backup api instance. + Args: + module (object): Ansible module object + return: + api_instance (object): domain manager backup api instance + """ + api_client = get_pc_api_client(module) + return ntnx_prism_py_client.DomainManagerBackupsApi(api_client=api_client) diff --git a/plugins/module_utils/v4/prism/spec/pc.py b/plugins/module_utils/v4/prism/spec/pc.py new file mode 100644 index 000000000..9f40e3c08 --- /dev/null +++ b/plugins/module_utils/v4/prism/spec/pc.py @@ -0,0 +1,328 @@ +# Copyright: 2021, Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause ) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import traceback +from copy import deepcopy + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + + from ...sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + + +class PrismSpecs: + """Module specs related to prism""" + + # cloud_init_script_allowed_types = { + # "user_data": prism_sdk.Userdata, + # "custom_key_values": prism_sdk.CustomKeyValues, + # } + + location_allowed_types = { + "cluster_location": prism_sdk.ClusterLocation, + "object_store_location": prism_sdk.ObjectStoreLocation, + } + + build_info_spec = dict( + version=dict(type="str"), + ) + + # kvpair_spec = dict(name=dict(type="str"), value=dict(type="raw", no_log=False)) + + # custom_key_values_spec = dict( + # key_value_pairs=dict( + # type="list", + # elements="dict", + # options=kvpair_spec, + # obj=prism_sdk.KVPair, + # no_log=False, + # ), + # ) + # user_data = dict( + # value=dict(type="str", required=True), + # ) + # cloud_init_script = dict( + # user_data=dict(type="dict", options=user_data, obj=prism_sdk.Userdata), + # custom_key_values=dict( + # type="dict", + # options=custom_key_values_spec, + # obj=prism_sdk.CustomKeyValues, + # no_log=False, + # ), + # ) + + # cloud_init_config_spec = dict( + # datasource_type=dict(type="str", choices=["CONFIG_DRIVE_V2"]), + # metadata=dict(type="str"), + # cloud_init_script=dict( + # type="dict", + # options=cloud_init_script, + # obj=cloud_init_script_allowed_types, + # mutually_exclusive=[("user_data", "custom_key_values")], + # ), + # ) + # environment_info_spec = dict( + # type=dict(type="str", choices=["NTNX_CLOUD", "ONPREM"]), + # provider_type=dict( + # type="str", choices=["VSPHERE", "AZURE", "NTNX", "GCP", "AWS"] + # ), + # provisioning_type=dict(type="str", choices=["NATIVE", "NTNX"]), + # ) + # bootstrap_config_spec = dict( + # cloud_init_config=dict( + # type="lsit", + # elements="dict", + # options=cloud_init_config_spec, + # obj=prism_sdk.CloudInit, + # ), + # environment_info=dict( + # type="dict", + # options=environment_info_spec, + # obj=prism_sdk.EnvironmentInfo, + # ), + # ) + + # credentials_spec = dict( + # username=dict(type="str", required=True), + # password=dict(type="str", required=True, no_log=True), + # ) + + resource_config_spec = dict( + container_ext_ids=dict(type="list", elements="str"), + data_disk_size_bytes=dict(type="int"), + memory_size_bytes=dict(type="int"), + num_vcpus=dict(type="int"), + ) + + config_spec = dict( + should_enable_lockdown_mode=dict(type="bool"), + build_info=dict( + type="dict", options=build_info_spec, obj=prism_sdk.BuildInfo, required=True + ), + name=dict(type="str", required=True), + size=dict( + type="str", + choices=["SMALL", "LARGE", "EXTRALARGE", "STARTER"], + required=True, + ), + # bootstrap_config=dict( + # type="dict", options=bootstrap_config_spec, obj=prism_sdk.BootstrapConfig + # ), + # credentials=dict( + # type="dict", + # options=credentials_spec, + # obj=prism_sdk.Credentials, + # required=True, + # ), + resource_config=dict( + type="dict", + options=resource_config_spec, + obj=prism_sdk.DomainManagerResourceConfig, + ), + ) + + ipv4_spec = dict( + value=dict(type="str", required=True), + prefix_length=dict(type="int", default=32), + ) + + ipv6_spec = dict( + value=dict(type="str", required=True), + prefix_length=dict(type="int", default=128), + ) + + ip_address_spec = dict( + ipv4=dict(type="dict", options=ipv4_spec, obj=prism_sdk.IPv4Address), + ipv6=dict(type="dict", options=ipv6_spec, obj=prism_sdk.IPv6Address), + ) + + fqdn_spec = dict( + value=dict(type="str"), + ) + + ipaddress_or_fqdn_spec = dict( + ipv4=dict(type="dict", options=ipv4_spec, obj=prism_sdk.IPv4Address), + ipv6=dict(type="dict", options=ipv6_spec, obj=prism_sdk.IPv6Address), + fqdn=dict(type="dict", options=fqdn_spec, obj=prism_sdk.FQDN), + ) + + ip_ranges_spec = dict( + begin=dict(type="dict", options=ip_address_spec, obj=prism_sdk.IPAddress), + end=dict(type="dict", options=ip_address_spec, obj=prism_sdk.IPAddress), + ) + + internal_networks_spec = dict( + default_gateway=dict( + type="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + subnet_mask=dict( + type="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + ip_ranges=dict( + type="list", + elements="dict", + options=ip_ranges_spec, + obj=prism_sdk.IpRange, + required=True, + ), + ) + + external_networks_spec = dict( + default_gateway=dict( + type="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + subnet_mask=dict( + type="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + ip_ranges=dict( + type="list", + elements="dict", + options=ip_ranges_spec, + obj=prism_sdk.IpRange, + required=True, + ), + network_ext_id=dict(type="str", required=True), + ) + + network_spec = dict( + external_address=dict( + type="dict", options=ip_address_spec, obj=prism_sdk.IPAddress + ), + name_servers=dict( + type="list", + elements="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + ntp_servers=dict( + type="list", + elements="dict", + options=ipaddress_or_fqdn_spec, + obj=prism_sdk.IPAddressOrFQDN, + required=True, + ), + internal_networks=dict( + type="list", + elements="dict", + options=internal_networks_spec, + obj=prism_sdk.BaseNetwork, + ), + external_networks=dict( + type="list", + elements="dict", + options=external_networks_spec, + obj=prism_sdk.ExternalNetwork, + required=True, + ), + ) + prism_spec = dict( + config=dict( + type="dict", + options=config_spec, + obj=prism_sdk.DomainManagerClusterConfig, + required=True, + ), + network=dict( + type="dict", + options=network_spec, + obj=prism_sdk.DomainManagerNetwork, + required=True, + ), + should_enable_high_availability=dict(type="bool", default=False), + ) + + cluster_reference = dict( + ext_id=dict(type="str", required=True), + ) + + cluster_location_spec = dict( + config=dict( + type="dict", + options=cluster_reference, + obj=prism_sdk.ClusterReference, + required=True, + ), + ) + + access_key_credentials = dict( + access_key_id=dict(type="str", required=True), + secret_access_key=dict(type="str", required=True, no_log=True), + ) + + provider_config_spec = dict( + bucket_name=dict(type="str", required=True), + region=dict(type="str", default="us-east-1"), + credentials=dict( + type="dict", + options=access_key_credentials, + obj=prism_sdk.AccessKeyCredentials, + ), + ) + + backup_policy_spec = dict( + rpo_in_minutes=dict(type="int", required=True), + ) + + object_store_location_spec = dict( + provider_config=dict( + type="dict", + options=provider_config_spec, + obj=prism_sdk.AWSS3Config, + required=True, + ), + backup_policy=dict( + type="dict", + options=backup_policy_spec, + obj=prism_sdk.BackupPolicy, + ), + ) + + location_spec = dict( + cluster_location=dict( + type="dict", + options=cluster_location_spec, + obj=prism_sdk.ClusterLocation, + ), + object_store_location=dict( + type="dict", + options=object_store_location_spec, + obj=prism_sdk.ObjectStoreLocation, + ), + ) + + location_backup_spec = dict( + location=dict( + type="dict", + options=location_spec, + obj=location_allowed_types, + mutually_exclusive=[("cluster_location", "object_store_location")], + ), + ) + + @classmethod + def get_prism_spec(cls): + return deepcopy(cls.prism_spec) + + @classmethod + def get_location_backup_spec(cls): + return deepcopy(cls.location_backup_spec) diff --git a/plugins/modules/ntnx_pc_backup_target_info_v2.py b/plugins/modules/ntnx_pc_backup_target_info_v2.py new file mode 100644 index 000000000..bf23cbb95 --- /dev/null +++ b/plugins/modules/ntnx_pc_backup_target_info_v2.py @@ -0,0 +1,179 @@ +#!/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_pc_backup_target_info_v2 +short_description: Get backup targets info +version_added: 2.1.0 +description: + - Fetch specific backup target info using external ID + - Fetch list of multiple backup targets info if external ID is not provided with optional filters +options: + ext_id: + description: External ID to fetch specific backup target info + type: str + domain_manager_ext_id: + description: External ID of the domain manager + type: str + required: True +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info_v2 +author: + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +- name: List all backup targets + nutanix.ncp.ntnx_pc_backup_target_info_v2: + nutanix_host: + nutanix_username: + nutanix_password: + domain_manager_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + register: backup_result + +- name: Get backup target info + nutanix.ncp.ntnx_pc_backup_target_info_v2: + nutanix_host: + nutanix_username: + nutanix_password: + domain_manager_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + ext_id: "cda893b8-2aee-34bf-817d-d2ee6026790b" + register: backup_result +""" + +RETURN = r""" +response: + description: + - Response for fetching backup targets info + - Backup target info if external ID is provided + - List of multiple backup targets info if external ID is not provided + type: dict + returned: always + sample: + { + "backup_pause_reason": null, + "ext_id": "00062c47-ac15-ee40-185b-ac1f6b6f97e2", + "is_backup_paused": false, + "last_sync_time": null, + "links": null, + "location": { + "config": { + "ext_id": "00062c47-ac15-ee40-185b-ac1f6b6f97e2", + "name": "auto_cluster_prod_e98628bb1f50" + } + }, + "tenant_id": null + } + +ext_id: + description: External ID of the backup target + returned: always + type: str + sample: "cda893b8-2aee-34bf-817d-d2ee6026790b" + +changed: + description: This indicates whether the task resulted in any changes + returned: always + type: bool + sample: true + +error: + description: This field typically holds information about if the task have errors that occurred during the task execution + returned: always + type: bool + sample: false + +failed: + description: This field typically holds information about if the task have failed + returned: always + type: bool + sample: false + +""" + +import warnings # noqa: E402 + +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, +) +from ..module_utils.v4.prism.helpers import get_backup_target # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + ext_id=dict(type="str"), + domain_manager_ext_id=dict(type="str", required=True), + ) + return module_args + + +def get_backup_target_with_ext_id(module, domain_manager_backups_api, result): + ext_id = module.params.get("ext_id") + resp = get_backup_target(module, domain_manager_backups_api, ext_id) + result["ext_id"] = ext_id + result["response"] = strip_internal_attributes(resp.to_dict()) + + +def get_backup_targets(module, domain_manager_backups_api, result): + domain_manager_ext_id = module.params.get("domain_manager_ext_id") + sg = SpecGenerator(module) + kwargs, err = sg.get_info_spec(attr=module.params) + + if err: + result["error"] = err + module.fail_json(msg="Failed generating backup targets info Spec", **result) + try: + resp = domain_manager_backups_api.list_backup_targets( + domainManagerExtId=domain_manager_ext_id, **kwargs + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching backup targets info", + ) + + result["response"] = strip_internal_attributes(resp.to_dict()).get("data") + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + mutually_exclusive=[ + ("ext_id", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + domain_manager_backups_api = get_domain_manager_backup_api_instance(module) + if module.params.get("ext_id"): + get_backup_target_with_ext_id(module, domain_manager_backups_api, result) + else: + get_backup_targets(module, domain_manager_backups_api, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_backup_target_v2.py b/plugins/modules/ntnx_pc_backup_target_v2.py new file mode 100644 index 000000000..023e44e87 --- /dev/null +++ b/plugins/modules/ntnx_pc_backup_target_v2.py @@ -0,0 +1,447 @@ +#!/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_pc_backup_target_v2 +short_description: Create, Update and Delete a cluster or object store as the backup target. +version_added: 2.1.0 +description: + - Create, Update and Delete a cluster or object store as the backup target. + - For a given Prism Central, there can be up to 3 clusters as backup targets and 1 object store as backup target. +options: + state: + description: + - State of the backup target whether to create, update or delete. + - If C(state) is present, it will create or update the backup target. + - If C(state) is set to C(present) and ext_id is not provided then it will create a new backup target. + - If C(state) is set to C(present) and ext_id is provided then it will update the backup target. + - If C(state) is absent, it will delete the backup target. + choices: ['present', 'absent'] + type: str + wait: + description: + - Wait for the task to complete. + type: bool + required: false + domain_manager_ext_id: + description: A unique identifier for the domain manager. + type: str + required: true + ext_id: + description: A unique identifier for the backup target. + type: str + required: false + location: + description: + - Location of the backup target. + - For example, a cluster or an object store endpoint, such as AWS s3. + type: dict + required: true + suboptions: + cluster_location: + description: Location of the cluster. + type: dict + suboptions: + config: + description: Configuration of the Cluster reference of the remote cluster to be connected. + type: dict + required: true + suboptions: + ext_id: + description: External ID of the remote cluster. + type: str + required: true + object_store_location: + description: Location of the object store. + type: dict + suboptions: + provider_config: + description: The base model of S3 object store endpoint where domain manager is backed up. + type: dict + required: true + suboptions: + bucket_name: + description: The bucket name of the object store endpoint where backup data of domain manager is to be stored. + type: str + required: true + region: + description: The region name of the object store endpoint where backup data of domain manager is stored. + type: str + default: us-east-1 + credentials: + description: Secret credentials model for the object store containing access key ID and secret access key. + type: dict + required: false + suboptions: + access_key_id: + description: Access key Id for the object store provided for backup target. + type: str + required: true + secret_access_key: + description: Secret access key for the object store provided for backup target. + type: str + required: true + backup_policy: + description: Backup policy for the object store provided. + type: dict + suboptions: + rpo_in_minutes: + description: RPO interval in minutes at which the backup will be taken + type: int + required: true +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +- name: Create backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + nutanix_host: + nutanix_username: + nutanix_password: + domain_manager_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + location: + cluster_location: + config: + ext_id: "00062c47-ac15-ee40-185b-ac1f6b6f97e2" + register: result + +- name: Update backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + nutanix_host: + nutanix_username: + nutanix_password: + ext_id: "00062c47-ac15-ee40-185b-ac1f6b6f97e2" + domain_manager_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + location: + object_store_location: + provider_config: + bucket_name: "mybucket" + region: "us-east-1" + credentials: + access_key_id: "access_key_id" + secret_access_key: "secret_access_key" + backup_policy: + rpo_in_minutes: 120 + register: result + +- name: Delete backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + nutanix_host: + nutanix_username: + nutanix_password: + ext_id: "00062c47-ac15-ee40-185b-ac1f6b6f97e2" + domain_manager_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + state: absent + register: result +""" + +RETURN = r""" +response: + description: Task status for the backup target operation. + type: dict + returned: always + { + "cluster_ext_ids": [ + "00062c47-ac15-ee40-185b-ac1f6b6f97e2" + ], + "completed_time": "2025-01-29T07:35:54.109510+00:00", + "completion_details": null, + "created_time": "2025-01-29T07:35:49.556281+00:00", + "entities_affected": [ + { + "ext_id": "18553f0f-7b41-4115-bf42-2f698fbe7117", + "name": "prism_central", + "rel": "prism:config:domain_manager" + } + ], + "error_messages": null, + "ext_id": "ZXJnb24=:5f63a855-6b6e-4aca-4efb-159a35ce0e52", + "is_background_task": false, + "is_cancelable": false, + "last_updated_time": "2025-01-29T07:35:54.109509+00:00", + "legacy_error_message": null, + "number_of_entities_affected": 1, + "number_of_subtasks": 0, + "operation": "kCreateBackupTarget", + "operation_description": "Create Backup Target", + "owned_by": { + "ext_id": "00000000-0000-0000-0000-000000000000", + "name": "admin" + }, + "parent_task": null, + "progress_percentage": 100, + "root_task": null, + "started_time": "2025-01-29T07:35:49.569607+00:00", + "status": "SUCCEEDED", + "sub_steps": null, + "sub_tasks": null, + "warnings": null + } + +task_ext_id: + description: Task ID for the backup target operation. + type: str + returned: always + sample: "ZXJnb24=:5f63a855-6b6e-4aca-4efb-159a35ce0e52" + +ext_id: + description: External ID of the backup target. + type: str + returned: always + sample: "00062c47-ac15-ee40-185b-ac1f6b6f97e2" + +changed: + description: This indicates whether the task resulted in any changes + returned: always + type: bool + sample: true + +error: + description: This field typically holds information about if the task have errors that occurred during the task execution + returned: always + type: bool + sample: false + +failed: + description: This field typically holds information about if the task have failed + returned: always + type: bool + sample: false + +""" + +import traceback # noqa: E402 +import warnings # noqa: E402 +from copy import deepcopy # noqa: E402 + +from ansible.module_utils.basic import missing_required_lib # noqa: E402 + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, + get_etag, +) +from ..module_utils.v4.prism.tasks import wait_for_completion # noqa: E402 +from ..module_utils.v4.prism.helpers import get_backup_target # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +from ..module_utils.v4.prism.spec.pc import PrismSpecs as prism_specs # noqa: E402 + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + + from ..module_utils.v4.sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + ext_id=dict(type="str"), + domain_manager_ext_id=dict(type="str", required=True), + ) + module_args.update(prism_specs.get_location_backup_spec()) + return module_args + + +def create_backup_target(module, domain_manager_backups_api, result): + """ + This method will create backup target. + Args: + module (object): Ansible module object + domain_manager_backups_api (object): DomainManagerBackupApi instance + result (dict): Result object + """ + sg = SpecGenerator(module) + default_spec = prism_sdk.BackupTarget() + spec, err = sg.generate_spec(obj=default_spec) + if err: + result["error"] = err + module.fail_json(msg="Failed generating backup target create Spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + domain_manager_ext_id = module.params.get("domain_manager_ext_id") + try: + resp = domain_manager_backups_api.create_backup_target( + domainManagerExtId=domain_manager_ext_id, body=spec + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Failed to create backup target", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def check_idempotency(current_spec, update_spec): + if current_spec != update_spec: + return False + return True + + +def update_backup_target(module, domain_manager_backups_api, result): + """ + This method will update backup target. + Args: + module (object): Ansible module object + domain_manager_backups_api (object): DomainManagerBackupApi instance + result (dict): Result object + """ + ext_id = module.params.get("ext_id") + domain_manager_ext_id = module.params.get("domain_manager_ext_id") + + sg = SpecGenerator(module) + default_spec = prism_sdk.BackupTarget() + spec, err = sg.generate_spec(obj=default_spec) + + if err: + result["error"] = err + module.fail_json(msg="Failed generating update backup target spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + + current_spec = get_backup_target(module, domain_manager_backups_api, ext_id) + etag_value = get_etag(data=current_spec) + if not etag_value: + return module.fail_json( + "Unable to fetch etag for Updating Backing Target", **result + ) + update_spec, err = sg.generate_spec(obj=deepcopy(current_spec)) + if err: + result["error"] = err + module.fail_json(msg="Failed generating backup target update Spec", **result) + + if check_idempotency(current_spec, update_spec): + result["skipped"] = True + module.exit_json(msg="Nothing to change.", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(update_spec.to_dict()) + return + + resp = None + try: + resp = domain_manager_backups_api.update_backup_target_by_id( + domainManagerExtId=domain_manager_ext_id, + extId=ext_id, + body=update_spec, + if_match=etag_value, + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Failed to update backup target", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def delete_backup_target(module, domain_manager_backups_api, result): + ext_id = module.params.get("ext_id") + domain_manager_ext_id = module.params.get("domain_manager_ext_id") + result["ext_id"] = ext_id + current_spec = get_backup_target(module, domain_manager_backups_api, ext_id) + + etag_value = get_etag(data=current_spec) + if not etag_value: + return module.fail_json( + "Unable to fetch etag for Deleting Backing Target", **result + ) + + resp = None + try: + resp = domain_manager_backups_api.delete_backup_target_by_id( + domainManagerExtId=domain_manager_ext_id, extId=ext_id, if_match=etag_value + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while deleting backup target", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_if=[ + ("state", "present", ["location"]), + ("state", "absent", ["ext_id"]), + ], + ) + if SDK_IMP_ERROR: + module.fail_json( + msg=missing_required_lib("ntnx_prism_py_client"), exception=SDK_IMP_ERROR + ) + + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "ext_id": None, + } + state = module.params.get("state") + domain_manager_backups_api = get_domain_manager_backup_api_instance(module) + if state == "present": + if module.params.get("ext_id"): + update_backup_target(module, domain_manager_backups_api, result) + else: + create_backup_target(module, domain_manager_backups_api, result) + else: + delete_backup_target(module, domain_manager_backups_api, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_config_info_v2.py b/plugins/modules/ntnx_pc_config_info_v2.py new file mode 100644 index 000000000..0b67ea810 --- /dev/null +++ b/plugins/modules/ntnx_pc_config_info_v2.py @@ -0,0 +1,160 @@ +#!/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_pc_config_info_v2 +short_description: Get PC Configuration info +version_added: 2.1.0 +description: + - Fetch specific PC Configuration info using external ID + - Fetch list of multiple PC Configuration info if external ID is not provided with optional filters +options: + ext_id: + description: External ID to fetch specific PC Configuration info + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info_v2 +author: + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +- name: List all PCs + nutanix.ncp.ntnx_pc_config_info_v2: + nutanix_host: + nutanix_username: + nutanix_password: + register: result + +- name: Fetch PC details using external ID + nutanix.ncp.ntnx_pc_config_info_v2: + ext_id: "cda893b8-2aee-34bf-817d-d2ee6026790b" + register: result + +- name: List all PCs with filter + nutanix.ncp.ntnx_pc_config_info_v2: + nutanix_host: + nutanix_username: + nutanix_password: + filter: extId eq '{{ domain_manager_ext_id }}' + register: pc_details +""" + +RETURN = r""" +response: + description: + - Response for fetching PC Configuration info + - PC Configuration info if external ID is provided + - List of multiple PC Configuration info if external ID is not provided + type: dict + returned: always + sample: + {} + +ext_id: + description: External ID of the PC + type: str + returned: always + sample: "cda893b8-2aee-34bf-817d-d2ee6026790b" + +changed: + description: This indicates whether the task resulted in any changes + returned: always + type: bool + sample: true + +error: + description: This field typically holds information about if the task have errors that occurred during the task execution + returned: always + type: bool + sample: false + +failed: + description: This field typically holds information about if the task have failed + returned: always + type: bool + sample: false + +""" + +import warnings # noqa: E402 + +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_api_instance, +) +from ..module_utils.v4.prism.helpers import get_pc_config # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + ext_id=dict(type="str"), + ) + return module_args + + +def get_pc_config_with_ext_id(module, domain_manager_api, result): + ext_id = module.params.get("ext_id") + resp = get_pc_config(module, domain_manager_api, ext_id) + result["ext_id"] = ext_id + result["response"] = strip_internal_attributes(resp.to_dict()) + + +def get_pc_configs(module, domain_manager_api, result): + sg = SpecGenerator(module) + kwargs, err = sg.get_info_spec(attr=module.params) + + if err: + result["error"] = err + module.fail_json(msg="Failed generating PC Configuration info Spec", **result) + try: + resp = domain_manager_api.list_domain_managers(**kwargs) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching PC Configuration info", + ) + result["response"] = strip_internal_attributes(resp.to_dict()).get("data") + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + mutually_exclusive=[ + ("ext_id", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + domain_manager_api = get_domain_manager_api_instance(module) + if module.params.get("ext_id"): + get_pc_config_with_ext_id(module, domain_manager_api, result) + else: + get_pc_configs(module, domain_manager_api, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_deploy_v2.py b/plugins/modules/ntnx_pc_deploy_v2.py new file mode 100644 index 000000000..be37d7bf1 --- /dev/null +++ b/plugins/modules/ntnx_pc_deploy_v2.py @@ -0,0 +1,654 @@ +#!/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_pc_deploy_v2 +short_description: Deploys a Prism Central using the provided details +version_added: 2.1.0 +description: + - Deploys a Prism Central using the provided details + - Prism Central Size, Network Config are mandatory fields to deploy Prism Central + - If wait is set to true, the module will wait for the task to complete +options: + wait: + description: Wait for the operation to complete. + type: bool + required: false + default: True + config: + description: Domain manager (Prism Central) cluster configuration details. + type: dict + required: true + suboptions: + should_enable_lockdown_mode: + description: A boolean value indicating whether to enable lockdown mode for a cluster. + type: bool + required: false + build_info: + description: Currently representing the build information to be used for the cluster creation. + type: dict + required: true + suboptions: + version: + description: Software version. + type: str + required: false + name: + description: Name of the domain manager (Prism Central). + type: str + required: true + size: + description: Domain manager (Prism Central) size is an enumeration of starter, small, large, or extra large starter values. + type: str + required: true + choices: + - SMALL + - LARGE + - EXTRALARGE + - STARTER + resource_config: + description: + - This configuration is used to provide the resource-related details like container external identifiers, number of VCPUs, memory size, + data disk size of the domain manager (Prism Central). + - In the case of a multi-node setup, the sum of resources like number of VCPUs, memory size and data disk size are provided. + type: dict + required: true + suboptions: + container_ext_ids: + description: The external identifier of the container that will be used to create the domain manager (Prism Central) cluster. + type: list + required: false + elements: str + network: + description: Domain manager (Prism Central) network configuration details. + type: dict + required: true + suboptions: + external_address: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + name_servers: + description: + - List of name servers on a cluster. + - For create operation, only ipv4 address / fqdn values are supported currently. + type: list + required: true + elements: dict + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ntp_servers: + description: + - List of NTP servers on a cluster + - For create operation, only ipv4 address / fqdn values are supported currently. + type: list + required: true + elements: dict + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + internal_networks: + description: This configuration is used to internally manage Prism Central network. + type: list + elements: dict + required: false + suboptions: + default_gateway: + description: + - The default gateway of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + subnet_mask: + description: + - The subnet mask of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ip_ranges: + description: Range of IPs used for Prism Central network setup. + type: list + elements: dict + suboptions: + begin: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + end: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + external_networks: + description: This configuration is used to manage Prism Central. + type: list + elements: dict + required: true + suboptions: + default_gateway: + description: + - The default gateway of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + subnet_mask: + description: + - The subnet mask of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ip_ranges: + description: Range of IPs used for Prism Central network setup. + type: list + elements: dict + suboptions: + begin: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + end: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + network_ext_id: + description: The network external identifier to which Domain Manager (Prism Central) is to be deployed or is already configured. + type: str + required: true + should_enable_high_availability: + description: This configuration enables Prism Central to be deployed in scale-out mode. + type: bool + required: false + default: false +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Prem Karat (@premkarat) + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import traceback # noqa: E402 +import warnings # noqa: E402 + +from ansible.module_utils.basic import missing_required_lib # noqa: E402 + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( + get_domain_manager_api_instance, +) # noqa: E402 +from ..module_utils.v4.prism.tasks import wait_for_completion # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) +from ..module_utils.v4.prism.spec.pc import PrismSpecs as prism_specs # noqa: E402 + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + + from ..module_utils.v4.sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = prism_specs.get_prism_spec() + return module_args + + +def deploy_pc(module, result): + """ + This method will deploy prism central. + Args: + module (object): Ansible module object + result (dict): Result object + """ + sg = SpecGenerator(module) + default_spec = prism_sdk.DomainManager() + spec, err = sg.generate_spec(obj=default_spec) + domain_manager_api = get_domain_manager_api_instance(module) + + if err: + result["error"] = err + module.fail_json(msg="Failed generating deploy prism central spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + + resp = None + try: + resp = domain_manager_api.create_domain_manager(body=spec) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while deploying prism central", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + ) + if SDK_IMP_ERROR: + module.fail_json( + msg=missing_required_lib("ntnx_prism_py_client"), exception=SDK_IMP_ERROR + ) + + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "ext_id": None, + } + deploy_pc(module, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_restorable_domain_managers_info_v2.py b/plugins/modules/ntnx_pc_restorable_domain_managers_info_v2.py new file mode 100644 index 000000000..42d0b37b8 --- /dev/null +++ b/plugins/modules/ntnx_pc_restorable_domain_managers_info_v2.py @@ -0,0 +1,105 @@ +#!/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_pc_restorable_domain_managers_info_v2 +short_description: Fetch restorable domain managers info +version_added: 2.1.0 +description: Fetch list of multiple restorable domain managers for a given restore source. +options: + restore_source_ext_id: + description: + - External ID of the restore source. + required: true + type: str +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +- name: Get all restorable domain managers for a given restore source + ntnx_pc_restorable_domain_managers_info_v2: + nutanix_host: + nutanix_username: + nutanix_password: + restore_source_ext_id: "d4e44c2b-944c-48b0-8de1-b0adae3d54c6" + register: result +""" + +RETURN = r""" +""" + +import warnings # noqa: E402 + +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, +) +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict(restore_source_ext_id=dict(type="str", required=True)) + return module_args + + +def get_restorable_domain_managers(module, domain_manager_backups_api, result): + restore_source_ext_id = module.params.get("restore_source_ext_id") + sg = SpecGenerator(module) + kwargs, err = sg.get_info_spec(attr=module.params) + if err: + result["error"] = err + module.fail_json( + msg="Failed generating restorable domain managers info Spec", **result + ) + try: + resp = domain_manager_backups_api.list_restorable_domain_managers( + restoreSourceExtId=restore_source_ext_id, **kwargs + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching restorable domain managers info", + ) + + result["response"] = strip_internal_attributes(resp.to_dict()).get("data") + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + mutually_exclusive=[ + ("ext_id", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + domain_manager_backups_api = get_domain_manager_backup_api_instance(module) + get_restorable_domain_managers(module, domain_manager_backups_api, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_restore_points_info_v2.py b/plugins/modules/ntnx_pc_restore_points_info_v2.py new file mode 100644 index 000000000..3f47f0f8b --- /dev/null +++ b/plugins/modules/ntnx_pc_restore_points_info_v2.py @@ -0,0 +1,111 @@ +#!/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""" +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import warnings # noqa: E402 + +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, +) +from ..module_utils.v4.prism.helpers import get_restore_point # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + restore_source_ext_id=dict(type="str", required=True), + restorable_domain_manager_ext_id=dict(type="str", required=True), + ext_id=dict(type="str"), + ) + return module_args + + +def get_restore_points(module, domain_manager_backups_api, result): + restore_source_ext_id = module.params.get("restore_source_ext_id") + restorable_domain_manager_ext_id = module.params.get( + "restorable_domain_manager_ext_id" + ) + sg = SpecGenerator(module) + kwargs, err = sg.get_info_spec(attr=module.params) + if err: + result["error"] = err + module.fail_json(msg="Failed generating restore points info Spec", **result) + try: + resp = domain_manager_backups_api.list_restore_points( + restoreSourceExtId=restore_source_ext_id, + restorableDomainManagerExtId=restorable_domain_manager_ext_id, + **kwargs, + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while fetching restore points info", + ) + + result["response"] = strip_internal_attributes(resp.to_dict()).get("data") + + +def get_restore_points_with_ext_id(module, domain_manager_backups_api, result): + restore_source_ext_id = module.params.get("restore_source_ext_id") + restorable_domain_manager_ext_id = module.params.get( + "restorable_domain_manager_ext_id" + ) + ext_id = module.params.get("ext_id") + resp = get_restore_point( + module, + domain_manager_backups_api, + ext_id, + restore_source_ext_id, + restorable_domain_manager_ext_id, + ) + result["ext_id"] = ext_id + result["response"] = strip_internal_attributes(resp.to_dict()) + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + mutually_exclusive=[ + ("ext_id", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + domain_manager_backups_api = get_domain_manager_backup_api_instance(module) + if module.params.get("ext_id"): + get_restore_points_with_ext_id(module, domain_manager_backups_api, result) + else: + get_restore_points(module, domain_manager_backups_api, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_restore_source_info_v2.py b/plugins/modules/ntnx_pc_restore_source_info_v2.py new file mode 100644 index 000000000..38ff25484 --- /dev/null +++ b/plugins/modules/ntnx_pc_restore_source_info_v2.py @@ -0,0 +1,84 @@ +#!/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_pc_restore_source_info_v2 +short_description: Get PC restore source info +version_added: 2.1.0 +description: + - Fetch specific restore source info using external ID +options: + ext_id: + description: External ID to fetch specific restore source info + type: str + required: True +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_info_v2 +author: + - Prem Karat (@premkarat) + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import warnings # noqa: E402 + +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.base_info_module import BaseInfoModule # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, +) +from ..module_utils.v4.prism.helpers import get_restore_source # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + strip_internal_attributes, +) + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict(ext_id=dict(type="str", required=True)) + return module_args + + +def get_restore_source_with_ext_id(module, prism, result): + ext_id = module.params.get("ext_id") + resp = get_restore_source(module, prism, ext_id) + result["ext_id"] = ext_id + result["response"] = strip_internal_attributes(resp.to_dict()) + + +def run_module(): + module = BaseInfoModule( + argument_spec=get_module_spec(), + supports_check_mode=False, + mutually_exclusive=[ + ("ext_id", "filter"), + ], + ) + remove_param_with_none_value(module.params) + result = {"changed": False, "error": None, "response": None} + prism = get_domain_manager_backup_api_instance(module) + if module.params.get("ext_id"): + get_restore_source_with_ext_id(module, prism, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_restore_source_v2.py b/plugins/modules/ntnx_pc_restore_source_v2.py new file mode 100644 index 000000000..44748cf19 --- /dev/null +++ b/plugins/modules/ntnx_pc_restore_source_v2.py @@ -0,0 +1,249 @@ +#!/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_pc_restore_source_v2 +short_description: Creates or Deletes a restore source pointing to a cluster or object store to restore the domain manager. +version_added: 2.1.0 +description: + - Creates or Deletes a restore source pointing to a cluster or object store to restore the domain manager. + - The created restore source is intended to be deleted after use. + - If the restore source is not deleted using the deleteRestoreSource API, then it is auto-deleted after sometime. +options: + state: + description: + - If C(present), will create a restore source. + - If C(absent), will delete a restore source. + type: str + required: False + choices: + - present + - absent + wait: + description: + - Wait for the task to complete. + type: bool + required: False + default: True + location: + description: + - Location of the backup target. + - For example, a cluster or an object store endpoint, such as AWS s3. + type: dict + required: true + suboptions: + cluster_location: + description: Location of the cluster. + type: dict + suboptions: + config: + description: Configuration of the Cluster reference of the remote cluster to be connected. + type: dict + required: true + suboptions: + ext_id: + description: External ID of the remote cluster. + type: str + required: true + object_store_location: + description: Location of the object store. + type: dict + suboptions: + provider_config: + description: The base model of S3 object store endpoint where domain manager is backed up. + type: dict + required: true + suboptions: + bucket_name: + description: The bucket name of the object store endpoint where backup data of domain manager is to be stored. + type: str + required: true + region: + description: The region name of the object store endpoint where backup data of domain manager is stored. + type: str + default: us-east-1 + credentials: + description: Secret credentials model for the object store containing access key ID and secret access key. + type: dict + required: false + suboptions: + access_key_id: + description: Access key Id for the object store provided for backup target. + type: str + required: true + secret_access_key: + description: Secret access key for the object store provided for backup target. + type: str + required: true + backup_policy: + description: Backup policy for the object store provided. + type: dict + suboptions: + rpo_in_minutes: + description: RPO interval in minutes at which the backup will be taken + type: int + required: true +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Prem Karat (@premkarat) + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import traceback # noqa: E402 +import warnings # noqa: E402 + +from ansible.module_utils.basic import missing_required_lib # noqa: E402 + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, + get_etag, +) +from ..module_utils.v4.prism.helpers import get_restore_source # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +from ..module_utils.v4.prism.spec.pc import PrismSpecs as prism_specs # noqa: E402 + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + + from ..module_utils.v4.sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + ext_id=dict(type="str"), + ) + module_args.update(prism_specs.get_location_backup_spec()) + return module_args + + +def create_restore_source(module, domain_manager_backups_api, result): + """ + This method will create restore source. + Args: + module (object): Ansible module object + domain_manager_backups_api (object): DomainManagerBackupApi instance + result (dict): Result object + """ + sg = SpecGenerator(module) + default_spec = prism_sdk.RestoreSource() + spec, err = sg.generate_spec(obj=default_spec) + if err: + result["error"] = err + module.fail_json(msg="Failed generating restore source spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + + resp = None + try: + resp = domain_manager_backups_api.create_restore_source(body=spec) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while creating restore source", + ) + result["ext_id"] = resp.data.ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + result["changed"] = True + + +def delete_restore_source(module, domain_manager_backups_api, result): + """ + This method will delete restore source. + Args: + module (object): Ansible module object + domain_manager_backups_api (object): DomainManagerBackupApi instance + result (dict): Result object + """ + ext_id = module.params.get("ext_id") + result["ext_id"] = ext_id + current_spec = get_restore_source(module, domain_manager_backups_api, ext_id) + + etag_value = get_etag(data=current_spec) + if not etag_value: + return module.fail_json( + "Unable to fetch etag for Deleting Restore Source", **result + ) + resp = None + try: + resp = domain_manager_backups_api.delete_restore_source_by_id( + extId=ext_id, if_match=etag_value + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while deleting restore source", + ) + result["response"] = strip_internal_attributes(resp) + result["changed"] = True + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + required_if=[ + ("state", "present", ["location"]), + ("state", "absent", ["ext_id"]), + ], + ) + if SDK_IMP_ERROR: + module.fail_json( + msg=missing_required_lib("ntnx_prism_py_client"), exception=SDK_IMP_ERROR + ) + + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "ext_id": None, + } + + state = module.params.get("state") + prsim = get_domain_manager_backup_api_instance(module) + if state == "present": + create_restore_source(module, prsim, result) + else: + delete_restore_source(module, prsim, result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_restore_v2.py b/plugins/modules/ntnx_pc_restore_v2.py new file mode 100644 index 000000000..40a517e26 --- /dev/null +++ b/plugins/modules/ntnx_pc_restore_v2.py @@ -0,0 +1,704 @@ +#!/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_pc_restore_v2 +short_description: Restores a domain manager from a cluster or object store backup location. +version_added: 2.1.0 +description: + - The restore domain manager is a task-driven operation to restore a domain manager + from a cluster or object store backup location based on the selected restore point. +options: + ext_id: + description: + - Restore point ID for the backup created in cluster/object store. + type: str + required: True + wait: + description: + - Wait for the task to complete. + type: bool + required: False + restore_source_ext_id: + description: A unique identifier obtained from the restore source API that corresponds to the details provided for the restore source. + type: str + required: True + restorable_domain_manager_ext_id: + description: External ID of the domain manager. + type: str + required: True + domain_manager: + description: Domain manager (Prism Central) details. + type: dict + required: True + suboptions: + config: + description: Domain manager (Prism Central) cluster configuration details. + type: dict + required: true + suboptions: + should_enable_lockdown_mode: + description: A boolean value indicating whether to enable lockdown mode for a cluster. + type: bool + required: false + build_info: + description: Currently representing the build information to be used for the cluster creation. + type: dict + required: true + suboptions: + version: + description: Software version. + type: str + required: false + name: + description: Name of the domain manager (Prism Central). + type: str + required: true + size: + description: Domain manager (Prism Central) size is an enumeration of starter, small, large, or extra large starter values. + type: str + required: true + choices: + - SMALL + - LARGE + - EXTRALARGE + - STARTER + resource_config: + description: + - This configuration is used to provide the resource-related details + like container external identifiers, number of VCPUs, memory size, data disk size of the domain manager (Prism Central). + - In the case of a multi-node setup, the sum of resources like number of VCPUs, memory size and data disk size are provided. + type: dict + required: false + suboptions: + container_ext_ids: + description: The external identifier of the container that will be used to create the domain manager (Prism Central) cluster. + type: list + required: false + elements: str + data_disk_size_bytes: + description: The size of the data disk in bytes. + type: int + required: false + memory_size_bytes: + description: The size of the memory in bytes. + type: int + required: false + num_vcpus: + description: The number of virtual CPUs. + type: int + required: false + network: + description: Domain manager (Prism Central) network configuration details. + type: dict + required: true + suboptions: + external_address: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + name_servers: + description: + - List of name servers on a cluster. + - For create operation, only ipv4 address / fqdn values are supported currently. + type: list + required: true + elements: dict + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ntp_servers: + description: + - List of NTP servers on a cluster + - For create operation, only ipv4 address / fqdn values are supported currently. + type: list + required: true + elements: dict + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + internal_networks: + description: This configuration is used to internally manage Prism Central network. + type: list + elements: dict + required: false + suboptions: + default_gateway: + description: + - The default gateway of the network. + - An unique address that identifies a device on the internet or a local network + in IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location + in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + subnet_mask: + description: + - The subnet mask of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location + in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ip_ranges: + description: Range of IPs used for Prism Central network setup. + type: list + required: true + elements: dict + suboptions: + begin: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + end: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + external_networks: + description: This configuration is used to manage Prism Central. + type: list + elements: dict + required: true + suboptions: + default_gateway: + description: + - The default gateway of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location + in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + subnet_mask: + description: + - The subnet mask of the network. + - An unique address that identifies a device on the internet or a local network in + IPv4/IPv6 format or a Fully Qualified Domain Name. + type: dict + required: true + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + fqdn: + description: A fully qualified domain name that specifies its exact location + in the tree hierarchy of the Domain Name System. + type: dict + required: false + suboptions: + value: + description: Fully Qualified Domain Name of the Host. + type: str + required: false + ip_ranges: + description: Range of IPs used for Prism Central network setup. + type: list + required: true + elements: dict + suboptions: + begin: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + end: + description: An unique address that identifies a device on the internet or a local network in IPv4 or IPv6 format. + type: dict + required: false + suboptions: + ipv4: + description: An unique address that identifies a device on the internet or a local network in IPv4 format. + type: dict + required: false + suboptions: + value: + description: The IPv4 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv4 address belongs. + type: int + required: false + default: 32 + ipv6: + description: An unique address that identifies a device on the internet or a local network in IPv6 format. + type: dict + required: false + suboptions: + value: + description: The IPv6 address of the host. + type: str + required: true + prefix_length: + description: The prefix length of the network to which this host IPv6 address belongs. + type: int + required: false + default: 128 + network_ext_id: + description: The network external identifier to which Domain Manager (Prism Central) is to be deployed or is already configured. + type: str + required: true + should_enable_high_availability: + description: This configuration enables Prism Central to be deployed in scale-out mode. + type: bool + required: false + default: false +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Prem Karat (@premkarat) + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +""" + +RETURN = r""" +""" + +import traceback # noqa: E402 +import warnings # noqa: E402 + +from ansible.module_utils.basic import missing_required_lib # noqa: E402 + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_backup_api_instance, +) +from ..module_utils.v4.prism.tasks import wait_for_completion # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) +from ..module_utils.v4.prism.spec.pc import PrismSpecs as prism_specs # noqa: E402 + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + + from ..module_utils.v4.sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + ext_id=dict(type="str", required=True), + restore_source_ext_id=dict(type="str", required=True), + restorable_domain_manager_ext_id=dict(type="str", required=True), + domain_manager=dict( + type="dict", + options=prism_specs.prism_spec, + obj=prism_sdk.DomainManager, + required=True, + ), + ) + return module_args + + +def restore_domain_manager(module, result): + sg = SpecGenerator(module) + default_spec = prism_sdk.RestoreSpec() + spec, err = sg.generate_spec(obj=default_spec) + domain_manager_api = get_domain_manager_backup_api_instance(module) + restore_source_ext_id = module.params.get("restore_source_ext_id") + restorable_domain_manager_ext_id = module.params.get( + "restorable_domain_manager_ext_id" + ) + ext_id = module.params.get("ext_id") + result["restore_source_ext_id"] = restore_source_ext_id + result["restorable_domain_manager_ext_id"] = restorable_domain_manager_ext_id + result["ext_id"] = ext_id + if err: + result["error"] = err + module.fail_json(msg="Failed generating restore domain manager spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + + resp = None + try: + resp = domain_manager_api.restore( + restoreSourceExtId=restore_source_ext_id, + restorableDomainManagerExtId=restorable_domain_manager_ext_id, + extId=ext_id, + body=spec, + ) + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="Api Exception raised while restoring domain manager", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + ) + if SDK_IMP_ERROR: + module.fail_json( + msg=missing_required_lib("ntnx_prism_py_client"), exception=SDK_IMP_ERROR + ) + + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "ext_id": None, + } + restore_domain_manager(module, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ntnx_pc_unregistration_v2.py b/plugins/modules/ntnx_pc_unregistration_v2.py new file mode 100644 index 000000000..e73ff12a4 --- /dev/null +++ b/plugins/modules/ntnx_pc_unregistration_v2.py @@ -0,0 +1,246 @@ +#!/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_pc_unregistration_v2 +short_description: Unregister a registered remote cluster from the local cluster. +version_added: 2.1.0 +description: + - Unregister a registered remote cluster from the local cluster. +options: + wait: + description: + - Wait for the task to complete. + type: bool + required: False + pc_ext_id: + description: + - External ID of the local cluster. + type: str + required: True + ext_id: + description: + - External ID of the remote cluster. + type: str + required: True +extends_documentation_fragment: + - nutanix.ncp.ntnx_credentials + - nutanix.ncp.ntnx_operations_v2 +author: + - Abhinav Bansal (@abhinavbansal29) +""" + +EXAMPLES = r""" +- name: Unregister PC + nutanix.ncp.ntnx_pc_unregistration_v2: + nutanix_host: + nutanix_username: + nutanix_password: + ext_id: "86b54161-3214-5874-9632-89afcd365004" + pc_ext_id: "18553f0f-8547-4115-9696-2f698fbe7117" + register: result +""" + +RETURN = r""" +response: + description: Task response for unregistering the remote cluster. + type: dict + returned: always + sample: + { + "cluster_ext_ids": [ + "00062c47-4512-1233-1122-ac1f6b6f97e2" + ], + "completed_time": "2025-01-29T06:51:25.368280+00:00", + "completion_details": null, + "created_time": "2025-01-29T06:51:20.378947+00:00", + "entities_affected": [ + { + "ext_id": "18553f0f-1232-4333-2222-2f698fbe7117", + "name": "PC_10.44.76.100", + "rel": "prism:management:domain_manager" + }, + { + "ext_id": "86b54161-1221-1233-9875-89afcd365004", + "name": null, + "rel": "prism:management:domain_manager" + } + ], + "error_messages": null, + "ext_id": "ZXJnb24=:7f0399f6-370c-59f8-b7a3-c50e4b91a6d0", + "is_background_task": false, + "is_cancelable": false, + "last_updated_time": "2025-01-29T06:51:25.368279+00:00", + "legacy_error_message": null, + "number_of_entities_affected": 2, + "number_of_subtasks": 0, + "operation": "UnregisterPC", + "operation_description": "Unregister Prism Central", + "owned_by": { + "ext_id": "00000000-0000-0000-0000-000000000000", + "name": "admin" + }, + "parent_task": null, + "progress_percentage": 100, + "root_task": null, + "started_time": "2025-01-29T06:51:20.392266+00:00", + "status": "SUCCEEDED", + "sub_steps": [ + { + "name": "Unregistering cluster started" + }, + { + "name": "Precheck completed successfully" + }, + { + "name": "Successfully unconfigured entities" + }, + { + "name": "Successfully revoked trust" + }, + { + "name": "Unregister cluster 86b54161-1221-1233-9875-89afcd365004 completed successfully" + } + ], + "sub_tasks": null, + "warnings": null + } +task_ext_id: + description: External ID of the task. + type: str + returned: always + sample: "ZXJnb24=:7f0399f6-370c-59f8-b7a3-c50e4b91a6d0" + +pc_ext_id: + description: External ID of the local cluster. + type: str + returned: always + sample: "18553f0f-8547-4115-9696-2f698fbe7117" + +changed: + description: This indicates whether the task resulted in any changes + type: bool + returned: always + sample: true + +error: + description: Error message if any. + type: str + returned: always + sample: null +""" + +import traceback # noqa: E402 +import warnings # noqa: E402 + +from ansible.module_utils.basic import missing_required_lib # noqa: E402 + +from ..module_utils.base_module import BaseModule # noqa: E402 +from ..module_utils.utils import remove_param_with_none_value # noqa: E402 +from ..module_utils.v4.prism.pc_api_client import ( # noqa: E402 + get_domain_manager_api_instance, + get_etag, +) +from ..module_utils.v4.prism.helpers import get_pc_config # noqa: E402 +from ..module_utils.v4.prism.tasks import wait_for_completion # noqa: E402 +from ..module_utils.v4.spec_generator import SpecGenerator # noqa: E402 +from ..module_utils.v4.utils import ( # noqa: E402 + raise_api_exception, + strip_internal_attributes, +) + +SDK_IMP_ERROR = None +try: + import ntnx_prism_py_client as prism_sdk # noqa: E402 +except ImportError: + from ..module_utils.v4.sdk_mock import mock_sdk as prism_sdk # noqa: E402 + + SDK_IMP_ERROR = traceback.format_exc() + +# Suppress the InsecureRequestWarning +warnings.filterwarnings("ignore", message="Unverified HTTPS request is being made") + + +def get_module_spec(): + module_args = dict( + pc_ext_id=dict(type="str", required=True), + ext_id=dict(type="str", required=True), + ) + return module_args + + +def unregister_cluster(module, domain_manager_api, result): + pc_ext_id = module.params.get("pc_ext_id") + result["pc_ext_id"] = pc_ext_id + sg = SpecGenerator(module) + default_spec = prism_sdk.ClusterUnregistrationSpec() + spec, err = sg.generate_spec(obj=default_spec) + if err: + result["error"] = err + module.fail_json(msg="Failed generating unregistering cluster spec", **result) + + if module.check_mode: + result["response"] = strip_internal_attributes(spec.to_dict()) + return + + current_spec = get_pc_config(module, domain_manager_api, pc_ext_id) + etag_value = get_etag(data=current_spec) + if not etag_value: + module.fail_json(msg="Failed to get etag value for the PC", **result) + + resp = None + try: + resp = domain_manager_api.unregister( + extId=pc_ext_id, body=spec, if_match=etag_value + ) + result["changed"] = True + except Exception as e: + raise_api_exception( + module=module, + exception=e, + msg="API Exception raised while unregistering cluster", + ) + task_ext_id = resp.data.ext_id + result["task_ext_id"] = task_ext_id + result["response"] = strip_internal_attributes(resp.data.to_dict()) + if task_ext_id and module.params.get("wait"): + task_status = wait_for_completion(module, task_ext_id) + result["response"] = strip_internal_attributes(task_status.to_dict()) + result["changed"] = True + + +def run_module(): + module = BaseModule( + argument_spec=get_module_spec(), + supports_check_mode=True, + ) + if SDK_IMP_ERROR: + module.fail_json( + msg=missing_required_lib("ntnx_prism_py_client"), + exception=SDK_IMP_ERROR, + ) + + remove_param_with_none_value(module.params) + result = { + "changed": False, + "error": None, + "response": None, + "ext_id": None, + } + domain_manager_api = get_domain_manager_api_instance(module) + unregister_cluster(module, domain_manager_api, result) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ntnx_prism_v2/aliases b/tests/integration/targets/ntnx_prism_v2/aliases new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/targets/ntnx_prism_v2/meta/main.yml b/tests/integration/targets/ntnx_prism_v2/meta/main.yml new file mode 100644 index 000000000..e4f447d3a --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_env diff --git a/tests/integration/targets/ntnx_prism_v2/tasks/cluster_location.yml b/tests/integration/targets/ntnx_prism_v2/tasks/cluster_location.yml new file mode 100644 index 000000000..ecda4d464 --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/tasks/cluster_location.yml @@ -0,0 +1,738 @@ +--- +- name: Start ntnx_prism_v2 tests + ansible.builtin.debug: + msg: Start ntnx_prism_v2 tests + +############################################################# +# List all clusters to get prism central external ID + +- name: List all clusters to get prism central external ID + nutanix.ncp.ntnx_clusters_info_v2: + filter: "config/clusterFunction/any(t:t eq Clustermgmt.Config.ClusterFunctionRef'PRISM_CENTRAL')" + register: result + ignore_errors: true + +- name: Get prism central external ID + ansible.builtin.set_fact: + domain_manager_ext_id: "{{ result.response[0].ext_id }}" + +############################################################# +# Generate spec using check mode for: +# - Creating backup target cluster +# - Updating backup target cluster +# - Creating restore source cluster +# - Creating restore source object store + +- name: Generate spec for creating backup target cluster using check mode + nutanix.ncp.ntnx_pc_backup_target_v2: + domain_manager_ext_id: "96325874-8523-9865-1478-074816fe2a4f" + location: + cluster_location: + config: + ext_id: "38648904-4859-1258-7485-074816fe2a4f" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for creating backup target cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.location.config.ext_id == "38648904-4859-1258-7485-074816fe2a4f" + fail_msg: "Generated spec for creating backup target cluster failed" + success_msg: "Generated spec for creating backup target cluster passed" + +- name: Generate spec for updating backup target cluster using check mode + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "84785699-8744-2895-9632-074816fe2a4f" + domain_manager_ext_id: "14855555-9999-1235-3141-074816fe2a4f" + location: + cluster_location: + config: + ext_id: "31415252-3577-7474-7788-074816fe2a4f" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for updating backup target cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == "84785699-8744-2895-9632-074816fe2a4f" + - result.response.location.config.ext_id == "31415252-3577-7474-7788-074816fe2a4f" + fail_msg: "Generated spec for updating backup target cluster failed" + success_msg: "Generated spec for updating backup target cluster passed" + +- name: Generate spec for creating restore source cluster using check mode + nutanix.ncp.ntnx_pc_restore_source_v2: + location: + cluster_location: + config: + ext_id: "38648904-4859-1258-7485-074816fe2a4f" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for creating restore source cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.location.config.ext_id == "38648904-4859-1258-7485-074816fe2a4f" + fail_msg: "Generated spec for creating restore source cluster failed" + success_msg: "Generated spec for creating restore source cluster passed" + +- name: Generate spec for creating restore source object store using check mode + nutanix.ncp.ntnx_pc_restore_source_v2: + location: + object_store_location: + provider_config: + bucket_name: "test1" + region: "us-east-1" + credentials: + access_key_id: "qwertyuiopasdfgh" + secret_access_key: "jklzxcvbnm" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for creating restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.location.provider_config.bucket_name == "test1" + - result.response.location.provider_config.region == "us-east-1" + - result.response.location.provider_config.credentials.access_key_id == "qwertyuiopasdfgh" + fail_msg: "Generated spec for creating restore source object store failed" + success_msg: "Generated spec for creating restore source object store passed" + +############################################################# +# Check if the backup target cluster exists +# If it exists, delete it + +- name: Check if backup target cluster exists + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: backup_result + ignore_errors: true + +- name: Initialize backup_target_cluster_ext_id + ansible.builtin.set_fact: + backup_target_cluster_ext_id: [] + +- name: Get external ID of the backup target cluster + ansible.builtin.set_fact: + backup_target_cluster_ext_id: >- + {{ backup_result.response + | selectattr('location', 'defined') + | selectattr('location.config', 'defined') + | selectattr('location.config.ext_id', 'equalto', cluster.uuid | default('')) + | map(attribute='ext_id') + | list }} + when: + - backup_result.response is not none + +- name: Delete backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_cluster_ext_id[0] }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + state: absent + register: result + ignore_errors: true + when: + - backup_target_cluster_ext_id | length > 0 + - backup_result.response is not none + +- name: Delete backup target cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Delete backup target cluster failed" + success_msg: "Delete backup target cluster passed" + when: + - backup_target_cluster_ext_id | length > 0 + - backup_result.response is not none + +############################################################ +# Create backup target cluster +# Get backup target cluster +# Check Idempotency by updating backup target cluster with the same values + +- name: Create backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + +- name: Create backup target cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Create backup target cluster failed" + success_msg: "Create backup target cluster passed" + +- name: List all backup targets and set backup target external ID + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: backup_result + ignore_errors: true + +- name: Get backup target cluster status + ansible.builtin.assert: + that: + - backup_result.response is defined + - backup_result.response | length > 0 + fail_msg: "Get backup target cluster failed" + success_msg: "Get backup target cluster passed" + +- name: Get external ID of the backup target cluster + ansible.builtin.set_fact: + backup_target_cluster_ext_id: >- + {{ backup_result.response + | selectattr('location', 'defined') + | selectattr('location.config', 'defined') + | selectattr('location.config.ext_id', 'equalto', cluster.uuid | default('')) + | map(attribute='ext_id') + | list }} + when: + - backup_result.response is not none + +- name: Set backup target cluster external ID + ansible.builtin.set_fact: + backup_target_ext_id: "{{ backup_target_cluster_ext_id[0] }}" + +- name: Check Idempotency by updating backup target cluster with the same values + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + +- name: Check Idempotency by updating backup target cluster with the same values status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.skipped == true + - result.msg == "Nothing to change." + fail_msg: "Check Idempotency by updating backup target cluster with the same values failed" + success_msg: "Check Idempotency by updating backup target cluster with the same values passed" + +############################################################# +# List all backup targets +# Fetch backup target details using external ID + +- name: List all backup targets + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + +- name: List all backup targets status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response | length > 0 + fail_msg: "List all backup targets failed" + success_msg: "List all backup targets passed" + +# Retry until last_sync_time is not none so that restore points are created successfully +- name: Fetch backup target details using external ID until last_sync_time is not none + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + ext_id: "{{ backup_target_ext_id }}" + retries: 120 + delay: 30 + until: result.response.last_sync_time is not none + register: result + ignore_errors: true + +- name: Fetch backup target details using external ID status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response.ext_id == backup_target_ext_id + - result.response.location.config.ext_id == cluster.uuid + - result.response.last_sync_time is not none + fail_msg: "Fetch backup target details using external ID failed" + success_msg: "Fetch backup target details using external ID passed" + +############################################################# +# List all PCs +# Fetch PC details using external ID + +- name: List all PCs + nutanix.ncp.ntnx_pc_config_info_v2: + register: result + ignore_errors: true + +- name: List all PCs status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response | length > 0 + fail_msg: "List all PCs failed" + success_msg: "List all PCs passed" + +- name: Fetch PC details using external ID + nutanix.ncp.ntnx_pc_config_info_v2: + ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + +- name: Fetch PC details using external ID status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response.ext_id == domain_manager_ext_id + fail_msg: "Fetch PC details using external ID failed" + success_msg: "Fetch PC details using external ID passed" + +############################################################# +# List all VMs and get PC VM external ID +# Get PC VM External ID +# List all PCs +# Get PC details + +- name: List all VMs and get PC VM external ID + ntnx_vms_info_v2: + register: result + ignore_errors: true + +- name: Get PC VMs + set_fact: + PC_VMs: >- + {{ + result.response + | selectattr('description', 'equalto', 'NutanixPrismCentral') + }} + +- name: Set filtered VMs + set_fact: + filtered_vm: [] + +- name: Get PC VM to power off + ansible.builtin.set_fact: + filtered_vm: "{{ filtered_vm + [item.0] }}" + loop: "{{ PC_VMs | subelements('nics') }}" + when: item.1.network_info.ipv4_info is defined and item.1.network_info.ipv4_info.learned_ip_addresses is defined and "'{{ ip }}' in item.1.network_info.ipv4_info.learned_ip_addresses | map(attribute='value')" + +- name: Status for Fetching PC VM + ansible.builtin.assert: + that: + - filtered_vm | length > 0 + fail_msg: "Fetching PC VM failed" + success_msg: "Fetching PC VM passed" + +- name: Set PC VM external ID + ansible.builtin.set_fact: + pc_vm_external_id: "{{ filtered_vm[0].ext_id }}" + +- name: List all PCs with filter + nutanix.ncp.ntnx_pc_config_info_v2: + filter: extId eq '{{ domain_manager_ext_id }}' + register: pc_details + ignore_errors: true + +- name: List all PCs with filter status + ansible.builtin.assert: + that: + - pc_details.changed == false + - pc_details.failed == false + - pc_details.response is defined + - pc_details.response | length > 0 + fail_msg: "List all PCs with filter failed" + success_msg: "List all PCs with filter passed" + +############################################################# +# Create restore source cluster +# Delete restore source cluster +# Create restore source cluster +# Get restore source cluster + +- name: Create restore source cluster + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + +- name: Create restore source cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.ext_id is defined + - result.response.ext_id == result.ext_id + - result.response.location.config.ext_id == cluster.uuid + fail_msg: "Create restore source cluster failed" + success_msg: "Create restore source cluster passed" + +- name: Get restore source cluster + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ result.response.ext_id }}" + register: result + ignore_errors: true + +- name: Get restore source cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.ext_id + - result.response.location.config.ext_id == cluster.uuid + fail_msg: "Get restore source cluster failed" + success_msg: "Get restore source cluster passed" + +- name: Set restore source cluster external ID + ansible.builtin.set_fact: + restore_source_ext_id: "{{ result.response.ext_id }}" + +- name: Delete restore source cluster + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_source_ext_id }}" + state: absent + register: result + ignore_errors: true + +- name: Get restore source cluster + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_source_ext_id }}" + register: result + ignore_errors: true + +- name: Verify that restore source cluster is deleted + ansible.builtin.assert: + that: + - result.error == "NOT FOUND" + - result.response.data.error | length > 0 + fail_msg: "Delete restore source cluster failed" + success_msg: "Delete restore source cluster passed" + +- name: Create restore source cluster + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + location: + cluster_location: + config: + ext_id: "{{ cluster.uuid }}" + register: result + ignore_errors: true + +- name: Create restore source cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.ext_id is defined + - result.response.ext_id == result.ext_id + - result.response.location.config.ext_id == cluster.uuid + fail_msg: "Create restore source cluster failed" + success_msg: "Create restore source cluster passed" + +- name: Get restore source cluster + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ result.response.ext_id }}" + register: result + ignore_errors: true + +- name: Get restore source cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.ext_id + - result.response.location.config.ext_id == cluster.uuid + fail_msg: "Get restore source cluster failed" + success_msg: "Get restore source cluster passed" + +- name: Set restore source cluster external ID + ansible.builtin.set_fact: + restore_source_ext_id: "{{ result.response.ext_id }}" + +############################################################# +# Get all restorable domain managers +# Get restorable domain manager external ID +# List all restore points +# Set restore point external ID + +- name: Get all restorable domain managers + nutanix.ncp.ntnx_pc_restorable_domain_managers_info_v2: + nutanix_host: "{{ ip_pe }}" + restore_source_ext_id: "{{ restore_source_ext_id }}" + register: result + ignore_errors: true + +- name: Get restorable domain manager external ID + set_fact: + domain_manager_details: "{{ result.response | selectattr('ext_id', 'equalto', domain_manager_ext_id) | list }}" + +- name: List all restore points + nutanix.ncp.ntnx_pc_restore_points_info_v2: + nutanix_host: "{{ ip_pe }}" + restore_source_ext_id: "{{ restore_source_ext_id }}" + restorable_domain_manager_ext_id: "{{ domain_manager_details[0].ext_id }}" + register: result + ignore_errors: true + +- name: Set restore point external ID + set_fact: + restore_point_ext_id: "{{ result.response[0].ext_id }}" + +############################################################# +# Power off PC VM + +- name: Power off PC VM + ntnx_vms_power_actions_v2: + state: power_off + ext_id: "{{ pc_vm_external_id }}" + register: result + ignore_errors: true + +- name: Sleep for 2 minutes after powering off VM + ansible.builtin.pause: + seconds: 120 + +############################################################# +# Restore PC + +- name: Generate spec for restoring PC using check mode + nutanix.ncp.ntnx_pc_restore_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "35d22fcc-0084-3751-a579-0621ce59a786" + restore_source_ext_id: "0a77819c-2e35-446b-87b1-89cbe62c15f5" + restorable_domain_manager_ext_id: "18553f0f-7b41-4115-bf42-2f698fbe7117" + domain_manager: + config: + should_enable_lockdown_mode: false + build_info: + version: "{{ pc_details.response[0].config.build_info.version }}" + name: "{{ pc_details.response[0].config.name }}" + size: "{{ pc_details.response[0].config.size }}" + resource_config: + data_disk_size_bytes: "{{ pc_details.response[0].config.resource_config.data_disk_size_bytes }}" + memory_size_bytes: "{{ pc_details.response[0].config.resource_config.memory_size_bytes }}" + num_vcpus: "{{ pc_details.response[0].config.resource_config.num_vcpus }}" + container_ext_ids: "{{ pc_details.response[0].config.resource_config.container_ext_ids }}" + network: + external_address: + ipv4: + value: "{{ pc_details.response[0].network.external_address.ipv4.value }}" + name_servers: + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[0].ipv4.value }}" + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[1].ipv4.value }}" + ntp_servers: + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[0].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[1].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[2].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[3].fqdn.value }}" + external_networks: + - network_ext_id: "{{ pc_details.response[0].network.external_networks[0].network_ext_id }}" + default_gateway: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value }}" + subnet_mask: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value }}" + ip_ranges: + - begin: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value }}" + end: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value }}" + register: result + ignore_errors: true + check_mode: true + +- name: Generate spec for restoring PC using check mode status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.domain_manager.config.name == pc_details.response[0].config.name + - result.response.domain_manager.config.size == pc_details.response[0].config.size + - result.response.domain_manager.config.resource_config.container_ext_ids == pc_details.response[0].config.resource_config.container_ext_ids + - result.response.domain_manager.config.resource_config.data_disk_size_bytes == pc_details.response[0].config.resource_config.data_disk_size_bytes + - result.response.domain_manager.config.resource_config.memory_size_bytes == pc_details.response[0].config.resource_config.memory_size_bytes + - result.response.domain_manager.config.resource_config.num_vcpus == pc_details.response[0].config.resource_config.num_vcpus + - result.response.domain_manager.network.external_address.ipv4.value == pc_details.response[0].network.external_address.ipv4.value + - result.response.domain_manager.network.name_servers[0].ipv4.value == pc_details.response[0].network.name_servers[0].ipv4.value + - result.response.domain_manager.network.name_servers[1].ipv4.value == pc_details.response[0].network.name_servers[1].ipv4.value + - result.response.domain_manager.network.ntp_servers[0].fqdn.value == pc_details.response[0].network.ntp_servers[0].fqdn.value + - result.response.domain_manager.network.ntp_servers[1].fqdn.value == pc_details.response[0].network.ntp_servers[1].fqdn.value + - result.response.domain_manager.network.ntp_servers[2].fqdn.value == pc_details.response[0].network.ntp_servers[2].fqdn.value + - result.response.domain_manager.network.ntp_servers[3].fqdn.value == pc_details.response[0].network.ntp_servers[3].fqdn.value + - result.response.domain_manager.network.external_networks[0].network_ext_id == pc_details.response[0].network.external_networks[0].network_ext_id + - result.response.domain_manager.network.external_networks[0].default_gateway.ipv4.value == pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value + - result.response.domain_manager.network.external_networks[0].subnet_mask.ipv4.value == pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value + - result.response.domain_manager.network.external_networks[0].ip_ranges[0].begin.ipv4.value == pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value + - result.response.domain_manager.network.external_networks[0].ip_ranges[0].end.ipv4.value == pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value + - result.restore_source_ext_id == "0a77819c-2e35-446b-87b1-89cbe62c15f5" + - result.restorable_domain_manager_ext_id == "18553f0f-7b41-4115-bf42-2f698fbe7117" + - result.ext_id == "35d22fcc-0084-3751-a579-0621ce59a786" + fail_msg: "Generated spec for restoring PC using check mode failed" + success_msg: "Generated spec for restoring PC using check mode passed" + +- name: Restore PC + nutanix.ncp.ntnx_pc_restore_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_point_ext_id }}" + restore_source_ext_id: "{{ restore_source_ext_id }}" + restorable_domain_manager_ext_id: "{{ domain_manager_details[0].ext_id }}" + domain_manager: + config: + should_enable_lockdown_mode: false + build_info: + version: "{{ pc_details.response[0].config.build_info.version }}" + name: "{{ pc_details.response[0].config.name }}" + size: "{{ pc_details.response[0].config.size }}" + resource_config: + data_disk_size_bytes: "{{ pc_details.response[0].config.resource_config.data_disk_size_bytes }}" + memory_size_bytes: "{{ pc_details.response[0].config.resource_config.memory_size_bytes }}" + num_vcpus: "{{ pc_details.response[0].config.resource_config.num_vcpus }}" + container_ext_ids: "{{ pc_details.response[0].config.resource_config.container_ext_ids }}" + network: + external_address: + ipv4: + value: "{{ pc_details.response[0].network.external_address.ipv4.value }}" + name_servers: + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[0].ipv4.value }}" + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[1].ipv4.value }}" + ntp_servers: + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[0].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[1].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[2].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[3].fqdn.value }}" + external_networks: + - network_ext_id: "{{ pc_details.response[0].network.external_networks[0].network_ext_id }}" + default_gateway: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value }}" + subnet_mask: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value }}" + ip_ranges: + - begin: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value }}" + end: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value }}" + register: result + ignore_errors: true + +- name: Restore PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Restore PC failed" + success_msg: "Restore PC passed" + +############################################################# +# Reset password after restore PC + +- name: Set password variables + ansible.builtin.set_fact: + password_count: 5 + password_length: 12 + special_characters: "@#" + passwords: [] + +- name: Generate passwords + ansible.builtin.set_fact: + passwords: >- + {{ passwords + [ + '.N.'.join( + (lookup('password', '/dev/null length=' + (password_length | int) | string + + ' chars=ascii_letters+digits+' + special_characters) | list) + + (lookup('password', '/dev/null length=1 chars=ascii_lowercase') | list) + + (lookup('password', '/dev/null length=1 chars=ascii_uppercase') | list) + + (lookup('password', '/dev/null length=1 chars=digits') | list) + + (lookup('password', '/dev/null length=1 chars=' + special_characters) | list) + | shuffle) + ] }} + with_sequence: count={{ password_count }} + +- name: Set variables for reset password + ansible.builtin.set_fact: + pc_ssh_cmd: sshpass -p '{{ domain_manager_ssh_password }}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {{ domain_manager_ssh_username }}@{{ ip }} + reset_username_password: /home/nutanix/prism/cli/ncli user reset-password user-name={{ username }} password={{ password }} + +- name: Set reset command + ansible.builtin.set_fact: + reset_command: '{{ pc_ssh_cmd }} "{{ reset_username_password }}"' + +- name: Change password five times randomly before resetting + ansible.builtin.command: "{{ pc_ssh_cmd }} /home/nutanix/prism/cli/ncli user reset-password user-name={{ username }} password='{{ item }}'" + register: result + ignore_errors: true + loop: "{{ passwords }}" + changed_when: result.rc == 0 + +- name: Change password five times randomly before resetting status + ansible.builtin.assert: + that: + - result.msg == "All items completed" + +- name: Reset username and password + ansible.builtin.command: "{{ reset_command }}" + register: result + ignore_errors: true + changed_when: result.rc != 0 + +- name: Reset username and password status + ansible.builtin.assert: + that: + - "'reset successfully' in result.stdout" + +############################################################# +# Delete backup target cluster + +- name: Delete backup target cluster + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + state: absent + register: result + ignore_errors: true + +- name: Delete backup target cluster status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Delete backup target cluster failed" + success_msg: "Delete backup target cluster passed" diff --git a/tests/integration/targets/ntnx_prism_v2/tasks/deploy_pc.yml b/tests/integration/targets/ntnx_prism_v2/tasks/deploy_pc.yml new file mode 100644 index 000000000..546d5cb45 --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/tasks/deploy_pc.yml @@ -0,0 +1,179 @@ +--- +- name: Start ntnx_pc_deploy_v2 tests + ansible.builtin.debug: + msg: Start ntnx_pc_deploy_v2 tests + +- name: Generate random strings + ansible.builtin.set_fact: + random_string: "{{ query('community.general.random_string', numbers=false, special=false, length=12) }}" + +# Generate spec using check mode for: +# - Deploying a PC + +- name: Generate spec for deploying a PC using check mode + nutanix.ncp.ntnx_pc_deploy_v2: + config: + name: "test" + size: "SMALL" + build_info: + version: "pc.2024.3" + resource_config: + container_ext_ids: + - "container-1" + should_enable_lockdown_mode: true + network: + external_address: + ipv4: + value: "10.0.0.1" + name_servers: + - ipv4: + value: "10.0.0.2" + - ipv4: + value: "10.0.0.3" + ntp_servers: + - ipv4: + value: "10.0.0.4" + - ipv4: + value: "10.0.0.5" + internal_networks: + - default_gateway: + ipv4: + value: "10.0.0.6" + subnet_mask: + ipv4: + value: "10.0.0.7" + ip_ranges: + - begin: + ipv4: + value: "10.0.0.8" + end: + ipv4: + value: "10.0.0.9" + - default_gateway: + ipv4: + value: "10.0.0.10" + subnet_mask: + ipv4: + value: "10.0.0.11" + ip_ranges: + - begin: + ipv4: + value: "10.0.0.12" + end: + ipv4: + value: "10.0.0.13" + external_networks: + - default_gateway: + ipv4: + value: "10.0.1.0" + subnet_mask: + ipv4: + value: "10.0.2.0" + ip_ranges: + - begin: + ipv4: + value: "10.0.3.0" + end: + ipv4: + value: "10.0.4.0" + network_ext_id: "16f59216-1234-3333-2222-074816fe2a4f" + - default_gateway: + ipv4: + value: "10.0.5.0" + subnet_mask: + ipv4: + value: "10.0.6.0" + ip_ranges: + - begin: + ipv4: + value: "10.0.7.0" + end: + ipv4: + value: "10.0.8.0" + network_ext_id: "16f59216-a071-41b7-aee3-074816fe2a4f" + should_enable_high_availability: true + delegate_to: localhost + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for deploying a PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.config.name == "test" + - result.response.config.size == "SMALL" + - result.response.config.build_info.version == "pc.2024.3" + - result.response.config.resource_config.container_ext_ids == ["container-1"] + - result.response.config.should_enable_lockdown_mode == true + - result.response.network.external_address.ipv4.value == "10.0.0.1" + - result.response.network.name_servers[0].ipv4.value == "10.0.0.2" + - result.response.network.name_servers[1].ipv4.value == "10.0.0.3" + - result.response.network.ntp_servers[0].ipv4.value == "10.0.0.4" + - result.response.network.ntp_servers[1].ipv4.value == "10.0.0.5" + - result.response.network.internal_networks[0].default_gateway.ipv4.value == "10.0.0.6" + - result.response.network.internal_networks[0].subnet_mask.ipv4.value == "10.0.0.7" + - result.response.network.internal_networks[0].ip_ranges[0].begin.ipv4.value == "10.0.0.8" + - result.response.network.internal_networks[0].ip_ranges[0].end.ipv4.value == "10.0.0.9" + - result.response.network.internal_networks[1].default_gateway.ipv4.value == "10.0.0.10" + - result.response.network.internal_networks[1].subnet_mask.ipv4.value == "10.0.0.11" + - result.response.network.internal_networks[1].ip_ranges[0].begin.ipv4.value == "10.0.0.12" + - result.response.network.internal_networks[1].ip_ranges[0].end.ipv4.value == "10.0.0.13" + - result.response.network.external_networks[0].default_gateway.ipv4.value == "10.0.1.0" + - result.response.network.external_networks[0].subnet_mask.ipv4.value == "10.0.2.0" + - result.response.network.external_networks[0].ip_ranges[0].begin.ipv4.value == "10.0.3.0" + - result.response.network.external_networks[0].ip_ranges[0].end.ipv4.value == "10.0.4.0" + - result.response.network.external_networks[0].network_ext_id == "16f59216-1234-3333-2222-074816fe2a4f" + - result.response.network.external_networks[1].default_gateway.ipv4.value == "10.0.5.0" + - result.response.network.external_networks[1].subnet_mask.ipv4.value == "10.0.6.0" + - result.response.network.external_networks[1].ip_ranges[0].begin.ipv4.value == "10.0.7.0" + - result.response.network.external_networks[1].ip_ranges[0].end.ipv4.value == "10.0.8.0" + - result.response.network.external_networks[1].network_ext_id == "16f59216-a071-41b7-aee3-074816fe2a4f" + - result.response.should_enable_high_availability == true + fail_msg: "Generated spec for deploying a PC failed" + success_msg: "Generated spec for deploying a PC passed" + +- name: Deploy PC + nutanix.ncp.ntnx_pc_deploy_v2: + config: + name: "{{ random_string }}_pc" + size: "{{ pc.size }}" + build_info: + version: "{{ pc.build_info.version }}" + network: + external_networks: + - default_gateway: + ipv4: + value: "{{ pc.external_networks.default_gateway.ipv4 }}" + subnet_mask: + ipv4: + value: "{{ pc.external_networks.subnet_mask.ipv4 }}" + ip_ranges: + - begin: + ipv4: + value: "{{ pc.external_networks.ip_ranges.begin.ipv4 }}" + end: + ipv4: + value: "{{ pc.external_networks.ip_ranges.end.ipv4 }}" + network_ext_id: "{{ pc.external_networks.network_ext_id }}" + name_servers: + - ipv4: + value: "{{ pc.name_servers.ipv4[0] }}" + - ipv4: + value: "{{ pc.name_servers.ipv4[1] }}" + ntp_servers: + - fqdn: + value: "{{ pc.ntp_servers.fqdn[0] }}" + - fqdn: + value: "{{ pc.ntp_servers.fqdn[1] }}" + register: result + ignore_errors: true + +- name: Deploy PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.task_ext_id + - result.response.status == "SUCCEEDED" + fail_msg: "Deploy PC failed" + success_msg: "Deploy PC passed" diff --git a/tests/integration/targets/ntnx_prism_v2/tasks/main.yml b/tests/integration/targets/ntnx_prism_v2/tasks/main.yml new file mode 100644 index 000000000..4c6f328ca --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Set module defaults + module_defaults: + group/nutanix.ncp.ntnx: + nutanix_host: "{{ ip }}" + nutanix_username: "{{ username }}" + nutanix_password: "{{ password }}" + validate_certs: "{{ validate_certs }}" + block: + - name: Import cluster_location.yml + ansible.builtin.import_tasks: "cluster_location.yml" + - name: Import object_store.yml + ansible.builtin.import_tasks: "object_store.yml" + - name: Import deploy_pc.yml + ansible.builtin.import_tasks: "deploy_pc.yml" + - name: Import unregister_pcs.yml + ansible.builtin.import_tasks: "unregister_pcs.yml" diff --git a/tests/integration/targets/ntnx_prism_v2/tasks/object_store.yml b/tests/integration/targets/ntnx_prism_v2/tasks/object_store.yml new file mode 100644 index 000000000..ea9726746 --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/tasks/object_store.yml @@ -0,0 +1,835 @@ +--- +- name: Start ntnx_prism_v2 tests + ansible.builtin.debug: + msg: Start ntnx_prism_v2 tests + +############################################################# +# List all clusters to get prism central external ID + +- name: List all clusters to get prism central external ID + nutanix.ncp.ntnx_clusters_info_v2: + filter: "config/clusterFunction/any(t:t eq Clustermgmt.Config.ClusterFunctionRef'PRISM_CENTRAL')" + register: result + ignore_errors: true + +- name: Get prism central external ID + ansible.builtin.set_fact: + domain_manager_ext_id: "{{ result.response[0].ext_id }}" + +############################################################# +# Generate spec using check mode for: +# - Creating backup target object store +# - Updating backup target object store +# - Creating restore source object store +# - Updating restore source object store + +- name: Generate spec for creating backup target object store using check mode + nutanix.ncp.ntnx_pc_backup_target_v2: + domain_manager_ext_id: "96325874-8523-9865-1478-074816fe2a4f" + location: + object_store_location: + provider_config: + bucket_name: "test1" + region: "us-east-1" + credentials: + access_key_id: "qwertyuiopasdfgh" + secret_access_key: "jklzxcvbnm" + backup_policy: + rpo_in_minutes: 60 + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for creating backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.location.provider_config.bucket_name == "test1" + - result.response.location.provider_config.region == "us-east-1" + - result.response.location.provider_config.credentials.access_key_id == "qwertyuiopasdfgh" + - result.response.location.backup_policy.rpo_in_minutes == 60 + fail_msg: "Generated spec for creating backup target object store failed" + success_msg: "Generated spec for creating backup target object store passed" + +- name: Generate spec for updating backup target object store using check mode + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "84785699-8744-2895-9632-074816fe2a4f" + domain_manager_ext_id: "14855555-9999-1235-3141-074816fe2a4f" + location: + object_store_location: + provider_config: + bucket_name: "test2" + region: "us-east-2" + credentials: + access_key_id: "qwertyuiopasdfgh" + secret_access_key: "jklzxcvbnm" + backup_policy: + rpo_in_minutes: 120 + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for updating backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == "84785699-8744-2895-9632-074816fe2a4f" + - result.response.location.provider_config.bucket_name == "test2" + - result.response.location.provider_config.region == "us-east-2" + - result.response.location.provider_config.credentials.access_key_id == "qwertyuiopasdfgh" + - result.response.location.backup_policy.rpo_in_minutes == 120 + fail_msg: "Generated spec for updating backup target object store failed" + success_msg: "Generated spec for updating backup target object store passed" + +- name: Generate spec for creating restore source object store using check mode + nutanix.ncp.ntnx_pc_restore_source_v2: + location: + object_store_location: + provider_config: + bucket_name: "test1" + region: "us-east-1" + credentials: + access_key_id: "qwertyuiopasdfgh" + secret_access_key: "jklzxcvbnm" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for creating restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.location.provider_config.bucket_name == "test1" + - result.response.location.provider_config.region == "us-east-1" + - result.response.location.provider_config.credentials.access_key_id == "qwertyuiopasdfgh" + fail_msg: "Generated spec for creating restore source object store failed" + success_msg: "Generated spec for creating restore source object store passed" + +- name: Generate spec for updating restore source object store using check mode + nutanix.ncp.ntnx_pc_restore_source_v2: + ext_id: "84785699-8744-2895-9632-074816fe2a4f" + location: + object_store_location: + provider_config: + bucket_name: "test2" + region: "us-east-2" + credentials: + access_key_id: "qwertyuiopasdfgh" + secret_access_key: "jklzxcvbnm" + check_mode: true + register: result + ignore_errors: true + +- name: Generate spec for updating restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == "84785699-8744-2895-9632-074816fe2a4f" + - result.response.location.provider_config.bucket_name == "test2" + - result.response.location.provider_config.region == "us-east-2" + - result.response.location.provider_config.credentials.access_key_id == "qwertyuiopasdfgh" + fail_msg: "Generated spec for updating restore source object store failed" + success_msg: "Generated spec for updating restore source object store passed" + +############################################################# +# Check if the backup target object store exists +# If it exists, delete it + +- name: Check if backup target object store exists + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: backup_result + ignore_errors: true + +- name: Initialize backup_target_object_store_ext_id + ansible.builtin.set_fact: + backup_target_object_store_ext_id: [] + +- name: Get external ID of the backup target object store + ansible.builtin.set_fact: + backup_target_object_store_ext_id: >- + {{ backup_result.response + | selectattr('location', 'defined') + | selectattr('location.provider_config', 'defined') + | selectattr('location.provider_config.bucket_name', 'defined') + | map(attribute='ext_id') + | list }} + when: + - backup_result.response is not none + +- name: Delete backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_object_store_ext_id[0] }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + state: absent + register: result + ignore_errors: true + when: + - backup_target_object_store_ext_id | length > 0 + - backup_result.response is not none + +- name: Delete backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Delete backup target object store failed" + success_msg: "Delete backup target object store passed" + when: + - backup_target_object_store_ext_id | length > 0 + - backup_result.response is not none + +############################################################# +# Create backup target object store +# Get backup target object store +# Update backup target object store +# Get backup target object store +# Check Idempotency for updating backup target object store + +- name: Create backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + object_store_location: + provider_config: + bucket_name: "{{ s3_bucket.bucket }}" + region: "{{ s3_bucket.region }}" + credentials: + access_key_id: "{{ s3_bucket.access_key }}" + secret_access_key: "{{ s3_bucket.secret_key }}" + backup_policy: + rpo_in_minutes: 60 + register: result + ignore_errors: true + +- name: Create backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Create backup target object store failed" + success_msg: "Create backup target object store passed" + +- name: List all backup targets and set backup target object store external ID + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: backup_result + ignore_errors: true + +- name: Get backup target object store status + ansible.builtin.assert: + that: + - backup_result.response is defined + - backup_result.response | length > 0 + fail_msg: "Get backup target object store failed" + success_msg: "Get backup target object store passed" + +- name: Get external ID of the backup target object store + ansible.builtin.set_fact: + backup_target_object_store_ext_id: >- + {{ backup_result.response + | selectattr('location', 'defined') + | selectattr('location.provider_config', 'defined') + | selectattr('location.provider_config.bucket_name', 'defined') + | map(attribute='ext_id') + | list }} + when: + - backup_result.response is not none + +- name: Set backup target object store external ID + ansible.builtin.set_fact: + backup_target_object_store_ext_id: "{{ backup_target_object_store_ext_id[0] }}" + +- name: Update backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_object_store_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + object_store_location: + provider_config: + bucket_name: "{{ s3_bucket.bucket }}" + region: "{{ s3_bucket.region }}" + credentials: + access_key_id: "{{ s3_bucket.access_key }}" + secret_access_key: "{{ s3_bucket.secret_key }}" + backup_policy: + rpo_in_minutes: 120 + register: result + ignore_errors: true + +- name: Update backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Update backup target object store failed" + success_msg: "Update backup target object store passed" + +- name: Get backup target object store + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + ext_id: "{{ backup_target_object_store_ext_id }}" + register: result + ignore_errors: true + +- name: Get backup target object store status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.ext_id == backup_target_object_store_ext_id + - result.response is defined + - result.response.ext_id == backup_target_object_store_ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + - result.response.location.backup_policy.rpo_in_minutes == 120 + fail_msg: "Get backup target object store failed" + success_msg: "Get backup target object store passed" + +- name: Check Idempotency for updating backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_object_store_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + location: + object_store_location: + provider_config: + bucket_name: "{{ s3_bucket.bucket }}" + region: "{{ s3_bucket.region }}" + backup_policy: + rpo_in_minutes: 120 + register: result + ignore_errors: true + +- name: Check Idempotency for updating backup target object store status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.skipped == true + - result.msg == "Nothing to change." + fail_msg: "Check Idempotency for updating backup target object store failed" + success_msg: "Check Idempotency for updating backup target object store passed" + +############################################################# +# List all backup targets +# Fetch backup target details using external ID + +- name: List all backup targets + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + +- name: List all backup targets status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response | length > 0 + fail_msg: "List all backup targets failed" + success_msg: "List all backup targets passed" + +# Retry until last_sync_time is not none so that restore points are created successfully +- name: Fetch backup target details using external ID until last_sync_time is not none + nutanix.ncp.ntnx_pc_backup_target_info_v2: + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + ext_id: "{{ backup_target_object_store_ext_id }}" + retries: 120 + delay: 30 + until: result.response.last_sync_time is not none + register: result + ignore_errors: true + +- name: Fetch backup target details using external ID status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response.ext_id == backup_target_object_store_ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + - result.response.location.backup_policy.rpo_in_minutes == 120 + - result.response.last_sync_time is not none + fail_msg: "Fetch backup target details using external ID failed" + success_msg: "Fetch backup target details using external ID passed" + +############################################################# +# List all PCs +# Fetch PC details using external ID + +- name: List all PCs + nutanix.ncp.ntnx_pc_config_info_v2: + register: result + ignore_errors: true + +- name: List all PCs status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response | length > 0 + fail_msg: "List all PCs failed" + success_msg: "List all PCs passed" + +- name: Fetch PC details using external ID + nutanix.ncp.ntnx_pc_config_info_v2: + ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + +- name: Fetch PC details using external ID status + ansible.builtin.assert: + that: + - result.changed == false + - result.failed == false + - result.response is defined + - result.response.ext_id == domain_manager_ext_id + fail_msg: "Fetch PC details using external ID failed" + success_msg: "Fetch PC details using external ID passed" + +############################################################# +# List all VMs and get PC VM external ID +# Get PC VM External ID +# List all PCs +# Get PC details + +- name: List all VMs and get PC VM external ID + ntnx_vms_info_v2: + register: result + ignore_errors: true + +- name: Get PC VMs + set_fact: + PC_VMs: >- + {{ + result.response + | selectattr('description', 'equalto', 'NutanixPrismCentral') + }} + +- name: Set filtered VMs + set_fact: + filtered_vm: [] + +- name: Get PC VM to power off + ansible.builtin.set_fact: + filtered_vm: "{{ filtered_vm + [item.0] }}" + loop: "{{ PC_VMs | subelements('nics') }}" + when: item.1.network_info.ipv4_info is defined and item.1.network_info.ipv4_info.learned_ip_addresses is defined and "'{{ ip }}' in item.1.network_info.ipv4_info.learned_ip_addresses | map(attribute='value')" + +- name: Status for Fetching PC VM + ansible.builtin.assert: + that: + - filtered_vm | length == 1 + fail_msg: "Fetching PC VM failed" + success_msg: "Fetching PC VM passed" + +- name: Set PC VM external ID + ansible.builtin.set_fact: + pc_vm_external_id: "{{ filtered_vm[0].ext_id }}" + +- name: List all PCs + nutanix.ncp.ntnx_pc_config_info_v2: + filter: extId eq '{{ domain_manager_ext_id }}' + register: pc_details + ignore_errors: true + +- name: List all PCs status + ansible.builtin.assert: + that: + - pc_details.changed == false + - pc_details.failed == false + - pc_details.response is defined + - pc_details.response | length > 0 + fail_msg: "List all PCs failed" + success_msg: "List all PCs passed" + +############################################################ +# Create restore source object store +# Delete restore source object store +# Create restore source object store +# Get restore source object store + +- name: Create restore source object store + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + location: + object_store_location: + provider_config: + bucket_name: "{{ s3_bucket.bucket }}" + region: "{{ s3_bucket.region }}" + credentials: + access_key_id: "{{ s3_bucket.access_key }}" + secret_access_key: "{{ s3_bucket.secret_key }}" + register: result + ignore_errors: true + +- name: Create restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.ext_id is defined + - result.response.ext_id == result.ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + fail_msg: "Create restore source object store failed" + success_msg: "Create restore source object store passed" + +- name: Get restore source object store + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ result.response.ext_id }}" + register: result + ignore_errors: true + +- name: Get restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + fail_msg: "Get restore source object store failed" + success_msg: "Get restore source object store passed" + +- name: Set restore source object store external ID + ansible.builtin.set_fact: + restore_source_object_store_ext_id: "{{ result.response.ext_id }}" + +- name: Delete restore source object store + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_source_object_store_ext_id }}" + state: absent + register: result + ignore_errors: true + +- name: Get restore source object store + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_source_object_store_ext_id }}" + register: result + ignore_errors: true + +- name: Verify that restore source object store is deleted + ansible.builtin.assert: + that: + - result.error == "NOT FOUND" + - result.response.data.error | length > 0 + fail_msg: "Delete restore source object store failed" + success_msg: "Delete restore source object store passed" + +- name: Create restore source object store + nutanix.ncp.ntnx_pc_restore_source_v2: + nutanix_host: "{{ ip_pe }}" + location: + object_store_location: + provider_config: + bucket_name: "{{ s3_bucket.bucket }}" + region: "{{ s3_bucket.region }}" + credentials: + access_key_id: "{{ s3_bucket.access_key }}" + secret_access_key: "{{ s3_bucket.secret_key }}" + register: result + ignore_errors: true + +- name: Create restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.ext_id is defined + - result.response.ext_id == result.ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + fail_msg: "Create restore source object store failed" + success_msg: "Create restore source object store passed" + +- name: Get restore source object store + nutanix.ncp.ntnx_pc_restore_source_info_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ result.response.ext_id }}" + register: result + ignore_errors: true + +- name: Get restore source object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.ext_id + - result.response.location.provider_config.bucket_name == s3_bucket.bucket + - result.response.location.provider_config.region == s3_bucket.region + fail_msg: "Get restore source object store failed" + success_msg: "Get restore source object store passed" + +- name: Set restore source object store external ID + ansible.builtin.set_fact: + restore_source_object_store_ext_id: "{{ result.response.ext_id }}" + +############################################################# +# Get all restorable domain managers +# Get restorable domain manager external ID +# List all restore points +# Set restore point external ID + +- name: Get all restorable domain managers + nutanix.ncp.ntnx_pc_restorable_domain_managers_info_v2: + nutanix_host: "{{ ip_pe }}" + restore_source_ext_id: "{{ restore_source_object_store_ext_id }}" + register: result + ignore_errors: true + +- name: Get restorable domain manager external ID + set_fact: + domain_manager_details: "{{ result.response | selectattr('ext_id', 'equalto', domain_manager_ext_id) | list }}" + +- name: List all restore points + nutanix.ncp.ntnx_pc_restore_points_info_v2: + nutanix_host: "{{ ip_pe }}" + restore_source_ext_id: "{{ restore_source_object_store_ext_id }}" + restorable_domain_manager_ext_id: "{{ domain_manager_details[0].ext_id }}" + register: result + ignore_errors: true + +- name: Set restore point external ID + set_fact: + restore_point_ext_id: "{{ result.response[0].ext_id }}" + +############################################################# +# Power off PC VM + +- name: Power off PC VM + ntnx_vms_power_actions_v2: + state: power_off + ext_id: "{{ pc_vm_external_id }}" + register: result + ignore_errors: true + +- name: Sleep for 2 minutes after powering off VM + ansible.builtin.pause: + seconds: 120 + +# ############################################################# +# Restore PC + +- name: Generate spec for restoring PC using check mode + nutanix.ncp.ntnx_pc_restore_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "35d22fcc-0084-3751-a579-0621ce59a786" + restore_source_ext_id: "0a77819c-2e35-446b-87b1-89cbe62c15f5" + restorable_domain_manager_ext_id: "18553f0f-7b41-4115-bf42-2f698fbe7117" + domain_manager: + config: + should_enable_lockdown_mode: false + build_info: + version: "{{ pc_details.response[0].config.build_info.version }}" + name: "{{ pc_details.response[0].config.name }}" + size: "{{ pc_details.response[0].config.size }}" + resource_config: + data_disk_size_bytes: "{{ pc_details.response[0].config.resource_config.data_disk_size_bytes }}" + memory_size_bytes: "{{ pc_details.response[0].config.resource_config.memory_size_bytes }}" + num_vcpus: "{{ pc_details.response[0].config.resource_config.num_vcpus }}" + container_ext_ids: "{{ pc_details.response[0].config.resource_config.container_ext_ids }}" + network: + external_address: + ipv4: + value: "{{ pc_details.response[0].network.external_address.ipv4.value }}" + name_servers: + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[0].ipv4.value }}" + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[1].ipv4.value }}" + ntp_servers: + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[0].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[1].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[2].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[3].fqdn.value }}" + external_networks: + - network_ext_id: "{{ pc_details.response[0].network.external_networks[0].network_ext_id }}" + default_gateway: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value }}" + subnet_mask: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value }}" + ip_ranges: + - begin: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value }}" + end: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value }}" + register: result + ignore_errors: true + check_mode: true + +- name: Generate spec for restoring PC using check mode status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.domain_manager.config.name == pc_details.response[0].config.name + - result.response.domain_manager.config.size == pc_details.response[0].config.size + - result.response.domain_manager.config.resource_config.container_ext_ids == pc_details.response[0].config.resource_config.container_ext_ids + - result.response.domain_manager.config.resource_config.data_disk_size_bytes == pc_details.response[0].config.resource_config.data_disk_size_bytes + - result.response.domain_manager.config.resource_config.memory_size_bytes == pc_details.response[0].config.resource_config.memory_size_bytes + - result.response.domain_manager.config.resource_config.num_vcpus == pc_details.response[0].config.resource_config.num_vcpus + - result.response.domain_manager.network.external_address.ipv4.value == pc_details.response[0].network.external_address.ipv4.value + - result.response.domain_manager.network.name_servers[0].ipv4.value == pc_details.response[0].network.name_servers[0].ipv4.value + - result.response.domain_manager.network.name_servers[1].ipv4.value == pc_details.response[0].network.name_servers[1].ipv4.value + - result.response.domain_manager.network.ntp_servers[0].fqdn.value == pc_details.response[0].network.ntp_servers[0].fqdn.value + - result.response.domain_manager.network.ntp_servers[1].fqdn.value == pc_details.response[0].network.ntp_servers[1].fqdn.value + - result.response.domain_manager.network.ntp_servers[2].fqdn.value == pc_details.response[0].network.ntp_servers[2].fqdn.value + - result.response.domain_manager.network.ntp_servers[3].fqdn.value == pc_details.response[0].network.ntp_servers[3].fqdn.value + - result.response.domain_manager.network.external_networks[0].network_ext_id == pc_details.response[0].network.external_networks[0].network_ext_id + - result.response.domain_manager.network.external_networks[0].default_gateway.ipv4.value == pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value + - result.response.domain_manager.network.external_networks[0].subnet_mask.ipv4.value == pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value + - result.response.domain_manager.network.external_networks[0].ip_ranges[0].begin.ipv4.value == pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value + - result.response.domain_manager.network.external_networks[0].ip_ranges[0].end.ipv4.value == pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value + - result.restore_source_ext_id == "0a77819c-2e35-446b-87b1-89cbe62c15f5" + - result.restorable_domain_manager_ext_id == "18553f0f-7b41-4115-bf42-2f698fbe7117" + - result.ext_id == "35d22fcc-0084-3751-a579-0621ce59a786" + fail_msg: "Generated spec for restoring PC using check mode failed" + success_msg: "Generated spec for restoring PC using check mode passed" + +- name: Restore PC + nutanix.ncp.ntnx_pc_restore_v2: + nutanix_host: "{{ ip_pe }}" + ext_id: "{{ restore_point_ext_id }}" + restore_source_ext_id: "{{ restore_source_object_store_ext_id }}" + restorable_domain_manager_ext_id: "{{ domain_manager_details[0].ext_id }}" + domain_manager: + config: + should_enable_lockdown_mode: false + build_info: + version: "{{ pc_details.response[0].config.build_info.version }}" + name: "{{ pc_details.response[0].config.name }}" + size: "{{ pc_details.response[0].config.size }}" + resource_config: + data_disk_size_bytes: "{{ pc_details.response[0].config.resource_config.data_disk_size_bytes }}" + memory_size_bytes: "{{ pc_details.response[0].config.resource_config.memory_size_bytes }}" + num_vcpus: "{{ pc_details.response[0].config.resource_config.num_vcpus }}" + container_ext_ids: "{{ pc_details.response[0].config.resource_config.container_ext_ids }}" + network: + external_address: + ipv4: + value: "{{ pc_details.response[0].network.external_address.ipv4.value }}" + name_servers: + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[0].ipv4.value }}" + - ipv4: + value: "{{ pc_details.response[0].network.name_servers[1].ipv4.value }}" + ntp_servers: + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[0].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[1].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[2].fqdn.value }}" + - fqdn: + value: "{{ pc_details.response[0].network.ntp_servers[3].fqdn.value }}" + external_networks: + - network_ext_id: "{{ pc_details.response[0].network.external_networks[0].network_ext_id }}" + default_gateway: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].default_gateway.ipv4.value }}" + subnet_mask: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].subnet_mask.ipv4.value }}" + ip_ranges: + - begin: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].begin.ipv4.value }}" + end: + ipv4: + value: "{{ pc_details.response[0].network.external_networks[0].ip_ranges[0].end.ipv4.value }}" + register: result + ignore_errors: true + +- name: Restore PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Restore PC failed" + success_msg: "Restore PC passed" + +############################################################# +# Reset password after restore PC + +- name: Set password variables + ansible.builtin.set_fact: + password_count: 5 + password_length: 5 + special_characters: "@#$" + passwords: [] + +- name: Generate passwords + ansible.builtin.set_fact: + passwords: >- + {{ passwords + [ + '.N.'.join( + (lookup('password', '/dev/null length=' + (password_length | int) | string + + ' chars=ascii_letters+digits+' + special_characters) | list) + + (lookup('password', '/dev/null length=1 chars=ascii_lowercase') | list) + + (lookup('password', '/dev/null length=1 chars=ascii_uppercase') | list) + + (lookup('password', '/dev/null length=1 chars=digits') | list) + + (lookup('password', '/dev/null length=1 chars=' + special_characters) | list) + | shuffle) + ] }} + with_sequence: count={{ password_count }} + +- name: Set variables for reset password + ansible.builtin.set_fact: + pc_ssh_cmd: sshpass -p '{{ domain_manager_ssh_password }}' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {{ domain_manager_ssh_username }}@{{ ip }} + reset_username_password: /home/nutanix/prism/cli/ncli user reset-password user-name={{ username }} password={{ password }} + +- name: Set reset command + ansible.builtin.set_fact: + reset_command: '{{ pc_ssh_cmd }} "{{ reset_username_password }}"' + +- name: Change password five times randomly before resetting + ansible.builtin.command: "{{ pc_ssh_cmd }} /home/nutanix/prism/cli/ncli user reset-password user-name={{ username }} password='{{ item }}'" + register: result + ignore_errors: true + loop: "{{ passwords }}" + changed_when: result.rc == 0 + +- name: Change password five times randomly before resetting status + ansible.builtin.assert: + that: + - result.msg == "All items completed" + +- name: Reset username and password + ansible.builtin.command: "{{ reset_command }}" + register: result + ignore_errors: true + changed_when: result.rc != 0 + +- name: Reset username and password status + ansible.builtin.assert: + that: + - "'reset successfully' in result.stdout" + +############################################################# +# Delete backup target object store + +- name: Delete backup target object store + nutanix.ncp.ntnx_pc_backup_target_v2: + ext_id: "{{ backup_target_object_store_ext_id }}" + domain_manager_ext_id: "{{ domain_manager_ext_id }}" + state: absent + register: result + ignore_errors: true + +- name: Delete backup target object store status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.status == "SUCCEEDED" + fail_msg: "Delete backup target object store failed" + success_msg: "Delete backup target object store passed" diff --git a/tests/integration/targets/ntnx_prism_v2/tasks/unregister_pcs.yml b/tests/integration/targets/ntnx_prism_v2/tasks/unregister_pcs.yml new file mode 100644 index 000000000..f9ca2ea86 --- /dev/null +++ b/tests/integration/targets/ntnx_prism_v2/tasks/unregister_pcs.yml @@ -0,0 +1,55 @@ +--- +- name: Start ntnx_pc_unregistration_v2 tests + ansible.builtin.debug: + msg: Start ntnx_pc_unregistration_v2 tests + +############################################################# +# List all clusters to get prism central external ID + +- name: List all clusters to get prism central external ID + nutanix.ncp.ntnx_clusters_info_v2: + filter: "config/clusterFunction/any(t:t eq Clustermgmt.Config.ClusterFunctionRef'PRISM_CENTRAL')" + register: result + ignore_errors: true + +- name: Get prism central external ID + ansible.builtin.set_fact: + domain_manager_ext_id: "{{ result.response[0].ext_id }}" + +############################################################# +# Generate spec for unregistering a PC +# Unregister PC + +- name: Generate spec for unregistering a PC + nutanix.ncp.ntnx_pc_unregistration_v2: + pc_ext_id: "e11acc65-479e-3aa2-9a98-1172e0c8b38a" + ext_id: "b3a6932b-f64e-49ee-924d-c5a5b8ce2f3f" + register: result + ignore_errors: true + check_mode: true + +- name: Generate spec for unregistering a PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == "b3a6932b-f64e-49ee-924d-c5a5b8ce2f3f" + - result.pc_ext_id == "e11acc65-479e-3aa2-9a98-1172e0c8b38a" + fail_msg: "Generate spec for unregistering a PC failed" + success_msg: "Generate spec for unregistering a PC passed" + +- name: Unregister PC + nutanix.ncp.ntnx_pc_unregistration_v2: + ext_id: "{{ pc_uuid }}" + pc_ext_id: "{{ domain_manager_ext_id }}" + register: result + ignore_errors: true + +- name: Unregister PC status + ansible.builtin.assert: + that: + - result.response is defined + - result.response.ext_id == result.task_ext_id + - result.pc_ext_id == domain_manager_ext_id + - result.response.status == "SUCCEEDED" + fail_msg: "Unregister PC failed" + success_msg: "Unregister PC passed"