#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
ntptrace - trace peers of an NTP server

Usage: ntptrace [-n | --numeric] [-m number | --max-hosts=number]
                [-r hostname | --host=hostname] [--help | --more-help]
                hostname

See the manual page for details.
"""
# SPDX-License-Identifier: BSD-2-Clause

from __future__ import print_function

import getopt
import re
import subprocess
import sys

try:
    import ntp.util
except ImportError as e:
    sys.stderr.write(
        "ntptrace: can't find Python NTP library.\n")
    sys.stderr.write("%s\n" % e)
    sys.exit(1)


def get_info(host):
    info = ntp_read_vars(0, [], host)
    if info is None or 'stratum' not in info:
        return

    info['offset'] = round(float(info['offset']) / 1000, 6)
    info['syncdistance'] = \
        (float(info['rootdisp']) + (float(info['rootdelay']) / 2)) / 1000

    return info


def get_next_host(peer, host):
    info = ntp_read_vars(peer, ["srcadr"], host)
    if info is None:
        return
    return info['srcadr']


def ntp_read_vars(peer, vars, host):
    obsolete = {'phase': 'offset',
                'rootdispersion': 'rootdisp'}

    if not len(vars):
        do_all = True
    else:
        do_all = False
    outvars = {}.fromkeys(vars)

    if do_all:
        outvars['status_line'] = {}

    cmd = ["ntpq", "-n", "-c", "rv %s %s" % (peer, ",".join(vars))]
    if host is not None:
        cmd.append(host)

    try:
        # sadly subprocess.check_output() is not in Python 2.6
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT)
        out = proc.communicate()[0]
        output = out.decode('utf-8').splitlines()
    except subprocess.CalledProcessError as e:
        print("Could not start ntpq: %s" % e.output, file=sys.stderr)
        raise SystemExit(1)
    except OSError as e:
        print("Could not start ntpq: %s" % e.strerror, file=sys.stderr)
        raise SystemExit(1)

    for line in output:
        if re.search(r'Connection refused', line):
            return

        match = re.search(r'^asso?c?id=0 status=(\S{4}) (\S+), (\S+),', line,
                          flags=re.IGNORECASE)
        if match:
            outvars['status_line']['status'] = match.group(1)
            outvars['status_line']['leap'] = match.group(2)
            outvars['status_line']['sync'] = match.group(3)

        iterator = re.finditer(r'(\w+)=([^,]+),?\s?', line)
        for match in iterator:
            key = match.group(1)
            val = match.group(2)
            val = re.sub(r'^"([^"]+)"$', r'\1', val)
            if key in obsolete:
                key = obsolete[key]
            if do_all or key in outvars:
                outvars[key] = val

    return outvars


usage = r"""ntptrace - trace peers of an NTP server
USAGE: ntptrace [-<flag> [<val>] | --<name>[{=| }<val>]]... [host]

    -n, --numeric                Print IP addresses instead of hostnames
    -m, --max-hosts=num          Maximum number of peers to trace
    -r, --host=str               Single remote host
    -?, --help                   Display usage information and exit
        --more-help              Pass the extended usage text through a pager

