Blame ntp2chrony.py

Packit Service 4a6500
#!/usr/bin/python
Packit Service 4a6500
#
Packit Service 4a6500
# Convert ntp configuration to chrony
Packit Service 4a6500
#
Packit Service 4a6500
# Copyright (C) 2018-2019  Miroslav Lichvar <mlichvar@redhat.com>
Packit Service 4a6500
#
Packit Service 4a6500
# Permission is hereby granted, free of charge, to any person obtaining
Packit Service 4a6500
# a copy of this software and associated documentation files (the
Packit Service 4a6500
# "Software"), to deal in the Software without restriction, including
Packit Service 4a6500
# without limitation the rights to use, copy, modify, merge, publish,
Packit Service 4a6500
# distribute, sublicense, and/or sell copies of the Software, and to
Packit Service 4a6500
# permit persons to whom the Software is furnished to do so, subject to
Packit Service 4a6500
# the following conditions:
Packit Service 4a6500
#
Packit Service 4a6500
# The above copyright notice and this permission notice shall be included
Packit Service 4a6500
# in all copies or substantial portions of the Software.
Packit Service 4a6500
#
Packit Service 4a6500
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
Packit Service 4a6500
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
Packit Service 4a6500
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
Packit Service 4a6500
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
Packit Service 4a6500
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
Packit Service 4a6500
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
Packit Service 4a6500
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Packit Service 4a6500
Packit Service 4a6500
Packit Service 4a6500
import argparse
Packit Service 4a6500
import ipaddress
Packit Service 4a6500
import logging
Packit Service 4a6500
import os
Packit Service 4a6500
import os.path
Packit Service 4a6500
import re
Packit Service 4a6500
import subprocess
Packit Service 4a6500
import sys
Packit Service 4a6500
Packit Service 4a6500
# python2 compatibility hacks
Packit Service 4a6500
if sys.version_info[0] < 3:
Packit Service 4a6500
    from io import open
Packit Service 4a6500
    reload(sys)
Packit Service 4a6500
    sys.setdefaultencoding("utf-8")
Packit Service 4a6500
Packit Service 4a6500
class NtpConfiguration(object):
Packit Service 4a6500
    def __init__(self, root_dir, ntp_conf, step_tickers):
Packit Service 4a6500
        self.root_dir = root_dir if root_dir != "/" else ""
Packit Service 4a6500
        self.ntp_conf_path = ntp_conf
Packit Service 4a6500
        self.step_tickers_path = step_tickers
Packit Service 4a6500
Packit Service 4a6500
        # Read and write files using an 8-bit transparent encoding
Packit Service 4a6500
        self.file_encoding = "latin-1"
Packit Service 4a6500
        self.enabled_services = set()
Packit Service 4a6500
        self.step_tickers = []
Packit Service 4a6500
        self.time_sources = []
Packit Service 4a6500
        self.fudges = {}
Packit Service 4a6500
        self.restrictions = {
Packit Service 4a6500
                # Built-in defaults
Packit Service 4a6500
                ipaddress.ip_network(u"0.0.0.0/0"): set(),
Packit Service 4a6500
                ipaddress.ip_network(u"::/0"): set(),
Packit Service 4a6500
        }
Packit Service 4a6500
        self.keyfile = ""
Packit Service 4a6500
        self.keys = []
Packit Service 4a6500
        self.trusted_keys = []
Packit Service 4a6500
        self.driftfile = ""
Packit Service 4a6500
        self.statistics = []
Packit Service 4a6500
        self.leapfile = ""
Packit Service 4a6500
        self.tos_options = {}
Packit Service 4a6500
        self.ignored_directives = set()
Packit Service 4a6500
        self.ignored_lines = []
Packit Service 4a6500
Packit Service 4a6500
        #self.detect_enabled_services()
Packit Service 4a6500
        self.parse_step_tickers()
Packit Service 4a6500
        self.parse_ntp_conf()
Packit Service 4a6500
Packit Service 4a6500
    def detect_enabled_services(self):
Packit Service 4a6500
        for service in ["ntpdate", "ntpd", "ntp-wait"]:
Packit Service 4a6500
            if os.path.islink("{}/etc/systemd/system/multi-user.target.wants/{}.service"
Packit Service 4a6500
                    .format(self.root_dir, service)):
Packit Service 4a6500
                self.enabled_services.add(service)
