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
+from curses import ascii
import rados
FS_TOP_PROG_STR = 'cephfs-top'
+FS_TOP_ALL_FS_APP = 'ALL_FS_APP'
+FS_TOP_FS_SELECTED_APP = 'SELECTED_FS_APP'
# version match b/w fstop and stats emitted by mgr/stats
-FS_TOP_SUPPORTED_VER = 1
+FS_TOP_SUPPORTED_VER = 2
-ITEMS_PAD_LEN = 1
+ITEMS_PAD_LEN = 3
ITEMS_PAD = " " * ITEMS_PAD_LEN
DEFAULT_REFRESH_INTERVAL = 1
# min refresh interval allowed
MGR_STATS_COUNTERS = list(MAIN_WINDOW_TOP_LINE_METRICS.keys())
FS_TOP_VERSION_HEADER_FMT = '{prog_name} - {now}'
-FS_TOP_CLIENT_HEADER_FMT = 'Client(s): {num_clients} - {num_mounts} FUSE, '\
- '{num_kclients} kclient, {num_libs} libcephfs'
+FS_TOP_CLIENT_HEADER_FMT = 'Total Client(s): {num_clients} - '\
+ '{num_mounts} FUSE, {num_kclients} kclient, {num_libs} libcephfs'
+FS_TOP_NAME_TOPL_FMT = 'Filesystem: {fs_name} - {client_count} client(s)'
CLIENT_METADATA_KEY = "client_metadata"
CLIENT_METADATA_MOUNT_POINT_KEY = "mount_point"
last_read_size = {}
last_write_size = {}
+fs_list = []
+# store the current states of cephfs-top
+# last_fs : last filesystem visited
+# last_field : last field selected for sorting
+# limit : last limit value
+current_states = {"last_fs": "", "last_field": 'chit', "limit": None}
+metrics_dict = {}
+
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.current_screen = ""
self.client_name = args.id
self.cluster_name = args.cluster
self.conffile = args.conffile
self.refresh_interval_secs = args.delay
- self.exit_ev = Event()
-
- def refresh_window_size(self):
- self.height, self.width = self.stdscr.getmaxyx()
+ self.PAD_HEIGHT = 10000 # height of the fstop_pad
+ self.PAD_WIDTH = 300 # width of the fstop_pad
+ self.exit_ev = threading.Event()
def handle_signal(self, signum, _):
self.exit_ev.set()
def init(self):
try:
if self.conffile:
- r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name,
+ r_rados = rados.Rados(rados_id=self.client_name,
+ clustername=self.cluster_name,
conffile=self.conffile)
else:
- r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name)
+ r_rados = rados.Rados(rados_id=self.client_name,
+ clustername=self.cluster_name)
r_rados.conf_read_file()
r_rados.connect()
self.rados = r_rados
except rados.Error as e:
if e.errno == errno.ENOENT:
- raise FSTopException(f'cluster {self.cluster_name} does not exist')
+ raise FSTopException(f'cluster {self.cluster_name}'
+ ' does not exist')
else:
raise FSTopException(f'error connecting to cluster: {e}')
self.verify_perf_stats_support()
stats_json = self.perf_stats_query()
if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
raise FSTopException('perf stats version mismatch!')
- missing = [m for m in stats_json["global_counters"] if m.upper() not in MGR_STATS_COUNTERS]
+ missing = [m for m in stats_json["global_counters"]
+ if m.upper() not in MGR_STATS_COUNTERS]
if missing:
- raise FSTopException('Cannot handle unknown metrics from \'ceph fs perf stats\': '
- f'{missing}')
+ 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
+ self.stdscr.keypad(True)
curses.use_default_colors()
curses.start_color()
try:
# If the terminal do not support the visibility
# requested it will raise an exception
pass
- self.run_display()
+ self.fstop_pad = curses.newpad(self.PAD_HEIGHT, self.PAD_WIDTH)
+ self.run_all_display()
+
+ def display_fs_menu(self, stdscr, selected_row_idx):
+ stdscr.clear()
+ h, w = stdscr.getmaxyx()
+ title = ['Filesystems', 'Press "q" to go back to the previous 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], curses.A_DIM)
+ 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 display_sort_menu(self, stdscr, selected_row_idx, field_menu):
+ stdscr.clear()
+ title = ['Fields', 'Press "q" to go back to the previous screen']
+ pos_x1 = 0
+ pos_x2 = 0
+ stdscr.addstr(1, pos_x1, title[0], curses.A_STANDOUT | curses.A_BOLD)
+ stdscr.addstr(3, pos_x2, title[1], curses.A_DIM)
+ for index, name in enumerate(field_menu):
+ x = 0
+ y = 5 + 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 display_menu(self, stdscr):
+ stdscr.clear()
+ h, w = stdscr.getmaxyx()
+ 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], curses.A_DIM)
+ stdscr.refresh()
+
+ def set_key(self, stdscr):
+ curses.curs_set(0)
+ curses.init_pair(1, curses.COLOR_MAGENTA, 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.stdscr.erase()
+ current_states['last_fs'] = fs_list[curr_row]
+ self.run_display()
+ endmenu = True
+ elif key == ord('q'):
+ self.stdscr.erase()
+ if isinstance(current_states['last_fs'], list):
+ self.run_all_display()
+ else:
+ self.run_display()
+ endmenu = True
+
+ try:
+ if not fs_list:
+ self.display_menu(stdscr)
+ else:
+ self.display_fs_menu(stdscr, curr_row)
+ except curses.error:
+ pass
+ curses.halfdelay(self.refresh_interval_secs)
+ key = stdscr.getch()
+
+ def choose_field(self, stdscr):
+ curses.curs_set(0)
+ curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
+ field_menu = ["chit= CAP_HIT", "dlease= DENTRY_LEASE", "ofiles= OPENED_FILES",
+ "oicaps= PINNED_ICAPS", "oinodes= OPENED_INODES",
+ "rtio= READ_IO_SIZES", "raio= READ_AVG_IO_SIZES",
+ "rsp= READ_IO_SPEED", "wtio= WRITE_IO_SIZES",
+ "waio= WRITE_AVG_IO_SIZES", "wsp= WRITE_IO_SPEED",
+ "rlatavg= AVG_READ_LATENCY", "rlatsd= STDEV_READ_LATENCY",
+ "wlatavg= AVG_WRITE_LATENCY", "wlatsd= STDEV_WRITE_LATENCY",
+ "mlatavg= AVG_METADATA_LATENCY", "mlatsd= STDEV_METADATA_LATENCY",
+ "Default"]
+ curr_row1 = 0
+ key = 0
+ endwhile = False
+ while not endwhile:
+ global current_states
+
+ if key == curses.KEY_UP and curr_row1 > 0:
+ curr_row1 -= 1
+ elif key == curses.KEY_DOWN and curr_row1 < len(field_menu) - 1:
+ curr_row1 += 1
+ elif key == curses.KEY_ENTER or key in [10, 13]:
+ self.stdscr.erase()
+ last_fs = current_states["last_fs"]
+ if curr_row1 != len(field_menu) - 1:
+ current_states["last_field"] = (field_menu[curr_row1].split('='))[0]
+ else:
+ current_states["last_field"] = 'chit'
+ if isinstance(last_fs, list):
+ self.run_all_display()
+ else:
+ self.run_display()
+ endwhile = True
+ elif key == ord('q'):
+ self.stdscr.erase()
+ if isinstance(current_states['last_fs'], list):
+ self.run_all_display()
+ else:
+ self.run_display()
+ endwhile = True
+
+ try:
+ if not fs_list:
+ self.display_menu(stdscr)
+ else:
+ self.display_sort_menu(stdscr, curr_row1, field_menu)
+ except curses.error:
+ pass
+ curses.halfdelay(self.refresh_interval_secs)
+ key = stdscr.getch()
+
+ def set_limit(self, stdscr):
+ key = ''
+ endwhile = False
+ while not endwhile:
+ stdscr.clear()
+ h, w = stdscr.getmaxyx()
+ title = 'Enter the limit you want to set (number) and press ENTER,'\
+ ' press "d" for default, "q" to go back to previous screen '
+ pos_x1 = w // 2 - len(title) // 2
+ try:
+ stdscr.addstr(1, pos_x1, title, curses.A_STANDOUT | curses.A_BOLD)
+ except curses.error:
+ pass
+ curses.halfdelay(self.refresh_interval_secs)
+ inp = stdscr.getch()
+ if inp in [ord('d'), ord('q')] or ascii.isdigit(inp):
+ key = key + chr(inp)
+ if key == 'd':
+ current_states["limit"] = None
+ elif key == 'q':
+ endwhile = True
+ elif (key).isnumeric():
+ i = 1
+ length = 4
+ while i <= length:
+ pos = w // 2 - len(key) // 2
+ try:
+ stdscr.move(3, 0)
+ stdscr.clrtoeol()
+ stdscr.addstr(3, pos, key, curses.A_BOLD)
+ except curses.error:
+ pass
+ if key[i - 1] == '\n':
+ break
+ inp = stdscr.getch()
+ if inp == ord('q'):
+ if current_states['limit'] is None:
+ key = current_states["limit"]
+ else:
+ key = current_states['limit'] + " "
+ break
+ if inp == curses.KEY_RESIZE:
+ stdscr.clear()
+ windowsize = stdscr.getmaxyx()
+ wd = windowsize[1] - 1
+ pos_x1 = wd // 2 - len(title) // 2
+ try:
+ stdscr.addstr(1, pos_x1, title, curses.A_STANDOUT | curses.A_BOLD)
+ except curses.error:
+ pass
+ if inp == curses.KEY_BACKSPACE or inp == curses.KEY_DC or inp == 127:
+ if i > 1:
+ key = key[:-1]
+ i = i - 1
+ stdscr.move(4, 0)
+ stdscr.clrtoeol()
+ elif i == 1:
+ curses.wrapper(self.set_limit)
+ elif i == length:
+ if inp == ord('\n'):
+ key = key + chr(inp)
+ i = i + 1
+ else:
+ info = "Max length is reached, press Backspace" \
+ " to edit or Enter to set the limit!"
+ pos = w // 2 - len(info) // 2
+ try:
+ stdscr.addstr(4, pos, info, curses.A_BOLD)
+ except curses.error:
+ pass
+ elif ascii.isdigit(inp) or inp == ord('\n'):
+ key = key + chr(inp)
+ i = i + 1
+ if key is None:
+ current_states["limit"] = key
+ elif int(key) != 0:
+ current_states["limit"] = key[:-1]
+ self.stdscr.erase()
+ if isinstance(current_states['last_fs'], list):
+ self.run_all_display()
+ else:
+ self.run_display()
+
+ def set_option(self, opt):
+ if opt == ord('m'):
+ if fs_list:
+ curses.wrapper(self.set_key)
+ else:
+ return False
+ elif opt == ord('s'):
+ if fs_list:
+ curses.wrapper(self.choose_field)
+ else:
+ return False
+ elif opt == ord('l'):
+ if fs_list:
+ curses.wrapper(self.set_limit)
+ else:
+ return False
+ elif opt == ord('r'):
+ current_states['last_field'] = 'chit'
+ current_states["limit"] = None
+ if isinstance(current_states['last_fs'], list):
+ self.run_all_display()
+ else:
+ self.run_display()
+ elif opt == ord('q'):
+ if self.current_screen == FS_TOP_ALL_FS_APP:
+ quit()
+ else:
+ self.run_all_display()
+ return True
def verify_perf_stats_support(self):
mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
if ret != 0:
raise FSTopException(f'error checking \'stats\' module: {out}')
if 'stats' not in json.loads(buf.decode('utf-8'))['enabled_modules']:
- raise FSTopException('\'stats\' module not enabled. Use \'ceph mgr module '
- 'enable stats\' to enable')
+ raise FSTopException('\'stats\' module not enabled. Use'
+ '\'ceph mgr module enable stats\' to enable')
def perf_stats_query(self):
mgr_cmd = {'prefix': 'fs perf stats', 'format': 'json'}
# return empty string for none type
return ''
- def refresh_top_line_and_build_coord(self):
- if self.topl is None:
- return
+ @staticmethod
+ def has_metric(metadata, metrics_key):
+ return metrics_key in metadata
+ @staticmethod
+ def has_metrics(metadata, metrics_keys):
+ for key in metrics_keys:
+ if not FSTop.has_metric(metadata, key):
+ return False
+ return True
+
+ def create_top_line_and_build_coord(self):
xp = 0
x_coord_map = {}
x_coord_map[item] = (xp, nlen)
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.refresh()
+ self.fsstats.addstr(self.tablehead_y, 0, title, curses.A_STANDOUT | curses.A_BOLD)
return x_coord_map
- @staticmethod
- def has_metric(metadata, metrics_key):
- return metrics_key in metadata
-
- @staticmethod
- def has_metrics(metadata, metrics_keys):
- for key in metrics_keys:
- if not FSTop.has_metric(metadata, key):
- return False
- return True
-
- def refresh_client(self, client_id, metrics, counters, client_meta, x_coord_map, y_coord):
+ def create_client(self, fs_name, client_id, metrics, counters,
+ client_meta, x_coord_map, y_coord):
global last_time
- size = 0
+ metrics_dict.setdefault(fs_name, {})
+ metrics_dict[fs_name].setdefault(client_id, {})
cur_time = time.time()
duration = cur_time - last_time
last_time = cur_time
- remaining_hlen = self.width - 1
for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
coord = x_coord_map[item]
- hlen = coord[1] - len(ITEMS_PAD)
- hlen = min(hlen, remaining_hlen)
- if remaining_hlen < coord[1]:
- remaining_hlen = 0
- else:
- remaining_hlen -= coord[1]
+ hlen = coord[1] - 1
if item == FS_TOP_MAIN_WINDOW_COL_CLIENT_ID:
- self.mainw.addnstr(y_coord, coord[0],
- wrap(client_id.split('.')[1], hlen),
- hlen)
+ self.fsstats.addstr(y_coord, coord[0],
+ wrap(client_id.split('.')[1], hlen), curses.A_DIM)
elif item == FS_TOP_MAIN_WINDOW_COL_MNT_ROOT:
- if FSTop.has_metric(client_meta, CLIENT_METADATA_MOUNT_ROOT_KEY):
- self.mainw.addnstr(y_coord, coord[0],
- wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen),
- hlen)
+ if FSTop.has_metric(client_meta,
+ CLIENT_METADATA_MOUNT_ROOT_KEY):
+ self.fsstats.addstr(
+ y_coord, coord[0],
+ wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen), curses.A_DIM)
else:
- self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
-
- if remaining_hlen == 0:
- return
+ self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
cidx = 0
for item in counters:
cidx += 1
continue
coord = x_coord_map[item]
- hlen = coord[1] - len(ITEMS_PAD)
- hlen = min(hlen, remaining_hlen)
- if remaining_hlen < coord[1]:
- remaining_hlen = 0
- else:
- remaining_hlen -= coord[1]
m = metrics[cidx]
key = MGR_STATS_COUNTERS[cidx]
typ = MAIN_WINDOW_TOP_LINE_METRICS[key]
- if item.lower() in client_meta.get(CLIENT_METADATA_VALID_METRICS_KEY, []):
+ if item.lower() in client_meta.get(
+ CLIENT_METADATA_VALID_METRICS_KEY, []):
if typ == MetricType.METRIC_TYPE_PERCENTAGE:
- self.mainw.addnstr(y_coord, coord[0], f'{calc_perc(m)}', hlen)
+ perc = calc_perc(m)
+ metrics_dict[fs_name][client_id][self.items(item)] = perc
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{perc}', curses.A_DIM)
elif typ == MetricType.METRIC_TYPE_LATENCY:
- self.mainw.addnstr(y_coord, coord[0], f'{calc_lat(m)}', hlen)
+ lat = calc_lat(m)
+ metrics_dict[fs_name][client_id][self.items(item)] = lat
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{lat}', curses.A_DIM)
elif typ == MetricType.METRIC_TYPE_STDEV:
- self.mainw.addnstr(y_coord, coord[0], f'{calc_stdev(m)}', hlen)
+ stdev = calc_stdev(m)
+ metrics_dict[fs_name][client_id][self.items(item)] = stdev
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{stdev}', curses.A_DIM)
elif typ == MetricType.METRIC_TYPE_SIZE:
- self.mainw.addnstr(y_coord, coord[0], f'{calc_size(m)}', hlen)
+ size = calc_size(m)
+ metrics_dict[fs_name][client_id][self.items(item)] = size
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{size}', curses.A_DIM)
# average io sizes
- if remaining_hlen == 0:
- return
if key == "READ_IO_SIZES":
coord = x_coord_map["READ_IO_AVG"]
elif key == "WRITE_IO_SIZES":
coord = x_coord_map["WRITE_IO_AVG"]
- hlen = coord[1] - len(ITEMS_PAD)
- hlen = min(hlen, remaining_hlen)
- if remaining_hlen < coord[1]:
- remaining_hlen = 0
- else:
- remaining_hlen -= coord[1]
- self.mainw.addnstr(y_coord, coord[0], f'{calc_avg_size(m)}', hlen)
+ avg_size = calc_avg_size(m)
+ metrics_dict[fs_name][client_id][self.avg_items(key)] = avg_size
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{avg_size}', curses.A_DIM)
# io speeds
- if remaining_hlen == 0:
- return
if key == "READ_IO_SIZES":
coord = x_coord_map["READ_IO_SPEED"]
elif key == "WRITE_IO_SIZES":
coord = x_coord_map["WRITE_IO_SPEED"]
- hlen = coord[1] - len(ITEMS_PAD)
- hlen = min(hlen, remaining_hlen)
- if remaining_hlen < coord[1]:
- remaining_hlen = 0
- else:
- remaining_hlen -= coord[1]
size = 0
if key == "READ_IO_SIZES":
if m[1] > 0:
last_size = last_write_size.get(client_id, 0)
size = m[1] - last_size
last_write_size[client_id] = m[1]
- self.mainw.addnstr(y_coord, coord[0],
- f'{calc_speed(abs(size), duration)}',
- hlen)
+ speed = calc_speed(abs(size), duration)
+ metrics_dict[fs_name][client_id][self.speed_items(key)] = speed
+ self.fsstats.addstr(y_coord, coord[0],
+ f'{speed}', curses.A_DIM)
else:
# display 0th element from metric tuple
- self.mainw.addnstr(y_coord, coord[0], f'{m[0]}', hlen)
+ self.fsstats.addstr(y_coord, coord[0], f'{m[0]}', curses.A_DIM)
else:
- self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
+ self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
cidx += 1
- if remaining_hlen == 0:
- return
-
for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
coord = x_coord_map[item]
- hlen = coord[1] - len(ITEMS_PAD)
+ wrapLen = self.PAD_WIDTH - coord[0]
# always place the FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR in the
# last, it will be a very long string to display
if item == FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR:
- if FSTop.has_metrics(client_meta, [CLIENT_METADATA_MOUNT_POINT_KEY,
- CLIENT_METADATA_HOSTNAME_KEY,
- CLIENT_METADATA_IP_KEY]):
- self.mainw.addnstr(y_coord, coord[0],
- f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'
- f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'
- f'{client_meta[CLIENT_METADATA_IP_KEY]}',
- remaining_hlen)
+ if FSTop.has_metrics(client_meta,
+ [CLIENT_METADATA_MOUNT_POINT_KEY,
+ CLIENT_METADATA_HOSTNAME_KEY,
+ CLIENT_METADATA_IP_KEY]):
+ mount_point = f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'\
+ f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'\
+ f'{client_meta[CLIENT_METADATA_IP_KEY]}'
+ self.fsstats.addstr(
+ y_coord, coord[0],
+ wrap(mount_point, wrapLen), curses.A_DIM)
else:
- self.mainw.addnstr(y_coord, coord[0], "N/A", remaining_hlen)
- hlen = min(hlen, remaining_hlen)
- if remaining_hlen < coord[1]:
- remaining_hlen = 0
- else:
- remaining_hlen -= coord[1]
- if remaining_hlen == 0:
- return
+ self.fsstats.addstr(y_coord, coord[0], "N/A", curses.A_DIM)
- def refresh_clients(self, x_coord_map, stats_json):
+ def create_clients(self, x_coord_map, stats_json, fs_name):
+ global metrics_dict, current_states
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):
- if self.mainw is None:
- return
- self.refresh_clients(x_coord_map, stats_json)
- self.mainw.refresh()
-
- def refresh_header(self, stats_json):
- hlen = self.width - 2
+ self.tablehead_y += 2
+ res = stats_json[GLOBAL_METRICS_KEY].get(fs_name, {})
+ client_cnt = len(res)
+ self.fsstats.addstr(self.tablehead_y, 0, FS_TOP_NAME_TOPL_FMT.format(
+ fs_name=fs_name, client_count=client_cnt), curses.A_BOLD)
+ self.tablehead_y += 2
+ metrics_dict_client = metrics_dict.get(fs_name, {})
+ if len(metrics_dict) > len(fs_list):
+ stale_fs = set(metrics_dict) - set(fs_list)
+ for key in stale_fs:
+ del metrics_dict[key]
+ if len(metrics_dict_client) > client_cnt:
+ stale_clients = set(metrics_dict_client) - set(res)
+ for key in stale_clients:
+ del metrics_dict_client[key]
+ if client_cnt:
+ if len(metrics_dict_client) != client_cnt:
+ sort_list = sorted(list(res.keys()))
+ else:
+ sort_arg = current_states['last_field']
+ sort_list = sorted(list(res.keys()),
+ key=lambda x: metrics_dict[fs_name].get(x, {}).get(sort_arg, 0),
+ reverse=True)
+ if current_states['limit'] is not None and int(current_states['limit']) < client_cnt:
+ sort_list = sort_list[0:int(current_states['limit'])]
+ for client_id in sort_list:
+ self.create_client(
+ fs_name, client_id, res.get(client_id, {}), counters,
+ stats_json[CLIENT_METADATA_KEY].get(fs_name, {}).get(client_id, {}),
+ x_coord_map, self.tablehead_y)
+ self.tablehead_y += 1
+
+ def create_header(self, stats_json, help, screen_title="", color_id=0):
+ 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)
+ self.header.addstr(0, 0, 'perf stats version mismatch!', curses.A_BOLD)
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),
- 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.refresh()
+ self.header.addstr(0, 0, FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR,
+ now=now), curses.A_BOLD)
+ self.header.addstr(2, 0, screen_title, curses.color_pair(color_id) | curses.A_BOLD)
+ self.header.addstr(3, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
+ num_mounts=num_mounts,
+ num_kclients=num_kclients,
+ num_libs=num_libs), curses.A_DIM)
+ self.header.addstr(4, 0, help, curses.A_DIM)
return True
def run_display(self):
+ # clear the pads to have a smooth refresh
+ self.header.erase()
+ self.fsstats.erase()
+
+ self.current_screen = FS_TOP_FS_SELECTED_APP
+ screen_title = "Selected Filesystem Info"
+ help_commands = "m - select a filesystem | s - sort menu | l - limit number of clients"\
+ " | r - reset to default | q - home (All Filesystem Info) screen"
+ curses.init_pair(3, curses.COLOR_MAGENTA, -1)
+
+ top, left = 0, 0 # where to place pad
+ vscrollOffset, hscrollOffset = 0, 0 # scroll offsets
+
+ # calculate the initial viewport height and width
+ windowsize = self.stdscr.getmaxyx()
+ self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+ # create header subpad
+ self.header_height = 7
+ self.header = self.fstop_pad.subwin(self.header_height, self.viewportWidth, 0, 0)
+
+ # create fsstats subpad
+ fsstats_begin_y = self.header_height
+ fsstats_height = self.PAD_HEIGHT - self.header_height
+ self.fsstats = self.fstop_pad.subwin(fsstats_height, self.PAD_WIDTH, fsstats_begin_y, 0)
+
+ curses.halfdelay(1)
+ cmd = self.stdscr.getch()
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
+ if cmd in [ord('m'), ord('s'), ord('l'), ord('r'), ord('q')]:
+ self.set_option(cmd)
+ self.exit_ev.set()
- # 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)
- 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)
+ global fs_list, current_states
+ fs_list = self.get_fs_names()
+ fs = current_states["last_fs"]
+ stats_json = self.perf_stats_query()
+ vscrollEnd = 0
+ if fs not in fs_list:
+ help = "Error: The selected filesystem is not available now. " + help_commands
+ self.header.erase() # erase previous text
+ self.create_header(stats_json, help, screen_title, 3)
+ else:
+ self.tablehead_y = 0
+ help = "COMMANDS: " + help_commands
+ self.fsstats.erase() # erase previous text
+
+ client_metadata = stats_json[GLOBAL_METRICS_KEY].get(fs, {})
+ if current_states['limit'] is not None and \
+ int(current_states['limit']) < len(client_metadata):
+ num_client = int(current_states['limit'])
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)
+ num_client = len(client_metadata)
+ vscrollEnd += num_client
+ if self.create_header(stats_json, help, screen_title, 3):
+ x_coord_map = self.create_top_line_and_build_coord()
+ self.create_clients(x_coord_map, stats_json, fs)
+
+ # scroll and refresh
+ if cmd == curses.KEY_DOWN:
+ if (vscrollEnd - vscrollOffset) > 1:
+ vscrollOffset += 1
else:
- self.mainw = None
- except curses.error:
- # this may happen when creating the sub windows the
- # terminal window size changed, just retry it
- continue
-
+ vscrollOffset = vscrollEnd
+ elif cmd == curses.KEY_UP:
+ if vscrollOffset > 0:
+ vscrollOffset -= 1
+ elif cmd == curses.KEY_NPAGE:
+ if (vscrollEnd - vscrollOffset) / 20 > 1:
+ vscrollOffset += 20
+ else:
+ vscrollOffset = vscrollEnd
+ elif cmd == curses.KEY_PPAGE:
+ if vscrollOffset / 20 >= 1:
+ vscrollOffset -= 20
+ else:
+ vscrollOffset = 0
+ elif cmd == curses.KEY_RIGHT:
+ if hscrollOffset < self.PAD_WIDTH - self.viewportWidth - 1:
+ hscrollOffset += 1
+ elif cmd == curses.KEY_LEFT:
+ if hscrollOffset > 0:
+ hscrollOffset -= 1
+ elif cmd == curses.KEY_HOME:
+ hscrollOffset = 0
+ elif cmd == curses.KEY_END:
+ hscrollOffset = self.PAD_WIDTH - self.viewportWidth - 1
+ elif cmd == curses.KEY_RESIZE:
+ # terminal resize event. Update the viewport dimensions
+ windowsize = self.stdscr.getmaxyx()
+ self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+ if cmd:
+ try:
+ # refresh the viewport for the header portion
+ if cmd not in [curses.KEY_DOWN,
+ curses.KEY_UP,
+ curses.KEY_NPAGE,
+ curses.KEY_PPAGE,
+ curses.KEY_RIGHT,
+ curses.KEY_LEFT]:
+ self.fstop_pad.refresh(0, 0,
+ top, left,
+ top + self.header_height, left + self.viewportWidth)
+ # refresh the viewport for the current table header portion in the fsstats pad
+ if cmd not in [curses.KEY_DOWN,
+ curses.KEY_UP,
+ curses.KEY_NPAGE,
+ curses.KEY_PPAGE]:
+ self.fstop_pad.refresh(fsstats_begin_y, hscrollOffset,
+ top + fsstats_begin_y, left,
+ 7, left + self.viewportWidth)
+ # refresh the viewport for the current client records portion in the fsstats pad
+ self.fstop_pad.refresh(fsstats_begin_y + 1 + vscrollOffset, hscrollOffset,
+ top + fsstats_begin_y + 2, left,
+ top + self.viewportHeight, left + self.viewportWidth)
+ except curses.error:
+ # This happens when the user switches to a terminal of different zoom size.
+ # just retry it.
+ pass
+ # End scroll and refresh
+
+ curses.halfdelay(self.refresh_interval_secs * 10)
+ cmd = self.stdscr.getch()
+
+ def run_all_display(self):
+ # clear text from the previous screen
+ if self.current_screen == FS_TOP_FS_SELECTED_APP:
+ self.header.erase()
+
+ self.current_screen = FS_TOP_ALL_FS_APP
+ screen_title = "All Filesystem Info"
+ curses.init_pair(2, curses.COLOR_CYAN, -1)
+
+ top, left = 0, 0 # where to place pad
+ vscrollOffset, hscrollOffset = 0, 0 # scroll offsets
+
+ # calculate the initial viewport height and width
+ windowsize = self.stdscr.getmaxyx()
+ self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+
+ # create header subpad
+ self.header_height = 7
+ self.header = self.fstop_pad.subwin(self.header_height, self.viewportWidth, 0, 0)
+
+ # create fsstats subpad
+ fsstats_begin_y = self.header_height
+ fsstats_height = self.PAD_HEIGHT - self.header_height
+ self.fsstats = self.fstop_pad.subwin(fsstats_height, self.PAD_WIDTH, fsstats_begin_y, 0)
+
+ curses.halfdelay(1)
+ cmd = self.stdscr.getch()
+ while not self.exit_ev.is_set():
+ if cmd in [ord('m'), ord('s'), ord('l'), ord('r'), ord('q')]:
+ if self.set_option(cmd):
+ self.exit_ev.set()
+
+ # header display
+ global fs_list, current_states
+ fs_list = self.get_fs_names()
+ current_states["last_fs"] = fs_list
stats_json = self.perf_stats_query()
- 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)
- except curses.error:
- # this may happen when addstr the terminal window
- # size changed, just retry it
- pass
+ vscrollEnd = 0
+ if not fs_list:
+ help = "INFO: No filesystem is available [Press 'q' to quit]"
+ self.header.erase() # erase previous text
+ self.fsstats.erase()
+ self.create_header(stats_json, help, screen_title, 2)
+ else:
+ self.tablehead_y = 0
+ num_client = 0
+ help = "COMMANDS: m - select a filesystem | s - sort menu |"\
+ " l - limit number of clients | r - reset to default | q - quit"
+ self.fsstats.erase() # erase previous text
+ for index, fs in enumerate(fs_list):
+ # Get the vscrollEnd in advance
+ client_metadata = stats_json[GLOBAL_METRICS_KEY].get(fs, {})
+ if current_states['limit'] is not None and \
+ int(current_states['limit']) < len(client_metadata):
+ num_client = int(current_states['limit'])
+ else:
+ num_client = len(client_metadata)
+ vscrollEnd += num_client
+ if self.create_header(stats_json, help, screen_title, 2):
+ if not index: # do it only for the first fs
+ x_coord_map = self.create_top_line_and_build_coord()
+ self.create_clients(x_coord_map, stats_json, fs)
+
+ # scroll and refresh
+ if cmd == curses.KEY_DOWN:
+ if (vscrollEnd - vscrollOffset) > 1:
+ vscrollOffset += 1
+ else:
+ vscrollOffset = vscrollEnd
+ elif cmd == curses.KEY_UP:
+ if vscrollOffset > 0:
+ vscrollOffset -= 1
+ elif cmd == curses.KEY_NPAGE:
+ if (vscrollEnd - vscrollOffset) / 20 > 1:
+ vscrollOffset += 20
+ else:
+ vscrollOffset = vscrollEnd
+ elif cmd == curses.KEY_PPAGE:
+ if vscrollOffset / 20 >= 1:
+ vscrollOffset -= 20
+ else:
+ vscrollOffset = 0
+ elif cmd == curses.KEY_RIGHT:
+ if hscrollOffset < self.PAD_WIDTH - self.viewportWidth - 1:
+ hscrollOffset += 1
+ elif cmd == curses.KEY_LEFT:
+ if hscrollOffset > 0:
+ hscrollOffset -= 1
+ elif cmd == curses.KEY_HOME:
+ hscrollOffset = 0
+ elif cmd == curses.KEY_END:
+ hscrollOffset = self.PAD_WIDTH - self.viewportWidth - 1
+ elif cmd == curses.KEY_RESIZE:
+ # terminal resize event. Update the viewport dimensions
+ windowsize = self.stdscr.getmaxyx()
+ self.viewportHeight, self.viewportWidth = windowsize[0] - 1, windowsize[1] - 1
+ if cmd:
+ try:
+ # refresh the viewport for the header portion
+ if cmd not in [curses.KEY_DOWN,
+ curses.KEY_UP,
+ curses.KEY_NPAGE,
+ curses.KEY_PPAGE,
+ curses.KEY_RIGHT,
+ curses.KEY_LEFT]:
+ self.fstop_pad.refresh(0, 0,
+ top, left,
+ top + self.header_height, left + self.viewportWidth)
+ # refresh the viewport for the current table header portion in the fsstats pad
+ if cmd not in [curses.KEY_DOWN,
+ curses.KEY_UP,
+ curses.KEY_NPAGE,
+ curses.KEY_PPAGE]:
+ self.fstop_pad.refresh(fsstats_begin_y, hscrollOffset,
+ top + fsstats_begin_y, left,
+ 7, left + self.viewportWidth)
+ # refresh the viewport for the current client records portion in the fsstats pad
+ self.fstop_pad.refresh(fsstats_begin_y + 1 + vscrollOffset, hscrollOffset,
+ top + fsstats_begin_y + 2, left,
+ top + self.viewportHeight, left + self.viewportWidth)
+ except curses.error:
+ # This happens when the user switches to a terminal of different zoom size.
+ # just retry it.
+ pass
+ # End scroll and refresh
+
+ curses.halfdelay(self.refresh_interval_secs * 10)
+ cmd = self.stdscr.getch()
+# End class FSTop
if __name__ == '__main__':
value = float(x)
if value < MIN_REFRESH_INTERVAL:
raise argparse.ArgumentTypeError(
- f'Refresh interval should be greater than or equal to {MIN_REFRESH_INTERVAL}')
+ 'Refresh interval should be greater than or equal to'
+ f' {MIN_REFRESH_INTERVAL}')
return value
parser = argparse.ArgumentParser(description='Ceph Filesystem top utility')
help='Path to cluster configuration file')
parser.add_argument('--selftest', dest='selftest', action='store_true',
help='Run in selftest mode')
- parser.add_argument('-d', '--delay', nargs='?', default=DEFAULT_REFRESH_INTERVAL,
- type=float_greater_than, help='Interval to refresh data '
+ parser.add_argument('-d', '--delay', nargs='?',
+ default=DEFAULT_REFRESH_INTERVAL,
+ type=float_greater_than,
+ help='Refresh interval in seconds '
f'(default: {DEFAULT_REFRESH_INTERVAL})')
args = parser.parse_args()