]> git.proxmox.com Git - ceph.git/blob - ceph/src/tools/cephfs/top/cephfs-top
4d359211f246f2f124970a9ceb840d354f072332
[ceph.git] / ceph / src / tools / cephfs / top / cephfs-top
1 #!/usr/bin/python3
2
3 import argparse
4 import sys
5 import curses
6 import errno
7 import json
8 import signal
9 import time
10 import math
11
12 from collections import OrderedDict
13 from datetime import datetime
14 from enum import Enum, unique
15 from threading import Event
16
17 import rados
18
19
20 class FSTopException(Exception):
21 def __init__(self, msg=''):
22 self.error_msg = msg
23
24 def get_error_msg(self):
25 return self.error_msg
26
27
28 @unique
29 class MetricType(Enum):
30 METRIC_TYPE_NONE = 0
31 METRIC_TYPE_PERCENTAGE = 1
32 METRIC_TYPE_LATENCY = 2
33 METRIC_TYPE_SIZE = 3
34 METRIC_TYPE_STDEV = 4
35
36
37 FS_TOP_PROG_STR = 'cephfs-top'
38
39 # version match b/w fstop and stats emitted by mgr/stats
40 FS_TOP_SUPPORTED_VER = 1
41
42 ITEMS_PAD_LEN = 1
43 ITEMS_PAD = " " * ITEMS_PAD_LEN
44 DEFAULT_REFRESH_INTERVAL = 1
45 # min refresh interval allowed
46 MIN_REFRESH_INTERVAL = 0.5
47
48 # metadata provided by mgr/stats
49 FS_TOP_MAIN_WINDOW_COL_CLIENT_ID = "client_id"
50 FS_TOP_MAIN_WINDOW_COL_MNT_ROOT = "mount_root"
51 FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR = "mount_point@host/addr"
52
53 MAIN_WINDOW_TOP_LINE_ITEMS_START = [ITEMS_PAD,
54 FS_TOP_MAIN_WINDOW_COL_CLIENT_ID,
55 FS_TOP_MAIN_WINDOW_COL_MNT_ROOT]
56 MAIN_WINDOW_TOP_LINE_ITEMS_END = [FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR]
57
58 MAIN_WINDOW_TOP_LINE_METRICS_LEGACY = ["READ_LATENCY",
59 "WRITE_LATENCY",
60 "METADATA_LATENCY"
61 ]
62
63 # adjust this map according to stats version and maintain order
64 # as emitted by mgr/stast
65 MAIN_WINDOW_TOP_LINE_METRICS = OrderedDict([
66 ("CAP_HIT", MetricType.METRIC_TYPE_PERCENTAGE),
67 ("READ_LATENCY", MetricType.METRIC_TYPE_LATENCY),
68 ("WRITE_LATENCY", MetricType.METRIC_TYPE_LATENCY),
69 ("METADATA_LATENCY", MetricType.METRIC_TYPE_LATENCY),
70 ("DENTRY_LEASE", MetricType.METRIC_TYPE_PERCENTAGE),
71 ("OPENED_FILES", MetricType.METRIC_TYPE_NONE),
72 ("PINNED_ICAPS", MetricType.METRIC_TYPE_NONE),
73 ("OPENED_INODES", MetricType.METRIC_TYPE_NONE),
74 ("READ_IO_SIZES", MetricType.METRIC_TYPE_SIZE),
75 ("WRITE_IO_SIZES", MetricType.METRIC_TYPE_SIZE),
76 ("AVG_READ_LATENCY", MetricType.METRIC_TYPE_LATENCY),
77 ("STDEV_READ_LATENCY", MetricType.METRIC_TYPE_STDEV),
78 ("AVG_WRITE_LATENCY", MetricType.METRIC_TYPE_LATENCY),
79 ("STDEV_WRITE_LATENCY", MetricType.METRIC_TYPE_STDEV),
80 ("AVG_METADATA_LATENCY", MetricType.METRIC_TYPE_LATENCY),
81 ("STDEV_METADATA_LATENCY", MetricType.METRIC_TYPE_STDEV),
82 ])
83 MGR_STATS_COUNTERS = list(MAIN_WINDOW_TOP_LINE_METRICS.keys())
84
85 FS_TOP_VERSION_HEADER_FMT = '{prog_name} - {now}'
86 FS_TOP_CLIENT_HEADER_FMT = 'Client(s): {num_clients} - {num_mounts} FUSE, '\
87 '{num_kclients} kclient, {num_libs} libcephfs'
88
89 CLIENT_METADATA_KEY = "client_metadata"
90 CLIENT_METADATA_MOUNT_POINT_KEY = "mount_point"
91 CLIENT_METADATA_MOUNT_ROOT_KEY = "root"
92 CLIENT_METADATA_IP_KEY = "IP"
93 CLIENT_METADATA_HOSTNAME_KEY = "hostname"
94 CLIENT_METADATA_VALID_METRICS_KEY = "valid_metrics"
95
96 GLOBAL_METRICS_KEY = "global_metrics"
97 GLOBAL_COUNTERS_KEY = "global_counters"
98
99 last_time = time.time()
100 last_read_size = {}
101 last_write_size = {}
102
103
104 def calc_perc(c):
105 if c[0] == 0 and c[1] == 0:
106 return 0.0
107 return round((c[0] / (c[0] + c[1])) * 100, 2)
108
109
110 def calc_lat(c):
111 return round(c[0] * 1000 + c[1] / 1000000, 2)
112
113
114 def calc_stdev(c):
115 stdev = 0.0
116 if c[1] > 1:
117 stdev = math.sqrt(c[0] / (c[1] - 1)) / 1000000
118 return round(stdev, 2)
119
120
121 # in MB
122 def calc_size(c):
123 return round(c[1] / (1024 * 1024), 2)
124
125
126 # in MB
127 def calc_avg_size(c):
128 if c[0] == 0:
129 return 0.0
130 return round(c[1] / (c[0] * 1024 * 1024), 2)
131
132
133 # in MB/s
134 def calc_speed(size, duration):
135 if duration == 0:
136 return 0.0
137 return round(size / (duration * 1024 * 1024), 2)
138
139
140 def wrap(s, sl):
141 """return a '+' suffixed wrapped string"""
142 if len(s) < sl:
143 return s
144 return f'{s[0:sl-1]}+'
145
146
147 class FSTop(object):
148 def __init__(self, args):
149 self.rados = None
150 self.stdscr = None # curses instance
151 self.client_name = args.id
152 self.cluster_name = args.cluster
153 self.conffile = args.conffile
154 self.refresh_interval_secs = args.delay
155 self.exit_ev = Event()
156
157 def refresh_window_size(self):
158 self.height, self.width = self.stdscr.getmaxyx()
159
160 def handle_signal(self, signum, _):
161 self.exit_ev.set()
162
163 def init(self):
164 try:
165 if self.conffile:
166 r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name,
167 conffile=self.conffile)
168 else:
169 r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name)
170 r_rados.conf_read_file()
171 r_rados.connect()
172 self.rados = r_rados
173 except rados.Error as e:
174 if e.errno == errno.ENOENT:
175 raise FSTopException(f'cluster {self.cluster_name} does not exist')
176 else:
177 raise FSTopException(f'error connecting to cluster: {e}')
178 self.verify_perf_stats_support()
179 signal.signal(signal.SIGTERM, self.handle_signal)
180 signal.signal(signal.SIGINT, self.handle_signal)
181
182 def fini(self):
183 if self.rados:
184 self.rados.shutdown()
185 self.rados = None
186
187 def selftest(self):
188 stats_json = self.perf_stats_query()
189 if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
190 raise FSTopException('perf stats version mismatch!')
191 missing = [m for m in stats_json["global_counters"] if m.upper() not in MGR_STATS_COUNTERS]
192 if missing:
193 raise FSTopException('Cannot handle unknown metrics from \'ceph fs perf stats\': '
194 f'{missing}')
195
196 def setup_curses(self, win):
197 self.stdscr = win
198 curses.use_default_colors()
199 curses.start_color()
200 try:
201 curses.curs_set(0)
202 except curses.error:
203 # If the terminal do not support the visibility
204 # requested it will raise an exception
205 pass
206 self.run_display()
207
208 def verify_perf_stats_support(self):
209 mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
210 try:
211 ret, buf, out = self.rados.mon_command(json.dumps(mon_cmd), b'')
212 except Exception as e:
213 raise FSTopException(f'error checking \'stats\' module: {e}')
214 if ret != 0:
215 raise FSTopException(f'error checking \'stats\' module: {out}')
216 if 'stats' not in json.loads(buf.decode('utf-8'))['enabled_modules']:
217 raise FSTopException('\'stats\' module not enabled. Use \'ceph mgr module '
218 'enable stats\' to enable')
219
220 def perf_stats_query(self):
221 mgr_cmd = {'prefix': 'fs perf stats', 'format': 'json'}
222 try:
223 ret, buf, out = self.rados.mgr_command(json.dumps(mgr_cmd), b'')
224 except Exception as e:
225 raise FSTopException(f'error in \'perf stats\' query: {e}')
226 if ret != 0:
227 raise FSTopException(f'error in \'perf stats\' query: {out}')
228 return json.loads(buf.decode('utf-8'))
229
230 def items(self, item):
231 if item == "CAP_HIT":
232 return "chit"
233 if item == "READ_LATENCY":
234 return "rlat"
235 if item == "WRITE_LATENCY":
236 return "wlat"
237 if item == "METADATA_LATENCY":
238 return "mlat"
239 if item == "DENTRY_LEASE":
240 return "dlease"
241 if item == "OPENED_FILES":
242 return "ofiles"
243 if item == "PINNED_ICAPS":
244 return "oicaps"
245 if item == "OPENED_INODES":
246 return "oinodes"
247 if item == "READ_IO_SIZES":
248 return "rtio"
249 if item == "WRITE_IO_SIZES":
250 return "wtio"
251 if item == 'AVG_READ_LATENCY':
252 return 'rlatavg'
253 if item == 'STDEV_READ_LATENCY':
254 return 'rlatsd'
255 if item == 'AVG_WRITE_LATENCY':
256 return 'wlatavg'
257 if item == 'STDEV_WRITE_LATENCY':
258 return 'wlatsd'
259 if item == 'AVG_METADATA_LATENCY':
260 return 'mlatavg'
261 if item == 'STDEV_METADATA_LATENCY':
262 return 'mlatsd'
263 else:
264 # return empty string for none type
265 return ''
266
267 def mtype(self, typ):
268 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
269 return "(%)"
270 elif typ == MetricType.METRIC_TYPE_LATENCY:
271 return "(ms)"
272 elif typ == MetricType.METRIC_TYPE_SIZE:
273 return "(MB)"
274 elif typ == MetricType.METRIC_TYPE_STDEV:
275 return "(ms)"
276 else:
277 # return empty string for none type
278 return ''
279
280 def avg_items(self, item):
281 if item == "READ_IO_SIZES":
282 return "raio"
283 if item == "WRITE_IO_SIZES":
284 return "waio"
285 else:
286 # return empty string for none type
287 return ''
288
289 def speed_items(self, item):
290 if item == "READ_IO_SIZES":
291 return "rsp"
292 if item == "WRITE_IO_SIZES":
293 return "wsp"
294 else:
295 # return empty string for none type
296 return ''
297
298 def speed_mtype(self, typ):
299 if typ == MetricType.METRIC_TYPE_SIZE:
300 return "(MB/s)"
301 else:
302 # return empty string for none type
303 return ''
304
305 def refresh_top_line_and_build_coord(self):
306 if self.topl is None:
307 return
308
309 xp = 0
310 x_coord_map = {}
311
312 heading = []
313 for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
314 heading.append(item)
315 nlen = len(item) + len(ITEMS_PAD)
316 x_coord_map[item] = (xp, nlen)
317 xp += nlen
318
319 for item, typ in MAIN_WINDOW_TOP_LINE_METRICS.items():
320 if item in MAIN_WINDOW_TOP_LINE_METRICS_LEGACY:
321 continue
322 it = f'{self.items(item)}{self.mtype(typ)}'
323 heading.append(it)
324 nlen = len(it) + len(ITEMS_PAD)
325 x_coord_map[item] = (xp, nlen)
326 xp += nlen
327
328 if item == "READ_IO_SIZES" or item == "WRITE_IO_SIZES":
329 # average io sizes
330 it = f'{self.avg_items(item)}{self.mtype(typ)}'
331 heading.append(it)
332 nlen = len(it) + len(ITEMS_PAD)
333 if item == "READ_IO_SIZES":
334 x_coord_map["READ_IO_AVG"] = (xp, nlen)
335 if item == "WRITE_IO_SIZES":
336 x_coord_map["WRITE_IO_AVG"] = (xp, nlen)
337 xp += nlen
338
339 # io speeds
340 it = f'{self.speed_items(item)}{self.speed_mtype(typ)}'
341 heading.append(it)
342 nlen = len(it) + len(ITEMS_PAD)
343 if item == "READ_IO_SIZES":
344 x_coord_map["READ_IO_SPEED"] = (xp, nlen)
345 if item == "WRITE_IO_SIZES":
346 x_coord_map["WRITE_IO_SPEED"] = (xp, nlen)
347 xp += nlen
348
349 for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
350 heading.append(item)
351 nlen = len(item) + len(ITEMS_PAD)
352 x_coord_map[item] = (xp, nlen)
353 xp += nlen
354 title = ITEMS_PAD.join(heading)
355 hlen = min(self.width - 2, len(title))
356 self.topl.addnstr(0, 0, title, hlen, curses.A_STANDOUT | curses.A_BOLD)
357 self.topl.refresh()
358 return x_coord_map
359
360 @staticmethod
361 def has_metric(metadata, metrics_key):
362 return metrics_key in metadata
363
364 @staticmethod
365 def has_metrics(metadata, metrics_keys):
366 for key in metrics_keys:
367 if not FSTop.has_metric(metadata, key):
368 return False
369 return True
370
371 def refresh_client(self, client_id, metrics, counters, client_meta, x_coord_map, y_coord):
372 global last_time
373 size = 0
374 cur_time = time.time()
375 duration = cur_time - last_time
376 last_time = cur_time
377 remaining_hlen = self.width - 1
378 for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
379 coord = x_coord_map[item]
380 hlen = coord[1] - len(ITEMS_PAD)
381 hlen = min(hlen, remaining_hlen)
382 if remaining_hlen < coord[1]:
383 remaining_hlen = 0
384 else:
385 remaining_hlen -= coord[1]
386 if item == FS_TOP_MAIN_WINDOW_COL_CLIENT_ID:
387 self.mainw.addnstr(y_coord, coord[0],
388 wrap(client_id.split('.')[1], hlen),
389 hlen)
390 elif item == FS_TOP_MAIN_WINDOW_COL_MNT_ROOT:
391 if FSTop.has_metric(client_meta, CLIENT_METADATA_MOUNT_ROOT_KEY):
392 self.mainw.addnstr(y_coord, coord[0],
393 wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen),
394 hlen)
395 else:
396 self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
397
398 if remaining_hlen == 0:
399 return
400
401 cidx = 0
402 for item in counters:
403 if item in MAIN_WINDOW_TOP_LINE_METRICS_LEGACY:
404 cidx += 1
405 continue
406 coord = x_coord_map[item]
407 hlen = coord[1] - len(ITEMS_PAD)
408 hlen = min(hlen, remaining_hlen)
409 if remaining_hlen < coord[1]:
410 remaining_hlen = 0
411 else:
412 remaining_hlen -= coord[1]
413 m = metrics[cidx]
414 key = MGR_STATS_COUNTERS[cidx]
415 typ = MAIN_WINDOW_TOP_LINE_METRICS[key]
416 if item.lower() in client_meta.get(CLIENT_METADATA_VALID_METRICS_KEY, []):
417 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
418 self.mainw.addnstr(y_coord, coord[0], f'{calc_perc(m)}', hlen)
419 elif typ == MetricType.METRIC_TYPE_LATENCY:
420 self.mainw.addnstr(y_coord, coord[0], f'{calc_lat(m)}', hlen)
421 elif typ == MetricType.METRIC_TYPE_STDEV:
422 self.mainw.addnstr(y_coord, coord[0], f'{calc_stdev(m)}', hlen)
423 elif typ == MetricType.METRIC_TYPE_SIZE:
424 self.mainw.addnstr(y_coord, coord[0], f'{calc_size(m)}', hlen)
425
426 # average io sizes
427 if remaining_hlen == 0:
428 return
429 if key == "READ_IO_SIZES":
430 coord = x_coord_map["READ_IO_AVG"]
431 elif key == "WRITE_IO_SIZES":
432 coord = x_coord_map["WRITE_IO_AVG"]
433 hlen = coord[1] - len(ITEMS_PAD)
434 hlen = min(hlen, remaining_hlen)
435 if remaining_hlen < coord[1]:
436 remaining_hlen = 0
437 else:
438 remaining_hlen -= coord[1]
439 self.mainw.addnstr(y_coord, coord[0], f'{calc_avg_size(m)}', hlen)
440
441 # io speeds
442 if remaining_hlen == 0:
443 return
444 if key == "READ_IO_SIZES":
445 coord = x_coord_map["READ_IO_SPEED"]
446 elif key == "WRITE_IO_SIZES":
447 coord = x_coord_map["WRITE_IO_SPEED"]
448 hlen = coord[1] - len(ITEMS_PAD)
449 hlen = min(hlen, remaining_hlen)
450 if remaining_hlen < coord[1]:
451 remaining_hlen = 0
452 else:
453 remaining_hlen -= coord[1]
454 size = 0
455 if key == "READ_IO_SIZES":
456 if m[1] > 0:
457 global last_read_size
458 last_size = last_read_size.get(client_id, 0)
459 size = m[1] - last_size
460 last_read_size[client_id] = m[1]
461 if key == "WRITE_IO_SIZES":
462 if m[1] > 0:
463 global last_write_size
464 last_size = last_write_size.get(client_id, 0)
465 size = m[1] - last_size
466 last_write_size[client_id] = m[1]
467 self.mainw.addnstr(y_coord, coord[0],
468 f'{calc_speed(abs(size), duration)}',
469 hlen)
470 else:
471 # display 0th element from metric tuple
472 self.mainw.addnstr(y_coord, coord[0], f'{m[0]}', hlen)
473 else:
474 self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
475 cidx += 1
476
477 if remaining_hlen == 0:
478 return
479
480 for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
481 coord = x_coord_map[item]
482 hlen = coord[1] - len(ITEMS_PAD)
483 # always place the FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR in the
484 # last, it will be a very long string to display
485 if item == FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR:
486 if FSTop.has_metrics(client_meta, [CLIENT_METADATA_MOUNT_POINT_KEY,
487 CLIENT_METADATA_HOSTNAME_KEY,
488 CLIENT_METADATA_IP_KEY]):
489 self.mainw.addnstr(y_coord, coord[0],
490 f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'
491 f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'
492 f'{client_meta[CLIENT_METADATA_IP_KEY]}',
493 remaining_hlen)
494 else:
495 self.mainw.addnstr(y_coord, coord[0], "N/A", remaining_hlen)
496 hlen = min(hlen, remaining_hlen)
497 if remaining_hlen < coord[1]:
498 remaining_hlen = 0
499 else:
500 remaining_hlen -= coord[1]
501 if remaining_hlen == 0:
502 return
503
504 def refresh_clients(self, x_coord_map, stats_json):
505 counters = [m.upper() for m in stats_json[GLOBAL_COUNTERS_KEY]]
506 y_coord = 0
507 for client_id, metrics in stats_json[GLOBAL_METRICS_KEY].items():
508 self.refresh_client(client_id,
509 metrics,
510 counters,
511 stats_json[CLIENT_METADATA_KEY][client_id],
512 x_coord_map,
513 y_coord)
514 y_coord += 1
515
516 def refresh_main_window(self, x_coord_map, stats_json):
517 if self.mainw is None:
518 return
519 self.refresh_clients(x_coord_map, stats_json)
520 self.mainw.refresh()
521
522 def refresh_header(self, stats_json):
523 hlen = self.width - 2
524 if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
525 self.header.addnstr(0, 0, 'perf stats version mismatch!', hlen)
526 return False
527 client_metadata = stats_json[CLIENT_METADATA_KEY]
528 num_clients = len(client_metadata)
529 num_mounts = len([client for client, metadata in client_metadata.items() if
530 CLIENT_METADATA_MOUNT_POINT_KEY in metadata
531 and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
532 num_kclients = len([client for client, metadata in client_metadata.items() if
533 "kernel_version" in metadata])
534 num_libs = num_clients - (num_mounts + num_kclients)
535 now = datetime.now().ctime()
536 self.header.addnstr(0, 0,
537 FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR, now=now),
538 hlen, curses.A_STANDOUT | curses.A_BOLD)
539 self.header.addnstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
540 num_mounts=num_mounts,
541 num_kclients=num_kclients,
542 num_libs=num_libs), hlen)
543 self.header.refresh()
544 return True
545
546 def run_display(self):
547 while not self.exit_ev.is_set():
548 # use stdscr.clear() instead of clearing each window
549 # to avoid screen blinking.
550 self.stdscr.clear()
551 self.refresh_window_size()
552 if self.width <= 2 or self.width <= 2:
553 self.exit_ev.wait(timeout=self.refresh_interval_secs)
554 continue
555
556 # coordinate constants for windowing -- (height, width, y, x)
557 # NOTE: requires initscr() call before accessing COLS, LINES.
558 try:
559 HEADER_WINDOW_COORD = (2, self.width - 1, 0, 0)
560 self.header = curses.newwin(*HEADER_WINDOW_COORD)
561 if self.height >= 3:
562 TOPLINE_WINDOW_COORD = (1, self.width - 1, 3, 0)
563 self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
564 else:
565 self.topl = None
566 if self.height >= 5:
567 MAIN_WINDOW_COORD = (self.height - 4, self.width - 1, 4, 0)
568 self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
569 else:
570 self.mainw = None
571 except curses.error:
572 # this may happen when creating the sub windows the
573 # terminal window size changed, just retry it
574 continue
575
576 stats_json = self.perf_stats_query()
577 try:
578 if self.refresh_header(stats_json):
579 x_coord_map = self.refresh_top_line_and_build_coord()
580 self.refresh_main_window(x_coord_map, stats_json)
581 self.exit_ev.wait(timeout=self.refresh_interval_secs)
582 except curses.error:
583 # this may happen when addstr the terminal window
584 # size changed, just retry it
585 pass
586
587
588 if __name__ == '__main__':
589 def float_greater_than(x):
590 value = float(x)
591 if value < MIN_REFRESH_INTERVAL:
592 raise argparse.ArgumentTypeError(
593 f'Refresh interval should be greater than or equal to {MIN_REFRESH_INTERVAL}')
594 return value
595
596 parser = argparse.ArgumentParser(description='Ceph Filesystem top utility')
597 parser.add_argument('--cluster', nargs='?', const='ceph', default='ceph',
598 help='Ceph cluster to connect (default: ceph)')
599 parser.add_argument('--id', nargs='?', const='fstop', default='fstop',
600 help='Ceph user to use to connection (default: fstop)')
601 parser.add_argument('--conffile', nargs='?', default=None,
602 help='Path to cluster configuration file')
603 parser.add_argument('--selftest', dest='selftest', action='store_true',
604 help='Run in selftest mode')
605 parser.add_argument('-d', '--delay', nargs='?', default=DEFAULT_REFRESH_INTERVAL,
606 type=float_greater_than, help='Interval to refresh data '
607 f'(default: {DEFAULT_REFRESH_INTERVAL})')
608
609 args = parser.parse_args()
610 err = False
611 ft = FSTop(args)
612 try:
613 ft.init()
614 if args.selftest:
615 ft.selftest()
616 sys.stdout.write("selftest ok\n")
617 else:
618 curses.wrapper(ft.setup_curses)
619 except FSTopException as fst:
620 err = True
621 sys.stderr.write(f'{fst.get_error_msg()}\n')
622 except Exception as e:
623 err = True
624 sys.stderr.write(f'exception: {e}\n')
625 finally:
626 ft.fini()
627 sys.exit(0 if not err else -1)