Packit Service 4a6500
        logging.info("Enabled services found in /etc/systemd/system: %s",
Packit Service 4a6500
                     " ".join(self.enabled_services))
Packit Service 4a6500
Packit Service 4a6500
    def parse_step_tickers(self):
Packit Service 4a6500
        if not self.step_tickers_path:
Packit Service 4a6500
            return
Packit Service 4a6500
Packit Service 4a6500
        path = os.path.join(self.root_dir, self.step_tickers_path)
Packit Service 4a6500
        if not os.path.isfile(path):
Packit Service 4a6500
            logging.info("Missing %s", path)
Packit Service 4a6500
            return
Packit Service 4a6500
Packit Service 4a6500
        with open(path, encoding=self.file_encoding) as f:
Packit Service 4a6500
            for line in f:
Packit Service 4a6500
                line = line[:line.find('#')]
Packit Service 4a6500
Packit Service 4a6500
                words = line.split()
Packit Service 4a6500
Packit Service 4a6500
                if not words:
Packit Service 4a6500
                    continue
Packit Service 4a6500
Packit Service 4a6500
                self.step_tickers.extend(words)
Packit Service 4a6500
Packit Service 4a6500
    def parse_ntp_conf(self, path=None):
Packit Service 4a6500
        if path is None:
Packit Service 4a6500
            path = os.path.join(self.root_dir, self.ntp_conf_path)
Packit Service 4a6500
Packit Service 4a6500
        with open(path, encoding=self.file_encoding) as f:
Packit Service 4a6500
            logging.info("Reading %s", path)
Packit Service 4a6500
Packit Service 4a6500
            for line in f:
Packit Service 4a6500
                line = line[:line.find('#')]
Packit Service 4a6500
Packit Service 4a6500
                words = line.split()
Packit Service 4a6500
Packit Service 4a6500
                if not words:
Packit Service 4a6500
                    continue
Packit Service 4a6500
Packit Service 4a6500
                if not self.parse_directive(words):
Packit Service 4a6500
                    self.ignored_lines.append(line)
Packit Service 4a6500
Packit Service 4a6500
    def parse_directive(self, words):
Packit Service 4a6500
        name = words.pop(0)
Packit Service 4a6500
        if name.startswith("logconfig"):
Packit Service 4a6500
            name = "logconfig"
Packit Service 4a6500
Packit Service 4a6500
        if words:
Packit Service 4a6500
            if name in ["server", "peer", "pool"]:
Packit Service 4a6500
                return self.parse_source(name, words)
Packit Service 4a6500
            elif name == "fudge":
Packit Service 4a6500
                return self.parse_fudge(words)
Packit Service 4a6500
            elif name == "restrict":
Packit Service 4a6500
                return self.parse_restrict(words)
Packit Service 4a6500
            elif name == "tos":
Packit Service 4a6500
                return self.parse_tos(words)
Packit Service 4a6500
            elif name == "includefile":
Packit Service 4a6500
                return self.parse_includefile(words)
Packit Service 4a6500
            elif name == "keys":
Packit Service 4a6500
                return self.parse_keys(words)
Packit Service 4a6500
            elif name == "trustedkey":
Packit Service 4a6500
                return self.parse_trustedkey(words)
Packit Service 4a6500
            elif name == "driftfile":
Packit Service 4a6500
                self.driftfile = words[0]
Packit Service 4a6500
            elif name == "statistics":
Packit Service 4a6500
                self.statistics = words
Packit Service 4a6500
            elif name == "leapfile":
Packit Service 4a6500
                self.leapfile = words[0]
Packit Service 4a6500
            else:
Packit Service 4a6500
                self.ignored_directives.add(name)
Packit Service 4a6500
                return False
Packit Service 4a6500
        else:
Packit Service 4a6500
            self.ignored_directives.add(name)
Packit Service 4a6500
            return False
Packit Service 4a6500
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_source(self, source_type, words):
Packit Service 4a6500
        ipv4_only = False
Packit Service 4a6500
        ipv6_only = False
Packit Service 4a6500
        source = {
Packit Service 4a6500
                "type": source_type,
Packit Service 4a6500
                "options": []
Packit Service 4a6500
        }
Packit Service 4a6500
Packit Service 4a6500
        if words[0] == "-4":
Packit Service 4a6500
            ipv4_only = True
Packit Service 4a6500
            words.pop(0)
Packit Service 4a6500
        elif words[0] == "-6":
Packit Service 4a6500
            ipv6_only = True
