1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# SPDX-License-Identifier: BSD-2-Clause 5'''\ 6Any keystroke causes a poll and update. Keystroke commands: 7 8'a': Change peer display to apeers mode, showing association IDs. 9'd': Toggle detail mode (some peer will be reverse-video highlighted when on). 10'h': Display helpscreen. 11'j': Select next peer (in select mode); arrow down also works. 12'k': Select previous peer (in select mode); arrow up also works. 13'm': Disable peers display, showing only MRU list 14'n': Toggle display of hostnames (vs. IP addresses). 15'o': Change peer display to opeers mode, showing destination address. 16'p': Change peer display to default mode, showing refid. 17'q': Cleanly terminate the program. 18's': Toggle display of only reachable hosts (default is all hosts). 19'u': Toggle display of units. 20'w': Toggle wide mode. 21'x': Cleanly terminate the program. 22' ': Rotate through a/n/o/p/r display modes. 23'+': Increase debugging level. Output goes to ntpmon.log 24'-': Decrease debugging level. 25'?': Display helpscreen. 26''' 27 28from __future__ import print_function, division 29 30import getopt 31import re 32import sys 33import time 34 35try: 36 import ntp.control 37 import ntp.magic 38 import ntp.ntpc 39 import ntp.packet 40 import ntp.util 41except ImportError as e: 42 sys.stderr.write( 43 "ntpmon: can't load Python NTP libraries -- check PYTHONPATH.\n") 44 sys.stderr.write("%s\n" % e) 45 sys.exit(1) 46 47# This used to force UTF-8 encoding, but that breaks the readline system. 48# Unfortunately sometimes sys.stdout.encoding lies about the encoding, 49# so expect random false positives. 50# LANG=C or LANG=POSIX refuse unicode when combined with curses 51disableunicode = ntp.util.check_unicode() 52 53 54try: 55 import locale 56except ImportError as e: 57 sys.stderr.write( 58 "ntpmon: can't find Python locale library.\n") 59 sys.stderr.write("%s\n" % e) 60 sys.exit(1) 61 62try: 63 import curses 64except ImportError as e: 65 sys.stderr.write( 66 "ntpmon: can't find Python curses library.\n") 67 sys.stderr.write("%s\n" % e) 68 sys.exit(1) 69 70stdscr = None 71 72 73def iso8601(t): 74 "ISO8601 string from Unix time." 75 return time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(t)) 76 77 78def statline(_peerlist, _mrulist, nyquist): 79 "Generate a status line" 80 # We don't use stdversion here because the presence of a date is confusing 81 leader = sysvars['version'][0] 82 leader = re.sub(r" \([^\)]*\)", "", leader) 83 if span.entries: 84 trailer = "Updated: %s (%s)" \ 85 % (iso8601(int(ntp.ntpc.lfptofloat(span.entries[0].last))), 86 ntp.util.PeerSummary.prettyinterval(nyquist)) 87 else: 88 trailer = "" 89 spacer = ((peer_report.termwidth - 1) - len(leader) - len(trailer)) * " " 90 return leader + spacer + trailer 91 92 93def peer_detail(variables, showunits=False): 94 "Show the things a peer summary doesn't, cooked slightly differently" 95 # All of an rv display except refid, reach, delay, offset, jitter. 96 # One of the goals here is to emit field values at fixed positions 97 # on the 2D display, so that changes in the details are easier to spot. 98 vcopy = {} 99 vcopyraw = {} 100 vcopy.update(variables) 101 width = ntp.util.termsize().width - 2 102 # Need to separate the casted from the raw 103 for key in vcopy.keys(): 104 vcopyraw[key] = vcopy[key][1] 105 vcopy[key] = vcopy[key][0] 106 vcopy["leap"] = ("no-leap", "add-leap", "del-leap", 107 "unsync")[vcopy["leap"]] 108 for fld in ('xmt', 'rec', 'reftime'): 109 if fld not in vcopy: 110 vcopy[fld] = "***missing***" 111 else: 112 vcopy[fld] = ntp.util.rfc3339(ntp.ntpc.lfptofloat(vcopy[fld])) 113 if showunits: 114 for name in ntp.util.MS_VARS: 115 if name in vcopy: 116 vcopy[name] = ntp.util.unitify(vcopyraw[name], 117 ntp.util.UNIT_MS, 118 width=None) 119 for name in ntp.util.PPM_VARS: 120 if name in vcopy: 121 vcopy[name] = ntp.util.unitify(vcopyraw[name], 122 ntp.util.UNIT_PPM, 123 width=None) 124 for name in ntp.util.S_VARS: 125 if name in vcopy: 126 vcopy[name] = ntp.util.unitify(vcopyraw[name], 127 ntp.util.UNIT_S, 128 width=None) 129 vcopy['filtdelay'] = ntp.util.stringfiltcooker(vcopyraw['filtdelay']) 130 vcopy['filtoffset'] = ntp.util.stringfiltcooker(vcopyraw['filtoffset']) 131 vcopy['filtdisp'] = ntp.util.stringfiltcooker(vcopyraw['filtdisp']) 132 else: 133 vcopy['filtdelay'] = ntp.util.stringfilt(vcopyraw['filtdelay']) 134 vcopy['filtoffset'] = ntp.util.stringfilt(vcopyraw['filtoffset']) 135 vcopy['filtdisp'] = ntp.util.stringfilt(vcopyraw['filtdisp']) 136 # annotate IPv6, to stand out from :port 137 if ':' in vcopy['srcadr']: 138 vcopy['srcadr'] = '[' + vcopy['srcadr'] + ']' 139 if ':' in vcopy['dstadr']: 140 vcopy['dstadr'] = '[' + vcopy['dstadr'] + ']' 141 vcopy['adr'] = "dstadr=%(dstadr)s:%(dstport)s " \ 142 "srcadr=%(srcadr)s:%(srcport)d" % vcopy 143 if len(vcopy['adr']) > width: 144 # too long, break the line 145 vcopy['adr'] = vcopy['adr'].replace(" ", "\n") 146 147 peerfmt = """\ 148%(adr)s 149reftime=%(reftime)s\tleap=%(leap)s\trootdelay=%(rootdelay)s 150 rec=%(rec)s\tstratum=%(stratum)2d\trootdisp=%(rootdisp)s 151 xmt=%(xmt)s\tprecision=%(precision)3d\tdispersion=%(dispersion)s 152unreach=%(unreach)d hmode=%(hmode)d pmode=%(pmode)d hpoll=%(hpoll)d \ 153ppoll=%(ppoll)d headway=%(headway)s flash=%(flash)s keyid=%(keyid)s 154filtdelay = %(filtdelay)s 155filtoffset = %(filtoffset)s 156filtdisp = %(filtdisp)s 157""" 158 str = peerfmt % vcopy 159 return str.expandtabs() 160 161 162class Fatal(Exception): 163 "Unrecoverable error." 164 165 def __init__(self, msg): 166 Exception.__init__(self) 167 self.msg = msg 168 169 def __str__(self): 170 return self.msg 171 172 173class OutputContext: 174 def __enter__(self): 175 "Begin critical region." 176 if sys.version_info[0] < 3 and not disableunicode: 177 # This appears to only be needed under python 2, it is only 178 # activated when we already have UTF-8. Otherwise we drop 179 # down to non-unicode versions. 180 locale.setlocale(locale.LC_ALL, "") 181 global stdscr 182 stdscr = curses.initscr() 183 try: 184 curses.curs_set(0) 185 except curses.error: 186 # VT100 terminal emulations can barf here. 187 pass 188 curses.cbreak() 189 curses.noecho() 190 stdscr.keypad(True) 191 # Design decision: The most important info is nearer the 192 # top of the display. Therefore, prevent scrolling. 193 stdscr.scrollok(False) 194 195 def __exit__(self, extype_unused, value_unused, traceback_unused): 196 curses.endwin() 197 198 199usage = ''' 200USAGE: ntpmon [-dhnuV] [-D lvl] [-l logfile] [host] 201 Flg Arg Option-Name Description 202 -d no debug-level Increase output debug message level 203 - may appear multiple times 204 -D Int set-debug-level Set the output debug message level 205 - may appear multiple times 206 -h no help Print a usage message. 207 -l Str logfile Logs debug messages to the provided filename 208 -n no numeric Numeric host addresses 209 -u no units Display time with units. 210 -V opt version Output version information and exit 211''' 212 213if __name__ == '__main__': 214 bin_ver = "ntpsec-@NTPSEC_VERSION_EXTENDED@" 215 if ntp.util.stdversion() != bin_ver: 216 sys.stderr.write("Module/Binary version mismatch\n") 217 sys.stderr.write("Binary: %s\n" % bin_ver) 218 sys.stderr.write("Module: %s\n" % ntp.util.stdversion()) 219 try: 220 (options, arguments) = getopt.getopt(sys.argv[1:], 221 "dD:hl:nsSuV", 222 ["debug", "help", "logfile=", 223 "numeric", "units", 224 "set-debug-level=", "version", 225 "srcname", "srcnumber"]) 226 except getopt.GetoptError as e: 227 sys.stderr.write("%s\n" % e) 228 sys.stderr.write(usage) 229 raise SystemExit(1) 230 progname = sys.argv[0] 231 232 showhostnames = 1 233 wideremote = False 234 showall = True 235 showpeers = True 236 showunits = False 237 nyquist = 1 238 debug = 0 239 logfp = None 240 defaultlog = "ntpmon.log" 241 242 for (switch, val) in options: 243 if switch in ("-d", "--debug"): 244 debug += 1 245 elif switch in ("-D", "--set-debug-level"): 246 errmsg = "Error: -D parameter '%s' not a number\n" 247 debug = ntp.util.safeargcast(val, int, errmsg, usage) 248 elif switch in ("-h", "--help"): 249 print(usage) 250 raise SystemExit(0) 251 elif switch in ("-l", "--logfile"): 252 if logfp is not None: 253 logfp.close() 254 logfp = open(val, "a", 1) # 1 => line buffered 255 elif switch in ("-n", "--numeric"): 256 showhostnames = 0 257 elif switch in ("-s", "--srcname"): 258 showhostnames = 3 259 elif switch in ("-S", "--srcnumber"): 260 showhostnames = 2 261 elif switch in ("-u", "--units"): 262 showunits = True 263 elif switch in ("-V", "--version"): 264 print("ntpmon %s" % ntp.util.stdversion()) 265 raise SystemExit(0) 266 267 if (logfp is None) and (debug > 0): 268 logfp = open(defaultlog, "a", 1) 269 270 poll_interval = 1 271 helpmode = selectmode = detailmode = False 272 selected = -1 273 peer_report = ntp.util.PeerSummary(displaymode="peers", 274 pktversion=ntp.magic.NTP_VERSION, 275 showhostnames=showhostnames, 276 wideremote=wideremote, 277 showunits=showunits, 278 termwidth=80, 279 debug=debug, 280 logfp=logfp) 281 mru_report = ntp.util.MRUSummary(showhostnames, 282 wideremote=wideremote, 283 debug=debug, logfp=logfp) 284 try: 285 session = ntp.packet.ControlSession() 286 session.debug = debug 287 session.logfp = logfp 288 session.openhost(arguments[0] if arguments else "localhost") 289 sysvars = session.readvar(raw=True) 290 with OutputContext() as ctx: 291 while True: 292 stdscr.erase() 293 stdscr.addstr(0, 0, u"".encode('UTF-8')) 294 if helpmode: 295 stdscr.addstr(__doc__.encode('UTF-8')) 296 tempStr = u"\nPress any key to resume monitoring" 297 stdscr.addstr(tempStr.encode('UTF-8')) 298 stdscr.refresh() 299 stdscr.timeout(-1) 300 else: 301 if showpeers: 302 try: 303 peers = session.readstat() 304 except ntp.packet.ControlException as e: 305 raise Fatal(e.message) 306 except IOError as e: 307 raise Fatal(e.strerror) 308 strconvert = peer_report.header() + "\n" 309 stdscr.addstr(strconvert.encode('UTF-8'), 310 curses.A_BOLD) 311 else: 312 peer_report.polls = [1] # Kluge! 313 peers = [] 314 if showpeers and not peers: 315 raise Fatal("no peers reported") 316 try: 317 initphase = False 318 for (i, peer) in enumerate(peers): 319 if (not showall and not ( 320 ntp.control.CTL_PEER_STATVAL(peer.status) & 321 (ntp.control.CTL_PST_CONFIG | 322 ntp.control.CTL_PST_REACH))): 323 continue 324 try: 325 variables = session.readvar(peer.associd, 326 raw=True) 327 except ntp.packet.ControlException as e: 328 if e.errorcode == ntp.control.CERR_BADASSOC: 329 # Probable race condition due to pool 330 # dropping an associaton during refresh; 331 # ignore. (GitLab issue #374.) 332 break 333 raise Fatal(e.message + "\n") 334 except IOError as e: 335 raise Fatal(e.strerror) 336 except IndexError: 337 raise Fatal( 338 "no 'hpoll' variable in peer response") 339 if not variables: 340 continue 341 if selectmode and selected == i: 342 retained = variables 343 hilite = curses.A_REVERSE 344 else: 345 hilite = curses.A_NORMAL 346 data = peer_report.summary(session.rstatus, 347 variables, 348 peer.associd) 349 data = data.encode('UTF-8') 350 stdscr.addstr(data, hilite) 351 if ('refid' in variables and 352 'INIT' in variables['refid']): 353 initphase = True 354 355 # Now the MRU report 356 limit = stdscr.getmaxyx()[0] - len(peers) 357 span = session.mrulist(variables={'recent': limit}) 358 mru_report.now = time.time() 359 360 # After init phase use Nyquist-interval 361 # sampling - half the smallest poll interval 362 # seen in the last cycle, rounded up to 1 363 # second. 364 if not initphase: 365 nyquist = int(min(peer_report.intervals()) / 2) 366 nyquist = 1 if nyquist == 0 else nyquist 367 ntp.util.dolog(logfp, 368 "nyquist is %d\n" % nyquist, 369 debug, 1) 370 # The status line 371 sl = statline(peer_report, mru_report, nyquist) 372 strconvert = sl + "\n" 373 stdscr.addstr(strconvert.encode('UTF-8'), 374 curses.A_REVERSE | curses.A_DIM) 375 if detailmode: 376 if ntp.util.PeerSummary.is_clock(retained): 377 dtype = ntp.ntpc.TYPE_CLOCK 378 else: 379 dtype = ntp.ntpc.TYPE_PEER 380 sw = ntp.ntpc.statustoa(dtype, 381 peers[selected].status) 382 strconvert = "assoc=%d: %s\n" 383 stdscr.addstr(strconvert 384 % (peers[selected].associd, sw)) 385 strconvert = peer_detail(retained, showunits) 386 stdscr.addstr(strconvert.encode('UTF-8')) 387 try: 388 clockvars = session.readvar( 389 peers[selected].associd, 390 opcode=ntp.control.CTL_OP_READCLOCK, 391 raw=True) 392 strconvert = ntp.util.cook(clockvars, 393 showunits, " ") 394 stdscr.addstr(strconvert.encode('UTF-8')) 395 except ntp.packet.ControlException as e: 396 pass 397 elif span.entries: 398 strconvert = ntp.util.MRUSummary.header + "\n" 399 stdscr.addstr(strconvert.encode('UTF-8'), 400 curses.A_BOLD) 401 for entry in reversed(span.entries): 402 strcon = mru_report.summary(entry) + "\n" 403 stdscr.addstr(strcon.encode('UTF-8')) 404 except curses.error: 405 # An addstr overran the screen, no worries 406 pass 407 # Display all 408 stdscr.refresh() 409 stdscr.timeout(nyquist * 1000) 410 try: 411 helpmode = False 412 key = stdscr.getkey() 413 if key == 'q' or key == 'x': 414 raise SystemExit(0) 415 elif key == 'a': 416 peer_report.displaymode = 'apeers' 417 elif key == 'd': 418 if not selectmode: 419 selected = 0 420 selectmode = not selectmode 421 detailmode = not detailmode 422 showpeers = True # detail + hide peers == crash 423 elif key == 'm': 424 showpeers = not showpeers 425 detailmode = False # detail + hide peers == crash 426 elif key == 'n': 427 if peer_report.showhostnames == 2: 428 peer_report.showhostnames = 0 429 elif peer_report.showhostnames == 0: 430 peer_report.showhostnames = 1 431 elif peer_report.showhostnames == 1: 432 peer_report.showhostnames = 3 433 else: 434 peer_report.showhostnames = 2 435 mru_report.showhostnames = \ 436 peer_report.showhostnames 437 elif key == 'o': 438 peer_report.displaymode = 'opeers' 439 elif key == 'p': 440 peer_report.displaymode = 'peers' 441 elif key == 'r': 442 peer_report.displaymode = 'rpeers' 443 elif key == 's': 444 showall = not showall 445 elif key == 'u': 446 showunits = not showunits 447 peer_report.showunits = showunits 448 elif key == 'w': 449 wideremote = not wideremote 450 peer_report.wideremote = wideremote 451 mru_report.wideremote = wideremote 452 elif key == " ": 453 if peer_report.displaymode == 'peers': 454 peer_report.displaymode = 'apeers' 455 elif peer_report.displaymode == 'apeers': 456 peer_report.displaymode = 'opeers' 457 elif peer_report.displaymode == 'opeers': 458 peer_report.displaymode = 'rpeers' 459 else: 460 peer_report.displaymode = 'peers' 461 elif key == 'j' or key == "KEY_DOWN": 462 if showpeers: 463 selected += 1 464 selected %= len(peers) 465 elif key == 'k' or key == "KEY_UP": 466 if showpeers: 467 selected += len(peers) - 1 468 selected %= len(peers) 469 elif key == '+': 470 if logfp is None: 471 logfp = open(defaultlog, "a", 1) 472 session.logfp = logfp 473 peer_report.logfp = logfp 474 mru_report.logfp = logfp 475 debug += 1 476 session.debug = debug 477 peer_report.debug = debug 478 mru_report.debug = debug 479 elif key == '-': 480 debug -= 1 481 session.debug = debug 482 peer_report.debug = debug 483 mru_report.debug = debug 484 elif key in ['?', 'h']: 485 helpmode = True 486 except curses.error: 487 pass 488 except KeyboardInterrupt: 489 print("") 490 except (Fatal, ntp.packet.ControlException) as e: 491 print(e) 492 except IOError: 493 print("Bailing out...") 494 495# end 496