Skip to content

Commit 7269b09

Browse files
author
Andrii Sultanov
committed
python3: Resurrect a metricsgraph.py helper script
This script was previously deleted in #3949, update it to Python 3 and the new API methods (VIF.get_metrics and the like were removed years ago), get rid of globals and remove the sanitycheck library usage. Install it alongside other libexec python scripts. The script outputs a short list of active objects and their graph in the graphviz format (it could be turned into an image with the help of various graphviz utilities like `echo digraph active_objects {...} | dot -Tsvg > output.svg`) ``` $ ./metricsgraph.py /* hosts : lcy2-dt29 */ /* resident VMs : CentOS Stream 9 (1), Control domain on host: lcy2-dt29, Windows 10 (64-bit) (1) */ /* active VIFs : OpaqueRef:051f732b-4897-17ab-1a03-9ab2e90a6d8e, OpaqueRef:b7291919-2f60-83a0-7777-96ffd6a73e32 */ /* active VBDs : OpaqueRef:21f94d40-1be3-1dee-d394-7c18eaaad8b4, OpaqueRef:44984583-9938-1177-e7e5-dbc4bd23f51e, OpaqueRef:6de310eb-2b0b-85c7-cd5b-d3ccbe46f3d8, OpaqueRef:543537d3-ec19-b67d-7422-545894cf6727 */ /* active networks : NPRI bond of 0 1 */ /* active PIFs : OpaqueRef:4d293e68-1c2a-c6a8-bdd9-823abca394ba */ digraph active_objects { node [shape="rect"]; "OpaqueRef:5cdd5f91-b5e2-ba4f-09df-0f4ef9b77ef4" [label="lcy2-dt29\ncpus=0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00\nmemory=0.65"]; "OpaqueRef:1d340059-3a7b-bac6-9d90-26e87ecc4a2a" [label="CentOS Stream 9 (1)\ncpus=0.00\nmemory=4095.98828125M"]; "OpaqueRef:f76036a2-17d8-c6fa-7959-1e5b10c50d3b" [label="Control domain on host: lcy2-dt29\ncpus=0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00\nmemory=2656.0M"]; "OpaqueRef:c613d702-b8d7-2313-34b5-2683accae3ed" [label="Windows 10 (64-bit) (1)\ncpus=0.00\nmemory=4097.98046875M"]; "OpaqueRef:051f732b-4897-17ab-1a03-9ab2e90a6d8e" [label="vif\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:b7291919-2f60-83a0-7777-96ffd6a73e32" [label="vif\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:21f94d40-1be3-1dee-d394-7c18eaaad8b4" [label="vbd\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:44984583-9938-1177-e7e5-dbc4bd23f51e" [label="vbd\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:6de310eb-2b0b-85c7-cd5b-d3ccbe46f3d8" [label="vbd\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:543537d3-ec19-b67d-7422-545894cf6727" [label="vbd\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:4d293e68-1c2a-c6a8-bdd9-823abca394ba" [label="pif\nread=0.00k\nwrite=0.00k"]; "OpaqueRef:cf42e13c-aa9d-c604-710c-bead3587948a" [label="NPRI bond of 0 1"]; "OpaqueRef:5cdd5f91-b5e2-ba4f-09df-0f4ef9b77ef4" -> "OpaqueRef:1d340059-3a7b-bac6-9d90-26e87ecc4a2a"; "OpaqueRef:5cdd5f91-b5e2-ba4f-09df-0f4ef9b77ef4" -> "OpaqueRef:c613d702-b8d7-2313-34b5-2683accae3ed"; "OpaqueRef:5cdd5f91-b5e2-ba4f-09df-0f4ef9b77ef4" -> "OpaqueRef:f76036a2-17d8-c6fa-7959-1e5b10c50d3b"; "OpaqueRef:1d340059-3a7b-bac6-9d90-26e87ecc4a2a" -> "OpaqueRef:b7291919-2f60-83a0-7777-96ffd6a73e32"; "OpaqueRef:c613d702-b8d7-2313-34b5-2683accae3ed" -> "OpaqueRef:051f732b-4897-17ab-1a03-9ab2e90a6d8e"; "OpaqueRef:1d340059-3a7b-bac6-9d90-26e87ecc4a2a" -> "OpaqueRef:21f94d40-1be3-1dee-d394-7c18eaaad8b4"; "OpaqueRef:1d340059-3a7b-bac6-9d90-26e87ecc4a2a" -> "OpaqueRef:6de310eb-2b0b-85c7-cd5b-d3ccbe46f3d8"; "OpaqueRef:c613d702-b8d7-2313-34b5-2683accae3ed" -> "OpaqueRef:44984583-9938-1177-e7e5-dbc4bd23f51e"; "OpaqueRef:c613d702-b8d7-2313-34b5-2683accae3ed" -> "OpaqueRef:543537d3-ec19-b67d-7422-545894cf6727"; "OpaqueRef:051f732b-4897-17ab-1a03-9ab2e90a6d8e" -> "OpaqueRef:cf42e13c-aa9d-c604-710c-bead3587948a"; "OpaqueRef:b7291919-2f60-83a0-7777-96ffd6a73e32" -> "OpaqueRef:cf42e13c-aa9d-c604-710c-bead3587948a"; "OpaqueRef:cf42e13c-aa9d-c604-710c-bead3587948a" -> "OpaqueRef:4d293e68-1c2a-c6a8-bdd9-823abca394ba"; } ``` Signed-off-by: Andrii Sultanov <[email protected]>
1 parent 6617803 commit 7269b09

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