Packit Service 4a6500
            words.pop(0)
Packit Service 4a6500
Packit Service 4a6500
        if not words:
Packit Service 4a6500
            return False
Packit Service 4a6500
Packit Service 4a6500
        source["address"] = words.pop(0)
Packit Service 4a6500
Packit Service 4a6500
        # Check if -4/-6 corresponds to the address and ignore hostnames
Packit Service 4a6500
        if ipv4_only or ipv6_only:
Packit Service 4a6500
            try:
Packit Service 4a6500
                version = ipaddress.ip_address(source["address"]).version
Packit Service 4a6500
                if (ipv4_only and version != 4) or (ipv6_only and version != 6):
Packit Service 4a6500
                    return False
Packit Service 4a6500
            except ValueError:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        if source["address"].startswith("127.127."):
Packit Service 4a6500
            if not source["address"].startswith("127.127.1."):
Packit Service 4a6500
                # Ignore non-LOCAL refclocks
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        while words:
Packit Service 4a6500
            if len(words) >= 2 and words[0] in ["minpoll", "maxpoll", "version", "key"]:
Packit Service 4a6500
                source["options"].append((words[0], words[1]))
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            elif words[0] in ["burst", "iburst", "noselect", "prefer", "true", "xleave"]:
Packit Service 4a6500
                source["options"].append((words[0],))
Packit Service 4a6500
                words.pop(0)
Packit Service 4a6500
            else:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        self.time_sources.append(source)
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_fudge(self, words):
Packit Service 4a6500
        address = words.pop(0)
Packit Service 4a6500
        options = {}
Packit Service 4a6500
Packit Service 4a6500
        while words:
Packit Service 4a6500
            if len(words) >= 2 and words[0] in ["stratum"]:
Packit Service 4a6500
                if not words[1].isdigit():
Packit Service 4a6500
                    return False
Packit Service 4a6500
                options[words[0]] = int(words[1])
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            elif len(words) >= 2:
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            else:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        self.fudges[address] = options
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_restrict(self, words):
Packit Service 4a6500
        ipv4_only = False
Packit Service 4a6500
        ipv6_only = False
Packit Service 4a6500
        flags = set()
Packit Service 4a6500
        mask = ""
Packit Service 4a6500
Packit Service 4a6500
        if words[0] == "-4":
Packit Service 4a6500
            ipv4_only = True
Packit Service 4a6500
            words.pop(0)
Packit Service 4a6500
        elif words[0] == "-6":
Packit Service 4a6500
            ipv6_only = True
Packit Service 4a6500
            words.pop(0)
Packit Service 4a6500
Packit Service 4a6500
        if not words:
Packit Service 4a6500
            return False
Packit Service 4a6500
Packit Service 4a6500
        address = words.pop(0)
Packit Service 4a6500
Packit Service 4a6500
        while words:
Packit Service 4a6500
            if len(words) >= 2 and words[0] == "mask":
Packit Service 4a6500
                mask = words[1]
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            else:
Packit Service 4a6500
                if words[0] not in ["kod", "nomodify", "notrap", "nopeer", "noquery",
Packit Service 4a6500
                                    "limited", "ignore", "noserve"]:
Packit Service 4a6500
                    return False
Packit Service 4a6500
                flags.add(words[0])
Packit Service 4a6500
                words.pop(0)
Packit Service 4a6500
Packit Service 4a6500
        # Convert to IP network(s), ignoring restrictions with hostnames
Packit Service 4a6500
        networks = []
Packit Service 4a6500
        if address == "default" and not mask:
Packit Service 4a6500
            if not ipv6_only:
Packit Service 4a6500
                networks.append(ipaddress.ip_network(u"0.0.0.0/0"))
Packit Service 4a6500
            if not ipv4_only:
Packit Service 4a6500
                networks.append(ipaddress.ip_network(u"::/0"))
Packit Service 4a6500
        else:
Packit Service 4a6500
            try:
Packit Service 4a6500
                if mask:
Packit Service 4a6500
                    networks.append(ipaddress.ip_network(u"{}/{}".format(address, mask)))
Packit Service 4a6500
                else:
Packit Service 4a6500
                    networks.append(ipaddress.ip_network(address))
Packit Service 4a6500
            except ValueError:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
            if (ipv4_only and networks[-1].version != 4) or \
Packit Service 4a6500
                    (ipv6_only and networks[-1].version != 6):
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        for network in networks:
Packit Service 4a6500
            self.restrictions[network] = flags
