Skip to content

Commit 44f19a2

Browse files
Merge pull request #23 from nutanixdev/batch_ops
Batch operations - $actions demo
2 parents 269a489 + ae75106 commit 44f19a2

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ build/
1919
*.egg-info/
2020

2121
*.log
22+
*.swp
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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

Comments
 (0)