python3/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ install:
2424
$(IDATA) dnf_plugins/ptoken.py $(DESTDIR)$(SITE3_DIR)/$(DNF_PLUGIN_DIR)/
2525

2626
$(IPROG) libexec/metrics.py $(DESTDIR)$(OPTDIR)/debug
27+
$(IPROG) libexec/metricsgraph.py $(DESTDIR)$(OPTDIR)/debug
2728
$(IPROG) libexec/host-display $(DESTDIR)$(LIBEXECDIR)
2829
$(IPROG) libexec/link-vms-by-sr.py $(DESTDIR)$(LIBEXECDIR)
2930
$(IPROG) libexec/usb_reset.py $(DESTDIR)$(LIBEXECDIR)

python3/libexec/metricsgraph.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/python3
2+
3+
# program to run through all the interesting metrics (i.e. those that are related in some way to a resident VM)
4+
# and output a graph suitable for graphviz.
5+
# note that graphviz comments are /*C-style*/
6+
# the fundamental type here is a 'record dictionary', indexing object records by their opaque refs.
7+
8+
import atexit
9+
import contextlib
10+
from pprint import pprint, pformat
11+
12+
import XenAPI
13+
14+
15+
# given a set of object references of type 'type_string', return a
16+
# dictionary linking the references to the associated records
17+
def get_records(sx, object_set, type_string):
18+
dic = {}
19+
for x in object_set:
20+
dic[x] = sx.__getattr__(type_string).get_record(x)
21+
return dic
22+
23+
24+
# given a record dictionary, print out the 'name_labels' of each entry if it exists, and the reference otherwise
25+
def print_names(dictionary, title):
26+
print("/*")
27+
print(
28+
title,
29+
":",
30+
", ".join([dictionary[x].get("name_label", x) for x in dictionary.keys()]),
31+
)
32+
print("*/")
33+
34+
35+
# for example, the record dictionary of VMs contains a key VIFs, which is a list of the associated VIFs
36+
# this function will take say, the dictionary of all the resident VMs, and return the set of all the VIFs
37+
# associated with them
38+
def set_from_key_in_dictionary_of_records(record_dict, key_name, key_is_list=True):
39+
s = set()
40+
for v in record_dict.values():
41+
key = v[key_name]
42+
if key_is_list:
43+
s.update(key)
44+
else:
45+
s.add(key)
46+
return s
47+
48+
49+
# and this composition function will, say, given the VM dictionary, and the key name VIFs, return the
50+
# dictionary of all associated VIFs
51+
def chase_key(sx, record_dictionary, type_name, key_name, key_is_list=True):
52+
new_set = set_from_key_in_dictionary_of_records(
53+
record_dictionary, key_name, key_is_list
54+
)
55+
return get_records(sx, new_set, type_name)
56+
57+
58+
# The metrics records hold a lot of data. The following functions take a reference/record pair of a particular type
59+
# get the associated metrics, and return a few interesting quantities we want to see on the graphs
60+
def host_data(sx, ref, record):
61+
data = {}
62+
data["name"] = record["name_label"]
63+
metrics = sx.host_metrics.get_record(sx.host.get_metrics(ref))
64+
cpu_utilisations = [
65+
sx.host_cpu.get_utilisation(x) for x in sx.host.get_host_CPUs(ref)
66+
]
67+
data["label"] = {}
68+
data["label"]["cpus"] = " ".join(["%.2f" % x for x in cpu_utilisations])
69+
data["label"]["memory"] = "%.2f" % (
70+
float(metrics["memory_free"]) / float(metrics["memory_total"])
71+
)
72+
return data
73+
74+
75+
def vm_data(sx, ref, record):
76+
data = {}
77+
data["name"] = record["name_label"]
78+
metrics = sx.VM_metrics.get_record(sx.VM.get_metrics(ref))
79+
data["label"] = {}
80+
data["label"]["cpus"] = " ".join(
81+
["%.2f" % x for x in metrics["VCPUs_utilisation"].values()]
82+
)
83+
data["label"]["memory"] = "%sM" % (
84+
float(metrics["memory_actual"]) / float(1024 * 1024)
85+
)
86+
return data
87+
88+
89+
# vifs vbds and pifs all work the same way, but we need a type variable for the xapi bindings dispatcher
90+
def io_data(sx, ref, record, type_name, data_owner, suffixes):
91+
data = {}
92+
data["name"] = type_name.lower()
93+
obj_class = sx.__getattr__(type_name)
94+
owner_class = sx.__getattr__(data_owner)
95+
belongs_to = obj_class.__getattr__(f"get_{data_owner}")(ref)
96+
device_number = obj_class.__getattr__("get_device")(ref)
97+
metric_name = f"{type_name.lower()}_{device_number}"
98+
metrics = [
99+
x
100+
for x in owner_class.get_data_sources(belongs_to)
101+
if x["name_label"].startswith(metric_name)
102+
]
103+
metrics = {x["name_label"]: x["value"] for x in metrics}
104+
105+
data["label"] = {}
106+
data["label"]["read"] = "%.2fk" % (metrics[f"{metric_name}_{suffixes[0]}"])
107+
data["label"]["write"] = "%.2fk" % (metrics[f"{metric_name}_{suffixes[1]}"])
108+
return data
109+
110+
111+
# these functions use the object lists constructed and metric analysis functions defined above
112+
# to print out the node and edge definitions required by graphviz
113+
def print_nodes(record_dictionary):
114+
for x in record_dictionary.keys():
115+
print('"%s" [label="%s"];' % (x, record_dictionary[x].get("name_label", "?")))
116+
117+
118+
# calls the metric analysis functions to output nodes labelled with their metrics as well as their names
119+
def print_metric_nodes(sx, dic, metricfn):
120+
for ref, record in dic.items():
121+
d = metricfn(sx, ref, record)
122+
label = (
123+
d["name"]
124+
+ "\\n"
125+
+ "\\n".join(["%s=%s" % (k, v) for k, v in d["label"].items()])
126+
)
127+
print('"%s" [label="%s"];' % (ref, label))
128+
129+
130+
# prints out the connecting edges, in similar manner to the key-chasing above when we first got the objects
131+
def print_edges(record_dictionary, key, key_is_list=True):
132+
for k, v in record_dictionary.items():
133+
if key_is_list:
134+
for x in v[key]:
135+
print('"%s" -> "%s";' % (k, x))
136+
else:
137+
print('"%s" -> "%s";' % (k, v[key]))
138+
139+
140+
def main():
141+
session = XenAPI.xapi_local()
142+
143+
def logout():
144+
with contextlib.suppress(Exception):
145+
session.xenapi.session.logout()
146+
147+
atexit.register(logout)
148+
149+
session.xenapi.login_with_password("", "", "1.0", "newmetricsgraph-script")
150+
sx = session.xenapi
151+
152+
# find all the hosts
153+
host_dic = get_records(sx, set(sx.host.get_all()), "host")
154+
155+
# chase the chain of types through hosts->VMs->VIFs->networks->PIFs and hosts->VMs->VBDs
156+
resident_vms_dic = chase_key(sx, host_dic, "VM", "resident_VMs")
157+
active_vifs_dic = chase_key(sx, resident_vms_dic, "VIF", "VIFs")
158+
active_vbds_dic = chase_key(sx, resident_vms_dic, "VBD", "VBDs")
159+
active_networks_dic = chase_key(sx, active_vifs_dic, "network", "network", False)
160+
active_pifs_dic = chase_key(sx, active_networks_dic, "PIF", "PIFs")
161+
162+
# print out the objects we found as a graphviz comment
163+
print_names(host_dic, "hosts")
164+
print_names(resident_vms_dic, "resident VMs")
165+
print_names(active_vifs_dic, "active VIFs")
166+
print_names(active_vbds_dic, "active VBDs")
167+
print_names(active_networks_dic, "active networks")
168+
print_names(active_pifs_dic, "active PIFs")
169+
170+
# We've now got all the objects we need, so we now need to get their metrics data and output it as a graphviz file
171+
172+
print('digraph active_objects {')
173+
print('node [shape="rect"];')
174+
175+
print_metric_nodes(sx, host_dic, host_data)
176+
print_metric_nodes(sx, resident_vms_dic, vm_data)
177+
print_metric_nodes(
178+
sx,
179+
active_vifs_dic,
180+
lambda sx, ref, record: io_data(sx, ref, record, "VIF", "VM", ["rx", "tx"]),
181+
)
182+
print_metric_nodes(
183+
sx,
184+
active_vbds_dic,
185+
lambda sx, ref, record: io_data(
186+
sx, ref, record, "VBD", "VM", ["read", "write"]
187+
),
188+
)
189+
print_metric_nodes(
190+
sx,
191+
active_pifs_dic,
192+
lambda sx, ref, record: io_data(sx, ref, record, "PIF", "host", ["rx", "tx"]),
193+
)
194+
195+
print_nodes(active_networks_dic)
196+
197+
print_edges(host_dic, "resident_VMs")
198+
print_edges(resident_vms_dic, "VIFs")
199+
print_edges(resident_vms_dic, "VBDs")
200+
print_edges(active_vifs_dic, "network", False)
201+
print_edges(active_networks_dic, "PIFs")
202+
203+
print("}")
204+
205+
206+
if __name__ == "__main__":
207+
main()

0 commit comments

Comments
 (0)