Packit Service 4a6500
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_tos(self, words):
Packit Service 4a6500
        options = {}
Packit Service 4a6500
        while words:
Packit Service 4a6500
            if len(words) >= 2 and words[0] in ["minsane", "orphan"]:
Packit Service 4a6500
                if not words[1].isdigit():
Packit Service 4a6500
                    return False
Packit Service 4a6500
                options[words[0]] = int(words[1])
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            elif len(words) >= 2 and words[0] in ["maxdist"]:
Packit Service 4a6500
                # Check if it is a float value
Packit Service 4a6500
                if not words[1].replace('.', '', 1).isdigit():
Packit Service 4a6500
                    return False
Packit Service 4a6500
                options[words[0]] = float(words[1])
Packit Service 4a6500
                words = words[2:]
Packit Service 4a6500
            else:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        self.tos_options.update(options)
Packit Service 4a6500
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_includefile(self, words):
Packit Service 4a6500
        path = os.path.join(self.root_dir, words[0])
Packit Service 4a6500
        if not os.path.isfile(path):
Packit Service 4a6500
            return False
Packit Service 4a6500
Packit Service 4a6500
        self.parse_ntp_conf(path)
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_keys(self, words):
Packit Service 4a6500
        keyfile = words[0]
Packit Service 4a6500
        path = os.path.join(self.root_dir, keyfile)
Packit Service 4a6500
        if not os.path.isfile(path):
Packit Service 4a6500
            logging.info("Missing %s", path)
Packit Service 4a6500
            return False
Packit Service 4a6500
Packit Service 4a6500
        with open(path, encoding=self.file_encoding) as f:
Packit Service 4a6500
            logging.info("Reading %s", path)
Packit Service 4a6500
            keys = []
Packit Service 4a6500
            for line in f:
Packit Service 4a6500
                words = line.split()
Packit Service 4a6500
                if len(words) < 3 or not words[0].isdigit():
Packit Service 4a6500
                    continue
Packit Service 4a6500
                keys.append((int(words[0]), words[1], words[2]))
Packit Service 4a6500
Packit Service 4a6500
            self.keyfile = keyfile
Packit Service 4a6500
            self.keys = keys
Packit Service 4a6500
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def parse_trustedkey(self, words):
Packit Service 4a6500
        key_ranges = []
Packit Service 4a6500
        for word in words:
Packit Service 4a6500
            if word.isdigit():
Packit Service 4a6500
                key_ranges.append((int(word), int(word)))
Packit Service 4a6500
            elif re.match("^[0-9]+-[0-9]+$", word):
Packit Service 4a6500
                first, last = word.split("-")
Packit Service 4a6500
                key_ranges.append((int(first), int(last)))
Packit Service 4a6500
            else:
Packit Service 4a6500
                return False
Packit Service 4a6500
Packit Service 4a6500
        self.trusted_keys = key_ranges
Packit Service 4a6500
        return True
Packit Service 4a6500
Packit Service 4a6500
    def write_chrony_configuration(self, chrony_conf_path, chrony_keys_path,
Packit Service 4a6500
                                   dry_run=False, backup=False):
Packit Service 4a6500
        chrony_conf = self.get_chrony_conf(chrony_keys_path)
Packit Service 4a6500
        logging.debug("Generated %s:\n%s", chrony_conf_path, chrony_conf)
Packit Service 4a6500
Packit Service 4a6500
        if not dry_run:
Packit Service 4a6500
            self.write_file(chrony_conf_path, 0o644, chrony_conf, backup)
Packit Service 4a6500
Packit Service 4a6500
        chrony_keys = self.get_chrony_keys()
Packit Service 4a6500
        if chrony_keys:
Packit Service 4a6500
            logging.debug("Generated %s:\n%s", chrony_keys_path, chrony_keys)
Packit Service 4a6500
Packit Service 4a6500
        if not dry_run:
Packit Service 4a6500
            self.write_file(chrony_keys_path, 0o640, chrony_keys, backup)
Packit Service 4a6500
Packit Service 4a6500
    def get_processed_time_sources(self):
Packit Service 4a6500
        # Convert {0,1,2,3}.*pool.ntp.org servers to 2.*pool.ntp.org pools
Packit Service 4a6500
Packit Service 4a6500
        # Make shallow copies of all sources (only type will be modified)
