I needed a tool to monitor VM's running on my server in relatively real-time (similar to top
or the many variants out there). The main things I need to keep track of are:
All VM's listed via
virsh list --all
;Id Name State ---------------------------------------------------- 13 Experiments-Proxy running - Experiments-PHP shut off - Experiments-Python shut off
All networks listed via
virsh net-list --all
;Name State Autostart Persistent ---------------------------------------------------------- default inactive yes yes net_10_1_1_0 active yes yes net_10_1_2_0 active yes yes net_10_1_3_0 active yes yes
All storage pools listed via
virsh pool-list --all
;Name State Autostart ------------------------------------------- Experiments active yes images active yes
To do this, I built a small Python script using curses that effectively does three things:
- Lists all the aforementioned components;
- Updates the list on a regular basis (every 2 seconds, basically);
- Allows basic management of the aforementioned components (start, stop);
All of this is rather simple, if long and convoluted.
To start with, I built a function that runs the virsh
command with the arguments I need to. I discarded the error output because I honestly don't care about it for this tool.
def virsh(command, arg):
out, _ = subprocess.Popen(['virsh', command, arg], stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
out = re.split('[\r\n]+', out.decode("utf-8"))
return list(map(lambda line: list(map(lambda x: x.strip(), re.split('\\s{2,}', line))), out))
This allows me to do something like the following further on in the script:
vms = virsh('list', '--all')[2:][:-1]
nets = virsh('net-list', '--all')[2:][:-1]
pools = virsh('pool-list', '--all')[2:][:-1]
Next, I needed a way to print a table in curses. This went relatively smooth as well, as all I did was push a list of columns and items into a function, with a few extra parameters:
def print_table(stdscr, head_color, sel_color, sel_i, x, y, cols, gray_sel, items):
total_len = sum(list(map(lambda col: col[1] + 1, cols)))
stdscr.insstr(y, x, ' ' * total_len, head_color)
col_offset = 0
if sel_i > -1:
stdscr.addstr(y + sel_i + 1, x, ' ' * total_len, sel_color)
c = 0
for (name, minsize, gray) in cols:
stdscr.addstr(y, x + col_offset, name, head_color)
i = 1
for item in items:
color_offset = 1 if sel_i == (i - 1) else 0
color = curses.color_pair(color_offset)
gray_color = curses.color_pair(color_offset + (3 if gray_sel(item) else 0))
stdscr.addstr(y + i, x + col_offset, item[c], gray_color if gray else color)
i += 1
col_offset += minsize + 1
c += 1
Next, I needed to print a "help" at the bottom of the screen. For this I simply list each keystroke / command, and a single word about what it does. I might have a list like [("TAB", "Next"), ("F1", "Start"), ("F2", "Stop"), ("F10", "Quit")]
:
def print_help(stdscr, help_color, helps):
height, width = stdscr.getmaxyx()
stdscr.insstr(height - 1, 0, ' ' * width, help_color)
max_len = max(list(map(lambda x: len(x[1]), helps))) + 1
offset = 0
for (key, name) in helps:
stdscr.insstr(height - 1, offset, key)
stdscr.insstr(height - 1, offset + len(key), name, help_color)
offset += len(key) + max_len
The next step is running all the logic to render the screen. For this, I built a render
function that takes all the parameters I need:
def set_x_for_yes(x): return 'X' if x == 'yes' else ' '
def render(stdscr, vms, nets, pools, sel, sel_i):
pool_diff = 2
longest_net = max(list(map(lambda net: len(net[0]), nets)))
longest_pool = max(list(map(lambda pool: len(pool[0]), pools)))
longest_net = max(longest_net, longest_pool - pool_diff)
height, width = stdscr.getmaxyx()
net_offset = width - longest_net - 9 - pool_diff - 3
vm_width = net_offset - 3 - 9 - 1 - 2
vm_table = [("ID", 3, False), ("VM", vm_width - 1, True), ("STATUS", 9, False)]
net_table = [("NET", longest_net, True), ("STATUS", 8, False), ("A", 1, False), ("P", 1, False)]
pool_table = [("POOL", longest_net + pool_diff, True), ("STATUS", 8, False), ("A", 1, False)]
nets = list(map(lambda net: [net[0], net[1], set_x_for_yes(net[2]), set_x_for_yes(net[3])], nets))
pools = list(map(lambda pool: [pool[0], pool[1], set_x_for_yes(pool[2])], pools))
tables = [
(0, 0, 0, vm_table, lambda vm: vm[2] != "running", vms),
(1, net_offset, 0, net_table, lambda net: net[1] != "active", nets),
(2, net_offset, len(nets) + 2, pool_table, lambda pool: pool[1] != "active", pools)
]
head_color = curses.color_pair(2)
sel_color = curses.color_pair(1)
for (sel_c, x, y, table, sel_test, items) in tables:
print_table(stdscr, head_color, sel_color, sel_i if sel == sel_c else -1, x, y, table, sel_test, items)
print_help(
stdscr,
curses.color_pair(1),
[("TAB", "Next"), ("F1", "Start"), ("F2", "Stop"), ("F10", "Quit")])
This builds up all the components to pass to the rendering functions.
Lastly, I have a main
function that I use curses.wrapper
to run. This allows curses to setup all the screen components, and clean the screen up when it ends (either with success or failure):
def main(stdscr):
curses.curs_set(0)
curses.halfdelay(20)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, 0, 6)
curses.init_pair(2, 0, 2)
curses.init_pair(3, 8, -1)
curses.init_pair(4, 8, 6)
sel = 0
sel_i = 0
start_commands = ['start', 'net-start', 'pool-start']
stop_commands = ['destroy', 'net-destroy', 'pool-destroy']
while True:
vms = virsh('list', '--all')[2:][:-1]
nets = virsh('net-list', '--all')[2:][:-1]
pools = virsh('pool-list', '--all')[2:][:-1]
args = [vms, nets, pools]
arg_indexes = [1, 0, 0]
stdscr.clear()
render(stdscr, vms, nets, pools, sel, sel_i)
stdscr.refresh()
c = stdscr.getch()
if c == curses.KEY_F10:
exit()
elif c == ord('\t'):
sel = 0 if sel == 2 else sel + 1
elif c == curses.KEY_DOWN or c == curses.KEY_UP:
sel_i += -1 if c == curses.KEY_UP else 1
elif (c == curses.KEY_F1 or c == curses.KEY_F2) and sel_i < len(args[sel]):
commands = stop_commands if c == curses.KEY_F2 else start_commands
virsh(commands[sel], args[sel][sel_i][arg_indexes[sel]])
if sel_i == -1:
sel_i += 1
if sel_i >= len(args[sel]):
sel_i = len(args[sel]) - 1
curses.wrapper(main)
This also has all the key-handling logic to adjust the scene.
I have yet to set up scrolling on each table, but that is beyond the scope of this question.
Once all is said and done, running the script gives me an output of the following:
Any and all comments welcome. I don't have any PEP-8 flags in PyChar, so I'm thinking I'm already off to a good start here.