File: //usr/bin/ntpsweep
#! /usr/bin/python3 -s
# -*- coding: utf-8 -*-
"""
ntpsweep - print various information about given NTP servers
USAGE: ntpsweep [-<flag> [<val>] | --<name>[{=| }<val>]]... [hostfile]
-h, --host=str Host to execute actions on
-l, --host-list=str Comma-delimited list of Hosts to
execute actions on
-p, --peers Recursively list all peers a host syncs to
-m, --maxlevel=num Traverse peers up to this level
(4 is a reasonable number)
-s, --strip=str Strip this string from hostnames
-V, --version Output version information and exit
Options are specified by doubled hyphens and their name or by a single
hyphen and the flag character.
"""
# SPDX-License-Identifier: BSD-2-Clause
#
# Python translation by ESR of a Perl script written long ago by
# Hans Lambermont <ntpsweep@lambermont.dyndns.org>
#
# It is unclear how useful this will be under modern conditions (e.g. most
# hosts refuse to be queried, and requests to them will just time out).
from __future__ import print_function
import os
import sys
import getopt
try:
import ntp.packet
import ntp.util
except ImportError as e:
sys.stderr.write(
"ntpsweep: can't find Python NTP library.\n")
sys.stderr.write("%s\n" % e)
sys.exit(1)
def ntp_peers(host):
"""Return: a list of peer IP addrs for a specified host,
empty list if query failed.
"""
try:
with os.popen("ntpq -npw " + host) as rp:
hostlines = rp.readlines()[2:] # Drop display header
# Strip tally mark from first field
return [ln.split()[0][1:] for
ln in hostlines if ln[0] in " x.-+#*o"]
except OSError:
return []
def scan_host(host, level):
stratum = 0
offset = 0
daemonversion = ""
system = ""
processor = ""
known_host = False
if host in known_host_info:
known_host = True
else:
session = ntp.packet.ControlSession()
session.openhost(host)
sysvars = session.readvar()
# got answers ? If so, go on.
if isinstance(sysvars, dict):
stratum = sysvars['stratum']
offset = sysvars['offset']
daemonversion = sysvars['version']
system = sysvars['system']
processor = sysvars['processor']
# Shorten daemon_version string.
# daemonversion =~ s/(|Mon|Tue|Wed|Thu|Fri|Sat|Sun).*$//
daemonversion = daemonversion.replace("version=", "")
daemonversion = daemonversion.replace("ntpd ", "")
daemonversion = daemonversion.replace("(", "").replace(")", "")
daemonversion = daemonversion.replace("beta", "b")
daemonversion = daemonversion.replace("multicast", "mc")
# Shorten system string. Note, the assumptions here
# are very old, reflecting ancient big-iron Unixes
system = system.replace("UNIX/", "")
system = system.replace("RELEASE", "r")
system = system.replace("CURRENT", "c")
# Shorten processor string
processor = processor.replace("unknown", "")
# got answers ? If so, go on.
if daemonversion and recurse:
# Consider doing something more intelligent on failure
# than simply returning an empty list. Though it might
# be the right thing to do under modern conditions in
# which most hosts will refuse to be queried.
known_host_peers[host] = ntp_peers(host)
# Collect info on host
if stratum:
known_host_info[host] = "%2d %9.3f %-11s %-12s %s" \
% (stratum, offset, daemonversion[:11],
system[:12], processor[0:9])
else:
# Stratum level 0 is considered invalid
known_host_info[host] = " ?"
if stratum or known_host: # Valid or known host
printhost = (' ' * level) + (ntp.util.canonicalize_dns(host) or host)
# Shorten host string
if strip:
printhost = printhost.replace(strip, "")
# append number of peers in brackets if requested and valid
if (recurse and (known_host_info[host] != " ?") and
(host in known_host_peers)):
printhost += " (%d)" % len(known_host_peers[host])
# Finally print complete host line
print("%-32s %s" % (printhost[:32], known_host_info[host]))
if recurse and (maxlevel == 0 or level < maxlevel):
trace.append(host)
# Loop through peers
for peer in known_host_peers[host]:
if peer in trace:
# we've detected a loop!
printhost = (' ' * (level + 1)) + "= " + peer
# Shorten host string
if strip:
printhost = printhost.replace(strip, "")
print("%-32s" % printhost[:32])
else:
# FIXME: Ugh! Magic-address assumption.
# Needed to deal with peers running legacy NTP.
# Might cause problems in the future. First
# part of the guard is an attempt to skip
# NTPsec-style clock IDs.
if peer[0].isdigit() and not peer.startswith("127"):
scan_host(peer, level + 1)
else: # We did not get answers from this host
printhost = (' ' * level) + (ntp.util.canonicalize_dns(host) or host)
if strip:
printhost = printhost.replace(strip, "")
print("%-32s ?" % printhost[:32])
if __name__ == '__main__':
bin_ver = "ntpsec-1.2.2a"
if ntp.util.stdversion() != bin_ver:
sys.stderr.write("Module/Binary version mismatch\n")
sys.stderr.write("Binary: %s\n" % bin_ver)
sys.stderr.write("Module: %s\n" % ntp.util.stdversion())
try:
(options, arguments) = getopt.getopt(
sys.argv[1:], "h:l:m:ps:?V",
["host=", "host-list=", "maxlevel=", "peers", "strip=", "version"])
except getopt.GetoptError as err:
sys.stderr.write(str(err) + "\n")
raise SystemExit(1)
hostlist = []
maxlevel = 1
recurse = False
strip = ""
for (switch, val) in options:
if switch == "-h" or switch == "--host":
hostlist = [val]
elif switch == "-l" or switch == "--host-list":
hostlist = val.split(",")
elif switch == "-m" or switch == "--maxlevel":
errmsg = "Error: -m parameter '%s' not a number\n"
maxlevel = ntp.util.safeargcast(val, int, errmsg, __doc__)
elif switch == "-p" or switch == "--peers":
recurse = True
elif switch == "-s" or switch == "--strip":
strip = val
elif switch == "-?" or switch == "--help":
print(__doc__, file=sys.stderr)
raise SystemExit(0)
elif switch == "-V" or switch == "--version":
print("ntpsweep %s" % ntp.util.stdversion())
raise SystemExit(0)
try:
if arguments:
hostlist += [ln.strip() for ln in open(arguments[0]).readlines()]
except IOError:
sys.stderr.write("Host file not found.\n")
raise SystemExit(1)
if not hostlist:
hostlist = ["localhost"]
# Print header
print("""\
Host st offset(s) version system processor
--------------------------------+--+---------+-----------+------------+---------\
""")
known_host_info = {}
known_host_peers = {}
trace = []
for host in hostlist:
try:
scan_host(host, 0)
except ntp.packet.ControlException as e:
sys.stderr.write(e.message + "\n")
continue
sys.exit(0)
# end