Options are specified by doubled hyphens and their name or by a single
hyphen and the flag character.""" + "\n"

try:
    (options, arguments) = getopt.getopt(
        sys.argv[1:], "m:nr:?",
        ["help", "host=", "max-hosts=", "more-help", "numeric"])
except getopt.GetoptError as err:
    sys.stderr.write(str(err) + "\n")
    raise SystemExit(1)

numeric = False
maxhosts = 99
host = '127.0.0.1'

for (switch, val) in options:
    if switch == "-m" or switch == "--max-hosts":
        errmsg = "Error: -m parameter '%s' not a number\n"
        maxhosts = ntp.util.safeargcast(val, int, errmsg, usage)
    elif switch == "-n" or switch == "--numeric":
        numeric = True
    elif switch == "-r" or switch == "--host":
        host = val
    elif switch == "-?" or switch == "--help" or switch == "--more-help":
        print(usage, file=sys.stderr)
        raise SystemExit(0)

if len(arguments):
    host = arguments[0]

hostcount = 0

while True:
    hostcount += 1

    info = get_info(host)

    if info is None:
        break

    if not numeric:
        host = ntp.util.canonicalize_dns(host)

    print("%s: stratum %d, offset %f, synch distance %f" %
          (host, int(info['stratum']), info['offset'], info['syncdistance']),
          end='')
    if int(info['stratum']) == 1:
        print(", refid '%s'" % info['refid'], end='')
    print()

    if (int(info['stratum']) == 0 or int(info['stratum']) == 1 or
            int(info['stratum']) == 16):
        break

    if re.search(r'^127\.127\.\d{1,3}\.\d{1,3}$', info['refid']):
        break

    if hostcount == maxhosts:
        break

    next_host = get_next_host(info['peer'], host)

    if next_host is None:
        break
    if re.search(r'^127\.127\.\d{1,3}\.\d{1,3}$', next_host):
        break

    host = next_host

[((118, 27, 120, 64), 'getopt.getopt', 'getopt.getopt', ({(119, 8, 119, 20): 'sys.argv[1:]', (119, 22, 119, 30): '"""m:nr:?"""', (120, 8, 120, 63): "['help', 'host=', 'max-hosts=', 'more-help', 'numeric']"}, {}), "(sys.argv[1:], 'm:nr:?', ['help', 'host=', 'max-hosts=',\n    'more-help', 'numeric'])", False, 'import getopt\n'), ((168, 7, 168, 64), 're.search', 're.search', ({(168, 17, 168, 48): '"""^127\\\\.127\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$"""', (168, 50, 168, 63): "info['refid']"}, {}), "('^127\\\\.127\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$', info['refid'])", False, 'import re\n'), ((178, 7, 178, 60), 're.search', 're.search', ({(178, 17, 178, 48): '"""^127\\\\.127\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$"""', (178, 50, 178, 59): 'next_host'}, {}), "('^127\\\\.127\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}$', next_host)", False, 'import re\n'), ((24, 4, 25, 53), 'sys.stderr.write', 'sys.stderr.write', ({(25, 8, 25, 52): '"""ntptrace: can\'t find Python NTP library.\n"""'}, {}), '("ntptrace: can\'t find Python NTP library.\\n")', False, 'import sys\n'), ((26, 4, 26, 32), 'sys.stderr.write', 'sys.stderr.write', ({(26, 21, 26, 31): "('%s\\n' % e)"}, {}), "('%s\\n' % e)", False, 'import sys\n'), ((27, 4, 27, 15), 'sys.exit', 'sys.exit', ({(27, 13, 27, 14): '(1)'}, {}), '(1)', False, 'import sys\n'), ((68, 15, 71, 37), 'subprocess.Popen', 'subprocess.Popen', (), '', False, 'import subprocess\n'), ((82, 11, 82, 49), 're.search', 're.search', ({(82, 21, 82, 42): '"""Connection refused"""', (82, 44, 82, 48): 'line'}, {}), "('Connection refused', line)", False, 'import re\n'), ((85, 16, 86, 46), 're.search', 're.search', (), '', False, 'import re\n'), ((92, 19, 92, 59), 're.finditer', 're.finditer', ({(92, 31, 92, 52): '"""(\\\\w+)=([^,]+),?\\\\s?"""', (92, 54, 92, 58): 'line'}, {}), "('(\\\\w+)=([^,]+),?\\\\s?', line)", False, 'import re\n'), ((96, 18, 96, 52), 're.sub', 're.sub', ({(96, 25, 96, 39): '"""^"([^"]+)"$"""', (96, 41, 96, 46): '"""\\\\1"""', (96, 48, 96, 51): 'val'}, {}), '(\'^"([^"]+)"$\', \'\\\\1\', val)', False, 'import re\n')]