I was reading VCDX56’s post Nutanix AHV VM Reporting Via REST API authored by Magnus Andersson @magander3 where, as the title suggests, he discusses a script he wrote to gather information about VMs running on a Nutanix AHV cluster using the REST APIs. At the end of the post, he mentioned that he would like to change the script from Bash to Python.
I have recently been doing quite a bit REST API scripting with Python, so I took a crack at it last night.
Before we get to the script a couple of things. I wrote the script for Python 3. I tested it on Python 3.5.2 on Linux (Ubuntu & CentOS), and with Python 3.6.3 on Windows 10. On the Nutanix side this script was tested against AOS 5.1.2 and Nutanix Community Edition (CE) 2017.05.22.
This script utilizes the “requests” module, which is a standard module that should already be installed on Ubuntu 16.04. For Windows or other situations where the requests module isn’t loaded, you will need to install it via “pip install requests”. If pip is not installed, reinstall/upgrade your Python installation and include pip as you install it.
Mac users should use Homebrew to install Python 3 by running “brew install python3” from Terminal. That will include pip3 so you can then run “pip3 install requests”. Then you can run the script by running “python3 ahv-vm-reporting.py”. Thanks to Peter Chang @virtualbacon for the Mac instructions/testing.
This script will collect the following information and dump it into a CSV file:
- VM Name
- Total Number of Cores
- Number of vCPUs
- Number of Cores per vCPU
- Memory in GB
- Number of vDisks attached (not including cdroms or Volume Groups)
- Disk Usage GB (total across all vDisks)
- Disk Allocated GB (total across all vDisks)
- Flash Mode Enabled
- Volume Group Attached
- Local AHV Snapshot Count
- Local Protection Domain Snapshot Count
- Remote Protection Domain Snapshot Count
- IP Address(es)
- Network Placement
- AHV Host Placement
Comparing to Magnus’ work, you will see that I added a couple of fields and I did not include the Self Service Portal information. For that, I’m waiting for the Nutanix Calm integration in Prism Central.
So, on to the script. Just as in Magnus’ Bash version, you will need to edit the following variables to make it work in your environment:
- user = “viewonly”
- passwd = “viewonly”
- CIP = “192.168.92.114”
As you can probably tell from the variables, this script requires a user with “View Only” rights to the Nutanix cluster. While you could use a more privileged account, there is no need. The CIP variable is the Cluster Virtual IP Address or resolvable DNS name of the cluster.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
#!/usr/bin/python3 import requests, datetime, time, os, subprocess, platform, re, csv from requests.auth import HTTPBasicAuth from requests.packages.urllib3.exceptions import InsecureRequestWarning # BEGIN User Defined Variables # Make sure to change these variables to match your environment! user = "viewonly" passwd = "viewonly" CIP = "192.168.92.114" # Cluster IP or DNS name (resolvable by the host from where you are running this script) # The rest of these variables shouldn't need to changed under normal circumstances. # EDIT WITH CAUTION! # CSV filename finalcsv = CIP + "-VM_Report-" + datetime.datetime.today().strftime('%Y%m%d%H%M') + ".csv" # Define URIs here. Keep them as variables as the API could change. buri2 = "https://" + CIP + ":9440/PrismGateway/services/rest/v2.0/" uricluster = "cluster/" urihost = "hosts/" urinetwork = "networks/" urivdisk = "virtual_disks/" urisnapshot = "snapshots/" uripdlocal = "protection_domains/dr_snapshots/" uripdremote = "remote_sites/dr_snapshots/" urivm = "vms/" ############################################################################### # DO NOT CHANGE ANYTHING BELOW THIS LINE (unless you know what you're doing)! # ############################################################################### start = time.perf_counter() headers = {'content-type': 'application/json'} requests.packages.urllib3.disable_warnings(InsecureRequestWarning) FNULL = open(os.devnull, 'w') def ping(host): ping_str = "-n 1" if platform.system().lower()=="windows" else "-c 1" args = "ping " + " " + ping_str + " " + host need_sh = False if platform.system().lower()=="windows" else True return subprocess.call(args, shell=need_sh, stdout=FNULL, stderr=subprocess.STDOUT) == 0 def restget(uri): response = requests.get(uri,auth=HTTPBasicAuth(user,passwd),headers=headers,verify=False) return(response) ######################## # MAIN # ######################## if __name__ == '__main__': if ping(CIP): print() ########################### # GET Cluster Information # ########################### uri = buri2 + uricluster status = restget(uri) try: status.raise_for_status() except: print("Unable to get Cluster Name:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: cname = status.json()['name'] cluversion = status.json()['version'] print("Cluster Name:\t " + cname) print("AOS Version:\t " + cluversion) print("") print("Collecting VM data...") ######################## # GET Host Information # ######################## # VM = host_uuid; host = name, serial uri = buri2 + urihost status = restget(uri) hosts = dict() try: status.raise_for_status() except: print("Unable to get hosts:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): hosts[status.json()['entities'][a]['uuid']] = status.json()['entities'][a]['name'] ########################### # GET Network Information # ########################### uri = buri2 + urinetwork status = restget(uri) vlans = dict() try: status.raise_for_status() except: print("Unable to get networks:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): vlans[status.json()['entities'][a]['uuid']] = status.json()['entities'][a]['name'] ######################### # GET vDisk Information # ######################### uri = buri2 + urivdisk status = restget(uri) vdisk = dict() try: status.raise_for_status() except: print("Unable to get virtual disks:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): vdisk[status.json()['entities'][a]['uuid']] = status.json()['entities'][a]['stats']['controller_user_bytes'] ############################ # GET Snapshot Information # ############################ uri = buri2 + urisnapshot status = restget(uri) snaplist = [] try: status.raise_for_status() except: print("Unable to get snapshot list:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): snaplist.append(status.json()['entities'][a]['vm_uuid']) ########################################### # GET Protection Domain Local Information # ########################################### uri = buri2 + uripdlocal status = restget(uri) pdlocallist = [] try: status.raise_for_status() except: print("Unable to get snapshot list:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): for b in range(len(status.json()['entities'][a]['vms'])): pdlocallist.append(status.json()['entities'][a]['vms'][b]['vm_id']) ############################################ # GET Protection Domain Remote Information # ############################################ uri = buri2 + uripdremote status = restget(uri) pdremotelist = [] try: status.raise_for_status() except: print("Unable to get snapshot list:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: for a in range(len(status.json()['entities'])): for b in range(len(status.json()['entities'][a]['vms'])): pdremotelist.append(status.json()['entities'][a]['vms'][b]['vm_id']) ###################### # GET VM Information # ###################### uri = buri2 + urivm + "?include_vm_disk_config=true&include_vm_nic_config=true" status = restget(uri) try: status.raise_for_status() except: print("Unable to get VM list:\t ", status ,"\n Please investigate. Exiting...") raise SystemExit else: with open(finalcsv, 'w', newline='') as csvfile: outfile = csv.writer(csvfile, delimiter=',') outfile.writerow(['VM Name', 'Total Cores', 'vCPUs', 'Cores per vCPU', 'Memory GB', 'Number of Disks', 'Disk Usage GB', 'Disk Allocated GB', 'Flash Mode Enabled', 'VG Attached', 'AHV Snapshots', 'PD Local Snapshots', 'PD Remote Snapshots', 'IP Address/IP Addresses', 'Network Placement', 'AHV Host placement']) for a in range(len(status.json()['entities'])): vmname = status.json()['entities'][a]['name'] vmuuid = status.json()['entities'][a]['uuid'] vmpower = status.json()['entities'][a]['power_state'] vcpu = int(status.json()['entities'][a]['num_cores_per_vcpu'] * status.json()['entities'][a]['num_vcpus']) vram = int(status.json()['entities'][a]['memory_mb'] / 1024) # Loop through vDisks if any NOT INCLUDING: cdrom or volume groups vmdisk = 0 vdiskuse = 0 vdiskcnt = 0 vfm = 0 try: status.json()['entities'][a]['vm_disk_info'] except: #print("no vdisks!", vdiskcnt, vmdisk) justkeepgoing = 1 else: vvol = 0 for b in range(len(status.json()['entities'][a]['vm_disk_info'])): if (status.json()['entities'][a]['vm_disk_info'][b]['is_cdrom'] == False): if ('volume_group_uuid' in status.json()['entities'][a]['vm_disk_info'][b]['disk_address']): vvol = 1 elif ('vmdisk_uuid' in status.json()['entities'][a]['vm_disk_info'][b]['disk_address']): vdiskcnt += 1 vmdisk += status.json()['entities'][a]['vm_disk_info'][b]['size'] if(status.json()['entities'][a]['vm_disk_info'][b]['flash_mode_enabled'] == True): vfm = 1 # vmdisk usage vdiskuse += int(vdisk[status.json()['entities'][a]['vm_disk_info'][b]['disk_address']['vmdisk_uuid']]) else: print("is NOT a vg!") if (vvol == 1): vvols = "Yes" else: vvols = "No" if (vfm == 1): vfms = "Yes" else: vfms = "No" vmip = dict() try: status.json()['entities'][a]['vm_nics'] except: # no vNICs assigned! vmip['No NIC present'] = "No NIC present" else: for b in range(len(status.json()['entities'][a]['vm_nics'])): # map the vNIC's network uuid to the Network name and store it with the IP nname = vlans[status.json()['entities'][a]['vm_nics'][b]['network_uuid']] try: status.json()['entities'][a]['vm_nics'][b]['requested_ip_address'] except: vmip["no ip address"] = nname else: vmip[status.json()['entities'][a]['vm_nics'][b]['requested_ip_address']] = nname # if VM powered on get the host if (vmpower == "on"): vmhost = hosts[status.json()['entities'][a]['host_uuid']] else: vmhost = "VM powered off" iplist = vmip.keys() vlanlist = vmip.values() outfile.writerow([vmname, vcpu, status.json()['entities'][a]['num_vcpus'], status.json()['entities'][a]['num_cores_per_vcpu'], vram, vdiskcnt, int(vdiskuse/1024/1024/1024), int(vmdisk/1024/1024/1024), vfms, vvols, snaplist.count(vmuuid), pdlocallist.count(vmuuid), pdremotelist.count(vmuuid), list(iplist), list(vlanlist), vmhost]) else: print("Cannot ping cluster at " + CIP) print("Please investigate. Exiting...") raise SystemExit print("Data collection time (seconds):","{0:.2f}".format(time.perf_counter() - start)) print() |
Download (copy/paste) the script and run it from your own workstation (no need to run this in a CVM). After running the script a csv file will be created in the directory from where the script was run. The name will be “CIP-VM_Report-YYYYMMDDHHMM.csv” where CIP is the IP or DNS name defined in the CIP variable in the script. Open up the CSV, select all the columns, autofit the columns to text, and center the text. Bold the top row for titles and here’s what it should look like (click the image below for full view):
Again, credit is very much due to Magnus @magander3 for the idea and creation of the Bash version of this script.
Feel free to ask questions in the comments!