CI opt-test: Drop Python2 & Bash in Fedora latest.
[ntpsec.git] / ntpclients / ntptrace.py
blob1aba17deed7c14f112e697811ac68f3e34569e8c
1 #! @PYSHEBANG@
2 # -*- coding: utf-8 -*-
3 """
4 ntptrace - trace peers of an NTP server
6 Usage: ntptrace [-n | --numeric] [-m number | --max-hosts=number]
7 [-r hostname | --host=hostname] [--help | --more-help]
8 [-V | --version]
9 hostname
11 See the manual page for details.
12 """
14 # Copyright the NTPsec project contributors
16 # SPDX-License-Identifier: BSD-2-Clause
18 from __future__ import print_function
20 import getopt
21 import re
22 import subprocess
23 import sys
25 try:
26 import ntp.util
27 except ImportError as e:
28 sys.stderr.write(
29 "ntptrace: can't find Python NTP library.\n")
30 sys.stderr.write("%s\n" % e)
31 sys.exit(1)
34 def get_info(host):
35 info = ntp_read_vars(0, [], host)
36 if info is None or 'stratum' not in info:
37 return
39 info['offset'] = round(float(info['offset']) / 1000, 6)
40 info['syncdistance'] = \
41 (float(info['rootdisp']) + (float(info['rootdelay']) / 2)) / 1000
43 return info
46 def get_next_host(peer, host):
47 info = ntp_read_vars(peer, ["srcadr"], host)
48 if info is None:
49 return
50 return info['srcadr']
53 def ntp_read_vars(peer, vars, host):
54 obsolete = {'phase': 'offset',
55 'rootdispersion': 'rootdisp'}
57 if not vars:
58 do_all = True
59 else:
60 do_all = False
61 outvars = {}.fromkeys(vars)
63 if do_all:
64 outvars['status_line'] = {}
66 cmd = ["ntpq", "-n", "-c", "rv %s %s" % (peer, ",".join(vars))]
67 if host is not None:
68 cmd.append(host)
70 try:
71 # sadly subprocess.check_output() is not in Python 2.6
72 proc = subprocess.Popen(
73 cmd,
74 stdout=subprocess.PIPE,
75 stderr=subprocess.STDOUT)
76 out = proc.communicate()[0]
77 output = out.decode('utf-8').splitlines()
78 except subprocess.CalledProcessError as e:
79 print("Could not start ntpq: %s" % e.output, file=sys.stderr)
80 raise SystemExit(1)
81 except OSError as e:
82 print("Could not start ntpq: %s" % e.strerror, file=sys.stderr)
83 raise SystemExit(1)
85 for line in output:
86 if re.search(r'Connection refused', line):
87 return
89 match = re.search(r'^asso?c?id=0 status=(\S{4}) (\S+), (\S+),', line,
90 flags=re.IGNORECASE)
91 if match:
92 outvars['status_line']['status'] = match.group(1)
93 outvars['status_line']['leap'] = match.group(2)
94 outvars['status_line']['sync'] = match.group(3)
96 iterator = re.finditer(r'(\w+)=([^,]+),?\s?', line)
97 for match in iterator:
98 key = match.group(1)
99 val = match.group(2)
100 val = re.sub(r'^"([^"]+)"$', r'\1', val)
101 if key in obsolete:
102 key = obsolete[key]
103 if do_all or key in outvars:
104 outvars[key] = val
106 return outvars
109 usage = r"""ntptrace - trace peers of an NTP server
110 USAGE: ntptrace [-<flag> [<val>] | --<name>[{=| }<val>]]... [host]
112 -n, --numeric Print IP addresses instead of hostnames
113 -m, --max-hosts=num Maximum number of peers to trace
114 -r, --host=str Single remote host
115 -?, --help Display usage information and exit
116 --more-help Pass the extended usage text through a pager
117 -V, --version Output version information and exit
119 Options are specified by doubled hyphens and their name or by a single
120 hyphen and the flag character.""" + "\n"
122 bin_ver = "ntpsec-@NTPSEC_VERSION_EXTENDED@"
123 ntp.util.stdversioncheck(bin_ver)
125 try:
126 (options, arguments) = getopt.getopt(
127 sys.argv[1:], "m:nr:?V",
128 ["help", "host=", "max-hosts=", "more-help", "numeric", "version"])
129 except getopt.GetoptError as err:
130 sys.stderr.write(str(err) + "\n")
131 raise SystemExit(1)
133 numeric = False
134 maxhosts = 99
135 host = '127.0.0.1'
137 for (switch, val) in options:
138 if switch == "-m" or switch == "--max-hosts":
139 errmsg = "Error: -m parameter '%s' not a number\n"
140 maxhosts = ntp.util.safeargcast(val, int, errmsg, usage)
141 elif switch == "-n" or switch == "--numeric":
142 numeric = True
143 elif switch == "-r" or switch == "--host":
144 host = val
145 elif switch == "-?" or switch == "--help" or switch == "--more-help":
146 print(usage, file=sys.stderr)
147 raise SystemExit(0)
148 elif switch == "-V" or switch == "--version":
149 print("ntptrace %s" % ntp.util.stdversion())
150 raise SystemExit(0)
152 if arguments:
153 host = arguments[0]
155 hostcount = 0
157 while True:
158 hostcount += 1
160 info = get_info(host)
162 if info is None:
163 break
165 if not numeric:
166 host = ntp.util.canonicalize_dns(host)
168 print("%s: stratum %d, offset %f, synch distance %f" %
169 (host, int(info['stratum']), info['offset'], info['syncdistance']),
170 end='')
171 if int(info['stratum']) == 1:
172 print(", refid '%s'" % info['refid'], end='')
173 print()
175 if (int(info['stratum']) == 0 or int(info['stratum']) == 1 or
176 int(info['stratum']) == 16):
177 break
179 if re.search(r'^127\.127\.\d{1,3}\.\d{1,3}$', info['refid']):
180 break
182 if hostcount == maxhosts:
183 break
185 next_host = get_next_host(info['peer'], host)
187 if next_host is None:
188 break
189 if re.search(r'^127\.127\.\d{1,3}\.\d{1,3}$', next_host):
190 break
192 host = next_host