Skip to content

Commit bdafbae

Browse files
committed
Add top-like monitoring tool
1 parent 664df47 commit bdafbae

File tree

3 files changed

+129
-0
lines changed

3 files changed

+129
-0
lines changed

qubes-rpc/qubes.GetMem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
# Return percentage of available memory.
3+
set -eu
4+
awk -- '
5+
BEGIN {total=0; available=0}
6+
/MemTotal/ {total=$2}
7+
/MemAvailable/ {available=$2}
8+
END {printf "%.0f", (available/total)*100}
9+
' /proc/meminfo

qubes-rpc/qvm-top

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
3+
## SPDX-FileCopyrightText: 2025 Benjamin Grande M. S. <ben.grande.b@gmail.com>
4+
##
5+
## SPDX-License-Identifier: GPL-2.0-only
6+
7+
"""
8+
Top-like info for Qubes.
9+
"""
10+
11+
import curses
12+
import time
13+
import subprocess
14+
from qubesadmin import Qubes
15+
16+
def get_xentop_data():
17+
"""
18+
Read data from xentop.
19+
"""
20+
## Only the 2nd iteration and after 1 second it shows the correct info.
21+
output = subprocess.check_output("xentop -bfi2d1", shell=True, text=True)
22+
lines = output.strip().split("\n")
23+
data = {}
24+
for line in lines:
25+
parts = line.split()
26+
if parts[1] == "STATE":
27+
continue
28+
if len(parts) >= 4:
29+
name = parts[0]
30+
if name == "Domain-0":
31+
name = "dom0"
32+
cpu_usage = int(float(parts[3]))
33+
data[name] = cpu_usage
34+
return data
35+
36+
37+
def get_filtered_qubes() -> list:
38+
"""
39+
Get information from qubes.
40+
"""
41+
qube_list = []
42+
xentop_data = get_xentop_data()
43+
for qube in Qubes().domains:
44+
power_state = qube.get_power_state()
45+
if power_state == "Halted":
46+
continue
47+
cpu_usage = xentop_data.get(qube.name, "ERROR")
48+
cpu_usage = f"{cpu_usage}%"
49+
if power_state != "Running":
50+
mem_usage = "NA"
51+
else:
52+
try:
53+
filter_esc = bool(qube.name != "dom0")
54+
untrusted_mem, _ = qube.run_service_for_stdio(
55+
"qubes.GetMem", user="root", stderr=None,
56+
filter_esc=filter_esc, wait=True
57+
)
58+
untrusted_mem = untrusted_mem.decode("ascii", errors="ignore")
59+
untrusted_mem = str(untrusted_mem.strip())
60+
if len(untrusted_mem) != 2 or not untrusted_mem.isdigit():
61+
raise ValueError
62+
mem_usage = f"{untrusted_mem}%"
63+
except ValueError:
64+
mem_usage = "ERROR"
65+
except subprocess.CalledProcessError:
66+
mem_usage = "NA"
67+
qube_list.insert(0 if qube.name == "dom0" else len(qube_list), {
68+
"name": qube.name,
69+
"status": power_state,
70+
"mem_usage": mem_usage,
71+
"cpu_usage": cpu_usage,
72+
})
73+
return qube_list
74+
75+
76+
def draw_table(stdscr, qubes):
77+
"""
78+
Draw a top-like table about qubes statuses.
79+
"""
80+
stdscr.clear()
81+
height, _ = stdscr.getmaxyx()
82+
83+
stdscr.attron(curses.A_BOLD)
84+
stdscr.attron(curses.A_REVERSE)
85+
stdscr.addstr(0, 0, "Qube".ljust(40))
86+
stdscr.addstr(0, 40, "State".ljust(50))
87+
stdscr.addstr(0, 51, "MEM(%)".ljust(57))
88+
stdscr.addstr(0, 59, "CPU(%)".ljust(64))
89+
stdscr.attroff(curses.A_BOLD)
90+
stdscr.attroff(curses.A_REVERSE)
91+
92+
for i, qube in enumerate(qubes, start=1):
93+
if i >= height - 1:
94+
break
95+
stdscr.addstr(i, 0, qube["name"].ljust(40))
96+
stdscr.addstr(i, 40, qube["status"].ljust(50))
97+
stdscr.addstr(i, 51, qube["mem_usage"].rjust(6))
98+
stdscr.addstr(i, 59, qube["cpu_usage"].rjust(6))
99+
100+
current_time = time.strftime('%Y-%m-%d %H:%M:%S')
101+
stdscr.addstr(height-1, 0, f"qvm-top - {current_time}")
102+
stdscr.refresh()
103+
104+
105+
def main(stdscr) -> None: # pylint:disable=missing-function-docstring
106+
curses.curs_set(0)
107+
stdscr.clear()
108+
stdscr.timeout(0)
109+
110+
while True:
111+
filtered_qubes = get_filtered_qubes()
112+
draw_table(stdscr, filtered_qubes)
113+
key = stdscr.getch()
114+
if key in [ord("q"), ord("Q"), 27]:
115+
break
116+
117+
118+
if __name__ == "__main__":
119+
curses.wrapper(main)

rpm_spec/core-agent.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,7 @@ rm -f %{name}-%{version}
897897
%config(noreplace) /etc/qubes-rpc/qubes.Filecopy
898898
%config(noreplace) /etc/qubes-rpc/qubes.OpenInVM
899899
%config(noreplace) /etc/qubes-rpc/qubes.OpenURL
900+
%config(noreplace) /etc/qubes-rpc/qubes.GetMem
900901
%config(noreplace) /etc/qubes-rpc/qubes.GetAppmenus
901902
%config(noreplace) /etc/qubes-rpc/qubes.ConnectTCP
902903
%config(noreplace) /etc/qubes-rpc/qubes.VMShell

0 commit comments

Comments
 (0)