Packit Service 4a6500
        time_sources = [s.copy() for s in self.time_sources]
Packit Service 4a6500
Packit Service 4a6500
        pools = {}
Packit Service 4a6500
        for source in time_sources:
Packit Service 4a6500
            if source["type"] != "server":
Packit Service 4a6500
                continue
Packit Service 4a6500
            m = re.match("^([0123])(\\.\\w+)?\\.pool\\.ntp\\.org$", source["address"])
Packit Service 4a6500
            if m is None:
Packit Service 4a6500
                continue
Packit Service 4a6500
            number = m.group(1)
Packit Service 4a6500
            zone = m.group(2)
Packit Service 4a6500
            if zone not in pools:
Packit Service 4a6500
                pools[zone] = []
Packit Service 4a6500
            pools[zone].append((int(number), source))
Packit Service 4a6500
Packit Service 4a6500
        remove_servers = set()
Packit Service 4a6500
        for zone, pool in pools.items():
Packit Service 4a6500
            # sort and skip all pools not in [0, 3] range
Packit Service 4a6500
            pool.sort()
Packit Service 4a6500
            if [number for number, source in pool] != [0, 1, 2, 3]:
Packit Service 4a6500
                # only exact group of 4 servers can be converted, nothing to do here
Packit Service 4a6500
                continue
Packit Service 4a6500
            # verify that parameters are the same for all servers in the pool
Packit Service 4a6500
            if not all([p[1]["options"] == pool[0][1]["options"] for p in pool]):
Packit Service 4a6500
                break
Packit Service 4a6500
            remove_servers.update([pool[i][1]["address"] for i in [0, 1, 3]])
Packit Service 4a6500
            pool[2][1]["type"] = "pool"
Packit Service 4a6500
Packit Service 4a6500
        processed_sources = []
Packit Service 4a6500
        for source in time_sources:
Packit Service 4a6500
            if source["type"] == "server" and source["address"] in remove_servers:
Packit Service 4a6500
                continue
Packit Service 4a6500
            processed_sources.append(source)
Packit Service 4a6500
        return processed_sources
Packit Service 4a6500
Packit Service 4a6500
    def get_chrony_conf_sources(self):
Packit Service 4a6500
        conf = ""
Packit Service 4a6500
Packit Service 4a6500
        if self.step_tickers:
Packit Service 4a6500
            conf += "# Specify NTP servers used for initial correction.\n"
Packit Service 4a6500
            conf += "initstepslew 0.1 {}\n".format(" ".join(self.step_tickers))
Packit Service 4a6500
            conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Specify time sources.\n"
Packit Service 4a6500
Packit Service 4a6500
        for source in self.get_processed_time_sources():
Packit Service 4a6500
            address = source["address"]
Packit Service 4a6500
            if address.startswith("127.127."):
Packit Service 4a6500
                if address.startswith("127.127.1."):
Packit Service 4a6500
                    continue
Packit Service 4a6500
                # No other refclocks are expected from the parser
Packit Service 4a6500
                assert False
Packit Service 4a6500
            else:
Packit Service 4a6500
                conf += "{} {}".format(source["type"], address)
Packit Service 4a6500
                for option in source["options"]:
Packit Service 4a6500
                    if option[0] in ["minpoll", "maxpoll", "version", "key",
Packit Service 4a6500
                                     "iburst", "noselect", "prefer", "xleave"]:
Packit Service 4a6500
                        conf += " {}".format(" ".join(option))
Packit Service 4a6500
                    elif option[0] == "burst":
Packit Service 4a6500
                        conf += " presend 6"
Packit Service 4a6500
                    elif option[0] == "true":
Packit Service 4a6500
                        conf += " trust"
Packit Service 4a6500
                    else:
Packit Service 4a6500
                        # No other options are expected from the parser
Packit Service 4a6500
                        assert False
Packit Service 4a6500
                conf += "\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        return conf
Packit Service 4a6500
Packit Service 4a6500
    def get_chrony_conf_allows(self):
Packit Service 4a6500
        allowed_networks = filter(lambda n: "ignore" not in self.restrictions[n] and
Packit Service 4a6500
                                    "noserve" not in self.restrictions[n],
Packit Service 4a6500
                                  self.restrictions.keys())
Packit Service 4a6500
Packit Service 4a6500
        conf = ""
Packit Service 4a6500
        for network in sorted(allowed_networks, key=lambda n: (n.version, n)):
