# # Copyright (C) 2012-2013 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the # GNU General Public License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # """ Module facilitating the work with NTP servers and NTP daemon's configuration """ import re import os import tempfile import shutil from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.async_utils import async_action_nowait from pyanaconda.core.i18n import N_, _ from pyanaconda.core.constants import NTP_SERVER_TIMEOUT, NTP_SERVER_QUERY, \ THREAD_NTP_SERVER_CHECK, NTP_SERVER_OK, NTP_SERVER_NOK from pyanaconda.core.signal import Signal from pyanaconda.core.util import execWithRedirect from pyanaconda.modules.common.structures.timezone import TimeSourceData from pyanaconda.core.threads import thread_manager NTP_CONFIG_FILE = "/etc/chrony.conf" #example line: #server 0.fedora.pool.ntp.org iburst SRV_LINE_REGEXP = re.compile(r"^\s*(server|pool)\s*([-a-zA-Z.0-9]+)\s?([a-zA-Z0-9\s]*)$") SRV_NOARG_OPTIONS = ["burst", "iburst", "nts", "prefer", "require", "trust", "noselect", "xleave"] SRV_ARG_OPTIONS = ["key", "minpoll", "maxpoll"] # Description of an NTP server status. NTP_SERVER_STATUS_DESCRIPTIONS = { NTP_SERVER_OK: N_("status: working"), NTP_SERVER_NOK: N_("status: not working"), NTP_SERVER_QUERY: N_("checking status") } log = get_module_logger(__name__) class NTPconfigError(Exception): """Exception class for NTP related problems""" pass def get_ntp_server_summary(server, states): """Generate a summary of an NTP server and its status. :param server: an NTP server :type server: an instance of TimeSourceData :param states: a cache of NTP server states :type states: an instance of NTPServerStatusCache :return: a string with a summary """ return "{} ({})".format( server.hostname, states.get_status_description(server) ) def get_ntp_servers_summary(servers, states): """Generate a summary of NTP servers and their states. :param servers: a list of NTP servers :type servers: a list of TimeSourceData :param states: a cache of NTP server states :type states: an instance of NTPServerStatusCache :return: a string with a summary """ summary = _("NTP servers:") for server in servers: summary += "\n" + get_ntp_server_summary(server, states) if not servers: summary += " " + _("not configured") return summary def ntp_server_working(server_hostname, nts_enabled): """Tries to do an NTP request to the server (timeout may take some time). If NTS is enabled, try making a TCP connection to the NTS-KE port instead. :param server_hostname: a host name or an IP address of an NTP server :type server_hostname: string :return: True if the given server is reachable and working, False otherwise :rtype: bool """ directive = ["server", server_hostname, "iburst", "maxsamples", "1"] if nts_enabled: directive.append("nts") arguments = ["-Q", " ".join(directive), "-t", str(NTP_SERVER_TIMEOUT)] return execWithRedirect("chronyd", arguments) == 0 def get_servers_from_config(conf_file_path=NTP_CONFIG_FILE): """Get NTP servers from a configuration file. Goes through the chronyd's configuration file looking for lines starting with 'server'. :param conf_file_path: a path to the chronyd's configuration file :return: servers found in the chronyd's configuration :rtype: a list of TimeSourceData instances """ servers = [] try: with open(conf_file_path, "r") as conf_file: for line in conf_file: match = SRV_LINE_REGEXP.match(line) if not match: continue server = TimeSourceData() server.type = match.group(1).upper() server.hostname = match.group(2) server.options = [] words = match.group(3).lower().split() skip_argument = False for i in range(len(words)): if skip_argument: skip_argument = False continue if words[i] in SRV_NOARG_OPTIONS: server.options.append(words[i]) elif words[i] in SRV_ARG_OPTIONS and i + 1 < len(words): server.options.append(' '.join(words[i:i+2])) skip_argument = True else: log.debug("Unknown NTP server option %s", words[i]) servers.append(server) except OSError as e: msg = "Cannot open config file {} for reading ({})." raise NTPconfigError(msg.format(conf_file_path, e.strerror)) from e return servers def save_servers_to_config(servers, conf_file_path=NTP_CONFIG_FILE, out_file_path=None): """Save NTP servers to a configuration file. Replaces the pools and servers defined in the chronyd's configuration file with the given ones. If the out_file is not None, then it is used for the resulting config. :param servers: a list of NTP servers and pools :type servers: a list of TimeSourceData instances :param conf_file_path: a path to the chronyd's configuration file :param out_file_path: a path to the file used for the resulting config """ temp_path = None try: old_conf_file = open(conf_file_path, "r") except OSError as e: msg = "Cannot open config file {} for reading ({})." raise NTPconfigError(msg.format(conf_file_path, e.strerror)) from e if out_file_path: try: new_conf_file = open(out_file_path, "w") except OSError as e: msg = "Cannot open new config file {} for writing ({})." raise NTPconfigError(msg.format(out_file_path, e.strerror)) from e else: try: (fields, temp_path) = tempfile.mkstemp() new_conf_file = os.fdopen(fields, "w") except OSError as e: msg = "Cannot open temporary file {} for writing ({})." raise NTPconfigError(msg.format(temp_path, e.strerror)) from e heading = "# These servers were defined in the installation:\n" # write info about the origin of the following lines new_conf_file.write(heading) # write new servers and pools for server in servers: args = [server.type.lower(), server.hostname] + server.options line = " ".join(args) + "\n" new_conf_file.write(line) new_conf_file.write("\n") # copy non-server lines from the old config and skip our heading for line in old_conf_file: if not SRV_LINE_REGEXP.match(line) and line != heading: new_conf_file.write(line) old_conf_file.close() new_conf_file.close() if not out_file_path: try: # Use copy rather then move to get the correct selinux context shutil.copyfile(temp_path, conf_file_path) os.unlink(temp_path) except OSError as oserr: msg = "Cannot replace the old config with the new one ({})." raise NTPconfigError(msg.format(oserr.strerror)) from oserr class NTPServerStatusCache(object): """The cache of NTP server states.""" def __init__(self): self._cache = {} self._changed = Signal() @property def changed(self): """The status changed signal.""" return self._changed def get_status(self, server): """Get the status of the given NTP server. :param TimeSourceData server: an NTP server :return int: a status of the NTP server """ return self._cache.get( server.hostname, NTP_SERVER_QUERY ) def get_status_description(self, server): """Get the status description of the given NTP server. :param TimeSourceData server: an NTP server :return str: a status description of the NTP server """ status = self.get_status(server) return _(NTP_SERVER_STATUS_DESCRIPTIONS[status]) def check_status(self, server): """Asynchronously check if given NTP servers appear to be working. :param TimeSourceData server: an NTP server """ # Get a hostname and NTS option. hostname = server.hostname nts_enabled = "nts" in server.options # Reset the current status. self._set_status(hostname, NTP_SERVER_QUERY) # Start the check. thread_manager.add_thread( prefix=THREAD_NTP_SERVER_CHECK, target=self._check_status, args=(hostname, nts_enabled) ) def _set_status(self, hostname, status): """Set the status of the given NTP server. :param str hostname: a hostname of an NTP server :return int: a status of the NTP server """ self._cache[hostname] = status @async_action_nowait def _report_status_changed(self): """Emit the status changed signal. Run callbacks in the context of the main loop, so they will not affect the running thread. """ self._changed.emit() def _check_status(self, hostname, nts_enabled): """Check if an NTP server appears to be working. :param str hostname: a hostname of an NTP server """ log.debug("Checking NTP server %s", hostname) result = ntp_server_working(hostname, nts_enabled) if result: log.debug("NTP server %s appears to be working.", hostname) self._set_status(hostname, NTP_SERVER_OK) else: log.debug("NTP server %s appears not to be working.", hostname) self._set_status(hostname, NTP_SERVER_NOK) self._report_status_changed()