1# -*- coding: utf-8 -*- 2"Common utility functions" 3# SPDX-License-Identifier: BSD-2-Clause 4 5from __future__ import print_function, division 6 7 8import collections 9import os 10import re 11import shutil 12import socket 13import string 14import sys 15import time 16import ntp.ntpc 17import ntp.magic 18import ntp.control 19 20 21# Old CTL_PST defines for version 2. 22OLD_CTL_PST_CONFIG = 0x80 23OLD_CTL_PST_AUTHENABLE = 0x40 24OLD_CTL_PST_AUTHENTIC = 0x20 25OLD_CTL_PST_REACH = 0x10 26OLD_CTL_PST_SANE = 0x08 27OLD_CTL_PST_DISP = 0x04 28 29OLD_CTL_PST_SEL_REJECT = 0 30OLD_CTL_PST_SEL_SELCAND = 1 31OLD_CTL_PST_SEL_SYNCCAND = 2 32OLD_CTL_PST_SEL_SYSPEER = 3 33 34 35# Units for formatting 36UNIT_NS = "ns" # nano second 37UNIT_US = u"µs" # micro second 38UNIT_MS = "ms" # milli second 39UNIT_S = "s" # second 40UNIT_KS = "ks" # kilo seconds 41UNITS_SEC = [UNIT_NS, UNIT_US, UNIT_MS, UNIT_S, UNIT_KS] 42UNIT_PPT = "ppt" # parts per trillion 43UNIT_PPB = "ppb" # parts per billion 44UNIT_PPM = "ppm" # parts per million 45UNIT_PPK = u"‰" # parts per thousand 46UNITS_PPX = [UNIT_PPT, UNIT_PPB, UNIT_PPM, UNIT_PPK] 47unitgroups = (UNITS_SEC, UNITS_PPX) 48 49 50# These two functions are not tested because they will muck up the module 51# for everything else, and they are simple. 52 53def check_unicode(): # pragma: no cover 54 if "UTF-8" != sys.stdout.encoding: 55 deunicode_units() 56 return True # needed by ntpmon 57 return False 58 59 60def deunicode_units(): # pragma: no cover 61 """Under certain conditions it is not possible to force unicode output, 62 this overwrites units that contain unicode with safe versions""" 63 global UNIT_US 64 global UNIT_PPK 65 # Replacement units 66 new_us = "us" 67 new_ppk = "ppk" 68 # Replace units in unit groups 69 UNITS_SEC[UNITS_SEC.index(UNIT_US)] = new_us 70 UNITS_PPX[UNITS_PPX.index(UNIT_PPK)] = new_ppk 71 # Replace the units themselves 72 UNIT_US = new_us 73 UNIT_PPK = new_ppk 74 75 76# Variables that have units 77S_VARS = ("tai", "poll") 78MS_VARS = ("rootdelay", "rootdisp", "rootdist", "offset", "sys_jitter", 79 "clk_jitter", "leapsmearoffset", "authdelay", "koffset", "kmaxerr", 80 "kesterr", "kprecis", "kppsjitter", "fuzz", "clk_wander_threshold", 81 "tick", "in", "out", "bias", "delay", "jitter", "dispersion", 82 "fudgetime1", "fudgetime2") 83PPM_VARS = ("frequency", "clk_wander") 84 85 86def dolog(logfp, text, debug, threshold): 87 """debug is the current debug value 88 threshold is the trigger for the current log""" 89 if logfp is None: 90 return # can turn off logging by supplying a None file descriptor 91 text = rfc3339(time.time()) + " " + text + "\n" 92 if debug >= threshold: 93 logfp.write(text) 94 logfp.flush() # we don't want to lose an important log to a crash 95 96 97def safeargcast(arg, castfunc, errtext, usage): 98 """Attempts to typecast an argument, prints and dies on failure. 99 errtext must contain a %s for splicing in the argument, and be 100 newline terminated.""" 101 try: 102 casted = castfunc(arg) 103 except ValueError: 104 sys.stderr.write(errtext % arg) 105 sys.stderr.write(usage) 106 raise SystemExit(1) 107 return casted 108 109 110def stdversion(): 111 "Returns the NTPsec version string in a standard format" 112 return "ntpsec-%s" % "@NTPSEC_VERSION_EXTENDED@" 113 114 115def rfc3339(t): 116 "RFC 3339 string from Unix time, including fractional second." 117 rep = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t)) 118 t = str(t) 119 if "." in t: 120 subsec = t.split(".", 1)[1] 121 if int(subsec) > 0: 122 rep += "." + subsec 123 rep += "Z" 124 return rep 125 126 127def deformatNTPTime(txt): 128 txt = txt[2:] # Strip '0x' 129 txt = "".join(txt.split(".")) # Strip '.' 130 value = ntp.util.hexstr2octets(txt) 131 return value 132 133 134def hexstr2octets(hexstr): 135 if (len(hexstr) % 2) != 0: 136 hexstr = hexstr[:-1] # slice off the last char 137 values = [] 138 for index in range(0, len(hexstr), 2): 139 values.append(chr(int(hexstr[index:index+2], 16))) 140 return "".join(values) 141 142 143def slicedata(data, slicepoint): 144 "Breaks a sequence into two pieces at the slice point" 145 return data[:slicepoint], data[slicepoint:] 146 147 148def portsplit(hostname): 149 portsuffix = "" 150 if hostname.count(":") == 1: # IPv4 with appended port 151 (hostname, portsuffix) = hostname.split(":") 152 portsuffix = ":" + portsuffix 153 elif ']' in hostname: # IPv6 154 rbrak = hostname.rindex("]") 155 if ":" in hostname[rbrak:]: 156 portsep = hostname.rindex(":") 157 portsuffix = hostname[portsep:] 158 hostname = hostname[:portsep] 159 hostname = hostname[1:-1] # Strip brackets 160 return (hostname, portsuffix) 161 162 163def parseConf(text): 164 inQuote = False 165 quoteStarter = "" 166 lines = [] 167 tokens = [] 168 current = [] 169 170 def pushToken(): 171 token = "".join(current) 172 if not inQuote: # Attempt type conversion 173 try: 174 token = int(token) 175 except ValueError: 176 try: 177 token = float(token) 178 except ValueError: 179 pass 180 wrapper = (inQuote, token) 181 tokens.append(wrapper) 182 current[:] = [] 183 184 def pushLine(): 185 if current: 186 pushToken() 187 if tokens: 188 lines.append(tokens[:]) 189 tokens[:] = [] 190 191 i = 0 192 tlen = len(text) 193 while i < tlen: 194 if inQuote: 195 if text[i] == quoteStarter: # Ending a text string 196 pushToken() 197 quoteStarter = "" 198 inQuote = False 199 elif text[i] == "\\": # Starting an escape sequence 200 i += 1 201 if text[i] in "'\"n\\": 202 current.append(eval("\'\\" + text[i] + "\'")) 203 else: 204 current.append(text[i]) 205 else: 206 if text[i] == "#": # Comment 207 while (i < tlen) and (text[i] != "\n"): 208 i += 1 # Advance to end of line... 209 i -= 1 # ...and back up so we don't skip the newline 210 elif text[i] in "'\"": # Starting a text string 211 inQuote = True 212 quoteStarter = text[i] 213 if current: 214 pushToken() 215 elif text[i] == "\\": # Linebreak escape 216 i += 1 217 if text[i] != "\n": 218 raise SyntaxError 219 elif text[i] == "\n": # EOL: break the lines 220 pushLine() 221 elif text[i] in string.whitespace: 222 if current: 223 pushToken() 224 else: 225 current.append(text[i]) 226 i += 1 227 pushLine() 228 return lines 229 230 231def stringfilt(data): 232 "Pretty print string of space separated numbers" 233 parts = data.split() 234 235 cooked = [] 236 for part in parts: 237 # These are expected to fit on a single 80-char line. 238 # Accounting for other factors this leaves each number with 239 # 7 chars + a space. 240 fitted = fitinfield(part, 7) 241 cooked.append(fitted) 242 rendered = " ".join(cooked) 243 return rendered 244 245 246def stringfiltcooker(data): 247 "Cooks a filt* string of space separated numbers, expects milliseconds" 248 parts = data.split() 249 oomcount = {} 250 minscale = -100000 # Keep track of the maxdownscale for each value 251 # Find out what the 'natural' unit of each value is 252 for part in parts: 253 # Only care about OOMs, the real scaling happens later 254 value, oom = scalestring(part) 255 # Track the highest maxdownscale so we do not invent precision 256 ds = maxdownscale(part) 257 minscale = max(ds, minscale) 258 oomcount[oom] = oomcount.get(oom, 0) + 1 259 # Find the most common unit 260 mostcommon = 0 261 highestcount = 0 262 for key in oomcount.keys(): 263 if key < minscale: 264 continue # skip any scale that would result in making up data 265 count = oomcount[key] 266 if count > highestcount: 267 mostcommon = key 268 highestcount = count 269 # Shift all values to the new unit 270 cooked = [] 271 for part in parts: 272 part = rescalestring(part, mostcommon) 273 fitted = fitinfield(part, 7) 274 cooked.append(fitted) 275 rendered = " ".join(cooked) + " " + UNITS_SEC[mostcommon + 276 UNITS_SEC.index(UNIT_MS)] 277 return rendered 278 279 280def getunitgroup(unit): 281 "Returns the unit group which contains a given unit" 282 for group in unitgroups: 283 if unit in group: 284 return group 285 286 287def oomsbetweenunits(a, b): 288 "Calculates how many orders of magnitude separate two units" 289 group = getunitgroup(a) 290 if b is None: # Caller is asking for the distance from the base unit 291 return group.index(a) * 3 292 elif b in group: 293 ia = group.index(a) 294 ib = group.index(b) 295 return abs((ia - ib) * 3) 296 return None 297 298 299def breaknumberstring(value): 300 "Breaks a number string into (aboveDecimal, belowDecimal, isNegative?)" 301 if value[0] == "-": 302 value = value[1:] 303 negative = True 304 else: 305 negative = False 306 if "." in value: 307 above, below = value.split(".") 308 else: 309 above = value 310 below = "" 311 return (above, below, negative) 312 313 314def gluenumberstring(above, below, isnegative): 315 "Glues together parts of a number string" 316 if above == "": 317 above = "0" 318 if below: 319 newvalue = ".".join((above, below)) 320 else: 321 newvalue = above 322 if isnegative: 323 newvalue = "-" + newvalue 324 return newvalue 325 326 327def maxdownscale(value): 328 "Maximum units a value can be scaled down without inventing data" 329 if "." in value: 330 digitcount = len(value.split(".")[1]) 331 # Return a negative so it can be fed directly to a scaling function 332 return -(digitcount // 3) 333 else: 334 # No decimals, the value is already at the maximum down-scale 335 return 0 336 337 338def rescalestring(value, unitsscaled): 339 "Rescale a number string by a given number of units" 340 whole, dec, negative = breaknumberstring(value) 341 if unitsscaled == 0: 342 # This may seem redundant, but glue forces certain formatting details 343 value = gluenumberstring(whole, dec, negative) 344 return value 345 hilen = len(whole) 346 lolen = len(dec) 347 digitsmoved = abs(unitsscaled * 3) 348 if unitsscaled > 0: # Scale to a larger unit, move decimal left 349 if hilen < digitsmoved: 350 # Scaling beyond the digits, pad it out. We can pad here 351 # without making up digits that don't exist 352 padcount = digitsmoved - hilen 353 newwhole = "" 354 newdec = ("0" * padcount) + whole + dec 355 else: # Scaling in the digits, no need to pad 356 choppoint = -digitsmoved 357 newdec = whole[choppoint:] + dec 358 newwhole = whole[:choppoint] 359 elif unitsscaled < 0: # scale to a smaller unit, move decimal right 360 if lolen < digitsmoved: 361 # Scaling beyond the digits would force us to make up data 362 # that doesn't exist. So fail. 363 # The caller should have already caught this with maxdownscale() 364 return None 365 else: 366 newwhole = whole + dec[:digitsmoved] 367 newdec = dec[digitsmoved:] 368 newwhole = newwhole.lstrip("0") 369 newvalue = gluenumberstring(newwhole, newdec, negative) 370 return newvalue 371 372 373def formatzero(value): 374 "Scale a zero value for the unit with the highest available precision" 375 scale = maxdownscale(value) 376 newvalue = rescalestring(value, scale).lstrip("-") 377 return (newvalue, scale) 378 379 380def scalestring(value): 381 "Scales a number string to fit in the range 1.0-999.9" 382 if isstringzero(value): 383 return formatzero(value) 384 whole, dec, negative = breaknumberstring(value) 385 hilen = len(whole) 386 if (hilen == 0) or isstringzero(whole): # Need to shift to smaller units 387 i = 0 388 lolen = len(dec) 389 while i < lolen: # need to find the actual digits 390 if dec[i] != "0": 391 break 392 i += 1 393 lounits = (i // 3) + 1 # always need to shift one more unit 394 movechars = lounits * 3 395 if lolen < movechars: 396 # Not enough digits to scale all the way down. Inventing 397 # digits is unacceptable, so scale down as much as we can. 398 lounits = (i // 3) # "always", unless out of digits 399 movechars = lounits * 3 400 newwhole = dec[:movechars].lstrip("0") 401 newdec = dec[movechars:] 402 unitsmoved = -lounits 403 else: # Shift to larger units 404 hiunits = hilen // 3 # How many we have, not how many to move 405 hidigits = hilen % 3 406 if hidigits == 0: # full unit above the decimal 407 hiunits -= 1 # the unit above the decimal doesn't count 408 hidigits = 3 409 newwhole = whole[:hidigits] 410 newdec = whole[hidigits:] + dec 411 unitsmoved = hiunits 412 newvalue = gluenumberstring(newwhole, newdec, negative) 413 return (newvalue, unitsmoved) 414 415 416def fitinfield(value, fieldsize): 417 "Attempt to fit value into a field, preserving as much data as possible" 418 vallen = len(value) 419 if fieldsize is None: 420 newvalue = value 421 elif vallen == fieldsize: # Goldilocks! 422 newvalue = value 423 elif vallen < fieldsize: # Extra room, pad it out 424 pad = " " * (fieldsize - vallen) 425 newvalue = pad + value 426 else: # Insufficient room, round as few digits as possible 427 if "." in value: # Ok, we *do* have decimals to crop 428 diff = vallen - fieldsize 429 declen = len(value.split(".")[1]) # length of decimals 430 croplen = min(declen, diff) # Never round above the decimal point 431 roundlen = declen - croplen # How many digits we round to 432 newvalue = str(round(float(value), roundlen)) 433 splitted = newvalue.split(".") # This should never fail 434 declen = len(splitted[1]) 435 if roundlen == 0: # if rounding all the decimals don't display .0 436 # but do display the point, to show that there is more beyond 437 newvalue = splitted[0] + "." 438 elif roundlen > declen: # some zeros have been cropped, fix that 439 padcount = roundlen - declen 440 newvalue = newvalue + ("0" * padcount) 441 else: # No decimals, nothing we can crop 442 newvalue = value 443 return newvalue 444 445 446def cropprecision(value, ooms): 447 "Crops digits below the maximum precision" 448 if "." not in value: # No decimals, nothing to crop 449 return value 450 if ooms == 0: # We are at the baseunit, crop it all 451 return value.split(".")[0] 452 dstart = value.find(".") + 1 453 dsize = len(value) - dstart 454 precision = min(ooms, dsize) 455 cropcount = dsize - precision 456 if cropcount > 0: 457 value = value[:-cropcount] 458 return value 459 460 461def isstringzero(value): 462 "Detects whether a string is equal to zero" 463 for i in value: 464 if i not in ("-", ".", "0"): 465 return False 466 return True 467 468 469def unitrelativeto(unit, move): 470 "Returns a unit at a different scale from the input unit" 471 for group in unitgroups: 472 if unit in group: 473 if move is None: # asking for the base unit 474 return group[0] 475 else: 476 index = group.index(unit) 477 index += move # index of the new unit 478 if 0 <= index < len(group): # found the new unit 479 return group[index] 480 else: # not in range 481 return None 482 return None # couldn't find anything 483 484 485def unitifyvar(value, varname, baseunit=None, width=8, unitSpace=False): 486 "Call unitify() with the correct units for varname" 487 if varname in S_VARS: 488 start = UNIT_S 489 elif varname in MS_VARS: 490 start = UNIT_MS 491 elif varname in PPM_VARS: 492 start = UNIT_PPM 493 else: 494 return value 495 return unitify(value, start, baseunit, width, unitSpace) 496 497 498def unitify(value, startingunit, baseunit=None, width=8, unitSpace=False): 499 "Formats a numberstring with relevant units. Attempts to fit in width." 500 if baseunit is None: 501 baseunit = getunitgroup(startingunit)[0] 502 ooms = oomsbetweenunits(startingunit, baseunit) 503 if isstringzero(value): 504 newvalue, unitsmoved = formatzero(value) 505 else: 506 newvalue = cropprecision(value, ooms) 507 newvalue, unitsmoved = scalestring(newvalue) 508 unitget = unitrelativeto(startingunit, unitsmoved) 509 if unitSpace: 510 spaceWidthAdjustment = 1 511 spacer = " " 512 else: 513 spaceWidthAdjustment = 0 514 spacer = "" 515 if unitget is not None: # We have a unit 516 if width is None: 517 realwidth = None 518 else: 519 realwidth = width - (len(unitget) + spaceWidthAdjustment) 520 newvalue = fitinfield(newvalue, realwidth) + spacer + unitget 521 else: # don't have a replacement unit, use original 522 newvalue = value + spacer + startingunit 523 if width is None: 524 newvalue = newvalue.strip() 525 return newvalue 526 527 528def f8dot4(f): 529 "Scaled floating point formatting to fit in 8 characters" 530 531 if isinstance(f, str): 532 # a string? pass it on as a signal 533 return "%8s" % f 534 if not isinstance(f, (int, float)): 535 # huh? 536 return " X" 537 if str(float(f)).lower() == 'nan': 538 # yes, this is a better test than math.isnan() 539 # it also catches None, strings, etc. 540 return " nan" 541 542 fmt = "%8d" # xxxxxxxx or -xxxxxxx 543 if f >= 0: 544 if f < 1000.0: 545 fmt = "%8.4f" # xxx.xxxx normal case 546 elif f < 10000.0: 547 fmt = "%8.3f" # xxxx.xxx 548 elif f < 100000.0: 549 fmt = "%8.2f" # xxxxx.xx 550 elif f < 1000000.0: 551 fmt = "%8.1f" # xxxxxx.x 552 else: 553 # negative number, account for minus sign 554 if f > -100.0: 555 fmt = "%8.4f" # -xx.xxxx normal case 556 elif f > -1000.0: 557 fmt = "%8.3f" # -xxx.xxx 558 elif f > -10000.0: 559 fmt = "%8.2f" # -xxxx.xx 560 elif f > -100000.0: 561 fmt = "%8.1f" # -xxxxx.x 562 563 return fmt % f 564 565 566def f8dot3(f): 567 "Scaled floating point formatting to fit in 8 characters" 568 if isinstance(f, str): 569 # a string? pass it on as a signal 570 return "%8s" % f 571 if not isinstance(f, (int, float)): 572 # huh? 573 return " X" 574 if str(float(f)).lower() == 'nan': 575 # yes, this is a better test than math.isnan() 576 # it also catches None, strings, etc. 577 return " nan" 578 579 fmt = "%8d" # xxxxxxxx or -xxxxxxx 580 if f >= 0: 581 if f < 10000.0: 582 fmt = "%8.3f" # xxxx.xxx normal case 583 elif f < 100000.0: 584 fmt = "%8.2f" # xxxxx.xx 585 elif f < 1000000.0: 586 fmt = "%8.1f" # xxxxxx.x 587 else: 588 # negative number, account for minus sign 589 if f > -1000.0: 590 fmt = "%8.3f" # -xxx.xxx normal case 591 elif f > -10000.0: 592 fmt = "%8.2f" # -xxxx.xx 593 elif f > -100000.0: 594 fmt = "%8.1f" # -xxxxx.x 595 596 return fmt % f 597 598 599def monoclock(): 600 "Try to get a monotonic clock value unaffected by NTP stepping." 601 try: 602 # Available in Python 3.3 and up. 603 return time.monotonic() 604 except AttributeError: 605 return time.time() 606 607 608class Cache: 609 "Simple time-based cache" 610 611 def __init__(self, defaultTimeout=300): # 5 min default TTL 612 self.defaultTimeout = defaultTimeout 613 self._cache = {} 614 615 def get(self, key): 616 if key in self._cache: 617 value, settime, ttl = self._cache[key] 618 if settime >= monoclock() - ttl: 619 return value 620 else: # key expired, delete it 621 del self._cache[key] 622 return None 623 else: 624 return None 625 626 def set(self, key, value, customTTL=None): 627 ttl = customTTL if customTTL is not None else self.defaultTimeout 628 self._cache[key] = (value, monoclock(), ttl) 629 630 631# A hack to avoid repeatedly hammering on DNS when ntpmon runs. 632canonicalization_cache = Cache() 633 634 635def canonicalize_dns(inhost, family=socket.AF_UNSPEC): 636 "Canonicalize a hostname or numeric IP address." 637 resname = canonicalization_cache.get(inhost) 638 if resname is not None: 639 return resname 640 # Catch garbaged hostnames in corrupted Mode 6 responses 641 m = re.match("([:.[\]]|\w)*", inhost) 642 if not m: 643 raise TypeError 644 (hostname, portsuffix) = portsplit(inhost) 645 try: 646 ai = socket.getaddrinfo(hostname, None, family, 0, 0, 647 socket.AI_CANONNAME) 648 except socket.gaierror: 649 return "DNSFAIL:%s" % hostname 650 (family, socktype, proto, canonname, sockaddr) = ai[0] 651 try: 652 name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD) 653 result = name[0].lower() + portsuffix 654 except socket.gaierror: 655 # On OS X, canonname is empty for hosts without rDNS. 656 # Fall back to the hostname. 657 canonicalized = canonname or hostname 658 result = canonicalized.lower() + portsuffix 659 canonicalization_cache.set(inhost, result) 660 return result 661 662 663TermSize = collections.namedtuple("TermSize", ["width", "height"]) 664 665 666# Python 2.x does not have the shutil.get_terminal_size function. 667# This conditional import is only needed by termsize() and should be kept 668# near it. It is not inside the function because the unit tests need to be 669# able to splice in a jig. 670if str is bytes: # We are on python 2.x 671 import fcntl 672 import termios 673 import struct 674 675 676def termsize(): # pragma: no cover 677 "Return the current terminal size." 678 # Alternatives at http://stackoverflow.com/questions/566746 679 # The way this is used makes it not a big deal if the default is wrong. 680 size = (80, 24) 681 if os.isatty(1): 682 if str is not bytes: 683 # str is bytes means we are >py3.0, but this will still fail 684 # on versions <3.3. We do not support those anyway. 685 (w, h) = shutil.get_terminal_size((80, 24)) 686 size = (w, h) 687 else: 688 try: 689 # OK, Python version < 3.3, cope 690 h, w, hp, wp = struct.unpack( 691 'HHHH', 692 fcntl.ioctl(2, termios.TIOCGWINSZ, 693 struct.pack('HHHH', 0, 0, 0, 0))) 694 size = (w, h) 695 except IOError: 696 pass 697 return TermSize(*size) 698 699 700class PeerStatusWord: 701 "A peer status word from readstats(), dissected for display" 702 703 def __init__(self, status, pktversion=ntp.magic.NTP_VERSION): 704 # Event 705 self.event = ntp.control.CTL_PEER_EVENT(status) 706 # Event count 707 self.event_count = ntp.control.CTL_PEER_NEVNT(status) 708 statval = ntp.control.CTL_PEER_STATVAL(status) 709 # Config 710 if statval & ntp.control.CTL_PST_CONFIG: 711 self.conf = "yes" 712 else: 713 self.conf = "no" 714 # Reach 715 if statval & ntp.control.CTL_PST_BCAST: 716 self.reach = "none" 717 elif statval & ntp.control.CTL_PST_REACH: 718 self.reach = "yes" 719 else: 720 self.reach = "no" 721 # Auth 722 if (statval & ntp.control.CTL_PST_AUTHENABLE) == 0: 723 self.auth = "none" 724 elif statval & ntp.control.CTL_PST_AUTHENTIC: 725 self.auth = "ok " 726 else: 727 self.auth = "bad" 728 # Condition 729 if pktversion > ntp.magic.NTP_OLDVERSION: 730 seldict = { 731 ntp.control.CTL_PST_SEL_REJECT: "reject", 732 ntp.control.CTL_PST_SEL_SANE: "falsetick", 733 ntp.control.CTL_PST_SEL_CORRECT: "excess", 734 ntp.control.CTL_PST_SEL_SELCAND: "outlier", 735 ntp.control.CTL_PST_SEL_SYNCCAND: "candidate", 736 ntp.control.CTL_PST_SEL_EXCESS: "backup", 737 ntp.control.CTL_PST_SEL_SYSPEER: "sys.peer", 738 ntp.control.CTL_PST_SEL_PPS: "pps.peer", 739 } 740 self.condition = seldict[statval & 0x7] 741 else: 742 if (statval & 0x3) == OLD_CTL_PST_SEL_REJECT: 743 if (statval & OLD_CTL_PST_SANE) == 0: 744 self.condition = "insane" 745 elif (statval & OLD_CTL_PST_DISP) == 0: 746 self.condition = "hi_disp" 747 else: 748 self.condition = "" 749 elif (statval & 0x3) == OLD_CTL_PST_SEL_SELCAND: 750 self.condition = "sel_cand" 751 elif (statval & 0x3) == OLD_CTL_PST_SEL_SYNCCAND: 752 self.condition = "sync_cand" 753 elif (statval & 0x3) == OLD_CTL_PST_SEL_SYSPEER: 754 self.condition = "sys_peer" 755 # Last Event 756 event_dict = { 757 ntp.magic.PEVNT_MOBIL: "mobilize", 758 ntp.magic.PEVNT_DEMOBIL: "demobilize", 759 ntp.magic.PEVNT_UNREACH: "unreachable", 760 ntp.magic.PEVNT_REACH: "reachable", 761 ntp.magic.PEVNT_RESTART: "restart", 762 ntp.magic.PEVNT_REPLY: "no_reply", 763 ntp.magic.PEVNT_RATE: "rate_exceeded", 764 ntp.magic.PEVNT_DENY: "access_denied", 765 ntp.magic.PEVNT_ARMED: "leap_armed", 766 ntp.magic.PEVNT_NEWPEER: "sys_peer", 767 ntp.magic.PEVNT_CLOCK: "clock_alarm", 768 } 769 self.last_event = event_dict.get(ntp.magic.PEER_EVENT | self.event, "") 770 771 def __str__(self): 772 return ("conf=%(conf)s, reach=%(reach)s, auth=%(auth)s, " 773 "cond=%(condition)s, event=%(last_event)s ec=%(event_count)s" 774 % self.__dict__) 775 776 777def cook(variables, showunits=False, sep=", "): 778 "Cooked-mode variable display." 779 width = ntp.util.termsize().width - 2 780 text = "" 781 specials = ("filtdelay", "filtoffset", "filtdisp", "filterror") 782 longestspecial = len(max(specials, key=len)) 783 for (name, (value, rawvalue)) in variables.items(): 784 if name in specials: # need special formatting for column alignment 785 formatter = "%" + str(longestspecial) + "s =" 786 item = formatter % name 787 else: 788 item = "%s=" % name 789 if name in ("reftime", "clock", "org", "rec", "xmt"): 790 item += ntp.ntpc.prettydate(value) 791 elif name in ("srcadr", "peeradr", "dstadr", "refid"): 792 # C ntpq cooked these in obscure ways. Since they 793 # came up from the daemon as human-readable 794 # strings this was probably a bad idea, but we'll 795 # leave this case separated in case somebody thinks 796 # re-cooking them is a good idea. 797 item += value 798 elif name == "leap": 799 item += ("00", "01", "10", "11")[value] 800 elif name == "reach": 801 item += "%03lo" % value 802 elif name in specials: 803 if showunits: 804 item += stringfiltcooker(value) 805 else: 806 item += "\t".join(value.split()) 807 elif name == "flash": 808 item += "%02x " % value 809 if value == 0: 810 item += "ok " 811 else: 812 # flasher bits 813 tstflagnames = ( 814 "pkt_dup", # BOGON1 815 "pkt_bogus", # BOGON2 816 "pkt_unsync", # BOGON3 817 "pkt_denied", # BOGON4 818 "pkt_auth", # BOGON5 819 "pkt_stratum", # BOGON6 820 "pkt_header", # BOGON7 821 "pkt_autokey", # BOGON8 822 "pkt_crypto", # BOGON9 823 "peer_stratum", # BOGON10 824 "peer_dist", # BOGON11 825 "peer_loop", # BOGON12 826 "peer_unreach" # BOGON13 827 ) 828 for (i, n) in enumerate(tstflagnames): 829 if (1 << i) & value: 830 item += tstflagnames[i] + " " 831 item = item[:-1] 832 elif name in MS_VARS: 833 # Note that this is *not* complete, there are definitely 834 # missing variables here. 835 # Completion cannot occur until all units are tracked down. 836 if showunits: 837 item += unitify(rawvalue, UNIT_MS, UNIT_NS, width=None) 838 else: 839 item += repr(value) 840 elif name in S_VARS: 841 if showunits: 842 item += unitify(rawvalue, UNIT_S, UNIT_NS, width=None) 843 else: 844 item += repr(value) 845 elif name in PPM_VARS: 846 if showunits: 847 item += unitify(rawvalue, UNIT_PPM, width=None) 848 else: 849 item += repr(value) 850 else: 851 item += repr(value) 852 # add field separator 853 item += sep 854 # add newline so we don't overflow screen 855 lastcount = 0 856 for c in text: 857 if c == '\n': 858 lastcount = 0 859 else: 860 lastcount += 1 861 if lastcount + len(item) > width: 862 text = text[:-1] + "\n" 863 text += item 864 text = text[:-2] + "\n" 865 return text 866 867 868class PeerSummary: 869 "Reusable report generator for peer statistics" 870 871 def __init__(self, displaymode, pktversion, showhostnames, 872 wideremote, showunits=False, termwidth=None, 873 debug=0, logfp=sys.stderr): 874 self.displaymode = displaymode # peers/apeers/opeers 875 self.pktversion = pktversion # interpretation of flash bits 876 self.showhostnames = showhostnames # If false, display numeric IPs 877 self.showunits = showunits # If False show old style float 878 self.wideremote = wideremote # show wide remote names? 879 self.debug = debug 880 self.logfp = logfp 881 self.termwidth = termwidth 882 # By default, the peer spreadsheet layout is designed so lines just 883 # fit in 80 characters. This tells us how much extra horizontal space 884 # we have available on a wider terminal emulator. 885 self.horizontal_slack = min((termwidth or 80) - 80, 24) 886 # Peer spreadsheet column widths. The reason we cap extra 887 # width used at 24 is that on very wide displays, slamming the 888 # non-hostname fields all the way to the right produces a huge 889 # river that makes the entries difficult to read as wholes. 890 # This choice caps the peername field width at that of the longest 891 # possible IPV6 numeric address. 892 self.namewidth = 15 + self.horizontal_slack 893 self.refidwidth = 15 894 # Compute peer spreadsheet headers 895 self.__remote = " remote ".ljust(self.namewidth) 896 self.__common = "st t when poll reach delay offset " 897 self.__header = None 898 self.polls = [] 899 900 @staticmethod 901 def prettyinterval(diff): 902 "Print an interval in natural time units." 903 if not isinstance(diff, int) or diff <= 0: 904 return '-' 905 if diff <= 2048: 906 return str(diff) 907 diff = (diff + 29) / 60 908 if diff <= 300: 909 return "%dm" % diff 910 diff = (diff + 29) / 60 911 if diff <= 96: 912 return "%dh" % diff 913 diff = (diff + 11) / 24 914 return "%dd" % diff 915 916 @staticmethod 917 def high_truncate(hostname, maxlen): 918 "Truncate on the left using leading _ to indicate 'more'." 919 # Used for local IPv6 addresses, best distinguished by low bits 920 if len(hostname) <= maxlen: 921 return hostname 922 else: 923 return '-' + hostname[-maxlen+1:] 924 925 @staticmethod 926 def is_clock(variables): 927 "Does a set of variables look like it returned from a clock?" 928 return "srchost" in variables and '(' in variables["srchost"][0] 929 930 def header(self): 931 "Column headers for peer display" 932 if self.displaymode == "apeers": 933 self.__header = self.__remote + \ 934 " refid assid ".ljust(self.refidwidth) + \ 935 self.__common + "jitter" 936 elif self.displaymode == "opeers": 937 self.__header = self.__remote + \ 938 " local ".ljust(self.refidwidth) + \ 939 self.__common + " disp" 940 elif self.displaymode == 'rpeers': 941 self.__header = ' st t when poll reach delay ' + \ 942 'offset jitter refid T remote' 943 else: 944 self.__header = self.__remote + \ 945 " refid ".ljust(self.refidwidth) + \ 946 self.__common + "jitter" 947 return self.__header 948 949 def width(self): 950 "Width of display" 951 return 79 + self.horizontal_slack 952 953 def summary(self, rstatus, variables, associd): 954 "Peer status summary line." 955 clock_name = '' 956 dstadr_refid = "" 957 dstport = 0 958 estdelay = '.' 959 estdisp = '.' 960 estjitter = '.' 961 estoffset = '.' 962 filtdelay = 0.0 963 filtdisp = 0.0 964 filtoffset = 0.0 965 flash = 0 966 have_jitter = False 967 headway = 0 968 hmode = 0 969 hpoll = 0 970 keyid = 0 971 last_sync = None 972 leap = 0 973 pmode = 0 974 ppoll = 0 975 precision = 0 976 ptype = '?' 977 reach = 0 978 rec = None 979 reftime = None 980 rootdelay = 0.0 981 saw6 = False # x.6 floats for delay and friends 982 srcadr = None 983 srchost = None 984 srcport = 0 985 stratum = 20 986 mode = 0 987 unreach = 0 988 xmt = 0 989 ntscookies = -1 990 991 now = time.time() 992 993 for item in variables.items(): 994 if 2 != len(item) or 2 != len(item[1]): 995 # bad item 996 continue 997 (name, (value, rawvalue)) = item 998 if name == "delay": 999 estdelay = rawvalue if self.showunits else value 1000 if len(rawvalue) > 6 and rawvalue[-7] == ".": 1001 saw6 = True 1002 elif name == "dstadr": 1003 # The C code tried to get a fallback ptype from this in case 1004 # the hmode field was not included 1005 if "local" in self.__header: 1006 dstadr_refid = rawvalue 1007 elif name == "dstport": 1008 # FIXME, dstport never used. 1009 dstport = value 1010 elif name == "filtdelay": 1011 # FIXME, filtdelay never used. 1012 filtdelay = value 1013 elif name == "filtdisp": 1014 # FIXME, filtdisp never used. 1015 filtdisp = value 1016 elif name == "filtoffset": 1017 # FIXME, filtoffset never used. 1018 filtoffset = value 1019 elif name == "flash": 1020 # FIXME, flash never used. 1021 flash = value 1022 elif name == "headway": 1023 # FIXME, headway never used. 1024 headway = value 1025 elif name == "hmode": 1026 hmode = value 1027 elif name == "hpoll": 1028 hpoll = value 1029 if hpoll < 0: 1030 hpoll = ntp.magic.NTP_MINPOLL 1031 elif name == "jitter": 1032 if "jitter" in self.__header: 1033 estjitter = rawvalue if self.showunits else value 1034 have_jitter = True 1035 elif name == "keyid": 1036 # FIXME, keyid never used. 1037 keyid = value 1038 elif name == "leap": 1039 # FIXME, leap never used. 1040 leap = value 1041 elif name == "offset": 1042 estoffset = rawvalue if self.showunits else value 1043 elif name == "pmode": 1044 # FIXME, pmode never used. 1045 pmode = value 1046 elif name == "ppoll": 1047 ppoll = value 1048 if ppoll < 0: 1049 ppoll = ntp.magic.NTP_MINPOLL 1050 elif name == "precision": 1051 # FIXME, precision never used. 1052 precision = value 1053 elif name == "reach": 1054 # Shipped as hex, displayed in octal 1055 reach = value 1056 elif name == "refid": 1057 # The C code for this looked crazily overelaborate. Best 1058 # guess is that it was designed to deal with formats that 1059 # no longer occur in this field. 1060 if "refid" in self.__header: 1061 dstadr_refid = rawvalue 1062 elif name == "rec": 1063 rec = value # l_fp timestamp 1064 last_sync = int(now - ntp.ntpc.lfptofloat(rec)) 1065 elif name == "reftime": 1066 reftime = value # l_fp timestamp 1067 last_sync = int(now - ntp.ntpc.lfptofloat(reftime)) 1068 elif name == "rootdelay": 1069 # FIXME, rootdelay never used. 1070 rootdelay = value # l_fp timestamp 1071 elif name == "rootdisp" or name == "dispersion": 1072 estdisp = rawvalue if self.showunits else value 1073 elif name in ("srcadr", "peeradr"): 1074 srcadr = value 1075 elif name == "srchost": 1076 srchost = value 1077 elif name == "srcport" or name == "peerport": 1078 # FIXME, srcport never used. 1079 srcport = value 1080 elif name == "stratum": 1081 stratum = value 1082 elif name == "mode": 1083 # FIXME, mode never used. 1084 mode = value 1085 elif name == "unreach": 1086 # FIXME, unreach never used. 1087 unreach = value 1088 elif name == "xmt": 1089 # FIXME, xmt never used. 1090 xmt = value 1091 elif name == "ntscookies": 1092 ntscookies = value 1093 else: 1094 # unknown name? 1095 # line = " name=%s " % (name) # debug 1096 # return line # debug 1097 continue 1098 if hmode == ntp.magic.MODE_BCLIENTX: 1099 # broadcastclient or multicastclient 1100 ptype = 'b' 1101 elif hmode == ntp.magic.MODE_BROADCASTx: 1102 # broadcast or multicast server 1103 if srcadr.startswith("224."): # IANA multicast address prefix 1104 ptype = 'M' 1105 else: 1106 ptype = 'B' 1107 elif hmode == ntp.magic.MODE_CLIENT: 1108 if PeerSummary.is_clock(variables): 1109 ptype = 'l' # local refclock 1110 elif dstadr_refid == "POOL": 1111 ptype = 'p' # pool 1112 elif srcadr.startswith("224."): 1113 ptype = 'a' # manycastclient (compatibility with Classic) 1114 elif ntscookies > -1: 1115 # FIXME: Will foo up if there are ever more than 9 cookies 1116 ptype = chr(ntscookies + ord('0')) 1117 else: 1118 ptype = 'u' # unicast 1119 elif hmode == ntp.magic.MODE_ACTIVEx: 1120 ptype = 's' # symmetric active 1121 elif hmode == ntp.magic.MODE_PASSIVEx: 1122 ptype = 'S' # symmetric passive 1123 1124 # 1125 # Got everything, format the line 1126 # 1127 line = "" 1128 poll_sec = 1 << min(ppoll, hpoll) 1129 self.polls.append(poll_sec) 1130 if self.pktversion > ntp.magic.NTP_OLDVERSION: 1131 c = " x.-+#*o"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x7] 1132 else: 1133 c = " .+*"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x3] 1134 # Source host or clockname or poolname or servername 1135 # After new DNS, 2017-Apr-17 1136 # servers setup via numerical IP Address have only srcadr 1137 # servers setup via DNS have both srcadr and srchost 1138 # refclocks have both srcadr and srchost 1139 # pool has "0.0.0.0" (or "::") and srchost 1140 # slots setup via pool have only srcadr 1141 if srcadr is not None \ 1142 and srcadr != "0.0.0.0" \ 1143 and not srcadr.startswith("127.127") \ 1144 and srcadr != "::": 1145 if self.showhostnames & 2 and 'srchost' in locals() and srchost: 1146 clock_name = srchost 1147 elif self.showhostnames & 1: 1148 try: 1149 if self.debug: 1150 self.logfp.write("DNS lookup begins...\n") 1151 clock_name = canonicalize_dns(srcadr) 1152 if self.debug: 1153 self.logfp.write("DNS lookup ends.\n") 1154 except TypeError: # pragma: no cover 1155 return '' 1156 else: 1157 clock_name = srcadr 1158 else: 1159 clock_name = srchost 1160 if clock_name is None: 1161 if srcadr: 1162 clock_name = srcadr 1163 else: 1164 clock_name = "" 1165 if self.displaymode != "rpeers": 1166 if self.wideremote and len(clock_name) > self.namewidth: 1167 line += ("%c%s\n" % (c, clock_name)) 1168 line += (" " * (self.namewidth + 2)) 1169 else: 1170 line += ("%c%-*.*s " % (c, self.namewidth, self.namewidth, 1171 clock_name[:self.namewidth])) 1172 # Destination address, assoc ID or refid. 1173 assocwidth = 7 if self.displaymode == "apeers" else 0 1174 if "." not in dstadr_refid and ":" not in dstadr_refid: 1175 dstadr_refid = "." + dstadr_refid + "." 1176 if assocwidth and len(dstadr_refid) >= self.refidwidth - assocwidth: 1177 visible = "..." 1178 else: 1179 visible = dstadr_refid 1180 if self.displaymode != "rpeers": 1181 line += self.high_truncate(visible, self.refidwidth) 1182 if self.displaymode == "apeers": 1183 line += (" " * (self.refidwidth - len(visible) - assocwidth + 1)) 1184 line += ("%-6d" % (associd)) 1185 else: 1186 line += (" " * (self.refidwidth - len(visible))) 1187 # The rest of the story 1188 if last_sync is None: 1189 last_sync = now 1190 jd = estjitter if have_jitter else estdisp 1191 if self.showunits: 1192 fini = lambda x : unitify(x, UNIT_MS) 1193 elif saw6: 1194 fini = lambda x : f8dot4(x) 1195 else: 1196 fini = lambda x : f8dot3(x) 1197 line += ( 1198 " %2ld %c %4.4s %4.4s %3lo %s %s %s" 1199 % (stratum, ptype, 1200 PeerSummary.prettyinterval(last_sync), 1201 PeerSummary.prettyinterval(poll_sec), reach, 1202 fini(estdelay), fini(estoffset), fini(jd))) 1203 line += "\n" 1204 # for debugging both case 1205 # if srcadr != None and srchost != None: 1206 # line += "srcadr: %s, srchost: %s\n" % (srcadr, srchost) 1207 return line 1208 1209 def intervals(self): 1210 "Return and flush the list of actual poll intervals." 1211 res = self.polls[:] 1212 self.polls = [] 1213 return res 1214 1215 1216class MRUSummary: 1217 "Reusable class for MRU entry summary generation." 1218 1219 def __init__(self, showhostnames, wideremote=False, 1220 debug=0, logfp=sys.stderr): 1221 self.debug = debug 1222 self.logfp = logfp 1223 self.now = None 1224 self.showhostnames = showhostnames # if & 1, display names 1225 self.wideremote = wideremote 1226 1227 header = " lstint avgint rstr r m v count score drop rport remote address" 1228 1229 def summary(self, entry): 1230 first = ntp.ntpc.lfptofloat(entry.first) 1231 last = ntp.ntpc.lfptofloat(entry.last) 1232 active = float(last - first) 1233 count = int(entry.ct) 1234 if self.now: 1235 lstint = int(self.now - last + 0.5) 1236 stats = "%7d" % lstint 1237 if count == 1: 1238 favgint = 0 1239 else: 1240 favgint = active / (count-1) 1241 avgint = int(favgint + 0.5) 1242 if 5.0 < favgint or 1 == count: 1243 stats += " %6d" % avgint 1244 elif 1.0 <= favgint: 1245 stats += " %6.2f" % favgint 1246 else: 1247 stats += " %6.3f" % favgint 1248 else: 1249 MJD_1970 = 40587 # MJD for 1 Jan 1970, Unix epoch 1250 days, lstint = divmod(int(last), 86400) 1251 stats = "%5d %5d %6d" % (days + MJD_1970, lstint, active) 1252 if entry.rs & ntp.magic.RES_KOD: 1253 rscode = 'K' 1254 elif entry.rs & ntp.magic.RES_LIMITED: 1255 rscode = 'L' 1256 else: 1257 rscode = '.' 1258 (ip, port) = portsplit(entry.addr) 1259 try: 1260 if not self.showhostnames & 1: # if not & 1 display numeric IPs 1261 dns = ip 1262 else: 1263 dns = canonicalize_dns(ip) 1264 # Forward-confirm the returned DNS 1265 confirmed = canonicalization_cache.get(dns) 1266 if confirmed is None: 1267 confirmed = False 1268 try: 1269 ai = socket.getaddrinfo(dns, None) 1270 for (_, _, _, _, sockaddr) in ai: 1271 if sockaddr and sockaddr[0] == ip: 1272 confirmed = True 1273 break 1274 except socket.gaierror: 1275 pass 1276 canonicalization_cache.set(dns, confirmed) 1277 if not confirmed: 1278 dns = "%s (%s)" % (ip, dns) 1279 if not self.wideremote: 1280 # truncate for narrow display 1281 dns = dns[:40] 1282 if entry.sc: 1283 score = float(entry.sc) 1284 if score > 100000.0: 1285 score = "%8.1f" % score 1286 elif score > 10000.0: 1287 score = "%8.2f" % score 1288 else: 1289 score = "%8.3f" % score 1290 else: 1291 score = "-" 1292 if entry.dr!= None: # 0 is valid 1293 drop = "%4d" % entry.dr 1294 else: 1295 drop = "-" 1296 stats += " %4hx %c %d %d %6d %8s %6s %5s %s" % \ 1297 (entry.rs, rscode, 1298 ntp.magic.PKT_MODE(entry.mv), 1299 ntp.magic.PKT_VERSION(entry.mv), 1300 entry.ct, score, drop, port[1:], dns) 1301 return stats 1302 except ValueError: 1303 # This can happen when ntpd ships a corrupt varlist 1304 return '' 1305 1306 1307class ReslistSummary: 1308 "Reusable class for reslist entry summary generation." 1309 header = """\ 1310 hits addr/prefix or addr mask 1311 restrictions 1312""" 1313 width = 72 1314 1315 @staticmethod 1316 def __getPrefix(mask): 1317 if not mask: 1318 prefix = '' 1319 if ':' in mask: 1320 sep = ':' 1321 base = 16 1322 else: 1323 sep = '.' 1324 base = 10 1325 prefix = sum([bin(int(x, base)).count('1') 1326 for x in mask.split(sep) if x]) 1327 return '/' + str(prefix) 1328 1329 def summary(self, variables): 1330 hits = variables.get("hits", "?") 1331 address = variables.get("addr", "?") 1332 mask = variables.get("mask", "?") 1333 if address == '?' or mask == '?': 1334 return '' 1335 address += ReslistSummary.__getPrefix(mask) 1336 flags = variables.get("flags", "?") 1337 # reslist responses are often corrupted 1338 s = "%10s %s\n %s\n" % (hits, address, flags) 1339 # Throw away corrupted entries. This is a shim - we really 1340 # want to make ntpd stop generating garbage 1341 for c in s: 1342 if not c.isalnum() and c not in "/.: \n": 1343 return '' 1344 return s 1345 1346 1347class IfstatsSummary: 1348 "Reusable class for ifstats entry summary generation." 1349 header = """\ 1350 interface name send 1351 # address/broadcast drop flag received sent failed peers uptime 1352 """ 1353 width = 74 1354 # Numbers are the fieldsize 1355 fields = {'name': '%-24.24s', 1356 'flags': '%4x', 1357 'rx': '%6d', 1358 'tx': '%6d', 1359 'txerr': '%6d', 1360 'pc': '%5d', 1361 'up': '%8d'} 1362 1363 def summary(self, i, variables): 1364 formatted = {} 1365 try: 1366 # Format the fields 1367 for name in self.fields.keys(): 1368 value = variables.get(name, "?") 1369 if value == "?": 1370 fmt = value 1371 else: 1372 fmt = self.fields[name] % value 1373 formatted[name] = fmt 1374 enFlag = '.' if variables.get('en', False) else 'D' 1375 address = variables.get("addr", "?") 1376 bcast = variables.get("bcast") 1377 # Assemble the fields into a line 1378 s = ("%3u %s %s %s %s %s %s %s %s\n %s\n" 1379 % (i, 1380 formatted['name'], 1381 enFlag, 1382 formatted['flags'], 1383 formatted['rx'], 1384 formatted['tx'], 1385 formatted['txerr'], 1386 formatted['pc'], 1387 formatted['up'], 1388 address)) 1389 if bcast: 1390 s += " %s\n" % bcast 1391 except TypeError: # pragma: no cover 1392 # Can happen when ntpd ships a corrupted response 1393 return '' 1394 1395 # FIXME, a brutal and slow way to check for invalid chars.. 1396 # maybe just strip non-printing chars? 1397 for c in s: 1398 if not c.isalnum() and c not in "/.:[] \%\n": 1399 return '' 1400 return s 1401 1402 1403try: 1404 from collections import OrderedDict 1405except ImportError: # pragma: no cover 1406 class OrderedDict(dict): 1407 "A stupid simple implementation in order to be back-portable to 2.6" 1408 1409 # This can be simple because it doesn't need to be fast. 1410 # The programs that use it only have to run at human speed, 1411 # and the collections are small. 1412 def __init__(self, items=None): 1413 dict.__init__(self) 1414 self.__keys = [] 1415 if items: 1416 for (k, v) in items: 1417 self[k] = v 1418 1419 def __setitem__(self, key, val): 1420 dict.__setitem__(self, key, val) 1421 self.__keys.append(key) 1422 1423 def __delitem__(self, key): 1424 dict.__delitem__(self, key) 1425 self.__keys.remove(key) 1426 1427 def keys(self): 1428 return self.__keys 1429 1430 def items(self): 1431 return tuple([(k, self[k]) for k in self.__keys]) 1432 1433 def __iter__(self): 1434 for key in self.__keys: 1435 yield key 1436 1437 1438def prettyuptime(uptime): 1439 result = '' 1440 if uptime >= 86400: 1441 result += '%dD ' % (uptime // 86400) 1442 result += '%02d:%02d:%02d' % ((uptime % 86400) // 1443 3600, (uptime % 3600) // 60, uptime % 60) 1444 return result 1445# end 1446