Packit Service 4a6500
            if network.num_addresses > 1:
Packit Service 4a6500
                conf += "allow {}\n".format(network)
Packit Service 4a6500
            else:
Packit Service 4a6500
                conf += "allow {}\n".format(network.network_address)
Packit Service 4a6500
Packit Service 4a6500
        if conf:
Packit Service 4a6500
            conf = "# Allow NTP client access.\n" + conf
Packit Service 4a6500
            conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        return conf
Packit Service 4a6500
Packit Service 4a6500
    def get_chrony_conf_cmdallows(self):
Packit Service 4a6500
        allowed_networks = filter(lambda n: "ignore" not in self.restrictions[n] and
Packit Service 4a6500
                                    "noquery" not in self.restrictions[n] and
Packit Service 4a6500
                                    n != ipaddress.ip_network(u"127.0.0.1/32") and
Packit Service 4a6500
                                    n != ipaddress.ip_network(u"::1/128"),
Packit Service 4a6500
                                  self.restrictions.keys())
Packit Service 4a6500
Packit Service 4a6500
        ip_versions = set()
Packit Service 4a6500
        conf = ""
Packit Service 4a6500
        for network in sorted(allowed_networks, key=lambda n: (n.version, n)):
Packit Service 4a6500
            ip_versions.add(network.version)
Packit Service 4a6500
            if network.num_addresses > 1:
Packit Service 4a6500
                conf += "cmdallow {}\n".format(network)
Packit Service 4a6500
            else:
Packit Service 4a6500
                conf += "cmdallow {}\n".format(network.network_address)
Packit Service 4a6500
Packit Service 4a6500
        if conf:
Packit Service 4a6500
            conf = "# Allow remote monitoring.\n" + conf
Packit Service 4a6500
            if 4 in ip_versions:
Packit Service 4a6500
                conf += "bindcmdaddress 0.0.0.0\n"
Packit Service 4a6500
            if 6 in ip_versions:
Packit Service 4a6500
                conf += "bindcmdaddress ::\n"
Packit Service 4a6500
            conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        return conf
Packit Service 4a6500
Packit Service 4a6500
    def get_chrony_conf(self, chrony_keys_path):
Packit Service 4a6500
        local_stratum = 0
Packit Service 4a6500
        maxdistance = 0.0
Packit Service 4a6500
        minsources = 1
Packit Service 4a6500
        orphan_stratum = 0
Packit Service 4a6500
        logs = []
Packit Service 4a6500
Packit Service 4a6500
        for source in self.time_sources:
Packit Service 4a6500
            address = source["address"]
Packit Service 4a6500
            if address.startswith("127.127.1."):
Packit Service 4a6500
                if address in self.fudges and "stratum" in self.fudges[address]:
Packit Service 4a6500
                    local_stratum = self.fudges[address]["stratum"]
Packit Service 4a6500
                else:
Packit Service 4a6500
                    local_stratum = 5
Packit Service 4a6500
Packit Service 4a6500
        if "maxdist" in self.tos_options:
Packit Service 4a6500
            maxdistance = self.tos_options["maxdist"]
Packit Service 4a6500
        if "minsane" in self.tos_options:
Packit Service 4a6500
            minsources = self.tos_options["minsane"]
Packit Service 4a6500
        if "orphan" in self.tos_options:
Packit Service 4a6500
            orphan_stratum = self.tos_options["orphan"]
Packit Service 4a6500
Packit Service 4a6500
        if "clockstats" in self.statistics:
Packit Service 4a6500
            logs.append("refclocks");
Packit Service 4a6500
        if "loopstats" in self.statistics:
Packit Service 4a6500
            logs.append("tracking")
Packit Service 4a6500
        if "peerstats" in self.statistics:
Packit Service 4a6500
            logs.append("statistics");
Packit Service 4a6500
        if "rawstats" in self.statistics:
Packit Service 4a6500
            logs.append("measurements")
Packit Service 4a6500
Packit Service 4a6500
        conf = "# This file was converted from {}{}.\n".format(
Packit Service 4a6500
                    self.ntp_conf_path,
Packit Service 4a6500
                    " and " + self.step_tickers_path if self.step_tickers_path else "")
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        if self.ignored_lines:
Packit Service 4a6500
            conf += "# The following directives were ignored in the conversion:\n"
Packit Service 4a6500
Packit Service 4a6500
            for line in self.ignored_lines:
Packit Service 4a6500
                # Remove sensitive information
