# -*- coding: utf-8 -*-
"Common utility functions"
# SPDX-License-Identifier: BSD-2-Clause
from __future__ import print_function, division
import collections
import os
import re
import shutil
import socket
import string
import sys
import time
import ntp.ntpc
import ntp.magic
import ntp.control
# Old CTL_PST defines for version 2.
OLD_CTL_PST_CONFIG = 0x80
OLD_CTL_PST_AUTHENABLE = 0x40
OLD_CTL_PST_AUTHENTIC = 0x20
OLD_CTL_PST_REACH = 0x10
OLD_CTL_PST_SANE = 0x08
OLD_CTL_PST_DISP = 0x04
OLD_CTL_PST_SEL_REJECT = 0
OLD_CTL_PST_SEL_SELCAND = 1
OLD_CTL_PST_SEL_SYNCCAND = 2
OLD_CTL_PST_SEL_SYSPEER = 3
# Units for formatting
UNIT_NS = "ns" # nano second
UNIT_US = u"µs" # micro second
UNIT_MS = "ms" # milli second
UNIT_S = "s" # second
UNIT_KS = "ks" # kilo seconds
UNITS_SEC = [UNIT_NS, UNIT_US, UNIT_MS, UNIT_S, UNIT_KS]
UNIT_PPT = "ppt" # parts per trillion
UNIT_PPB = "ppb" # parts per billion
UNIT_PPM = "ppm" # parts per million
UNIT_PPK = u"‰" # parts per thousand
UNITS_PPX = [UNIT_PPT, UNIT_PPB, UNIT_PPM, UNIT_PPK]
unitgroups = (UNITS_SEC, UNITS_PPX)
# These two functions are not tested because they will muck up the module
# for everything else, and they are simple.
def check_unicode(): # pragma: no cover
if "UTF-8" != sys.stdout.encoding:
deunicode_units()
return True # needed by ntpmon
return False
def deunicode_units(): # pragma: no cover
"""Under certain conditions it is not possible to force unicode output,
this overwrites units that contain unicode with safe versions"""
global UNIT_US
global UNIT_PPK
# Replacement units
new_us = "us"
new_ppk = "ppk"
# Replace units in unit groups
UNITS_SEC[UNITS_SEC.index(UNIT_US)] = new_us
UNITS_PPX[UNITS_PPX.index(UNIT_PPK)] = new_ppk
# Replace the units themselves
UNIT_US = new_us
UNIT_PPK = new_ppk
# Variables that have units
S_VARS = ("tai", "poll")
MS_VARS = ("rootdelay", "rootdisp", "rootdist", "offset", "sys_jitter",
"clk_jitter", "leapsmearoffset", "authdelay", "koffset", "kmaxerr",
"kesterr", "kprecis", "kppsjitter", "fuzz", "clk_wander_threshold",
"tick", "in", "out", "bias", "delay", "jitter", "dispersion",
"fudgetime1", "fudgetime2")
PPM_VARS = ("frequency", "clk_wander")
def dolog(logfp, text, debug, threshold):
"""debug is the current debug value
threshold is the trigger for the current log"""
if logfp is None:
return # can turn off logging by supplying a None file descriptor
text = rfc3339(time.time()) + " " + text + "\n"
if debug >= threshold:
logfp.write(text)
logfp.flush() # we don't want to lose an important log to a crash
def safeargcast(arg, castfunc, errtext, usage):
"""Attempts to typecast an argument, prints and dies on failure.
errtext must contain a %s for splicing in the argument, and be
newline terminated."""
try:
casted = castfunc(arg)
except ValueError:
sys.stderr.write(errtext % arg)
sys.stderr.write(usage)
raise SystemExit(1)
return casted
def stdversion():
"Returns the NTPsec version string in a standard format"
return "ntpsec-%s" % "1.2.2"
def rfc3339(t):
"RFC 3339 string from Unix time, including fractional second."
rep = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t))
t = str(t)
if "." in t:
subsec = t.split(".", 1)[1]
if int(subsec) > 0:
rep += "." + subsec
rep += "Z"
return rep
def deformatNTPTime(txt):
txt = txt[2:] # Strip '0x'
txt = "".join(txt.split(".")) # Strip '.'
value = ntp.util.hexstr2octets(txt)
return value
def hexstr2octets(hexstr):
if (len(hexstr) % 2) != 0:
hexstr = hexstr[:-1] # slice off the last char
values = []
for index in range(0, len(hexstr), 2):
values.append(chr(int(hexstr[index:index+2], 16)))
return "".join(values)
def slicedata(data, slicepoint):
"Breaks a sequence into two pieces at the slice point"
return data[:slicepoint], data[slicepoint:]
def portsplit(hostname):
portsuffix = ""
if hostname.count(":") == 1: # IPv4 with appended port
(hostname, portsuffix) = hostname.split(":")
portsuffix = ":" + portsuffix
elif ']' in hostname: # IPv6
rbrak = hostname.rindex("]")
if ":" in hostname[rbrak:]:
portsep = hostname.rindex(":")
portsuffix = hostname[portsep:]
hostname = hostname[:portsep]
hostname = hostname[1:-1] # Strip brackets
return (hostname, portsuffix)
def parseConf(text):
inQuote = False
quoteStarter = ""
lines = []
tokens = []
current = []
def pushToken():
token = "".join(current)
if not inQuote: # Attempt type conversion
try:
token = int(token)
except ValueError:
try:
token = float(token)
except ValueError:
pass
wrapper = (inQuote, token)
tokens.append(wrapper)
current[:] = []
def pushLine():
if current:
pushToken()
if tokens:
lines.append(tokens[:])
tokens[:] = []
i = 0
tlen = len(text)
while i < tlen:
if inQuote:
if text[i] == quoteStarter: # Ending a text string
pushToken()
quoteStarter = ""
inQuote = False
elif text[i] == "\\": # Starting an escape sequence
i += 1
if text[i] in "'\"n\\":
current.append(eval("\'\\" + text[i] + "\'"))
else:
current.append(text[i])
else:
if text[i] == "#": # Comment
while (i < tlen) and (text[i] != "\n"):
i += 1 # Advance to end of line...
i -= 1 # ...and back up so we don't skip the newline
elif text[i] in "'\"": # Starting a text string
inQuote = True
quoteStarter = text[i]
if current:
pushToken()
elif text[i] == "\\": # Linebreak escape
i += 1
if text[i] != "\n":
raise SyntaxError
elif text[i] == "\n": # EOL: break the lines
pushLine()
elif text[i] in string.whitespace:
if current:
pushToken()
else:
current.append(text[i])
i += 1
pushLine()
return lines
def stringfilt(data):
"Pretty print string of space separated numbers"
parts = data.split()
cooked = []
for part in parts:
# These are expected to fit on a single 80-char line.
# Accounting for other factors this leaves each number with
# 7 chars + a space.
fitted = fitinfield(part, 7)
cooked.append(fitted)
rendered = " ".join(cooked)
return rendered
def stringfiltcooker(data):
"Cooks a filt* string of space separated numbers, expects milliseconds"
parts = data.split()
oomcount = {}
minscale = -100000 # Keep track of the maxdownscale for each value
# Find out what the 'natural' unit of each value is
for part in parts:
# Only care about OOMs, the real scaling happens later
value, oom = scalestring(part)
# Track the highest maxdownscale so we do not invent precision
ds = maxdownscale(part)
minscale = max(ds, minscale)
oomcount[oom] = oomcount.get(oom, 0) + 1
# Find the most common unit
mostcommon = 0
highestcount = 0
for key in oomcount.keys():
if key < minscale:
continue # skip any scale that would result in making up data
count = oomcount[key]
if count > highestcount:
mostcommon = key
highestcount = count
# Shift all values to the new unit
cooked = []
for part in parts:
part = rescalestring(part, mostcommon)
fitted = fitinfield(part, 7)
cooked.append(fitted)
rendered = " ".join(cooked) + " " + UNITS_SEC[mostcommon +
UNITS_SEC.index(UNIT_MS)]
return rendered
def getunitgroup(unit):
"Returns the unit group which contains a given unit"
for group in unitgroups:
if unit in group:
return group
def oomsbetweenunits(a, b):
"Calculates how many orders of magnitude separate two units"
group = getunitgroup(a)
if b is None: # Caller is asking for the distance from the base unit
return group.index(a) * 3
elif b in group:
ia = group.index(a)
ib = group.index(b)
return abs((ia - ib) * 3)
return None
def breaknumberstring(value):
"Breaks a number string into (aboveDecimal, belowDecimal, isNegative?)"
if value[0] == "-":
value = value[1:]
negative = True
else:
negative = False
if "." in value:
above, below = value.split(".")
else:
above = value
below = ""
return (above, below, negative)
def gluenumberstring(above, below, isnegative):
"Glues together parts of a number string"
if above == "":
above = "0"
if below:
newvalue = ".".join((above, below))
else:
newvalue = above
if isnegative:
newvalue = "-" + newvalue
return newvalue
def maxdownscale(value):
"Maximum units a value can be scaled down without inventing data"
if "." in value:
digitcount = len(value.split(".")[1])
# Return a negative so it can be fed directly to a scaling function
return -(digitcount // 3)
else:
# No decimals, the value is already at the maximum down-scale
return 0
def rescalestring(value, unitsscaled):
"Rescale a number string by a given number of units"
whole, dec, negative = breaknumberstring(value)
if unitsscaled == 0:
# This may seem redundant, but glue forces certain formatting details
value = gluenumberstring(whole, dec, negative)
return value
hilen = len(whole)
lolen = len(dec)
digitsmoved = abs(unitsscaled * 3)
if unitsscaled > 0: # Scale to a larger unit, move decimal left
if hilen < digitsmoved:
# Scaling beyond the digits, pad it out. We can pad here
# without making up digits that don't exist
padcount = digitsmoved - hilen
newwhole = ""
newdec = ("0" * padcount) + whole + dec
else: # Scaling in the digits, no need to pad
choppoint = -digitsmoved
newdec = whole[choppoint:] + dec
newwhole = whole[:choppoint]
elif unitsscaled < 0: # scale to a smaller unit, move decimal right
if lolen < digitsmoved:
# Scaling beyond the digits would force us to make up data
# that doesn't exist. So fail.
# The caller should have already caught this with maxdownscale()
return None
else:
newwhole = whole + dec[:digitsmoved]
newdec = dec[digitsmoved:]
newwhole = newwhole.lstrip("0")
newvalue = gluenumberstring(newwhole, newdec, negative)
return newvalue
def formatzero(value):
"Scale a zero value for the unit with the highest available precision"
scale = maxdownscale(value)
newvalue = rescalestring(value, scale).lstrip("-")
return (newvalue, scale)
def scalestring(value):
"Scales a number string to fit in the range 1.0-999.9"
if isstringzero(value):
return formatzero(value)
whole, dec, negative = breaknumberstring(value)
hilen = len(whole)
if (hilen == 0) or isstringzero(whole): # Need to shift to smaller units
i = 0
lolen = len(dec)
while i < lolen: # need to find the actual digits
if dec[i] != "0":
break
i += 1
lounits = (i // 3) + 1 # always need to shift one more unit
movechars = lounits * 3
if lolen < movechars:
# Not enough digits to scale all the way down. Inventing
# digits is unacceptable, so scale down as much as we can.
lounits = (i // 3) # "always", unless out of digits
movechars = lounits * 3
newwhole = dec[:movechars].lstrip("0")
newdec = dec[movechars:]
unitsmoved = -lounits
else: # Shift to larger units
hiunits = hilen // 3 # How many we have, not how many to move
hidigits = hilen % 3
if hidigits == 0: # full unit above the decimal
hiunits -= 1 # the unit above the decimal doesn't count
hidigits = 3
newwhole = whole[:hidigits]
newdec = whole[hidigits:] + dec
unitsmoved = hiunits
newvalue = gluenumberstring(newwhole, newdec, negative)
return (newvalue, unitsmoved)
def fitinfield(value, fieldsize):
"Attempt to fit value into a field, preserving as much data as possible"
vallen = len(value)
if fieldsize is None:
newvalue = value
elif vallen == fieldsize: # Goldilocks!
newvalue = value
elif vallen < fieldsize: # Extra room, pad it out
pad = " " * (fieldsize - vallen)
newvalue = pad + value
else: # Insufficient room, round as few digits as possible
if "." in value: # Ok, we *do* have decimals to crop
diff = vallen - fieldsize
declen = len(value.split(".")[1]) # length of decimals
croplen = min(declen, diff) # Never round above the decimal point
roundlen = declen - croplen # How many digits we round to
newvalue = str(round(float(value), roundlen))
splitted = newvalue.split(".") # This should never fail
declen = len(splitted[1])
if roundlen == 0: # if rounding all the decimals don't display .0
# but do display the point, to show that there is more beyond
newvalue = splitted[0] + "."
elif roundlen > declen: # some zeros have been cropped, fix that
padcount = roundlen - declen
newvalue = newvalue + ("0" * padcount)
else: # No decimals, nothing we can crop
newvalue = value
return newvalue
def cropprecision(value, ooms):
"Crops digits below the maximum precision"
if "." not in value: # No decimals, nothing to crop
return value
if ooms == 0: # We are at the baseunit, crop it all
return value.split(".")[0]
dstart = value.find(".") + 1
dsize = len(value) - dstart
precision = min(ooms, dsize)
cropcount = dsize - precision
if cropcount > 0:
value = value[:-cropcount]
return value
def isstringzero(value):
"Detects whether a string is equal to zero"
for i in value:
if i not in ("-", ".", "0"):
return False
return True
def unitrelativeto(unit, move):
"Returns a unit at a different scale from the input unit"
for group in unitgroups:
if unit in group:
if move is None: # asking for the base unit
return group[0]
else:
index = group.index(unit)
index += move # index of the new unit
if 0 <= index < len(group): # found the new unit
return group[index]
else: # not in range
return None
return None # couldn't find anything
def unitifyvar(value, varname, baseunit=None, width=8, unitSpace=False):
"Call unitify() with the correct units for varname"
if varname in S_VARS:
start = UNIT_S
elif varname in MS_VARS:
start = UNIT_MS
elif varname in PPM_VARS:
start = UNIT_PPM
else:
return value
return unitify(value, start, baseunit, width, unitSpace)
def unitify(value, startingunit, baseunit=None, width=8, unitSpace=False):
"Formats a numberstring with relevant units. Attempts to fit in width."
if baseunit is None:
baseunit = getunitgroup(startingunit)[0]
ooms = oomsbetweenunits(startingunit, baseunit)
if isstringzero(value):
newvalue, unitsmoved = formatzero(value)
else:
newvalue = cropprecision(value, ooms)
newvalue, unitsmoved = scalestring(newvalue)
unitget = unitrelativeto(startingunit, unitsmoved)
if unitSpace:
spaceWidthAdjustment = 1
spacer = " "
else:
spaceWidthAdjustment = 0
spacer = ""
if unitget is not None: # We have a unit
if width is None:
realwidth = None
else:
realwidth = width - (len(unitget) + spaceWidthAdjustment)
newvalue = fitinfield(newvalue, realwidth) + spacer + unitget
else: # don't have a replacement unit, use original
newvalue = value + spacer + startingunit
if width is None:
newvalue = newvalue.strip()
return newvalue
def f8dot4(f):
"Scaled floating point formatting to fit in 8 characters"
if isinstance(f, str):
# a string? pass it on as a signal
return "%8s" % f
if not isinstance(f, (int, float)):
# huh?
return " X"
if str(float(f)).lower() == 'nan':
# yes, this is a better test than math.isnan()
# it also catches None, strings, etc.
return " nan"
fmt = "%8d" # xxxxxxxx or -xxxxxxx
if f >= 0:
if f < 1000.0:
fmt = "%8.4f" # xxx.xxxx normal case
elif f < 10000.0:
fmt = "%8.3f" # xxxx.xxx
elif f < 100000.0:
fmt = "%8.2f" # xxxxx.xx
elif f < 1000000.0:
fmt = "%8.1f" # xxxxxx.x
else:
# negative number, account for minus sign
if f > -100.0:
fmt = "%8.4f" # -xx.xxxx normal case
elif f > -1000.0:
fmt = "%8.3f" # -xxx.xxx
elif f > -10000.0:
fmt = "%8.2f" # -xxxx.xx
elif f > -100000.0:
fmt = "%8.1f" # -xxxxx.x
return fmt % f
def f8dot3(f):
"Scaled floating point formatting to fit in 8 characters"
if isinstance(f, str):
# a string? pass it on as a signal
return "%8s" % f
if not isinstance(f, (int, float)):
# huh?
return " X"
if str(float(f)).lower() == 'nan':
# yes, this is a better test than math.isnan()
# it also catches None, strings, etc.
return " nan"
fmt = "%8d" # xxxxxxxx or -xxxxxxx
if f >= 0:
if f < 10000.0:
fmt = "%8.3f" # xxxx.xxx normal case
elif f < 100000.0:
fmt = "%8.2f" # xxxxx.xx
elif f < 1000000.0:
fmt = "%8.1f" # xxxxxx.x
else:
# negative number, account for minus sign
if f > -1000.0:
fmt = "%8.3f" # -xxx.xxx normal case
elif f > -10000.0:
fmt = "%8.2f" # -xxxx.xx
elif f > -100000.0:
fmt = "%8.1f" # -xxxxx.x
return fmt % f
def monoclock():
"Try to get a monotonic clock value unaffected by NTP stepping."
try:
# Available in Python 3.3 and up.
return time.monotonic()
except AttributeError:
return time.time()
class Cache:
"Simple time-based cache"
def __init__(self, defaultTimeout=300): # 5 min default TTL
self.defaultTimeout = defaultTimeout
self._cache = {}
def get(self, key):
if key in self._cache:
value, settime, ttl = self._cache[key]
if settime >= monoclock() - ttl:
return value
else: # key expired, delete it
del self._cache[key]
return None
else:
return None
def set(self, key, value, customTTL=None):
ttl = customTTL if customTTL is not None else self.defaultTimeout
self._cache[key] = (value, monoclock(), ttl)
# A hack to avoid repeatedly hammering on DNS when ntpmon runs.
canonicalization_cache = Cache()
def canonicalize_dns(inhost, family=socket.AF_UNSPEC):
"Canonicalize a hostname or numeric IP address."
resname = canonicalization_cache.get(inhost)
if resname is not None:
return resname
# Catch garbaged hostnames in corrupted Mode 6 responses
m = re.match("([:.[\]]|\w)*", inhost)
if not m:
raise TypeError
(hostname, portsuffix) = portsplit(inhost)
try:
ai = socket.getaddrinfo(hostname, None, family, 0, 0,
socket.AI_CANONNAME)
except socket.gaierror:
return "DNSFAIL:%s" % hostname
(family, socktype, proto, canonname, sockaddr) = ai[0]
try:
name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
result = name[0].lower() + portsuffix
except socket.gaierror:
# On OS X, canonname is empty for hosts without rDNS.
# Fall back to the hostname.
canonicalized = canonname or hostname
result = canonicalized.lower() + portsuffix
canonicalization_cache.set(inhost, result)
return result
TermSize = collections.namedtuple("TermSize", ["width", "height"])
# Python 2.x does not have the shutil.get_terminal_size function.
# This conditional import is only needed by termsize() and should be kept
# near it. It is not inside the function because the unit tests need to be
# able to splice in a jig.
if str is bytes: # We are on python 2.x
import fcntl
import termios
import struct
def termsize(): # pragma: no cover
"Return the current terminal size."
# Alternatives at http://stackoverflow.com/questions/566746
# The way this is used makes it not a big deal if the default is wrong.
size = (80, 24)
if os.isatty(1):
if str is not bytes:
# str is bytes means we are >py3.0, but this will still fail
# on versions <3.3. We do not support those anyway.
(w, h) = shutil.get_terminal_size((80, 24))
size = (w, h)
else:
try:
# OK, Python version < 3.3, cope
h, w, hp, wp = struct.unpack(
'HHHH',
fcntl.ioctl(2, termios.TIOCGWINSZ,
struct.pack('HHHH', 0, 0, 0, 0)))
size = (w, h)
except IOError:
pass
return TermSize(*size)
class PeerStatusWord:
"A peer status word from readstats(), dissected for display"
def __init__(self, status, pktversion=ntp.magic.NTP_VERSION):
# Event
self.event = ntp.control.CTL_PEER_EVENT(status)
# Event count
self.event_count = ntp.control.CTL_PEER_NEVNT(status)
statval = ntp.control.CTL_PEER_STATVAL(status)
# Config
if statval & ntp.control.CTL_PST_CONFIG:
self.conf = "yes"
else:
self.conf = "no"
# Reach
if statval & ntp.control.CTL_PST_BCAST:
self.reach = "none"
elif statval & ntp.control.CTL_PST_REACH:
self.reach = "yes"
else:
self.reach = "no"
# Auth
if (statval & ntp.control.CTL_PST_AUTHENABLE) == 0:
self.auth = "none"
elif statval & ntp.control.CTL_PST_AUTHENTIC:
self.auth = "ok "
else:
self.auth = "bad"
# Condition
if pktversion > ntp.magic.NTP_OLDVERSION:
seldict = {
ntp.control.CTL_PST_SEL_REJECT: "reject",
ntp.control.CTL_PST_SEL_SANE: "falsetick",
ntp.control.CTL_PST_SEL_CORRECT: "excess",
ntp.control.CTL_PST_SEL_SELCAND: "outlier",
ntp.control.CTL_PST_SEL_SYNCCAND: "candidate",
ntp.control.CTL_PST_SEL_EXCESS: "backup",
ntp.control.CTL_PST_SEL_SYSPEER: "sys.peer",
ntp.control.CTL_PST_SEL_PPS: "pps.peer",
}
self.condition = seldict[statval & 0x7]
else:
if (statval & 0x3) == OLD_CTL_PST_SEL_REJECT:
if (statval & OLD_CTL_PST_SANE) == 0:
self.condition = "insane"
elif (statval & OLD_CTL_PST_DISP) == 0:
self.condition = "hi_disp"
else:
self.condition = ""
elif (statval & 0x3) == OLD_CTL_PST_SEL_SELCAND:
self.condition = "sel_cand"
elif (statval & 0x3) == OLD_CTL_PST_SEL_SYNCCAND:
self.condition = "sync_cand"
elif (statval & 0x3) == OLD_CTL_PST_SEL_SYSPEER:
self.condition = "sys_peer"
# Last Event
event_dict = {
ntp.magic.PEVNT_MOBIL: "mobilize",
ntp.magic.PEVNT_DEMOBIL: "demobilize",
ntp.magic.PEVNT_UNREACH: "unreachable",
ntp.magic.PEVNT_REACH: "reachable",
ntp.magic.PEVNT_RESTART: "restart",
ntp.magic.PEVNT_REPLY: "no_reply",
ntp.magic.PEVNT_RATE: "rate_exceeded",
ntp.magic.PEVNT_DENY: "access_denied",
ntp.magic.PEVNT_ARMED: "leap_armed",
ntp.magic.PEVNT_NEWPEER: "sys_peer",
ntp.magic.PEVNT_CLOCK: "clock_alarm",
}
self.last_event = event_dict.get(ntp.magic.PEER_EVENT | self.event, "")
def __str__(self):
return ("conf=%(conf)s, reach=%(reach)s, auth=%(auth)s, "
"cond=%(condition)s, event=%(last_event)s ec=%(event_count)s"
% self.__dict__)
def cook(variables, showunits=False, sep=", "):
"Cooked-mode variable display."
width = ntp.util.termsize().width - 2
text = ""
specials = ("filtdelay", "filtoffset", "filtdisp", "filterror")
longestspecial = len(max(specials, key=len))
for (name, (value, rawvalue)) in variables.items():
if name in specials: # need special formatting for column alignment
formatter = "%" + str(longestspecial) + "s ="
item = formatter % name
else:
item = "%s=" % name
if name in ("reftime", "clock", "org", "rec", "xmt"):
item += ntp.ntpc.prettydate(value)
elif name in ("srcadr", "peeradr", "dstadr", "refid"):
# C ntpq cooked these in obscure ways. Since they
# came up from the daemon as human-readable
# strings this was probably a bad idea, but we'll
# leave this case separated in case somebody thinks
# re-cooking them is a good idea.
item += value
elif name == "leap":
item += ("00", "01", "10", "11")[value]
elif name == "reach":
item += "%03lo" % value
elif name in specials:
if showunits:
item += stringfiltcooker(value)
else:
item += "\t".join(value.split())
elif name == "flash":
item += "%02x " % value
if value == 0:
item += "ok "
else:
# flasher bits
tstflagnames = (
"pkt_dup", # BOGON1
"pkt_bogus", # BOGON2
"pkt_unsync", # BOGON3
"pkt_denied", # BOGON4
"pkt_auth", # BOGON5
"pkt_stratum", # BOGON6
"pkt_header", # BOGON7
"pkt_autokey", # BOGON8
"pkt_crypto", # BOGON9
"peer_stratum", # BOGON10
"peer_dist", # BOGON11
"peer_loop", # BOGON12
"peer_unreach" # BOGON13
)
for (i, n) in enumerate(tstflagnames):
if (1 << i) & value:
item += tstflagnames[i] + " "
item = item[:-1]
elif name in MS_VARS:
# Note that this is *not* complete, there are definitely
# missing variables here.
# Completion cannot occur until all units are tracked down.
if showunits:
item += unitify(rawvalue, UNIT_MS, UNIT_NS, width=None)
else:
item += repr(value)
elif name in S_VARS:
if showunits:
item += unitify(rawvalue, UNIT_S, UNIT_NS, width=None)
else:
item += repr(value)
elif name in PPM_VARS:
if showunits:
item += unitify(rawvalue, UNIT_PPM, width=None)
else:
item += repr(value)
else:
item += repr(value)
# add field separator
item += sep
# add newline so we don't overflow screen
lastcount = 0
for c in text:
if c == '\n':
lastcount = 0
else:
lastcount += 1
if lastcount + len(item) > width:
text = text[:-1] + "\n"
text += item
text = text[:-2] + "\n"
return text
class PeerSummary:
"Reusable report generator for peer statistics"
def __init__(self, displaymode, pktversion, showhostnames,
wideremote, showunits=False, termwidth=None,
debug=0, logfp=sys.stderr):
self.displaymode = displaymode # peers/apeers/opeers
self.pktversion = pktversion # interpretation of flash bits
self.showhostnames = showhostnames # If false, display numeric IPs
self.showunits = showunits # If False show old style float
self.wideremote = wideremote # show wide remote names?
self.debug = debug
self.logfp = logfp
self.termwidth = termwidth
# By default, the peer spreadsheet layout is designed so lines just
# fit in 80 characters. This tells us how much extra horizontal space
# we have available on a wider terminal emulator.
self.horizontal_slack = min((termwidth or 80) - 80, 24)
# Peer spreadsheet column widths. The reason we cap extra
# width used at 24 is that on very wide displays, slamming the
# non-hostname fields all the way to the right produces a huge
# river that makes the entries difficult to read as wholes.
# This choice caps the peername field width at that of the longest
# possible IPV6 numeric address.
self.namewidth = 15 + self.horizontal_slack
self.refidwidth = 15
# Compute peer spreadsheet headers
self.__remote = " remote ".ljust(self.namewidth)
self.__common = "st t when poll reach delay offset "
self.__header = None
self.polls = []
@staticmethod
def prettyinterval(diff):
"Print an interval in natural time units."
if not isinstance(diff, int) or diff <= 0:
return '-'
if diff <= 2048:
return str(diff)
diff = (diff + 29) / 60
if diff <= 300:
return "%dm" % diff
diff = (diff + 29) / 60
if diff <= 96:
return "%dh" % diff
diff = (diff + 11) / 24
return "%dd" % diff
@staticmethod
def high_truncate(hostname, maxlen):
"Truncate on the left using leading _ to indicate 'more'."
# Used for local IPv6 addresses, best distinguished by low bits
if len(hostname) <= maxlen:
return hostname
else:
return '-' + hostname[-maxlen+1:]
@staticmethod
def is_clock(variables):
"Does a set of variables look like it returned from a clock?"
return "srchost" in variables and '(' in variables["srchost"][0]
def header(self):
"Column headers for peer display"
if self.displaymode == "apeers":
self.__header = self.__remote + \
" refid assid ".ljust(self.refidwidth) + \
self.__common + "jitter"
elif self.displaymode == "opeers":
self.__header = self.__remote + \
" local ".ljust(self.refidwidth) + \
self.__common + " disp"
elif self.displaymode == 'rpeers':
self.__header = ' st t when poll reach delay ' + \
'offset jitter refid T remote'
else:
self.__header = self.__remote + \
" refid ".ljust(self.refidwidth) + \
self.__common + "jitter"
return self.__header
def width(self):
"Width of display"
return 79 + self.horizontal_slack
def summary(self, rstatus, variables, associd):
"Peer status summary line."
clock_name = ''
dstadr_refid = ""
dstport = 0
estdelay = '.'
estdisp = '.'
estjitter = '.'
estoffset = '.'
filtdelay = 0.0
filtdisp = 0.0
filtoffset = 0.0
flash = 0
have_jitter = False
headway = 0
hmode = 0
hpoll = 0
keyid = 0
last_sync = None
leap = 0
pmode = 0
ppoll = 0
precision = 0
ptype = '?'
reach = 0
rec = None
reftime = None
rootdelay = 0.0
saw6 = False # x.6 floats for delay and friends
srcadr = None
srchost = None
srcport = 0
stratum = 20
mode = 0
unreach = 0
xmt = 0
ntscookies = -1
now = time.time()
for item in variables.items():
if 2 != len(item) or 2 != len(item[1]):
# bad item
continue
(name, (value, rawvalue)) = item
if name == "delay":
estdelay = rawvalue if self.showunits else value
if len(rawvalue) > 6 and rawvalue[-7] == ".":
saw6 = True
elif name == "dstadr":
# The C code tried to get a fallback ptype from this in case
# the hmode field was not included
if "local" in self.__header:
dstadr_refid = rawvalue
elif name == "dstport":
# FIXME, dstport never used.
dstport = value
elif name == "filtdelay":
# FIXME, filtdelay never used.
filtdelay = value
elif name == "filtdisp":
# FIXME, filtdisp never used.
filtdisp = value
elif name == "filtoffset":
# FIXME, filtoffset never used.
filtoffset = value
elif name == "flash":
# FIXME, flash never used.
flash = value
elif name == "headway":
# FIXME, headway never used.
headway = value
elif name == "hmode":
hmode = value
elif name == "hpoll":
hpoll = value
if hpoll < 0:
hpoll = ntp.magic.NTP_MINPOLL
elif name == "jitter":
if "jitter" in self.__header:
estjitter = rawvalue if self.showunits else value
have_jitter = True
elif name == "keyid":
# FIXME, keyid never used.
keyid = value
elif name == "leap":
# FIXME, leap never used.
leap = value
elif name == "offset":
estoffset = rawvalue if self.showunits else value
elif name == "pmode":
# FIXME, pmode never used.
pmode = value
elif name == "ppoll":
ppoll = value
if ppoll < 0:
ppoll = ntp.magic.NTP_MINPOLL
elif name == "precision":
# FIXME, precision never used.
precision = value
elif name == "reach":
# Shipped as hex, displayed in octal
reach = value
elif name == "refid":
# The C code for this looked crazily overelaborate. Best
# guess is that it was designed to deal with formats that
# no longer occur in this field.
if "refid" in self.__header:
dstadr_refid = rawvalue
elif name == "rec":
rec = value # l_fp timestamp
last_sync = int(now - ntp.ntpc.lfptofloat(rec))
elif name == "reftime":
reftime = value # l_fp timestamp
last_sync = int(now - ntp.ntpc.lfptofloat(reftime))
elif name == "rootdelay":
# FIXME, rootdelay never used.
rootdelay = value # l_fp timestamp
elif name == "rootdisp" or name == "dispersion":
estdisp = rawvalue if self.showunits else value
elif name in ("srcadr", "peeradr"):
srcadr = value
elif name == "srchost":
srchost = value
elif name == "srcport" or name == "peerport":
# FIXME, srcport never used.
srcport = value
elif name == "stratum":
stratum = value
elif name == "mode":
# FIXME, mode never used.
mode = value
elif name == "unreach":
# FIXME, unreach never used.
unreach = value
elif name == "xmt":
# FIXME, xmt never used.
xmt = value
elif name == "ntscookies":
ntscookies = value
else:
# unknown name?
# line = " name=%s " % (name) # debug
# return line # debug
continue
if hmode == ntp.magic.MODE_BCLIENTX:
# broadcastclient or multicastclient
ptype = 'b'
elif hmode == ntp.magic.MODE_BROADCASTx:
# broadcast or multicast server
if srcadr.startswith("224."): # IANA multicast address prefix
ptype = 'M'
else:
ptype = 'B'
elif hmode == ntp.magic.MODE_CLIENT:
if PeerSummary.is_clock(variables):
ptype = 'l' # local refclock
elif dstadr_refid == "POOL":
ptype = 'p' # pool
elif srcadr.startswith("224."):
ptype = 'a' # manycastclient (compatibility with Classic)
elif ntscookies > -1:
# FIXME: Will foo up if there are ever more than 9 cookies
ptype = chr(ntscookies + ord('0'))
else:
ptype = 'u' # unicast
elif hmode == ntp.magic.MODE_ACTIVEx:
ptype = 's' # symmetric active
elif hmode == ntp.magic.MODE_PASSIVEx:
ptype = 'S' # symmetric passive
#
# Got everything, format the line
#
line = ""
poll_sec = 1 << min(ppoll, hpoll)
self.polls.append(poll_sec)
if self.pktversion > ntp.magic.NTP_OLDVERSION:
c = " x.-+#*o"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x7]
else:
c = " .+*"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x3]
# Source host or clockname or poolname or servername
# After new DNS, 2017-Apr-17
# servers setup via numerical IP Address have only srcadr
# servers setup via DNS have both srcadr and srchost
# refclocks have both srcadr and srchost
# pool has "0.0.0.0" (or "::") and srchost
# slots setup via pool have only srcadr
if srcadr is not None \
and srcadr != "0.0.0.0" \
and not srcadr.startswith("127.127") \
and srcadr != "::":
if self.showhostnames & 2 and 'srchost' in locals() and srchost:
clock_name = srchost
elif self.showhostnames & 1:
try:
if self.debug:
self.logfp.write("DNS lookup begins...\n")
clock_name = canonicalize_dns(srcadr)
if self.debug:
self.logfp.write("DNS lookup ends.\n")
except TypeError: # pragma: no cover
return ''
else:
clock_name = srcadr
else:
clock_name = srchost
if clock_name is None:
if srcadr:
clock_name = srcadr
else:
clock_name = ""
if self.displaymode != "rpeers":
if self.wideremote and len(clock_name) > self.namewidth:
line += ("%c%s\n" % (c, clock_name))
line += (" " * (self.namewidth + 2))
else:
line += ("%c%-*.*s " % (c, self.namewidth, self.namewidth,
clock_name[:self.namewidth]))
# Destination address, assoc ID or refid.
assocwidth = 7 if self.displaymode == "apeers" else 0
if "." not in dstadr_refid and ":" not in dstadr_refid:
dstadr_refid = "." + dstadr_refid + "."
if assocwidth and len(dstadr_refid) >= self.refidwidth - assocwidth:
visible = "..."
else:
visible = dstadr_refid
if self.displaymode != "rpeers":
line += self.high_truncate(visible, self.refidwidth)
if self.displaymode == "apeers":
line += (" " * (self.refidwidth - len(visible) - assocwidth + 1))
line += ("%-6d" % (associd))
else:
line += (" " * (self.refidwidth - len(visible)))
# The rest of the story
if last_sync is None:
last_sync = now
jd = estjitter if have_jitter else estdisp
if self.showunits:
fini = lambda x : unitify(x, UNIT_MS)
elif saw6:
fini = lambda x : f8dot4(x)
else:
fini = lambda x : f8dot3(x)
line += (
" %2ld %c %4.4s %4.4s %3lo %s %s %s"
% (stratum, ptype,
PeerSummary.prettyinterval(last_sync),
PeerSummary.prettyinterval(poll_sec), reach,
fini(estdelay), fini(estoffset), fini(jd)))
line += "\n"
# for debugging both case
# if srcadr != None and srchost != None:
# line += "srcadr: %s, srchost: %s\n" % (srcadr, srchost)
return line
def intervals(self):
"Return and flush the list of actual poll intervals."
res = self.polls[:]
self.polls = []
return res
class MRUSummary:
"Reusable class for MRU entry summary generation."
def __init__(self, showhostnames, wideremote=False,
debug=0, logfp=sys.stderr):
self.debug = debug
self.logfp = logfp
self.now = None
self.showhostnames = showhostnames # if & 1, display names
self.wideremote = wideremote
header = " lstint avgint rstr r m v count score drop rport remote address"
def summary(self, entry):
first = ntp.ntpc.lfptofloat(entry.first)
last = ntp.ntpc.lfptofloat(entry.last)
active = float(last - first)
count = int(entry.ct)
if self.now:
lstint = int(self.now - last + 0.5)
stats = "%7d" % lstint
if count == 1:
favgint = 0
else:
favgint = active / (count-1)
avgint = int(favgint + 0.5)
if 5.0 < favgint or 1 == count:
stats += " %6d" % avgint
elif 1.0 <= favgint:
stats += " %6.2f" % favgint
else:
stats += " %6.3f" % favgint
else:
MJD_1970 = 40587 # MJD for 1 Jan 1970, Unix epoch
days, lstint = divmod(int(last), 86400)
stats = "%5d %5d %6d" % (days + MJD_1970, lstint, active)
if entry.rs & ntp.magic.RES_KOD:
rscode = 'K'
elif entry.rs & ntp.magic.RES_LIMITED:
rscode = 'L'
else:
rscode = '.'
(ip, port) = portsplit(entry.addr)
try:
if not self.showhostnames & 1: # if not & 1 display numeric IPs
dns = ip
else:
dns = canonicalize_dns(ip)
# Forward-confirm the returned DNS
confirmed = canonicalization_cache.get(dns)
if confirmed is None:
confirmed = False
try:
ai = socket.getaddrinfo(dns, None)
for (_, _, _, _, sockaddr) in ai:
if sockaddr and sockaddr[0] == ip:
confirmed = True
break
except socket.gaierror:
pass
canonicalization_cache.set(dns, confirmed)
if not confirmed:
dns = "%s (%s)" % (ip, dns)
if not self.wideremote:
# truncate for narrow display
dns = dns[:40]
if entry.sc:
score = float(entry.sc)
if score > 100000.0:
score = "%8.1f" % score
elif score > 10000.0:
score = "%8.2f" % score
else:
score = "%8.3f" % score
else:
score = "-"
if entry.dr!= None: # 0 is valid
drop = "%4d" % entry.dr
else:
drop = "-"
stats += " %4hx %c %d %d %6d %8s %6s %5s %s" % \
(entry.rs, rscode,
ntp.magic.PKT_MODE(entry.mv),
ntp.magic.PKT_VERSION(entry.mv),
entry.ct, score, drop, port[1:], dns)
return stats
except ValueError:
# This can happen when ntpd ships a corrupt varlist
return ''
class ReslistSummary:
"Reusable class for reslist entry summary generation."
header = """\
hits addr/prefix or addr mask
restrictions
"""
width = 72
@staticmethod
def __getPrefix(mask):
if not mask:
prefix = ''
if ':' in mask:
sep = ':'
base = 16
else:
sep = '.'
base = 10
prefix = sum([bin(int(x, base)).count('1')
for x in mask.split(sep) if x])
return '/' + str(prefix)
def summary(self, variables):
hits = variables.get("hits", "?")
address = variables.get("addr", "?")
mask = variables.get("mask", "?")
if address == '?' or mask == '?':
return ''
address += ReslistSummary.__getPrefix(mask)
flags = variables.get("flags", "?")
# reslist responses are often corrupted
s = "%10s %s\n %s\n" % (hits, address, flags)
# Throw away corrupted entries. This is a shim - we really
# want to make ntpd stop generating garbage
for c in s:
if not c.isalnum() and c not in "/.: \n":
return ''
return s
class IfstatsSummary:
"Reusable class for ifstats entry summary generation."
header = """\
interface name send
# address/broadcast drop flag received sent failed peers uptime
"""
width = 74
# Numbers are the fieldsize
fields = {'name': '%-24.24s',
'flags': '%4x',
'rx': '%6d',
'tx': '%6d',
'txerr': '%6d',
'pc': '%5d',
'up': '%8d'}
def summary(self, i, variables):
formatted = {}
try:
# Format the fields
for name in self.fields.keys():
value = variables.get(name, "?")
if value == "?":
fmt = value
else:
fmt = self.fields[name] % value
formatted[name] = fmt
enFlag = '.' if variables.get('en', False) else 'D'
address = variables.get("addr", "?")
bcast = variables.get("bcast")
# Assemble the fields into a line
s = ("%3u %s %s %s %s %s %s %s %s\n %s\n"
% (i,
formatted['name'],
enFlag,
formatted['flags'],
formatted['rx'],
formatted['tx'],
formatted['txerr'],
formatted['pc'],
formatted['up'],
address))
if bcast:
s += " %s\n" % bcast
except TypeError: # pragma: no cover
# Can happen when ntpd ships a corrupted response
return ''
# FIXME, a brutal and slow way to check for invalid chars..
# maybe just strip non-printing chars?
for c in s:
if not c.isalnum() and c not in "/.:[] \%\n":
return ''
return s
try:
from collections import OrderedDict
except ImportError: # pragma: no cover
class OrderedDict(dict):
"A stupid simple implementation in order to be back-portable to 2.6"
# This can be simple because it doesn't need to be fast.
# The programs that use it only have to run at human speed,
# and the collections are small.
def __init__(self, items=None):
dict.__init__(self)
self.__keys = []
if items:
for (k, v) in items:
self[k] = v
def __setitem__(self, key, val):
dict.__setitem__(self, key, val)
self.__keys.append(key)
def __delitem__(self, key):
dict.__delitem__(self, key)
self.__keys.remove(key)
def keys(self):
return self.__keys
def items(self):
return tuple([(k, self[k]) for k in self.__keys])
def __iter__(self):
for key in self.__keys:
yield key
def packetize(packets, period, clipdigits=0, periodized=False):
"""Given a number of packets and a duration (s) return a tuple.
return the packet quantity, and a two part rate in packets/seconds
or seconds/packet. On error the latter fields should be blank, the
first the number of packets if zero otherwise unhelpful text."""
if not isinstance(packets, int):
return ("???", "", "")
if packets == 0 or not isinstance(period, (int, float)):
return (packets, "", "")
if packets > period:
return (packets, round(packets / period, clipdigits), "p/s")
if periodized:
return (packets, periodize(period / packets, clipdigits)[1], "/p")
return (packets, round(period / packets, clipdigits), "s/p")
def periodize(period, clipdigits=0):
"""Given a number of seconds, return number and pretty string.
On error return None for the number and an unhelpful string."""
clip = clipdigits if isinstance(clipdigits, (int, float)) else 0
if not isinstance(period, (int, float)):
return (None, "???")
result = ""
_ = round(period, clip)
nperiod = int(_) if clip < 1 else _
if nperiod >= 86400:
result += "%dD " % (nperiod // 86400)
result += "%02d:%02d:%02d" % (
(nperiod % 86400) // 3600,
(nperiod % 3600) // 60,
nperiod % 60,
)
return (nperiod, result)
uptime = lambda p: periodize(p)[1]
# end
|