diff --git a/python/nutanix_objects/requirements.txt b/python/nutanix_objects/requirements.txt old mode 100755 new mode 100644 index a2a7727..e67625a --- a/python/nutanix_objects/requirements.txt +++ b/python/nutanix_objects/requirements.txt @@ -1,2 +1,2 @@ boto3==1.17.84 -black==20.8b1 \ No newline at end of file +black==24.3.0 \ No newline at end of file diff --git a/python/v4api_sdk/api_key_auth_sdk.py b/python/v4api_sdk/api_key_auth_sdk.py new file mode 100644 index 0000000..bd859c6 --- /dev/null +++ b/python/v4api_sdk/api_key_auth_sdk.py @@ -0,0 +1,227 @@ +""" +Use the Nutanix v4 API SDKs to setup API key authentication +Requires Prism Central 2024.3 or later and AOS 7.0 or later +""" + +import getpass +import argparse +import sys +import urllib3 +import json + +import ntnx_vmm_py_client +from ntnx_vmm_py_client import Configuration as VMMConfiguration +from ntnx_vmm_py_client import ApiClient as VMMClient + +import ntnx_iam_py_client +from ntnx_iam_py_client import Configuration as IAMConfiguration +from ntnx_iam_py_client import ApiClient as IAMClient +from ntnx_iam_py_client.rest import ApiException as IAMException + +from ntnx_iam_py_client import UsersApi, AuthorizationPoliciesApi +from ntnx_iam_py_client import User, UserType, CreationType, UserStatusType +from ntnx_iam_py_client import Key, KeyKind +from ntnx_iam_py_client import AuthorizationPolicy, AuthorizationPolicyType +from ntnx_iam_py_client import EntityFilter, IdentityFilter + +from tme.utils import Utils + + +def main(): + """ + suppress warnings about insecure connections + please consider the security implications before + doing this in a production environment + """ + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + """ + setup the command line parameters + for this example only two parameters are required + - the Prism Central IP address or FQDN + - the Prism Central username; the script will prompt for the user's password + so that it never needs to be stored in plain text + """ + parser = argparse.ArgumentParser() + parser.add_argument("pc_ip", help="Prism Central IP address or FQDN") + parser.add_argument("username", help="Prism Central username") + parser.add_argument( + "-p", "--poll", help="Time between task polling, in seconds", default=1 + ) + args = parser.parse_args() + + # get the cluster password + cluster_password = getpass.getpass( + prompt="Enter your Prism Central \ +password: ", + stream=None, + ) + + pc_ip = args.pc_ip + username = args.username + + # make sure the user enters a password + if not cluster_password: + while not cluster_password: + print( + "Password cannot be empty. \ + Enter a password or Ctrl-C/Ctrl-D to exit." + ) + cluster_password = getpass.getpass( + prompt="Enter your Prism Central password: ", stream=None + ) + + try: + # create utils instance for re-use later + utils = Utils(pc_ip=pc_ip, username=username, password=cluster_password) + + vmm_config = VMMConfiguration() + iam_config = IAMConfiguration() + for config in [iam_config]: + # create the configuration instances + config.host = pc_ip + config.username = username + config.password = cluster_password + config.verify_ssl = False + config.debug = False + + # setup VMM configuration + # note we are NOT setting the username and password at this time + # later we will list VMs using API key authentication + vmm_config.host = pc_ip + vmm_config.port = "9440" + vmm_config.verify_ssl = False + vmm_config.debug = False + + vmm_config.logger_file = "./vmm.log" + iam_config.logger_file = "./iam.log" + + # before configuring API key auth, we need to get + # the extId of an existing role + # for this demo, we will use the built-in 'Super Admin' role + iam_client = IAMClient(configuration=iam_config) + iam_instance = ntnx_iam_py_client.api.RolesApi(api_client=iam_client) + print("Retrieving filtered role list ...") + role_list = iam_instance.list_roles( + async_req=False, _filter="contains(displayName, 'Super')" + ) + if len(role_list.data) > 0: + super_admin_ext_id = role_list.data[0].ext_id + print(f"Super Admin role extId: {super_admin_ext_id}") + else: + print("No role found containing the word \"Super\". Exiting ...") + sys.exit() + + vmm_client = VMMClient(configuration=vmm_config) + vmm_instance = ntnx_vmm_py_client.api.VmApi(api_client=vmm_client) + + sa_username = "api_key_service_account" + sa_email = "" + sa_display_name = "API key service account" + sa_description = "Service account for API key authentication" + key_name = "service_account_api_key" + acp_display_name = "API Key Auth Policy" + + print("\nThe following configuration will be used for API key authentication.") + print(f" Username: {sa_username}") + print(f" Description: {sa_description}") + print(f" Email: {sa_email}") + print(f" Display name: {sa_display_name}") + print(f" Key name: {key_name}") + print(f" Authorization policy display name: {acp_display_name}\n") + + confirm_continue = utils.confirm("Continue API key configuration?") + + if confirm_continue: + # create service account + service_account = User( + username=sa_username, + email=sa_email, + display_name=sa_display_name, + description=sa_description, + creation_type=CreationType.USERDEFINED, + status=UserStatusType.ACTIVE, + user_type=UserType.SERVICE_ACCOUNT, + ) + + iam_instance = UsersApi(api_client=iam_client) + create_sa = iam_instance.create_user(async_req=False, body=service_account) + + if create_sa: + print("Service account created successfully.") + else: + print("Service account creation failed. Check iam.log for details.") + sys.exit() + + # get the new service account user's ext_id + sa_ext_id = create_sa.data.ext_id + + # create API key + api_key = Key(name=key_name, key_type=KeyKind.API_KEY) + + create_key = iam_instance.create_user_key( + async_req=False, userExtId=sa_ext_id, body=api_key + ) + if create_key: + print("API key created successfully.") + print( + f"The API key will only be shown ONCE: {create_key.data.key_details.api_key}" + ) + api_key_value = create_key.data.key_details.api_key + + entities = [EntityFilter({"*": {"*": {"eq": "*"}}})] + + identities = [IdentityFilter({"user": {"uuid": {"anyof": [sa_ext_id]}}})] + + # create authorization policy for new service account + iam_instance = AuthorizationPoliciesApi(api_client=iam_client) + auth_policy = AuthorizationPolicy( + display_name=acp_display_name, + description="Authorization policy for use with API key service accounts", + authorization_policy_type=AuthorizationPolicyType.USER_DEFINED, + entities=entities, + identities=identities, + role=super_admin_ext_id, + ) + + create_acp = iam_instance.create_authorization_policy( + async_req=False, body=auth_policy + ) + if create_acp: + print("Authorization policy created successfully.") + else: + print( + "Authorization policy creation failed. Check iam.log for details." + ) + + auth_with_key = utils.confirm( + "\nAll configuration completed successfully. Attempt to list VMs with API authentication?" + ) + if auth_with_key: + print( + "Attempting to list Prism Central VMs using API key authentication ..." + ) + vmm_client.add_default_header( + header_name="X-Ntnx-Api-Key", header_value=api_key_value + ) + vm_list = vmm_instance.list_vms(async_req=False) + if vm_list: + print( + f"{len(vm_list.data)} VMs found. API key authentication successful." + ) + else: + print("VM list operation failed. Check vmm.log for details.") + else: + print("API key authentication cancelled.") + else: + print("API key configuration cancelled.") + sys.exit() + + except IAMException as iam_exception: + print( + f"Error sending request. Exception details:\n {json.loads(iam_exception.body)['data']['error'][0]['message']}" + ) + + +if __name__ == "__main__": + main() diff --git a/python/v4api_sdk/dr/LICENSE b/python/v4api_sdk/dr/LICENSE new file mode 100644 index 0000000..76106d8 --- /dev/null +++ b/python/v4api_sdk/dr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Nutanix Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/v4api_sdk/dr/NOTICES b/python/v4api_sdk/dr/NOTICES new file mode 100644 index 0000000..43e30f8 --- /dev/null +++ b/python/v4api_sdk/dr/NOTICES @@ -0,0 +1,4 @@ +Dependencies and Licenses +------------------------- + +ntnx-dataprotection-py-client: Nutanix Proprietary License, https://developers.nutanix.com/license diff --git a/python/v4api_sdk/dr/README.md b/python/v4api_sdk/dr/README.md new file mode 100644 index 0000000..2314a25 --- /dev/null +++ b/python/v4api_sdk/dr/README.md @@ -0,0 +1,6 @@ +# Nutanix v4 Disaster Recovery APIs + +The code samples in this directory are intended for use in conjunction with the following [Nutanix.dev](https://www.nutanix.dev) articles: + +- [Nutanix v4 Disaster Recovery API Series Part 1: Volume Shadow Copy Service Metadata](https://www.nutanix.dev/2025/01/14/nutanix-v4-disaster-recovery-api-series-part-1-volume-shadow-copy-service-metadata/ "Nutanix v4 Disaster Recovery API Series Part 1: Volume Shadow Copy Service Metadata") +- [Nutanix v4 Disaster Recovery API Series Part 2: Changed Blocks Tracking (CBT) and Changed Regions Tracking (CRT)](https://www.nutanix.dev/2025/01/15/nutanix-v4-disaster-recovery-api-series-part-2-changed-blocks-tracking-cbt-and-changed-regions-tracking-crt/ "Nutanix v4 Disaster Recovery API Series Part 2: Changed Blocks Tracking (CBT) and Changed Regions Tracking (CRT)") diff --git a/python/v4api_sdk/dr/cbt_api_code.py b/python/v4api_sdk/dr/cbt_api_code.py new file mode 100644 index 0000000..460a466 --- /dev/null +++ b/python/v4api_sdk/dr/cbt_api_code.py @@ -0,0 +1,139 @@ +import sys + +''' +This example is specific to VM recovery points. +''' + +# Import ntnx dataprotection api libraries. +import ntnx_dataprotection_py_client + + +# Discover cluster specific libraries. +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.ClusterDiscoverSpec import ClusterDiscoverSpec +from ntnx_dataprotection_py_client.models.dataprotection.v4.common.ClusterInfo import ClusterInfo +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.ClusterDiscoverOperation import ClusterDiscoverOperation +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.ComputeChangedRegionsClusterDiscoverSpec import ComputeChangedRegionsClusterDiscoverSpec +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.VmDiskRecoveryPointReference import VmDiskRecoveryPointReference +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.VmRecoveryPointChangedRegionsComputeSpec import VmRecoveryPointChangedRegionsComputeSpec + + +def get_pc_client_config(): + # Configure the client. + config = ntnx_dataprotection_py_client.Configuration() + # IPv4/IPv6 address or FQDN of the cluster + config.host = sys.argv[1] + # Port to which to connect to. + config.port = 9440 + # Max retry attempts while reconnecting on a loss of connection + config.max_retry_attempts = 3 + # Backoff factor to use during retry attempts + config.backoff_factor = 3 + # UserName to connect to the cluster + config.username = + # Password to connect to the cluster + config.password = + return config + + +def get_pe_client_config(target_cluster_ip): + # Configure the client. + config = ntnx_dataprotection_py_client.Configuration() + # IPv4/IPv6 address or FQDN of the cluster + config.host = target_cluster_ip + # Port to which to connect to. + config.port = 9440 + # Max retry attempts while reconnecting on a loss of connection + config.max_retry_attempts = 3 + # Backoff factor to use during retry attempts + config.backoff_factor = 3 + return config + +def prepare_vm_disk_recovery_point_reference(recovery_point_ext_id, vm_recovery_point_ext_id, disk_recovery_point_ext_id): + disk_recovery_point = VmDiskRecoveryPointReference() + disk_recovery_point.recovery_point_ext_id = recovery_point_ext_id + disk_recovery_point.vm_recovery_point_ext_id = vm_recovery_point_ext_id + disk_recovery_point.disk_recovery_point_ext_id = disk_recovery_point_ext_id + return disk_recovery_point + +def prepare_discover_cluster_request_body(base_disk_recovery_point, reference_disk_recovery_point=None): + # Discover cluster body model. + cluster_discover_spec = ClusterDiscoverSpec() + # Set the operation for which you want to send the discover cluster request. + cluster_discover_spec.operation = ClusterDiscoverOperation.COMPUTE_CHANGED_REGIONS + # prepare COMPUTE_CHANGED_REGIONS spec body. + cbt_spec = ComputeChangedRegionsClusterDiscoverSpec() + cbt_spec.disk_recovery_point = base_disk_recovery_point + if reference_disk_recovery_point: + cbt_spec.reference_disk_recovery_point = reference_disk_recovery_point + + # Set the vm cbt spec in cluster discover spec body. + cluster_discover_spec.spec = cbt_spec + return cluster_discover_spec + + +def get_pc_recovery_point_api_client(): + # Get the client configuration. + pc_client_config = get_pc_client_config() + # Intialize the PC ApiClient. + pc_client = ntnx_dataprotection_py_client.ApiClient(configuration=pc_client_config) + pc_recovery_points_api = ntnx_dataprotection_py_client.RecoveryPointsApi(api_client=pc_client) + return pc_recovery_points_api + + +def get_pe_recovery_point_api_client(target_cluster_ip, certificate=None): + # Get the client configuration. + pe_client_config = get_pe_client_config(target_cluster_ip) + # Intialize the PC ApiClient. + pe_client = ntnx_dataprotection_py_client.ApiClient(configuration=pe_client_config) + # Set the cookie header. + igw_header = "NTNX_IGW_SESSION={}".format(certificate) + pe_client.add_default_header("cookie", igw_header) + pe_recovery_points_api = ntnx_dataprotection_py_client.RecoveryPointsApi(api_client=pe_client) + return pe_recovery_points_api + +def prepare_vm_changed_regions_request_body(offset, length=None, block_size_byte=None, ref_recovery_point_ext_id=None, + ref_vm_recovery_point_ext_id=None, ref_disk_recovery_point_ext_id=None): + body = VmRecoveryPointChangedRegionsComputeSpec() + body.offset = offset # Int64 number. + if length: + body.length = length # Int64 number. + if block_size_byte: + body.block_size_byte = block_size_byte # Int64 number. + if ref_recovery_point_ext_id: + body.reference_recovery_point_ext_id = ref_recovery_point_ext_id + body.reference_vm_recovery_point_ext_id = ref_vm_recovery_point_ext_id + body.reference_disk_recovery_point_ext_id = ref_disk_recovery_point_ext_id + return body + +if __name__ == "__main__": + recovery_point_ext_id = sys.argv[2] + vm_recovery_point_ext_id = sys.argv[3] + disk_recovery_point_ext_id = sys.argv[4] + base_disk_recovery_point = prepare_vm_disk_recovery_point_reference(recovery_point_ext_id, vm_recovery_point_ext_id, disk_recovery_point_ext_id) + # This code is testing without reference recovery point. + # reference_disk_recovery_point = prepare_vm_disk_recovery_point_reference(ref_recovery_point_ext_id, ref_vm_recovery_point_ext_id, ref_disk_recovery_point_ext_id) + clusterDiscoverSpec = prepare_discover_cluster_request_body(base_disk_recovery_point=base_disk_recovery_point) + pe_auth_certificate = None + try: + pc_rp_api_client = get_pc_recovery_point_api_client() + pc_api_response = pc_rp_api_client.discover_cluster_for_recovery_point_id(extId=recovery_point_ext_id, body=clusterDiscoverSpec) + print(pc_api_response) + pe_auth_certificate = pc_api_response.data.jwt_token + except ntnx_dataprotection_py_client.rest.ApiException as e: + print(e) + + + # PE API call. + try: + offset = 0 + nextOffset = -1 + target_cluster_ip = pc_api_response.data.cluster_ip.ipv4.value + pe_rp_api_client = get_pe_recovery_point_api_client(target_cluster_ip, certificate=pe_auth_certificate) + while nextOffset != 0: + request_body = prepare_vm_changed_regions_request_body(offset) + pe_api_response = pe_rp_api_client.vm_recovery_point_compute_changed_regions(recoveryPointExtId=recovery_point_ext_id, vmRecoveryPointExtId=vm_recovery_point_ext_id, extId=disk_recovery_point_ext_id, body=request_body) + print(pe_api_response) + offset = int(pe_api_response.metadata.extra_info[0].value) + nextOffset = int(pe_api_response.metadata.extra_info[0].value) + except ntnx_dataprotection_py_client.rest.ApiException as e: + print(e) diff --git a/python/v4api_sdk/dr/requirements.txt b/python/v4api_sdk/dr/requirements.txt new file mode 100644 index 0000000..27dab31 --- /dev/null +++ b/python/v4api_sdk/dr/requirements.txt @@ -0,0 +1 @@ +ntnx_dataprotection_py_client==4.0.1 diff --git a/python/v4api_sdk/dr/vss_api_code.py b/python/v4api_sdk/dr/vss_api_code.py new file mode 100644 index 0000000..7133260 --- /dev/null +++ b/python/v4api_sdk/dr/vss_api_code.py @@ -0,0 +1,98 @@ +import sys + + +# Import ntnx dataprotection api libraries. +import ntnx_dataprotection_py_client + + +# Discover cluster specific libraries. +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.ClusterDiscoverSpec import ClusterDiscoverSpec +from ntnx_dataprotection_py_client.models.dataprotection.v4.common.ClusterInfo import ClusterInfo +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.ClusterDiscoverOperation import ClusterDiscoverOperation +from ntnx_dataprotection_py_client.models.dataprotection.v4.content.GetVssMetadataClusterDiscoverSpec import GetVssMetadataClusterDiscoverSpec + + +def get_pc_client_config(): + # Configure the client. + config = ntnx_dataprotection_py_client.Configuration() + # IPv4/IPv6 address or FQDN of the cluster + config.host = sys.argv[1] + # Port to which to connect to. + config.port = 9440 + # Max retry attempts while reconnecting on a loss of connection + config.max_retry_attempts = 3 + # Backoff factor to use during retry attempts + config.backoff_factor = 3 + # UserName to connect to the cluster + config.username = + # Password to connect to the cluster + config.password = + return config + + +def get_pe_client_config(target_cluster_ip): + # Configure the client. + config = ntnx_dataprotection_py_client.Configuration() + # IPv4/IPv6 address or FQDN of the cluster + config.host = target_cluster_ip + # Port to which to connect to. + config.port = 9440 + # Max retry attempts while reconnecting on a loss of connection + config.max_retry_attempts = 3 + # Backoff factor to use during retry attempts + config.backoff_factor = 3 + return config + + +def prepare_discover_cluster_request_body(vmRecoveryPointExtId=None): + # Discover cluster body model. + clusterDiscoverSpec = ClusterDiscoverSpec() + # Set the operation for which you want to send the discover cluster request. + clusterDiscoverSpec.operation = ClusterDiscoverOperation.GET_VSS_METADATA + # prepare GET_VSS_METADATA spec body. + vss_spec = GetVssMetadataClusterDiscoverSpec(vm_recovery_point_ext_id=vmRecoveryPointExtId) + # Set the vss spec in cluster discover spec body. + clusterDiscoverSpec.spec = vss_spec + return clusterDiscoverSpec + + +def get_pc_recovery_point_api_client(): + # Get the client configuration. + pc_client_config = get_pc_client_config() + # Intialize the PC ApiClient. + pc_client = ntnx_dataprotection_py_client.ApiClient(configuration=pc_client_config) + pc_recovery_points_api = ntnx_dataprotection_py_client.RecoveryPointsApi(api_client=pc_client) + return pc_recovery_points_api + +def get_pe_recovery_point_api_client(target_cluster_ip, certificate=None): + # Get the client configuration. + pe_client_config = get_pe_client_config(target_cluster_ip) + # Intialize the PC ApiClient. + pe_client = ntnx_dataprotection_py_client.ApiClient(configuration=pe_client_config) + # Set the cookie header. + igw_header = "NTNX_IGW_SESSION={}".format(certificate) + pe_client.add_default_header("cookie", igw_header) + pe_recovery_points_api = ntnx_dataprotection_py_client.RecoveryPointsApi(api_client=pe_client) + return pe_recovery_points_api + +if __name__ == "__main__": + recovery_point_ext_id = sys.argv[2] + vm_recovery_point_ext_id = sys.argv[3] + clusterDiscoverSpec = prepare_discover_cluster_request_body(vmRecoveryPointExtId=vm_recovery_point_ext_id) + pe_auth_certificate = None + try: + pc_rp_api_client = get_pc_recovery_point_api_client() + pc_api_response = pc_rp_api_client.discover_cluster_for_recovery_point_id(extId=recovery_point_ext_id, body=clusterDiscoverSpec) + print(pc_api_response) + pe_auth_certificate = pc_api_response.data.jwt_token + except ntnx_dataprotection_py_client.rest.ApiException as e: + print(e) + + # PE API call. + try: + target_cluster_ip = pc_api_response.data.cluster_ip.ipv4.value + pe_rp_api_client = get_pe_recovery_point_api_client(target_cluster_ip, certificate=pe_auth_certificate) + pe_api_response = pe_rp_api_client.get_vss_metadata_by_vm_recovery_point_id(recoveryPointExtId=recovery_point_ext_id,vmRecoveryPointExtId=vm_recovery_point_ext_id) + print(pe_api_response) + except ntnx_dataprotection_py_client.rest.ApiException as e: + print(e) diff --git a/python/v4api_sdk/requirements.txt b/python/v4api_sdk/requirements.txt index 4db7199..a037bca 100644 --- a/python/v4api_sdk/requirements.txt +++ b/python/v4api_sdk/requirements.txt @@ -1,4 +1,4 @@ -requests==2.31.0 +requests==2.32.2 ruff==0.1.15 ntnx_vmm_py_client==4.0.1 ntnx_lifecycle_py_client==4.0.1