|
| 1 | +""" |
| 2 | +Use the Nutanix v4 API SDKs to demonstrate Prism batch ACTION operations |
| 3 | +Requires Prism Central 2024.1 or later and AOS 6.8 or later |
| 4 | +""" |
| 5 | + |
| 6 | +import getpass |
| 7 | +import argparse |
| 8 | +import sys |
| 9 | +import urllib3 |
| 10 | +from pprint import pprint |
| 11 | + |
| 12 | +import ntnx_prism_py_client |
| 13 | +from ntnx_prism_py_client import Configuration as PrismConfiguration |
| 14 | +from ntnx_prism_py_client import ApiClient as PrismClient |
| 15 | +from ntnx_prism_py_client.rest import ApiException as PrismException |
| 16 | + |
| 17 | +import ntnx_vmm_py_client |
| 18 | +from ntnx_vmm_py_client import Configuration as VMMConfiguration |
| 19 | +from ntnx_vmm_py_client import ApiClient as VMMClient |
| 20 | +from ntnx_vmm_py_client.rest import ApiException as VMMException |
| 21 | + |
| 22 | +from ntnx_vmm_py_client.models.vmm.v4.ahv.config.AssociateVmCategoriesParams import ( |
| 23 | + AssociateVmCategoriesParams, |
| 24 | +) |
| 25 | +from ntnx_vmm_py_client.models.vmm.v4.ahv.config.CategoryReference import ( |
| 26 | + CategoryReference, |
| 27 | +) |
| 28 | + |
| 29 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpec import BatchSpec |
| 30 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpecMetadata import ( |
| 31 | + BatchSpecMetadata, |
| 32 | +) |
| 33 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpecPayload import ( |
| 34 | + BatchSpecPayload, |
| 35 | +) |
| 36 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpecPayloadMetadata import ( |
| 37 | + BatchSpecPayloadMetadata, |
| 38 | +) |
| 39 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpecPayloadMetadataHeader import ( |
| 40 | + BatchSpecPayloadMetadataHeader, |
| 41 | +) |
| 42 | +from ntnx_prism_py_client.models.prism.v4.operations.BatchSpecPayloadMetadataPath import ( |
| 43 | + BatchSpecPayloadMetadataPath, |
| 44 | +) |
| 45 | + |
| 46 | +from ntnx_prism_py_client.models.prism.v4.operations.ActionType import ActionType |
| 47 | + |
| 48 | + |
| 49 | +from tme import Utils |
| 50 | + |
| 51 | + |
| 52 | +def confirm_entity(api, client, entity_name: str) -> str: |
| 53 | + """ |
| 54 | + make sure the user is selecting the correct entity |
| 55 | + """ |
| 56 | + instance = api(api_client=client) |
| 57 | + print(f"Retrieving {entity_name} list ...") |
| 58 | + |
| 59 | + try: |
| 60 | + if entity_name == "category": |
| 61 | + # this filter is specific to this code sample and would need |
| 62 | + # to be modified before use elsewhere |
| 63 | + entities = instance.list_categories( |
| 64 | + async_req=False, |
| 65 | + _filter="type eq Schema.Enums.CategoryType'USER' and not contains(key, 'Calm')", |
| 66 | + ) |
| 67 | + else: |
| 68 | + print(f"{entity_name} is not supported. Exiting.") |
| 69 | + sys.exit() |
| 70 | + except PrismException as ex: |
| 71 | + print( |
| 72 | + f"\nAn exception occurred while retrieving the {entity_name} list.\ |
| 73 | + Details:\n" |
| 74 | + ) |
| 75 | + print(ex) |
| 76 | + sys.exit() |
| 77 | + except urllib3.exceptions.MaxRetryError as ex: |
| 78 | + print( |
| 79 | + f"Error connecting to {client.configuration.host}. Check connectivity, then try again. Details:" |
| 80 | + ) |
| 81 | + print(ex) |
| 82 | + sys.exit() |
| 83 | + |
| 84 | + # do some verification and make sure the user selects |
| 85 | + # the correct entity |
| 86 | + found_entities = [] |
| 87 | + for entity in entities.data: |
| 88 | + found_entities.append( |
| 89 | + { |
| 90 | + "key": entity.key, |
| 91 | + "value": entity.value, |
| 92 | + "ext_id": entity.ext_id, |
| 93 | + } |
| 94 | + ) |
| 95 | + print(f"The following categories ({len(found_entities)}) were found.") |
| 96 | + pprint(found_entities) |
| 97 | + |
| 98 | + expected_entity_ext_id = input( |
| 99 | + f"\nPlease enter the ext_id of the selected {entity_name}: " |
| 100 | + ).lower() |
| 101 | + matches = [ |
| 102 | + x |
| 103 | + for x in found_entities |
| 104 | + if x["ext_id"].lower() == expected_entity_ext_id.lower() |
| 105 | + ] |
| 106 | + if not matches: |
| 107 | + print( |
| 108 | + f"No {entity_name} was found matching the ext_id \ |
| 109 | +{expected_entity_ext_id}. Exiting." |
| 110 | + ) |
| 111 | + sys.exit() |
| 112 | + # get the entity ext_id |
| 113 | + ext_id = matches[0]["ext_id"] |
| 114 | + return ext_id |
| 115 | + |
| 116 | + |
| 117 | +def main(): |
| 118 | + """ |
| 119 | + suppress warnings about insecure connections |
| 120 | + please consider the security implications before |
| 121 | + doing this in a production environment |
| 122 | + """ |
| 123 | + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
| 124 | + |
| 125 | + """ |
| 126 | + setup the command line parameters |
| 127 | + for this example only two parameters are required |
| 128 | + - the Prism Central IP address or FQDN |
| 129 | + - the Prism Central username; the script will prompt for the user's password |
| 130 | + so that it never needs to be stored in plain text |
| 131 | + """ |
| 132 | + parser = argparse.ArgumentParser() |
| 133 | + parser.add_argument("pc_ip", help="Prism Central IP address or FQDN") |
| 134 | + parser.add_argument("username", help="Prism Central username") |
| 135 | + parser.add_argument( |
| 136 | + "-p", "--poll", help="Time between task polling, in seconds", default=1 |
| 137 | + ) |
| 138 | + args = parser.parse_args() |
| 139 | + |
| 140 | + # get the cluster password |
| 141 | + cluster_password = getpass.getpass( |
| 142 | + prompt="Enter your Prism Central \ |
| 143 | +password: ", |
| 144 | + stream=None, |
| 145 | + ) |
| 146 | + |
| 147 | + pc_ip = args.pc_ip |
| 148 | + username = args.username |
| 149 | + poll_timeout = args.poll |
| 150 | + |
| 151 | + # make sure the user enters a password |
| 152 | + if not cluster_password: |
| 153 | + while not cluster_password: |
| 154 | + print( |
| 155 | + "Password cannot be empty. \ |
| 156 | + Enter a password or Ctrl-C/Ctrl-D to exit." |
| 157 | + ) |
| 158 | + cluster_password = getpass.getpass( |
| 159 | + prompt="Enter your Prism Central password: ", stream=None |
| 160 | + ) |
| 161 | + |
| 162 | + try: |
| 163 | + # create utils instance for re-use later |
| 164 | + utils = Utils(pc_ip=pc_ip, username=username, password=cluster_password) |
| 165 | + |
| 166 | + prism_config = PrismConfiguration() |
| 167 | + vmm_config = VMMConfiguration() |
| 168 | + for config in [prism_config, vmm_config]: |
| 169 | + # create the configuration instances |
| 170 | + config.host = pc_ip |
| 171 | + config.username = username |
| 172 | + config.password = cluster_password |
| 173 | + config.verify_ssl = False |
| 174 | + |
| 175 | + prism_client = PrismClient(configuration=prism_config) |
| 176 | + vmm_client = VMMClient(configuration=vmm_config) |
| 177 | + |
| 178 | + batch_instance = ntnx_prism_py_client.api.BatchesApi(api_client=prism_client) |
| 179 | + vmm_instance = ntnx_vmm_py_client.api.VmApi(api_client=vmm_client) |
| 180 | + |
| 181 | + # vm_extid = "0628ac56-ba0b-4de6-61aa-4ae4e287acdc" |
| 182 | + # existing_vm = vmm_instance.get_vm_by_id(vm_extid) |
| 183 | + # etag = vmm_client.get_etag(existing_vm) |
| 184 | + |
| 185 | + input( |
| 186 | + "\nThis demo uses the Nutanix v4 API `prism` namespace's \ |
| 187 | +batch APIs to assign matching virtual machines to a specific \ |
| 188 | +category.\nVM matches are based on a list of VMs built using OData \ |
| 189 | +filters to include those with a name containing the string \ |
| 190 | +'batchdemo'.\n\nYou will now be prompted for the ext_id of the category \ |
| 191 | +to which these VMs will be assigned.\n\nPress ENTER to continue." |
| 192 | + ) |
| 193 | + |
| 194 | + """ |
| 195 | + ask the user to confirm the category ext_id |
| 196 | + """ |
| 197 | + category_ext_id = confirm_entity( |
| 198 | + ntnx_prism_py_client.api.CategoriesApi, prism_client, "category" |
| 199 | + ) |
| 200 | + |
| 201 | + print("Building filtered list of existing VMs ...") |
| 202 | + print("Note: By default this will retrieve a maximum of 50 VMs.") |
| 203 | + vm_list = vmm_instance.list_vms( |
| 204 | + async_req=False, _filter="startswith(name, 'batchdemo')" |
| 205 | + ) |
| 206 | + if vm_list.data: |
| 207 | + print(f"{len(vm_list.data)} VM(s) found:") |
| 208 | + for vm in vm_list.data: |
| 209 | + print(f"- {vm.name}") |
| 210 | + else: |
| 211 | + print("No matching VMs found. Exiting ...") |
| 212 | + sys.exit() |
| 213 | + |
| 214 | + confirm_action = utils.confirm("Submit batch operation?") |
| 215 | + if confirm_action: |
| 216 | + # initiate the list of VMs that will be modified |
| 217 | + # this is a list of BatchSpecPayload |
| 218 | + batch_spec_payload_list = [] |
| 219 | + |
| 220 | + print("Building VM batch $action payload ...") |
| 221 | + for vm in vm_list.data: |
| 222 | + existing_vm = vmm_instance.get_vm_by_id(vm.ext_id) |
| 223 | + vm_ext_id = existing_vm.data.ext_id |
| 224 | + etag = vmm_client.get_etag(existing_vm) |
| 225 | + |
| 226 | + # build the payload that will be used when assigning categories |
| 227 | + batch_spec_payload_list.append( |
| 228 | + BatchSpecPayload( |
| 229 | + data=AssociateVmCategoriesParams( |
| 230 | + categories=[CategoryReference(ext_id=category_ext_id)] |
| 231 | + ), |
| 232 | + metadata=BatchSpecPayloadMetadata( |
| 233 | + headers=[ |
| 234 | + BatchSpecPayloadMetadataHeader( |
| 235 | + name="If-Match", value=etag |
| 236 | + ) |
| 237 | + ], |
| 238 | + path=[ |
| 239 | + BatchSpecPayloadMetadataPath( |
| 240 | + name="extId", value=vm_ext_id |
| 241 | + ) |
| 242 | + ], |
| 243 | + ), |
| 244 | + ) |
| 245 | + ) |
| 246 | + |
| 247 | + batch_spec = BatchSpec( |
| 248 | + metadata=BatchSpecMetadata( |
| 249 | + action=ActionType.ACTION, |
| 250 | + name="Associate Categories", |
| 251 | + stop_on_error=True, |
| 252 | + chunk_size=1, |
| 253 | + uri="/api/vmm/v4.0.b1/ahv/config/vms/{extId}/$actions/associate-categories", |
| 254 | + ), |
| 255 | + payload=batch_spec_payload_list, |
| 256 | + ) |
| 257 | + |
| 258 | + print("Submitting batch operation to assign VM categories ...") |
| 259 | + batch_response = batch_instance.submit_batch( |
| 260 | + async_req=False, body=batch_spec |
| 261 | + ) |
| 262 | + |
| 263 | + # grab the ext ID of the batch operation task |
| 264 | + modify_ext_id = batch_response.data.ext_id |
| 265 | + utils.monitor_task( |
| 266 | + task_ext_id=modify_ext_id, |
| 267 | + task_name="Batch VM Category Assignment", |
| 268 | + pc_ip=pc_ip, |
| 269 | + username=username, |
| 270 | + password=cluster_password, |
| 271 | + poll_timeout=poll_timeout, |
| 272 | + ) |
| 273 | + print(f"{len(batch_spec_payload_list)} VMs assigned to category.") |
| 274 | + else: |
| 275 | + print("Batch operation cancelled.") |
| 276 | + |
| 277 | + except VMMException as vmm_exception: |
| 278 | + print( |
| 279 | + f"Unable to authenticate using the supplied credentials. \ |
| 280 | +Check your username and/or password, then try again. \ |
| 281 | +Exception details: {vmm_exception}" |
| 282 | + ) |
| 283 | + |
| 284 | + |
| 285 | +if __name__ == "__main__": |
| 286 | + main() |
0 commit comments