import signal
import time
import math
+import threading
from collections import OrderedDict
from datetime import datetime
from enum import Enum, unique
-from threading import Event
import rados
last_read_size = {}
last_write_size = {}
+fs_list = []
+
def calc_perc(c):
if c[0] == 0 and c[1] == 0:
def __init__(self, args):
self.rados = None
self.stdscr = None # curses instance
+ self.flag = 1
self.client_name = args.id
self.cluster_name = args.cluster
self.conffile = args.conffile
self.refresh_interval_secs = args.delay
- self.exit_ev = Event()
+ self.exit_ev = threading.Event()
def refresh_window_size(self):
self.height, self.width = self.stdscr.getmaxyx()
raise FSTopException('Cannot handle unknown metrics from'
f'\'ceph fs perf stats\': {missing}')
+ def get_fs_names(self):
+ mon_cmd = {'prefix': 'fs ls', 'format': 'json'}
+ try:
+ ret, buf, out = self.rados.mon_command(json.dumps(mon_cmd), b'')
+ except Exception as e:
+ raise FSTopException(f'Error in fs ls: {e}')
+ fs_map = json.loads(buf.decode('utf-8'))
+ global fs_list
+ fs_list.clear()
+ for filesystem in fs_map:
+ fs = filesystem['name']
+ fs_list.append(fs)
+ return fs_list
+
def setup_curses(self, win):
self.stdscr = win
curses.use_default_colors()
# If the terminal do not support the visibility
# requested it will raise an exception
pass
- self.run_display()
+ self.run_all_display()
+
+ def display_menu(self, stdscr, selected_row_idx):
+ stdscr.clear()
+ h, w = stdscr.getmaxyx()
+ global fs_list
+ if not fs_list:
+ title = ['No filesystem available',
+ 'Press "q" to go back to home (all filesystem info) screen']
+ pos_x1 = w // 2 - len(title[0]) // 2
+ pos_x2 = w // 2 - len(title[1]) // 2
+ stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+ stdscr.addstr(3, pos_x2, title[1])
+ else:
+ title = ['Filesystems', 'Press "q" to go back to home (all filesystem info) screen']
+ pos_x1 = w // 2 - len(title[0]) // 2
+ pos_x2 = w // 2 - len(title[1]) // 2
+ stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+ stdscr.addstr(3, pos_x2, title[1])
+ for index, name in enumerate(fs_list):
+ x = w // 2 - len(name) // 2
+ y = h // 2 - len(fs_list) // 2 + index
+ if index == selected_row_idx:
+ stdscr.attron(curses.color_pair(1))
+ stdscr.addstr(y, x, name)
+ stdscr.attroff(curses.color_pair(1))
+ else:
+ stdscr.addstr(y, x, name)
+ stdscr.refresh()
+
+ def set_key(self, stdscr):
+ curses.curs_set(0)
+ curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ curr_row = 0
+ key = 0
+ endmenu = False
+ while not endmenu:
+ global fs_list
+ fs_list = self.get_fs_names()
+
+ if key == curses.KEY_UP and curr_row > 0:
+ curr_row -= 1
+ elif key == curses.KEY_DOWN and curr_row < len(fs_list) - 1:
+ curr_row += 1
+ elif (key in [curses.KEY_ENTER, 10, 13]) and fs_list:
+ self.run_display(fs_list[curr_row])
+ endmenu = True
+ elif key == ord('q'):
+ self.run_all_display()
+ endmenu = True
+
+ try:
+ self.display_menu(stdscr, curr_row)
+ except curses.error:
+ pass
+ curses.halfdelay(self.refresh_interval_secs)
+ key = stdscr.getch()
+
+ def set_option(self, opt):
+ if opt == ord('m'):
+ curses.wrapper(self.set_key)
+ elif opt == ord('q'):
+ if self.flag == 1:
+ quit()
+ else:
+ self.run_all_display()
def verify_perf_stats_support(self):
mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
xp += nlen
title = ITEMS_PAD.join(heading)
hlen = min(self.width - 2, len(title))
- self.topl.addnstr(0, 0, title, hlen, curses.A_STANDOUT | curses.A_BOLD)
+ self.topl.addnstr(2, 0, title, hlen, curses.A_STANDOUT | curses.A_BOLD)
self.topl.refresh()
return x_coord_map
if remaining_hlen == 0:
return
- def refresh_clients(self, x_coord_map, stats_json):
+ def refresh_clients(self, x_coord_map, stats_json, fs_name):
counters = [m.upper() for m in stats_json[GLOBAL_COUNTERS_KEY]]
y_coord = 0
- for client_id, metrics in stats_json[GLOBAL_METRICS_KEY].items():
- self.refresh_client(client_id,
- metrics,
- counters,
- stats_json[CLIENT_METADATA_KEY][client_id],
- x_coord_map,
- y_coord)
- y_coord += 1
-
- def refresh_main_window(self, x_coord_map, stats_json):
+ hlen = min(self.width - 2, len(FS_TOP_NAME_TOPL_FMT))
+ res = stats_json[CLIENT_METADATA_KEY].get(fs_name, {})
+ client_cnt = len(res)
+ self.topl.addnstr(0, 0, FS_TOP_NAME_TOPL_FMT.format(
+ fs_name=fs_name, client_count=client_cnt), hlen)
+ self.topl.refresh()
+ if client_cnt:
+ for client_id, metrics in \
+ stats_json[GLOBAL_METRICS_KEY][fs_name].items():
+ self.refresh_client(
+ client_id, metrics, counters, res[client_id],
+ x_coord_map, y_coord)
+ y_coord += 1
+
+ def refresh_main_window(self, x_coord_map, stats_json, fs_name):
if self.mainw is None:
return
- self.refresh_clients(x_coord_map, stats_json)
+ self.refresh_clients(x_coord_map, stats_json, fs_name)
self.mainw.refresh()
- def refresh_header(self, stats_json):
+ def refresh_header(self, stats_json, headline):
hlen = self.width - 2
+ num_clients, num_mounts, num_kclients, num_libs = 0, 0, 0, 0
if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
self.header.addnstr(0, 0, 'perf stats version mismatch!', hlen)
return False
- client_metadata = stats_json[CLIENT_METADATA_KEY]
- num_clients = len(client_metadata)
- num_mounts = len([client for client, metadata in client_metadata.items() if
- CLIENT_METADATA_MOUNT_POINT_KEY in metadata
- and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
- num_kclients = len([client for client, metadata in client_metadata.items() if
- "kernel_version" in metadata])
- num_libs = num_clients - (num_mounts + num_kclients)
+ global fs_list
+ for fs_name in fs_list:
+ client_metadata = stats_json[CLIENT_METADATA_KEY].get(fs_name, {})
+ client_cnt = len(client_metadata)
+ if client_cnt:
+ num_clients = num_clients + client_cnt
+ num_mounts = num_mounts + len(
+ [client for client, metadata in client_metadata.items() if
+ CLIENT_METADATA_MOUNT_POINT_KEY in metadata
+ and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
+ num_kclients = num_kclients + len(
+ [client for client, metadata in client_metadata.items() if
+ "kernel_version" in metadata])
+ num_libs = num_clients - (num_mounts + num_kclients)
now = datetime.now().ctime()
- self.header.addnstr(0, 0,
- FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR, now=now),
+ self.header.addnstr(0, 0, FS_TOP_VERSION_HEADER_FMT.format(
+ prog_name=FS_TOP_PROG_STR, now=now),
hlen, curses.A_STANDOUT | curses.A_BOLD)
- self.header.addnstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
- num_mounts=num_mounts,
- num_kclients=num_kclients,
- num_libs=num_libs), hlen)
+ self.header.addnstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(
+ num_clients=num_clients, num_mounts=num_mounts,
+ num_kclients=num_kclients, num_libs=num_libs), hlen)
+ self.header.addnstr(2, 0, headline, hlen)
self.header.refresh()
return True
- def run_display(self):
+ def run_display(self, fs):
while not self.exit_ev.is_set():
# use stdscr.clear() instead of clearing each window
# to avoid screen blinking.
self.stdscr.clear()
self.refresh_window_size()
- if self.width <= 2 or self.width <= 2:
- self.exit_ev.wait(timeout=self.refresh_interval_secs)
- continue
+
+ global fs_list
+ fs_list = self.get_fs_names()
+ stats_json = self.perf_stats_query()
+ self.flag = 0
+ help_text = " [Press 'q' to go back to home (all filesystem info) screen"\
+ " and 'm' to select another filesystem]"
# coordinate constants for windowing -- (height, width, y, x)
# NOTE: requires initscr() call before accessing COLS, LINES.
try:
- HEADER_WINDOW_COORD = (2, self.width - 1, 0, 0)
+ HEADER_WINDOW_COORD = (4, self.width - 1, 0, 0)
self.header = curses.newwin(*HEADER_WINDOW_COORD)
- if self.height >= 3:
- TOPLINE_WINDOW_COORD = (1, self.width - 1, 3, 0)
- self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
- else:
- self.topl = None
- if self.height >= 5:
- MAIN_WINDOW_COORD = (self.height - 4, self.width - 1, 4, 0)
- self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
+ if fs not in fs_list:
+ self.stdscr.refresh()
+ headline = "INFO: Selected filesystem is not available now" + help_text
+ self.refresh_header(stats_json, headline)
else:
- self.mainw = None
+ if self.height >= 5:
+ TOPLINE_WINDOW_COORD = (3, self.width - 1, 4, 0)
+ self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
+ else:
+ self.topl = None
+ if self.height >= 8:
+ MAIN_WINDOW_COORD = (self.height - 7, self.width - 1, 7, 0)
+ self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
+ else:
+ self.mainw = None
+ headline = "HELP: Selected filesystem info" + help_text
+ if self.refresh_header(stats_json, headline):
+ x_coord_map = self.refresh_top_line_and_build_coord()
+ self.refresh_main_window(x_coord_map, stats_json, fs)
+ curses.halfdelay(self.refresh_interval_secs * 10)
+ c = self.header.getch()
+ if c in [ord('m'), ord('q')]:
+ self.set_option(c)
except curses.error:
- # this may happen when creating the sub windows the
- # terminal window size changed, just retry it
- continue
+ # this may happen when addstr the terminal window
+ # size changed, just retry it
+ pass
+ def run_all_display(self):
+ while not self.exit_ev.is_set():
+ # use stdscr.clear() instead of clearing each window
+ # to avoid screen blinking.
+ self.stdscr.clear()
+ self.refresh_window_size()
+
+ self.flag = 1
+ global fs_list
+ fs_list = self.get_fs_names()
stats_json = self.perf_stats_query()
+
+ # coordinate constants for windowing -- (height, width, y, x)
+ # NOTE: requires initscr() call before accessing COLS, LINES.
try:
- if self.refresh_header(stats_json):
- x_coord_map = self.refresh_top_line_and_build_coord()
- self.refresh_main_window(x_coord_map, stats_json)
- self.exit_ev.wait(timeout=self.refresh_interval_secs)
+ HEADER_WINDOW_COORD = (4, self.width - 1, 0, 0)
+ self.header = curses.newwin(*HEADER_WINDOW_COORD)
+ if not fs_list:
+ self.stdscr.refresh()
+ headline = "INFO: No filesystem is available"\
+ " [Press 'q' to quit]"
+ self.refresh_header(stats_json, headline)
+ else:
+ main, top, num_client = 7, 4, 0
+ for fs in fs_list:
+ if self.height >= 5 + num_client:
+ TOPLINE_WINDOW_COORD = (3, self.width - 1, top, 0)
+ self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
+ else:
+ self.topl = None
+ if self.height >= 8 + num_client:
+ MAIN_WINDOW_COORD = (self.height - 7, self.width - 1,
+ main, 0)
+ self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
+ else:
+ self.mainw = None
+ client_metadata = stats_json[CLIENT_METADATA_KEY].get(fs, {})
+ num_client = len(client_metadata)
+ top = top + (num_client + 4)
+ main = main + (num_client + 4)
+ headline = "HELP: All filesystem info"\
+ " [Press 'm' to select a filesystem and 'q' to quit]"
+ if self.refresh_header(stats_json, headline):
+ x_coord_map = self.refresh_top_line_and_build_coord()
+ self.refresh_main_window(x_coord_map, stats_json, fs)
+ curses.halfdelay(self.refresh_interval_secs * 10)
+ c = self.header.getch()
+ if c in [ord('m'), ord('q')]:
+ self.set_option(c)
except curses.error:
# this may happen when addstr the terminal window
# size changed, just retry it
parser.add_argument('-d', '--delay', nargs='?',
default=DEFAULT_REFRESH_INTERVAL,
type=float_greater_than,
- help='Interval to refresh data '
+ help='Refresh interval in seconds '
f'(default: {DEFAULT_REFRESH_INTERVAL})')
args = parser.parse_args()