Packit Service 4a6500
                line = re.sub(r"\s+pw\s+\S+", " pw XXX", line.rstrip())
Packit Service 4a6500
                conf += "# " + line + "\n"
Packit Service 4a6500
            conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += self.get_chrony_conf_sources()
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Record the rate at which the system clock gains/losses time.\n"
Packit Service 4a6500
        if not self.driftfile:
Packit Service 4a6500
            conf += "#"
Packit Service 4a6500
        conf += "driftfile /var/lib/chrony/drift\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Allow the system clock to be stepped in the first three updates\n"
Packit Service 4a6500
        conf += "# if its offset is larger than 1 second.\n"
Packit Service 4a6500
        conf += "makestep 1.0 3\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Enable kernel synchronization of the real-time clock (RTC).\n"
Packit Service 4a6500
        conf += "rtcsync\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Enable hardware timestamping on all interfaces that support it.\n"
Packit Service 4a6500
        conf += "#hwtimestamp *\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        if maxdistance > 0.0:
Packit Service 4a6500
            conf += "# Specify the maximum distance of sources to be selectable.\n"
Packit Service 4a6500
            conf += "maxdistance {}\n".format(maxdistance)
Packit Service 4a6500
            conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Increase the minimum number of selectable sources required to adjust\n"
Packit Service 4a6500
        conf += "# the system clock.\n"
Packit Service 4a6500
        if minsources > 1:
Packit Service 4a6500
            conf += "minsources {}\n".format(minsources)
Packit Service 4a6500
        else:
Packit Service 4a6500
            conf += "#minsources 2\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += self.get_chrony_conf_allows()
Packit Service 4a6500
Packit Service 4a6500
        conf += self.get_chrony_conf_cmdallows()
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Serve time even if not synchronized to a time source.\n"
Packit Service 4a6500
        if orphan_stratum > 0 and orphan_stratum < 16:
Packit Service 4a6500
            conf += "local stratum {} orphan\n".format(orphan_stratum)
Packit Service 4a6500
        elif local_stratum > 0 and local_stratum < 16:
Packit Service 4a6500
            conf += "local stratum {}\n".format(local_stratum)
Packit Service 4a6500
        else:
Packit Service 4a6500
            conf += "#local stratum 10\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Specify file containing keys for NTP authentication.\n"
Packit Service 4a6500
        conf += "keyfile {}\n".format(chrony_keys_path)
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Get TAI-UTC offset and leap seconds from the system tz database.\n"
Packit Service 4a6500
        conf += "leapsectz right/UTC\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Specify directory for log files.\n"
Packit Service 4a6500
        conf += "logdir /var/log/chrony\n"
Packit Service 4a6500
        conf += "\n"
Packit Service 4a6500
Packit Service 4a6500
        conf += "# Select which information is logged.\n"
Packit Service 4a6500
        if logs:
Packit Service 4a6500
            conf += "log {}\n".format(" ".join(logs))
Packit Service 4a6500
        else:
Packit Service 4a6500
            conf += "#log measurements statistics tracking\n"
Packit Service 4a6500
Packit Service 4a6500
        return conf
Packit Service 4a6500
Packit Service 4a6500
    def get_chrony_keys(self):
Packit Service 4a6500
        if not self.keyfile:
Packit Service 4a6500
            return ""
Packit Service 4a6500
Packit Service 4a6500
        keys = "# This file was converted from {}.\n".format(self.keyfile)
Packit Service 4a6500
        keys += "\n"
Packit Service 4a6500
Packit Service 4a6500
        for key in self.keys:
Packit Service 4a6500
            key_id = key[0]
Packit Service 4a6500
            key_type = key[1]
Packit Service 4a6500
            password = key[2]
Packit Service 4a6500
Packit Service 4a6500
            if key_type in ["m", "M"]:
Packit Service 4a6500
                key_type = "MD5"
Packit Service 4a6500
            elif key_type not in ["MD5", "SHA1", "SHA256", "SHA384", "SHA512"]:
Packit Service 4a6500
                continue
Packit Service 4a6500
Packit Service 4a6500
            prefix = "ASCII" if len(password) <= 20 else "HEX"
Packit Service 4a6500
Packit Service 4a6500
            for first, last in self.trusted_keys:
Packit Service 4a6500
                if first <= key_id <= last:
Packit Service 4a6500
                    trusted = True
Packit Service 4a6500
                    break
