Blame SOURCES/ntp2chrony.py

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