1#!/usr/local/bin/python3.8 2# vim:ft=python 3# 4# Copyright (C) 2011 John Feuerstein <john@feurix.com> 5# 6# Project URL: https://github.com/jhunt/hatop 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 3 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU Library General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program; if not, write to the Free Software 20# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 21# 22'''\ 23HATop is an interactive ncurses client for HAProxy 24================================================== 25 26Display mode reference: 27 28ID Mode Description 29 301 STATUS The default mode with health, session and queue statistics 312 TRAFFIC Display connection and request rates as well as traffic stats 323 HTTP Display various statistical information related to HTTP 334 ERRORS Display health info, various error counters and downtimes 345 CLI Display embedded command line client for the unix socket 35 36Keybind reference: 37 38Key Action 39 40Hh? Display this help screen 41CTRL-C / Qq Quit 42 43TAB Cycle mode forwards 44SHIFT-TAB Cycle mode backwards 45ALT-n / ESC-n Switch to mode n, where n is the numeric mode id 46ESC-ESC Jump to previous mode 47 48ENTER Display hotkey menu for selected service 49SPACE Copy and paste selected service identifier to the CLI 50 51You can scroll the stat views using UP / DOWN / PGUP / PGDOWN / HOME / END. 52 53The reverse colored cursor line is used to select a given service instance. 54 55Some common administrative actions have hotkeys: 56 57Hotkey Action 58 59F1 Enable server on all backends (return from maintenance mode) 60F2 Disable server on all backends (put into maintenance mode) 61 62F4 Restore initial server weight 63 64F5 Decrease server weight: - 10 65F6 Decrease server weight: - 1 66F7 Increase server weight: + 1 67F8 Increase server weight: + 10 68 69F9 Enable server on one backend (return from maintenance mode) 70F10 Disable server on one backend (put into maintenance mode) 71 72Hotkey actions and server responses are logged on the CLI viewport. 73You can scroll the output on the CLI view using PGUP / PGDOWN. 74 75Header reference: 76 77Node configured name of the haproxy node 78Uptime runtime since haproxy was initially started 79Pipes pipes are currently used for kernel-based tcp slicing 80Procs number of haproxy processes 81Tasks number of actice process tasks 82Queue number of queued process tasks (run queue) 83Proxies number of configured proxies 84Services number of configured services 85 86In multiple modes: 87 88NAME name of the proxy and his services 89W configured weight of the service 90STATUS service status (UP/DOWN/NOLB/MAINT/MAINT(via)...) 91CHECK status of last health check (see status reference below) 92 93In STATUS mode: 94 95ACT server is active (server), number of active servers (backend) 96BCK server is backup (server), number of backup servers (backend) 97QCUR current queued requests 98QMAX max queued requests 99SCUR current sessions 100SMAX max sessions 101SLIM sessions limit 102STOT total sessions 103 104In TRAFFIC mode: 105 106LBTOT total number of times a server was selected 107RATE number of sessions per second over last elapsed second 108RLIM limit on new sessions per second 109RMAX max number of new sessions per second 110BIN bytes in (IEEE 1541-2002) 111BOUT bytes out (IEEE 1541-2002) 112 113In HTTP mode: 114 115RATE HTTP requests per second over last elapsed second 116RMAX max number of HTTP requests per second observed 117RTOT total number of HTTP requests received 1181xx number of HTTP responses with 1xx code 1192xx number of HTTP responses with 2xx code 1203xx number of HTTP responses with 3xx code 1214xx number of HTTP responses with 4xx code 1225xx number of HTTP responses with 5xx code 123?xx number of HTTP responses with other codes (protocol error) 124 125In ERRORS mode: 126 127CF number of failed checks 128CD number of UP->DOWN transitions 129CL last status change 130ECONN connection errors 131EREQ request errors 132ERSP response errors 133DREQ denied requests 134DRSP denied responses 135DOWN total downtime 136 137Health check status reference: 138 139UNK unknown 140INI initializing 141SOCKERR socket error 142L4OK check passed on layer 4, no upper layers testing enabled 143L4TMOUT layer 1-4 timeout 144L4CON layer 1-4 connection problem, for example 145 "Connection refused" (tcp rst) or "No route to host" (icmp) 146L6OK check passed on layer 6 147L6TOUT layer 6 (SSL) timeout 148L6RSP layer 6 invalid response - protocol error 149L7OK check passed on layer 7 150L7OKC check conditionally passed on layer 7, for example 404 with 151 disable-on-404 152L7TOUT layer 7 (HTTP/SMTP) timeout 153L7RSP layer 7 invalid response - protocol error 154L7STS layer 7 response error, for example HTTP 5xx 155''' 156__author__ = 'John Feuerstein <john@feurix.com>' 157__copyright__ = 'Copyright (C) 2011 %s' % __author__ 158__license__ = 'GNU GPLv3' 159__version__ = '0.8.0' 160 161import fcntl 162import os 163import re 164import signal 165import socket 166import struct 167import sys 168import time 169import tty 170 171import curses 172import curses.ascii 173 174from socket import error as SocketError 175from _curses import error as CursesError 176 177from collections import deque 178from textwrap import TextWrapper 179 180# ------------------------------------------------------------------------- # 181# GLOBALS # 182# ------------------------------------------------------------------------- # 183 184# Settings of interactive command session over the unix-socket 185HAPROXY_CLI_BUFSIZE = 4096 186HAPROXY_CLI_TIMEOUT = 60 187HAPROXY_CLI_PROMPT = '> ' 188HAPROXY_CLI_CMD_SEP = ';' 189HAPROXY_CLI_CMD_TIMEOUT = 1 190HAPROXY_CLI_MAXLINES = 1000 191 192# Settings of the embedded CLI 193CLI_MAXLINES = 1000 194CLI_MAXHIST = 100 195CLI_INPUT_LIMIT = 200 196CLI_INPUT_RE = re.compile('[a-zA-Z0-9_:\.\-\+; /#%]') 197CLI_INPUT_DENY_CMD = ['prompt', 'set timeout cli', 'quit'] 198 199# Note: Only the last 3 lines are visible instantly on 80x25 200CLI_HELP_TEXT = '''\ 201 Welcome on the embedded interactive HAProxy shell! 202 203 Type `help' to get a command reference 204''' 205 206# Screen setup 207SCREEN_XMIN = 78 208SCREEN_YMIN = 20 209SCREEN_XMAX = 200 210SCREEN_YMAX = 100 211SCREEN_HPOS = 11 212 213HAPROXY_INFO_RE = { 214'software_name': re.compile('^Name:\s*(?P<value>\S+)'), 215'software_version': re.compile('^Version:\s*(?P<value>\S+)'), 216'software_release': re.compile('^Release_date:\s*(?P<value>\S+)'), 217'nproc': re.compile('^Nbproc:\s*(?P<value>\d+)'), 218'procn': re.compile('^Process_num:\s*(?P<value>\d+)'), 219'pid': re.compile('^Pid:\s*(?P<value>\d+)'), 220'uptime': re.compile('^Uptime:\s*(?P<value>[\S ]+)$'), 221'maxconn': re.compile('^Maxconn:\s*(?P<value>\d+)'), 222'curconn': re.compile('^CurrConns:\s*(?P<value>\d+)'), 223'maxpipes': re.compile('^Maxpipes:\s*(?P<value>\d+)'), 224'curpipes': re.compile('^PipesUsed:\s*(?P<value>\d+)'), 225'tasks': re.compile('^Tasks:\s*(?P<value>\d+)'), 226'runqueue': re.compile('^Run_queue:\s*(?P<value>\d+)'), 227'node': re.compile('^node:\s*(?P<value>\S+)'), 228} 229 230HAPROXY_STAT_MAX_SERVICES = 1000 231HAPROXY_STAT_LIMIT_WARNING = '''\ 232Warning: You have reached the stat parser limit! (%d) 233Use --filter to parse specific service stats only. 234''' % HAPROXY_STAT_MAX_SERVICES 235HAPROXY_STAT_FILTER_RE = re.compile( 236 '^(?P<iid>-?\d+)\s+(?P<type>-?\d+)\s+(?P<sid>-?\d+)$') 237HAPROXY_STAT_PROXY_FILTER_RE = re.compile( 238 '^(?P<pxname>[a-zA-Z0-9_:\.\-]+)$') 239HAPROXY_STAT_COMMENT = '#' 240HAPROXY_STAT_SEP = ',' 241HAPROXY_STAT_CSV = [ 242# Note: Fields must be listed in correct order, as described in: 243# http://haproxy.1wt.eu/download/1.4/doc/configuration.txt [9.1] 244 245# TYPE FIELD 246 247(str, 'pxname'), # proxy name 248(str, 'svname'), # service name (FRONTEND / BACKEND / name) 249(int, 'qcur'), # current queued requests 250(int, 'qmax'), # max queued requests 251(int, 'scur'), # current sessions 252(int, 'smax'), # max sessions 253(int, 'slim'), # sessions limit 254(int, 'stot'), # total sessions 255(int, 'bin'), # bytes in 256(int, 'bout'), # bytes out 257(int, 'dreq'), # denied requests 258(int, 'dresp'), # denied responses 259(int, 'ereq'), # request errors 260(int, 'econ'), # connection errors 261(int, 'eresp'), # response errors (among which srv_abrt) 262(int, 'wretr'), # retries (warning) 263(int, 'wredis'), # redispatches (warning) 264(str, 'status'), # status (UP/DOWN/NOLB/MAINT/MAINT(via)...) 265(int, 'weight'), # server weight (server), total weight (backend) 266(int, 'act'), # server is active (server), 267 # number of active servers (backend) 268(int, 'bck'), # server is backup (server), 269 # number of backup servers (backend) 270(int, 'chkfail'), # number of failed checks 271(int, 'chkdown'), # number of UP->DOWN transitions 272(int, 'lastchg'), # last status change (in seconds) 273(int, 'downtime'), # total downtime (in seconds) 274(int, 'qlimit'), # queue limit 275(int, 'pid'), # process id 276(int, 'iid'), # unique proxy id 277(int, 'sid'), # service id (unique inside a proxy) 278(int, 'throttle'), # warm up status 279(int, 'lbtot'), # total number of times a server was selected 280(str, 'tracked'), # id of proxy/server if tracking is enabled 281(int, 'type'), # (0=frontend, 1=backend, 2=server, 3=socket) 282(int, 'rate'), # number of sessions per second 283 # over the last elapsed second 284(int, 'rate_lim'), # limit on new sessions per second 285(int, 'rate_max'), # max number of new sessions per second 286(str, 'check_status'), # status of last health check 287(int, 'check_code'), # layer5-7 code, if available 288(int, 'check_duration'), # time in ms took to finish last health check 289(int, 'hrsp_1xx'), # http responses with 1xx code 290(int, 'hrsp_2xx'), # http responses with 2xx code 291(int, 'hrsp_3xx'), # http responses with 3xx code 292(int, 'hrsp_4xx'), # http responses with 4xx code 293(int, 'hrsp_5xx'), # http responses with 5xx code 294(int, 'hrsp_other'), # http responses with other codes (protocol error) 295(str, 'hanafail'), # failed health checks details 296(int, 'req_rate'), # HTTP requests per second 297(int, 'req_rate_max'), # max number of HTTP requests per second 298(int, 'req_tot'), # total number of HTTP requests received 299(int, 'cli_abrt'), # number of data transfers aborted by client 300(int, 'srv_abrt'), # number of data transfers aborted by server 301] 302HAPROXY_STAT_NUMFIELDS = len(HAPROXY_STAT_CSV) 303HAPROXY_STAT_CSV = [(k, v) for k, v in enumerate(HAPROXY_STAT_CSV)] 304 305# All big numeric values on the screen are prefixed using the metric prefix 306# set, while everything byte related is prefixed using binary prefixes. 307# Note: If a non-byte numeric value fits into the field, we skip prefixing. 308PREFIX_BINARY = { 309 1024: 'K', 310 1024**2: 'M', 311} 312PREFIX_METRIC = { 313 1000: 'k', 314 1000**2: 'M', 315 1000**3: 'G', 316} 317PREFIX_TIME = { 318 60: 'm', 319 60*60: 'h', 320 60*60*24:'d', 321} 322 323# ------------------------------------------------------------------------- # 324# CLASS DEFINITIONS # 325# ------------------------------------------------------------------------- # 326 327# Use bounded length deque if available (Python 2.6+) 328try: 329 deque(maxlen=0) 330 331 class RingBuffer(deque): 332 333 def __init__(self, maxlen): 334 assert maxlen > 0 335 deque.__init__(self, maxlen=maxlen) 336 337except TypeError: 338 339 class RingBuffer(deque): 340 341 def __init__(self, maxlen): 342 assert maxlen > 0 343 deque.__init__(self) 344 self.maxlen = maxlen 345 346 def append(self, item): 347 if len(self) == self.maxlen: 348 self.popleft() 349 deque.append(self, item) 350 351 def appendleft(self, item): 352 if len(self) == self.maxlen: 353 self.popright() 354 deque.appendleft(self, item) 355 356 def extend(self, iterable): 357 for item in iterable: 358 self.append(item) 359 360 def extendleft(self, iterable): 361 for item in iterable: 362 self.appendleft(item) 363 364 365class Socket: 366 367 def __init__(self, path, readonly=False, tcp=False): 368 self.path = path 369 self.ro = readonly 370 self.tcp = tcp 371 if not self.tcp: 372 self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 373 else: 374 self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 375 376 def _recv(self): 377 # socket.recv() wrapper raising SocketError if we receive 378 # EOF before seeing the interactive socket prompt. 379 data = self._socket.recv(HAPROXY_CLI_BUFSIZE) 380 if not data: 381 raise SocketError('error while waiting for prompt') 382 return data.decode() 383 384 def connect(self): 385 # Initialize socket connection 386 self._socket.settimeout(HAPROXY_CLI_CMD_TIMEOUT) 387 if self.tcp: 388 host, port = self.path.split(":") 389 self._socket.connect((host, int(port))) 390 else: 391 self._socket.connect(self.path) 392 393 # Enter the interactive socket mode. This requires HAProxy 1.4+ and 394 # allows us to error out early if connected to an older version. 395 try: 396 self.send(b'prompt') 397 self.wait() 398 self.send(b'set timeout cli %d' % HAPROXY_CLI_TIMEOUT) 399 self.wait() 400 except SocketError: 401 raise SocketError('error while initializing interactive mode') 402 403 def close(self): 404 try: 405 self.send(b'quit') 406 except: 407 pass 408 try: 409 self._socket.close() 410 except: 411 pass 412 413 def send(self, cmdline): 414 self._socket.sendall(b'%s\n' % cmdline) 415 416 def wait(self): 417 # Wait for the prompt and discard data. 418 rbuf = '' 419 while not rbuf.endswith(HAPROXY_CLI_PROMPT): 420 data = self._recv() 421 rbuf = rbuf[-(len(HAPROXY_CLI_PROMPT)-1):] + data 422 423 def recv(self): 424 # Receive lines until HAPROXY_CLI_MAXLINES or the prompt is reached. 425 # If the prompt was still not found, discard data and wait for it. 426 linecount = 0 427 rbuf = '' 428 while not rbuf.endswith(HAPROXY_CLI_PROMPT): 429 430 if linecount == HAPROXY_CLI_MAXLINES: 431 data = self._recv() 432 rbuf = rbuf[-(len(HAPROXY_CLI_PROMPT)-1):] + data 433 continue 434 435 data = self._recv() 436 rbuf += data 437 438 while linecount < HAPROXY_CLI_MAXLINES and '\n' in rbuf: 439 line, rbuf = rbuf.split('\n', 1) 440 linecount += 1 441 yield line 442 443 444class SocketData: 445 446 def __init__(self, socket): 447 self.socket = socket 448 self.pxcount = 0 449 self.svcount = 0 450 self.info = {} 451 self.stat = {} 452 self._filters = set() 453 454 def register_stat_filter(self, stat_filter): 455 456 # Validate and register filters 457 stat_filter_set = set(stat_filter) 458 for filter in stat_filter_set: 459 match = HAPROXY_STAT_FILTER_RE.match(filter) 460 if not match: 461 raise ValueError('invalid stat filter: %s' % filter) 462 self._filters.add(( 463 int(match.group('iid'), 10), 464 int(match.group('type'), 10), 465 int(match.group('sid'), 10), 466 )) 467 468 def register_proxy_filter(self, proxy_filter): 469 470 # Validate filters 471 proxy_filter_set = set(proxy_filter) 472 for filter in proxy_filter_set: 473 if not HAPROXY_STAT_PROXY_FILTER_RE.match(filter): 474 raise ValueError('invalid proxy filter: %s' % filter) 475 476 # Convert proxy filters into more efficient stat filters 477 self.socket.send(b'show stat') 478 pxstat, pxcount, svcount = parse_stat(self.socket.recv()) 479 480 proxy_iid_map = {} # {pxname: iid, ...} 481 482 for pxname in proxy_filter_set: 483 for iid in pxstat: 484 for sid in pxstat[iid]: 485 if pxstat[iid][sid]['pxname'] == pxname: 486 proxy_iid_map[pxname] = iid 487 break 488 if pxname in proxy_iid_map: 489 break 490 491 for pxname in proxy_filter_set: 492 if not pxname in proxy_iid_map: 493 raise RuntimeError('proxy not found: %s' % pxname) 494 495 # Register filters 496 for iid in proxy_iid_map.values(): 497 self._filters.add((iid, -1, -1)) 498 499 def update_info(self): 500 self.socket.send(b'show info') 501 iterable = self.socket.recv() 502 self.info = parse_info(iterable) 503 504 def update_stat(self): 505 # Store current data 506 pxcount_old = self.pxcount 507 svcount_old = self.svcount 508 stat_old = self.stat 509 510 # Reset current data 511 self.pxcount = 0 512 self.svcount = 0 513 self.stat = {} 514 515 if self._filters: 516 for filter in self._filters: 517 self.socket.send(b'show stat %d %d %d' % filter) 518 filter_stat, filter_pxcount, filter_svcount = \ 519 parse_stat(self.socket.recv()) 520 521 if filter_pxcount == 0: 522 raise RuntimeError('stale stat filter: %d %d %d' % filter) 523 524 self.pxcount += filter_pxcount 525 self.svcount += filter_svcount 526 self.stat.update(filter_stat) 527 else: 528 self.socket.send(b'show stat') 529 self.stat, self.pxcount, self.svcount = \ 530 parse_stat(self.socket.recv()) 531 532 if self.pxcount == 0: 533 raise RuntimeWarning('no stat data available') 534 535 # Warn if the HAProxy configuration has changed on-the-fly 536 pxdiff = 0 537 svdiff = 0 538 539 if self.pxcount < pxcount_old: 540 pxdiff -= pxcount_old - self.pxcount 541 if pxcount_old > 0 and self.pxcount > pxcount_old: 542 pxdiff += self.pxcount - pxcount_old 543 if self.svcount < svcount_old: 544 svdiff -= svcount_old - self.svcount 545 if svcount_old > 0 and self.svcount > svcount_old: 546 svdiff += self.svcount - svcount_old 547 548 if pxdiff != 0 or svdiff != 0: 549 raise RuntimeWarning( 550 'config changed: proxy %+d, service %+d ' 551 '(reloading...)' % (pxdiff, svdiff)) 552 553 554class ScreenCLI: 555 556 def __init__(self, screen): 557 self.screen = screen 558 559 # Output 560 self.obuf = RingBuffer(CLI_MAXLINES) 561 self.ypos = 0 562 self.wrapper = TextWrapper() 563 self.screenlines = [] 564 565 # Input 566 self.ihist = RingBuffer(CLI_MAXHIST) 567 self.ibuf = [] 568 self.ibpos = 0 569 self.ibmin = 0 570 571 572 # INPUT 573 @property 574 def imin(self): 575 return self.screen.xmin + 2 576 577 @property 578 def imax(self): 579 return self.screen.xmax - 4 580 581 @property 582 def ispan(self): 583 return self.imax - self.imin 584 585 @property 586 def ipos(self): 587 return self.ibpos - self.ibmin 588 589 @property 590 def ibmax(self): 591 return self.ibmin + self.ispan 592 593 @property 594 def iblen(self): 595 return len(self.ibuf) 596 597 @property 598 def cmdline(self): 599 return ''.join(self.ibuf) 600 601 # OUTPUT 602 @property 603 def ospan(self): 604 return self.screen.span - 1 605 606 607 def setup(self): 608 self.ipad = curses.newpad(1, SCREEN_XMAX) # input 609 self.opad = curses.newpad(SCREEN_YMAX, SCREEN_XMAX) # output 610 611 # Display initial help text... 612 self.obuf.extend(CLI_HELP_TEXT.split('\n')) 613 self.update_screenlines() 614 615 def start(self): 616 try: 617 curses.curs_set(1) 618 except CursesError: 619 pass 620 self.draw_output() 621 self.draw_input() 622 623 def stop(self): 624 try: 625 curses.curs_set(0) 626 except CursesError: 627 pass 628 629 def update_screenlines(self): 630 self.wrapper.width = self.screen.xmax 631 self.screenlines = [] 632 for line in self.obuf: 633 if len(line) > self.wrapper.width: 634 self.screenlines.extend(self.wrapper.wrap(line)) 635 else: 636 self.screenlines.append(line) 637 self.ypos = len(self.screenlines) 638 639 def reset_input(self): 640 self.ibuf = [] 641 self.ibpos = 0 642 self.ibmin = 0 643 644 def refresh_input(self, sync=False): 645 if sync: 646 refresh = self.ipad.refresh 647 else: 648 refresh = self.ipad.noutrefresh 649 650 refresh(0, 0, 651 self.screen.smax, self.screen.xmin, 652 self.screen.smax, self.screen.xmax - 1) 653 654 def refresh_output(self, sync=False): 655 if sync: 656 refresh = self.opad.refresh 657 else: 658 refresh = self.opad.noutrefresh 659 660 refresh(0, 0, 661 self.screen.smin, self.screen.xmin, 662 self.screen.smax - 2, self.screen.xmax - 1) 663 664 def draw_input(self): 665 self.ipad.clear() 666 self.ipad.addstr(0, 0, '> ', curses.A_BOLD) 667 self.ipad.addstr(0, 2, ''.join(self.ibuf[self.ibmin:self.ibmax])) 668 669 # Mark input lines longer than the visible input span 670 if self.ibmin > 0: 671 self.ipad.addstr(0, self.imin - 1, '<') 672 if self.iblen > self.ibmax: 673 self.ipad.addstr(0, self.imax, '>') 674 675 self.ipad.move(0, self.imin + self.ipos) 676 677 def draw_output(self): 678 self.opad.clear() 679 vmin = max(0, self.ypos - self.ospan) 680 vmax = vmin + self.ospan 681 lines = self.screenlines[vmin:vmax] 682 self.opad.addstr(0, 0, '\n'.join(lines)) 683 684 # INPUT 685 def prev(self): 686 if len(self.ihist) == 0: 687 return 688 if len(self.ibuf) == 0: 689 self.ibuf = list(self.ihist[-1]) 690 self.mvend() 691 return 692 if self.ibuf != self.ihist[-1]: 693 self.ihist.append(self.ibuf) 694 self.ihist.rotate(1) 695 self.ibuf = list(self.ihist[-1]) 696 self.mvend() 697 698 def __next__(self): 699 if len(self.ihist) == 0: 700 return 701 self.ihist.rotate(-1) 702 self.ibuf = list(self.ihist[-1]) 703 self.mvend() 704 705 def puts(self, s): 706 s = list(s) 707 if len(self.ibuf) + len(s) >= CLI_INPUT_LIMIT: 708 return 709 for c in s: 710 if not CLI_INPUT_RE.match(c): 711 return 712 713 if self.ibpos < self.iblen: 714 self.ibuf = self.ibuf[:self.ibpos] + s + self.ibuf[self.ibpos:] 715 else: 716 self.ibuf.extend(s) 717 718 self.mvc(len(s)) 719 return True 720 721 def putc(self, c): 722 if len(self.ibuf) == CLI_INPUT_LIMIT: 723 return 724 if not CLI_INPUT_RE.match(c): 725 return 726 727 if self.ibpos < self.iblen: 728 self.ibuf.insert(self.ibpos, c) 729 else: 730 self.ibuf.append(c) 731 732 self.mvc(1) 733 734 def delc(self, n): 735 if n == 0 or self.iblen == 0: 736 return 737 738 # Delete LEFT 739 elif n < 0 and self.ibpos >= 1: 740 self.ibuf.pop(self.ibpos - 1) 741 self.mvc(-1) 742 743 # Delete RIGHT 744 elif n > 0 and self.ibpos < self.iblen: 745 self.ibuf.pop(self.ibpos) 746 self.draw_input() 747 self.refresh_input(sync=True) 748 749 def mvhome(self): 750 self.ibmin = 0 751 self.ibpos = 0 752 self.draw_input() 753 self.refresh_input(sync=True) 754 755 def mvend(self): 756 self.ibmin = max(0, self.iblen - self.ispan) 757 self.ibpos = self.iblen 758 self.draw_input() 759 self.refresh_input(sync=True) 760 761 def mvc(self, n): 762 if n == 0: 763 return 764 765 # Move LEFT 766 if n < 0: 767 self.ibpos = max(0, self.ibpos + n) 768 if self.ibpos < self.ibmin: 769 self.ibmin = self.ibpos 770 771 # Move RIGHT 772 elif n > 0: 773 self.ibpos = min(self.iblen, self.ibpos + n) 774 if self.ibpos > self.ibmax: 775 self.ibmin += n 776 777 self.draw_input() 778 self.refresh_input(sync=True) 779 780 # OUTPUT 781 def mvo(self, n): 782 if n == 0: 783 return 784 785 # Move UP 786 if n < 0 and self.ypos > self.ospan: 787 self.ypos = max(self.ospan, self.ypos + n) 788 789 # Move DOWN 790 elif n > 0 and self.ypos < len(self.screenlines): 791 self.ypos = min(len(self.screenlines), self.ypos + n) 792 793 self.draw_output() 794 self.refresh_output(sync=True) 795 796 def execute(self): 797 798 # Nothing to do... print marker line instead. 799 if self.iblen == 0: 800 self.obuf.append('- %s %s' % (time.ctime(), '-' * 50)) 801 self.obuf.append('') 802 self.update_screenlines() 803 self.draw_output() 804 self.refresh_output(sync=True) 805 self.refresh_input(sync=True) 806 return 807 808 # Validate each command on the command line 809 cmds = [cmd.strip() for cmd in 810 self.cmdline.split(HAPROXY_CLI_CMD_SEP)] 811 812 for pattern in CLI_INPUT_DENY_CMD: 813 for cmd in cmds: 814 if re.match(r'^\s*%s(?:\s|$)' % pattern, cmd): 815 self.obuf.append('* command not allowed: %s' % cmd) 816 self.obuf.append('') 817 self.update_screenlines() 818 self.draw_output() 819 self.refresh_output(sync=True) 820 self.refresh_input(sync=True) 821 return 822 823 self.execute_cmdline(self.cmdline) 824 825 self.draw_output() 826 self.refresh_output(sync=True) 827 828 self.ihist.append(self.ibuf) 829 self.reset_input() 830 self.draw_input() 831 self.refresh_input(sync=True) 832 833 def execute_cmdline(self, cmdline): 834 self.obuf.append('* %s' % time.ctime()) 835 self.obuf.append('> %s' % cmdline) 836 self.screen.data.socket.send(cmdline.encode()) 837 self.obuf.extend(self.screen.data.socket.recv()) 838 self.update_screenlines() 839 840 841class Screen: 842 843 def __init__(self, data, mid=1): 844 self.data = data 845 self.modes = SCREEN_MODES 846 self.sb_conn = StatusBar() 847 self.sb_pipe = StatusBar() 848 self.lines = [] 849 self.screen = None 850 self.xmin = 0 851 self.xmax = SCREEN_XMIN 852 self.ymin = 0 853 self.ymax = SCREEN_YMIN 854 self.vmin = 0 855 self.cmin = 0 856 self.cpos = 0 857 self.hpos = SCREEN_HPOS 858 self.help = ScreenHelp(self) 859 self.cli = ScreenCLI(self) 860 861 self._pmid = mid # previous mode id 862 self._cmid = mid # current mode id 863 self._mode = self.modes[mid] 864 865 # Display state 866 self.active = False 867 868 # Assume a dumb TTY, setup() will detect smart features. 869 self.dumbtty = True 870 871 # Toggled by the SIGWINCH handler... 872 # Note: Defaults to true to force the initial size sync. 873 self._resized = True 874 875 # Display given exceptions on screen 876 self.exceptions = [] 877 878 # Show cursor line? 879 self.cursor = True 880 881 # Show hotkeys? 882 self.hotkeys = False 883 884 def _sigwinchhandler(self, signum, frame): 885 self._resized = True 886 887 @property 888 def resized(self): 889 if self.dumbtty: 890 ymax, xmax = self.getmaxyx() 891 return ymax != self.ymax or xmax != self.xmax 892 else: 893 return self._resized 894 895 @property 896 def mid(self): 897 return self._cmid 898 899 @property 900 def mode(self): 901 return self._mode 902 903 @property 904 def ncols(self): 905 return self.xmax - self.xmin 906 907 @property 908 def smin(self): 909 return self.hpos + 2 910 911 @property 912 def smax(self): 913 return self.ymax - 3 914 915 @property 916 def span(self): 917 return self.smax - self.smin 918 919 @property 920 def cmax(self): 921 return min(self.span, len(self.lines) - 1) 922 923 @property 924 def cstat(self): 925 return self.lines[self.vpos].stat 926 927 @property 928 def vpos(self): 929 return self.vmin + self.cpos 930 931 @property 932 def vmax(self): 933 return min(self.vmin + self.span, len(self.lines) - 1) 934 935 @property 936 def screenlines(self): 937 return enumerate(self.lines[self.vmin:self.vmax + 1]) 938 939 # Proxies 940 def getch(self, *args, **kwargs): 941 return self.screen.getch(*args, **kwargs) 942 def hline(self, *args, **kwargs): 943 return self.screen.hline(*args, **kwargs) 944 def addstr(self, *args, **kwargs): 945 return self.screen.addstr(*args, **kwargs) 946 947 def setup(self): 948 self.screen = curses_init() 949 self.screen.keypad(1) 950 self.screen.nodelay(1) 951 self.screen.idlok(1) 952 self.screen.move(0, 0) 953 curses.def_prog_mode() 954 self.help.setup() 955 self.cli.setup() 956 957 # Register some terminal resizing magic if supported... 958 if hasattr(curses, 'resize_term') and hasattr(signal, 'SIGWINCH'): 959 self.dumbtty = False 960 signal.signal(signal.SIGWINCH, self._sigwinchhandler) 961 962 # If we came this far the display is active 963 self.active = True 964 965 def getmaxyx(self): 966 ymax, xmax = self.screen.getmaxyx() 967 xmax = min(xmax, SCREEN_XMAX) 968 ymax = min(ymax, SCREEN_YMAX) 969 return ymax, xmax 970 971 def resize(self): 972 if not self.dumbtty: 973 self.clear() 974 size = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678') 975 size = struct.unpack('4H', size) 976 curses.resize_term(size[0], size[1]) 977 978 ymax, xmax = self.getmaxyx() 979 980 if xmax < SCREEN_XMIN or ymax < SCREEN_YMIN: 981 raise RuntimeError( 982 'screen too small, need at least %dx%d' 983 % (SCREEN_XMIN, SCREEN_YMIN)) 984 if ymax == self.ymax and xmax == self.xmax: 985 self._resized = False 986 return 987 if xmax != self.xmax: 988 self.xmax = xmax 989 if ymax != self.ymax: 990 self.ymax = ymax 991 992 self.mode.sync(self) 993 994 # Force re-wrapping of the screenlines in CLI mode 995 if self.mid == 5: 996 self.cli.update_screenlines() 997 self.cli.draw_output() 998 999 self._resized = False 1000 1001 def reset(self): 1002 if not self.active: 1003 return 1004 curses_reset(self.screen) 1005 1006 def recover(self): 1007 curses.reset_prog_mode() 1008 1009 def refresh(self): 1010 self.screen.noutrefresh() 1011 1012 if self.mid == 0: 1013 self.help.refresh() 1014 elif self.mid == 5: 1015 self.cli.refresh_output() 1016 self.cli.refresh_input() 1017 1018 curses.doupdate() 1019 1020 def clear(self): 1021 # Note: Forces the whole screen to be repainted upon refresh() 1022 self.screen.clear() 1023 1024 def erase(self): 1025 self.screen.erase() 1026 1027 def switch_mode(self, mid): 1028 if mid == 5 and self.data.socket.ro: 1029 return # noop 1030 1031 mode = self.modes[mid] 1032 mode.sync(self) 1033 1034 if self.mid != 5 and mid == 5: 1035 self.cli.start() 1036 elif self.mid == 5 and mid != 5: 1037 self.cli.stop() 1038 1039 self._pmid = self._cmid 1040 self._cmid, self._mode = mid, mode 1041 1042 def toggle_mode(self): 1043 if self._pmid == self._cmid: 1044 return 1045 self.switch_mode(self._pmid) 1046 1047 def cycle_mode(self, n): 1048 if n == 0: 1049 return 1050 1051 if self.data.socket.ro: 1052 border = 4 1053 else: 1054 border = 5 1055 1056 if self._cmid == 0: 1057 self.switch_mode(1) 1058 elif n < 0 and self._cmid == 1: 1059 self.switch_mode(border) 1060 elif n > 0 and self._cmid == border: 1061 self.switch_mode(1) 1062 else: 1063 self.switch_mode(self._cmid + n) 1064 1065 def update_data(self): 1066 self.data.update_info() 1067 try: 1068 self.data.update_stat() 1069 except RuntimeWarning as x: 1070 self.exceptions.append(x) 1071 1072 def update_bars(self): 1073 self.sb_conn.update_max(int(self.data.info['maxconn'], 10)) 1074 self.sb_conn.update_cur(int(self.data.info['curconn'], 10)) 1075 self.sb_pipe.update_max(int(self.data.info['maxpipes'], 10)) 1076 self.sb_pipe.update_cur(int(self.data.info['curpipes'], 10)) 1077 1078 def update_lines(self): 1079 # Display non-fatal exceptions on screen 1080 if self.exceptions: 1081 self.mvhome() 1082 self.lines = [] 1083 self.cursor = False 1084 for x in self.exceptions: 1085 for line in str(x).splitlines(): 1086 line = line.center(SCREEN_XMIN) 1087 self.lines.append(ScreenLine(text=line)) 1088 self.exceptions = [] 1089 return 1090 1091 # Reset cursor visibility 1092 if not self.cursor: 1093 self.cursor = True 1094 1095 # Update screen lines 1096 self.lines = get_screenlines(self.data.stat) 1097 if self.data.svcount >= HAPROXY_STAT_MAX_SERVICES: 1098 self.lines.append(ScreenLine()) 1099 for line in HAPROXY_STAT_LIMIT_WARNING.splitlines(): 1100 self.lines.append(ScreenLine(text=line)) 1101 1102 def draw_line(self, ypos, xpos=0, text=None, 1103 attr=curses.A_REVERSE): 1104 self.hline(ypos, self.xmin, ' ', self.xmax, attr) 1105 if text: 1106 self.addstr(ypos, self.xmin + xpos, text, attr) 1107 1108 def draw_head(self): 1109 self.draw_line(self.ymin) 1110 attr = curses.A_REVERSE | curses.A_BOLD 1111 self.addstr(self.ymin, self.xmin, 1112 time.ctime().rjust(self.xmax - 1), attr) 1113 self.addstr(self.ymin, self.xmin + 1, 1114 'HATop version ' + __version__, attr) 1115 1116 def draw_info(self): 1117 self.addstr(self.ymin + 2, self.xmin + 2, 1118 '%s Version: %s (released: %s)' % ( 1119 self.data.info['software_name'], 1120 self.data.info['software_version'], 1121 self.data.info['software_release'], 1122 ), curses.A_BOLD) 1123 self.addstr(self.ymin + 2, self.xmin + 56, 1124 'PID: %d (proc %d)' % ( 1125 int(self.data.info['pid'], 10), 1126 int(self.data.info['procn'], 10), 1127 ), curses.A_BOLD) 1128 self.addstr(self.ymin + 4, self.xmin + 2, 1129 ' Node: %s (uptime %s)' % ( 1130 self.data.info['node'] or 'unknown', 1131 self.data.info['uptime'], 1132 )) 1133 self.addstr(self.ymin + 6, self.xmin + 2, 1134 ' Pipes: %s' % self.sb_pipe) 1135 self.addstr(self.ymin + 7, self.xmin + 2, 1136 'Connections: %s' % self.sb_conn) 1137 self.addstr(self.ymin + 9, self.xmin + 2, 1138 'Procs: %3d Tasks: %5d Queue: %5d ' 1139 'Proxies: %3d Services: %4d' % ( 1140 int(self.data.info['nproc'], 10), 1141 int(self.data.info['tasks'], 10), 1142 int(self.data.info['runqueue'], 10), 1143 self.data.pxcount, 1144 self.data.svcount, 1145 )) 1146 1147 def draw_cols(self): 1148 self.draw_line(self.hpos, text=self.mode.head, 1149 attr=curses.A_REVERSE | curses.A_BOLD) 1150 1151 def draw_foot(self): 1152 xpos = self.xmin 1153 ypos = self.ymax - 1 1154 self.draw_line(ypos) 1155 attr_active = curses.A_BOLD 1156 attr_inactive = curses.A_BOLD | curses.A_REVERSE 1157 1158 # HOTKEYS 1159 if (self.hotkeys and 1160 0 < self.mid < 5 and 1161 self.cstat and 1162 self.cstat['iid'] > 0 and 1163 self.cstat['sid'] > 0): 1164 self.draw_line(ypos) 1165 self.addstr(ypos, 1, 'HOTKEYS:', 1166 curses.A_BOLD | curses.A_REVERSE) 1167 self.addstr(ypos, 11, 1168 'F1=ENABLE F2=DISABLE ' 1169 'F4=W-RESET ' 1170 'F5=W-10 F6=W-1 F7=W+1 F8=W+10 ' 1171 'F9=ENABLE (HERE) F10=DISABLE (HERE)', 1172 curses.A_NORMAL | curses.A_REVERSE) 1173 return 1174 1175 # VIEWPORTS 1176 for mid, mode in enumerate(self.modes): 1177 if mid == 0: 1178 continue 1179 if mid == 5 and self.data.socket.ro: 1180 continue 1181 if mid == self.mid: 1182 attr = attr_active 1183 else: 1184 attr = attr_inactive 1185 1186 s = ' %d-%s ' % (mid, mode.name) 1187 self.addstr(ypos, xpos, s, attr) 1188 xpos += len(s) 1189 1190 if 0 < self.mid < 5 and self.cstat: 1191 if self.cstat['iid'] > 0 and self.cstat['sid'] > 0: 1192 if self.data.socket.ro: 1193 s = 'READ-ONLY [#%d/#%d]' % ( 1194 self.cstat['iid'], self.cstat['sid']) 1195 else: 1196 s = 'ENTER=MENU SPACE=SEL [#%d/#%d]' % ( 1197 self.cstat['iid'], self.cstat['sid']) 1198 else: 1199 if self.data.socket.ro: 1200 s = 'READ-ONLY [#%d/#%d]' % ( 1201 self.cstat['iid'], self.cstat['sid']) 1202 else: 1203 s = '[#%d/#%d]' % ( 1204 self.cstat['iid'], self.cstat['sid']) 1205 elif self.mid == 5: 1206 s = 'PGUP/PGDOWN=SCROLL' 1207 else: 1208 s = 'UP/DOWN=SCROLL H=HELP Q=QUIT' 1209 self.addstr(ypos, self.xmax - len(s) - 1, s, attr_inactive) 1210 1211 def draw_stat(self): 1212 for idx, line in self.screenlines: 1213 if self.cursor and idx == self.cpos: 1214 attr = line.attr | curses.A_REVERSE 1215 else: 1216 attr = line.attr 1217 if not line.stat: 1218 screenline = get_cell(self.xmax, 'L', line.text) 1219 elif 'message' in line.stat: 1220 screenline = get_cell(self.xmax, 'L', line.stat['message']) 1221 else: 1222 screenline = get_screenline(self.mode, line.stat) 1223 self.addstr(self.smin + idx, self.xmin, screenline, attr) 1224 1225 def draw_mode(self): 1226 if self.mid == 0: 1227 self.help.draw() 1228 elif self.mid == 5 and self._pmid == self._cmid: 1229 self.cli.start() # initial mid was 5 1230 elif 0 < self.mid < 5: 1231 self.draw_stat() 1232 1233 def mvc(self, n): 1234 if n == 0: 1235 return 1236 1237 # Move DOWN 1238 if n > 0: 1239 # move cursor 1240 if self.cpos < self.cmax: 1241 self.cpos = min(self.cmax, self.cpos + n) 1242 return 1243 # move screenlines 1244 maxvmin = max(0, len(self.lines) - self.span - 1) 1245 if self.cpos == self.cmax and self.vmin < maxvmin: 1246 self.vmin = min(maxvmin, self.vmin + n) 1247 1248 # Move UP 1249 elif n < 0: # UP 1250 # move cursor 1251 if self.cpos > self.cmin: 1252 self.cpos = max(self.cmin, self.cpos + n) 1253 return 1254 # move screenlines 1255 if self.cpos == self.cmin and self.vmin > 0: 1256 self.vmin = max(0, self.vmin + n) 1257 1258 def mvhome(self): 1259 # move cursor 1260 if self.cpos != self.cmin: 1261 self.cpos = self.cmin 1262 # move screenlines 1263 if self.vmin != 0: 1264 self.vmin = 0 1265 1266 def mvend(self): 1267 # move cursor 1268 if self.cpos != self.cmax: 1269 self.cpos = self.cmax 1270 # move screenlines 1271 maxvmin = max(0, len(self.lines) - self.span - 1) 1272 if self.vmin != maxvmin: 1273 self.vmin = maxvmin 1274 1275 1276class ScreenHelp: 1277 1278 def __init__(self, screen): 1279 self.screen = screen 1280 self.xmin = screen.xmin + 1 1281 self.xmax = screen.xmax 1282 self.ymin = 0 1283 self.ymax = __doc__.count('\n') 1284 self.xpos = 0 1285 self.ypos = 0 1286 1287 def setup(self): 1288 self.pad = curses.newpad(self.ymax + 1, self.xmax + 1) 1289 1290 def addstr(self, *args, **kwargs): 1291 return self.pad.addstr(*args, **kwargs) 1292 1293 def refresh(self): 1294 self.pad.noutrefresh( 1295 self.ypos, self.xpos, 1296 self.screen.smin, self.xmin, 1297 self.screen.smax, self.xmax - 2) 1298 1299 def draw(self): 1300 self.addstr(0, 0, __doc__) 1301 1302 def mvc(self, n): 1303 if n == 0: 1304 return 1305 1306 # Move DOWN 1307 if n > 0: 1308 self.ypos = min(self.ymax - self.screen.span, self.ypos + n) 1309 1310 # Move UP 1311 elif n < 0: 1312 self.ypos = max(self.ymin, self.ypos + n) 1313 1314 def mvhome(self): 1315 self.ypos = self.ymin 1316 1317 def mvend(self): 1318 self.ypos = self.ymax - self.screen.span 1319 1320 1321class ScreenMode: 1322 1323 def __init__(self, name): 1324 self.name = name 1325 self.columns = [] 1326 1327 @property 1328 def head(self): 1329 return get_head(self) 1330 1331 def sync(self, screen): 1332 for idx, column in enumerate(self.columns): 1333 column.width = get_width(column.minwidth, screen.xmax, 1334 len(self.columns), idx) 1335 1336 1337class ScreenColumn: 1338 1339 def __init__(self, name, header, minwidth, maxwidth, align, filters={}): 1340 self.name = name 1341 self.header = header 1342 self.align = align 1343 self.minwidth = minwidth 1344 self.maxwidth = maxwidth 1345 self.width = minwidth 1346 self.filters = {'always': [], 'ondemand': []} 1347 self.filters.update(filters) 1348 1349 def get_width(self): 1350 return self._width 1351 1352 def set_width(self, n): 1353 if self.maxwidth: 1354 self._width = min(self.maxwidth, n) 1355 self._width = max(self.minwidth, n) 1356 1357 width = property(get_width, set_width) 1358 1359 1360class ScreenLine: 1361 1362 def __init__(self, stat=None, text='', attr=0): 1363 self.stat = stat 1364 self.text = text 1365 self.attr = attr 1366 1367 1368class StatusBar: 1369 1370 def __init__(self, width=60, min=0, max=100, status=True): 1371 self.width = width 1372 self.curval = min 1373 self.minval = min 1374 self.maxval = max 1375 self.status = status 1376 self.prepend = '[' 1377 self.append = ']' 1378 self.usedchar = '|' 1379 self.freechar = ' ' 1380 1381 def __str__(self): 1382 if self.status: 1383 status = '%d/%d' % (self.curval, self.maxval) 1384 1385 space = self.width - len(self.prepend) - len(self.append) 1386 span = self.maxval - self.minval 1387 1388 if span: 1389 used = min(float(self.curval) / float(span), 1.0) 1390 else: 1391 used = 0.0 1392 free = 1.0 - used 1393 1394 # 100% equals full bar width, ignoring status text within the bar 1395 bar = self.prepend 1396 bar += self.usedchar * int(space * used) 1397 bar += self.freechar * int(space * free) 1398 if self.status: 1399 bar = bar[:(self.width - len(status) - len(self.append))] 1400 bar += status 1401 bar += self.append 1402 1403 return bar 1404 1405 def update_cur(self, value): 1406 value = min(self.maxval, value) 1407 value = max(self.minval, value) 1408 self.curval = value 1409 1410 def update_max(self, value): 1411 if value >= self.minval: 1412 self.maxval = value 1413 else: 1414 self.maxval = self.minval 1415 1416# ------------------------------------------------------------------------- # 1417# DISPLAY FILTERS # 1418# ------------------------------------------------------------------------- # 1419 1420def human_seconds(numeric): 1421 for minval, prefix in sorted(list(PREFIX_TIME.items()), reverse=True): 1422 if (numeric/minval): 1423 return '%d%s' % (numeric/minval, prefix) 1424 return '%ds' % numeric 1425 1426def human_metric(numeric): 1427 for minval, prefix in sorted(list(PREFIX_METRIC.items()), reverse=True): 1428 if (numeric/minval): 1429 return '%d%s' % (numeric/minval, prefix) 1430 return str(numeric) 1431 1432def human_binary(numeric): 1433 for minval, prefix in sorted(list(PREFIX_BINARY.items()), reverse=True): 1434 if (numeric/minval): 1435 return '%.2f%s' % (float(numeric)/float(minval), prefix) 1436 return '%dB' % numeric 1437 1438def trim(string, length): 1439 if len(string) <= length: 1440 return string 1441 if length == 1: 1442 return string[0] 1443 if length > 5: 1444 return '..%s' % string[-(length-2):] 1445 return '...' 1446 1447# ------------------------------------------------------------------------- # 1448# SCREEN LAYOUT # 1449# ------------------------------------------------------------------------- # 1450 1451SCREEN_MODES = [ 1452 ScreenMode('HELP'), 1453 ScreenMode('STATUS'), 1454 ScreenMode('TRAFFIC'), 1455 ScreenMode('HTTP'), 1456 ScreenMode('ERRORS'), 1457 ScreenMode('CLI'), 1458] 1459 1460# Mode: HELP name header xmin xmax align 1461SCREEN_MODES[0].columns = [ 1462 ScreenColumn('help', ' HATop Online Help ', 1463 SCREEN_XMIN, 0, 'L'), 1464] 1465 1466# Mode: STATUS name header xmin xmax align 1467SCREEN_MODES[1].columns = [ 1468 ScreenColumn('svname', 'NAME', 10, 50, 'L'), 1469 ScreenColumn('weight', 'W', 4, 6, 'R'), 1470 ScreenColumn('status', 'STATUS', 6, 10, 'L'), 1471 ScreenColumn('check_status', 'CHECK', 7, 20, 'L'), 1472 ScreenColumn('act', 'ACT', 3, 0, 'R', 1473 filters={'ondemand': [human_metric]}), 1474 ScreenColumn('bck', 'BCK', 3, 0, 'R', 1475 filters={'ondemand': [human_metric]}), 1476 ScreenColumn('qcur', 'QCUR', 5, 0, 'R', 1477 filters={'ondemand': [human_metric]}), 1478 ScreenColumn('qmax', 'QMAX', 5, 0, 'R', 1479 filters={'ondemand': [human_metric]}), 1480 ScreenColumn('scur', 'SCUR', 6, 0, 'R', 1481 filters={'ondemand': [human_metric]}), 1482 ScreenColumn('smax', 'SMAX', 6, 0, 'R', 1483 filters={'ondemand': [human_metric]}), 1484 ScreenColumn('slim', 'SLIM', 6, 0, 'R', 1485 filters={'ondemand': [human_metric]}), 1486 ScreenColumn('stot', 'STOT', 6, 0, 'R', 1487 filters={'ondemand': [human_metric]}), 1488] 1489 1490# Mode: TRAFFIC name header xmin xmax align 1491SCREEN_MODES[2].columns = [ 1492 ScreenColumn('svname', 'NAME', 10, 50, 'L'), 1493 ScreenColumn('weight', 'W', 4, 6, 'R'), 1494 ScreenColumn('status', 'STATUS', 6, 10, 'L'), 1495 ScreenColumn('lbtot', 'LBTOT', 8, 0, 'R', 1496 filters={'ondemand': [human_metric]}), 1497 ScreenColumn('rate', 'RATE', 6, 0, 'R', 1498 filters={'ondemand': [human_metric]}), 1499 ScreenColumn('rate_lim', 'RLIM', 6, 0, 'R', 1500 filters={'ondemand': [human_metric]}), 1501 ScreenColumn('rate_max', 'RMAX', 6, 0, 'R', 1502 filters={'ondemand': [human_metric]}), 1503 ScreenColumn('bin', 'BIN', 12, 0, 'R', 1504 filters={'always': [human_binary]}), 1505 ScreenColumn('bout', 'BOUT', 12, 0, 'R', 1506 filters={'always': [human_binary]}), 1507] 1508 1509# Mode: HTTP name header xmin xmax align 1510SCREEN_MODES[3].columns = [ 1511 ScreenColumn('svname', 'NAME', 10, 50, 'L'), 1512 ScreenColumn('weight', 'W', 4, 6, 'R'), 1513 ScreenColumn('status', 'STATUS', 6, 10, 'L'), 1514 ScreenColumn('req_rate', 'RATE', 5, 0, 'R', 1515 filters={'ondemand': [human_metric]}), 1516 ScreenColumn('req_rate_max', 'RMAX', 5, 0, 'R', 1517 filters={'ondemand': [human_metric]}), 1518 ScreenColumn('req_tot', 'RTOT', 7, 0, 'R', 1519 filters={'ondemand': [human_metric]}), 1520 ScreenColumn('hrsp_1xx', '1xx', 5, 0, 'R', 1521 filters={'ondemand': [human_metric]}), 1522 ScreenColumn('hrsp_2xx', '2xx', 5, 0, 'R', 1523 filters={'ondemand': [human_metric]}), 1524 ScreenColumn('hrsp_3xx', '3xx', 5, 0, 'R', 1525 filters={'ondemand': [human_metric]}), 1526 ScreenColumn('hrsp_4xx', '4xx', 5, 0, 'R', 1527 filters={'ondemand': [human_metric]}), 1528 ScreenColumn('hrsp_5xx', '5xx', 5, 0, 'R', 1529 filters={'ondemand': [human_metric]}), 1530 ScreenColumn('hrsp_other', '?xx', 5, 0, 'R', 1531 filters={'ondemand': [human_metric]}), 1532] 1533 1534# Mode: ERRORS name header xmin xmax align 1535SCREEN_MODES[4].columns = [ 1536 ScreenColumn('svname', 'NAME', 10, 50, 'L'), 1537 ScreenColumn('weight', 'W', 4, 6, 'R'), 1538 ScreenColumn('status', 'STATUS', 6, 10, 'L'), 1539 ScreenColumn('check_status', 'CHECK', 7, 20, 'L'), 1540 ScreenColumn('chkfail', 'CF', 3, 0, 'R', 1541 filters={'ondemand': [human_metric]}), 1542 ScreenColumn('chkdown', 'CD', 3, 0, 'R', 1543 filters={'ondemand': [human_metric]}), 1544 ScreenColumn('lastchg', 'CL', 3, 0, 'R', 1545 filters={'always': [human_seconds]}), 1546 ScreenColumn('econ', 'ECONN', 5, 0, 'R', 1547 filters={'ondemand': [human_metric]}), 1548 ScreenColumn('ereq', 'EREQ', 5, 0, 'R', 1549 filters={'ondemand': [human_metric]}), 1550 ScreenColumn('eresp', 'ERSP', 5, 0, 'R', 1551 filters={'ondemand': [human_metric]}), 1552 ScreenColumn('dreq', 'DREQ', 5, 0, 'R', 1553 filters={'ondemand': [human_metric]}), 1554 ScreenColumn('dresp', 'DRSP', 5, 0, 'R', 1555 filters={'ondemand': [human_metric]}), 1556 ScreenColumn('downtime', 'DOWN', 5, 0, 'R', 1557 filters={'always': [human_seconds]}), 1558] 1559 1560# Mode: CLI name header xmin xmax align 1561SCREEN_MODES[5].columns = [ 1562 ScreenColumn('cli', 1563 ' haproxy command line ' 1564 ' use ALT-n / ESC-n to escape', 1565 SCREEN_XMIN, 0, 'L'), 1566] 1567 1568# ------------------------------------------------------------------------- # 1569# HELPERS # 1570# ------------------------------------------------------------------------- # 1571 1572def log(msg): 1573 sys.stderr.write('%s\n' % msg) 1574 1575def parse_stat(iterable): 1576 pxcount = svcount = 0 1577 pxstat = {} # {iid: {sid: svstat, ...}, ...} 1578 1579 idx_iid = get_idx('iid') 1580 idx_sid = get_idx('sid') 1581 1582 for line in iterable: 1583 if not line: 1584 continue 1585 if line.startswith(HAPROXY_STAT_COMMENT): 1586 continue # comment 1587 if line.count(HAPROXY_STAT_SEP) < HAPROXY_STAT_NUMFIELDS: 1588 continue # unknown format 1589 1590 csv = line.split(HAPROXY_STAT_SEP, HAPROXY_STAT_NUMFIELDS) 1591 1592 # Skip further parsing? 1593 if svcount > HAPROXY_STAT_MAX_SERVICES: 1594 try: 1595 iid = csv[idx_iid] 1596 iid = int(iid, 10) 1597 except ValueError: 1598 raise RuntimeError( 1599 'garbage proxy identifier: iid="%s" (need %s)' % 1600 (iid, int)) 1601 try: 1602 sid = csv[idx_sid] 1603 sid = int(sid, 10) 1604 except ValueError: 1605 raise RuntimeError( 1606 'garbage service identifier: sid="%s" (need %s)' % 1607 (sid, int)) 1608 if iid not in pxstat: 1609 pxcount += 1 1610 svcount += 1 1611 elif sid not in pxstat[iid]: 1612 svcount += 1 1613 continue 1614 1615 # Parse stat... 1616 svstat = {} # {field: value, ...} 1617 1618 for idx, field in HAPROXY_STAT_CSV: 1619 field_type, field_name = field 1620 value = csv[idx] 1621 1622 try: 1623 if field_type is int: 1624 if len(value): 1625 value = int(value, 10) 1626 else: 1627 value = 0 1628 elif field_type is not type(value): 1629 value = field_type(value) 1630 except ValueError: 1631 raise RuntimeError('garbage field: %s="%s" (need %s)' % ( 1632 field_name, value, field_type)) 1633 1634 # Special case 1635 if field_name == 'status' and value == 'no check': 1636 value = '-' 1637 elif field_name == 'check_status' and svstat['status'] == '-': 1638 value = 'none' 1639 1640 svstat[field_name] = value 1641 1642 # Record result... 1643 iid = svstat['iid'] 1644 stype = svstat['type'] 1645 1646 if stype == 0 or stype == 1: # FRONTEND / BACKEND 1647 id = svstat['svname'] 1648 else: 1649 id = svstat['sid'] 1650 1651 try: 1652 pxstat[iid][id] = svstat 1653 except KeyError: 1654 pxstat[iid] = { id: svstat } 1655 pxcount += 1 1656 svcount += 1 1657 1658 return pxstat, pxcount, svcount 1659 1660def parse_info(iterable): 1661 info = {} 1662 for line in iterable: 1663 line = line.strip() 1664 if not line: 1665 continue 1666 for key, regexp in HAPROXY_INFO_RE.items(): 1667 match = regexp.match(line) 1668 if match: 1669 info[key] = match.group('value') 1670 break 1671 1672 for key in HAPROXY_INFO_RE.keys(): 1673 if not key in info: 1674 raise RuntimeError('missing "%s" in info data' % key) 1675 1676 return info 1677 1678def get_idx(field): 1679 return [x for x in HAPROXY_STAT_CSV if x[1][1] == field][0][0] 1680 1681def get_width(width, xmax, ncols, idx): 1682 # distribute excess space evenly from left to right 1683 if xmax > SCREEN_XMIN: 1684 xdiff = xmax - SCREEN_XMIN 1685 if xdiff <= ncols: 1686 if idx < xdiff: 1687 width += 1 1688 else: 1689 if idx < (xdiff - (xdiff / ncols) * ncols): 1690 width += 1 # compensate rounding 1691 width = width + xdiff // ncols 1692 return width 1693 1694def get_cell(width, align, value): 1695 s = str(value) 1696 if align == 'L': 1697 s = s.ljust(width) 1698 elif align == 'C': 1699 s = s.center(width) 1700 elif align == 'R': 1701 s = s.rjust(width) 1702 return s 1703 1704def get_head(mode): 1705 columns = [] 1706 for column in mode.columns: 1707 s = column.header 1708 s = get_cell(column.width, column.align, s) 1709 columns.append(s) 1710 return ' '.join(columns) 1711 1712def get_screenlines(stat): 1713 screenlines = [] 1714 1715 for iid, svstats in stat.items(): 1716 lines = [] 1717 1718 try: 1719 frontend = svstats.pop('FRONTEND') 1720 except KeyError: 1721 frontend = None 1722 try: 1723 backend = svstats.pop('BACKEND') 1724 except KeyError: 1725 backend = None 1726 1727 if frontend: 1728 lines.append(ScreenLine(stat=frontend)) 1729 1730 for sid, svstat in sorted(svstats.items()): 1731 lines.append(ScreenLine(stat=svstat)) 1732 1733 if backend: 1734 lines.append(ScreenLine(stat=backend)) 1735 1736 if not len(lines): 1737 continue 1738 1739 pxname = lines[0].stat['pxname'] 1740 screenlines.append(ScreenLine(attr=curses.A_BOLD, 1741 text='>>> %s' % pxname)) 1742 screenlines += lines 1743 screenlines.append(ScreenLine()) 1744 1745 # remove trailing empty line 1746 if len(screenlines) > 1: 1747 screenlines.pop() 1748 1749 return screenlines 1750 1751def get_screenline(mode, stat): 1752 cells = [] 1753 for column in mode.columns: 1754 value = stat[column.name] 1755 1756 for filter in column.filters['always']: 1757 value = list(filter(value)) 1758 1759 if len(str(value)) > column.width: 1760 for filter in column.filters['ondemand']: 1761 value = list(filter(value)) 1762 1763 value = str(value) 1764 value = trim(value, column.width) 1765 cells.append(get_cell(column.width, column.align, value)) 1766 1767 return ' '.join(cells) 1768 1769# ------------------------------------------------------------------------- # 1770# CURSES HELPERS # 1771# ------------------------------------------------------------------------- # 1772 1773def curses_init(): 1774 screen = curses.initscr() 1775 curses.noecho() 1776 curses.nonl() 1777 curses.raw() 1778 1779 # Some terminals don't support different cursor visibilities 1780 try: 1781 curses.curs_set(0) 1782 except CursesError: 1783 pass 1784 1785 # Some terminals don't support the default color palette 1786 try: 1787 curses.start_color() 1788 curses.use_default_colors() 1789 except CursesError: 1790 pass 1791 1792 return screen 1793 1794def curses_reset(screen): 1795 if not screen: 1796 return 1797 screen.keypad(0) 1798 curses.noraw() 1799 curses.echo() 1800 curses.endwin() 1801 1802# ------------------------------------------------------------------------- # 1803# MAIN LOOP # 1804# ------------------------------------------------------------------------- # 1805 1806def mainloop(screen, interval): 1807 # Sleep time of each iteration in seconds 1808 scan = 1.0 / 100.0 1809 # Query socket and redraw the screen in the given interval 1810 iterations = interval / scan 1811 1812 i = 0 1813 update_stat = True # Toggle stat update (query socket, parse csv) 1814 update_lines = True # Toggle screen line update (sync with stat data) 1815 update_display = True # Toggle screen update (resize, repaint, refresh) 1816 switch_mode = False # Toggle mode / viewport switch 1817 1818 while True: 1819 1820 # Resize toggled by SIGWINCH? 1821 if screen.resized: 1822 screen.resize() 1823 update_display = True 1824 1825 # Update interval reached... 1826 if i == iterations: 1827 update_stat = True 1828 if 0 < screen.mid < 5: 1829 update_lines = True 1830 update_display = True 1831 i = 0 1832 1833 # Refresh screen? 1834 if update_display: 1835 1836 if update_stat: 1837 screen.update_data() 1838 screen.update_bars() 1839 update_stat = False 1840 1841 if update_lines: 1842 screen.update_lines() 1843 update_lines = False 1844 1845 screen.erase() 1846 screen.draw_head() 1847 screen.draw_info() 1848 screen.draw_cols() 1849 screen.draw_mode() 1850 screen.draw_foot() 1851 screen.refresh() 1852 1853 update_display = False 1854 1855 c = screen.getch() 1856 1857 if c < 0: 1858 time.sleep(scan) 1859 i += 1 1860 continue 1861 1862 # Toggle hotkey footer 1863 if screen.hotkeys: 1864 if c not in ( 1865 curses.KEY_F1, # enable server on all backends 1866 curses.KEY_F2, # disable server on all backends 1867 curses.KEY_F4, # reset weight 1868 curses.KEY_F5, # weight -10 1869 curses.KEY_F6, # weight -1 1870 curses.KEY_F7, # weight +1 1871 curses.KEY_F8, # weight +10 1872 curses.KEY_F9, # enable server on one backend 1873 curses.KEY_F10, # disable server on one backend 1874 ): 1875 screen.hotkeys = False 1876 update_display = True 1877 if c in ( 1878 curses.KEY_ENTER, 1879 curses.ascii.CR 1880 ): 1881 continue 1882 1883 if c == curses.ascii.ETX: 1884 raise KeyboardInterrupt() 1885 1886 # Mode switch (ALT-n / ESC-n) or toggle (ESC / ESC-ESC) 1887 if c == curses.ascii.ESC: 1888 c = screen.getch() 1889 if c < 0 or c == curses.ascii.ESC: 1890 screen.toggle_mode() 1891 update_display = True 1892 continue 1893 if 0 < c < 256: 1894 c = chr(c) 1895 if c in 'qQHh?12345': 1896 switch_mode = True 1897 1898 # Mode cycle (TAB / BTAB) 1899 elif c == ord('\t'): 1900 screen.cycle_mode(1) 1901 update_display = True 1902 continue 1903 elif c == curses.KEY_BTAB: 1904 screen.cycle_mode(-1) 1905 update_display = True 1906 continue 1907 1908 # Mode switch in non-CLI modes using the number only 1909 elif 0 <= screen.mid < 5 and 0 < c < 256: 1910 c = chr(c) 1911 if c in 'qQHh?12345': 1912 switch_mode = True 1913 1914 if switch_mode: 1915 switch_mode = False 1916 if c in 'qQ': 1917 raise StopIteration() 1918 if c != str(screen.mid) or (c in 'Hh?' and screen.mid != 0): 1919 if c in 'Hh?': 1920 screen.switch_mode(0) 1921 elif c in '12345': 1922 screen.switch_mode(int(c)) 1923 1924 # Force screen update with existing data 1925 update_display = True 1926 continue 1927 1928 # -> HELP 1929 if screen.mid == 0: 1930 if c == curses.KEY_UP and screen.help.ypos > 0: 1931 screen.help.mvc(-1) 1932 elif c == curses.KEY_DOWN and \ 1933 screen.help.ypos < screen.help.ymax - screen.span: 1934 screen.help.mvc(1) 1935 elif c == curses.KEY_PPAGE and screen.help.ypos > 0: 1936 screen.help.mvc(-10) 1937 elif c == curses.KEY_NPAGE and \ 1938 screen.help.ypos < screen.help.ymax - screen.span: 1939 screen.help.mvc(10) 1940 elif c == curses.ascii.SOH or c == curses.KEY_HOME: 1941 screen.help.mvhome() 1942 elif c == curses.ascii.ENQ or c == curses.KEY_END: 1943 screen.help.mvend() 1944 1945 # -> STATUS / TRAFFIC / HTTP / ERRORS 1946 elif 1 <= screen.mid <= 4: 1947 1948 # movements 1949 if c == curses.KEY_UP: 1950 screen.mvc(-1) 1951 elif c == curses.KEY_DOWN: 1952 screen.mvc(1) 1953 elif c == curses.KEY_PPAGE: 1954 screen.mvc(-10) 1955 elif c == curses.KEY_NPAGE: 1956 screen.mvc(10) 1957 elif c == curses.ascii.SOH or c == curses.KEY_HOME: 1958 screen.mvhome() 1959 elif c == curses.ascii.ENQ or c == curses.KEY_END: 1960 screen.mvend() 1961 1962 # actions 1963 elif c in ( 1964 curses.KEY_ENTER, # show hotkeys 1965 chr(curses.ascii.CR), # show hotkeys 1966 chr(curses.ascii.SP), # copy & paste identifier 1967 curses.KEY_F1, # enable server on all backends 1968 curses.KEY_F2, # disable server on all backends 1969 curses.KEY_F4, # reset weight 1970 curses.KEY_F5, # weight -10 1971 curses.KEY_F6, # weight -1 1972 curses.KEY_F7, # weight +1 1973 curses.KEY_F8, # weight +10 1974 curses.KEY_F9, # enable server on one backend 1975 curses.KEY_F10, # disable server on one backend 1976 ): 1977 if screen.data.socket.ro: 1978 continue 1979 if not screen.cstat: 1980 continue 1981 1982 if c == curses.KEY_ENTER or c == chr(curses.ascii.CR): 1983 screen.hotkeys = True 1984 update_display = True 1985 continue 1986 1987 iid = screen.cstat['iid'] 1988 sid = screen.cstat['sid'] 1989 1990 if iid <= 0 or sid <= 0: 1991 continue 1992 1993 pxname = screen.cstat['pxname'] 1994 svname = screen.cstat['svname'] 1995 1996 if not pxname or not svname: 1997 continue 1998 1999 notify = False 2000 2001 if c == ' ': 2002 if screen.cli.puts('%s/%s' % (pxname, svname)): 2003 screen.switch_mode(5) 2004 update_display = True 2005 continue 2006 elif c == curses.KEY_F1 or c == curses.KEY_F2: 2007 cliname = 'enable server' 2008 if c == curses.KEY_F2: 2009 cliname = 'disable server' 2010 2011 # loop on all proxies (backends) 2012 for px in screen.data.stat.values(): 2013 # in a given proxy, loop on all services id (unique inside a proxy) 2014 # a service can be a frontend, backend, server or socket object 2015 for sv in px.values(): 2016 if sv['type'] == 2: # only the type "server" interests us 2017 # selects only the service name (hostname) 2018 # which matches that of the current line 2019 if sv['svname'] == svname: 2020 screen.cli.execute_cmdline( 2021 '%s %s/%s' % (cliname, sv['pxname'], svname)) 2022 notify = True 2023 elif c == curses.KEY_F4: 2024 screen.cli.execute_cmdline( 2025 'set weight %s/%s 100%%' % (pxname, svname)) 2026 notify = True 2027 elif c == curses.KEY_F5: 2028 curweight = screen.cstat['weight'] 2029 if curweight <= 0: 2030 continue 2031 weight = max(0, curweight - 10) # - 10 2032 screen.cli.execute_cmdline( 2033 'set weight %s/%s %d' % (pxname, svname, weight)) 2034 notify = True 2035 elif c == curses.KEY_F6: 2036 curweight = screen.cstat['weight'] 2037 if curweight <= 0: 2038 continue 2039 weight = max(0, curweight - 1) # - 1 2040 screen.cli.execute_cmdline( 2041 'set weight %s/%s %d' % (pxname, svname, weight)) 2042 notify = True 2043 elif c == curses.KEY_F7: 2044 curweight = screen.cstat['weight'] 2045 if curweight >= 256: 2046 continue 2047 weight = min(256, curweight + 1) # + 1 2048 screen.cli.execute_cmdline( 2049 'set weight %s/%s %d' % (pxname, svname, weight)) 2050 notify = True 2051 elif c == curses.KEY_F8: 2052 curweight = screen.cstat['weight'] 2053 if curweight >= 256: 2054 continue 2055 weight = min(256, curweight + 10) # + 10 2056 screen.cli.execute_cmdline( 2057 'set weight %s/%s %d' % (pxname, svname, weight)) 2058 notify = True 2059 elif c == curses.KEY_F9: 2060 screen.cli.execute_cmdline( 2061 'enable server %s/%s' % (pxname, svname)) 2062 notify = True 2063 elif c == curses.KEY_F10: 2064 screen.cli.execute_cmdline( 2065 'disable server %s/%s' % (pxname, svname)) 2066 notify = True 2067 2068 # Refresh the screen indicating pending changes... 2069 if notify: 2070 screen.cstat['message'] = 'updating...' 2071 update_display = True 2072 continue 2073 2074 # -> CLI 2075 elif screen.mid == 5: 2076 2077 # enter 2078 if c == curses.KEY_ENTER or c == curses.ascii.CR: 2079 screen.cli.execute() 2080 2081 # input movements 2082 elif c == curses.KEY_LEFT: 2083 screen.cli.mvc(-1) 2084 elif c == curses.KEY_RIGHT: 2085 screen.cli.mvc(1) 2086 elif c == curses.ascii.SOH or c == curses.KEY_HOME: 2087 screen.cli.mvhome() 2088 elif c == curses.ascii.ENQ or c == curses.KEY_END: 2089 screen.cli.mvend() 2090 2091 # input editing 2092 elif c == curses.ascii.ETB: 2093 pass # TODO (CTRL-W) 2094 elif c == curses.KEY_DC: 2095 screen.cli.delc(1) 2096 elif c == curses.KEY_BACKSPACE or c == curses.ascii.DEL: 2097 screen.cli.delc(-1) 2098 2099 # input history 2100 elif c == curses.KEY_UP: 2101 screen.cli.prev() 2102 elif c == curses.KEY_DOWN: 2103 next(screen.cli) 2104 2105 # output history 2106 elif c == curses.KEY_PPAGE: 2107 screen.cli.mvo(-1) 2108 elif c == curses.KEY_NPAGE: 2109 screen.cli.mvo(1) 2110 2111 elif 0 < c < 256: 2112 screen.cli.putc(chr(c)) 2113 2114 # Force screen update with existing data if key was a movement key 2115 if c in ( 2116 curses.KEY_UP, 2117 curses.KEY_DOWN, 2118 curses.KEY_PPAGE, 2119 curses.KEY_NPAGE, 2120 ) or screen.mid != 5 and c in ( 2121 curses.ascii.SOH, curses.KEY_HOME, 2122 curses.ascii.ENQ, curses.KEY_END, 2123 ): 2124 update_display = True 2125 2126 time.sleep(scan) 2127 i += 1 2128 2129 2130if __name__ == '__main__': 2131 2132 from optparse import OptionParser, OptionGroup 2133 2134 version = 'hatop version %s' % __version__ 2135 usage = 'Usage: hatop (-s SOCKET| -t HOST:PORT) [OPTIONS]...' 2136 2137 parser = OptionParser(usage=usage, version=version) 2138 2139 opts = OptionGroup(parser, 'Mandatory') 2140 opts.add_option('-s', '--unix-socket', dest='socket', 2141 help='path to the haproxy unix stats socket') 2142 opts.add_option('-t', '--tcp-socket', dest='tcp_socket', 2143 help='address of the haproxy tcp stats socket') 2144 parser.add_option_group(opts) 2145 2146 opts = OptionGroup(parser, 'Optional') 2147 opts.add_option('-i', '--update-interval', type='int', dest='interval', 2148 help='update interval in seconds (1-30, default: 3)', default=3) 2149 opts.add_option('-m', '--mode', type='int', dest='mode', 2150 help='start in specific mode (1-5, default: 1)', default=1) 2151 opts.add_option('-n', '--read-only', action='store_true', dest='ro', 2152 help='disable the cli and query for stats only') 2153 parser.add_option_group(opts) 2154 2155 opts = OptionGroup(parser, 'Filters', 2156 'Note: All filter options may be given multiple times.') 2157 opts.add_option('-f', '--filter', action='append', dest='stat_filter', 2158 default=[], metavar='FILTER', 2159 help='stat filter in format "<iid> <type> <sid>"') 2160 opts.add_option('-p', '--proxy', action='append', dest='proxy_filter', 2161 default=[], metavar='PROXY', 2162 help='proxy filter in format "<pxname>"') 2163 parser.add_option_group(opts) 2164 2165 opts, args = parser.parse_args() 2166 2167 if not 1 <= opts.interval <= 30: 2168 log('invalid update interval: %d' % opts.interval) 2169 sys.exit(1) 2170 if not 1 <= opts.mode <= 5: 2171 log('invalid mode: %d' % opts.mode) 2172 sys.exit(1) 2173 if len(opts.stat_filter) + len(opts.proxy_filter) > 50: 2174 log('filter limit exceeded (50)') 2175 sys.exit(1) 2176 if opts.ro and opts.mode == 5: 2177 log('cli not available in read-only mode') 2178 sys.exit(1) 2179 if opts.socket and opts.tcp_socket: 2180 log('Specify either --unix-socket OR --tcp-socket') 2181 parser.print_help() 2182 sys.exit(1) 2183 if not opts.socket and not opts.tcp_socket: 2184 parser.print_help() 2185 sys.exit(0) 2186 if opts.socket and not os.access(opts.socket, os.R_OK | os.W_OK): 2187 log('insufficient permissions for socket path %s' % opts.socket) 2188 sys.exit(2) 2189 2190 if opts.socket: 2191 socket = Socket(opts.socket, opts.ro) 2192 else: 2193 socket = Socket(opts.tcp_socket, opts.ro, True) 2194 2195 data = SocketData(socket) 2196 screen = Screen(data, opts.mode) 2197 2198 signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0)) 2199 2200 try: 2201 try: 2202 socket.connect() 2203 screen.setup() 2204 2205 # Register filters 2206 data.register_stat_filter(opts.stat_filter) 2207 data.register_proxy_filter(opts.proxy_filter) 2208 2209 while True: 2210 try: 2211 mainloop(screen, opts.interval) 2212 except StopIteration: 2213 break 2214 except KeyboardInterrupt: 2215 break 2216 except CursesError as e: 2217 screen.reset() 2218 log('curses error: %s, restarting...' % e) 2219 time.sleep(1) 2220 screen.recover() 2221 2222 except ValueError as e: 2223 screen.reset() 2224 log('value error: %s' % e) 2225 sys.exit(1) 2226 except RuntimeError as e: 2227 screen.reset() 2228 log('runtime error: %s' % e) 2229 sys.exit(1) 2230 except SocketError as e: 2231 screen.reset() 2232 log('socket error: %s' % e) 2233 sys.exit(2) 2234 2235 finally: 2236 screen.reset() 2237 socket.close() 2238 2239 sys.exit(0) 2240 2241# vim: et sw=4 sts=4 ts=4 tw=78 fdn=1 fdm=indent 2242