Packit Service 4a6500
            else:
Packit Service 4a6500
                trusted = False
Packit Service 4a6500
Packit Service 4a6500
            # Disable keys that were not marked as trusted
Packit Service 4a6500
            if not trusted:
Packit Service 4a6500
                keys += "#"
Packit Service 4a6500
Packit Service 4a6500
            keys += "{} {} {}:{}\n".format(key_id, key_type, prefix, password)
Packit Service 4a6500
Packit Service 4a6500
        return keys
Packit Service 4a6500
Packit Service 4a6500
    def write_file(self, path, mode, content, backup):
Packit Service 4a6500
        path = self.root_dir + path
Packit Service 4a6500
        if backup and os.path.isfile(path):
Packit Service 4a6500
            os.rename(path, path + ".old")
Packit Service 4a6500
Packit Service 4a6500
        with open(os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL, mode), "w",
Packit Service 4a6500
                  encoding=self.file_encoding) as f:
Packit Service 4a6500
            logging.info("Writing %s", path)
Packit Service 4a6500
            f.write(u"" + content)
Packit Service 4a6500
Packit Service 4a6500
        # Fix SELinux context if restorecon is installed
Packit Service 4a6500
        try:
Packit Service 4a6500
            subprocess.call(["restorecon", path])
Packit Service 4a6500
        except OSError:
Packit Service 4a6500
            pass
Packit Service 4a6500
Packit Service 4a6500
Packit Service 4a6500
def main():
Packit Service 4a6500
    parser = argparse.ArgumentParser(description="Convert ntp configuration to chrony.")
Packit Service 4a6500
    parser.add_argument("-r", "--root", dest="roots", default=["/"], nargs="+",
Packit Service 4a6500
                        metavar="DIR", help="specify root directory (default /)")
Packit Service 4a6500
    parser.add_argument("--ntp-conf", action="store", default="/etc/ntp.conf",
Packit Service 4a6500
                        metavar="FILE", help="specify ntp config (default /etc/ntp.conf)")
Packit Service 4a6500
    parser.add_argument("--step-tickers", action="store", default="",
Packit Service 4a6500
                        metavar="FILE", help="specify ntpdate step-tickers config (no default)")
Packit Service 4a6500
    parser.add_argument("--chrony-conf", action="store", default="/etc/chrony.conf",
Packit Service 4a6500
                        metavar="FILE", help="specify chrony config (default /etc/chrony.conf)")
Packit Service 4a6500
    parser.add_argument("--chrony-keys", action="store", default="/etc/chrony.keys",
Packit Service 4a6500
                        metavar="FILE", help="specify chrony keyfile (default /etc/chrony.keys)")
Packit Service 4a6500
    parser.add_argument("-b", "--backup", action="store_true", help="backup existing configs before writing")
Packit Service 4a6500
    parser.add_argument("-L", "--ignored-lines", action="store_true", help="print ignored lines")
Packit Service 4a6500
    parser.add_argument("-D", "--ignored-directives", action="store_true",
Packit Service 4a6500
                        help="print names of ignored directives")
Packit Service 4a6500
    parser.add_argument("-n", "--dry-run", action="store_true", help="don't make any changes")
Packit Service 4a6500
    parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
Packit Service 4a6500
Packit Service 4a6500
    args = parser.parse_args()
Packit Service 4a6500
Packit Service 4a6500
    logging.basicConfig(format="%(message)s",
Packit Service 4a6500
                        level=[logging.ERROR, logging.INFO, logging.DEBUG][min(args.verbose, 2)])
Packit Service 4a6500
Packit Service 4a6500
    for root in args.roots:
Packit Service 4a6500
        conf = NtpConfiguration(root, args.ntp_conf, args.step_tickers)
Packit Service 4a6500
Packit Service 4a6500
        if args.ignored_lines:
Packit Service 4a6500
            for line in conf.ignored_lines:
Packit Service 4a6500
                print(line)
Packit Service 4a6500
Packit Service 4a6500
        if args.ignored_directives:
Packit Service 4a6500
            for directive in conf.ignored_directives:
Packit Service 4a6500
                print(directive)
Packit Service 4a6500
Packit Service 4a6500
        conf.write_chrony_configuration(args.chrony_conf, args.chrony_keys, args.dry_run, args.backup)
Packit Service 4a6500
Packit Service 4a6500
if __name__ == "__main__":
Packit Service 4a6500
    main()