""" gets performance metrics from Prism Central using v4 API and python SDK
Args:
prism: The IP or FQDN of Prism.
username: The Prism user name.
secure: True or False to control SSL certs verification.
Returns:
html and excel report files.
"""
#region #*IMPORT
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
import math
import time
import datetime
import argparse
import getpass
from humanfriendly import format_timespan
import urllib3
import pandas as pd
import keyring
import tqdm
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import ntnx_aiops_py_client
import ntnx_vmm_py_client
#endregion #*IMPORT
#region #*CLASS
class PrintColors:
"""Used for colored output formatting.
"""
OK = '\033[92m' #GREEN
SUCCESS = '\033[96m' #CYAN
DATA = '\033[097m' #WHITE
WARNING = '\033[93m' #YELLOW
FAIL = '\033[91m' #RED
STEP = '\033[95m' #PURPLE
RESET = '\033[0m' #RESET COLOR
#endregion #*CLASS
#region #*FUNCTIONS
def fetch_entities(client,module,entity_api,function,page,limit=50):
'''fetch_entities function.
Args:
client: a v4 Python SDK client object.
module: name of the v4 Python SDK module to use.
entity_api: name of the entity API to use.
function: name of the function to use.
page: page number to fetch.
limit: number of entities to fetch.
Returns:
'''
entity_api_module = getattr(module, entity_api)
entity_api = entity_api_module(api_client=client)
list_function = getattr(entity_api, function)
response = list_function(_page=page,_limit=limit)
return response
def fetch_entity_descriptors(client,source_ext_id,page,limit=50):
'''fetch_entity_descriptors function.
Args:
client: a v4 Python SDK client object.
source_ext_id: uuid of a valid source.
page: page number to fetch.
limit: number of entities to fetch.
Returns:
'''
entity_api = ntnx_aiops_py_client.StatsApi(api_client=client)
response = entity_api.get_entity_descriptors_v4(sourceExtId=source_ext_id,_page=page,_limit=limit)
return response
def get_vm_metrics(client,vm,minutes_ago,sampling_interval,stat_type,graph,csv_export):
'''get_vm_metrics function.
Fetches metrics for a specified vm and generates graphs for that entity.
Args:
client: a v4 Python SDK client object.
vm: a virtual machine name
minutes_ago: integer indicating the number of minutes to get metrics for (exp: 60 would mean get the metrics for the last hour).
sampling_interval: integer used to specify in seconds the sampling interval.
stat_type: The operator to use while performing down-sampling on stats data. Allowed values are SUM, MIN, MAX, AVG, COUNT and LAST.
Returns:
'''
""" print(f"(get_vm_metrics) show graphs: {graph}")
print(f"(get_vm_metrics) csv exports: {csv_export}") """
#* fetch vm object to figure out extId
entity_api = ntnx_vmm_py_client.VmApi(api_client=client)
query_filter = f"name eq '{vm}'"
response = entity_api.list_vms(_filter=query_filter)
vm_uuid = response.data[0].ext_id
#print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Fetching metrics for VM {vm} with uuid {vm_uuid}...{PrintColors.RESET}")
#* fetch metrics for vm
entity_api = ntnx_vmm_py_client.StatsApi(api_client=client)
start_time = (datetime.datetime.now(datetime.timezone.utc)-datetime.timedelta(minutes=minutes_ago)).isoformat()
end_time = (datetime.datetime.now(datetime.timezone.utc)).isoformat()
response = entity_api.get_vm_stats_by_id(vm_uuid, _startTime=start_time, _endTime=end_time, _samplingInterval=sampling_interval, _statType=stat_type, _select='*')
vm_stats = [stat for stat in response.data.stats if stat.cluster is None]
#print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Found {len(vm_stats)} data points for VM {vm} with uuid {vm_uuid}...{PrintColors.RESET}")
#* building pandas dataframe from the retrieved data
data_points = []
for data_point in vm_stats:
data_points.append(data_point.to_dict())
df = pd.DataFrame(data_points)
df = df.set_index('timestamp')
df.drop('_reserved', axis=1, inplace=True)
df.drop('_object_type', axis=1, inplace=True)
df.drop('_unknown_fields', axis=1, inplace=True)
df.drop('cluster', axis=1, inplace=True)
df.drop('hypervisor_type', axis=1, inplace=True)
df.drop('check_score', axis=1, inplace=True)
#* building graphs
if graph is True:
df = df.dropna(subset=['disk_usage_ppm'])
df['disk_usage'] = (df['disk_usage_ppm'] / 10000).round(2)
df = df.dropna(subset=['memory_usage_ppm'])
df['memory_usage'] = (df['memory_usage_ppm'] / 10000).round(2)
df = df.dropna(subset=['hypervisor_cpu_usage_ppm'])
df['hypervisor_cpu_usage'] = (df['hypervisor_cpu_usage_ppm'] / 10000).round(2)
df = df.dropna(subset=['hypervisor_cpu_ready_time_ppm'])
df['hypervisor_cpu_ready_time'] = (df['hypervisor_cpu_ready_time_ppm'] / 10000).round(2)
fig = make_subplots(rows=2, cols=2,
subplot_titles=(f"{vm} Overview", f"{vm} Storage IOPS", f"{vm} Storage Bandwidth", f"{vm} Storage Latency"),
x_title="Time") # Shared x-axis title
# Subplot 1: Overview
y_cols1 = ["hypervisor_cpu_usage", "hypervisor_cpu_ready_time", "memory_usage", "disk_usage"]
for y_col in y_cols1:
fig.add_trace(go.Scatter(x=df.index, y=df[y_col], hovertemplate="%{x}
%%{y}", name=y_col, mode='lines', legendgroup='group1'), row=1, col=1)
fig.update_yaxes(title_text="% Utilized", range=[0, 100], row=1, col=1)
# Subplot 2: Storage IOPS
y_cols2 = ["controller_num_iops", "controller_num_read_iops", "controller_num_write_iops"]
for y_col in y_cols2:
fig.add_trace(go.Scatter(x=df.index, y=df[y_col], hovertemplate="%{x}
%{y} iops", name=y_col, mode='lines', legendgroup='group2'), row=1, col=2)
fig.update_yaxes(title_text="IOPS", row=1, col=2)
# Subplot 3: Storage Bandwidth
y_cols3 = ["controller_io_bandwidth_kbps", "controller_read_io_bandwidth_kbps", "controller_write_io_bandwidth_kbps"]
for y_col in y_cols3:
fig.add_trace(go.Scatter(x=df.index, y=df[y_col], hovertemplate="%{x}
%{y} kbps", name=y_col, mode='lines', legendgroup='group3'), row=2, col=1)
fig.update_yaxes(title_text="Kbps", row=2, col=1)
# Subplot 4: Storage Latency
y_cols4 = ["controller_avg_io_latency_micros", "controller_avg_read_io_latency_micros", "controller_avg_write_io_latency_micros"]
for y_col in y_cols4:
fig.add_trace(go.Scatter(x=df.index, y=df[y_col], hovertemplate="%{x}
%{y} usec", name=y_col, mode='lines', legendgroup='group4'), row=2, col=2)
fig.update_yaxes(title_text="Microseconds", row=2, col=2)
fig.update_layout(height=800, legend_title_text="Metric") # Shared legend title
fig.show()
#* exporting results to csv
if csv_export is True:
for column in df.columns:
df[column].to_csv(f"{vm}_{column}.csv", index=True)
def main(api_server,username,secret,vms,graph,csv_export,minutes_ago=5,sampling_interval=30,stat_type="AVG",secure=False,show=False):
'''main function.
Args:
api_server: IP or FQDN of the REST API server.
username: Username to use for authentication.
secret: Secret for the username.
secure: indicates if certs should be verified.
Returns:
html and excel report files.
'''
processing_start_time = time.time()
limit=100
if show is True:
#* initialize variable for API client configuration
api_client_configuration = ntnx_aiops_py_client.Configuration()
api_client_configuration.host = api_server
api_client_configuration.username = username
api_client_configuration.password = secret
if secure is False:
#! suppress warnings about insecure connections
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#! suppress ssl certs verification
api_client_configuration.verify_ssl = False
#* getting list of sources
client = ntnx_aiops_py_client.ApiClient(configuration=api_client_configuration)
entity_api = ntnx_aiops_py_client.StatsApi(api_client=client)
print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Fetching available sources...{PrintColors.RESET}")
response = entity_api.get_sources_v4()
source_ext_id = next(iter([source.ext_id for source in response.data if source.source_name == 'nutanix']))
#* getting entities and metrics descriptor for nutanix source
print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Fetching entities and descriptors for source nutanix...{PrintColors.RESET}")
entity_list=[]
response = entity_api.get_entity_descriptors_v4(sourceExtId=source_ext_id,_page=0,_limit=1)
total_available_results=response.metadata.total_available_results
page_count = math.ceil(total_available_results/limit)
with tqdm.tqdm(total=page_count, desc="Fetching pages") as progress_bar:
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(
fetch_entity_descriptors,
client=client,
source_ext_id=source_ext_id,
page=page_number,
limit=limit
) for page_number in range(0, page_count, 1)]
for future in as_completed(futures):
try:
entities = future.result()
entity_list.extend(entities.data)
except Exception as e:
print(f"{PrintColors.WARNING}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [WARNING] Task failed: {e}{PrintColors.RESET}")
finally:
progress_bar.update(1)
entity_descriptors_list = entity_list
descriptors={}
for item in entity_descriptors_list:
entity_type = item.entity_type
descriptors[entity_type] = {}
for metric in item.metrics:
metric_name = metric.name
descriptors[entity_type][metric_name] = {}
descriptors[entity_type][metric_name]['name'] = metric.name
descriptors[entity_type][metric_name]['value_type'] = metric.value_type
if metric.additional_properties is not None:
descriptors[entity_type][metric_name]['description'] = next(iter([metric_property.value for metric_property in metric.additional_properties if metric_property.name == 'description']),None)
else:
descriptors[entity_type][metric_name]['description'] = None
for entity_type in descriptors.keys():
print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Available metrics for {entity_type} are:{PrintColors.RESET}")
for metric in sorted(descriptors[entity_type]):
print(f" {descriptors[entity_type][metric]['name']},{descriptors[entity_type][metric]['value_type']},{descriptors[entity_type][metric]['description']}")
elif vms:
#* initialize variable for API client configuration
api_client_configuration = ntnx_vmm_py_client.Configuration()
api_client_configuration.host = api_server
api_client_configuration.username = username
api_client_configuration.password = secret
if secure is False:
#! suppress warnings about insecure connections
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#! suppress ssl certs verification
api_client_configuration.verify_ssl = False
client = ntnx_vmm_py_client.ApiClient(configuration=api_client_configuration)
with tqdm.tqdm(total=len(vms), desc="Processing VMs") as progress_bar:
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(
get_vm_metrics,
client=client,
vm=vm,
minutes_ago=minutes_ago,
sampling_interval=sampling_interval,
stat_type=stat_type,
graph=graph,
csv_export=csv_export,
) for vm in vms]
for future in as_completed(futures):
try:
entities = future.result()
except Exception as e:
print(f"{PrintColors.WARNING}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [WARNING] Task failed: {e}{PrintColors.RESET}")
finally:
progress_bar.update(1)
#! if you wanted to show 4 graphs on separate pages, use this instead:
""" fig = make_subplots(rows=2, cols=2, subplot_titles=(f"{vm} Overview", f"{vm} Storage IOPS", f"{vm} Storage Bandwidth", f"{vm} Storage Latency"))
fig.add_trace(go.Line(y=df["hypervisor_cpu_usage", "hypervisor_cpu_ready_time", "memory_usage", "disk_usage"]), row=1, col=1)
fig.add_trace(go.Line(y=df["hypervisor_cpu_usage", "hypervisor_cpu_ready_time", "memory_usage", "disk_usage"]), row=1, col=2)
fig.add_trace(go.Line(y=df["hypervisor_cpu_usage", "hypervisor_cpu_ready_time", "memory_usage", "disk_usage"]), row=2, col=1)
fig.add_trace(go.Line(y=df["hypervisor_cpu_usage", "hypervisor_cpu_ready_time", "memory_usage", "disk_usage"]), row=2, col=2)
fig.update_yaxes(range=[0, 100], row=1, col=1)
fig.update_yaxes(range=[0, 100], row=1, col=2)
fig.update_yaxes(range=[0, 100], row=2, col=1)
fig.update_yaxes(range=[0, 100], row=2, col=2)
fig.update_layout(xaxis_title="Time", # For shared x-axis title
yaxis_title="% Utilized", # For the first subplot's y-axis
yaxis2_title="% Utilized", # For the first subplot's y-axis
yaxis3_title="% Utilized", # For the first subplot's y-axis
yaxis4_title="% Utilized",
legend_title_text="Metric")
fig.show() """
processing_end_time = time.time()
elapsed_time = processing_end_time - processing_start_time
print(f"{PrintColors.STEP}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [SUM] Process completed in {format_timespan(elapsed_time)}{PrintColors.RESET}")
#endregion #*FUNCTIONS
if __name__ == '__main__':
# * parsing script arguments
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-p", "--prism", help="prism server.")
parser.add_argument("-u", "--username", default='admin', help="username for prism server.")
parser.add_argument("-s", "--secure", default=False, action=argparse.BooleanOptionalAction, help="Control SSL certs verification.")
parser.add_argument("-sh", "--show", action=argparse.BooleanOptionalAction, help="Show available entity types and metrics.")
parser.add_argument("-g", "--graph", action=argparse.BooleanOptionalAction, help="Indicate you want graphs to be generated. Defaults to True.")
parser.add_argument("-e", "--export", action=argparse.BooleanOptionalAction, help="Indicate you want csv exports to be generated (1 csv file per metric for each vm). Defaults to False.")
parser.add_argument("-v", "--vm", type=str, help="Comma separated list of VM names you want to process.")
parser.add_argument("-c", "--csv", type=str, help="Path and name of csv file with vm names (header: vm_name and then one vm name per line).")
parser.add_argument("-t", "--time", type=int, default=5, help="Integer used to specify how many minutes ago you want to collect metrics for (defaults to 5 minutes ago).")
parser.add_argument("-i", "--interval", type=int, default=30, help="Integer used to specify in seconds the sampling interval (defaults to 30 seconds).")
parser.add_argument("-st", "--stat_type", default="AVG", choices=["AVG","MIN","MAX","LAST","SUM","COUNT"], help="The operator to use while performing down-sampling on stats data. Allowed values are SUM, MIN, MAX, AVG, COUNT and LAST. Defaults to AVG")
args = parser.parse_args()
# * check for password (we use keyring python module to access the workstation operating system password store in an "ntnx" section)
print(f"{PrintColors.OK}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [INFO] Trying to retrieve secret for user {args.username} from the password store.{PrintColors.RESET}")
pwd = keyring.get_password("ntnx",args.username)
if not pwd:
try:
pwd = getpass.getpass()
keyring.set_password("ntnx",args.username,pwd)
except Exception as error:
print(f"{PrintColors.FAIL}{(datetime.now(timezone.utc)).strftime('%Y-%m-%d %H:%M:%S')} [ERROR] {error}.{PrintColors.RESET}")
exit(1)
if args.show is True:
target_vms = None
elif args.csv:
data=pd.read_csv(args.csv)
target_vms = data['vm_name'].tolist()
elif args.vm:
target_vms = args.vm.split(',')
main(api_server=args.prism,username=args.username,secret=pwd,secure=args.secure,show=args.show,vms=target_vms,minutes_ago=args.time,sampling_interval=args.interval,stat_type=args.stat_type,graph=args.graph,csv_export=args.export)