From 54f882c593dd29e9577fe2af67bd267f0527c767 Mon Sep 17 00:00:00 2001 From: eaglegai Date: Fri, 4 Dec 2020 17:45:47 +0800 Subject: [PATCH] improve ntp servers to fix unkown error --- anaconda.spec | 20 +- ...-new-DBus-structure-for-time-sources.patch | 119 ++++ ...structure-for-time-sources-in-ntp-py.patch | 290 ++++++++ ...-time-sources-in-the-Timezone-module.patch | 406 ++++++++++++ ...ture-for-time-sources-in-anaconda-py.patch | 113 ++++ ...cture-for-time-sources-in-network-py.patch | 80 +++ ...port-for-the-NTP-server-status-cache.patch | 120 ++++ ...erating-a-summary-of-the-NTP-servers.patch | 60 ++ ...he-structure-for-time-sources-in-TUI.patch | 468 +++++++++++++ ...he-structure-for-time-sources-in-GUI.patch | 620 ++++++++++++++++++ ...for-the-timesource-kickstart-command.patch | 284 ++++++++ 11 files changed, 2578 insertions(+), 2 deletions(-) create mode 100644 ntp-servers-improve-001-Create-a-new-DBus-structure-for-time-sources.patch create mode 100644 ntp-servers-improve-002-Use-the-structure-for-time-sources-in-ntp-py.patch create mode 100644 ntp-servers-improve-003-Use-the-structure-for-time-sources-in-the-Timezone-module.patch create mode 100644 ntp-servers-improve-004-Use-the-structure-for-time-sources-in-anaconda-py.patch create mode 100644 ntp-servers-improve-005-Use-the-structure-for-time-sources-in-network-py.patch create mode 100644 ntp-servers-improve-006-Add-support-for-the-NTP-server-status-cache.patch create mode 100644 ntp-servers-improve-007-Add-support-for-generating-a-summary-of-the-NTP-servers.patch create mode 100644 ntp-servers-improve-008-Use-the-structure-for-time-sources-in-TUI.patch create mode 100644 ntp-servers-improve-009-Use-the-structure-for-time-sources-in-GUI.patch create mode 100644 ntp-servers-improve-010-Add-support-for-the-timesource-kickstart-command.patch diff --git a/anaconda.spec b/anaconda.spec index b0e5b43..944bf06 100644 --- a/anaconda.spec +++ b/anaconda.spec @@ -1,7 +1,7 @@ %define _empty_manifest_terminate_build 0 Name: anaconda Version: 33.19 -Release: 13 +Release: 14 Summary: Graphical system installer License: GPLv2+ and MIT URL: http://fedoraproject.org/wiki/Anaconda @@ -41,6 +41,16 @@ Patch9023: bugfix-add-dnf-transaction-timeout.patch Patch6007: fix-0-storage-devices-selected.patch Patch6008: fix-remove-unknow-partition-is-sda-failed.patch Patch6009: use-modinfo-to-check-ko-before-modprobe.patch +Patch6010: ntp-servers-improve-001-Create-a-new-DBus-structure-for-time-sources.patch +Patch6011: ntp-servers-improve-002-Use-the-structure-for-time-sources-in-ntp-py.patch +Patch6012: ntp-servers-improve-003-Use-the-structure-for-time-sources-in-the-Timezone-module.patch +Patch6013: ntp-servers-improve-004-Use-the-structure-for-time-sources-in-anaconda-py.patch +Patch6014: ntp-servers-improve-005-Use-the-structure-for-time-sources-in-network-py.patch +Patch6015: ntp-servers-improve-006-Add-support-for-the-NTP-server-status-cache.patch +Patch6016: ntp-servers-improve-007-Add-support-for-generating-a-summary-of-the-NTP-servers.patch +Patch6017: ntp-servers-improve-008-Use-the-structure-for-time-sources-in-TUI.patch +Patch6018: ntp-servers-improve-009-Use-the-structure-for-time-sources-in-GUI.patch +Patch6019: ntp-servers-improve-010-Add-support-for-the-timesource-kickstart-command.patch %define dbusver 1.2.3 %define dnfver 3.6.0 @@ -57,7 +67,7 @@ Patch6009: use-modinfo-to-check-ko-before-modprobe.patch %define libxklavierver 5.4 %define mehver 0.23-1 %define nmver 1.0 -%define pykickstartver 3.25-1 +%define pykickstartver 3.27-1 %define pypartedver 2.5-2 %define rpmver 4.10.0 %define simplelinever 1.1-1 @@ -254,6 +264,12 @@ update-desktop-database &> /dev/null || : %{_datadir}/gtk-doc %changelog +* Fri Dec 04 2020 gaihuiying - 33.19-14 +- Type:bugfix +- ID:NA +- SUG:NA +- DESC:improve ntp servers to fix unkown error + * Sat Nov 28 2020 lunankun - 33.19-13 - Type:bugfix - ID:NA diff --git a/ntp-servers-improve-001-Create-a-new-DBus-structure-for-time-sources.patch b/ntp-servers-improve-001-Create-a-new-DBus-structure-for-time-sources.patch new file mode 100644 index 0000000..49d5eb5 --- /dev/null +++ b/ntp-servers-improve-001-Create-a-new-DBus-structure-for-time-sources.patch @@ -0,0 +1,119 @@ +From 554be1cfd3d09035e0370f1efc46d3fc40f8496d Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 11:11:46 +0200 +Subject: [PATCH] Create a new DBus structure for time sources + +Use the class TimeSourceData to represent a time source. +--- + pyanaconda/core/constants.py | 4 + + .../modules/common/structures/timezone.py | 84 +++++++++++++++++++ + 2 files changed, 88 insertions(+) + create mode 100644 pyanaconda/modules/common/structures/timezone.py + +diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py +index 536529f4e0..5124f05b7f 100644 +--- a/pyanaconda/core/constants.py ++++ b/pyanaconda/core/constants.py +@@ -306,6 +306,10 @@ class SecretStatus(Enum): + # Window title text + WINDOW_TITLE_TEXT = N_("Anaconda Installer") + ++# Types of time sources. ++TIME_SOURCE_SERVER = "SERVER" ++TIME_SOURCE_POOL = "POOL" ++ + # NTP server checking + NTP_SERVER_OK = 0 + NTP_SERVER_NOK = 1 +diff --git a/pyanaconda/modules/common/structures/timezone.py b/pyanaconda/modules/common/structures/timezone.py +new file mode 100644 +index 0000000000..d18234f681 +--- /dev/null ++++ b/pyanaconda/modules/common/structures/timezone.py +@@ -0,0 +1,84 @@ ++# ++# DBus structures for the timezone data. ++# ++# Copyright (C) 2020 Red Hat, Inc. All rights reserved. ++# ++# This program is free software; you can redistribute it and/or modify ++# it under the terms of the GNU General Public License as published by ++# the Free Software Foundation; either version 2 of the License, or ++# (at your option) any later version. ++# ++# This program is distributed in the hope that it will be useful, ++# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see . ++# ++from dasbus.structure import DBusData ++from dasbus.typing import * # pylint: disable=wildcard-import ++ ++from pyanaconda.core.constants import TIME_SOURCE_SERVER ++ ++__all__ = ["TimeSourceData"] ++ ++ ++class TimeSourceData(DBusData): ++ """Data for a time source.""" ++ ++ def __init__(self): ++ self._type = TIME_SOURCE_SERVER ++ self._hostname = "" ++ self._options = [] ++ ++ @property ++ def type(self) -> Str: ++ """Type of the time source. ++ ++ Supported values: ++ ++ SERVER A single NTP server ++ POOL A pool of NTP servers ++ ++ :return: a type of the time source ++ """ ++ return self._type ++ ++ @type.setter ++ def type(self, value: Str): ++ self._type = value ++ ++ @property ++ def hostname(self) -> Str: ++ """Name of the time server. ++ ++ For example: ++ ++ ntp.cesnet.cz ++ ++ :return: a host name ++ """ ++ return self._hostname ++ ++ @hostname.setter ++ def hostname(self, value: Str): ++ self._hostname = value ++ ++ @property ++ def options(self) -> List[Str]: ++ """Options of the time source. ++ ++ For example: ++ ++ nts, ntsport 1234, iburst ++ ++ See ``man chrony.conf``. ++ ++ :return: a list of options ++ """ ++ return self._options ++ ++ @options.setter ++ def options(self, value): ++ self._options = value +-- +2.23.0 diff --git a/ntp-servers-improve-002-Use-the-structure-for-time-sources-in-ntp-py.patch b/ntp-servers-improve-002-Use-the-structure-for-time-sources-in-ntp-py.patch new file mode 100644 index 0000000..0bf217c --- /dev/null +++ b/ntp-servers-improve-002-Use-the-structure-for-time-sources-in-ntp-py.patch @@ -0,0 +1,290 @@ +From a645a1b8d17310533ef2d9232855c1852558e2b8 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 12:04:04 +0200 +Subject: [PATCH] Use the structure for time sources in ntp.py + +Modify ntp.py to work with TimeSourceData instead of strings and clean up +its functions a little. +--- + pyanaconda/ntp.py | 174 +++++++++++++++++++--------------------------- + 1 file changed, 73 insertions(+), 101 deletions(-) + +diff --git a/pyanaconda/ntp.py b/pyanaconda/ntp.py +index 16eece65e4..1b74ac9433 100644 +--- a/pyanaconda/ntp.py ++++ b/pyanaconda/ntp.py +@@ -31,6 +31,7 @@ + from pyanaconda import isys + from pyanaconda.threading import threadMgr, AnacondaThread + from pyanaconda.core.constants import THREAD_SYNC_TIME_BASENAME ++from pyanaconda.modules.common.structures.timezone import TimeSourceData + + NTP_CONFIG_FILE = "/etc/chrony.conf" + +@@ -47,21 +48,18 @@ class NTPconfigError(Exception): + pass + + +-def ntp_server_working(server): +- """ +- Tries to do an NTP request to the $server (timeout may take some time). ++def ntp_server_working(server_hostname): ++ """Tries to do an NTP request to the server (timeout may take some time). + +- :param server: hostname or IP address of an NTP server +- :type server: string ++ :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 +- + """ +- + client = ntplib.NTPClient() + + try: +- client.request(server) ++ client.request(server_hostname) + except ntplib.NTPException: + return False + # address related error +@@ -75,118 +73,89 @@ def ntp_server_working(server): + return True + + +-def pools_servers_to_internal(pools, servers): +- ret = [] +- for pool in pools: +- ret.extend(SERVERS_PER_POOL * [pool]) +- ret.extend(servers) +- +- return ret +- +- +-def internal_to_pools_and_servers(pools_servers): +- server_nums = dict() +- pools = [] +- servers = [] +- +- for item in pools_servers: +- server_nums[item] = server_nums.get(item, 0) + 1 +- +- for item in server_nums.keys(): +- if server_nums[item] >= SERVERS_PER_POOL: +- pools.extend((server_nums[item] // SERVERS_PER_POOL) * [item]) +- servers.extend((server_nums[item] % SERVERS_PER_POOL) * [item]) +- else: +- servers.extend(server_nums[item] * [item]) ++def get_servers_from_config(conf_file_path=NTP_CONFIG_FILE): ++ """Get NTP servers from a configuration file. + +- return (pools, servers) +- +- +-def get_servers_from_config(conf_file_path=NTP_CONFIG_FILE, +- srv_regexp=SRV_LINE_REGEXP): +- """ + 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: list +- ++ :rtype: a list of TimeSourceData instances + """ +- +- pools = list() +- servers = list() ++ servers = [] + + try: + with open(conf_file_path, "r") as conf_file: + for line in conf_file: +- match = srv_regexp.match(line) +- if match: +- if match.group(1) == "pool": +- pools.append(match.group(2)) +- else: +- servers.append(match.group(2)) ++ 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 = ["iburst"] ++ servers.append(server) + + except IOError as ioerr: +- msg = "Cannot open config file %s for reading (%s)" % (conf_file_path, +- ioerr.strerror) +- raise NTPconfigError(msg) ++ msg = "Cannot open config file {} for reading ({})." ++ raise NTPconfigError(msg.format(conf_file_path, ioerr.strerror)) + +- return (pools, servers) ++ return servers + + +-def save_servers_to_config(pools, servers, conf_file_path=NTP_CONFIG_FILE, +- srv_regexp=SRV_LINE_REGEXP, out_file_path=None): +- """ ++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. + +- :type pools: iterable +- :type servers: iterable +- :param out_file_path: path to the file 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 IOError as ioerr: +- msg = "Cannot open config file %s for reading (%s)" % (conf_file_path, +- ioerr.strerror) +- raise NTPconfigError(msg) ++ msg = "Cannot open config file {} for reading ({})." ++ raise NTPconfigError(msg.format(conf_file_path, ioerr.strerror)) + +- try: +- if out_file_path: ++ if out_file_path: ++ try: + new_conf_file = open(out_file_path, "w") +- else: +- (fildes, temp_path) = tempfile.mkstemp() +- new_conf_file = os.fdopen(fildes, "w") +- +- except IOError as ioerr: +- if out_file_path: +- msg = "Cannot open new config file %s "\ +- "for writing (%s)" % (out_file_path, ioerr.strerror) +- else: +- msg = "Cannot open temporary file %s "\ +- "for writing (%s)" % (temp_path, ioerr.strerror) +- +- raise NTPconfigError(msg) ++ except IOError as ioerr: ++ msg = "Cannot open new config file {} for writing ({})." ++ raise NTPconfigError(msg.format(out_file_path, ioerr.strerror)) ++ else: ++ try: ++ (fields, temp_path) = tempfile.mkstemp() ++ new_conf_file = os.fdopen(fields, "w") ++ except IOError as ioerr: ++ msg = "Cannot open temporary file {} for writing ({})." ++ raise NTPconfigError(msg.format(temp_path, ioerr.strerror)) + + heading = "# These servers were defined in the installation:\n" + +- #write info about the origin of the following lines ++ # write info about the origin of the following lines + new_conf_file.write(heading) + +- #write new servers and pools +- for pool in pools: +- new_conf_file.write("pool " + pool + " iburst\n") +- ++ # write new servers and pools + for server in servers: +- new_conf_file.write("server " + server + " iburst\n") ++ args = [server.type.lower(), server.hostname] + server.options ++ line = " ".join(args) + "\n" ++ new_conf_file.write(line) + +- #copy non-server lines from the old config and skip our heading ++ 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_regexp.match(line) and line != heading: ++ if not SRV_LINE_REGEXP.match(line) and line != heading: + new_conf_file.write(line) + + old_conf_file.close() +@@ -199,28 +168,27 @@ def save_servers_to_config(pools, servers, conf_file_path=NTP_CONFIG_FILE, + os.unlink(temp_path) + + except OSError as oserr: +- msg = "Cannot replace the old config with "\ +- "the new one (%s)" % (oserr.strerror) ++ msg = "Cannot replace the old config with the new one ({})." ++ raise NTPconfigError(msg.format(oserr.strerror)) + +- raise NTPconfigError(msg) + ++def _one_time_sync(server, callback=None): ++ """Synchronize the system time with a given NTP server. + +-def one_time_sync(server, callback=None): +- """ + Synchronize the system time with a given NTP server. Note that this + function is blocking and will not return until the time gets synced or + querying server fails (may take some time before timeouting). + +- :param server: NTP server ++ :param server: an NTP server ++ :type server: an instance of TimeSourceData + :param callback: callback function to run after sync or failure + :type callback: a function taking one boolean argument (success) + :return: True if the sync was successful, False otherwise +- + """ + + client = ntplib.NTPClient() + try: +- results = client.request(server) ++ results = client.request(server.hostname) + isys.set_system_time(int(results.tx_time)) + success = True + except ntplib.NTPException: +@@ -235,22 +203,26 @@ def one_time_sync(server, callback=None): + + + def one_time_sync_async(server, callback=None): +- """ ++ """Asynchronously synchronize the system time with a given NTP server. ++ + Asynchronously synchronize the system time with a given NTP server. This + function is non-blocking it starts a new thread for synchronization and + returns. Use callback argument to specify the function called when the + new thread finishes if needed. + +- :param server: NTP server ++ :param server: an NTP server ++ :type server: an instance of TimeSourceData + :param callback: callback function to run after sync or failure + :type callback: a function taking one boolean argument (success) +- + """ ++ thread_name = "%s_%s" % (THREAD_SYNC_TIME_BASENAME, server.hostname) + +- thread_name = "%s_%s" % (THREAD_SYNC_TIME_BASENAME, server) ++ # syncing with the same server running + if threadMgr.get(thread_name): +- #syncing with the same server running + return + +- threadMgr.add(AnacondaThread(name=thread_name, target=one_time_sync, +- args=(server, callback))) ++ threadMgr.add(AnacondaThread( ++ name=thread_name, ++ target=_one_time_sync, ++ args=(server, callback) ++ )) +-- +2.23.0 diff --git a/ntp-servers-improve-003-Use-the-structure-for-time-sources-in-the-Timezone-module.patch b/ntp-servers-improve-003-Use-the-structure-for-time-sources-in-the-Timezone-module.patch new file mode 100644 index 0000000..ee6e626 --- /dev/null +++ b/ntp-servers-improve-003-Use-the-structure-for-time-sources-in-the-Timezone-module.patch @@ -0,0 +1,406 @@ +From 4bc1b7305199fffc78439ab1ad1cdb8272988d52 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 12:21:11 +0200 +Subject: [PATCH] Use the structure for time sources in the Timezone module + +Modify the Timezone module to work with the DBus structures for time sources +instead of the strings. Rename the DBus property NTPServers to TimeSources. +--- + pyanaconda/modules/timezone/installation.py | 14 ++- + pyanaconda/modules/timezone/timezone.py | 44 ++++--- + .../modules/timezone/timezone_interface.py | 34 +++--- + .../pyanaconda_tests/module_timezone_test.py | 107 ++++++++++++++---- + 4 files changed, 141 insertions(+), 58 deletions(-) + +diff --git a/pyanaconda/modules/timezone/installation.py b/pyanaconda/modules/timezone/installation.py +index 6383df1103..c3ea4d7179 100644 +--- a/pyanaconda/modules/timezone/installation.py ++++ b/pyanaconda/modules/timezone/installation.py +@@ -145,12 +145,14 @@ def run(self): + return + + chronyd_conf_path = os.path.normpath(self._sysroot + ntp.NTP_CONFIG_FILE) +- pools, servers = ntp.internal_to_pools_and_servers(self._ntp_servers) + + if os.path.exists(chronyd_conf_path): + log.debug("Modifying installed chrony configuration") + try: +- ntp.save_servers_to_config(pools, servers, conf_file_path=chronyd_conf_path) ++ ntp.save_servers_to_config( ++ self._ntp_servers, ++ conf_file_path=chronyd_conf_path ++ ) + except ntp.NTPconfigError as ntperr: + log.warning("Failed to save NTP configuration: %s", ntperr) + +@@ -160,9 +162,11 @@ def run(self): + log.debug("Creating chrony configuration based on the " + "configuration from installation environment") + try: +- ntp.save_servers_to_config(pools, servers, +- conf_file_path=ntp.NTP_CONFIG_FILE, +- out_file_path=chronyd_conf_path) ++ ntp.save_servers_to_config( ++ self._ntp_servers, ++ conf_file_path=ntp.NTP_CONFIG_FILE, ++ out_file_path=chronyd_conf_path ++ ) + except ntp.NTPconfigError as ntperr: + log.warning("Failed to save NTP configuration without chrony package: %s", + ntperr) +diff --git a/pyanaconda/modules/timezone/timezone.py b/pyanaconda/modules/timezone/timezone.py +index 0678072978..ff89d1ea77 100644 +--- a/pyanaconda/modules/timezone/timezone.py ++++ b/pyanaconda/modules/timezone/timezone.py +@@ -18,10 +18,12 @@ + # Red Hat, Inc. + # + from pyanaconda.core.configuration.anaconda import conf ++from pyanaconda.core.constants import TIME_SOURCE_SERVER + from pyanaconda.core.dbus import DBus + from pyanaconda.core.signal import Signal + from pyanaconda.modules.common.base import KickstartService + from pyanaconda.modules.common.constants.services import TIMEZONE ++from pyanaconda.modules.common.structures.timezone import TimeSourceData + from pyanaconda.timezone import NTP_PACKAGE + from pyanaconda.modules.common.containers import TaskContainer + from pyanaconda.modules.common.structures.requirement import Requirement +@@ -48,8 +50,8 @@ def __init__(self): + self.ntp_enabled_changed = Signal() + self._ntp_enabled = True + +- self.ntp_servers_changed = Signal() +- self._ntp_servers = [] ++ self.time_sources_changed = Signal() ++ self._time_sources = [] + + # FIXME: temporary workaround until PAYLOAD module is available + self._ntp_excluded = False +@@ -70,7 +72,17 @@ def process_kickstart(self, data): + self.set_timezone(data.timezone.timezone) + self.set_is_utc(data.timezone.isUtc) + self.set_ntp_enabled(not data.timezone.nontp) +- self.set_ntp_servers(data.timezone.ntpservers) ++ ++ servers = [] ++ ++ for hostname in data.timezone.ntpservers: ++ server = TimeSourceData() ++ server.type = TIME_SOURCE_SERVER ++ server.hostname = hostname ++ server.options = ["iburst"] ++ servers.append(server) ++ ++ self.set_time_sources(servers) + + def setup_kickstart(self, data): + """Set up the kickstart data.""" +@@ -78,8 +90,12 @@ def setup_kickstart(self, data): + data.timezone.isUtc = self.is_utc + data.timezone.nontp = not self.ntp_enabled + +- if self.ntp_enabled: +- data.timezone.ntpservers = list(self.ntp_servers) ++ if not self.ntp_enabled: ++ return ++ ++ data.timezone.ntpservers = [ ++ server.hostname for server in self.time_sources ++ ] + + @property + def timezone(self): +@@ -115,15 +131,15 @@ def set_ntp_enabled(self, ntp_enabled): + log.debug("NTP is set to %s.", ntp_enabled) + + @property +- def ntp_servers(self): +- """Return a list of NTP servers.""" +- return self._ntp_servers ++ def time_sources(self): ++ """Return a list of time sources.""" ++ return self._time_sources + +- def set_ntp_servers(self, servers): +- """Set NTP servers.""" +- self._ntp_servers = list(servers) +- self.ntp_servers_changed.emit() +- log.debug("NTP servers are set to %s.", servers) ++ def set_time_sources(self, servers): ++ """Set time sources.""" ++ self._time_sources = list(servers) ++ self.time_sources_changed.emit() ++ log.debug("Time sources are set to: %s", servers) + + def collect_requirements(self): + """Return installation requirements for this module. +@@ -168,6 +184,6 @@ def install_with_tasks(self): + ConfigureNTPTask( + sysroot=conf.target.system_root, + ntp_enabled=self.ntp_enabled, +- ntp_servers=self.ntp_servers ++ ntp_servers=self.time_sources + ) + ] +diff --git a/pyanaconda/modules/timezone/timezone_interface.py b/pyanaconda/modules/timezone/timezone_interface.py +index 03c5003f1e..f36e0b3723 100644 +--- a/pyanaconda/modules/timezone/timezone_interface.py ++++ b/pyanaconda/modules/timezone/timezone_interface.py +@@ -17,13 +17,15 @@ + # License and may only be used or replicated with the express permission of + # Red Hat, Inc. + # +-from pyanaconda.modules.common.constants.services import TIMEZONE +-from pyanaconda.modules.common.containers import TaskContainer + from dasbus.server.property import emits_properties_changed + from dasbus.typing import * # pylint: disable=wildcard-import +-from pyanaconda.modules.common.base import KickstartModuleInterface + from dasbus.server.interface import dbus_interface + ++from pyanaconda.modules.common.base import KickstartModuleInterface ++from pyanaconda.modules.common.constants.services import TIMEZONE ++from pyanaconda.modules.common.containers import TaskContainer ++from pyanaconda.modules.common.structures.timezone import TimeSourceData ++ + + @dbus_interface(TIMEZONE.interface_name) + class TimezoneInterface(KickstartModuleInterface): +@@ -34,7 +36,7 @@ def connect_signals(self): + self.watch_property("Timezone", self.implementation.timezone_changed) + self.watch_property("IsUTC", self.implementation.is_utc_changed) + self.watch_property("NTPEnabled", self.implementation.ntp_enabled_changed) +- self.watch_property("NTPServers", self.implementation.ntp_servers_changed) ++ self.watch_property("TimeSources", self.implementation.time_sources_changed) + + @property + def Timezone(self) -> Str: +@@ -91,22 +93,26 @@ def SetNTPEnabled(self, ntp_enabled: Bool): + self.implementation.set_ntp_enabled(ntp_enabled) + + @property +- def NTPServers(self) -> List[Str]: +- """A list of NTP servers. ++ def TimeSources(self) -> List[Structure]: ++ """A list of time sources. + +- :return: a list of servers ++ :return: a list of time source data ++ :rtype: a list of structures of the type TimeSourceData + """ +- return self.implementation.ntp_servers ++ return TimeSourceData.to_structure_list( ++ self.implementation.time_sources ++ ) + + @emits_properties_changed +- def SetNTPServers(self, servers: List[Str]): +- """Set the NTP servers. ++ def SetTimeSources(self, sources: List[Structure]): ++ """Set the time sources. + +- Example: [ntp.cesnet.cz] +- +- :param servers: a list of servers ++ :param sources: a list of time sources ++ :type sources: a list of structures of the type TimeSourceData + """ +- self.implementation.set_ntp_servers(servers) ++ self.implementation.set_time_sources( ++ TimeSourceData.from_structure_list(sources) ++ ) + + def ConfigureNTPServiceEnablementWithTask(self, ntp_excluded: Bool) -> ObjPath: + """Enable or disable NTP service. +diff --git a/tests/nosetests/pyanaconda_tests/module_timezone_test.py b/tests/nosetests/pyanaconda_tests/module_timezone_test.py +index f991f1e992..bb751d6f4b 100644 +--- a/tests/nosetests/pyanaconda_tests/module_timezone_test.py ++++ b/tests/nosetests/pyanaconda_tests/module_timezone_test.py +@@ -23,8 +23,13 @@ + from shutil import copytree, copyfile + from unittest.mock import Mock, patch + ++from dasbus.structure import compare_data ++from dasbus.typing import * # pylint: disable=wildcard-import ++ ++from pyanaconda.core.constants import TIME_SOURCE_SERVER, TIME_SOURCE_POOL + from pyanaconda.modules.common.constants.services import TIMEZONE + from pyanaconda.modules.common.errors.installation import TimezoneConfigurationError ++from pyanaconda.modules.common.structures.timezone import TimeSourceData + from pyanaconda.modules.timezone.installation import ConfigureNTPTask, ConfigureTimezoneTask, \ + ConfigureNTPServiceEnablementTask + from pyanaconda.modules.common.structures.kickstart import KickstartReport +@@ -33,7 +38,7 @@ + from pyanaconda.ntp import NTP_CONFIG_FILE, NTPconfigError + from tests.nosetests.pyanaconda_tests import check_kickstart_interface, \ + patch_dbus_publish_object, PropertiesChangedCallback, check_task_creation, \ +- patch_dbus_get_proxy, check_task_creation_list ++ patch_dbus_get_proxy, check_task_creation_list, check_dbus_property + from pyanaconda.timezone import NTP_SERVICE + + +@@ -50,6 +55,14 @@ def setUp(self): + self.callback = PropertiesChangedCallback() + self.timezone_interface.PropertiesChanged.connect(self.callback) + ++ def _check_dbus_property(self, *args, **kwargs): ++ check_dbus_property( ++ self, ++ TIMEZONE, ++ self.timezone_interface, ++ *args, **kwargs ++ ) ++ + def kickstart_properties_test(self): + """Test kickstart properties.""" + self.assertEqual(self.timezone_interface.KickstartCommands, ["timezone"]) +@@ -76,12 +89,24 @@ def ntp_property_test(self): + self.assertEqual(self.timezone_interface.NTPEnabled, False) + self.callback.assert_called_once_with(TIMEZONE.interface_name, {'NTPEnabled': False}, []) + +- def ntp_servers_property_test(self): +- """Test the NTPServers property.""" +- self.timezone_interface.SetNTPServers(["ntp.cesnet.cz"]) +- self.assertEqual(self.timezone_interface.NTPServers, ["ntp.cesnet.cz"]) +- self.callback.assert_called_once_with( +- TIMEZONE.interface_name, {'NTPServers': ["ntp.cesnet.cz"]}, []) ++ def time_sources_property_test(self): ++ """Test the TimeSources property.""" ++ server = { ++ "type": get_variant(Str, TIME_SOURCE_SERVER), ++ "hostname": get_variant(Str, "ntp.cesnet.cz"), ++ "options": get_variant(List[Str], ["iburst"]), ++ } ++ ++ pool = { ++ "type": get_variant(Str, TIME_SOURCE_POOL), ++ "hostname": get_variant(Str, "0.fedora.pool.ntp.org"), ++ "options": get_variant(List[Str], []), ++ } ++ ++ self._check_dbus_property( ++ "TimeSources", ++ [server, pool] ++ ) + + def _test_kickstart(self, ks_in, ks_out): + check_kickstart_interface(self, self.timezone_interface, ks_in, ks_out) +@@ -162,10 +187,19 @@ def install_with_tasks_configured_test(self, publisher): + self.timezone_interface.SetNTPEnabled(False) + # --nontp and --ntpservers are mutually exclusive in kicstart but + # there is no such enforcement in the module so for testing this is ok +- self.timezone_interface.SetNTPServers([ +- "clock1.example.com", +- "clock2.example.com", +- ]) ++ ++ server = TimeSourceData() ++ server.type = TIME_SOURCE_SERVER ++ server.hostname = "clock1.example.com" ++ server.options = ["iburst"] ++ ++ pool = TimeSourceData() ++ pool.type = TIME_SOURCE_POOL ++ pool.hostname = "clock2.example.com" ++ ++ self.timezone_interface.SetTimeSources( ++ TimeSourceData.to_structure_list([server, pool]) ++ ) + + task_classes = [ + ConfigureTimezoneTask, +@@ -182,10 +216,9 @@ def install_with_tasks_configured_test(self, publisher): + # ConfigureNTPTask + obj = task_objs[1] + self.assertEqual(obj.implementation._ntp_enabled, False) +- self.assertEqual(obj.implementation._ntp_servers, [ +- "clock1.example.com", +- "clock2.example.com", +- ]) ++ self.assertEqual(len(obj.implementation._ntp_servers), 2) ++ self.assertTrue(compare_data(obj.implementation._ntp_servers[0], server)) ++ self.assertTrue(compare_data(obj.implementation._ntp_servers[1], pool)) + + @patch_dbus_publish_object + def configure_ntp_service_enablement_default_test(self, publisher): +@@ -354,13 +387,13 @@ class NTPTasksTestCase(unittest.TestCase): + + def ntp_task_success_test(self): + """Test the success cases for NTP setup D-Bus task.""" +- self._test_ntp_inputs(False, False, ["unique.ntp.server", "another.unique.server"]) +- self._test_ntp_inputs(False, True, ["unique.ntp.server", "another.unique.server"]) ++ self._test_ntp_inputs(False, False) ++ self._test_ntp_inputs(False, True) + + def ntp_overwrite_test(self): + """Test overwriting existing config for NTP setup D-Bus task.""" +- self._test_ntp_inputs(True, True, ["unique.ntp.server", "another.unique.server"]) +- self._test_ntp_inputs(True, False, ["unique.ntp.server", "another.unique.server"]) ++ self._test_ntp_inputs(True, True) ++ self._test_ntp_inputs(True, False) + + def ntp_save_failure_test(self): + """Test failure when saving NTP config in D-Bus task.""" +@@ -368,6 +401,25 @@ def ntp_save_failure_test(self): + self._test_ntp_exception(True) + self._test_ntp_exception(False) + ++ def _get_test_sources(self): ++ """Get a list of sources""" ++ server = TimeSourceData() ++ server.type = TIME_SOURCE_SERVER ++ server.hostname = "unique.ntp.server" ++ server.options = ["iburst"] ++ ++ pool = TimeSourceData() ++ pool.type = TIME_SOURCE_POOL ++ pool.hostname = "another.unique.server" ++ ++ return [server, pool] ++ ++ def _get_expected_lines(self): ++ return [ ++ "server unique.ntp.server iburst\n", ++ "pool another.unique.server\n" ++ ] ++ + @patch("pyanaconda.modules.timezone.installation.ntp.save_servers_to_config", + side_effect=NTPconfigError) + def _test_ntp_exception(self, make_chronyd, mock_save): +@@ -376,11 +428,14 @@ def _test_ntp_exception(self, make_chronyd, mock_save): + with self.assertLogs("anaconda.modules.timezone.installation", level="WARNING"): + self._execute_task(sysroot, True, ["ntp.example.com"]) + +- def _test_ntp_inputs(self, make_chronyd, ntp_enabled, ntp_servers): ++ def _test_ntp_inputs(self, make_chronyd, ntp_enabled): ++ ntp_servers = self._get_test_sources() ++ expected_lines = self._get_expected_lines() ++ + with tempfile.TemporaryDirectory() as sysroot: + self._setup_environment(sysroot, make_chronyd) + self._execute_task(sysroot, ntp_enabled, ntp_servers) +- self._validate_ntp_config(sysroot, make_chronyd, ntp_enabled, ntp_servers) ++ self._validate_ntp_config(sysroot, make_chronyd, ntp_enabled, expected_lines) + + def _setup_environment(self, sysroot, make_chronyd): + os.mkdir(sysroot + "/etc") +@@ -395,12 +450,14 @@ def _execute_task(self, sysroot, ntp_enabled, ntp_servers): + ) + task.run() + +- def _validate_ntp_config(self, sysroot, was_present, was_enabled, expected_servers): ++ def _validate_ntp_config(self, sysroot, was_present, was_enabled, expected_lines): + if was_enabled: + with open(sysroot + NTP_CONFIG_FILE) as fobj: +- all_lines = "\n".join(fobj.readlines()) +- for server in expected_servers: +- self.assertIn(server, all_lines) ++ all_lines = fobj.readlines() ++ ++ for line in expected_lines: ++ self.assertIn(line, all_lines) ++ + elif not was_present: + self.assertFalse(os.path.exists(sysroot + NTP_CONFIG_FILE)) + +-- +2.23.0 diff --git a/ntp-servers-improve-004-Use-the-structure-for-time-sources-in-anaconda-py.patch b/ntp-servers-improve-004-Use-the-structure-for-time-sources-in-anaconda-py.patch new file mode 100644 index 0000000..f1d8ca4 --- /dev/null +++ b/ntp-servers-improve-004-Use-the-structure-for-time-sources-in-anaconda-py.patch @@ -0,0 +1,113 @@ +From 19dea71f13d55d49e9dfbcc5d941afd5eb5d9e6d Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 12:08:05 +0200 +Subject: [PATCH] Use the structure for time sources in anaconda.py + +Modify anaconda.py to work with TimeSourceData instead of strings. +--- + anaconda.py | 11 +-------- + pyanaconda/startup_utils.py | 45 +++++++++++++++++++++++++++---------- + 2 files changed, 34 insertions(+), 22 deletions(-) + +diff --git a/anaconda.py b/anaconda.py +index 1abdeb2e1a..d6bb57190c 100755 +--- a/anaconda.py ++++ b/anaconda.py +@@ -325,7 +325,6 @@ def setup_environment(): + + from pyanaconda import vnc + from pyanaconda import kickstart +- from pyanaconda import ntp + from pyanaconda import keyboard + # we are past the --version and --help shortcut so we can import display & + # startup_utils, which import Blivet, without slowing down anything critical +@@ -714,15 +713,7 @@ def _earlyExceptionHandler(ty, value, traceback): + geoloc.geoloc.refresh() + + # setup ntp servers and start NTP daemon if not requested otherwise +- if conf.system.can_set_time_synchronization: +- kickstart_ntpservers = timezone_proxy.NTPServers +- +- if kickstart_ntpservers: +- pools, servers = ntp.internal_to_pools_and_servers(kickstart_ntpservers) +- ntp.save_servers_to_config(pools, servers) +- +- if timezone_proxy.NTPEnabled: +- util.start_service("chronyd") ++ startup_utils.start_chronyd() + + # Finish the initialization of the setup on boot action. + # This should be done sooner and somewhere else once it is possible. +diff --git a/pyanaconda/startup_utils.py b/pyanaconda/startup_utils.py +index f08b19e11a..e53d5491c1 100644 +--- a/pyanaconda/startup_utils.py ++++ b/pyanaconda/startup_utils.py +@@ -17,29 +17,28 @@ + # License and may only be used or replicated with the express permission of + # Red Hat, Inc. + # +-from pyanaconda.core.configuration.anaconda import conf +-from pyanaconda.core.i18n import _ +- +-from pyanaconda.anaconda_loggers import get_stdout_logger, get_storage_logger, get_packaging_logger +-stdout_log = get_stdout_logger() +- +-from pyanaconda.anaconda_loggers import get_module_logger +-log = get_module_logger(__name__) +- + import sys + import time + import os ++import blivet + +-from pyanaconda.core import util, constants +-from pyanaconda import product ++from pyanaconda import product, ntp + from pyanaconda import anaconda_logging + from pyanaconda import network + from pyanaconda import safe_dbus + from pyanaconda import kickstart ++from pyanaconda.anaconda_loggers import get_stdout_logger, get_storage_logger, \ ++ get_packaging_logger, get_module_logger ++from pyanaconda.core import util, constants ++from pyanaconda.core.configuration.anaconda import conf ++from pyanaconda.core.i18n import _ + from pyanaconda.flags import flags + from pyanaconda.screensaver import inhibit_screensaver ++from pyanaconda.modules.common.structures.timezone import TimeSourceData ++from pyanaconda.modules.common.constants.services import TIMEZONE + +-import blivet ++stdout_log = get_stdout_logger() ++log = get_module_logger(__name__) + + + def gtk_warning(title, reason): +@@ -373,3 +372,25 @@ def parse_kickstart(ks, addon_paths, strict_mode=False): + kickstart.parseKickstart(ksdata, ks, strict_mode=strict_mode, pass_to_boss=True) + + return ksdata ++ ++ ++def start_chronyd(): ++ """Start the NTP daemon chronyd. ++ ++ Set up NTP servers and start NTP daemon if not requested otherwise. ++ """ ++ if not conf.system.can_set_time_synchronization: ++ log.debug("Skip the time synchronization.") ++ return ++ ++ timezone_proxy = TIMEZONE.get_proxy() ++ enabled = timezone_proxy.NTPEnabled ++ servers = TimeSourceData.from_structure_list( ++ timezone_proxy.TimeSources ++ ) ++ ++ if servers: ++ ntp.save_servers_to_config(servers) ++ ++ if enabled: ++ util.start_service("chronyd") +-- +2.23.0 diff --git a/ntp-servers-improve-005-Use-the-structure-for-time-sources-in-network-py.patch b/ntp-servers-improve-005-Use-the-structure-for-time-sources-in-network-py.patch new file mode 100644 index 0000000..1776584 --- /dev/null +++ b/ntp-servers-improve-005-Use-the-structure-for-time-sources-in-network-py.patch @@ -0,0 +1,80 @@ +From 4635e846a98182901777ab6de492020082f313cb Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 12:12:33 +0200 +Subject: [PATCH] Use the structure for time sources in network.py + +Modify network.py to work with TimeSourceData instead of strings. +--- + pyanaconda/network.py | 31 +++++++++++++++++++++---------- + 1 file changed, 21 insertions(+), 10 deletions(-) + +diff --git a/pyanaconda/network.py b/pyanaconda/network.py +index f8e9b19a15..bce57354b1 100644 +--- a/pyanaconda/network.py ++++ b/pyanaconda/network.py +@@ -16,14 +16,7 @@ + # + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . +- +-import gi +-gi.require_version("NM", "1.0") +- +-from gi.repository import NM +- + import shutil +-from pyanaconda.core import util, constants + import socket + import itertools + import os +@@ -34,17 +27,24 @@ + + from dasbus.typing import get_native + ++from pyanaconda.anaconda_loggers import get_module_logger ++from pyanaconda.core import util, constants + from pyanaconda.core.i18n import _ + from pyanaconda.core.kernel import kernel_arguments + from pyanaconda.core.regexes import HOSTNAME_PATTERN_WITHOUT_ANCHORS, \ + IPV6_ADDRESS_IN_DRACUT_IP_OPTION + from pyanaconda.core.configuration.anaconda import conf ++from pyanaconda.core.constants import TIME_SOURCE_SERVER + from pyanaconda.modules.common.constants.services import NETWORK, TIMEZONE, STORAGE + from pyanaconda.modules.common.constants.objects import FCOE + from pyanaconda.modules.common.task import sync_run_task + from pyanaconda.modules.common.structures.network import NetworkDeviceInfo ++from pyanaconda.modules.common.structures.timezone import TimeSourceData ++ ++import gi ++gi.require_version("NM", "1.0") ++from gi.repository import NM + +-from pyanaconda.anaconda_loggers import get_module_logger + log = get_module_logger(__name__) + + DEFAULT_HOSTNAME = "localhost.localdomain" +@@ -347,9 +347,20 @@ def _set_ntp_servers_from_dhcp(): + hostnames.append(hostname) + + # check if some NTP servers were specified from kickstart +- if not timezone_proxy.NTPServers and conf.target.is_hardware: ++ if not timezone_proxy.TimeSources and conf.target.is_hardware: + # no NTP servers were specified, add those from DHCP +- timezone_proxy.SetNTPServers(hostnames) ++ servers = [] ++ ++ for hostname in hostnames: ++ server = TimeSourceData() ++ server.type = TIME_SOURCE_SERVER ++ server.hostname = hostname ++ server.options = ["iburst"] ++ servers.append(server) ++ ++ timezone_proxy.SetTimeSources( ++ TimeSourceData.to_structure_list(servers) ++ ) + + + def wait_for_connected_NM(timeout=constants.NETWORK_CONNECTION_TIMEOUT, only_connecting=False): +-- +2.23.0 diff --git a/ntp-servers-improve-006-Add-support-for-the-NTP-server-status-cache.patch b/ntp-servers-improve-006-Add-support-for-the-NTP-server-status-cache.patch new file mode 100644 index 0000000..1b97cd0 --- /dev/null +++ b/ntp-servers-improve-006-Add-support-for-the-NTP-server-status-cache.patch @@ -0,0 +1,120 @@ +From 06ed7b6cee7baf64cf83411645bfa52a05767b92 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Mon, 6 Jul 2020 14:16:40 +0200 +Subject: [PATCH] Add support for the NTP server status cache + +Use the class NTPServerStatusCache to check the status of the given NTP server. +The cache remembers results of all checked host names. +--- + pyanaconda/ntp.py | 84 +++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 82 insertions(+), 2 deletions(-) + +diff --git a/pyanaconda/ntp.py b/pyanaconda/ntp.py +index 1b74ac9433..637d31f63e 100644 +--- a/pyanaconda/ntp.py ++++ b/pyanaconda/ntp.py +@@ -29,9 +29,12 @@ + import socket + + from pyanaconda import isys +-from pyanaconda.threading import threadMgr, AnacondaThread +-from pyanaconda.core.constants import THREAD_SYNC_TIME_BASENAME ++from pyanaconda.anaconda_loggers import get_module_logger ++from pyanaconda.core.i18n import N_, _ ++from pyanaconda.core.constants import THREAD_SYNC_TIME_BASENAME, NTP_SERVER_QUERY, \ ++ THREAD_NTP_SERVER_CHECK, NTP_SERVER_OK, NTP_SERVER_NOK + from pyanaconda.modules.common.structures.timezone import TimeSourceData ++from pyanaconda.threading import threadMgr, AnacondaThread + + NTP_CONFIG_FILE = "/etc/chrony.conf" + +@@ -42,6 +45,15 @@ + #treat pools as four servers with the same name + SERVERS_PER_POOL = 4 + ++# 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""" +@@ -226,3 +238,71 @@ def one_time_sync_async(server, callback=None): + target=_one_time_sync, + args=(server, callback) + )) ++ ++ ++class NTPServerStatusCache(object): ++ """The cache of NTP server states.""" ++ ++ def __init__(self): ++ self._cache = {} ++ ++ 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. ++ hostname = server.hostname ++ ++ # Reset the current status. ++ self._set_status(hostname, NTP_SERVER_QUERY) ++ ++ # Start the check. ++ threadMgr.add(AnacondaThread( ++ prefix=THREAD_NTP_SERVER_CHECK, ++ target=self._check_status, ++ args=(hostname, )) ++ ) ++ ++ 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 ++ ++ def _check_status(self, hostname): ++ """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) ++ ++ 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) +-- +2.23.0 diff --git a/ntp-servers-improve-007-Add-support-for-generating-a-summary-of-the-NTP-servers.patch b/ntp-servers-improve-007-Add-support-for-generating-a-summary-of-the-NTP-servers.patch new file mode 100644 index 0000000..721e007 --- /dev/null +++ b/ntp-servers-improve-007-Add-support-for-generating-a-summary-of-the-NTP-servers.patch @@ -0,0 +1,60 @@ +From 716db242314b710b881c073d290b6d1ad8670d36 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Mon, 6 Jul 2020 14:19:46 +0200 +Subject: [PATCH] Add support for generating a summary of the NTP servers + +Call the functions get_ntp_server_summary and get_ntp_servers_summary to +generate a string with a summary of the specified NTP servers and their +states. +--- + pyanaconda/ntp.py | 35 +++++++++++++++++++++++++++++++++++ + 1 file changed, 35 insertions(+) + +diff --git a/pyanaconda/ntp.py b/pyanaconda/ntp.py +index 637d31f63e..eed4b34307 100644 +--- a/pyanaconda/ntp.py ++++ b/pyanaconda/ntp.py +@@ -60,6 +60,41 @@ class NTPconfigError(Exception): + 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): + """Tries to do an NTP request to the server (timeout may take some time). + +-- +2.23.0 diff --git a/ntp-servers-improve-008-Use-the-structure-for-time-sources-in-TUI.patch b/ntp-servers-improve-008-Use-the-structure-for-time-sources-in-TUI.patch new file mode 100644 index 0000000..0fd92ef --- /dev/null +++ b/ntp-servers-improve-008-Use-the-structure-for-time-sources-in-TUI.patch @@ -0,0 +1,468 @@ +From 8a10cee0ab94b844c65d1493b3d78df5210e4e34 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 14:41:37 +0200 +Subject: [PATCH] Use the structure for time sources in TUI + +Modify TUI to work with TimeSourceData instead of strings. +--- + pyanaconda/ui/tui/spokes/time_spoke.py | 286 ++++++++++--------------- + 1 file changed, 109 insertions(+), 177 deletions(-) + +diff --git a/pyanaconda/ui/tui/spokes/time_spoke.py b/pyanaconda/ui/tui/spokes/time_spoke.py +index b93ab41eec..b88a17960f 100644 +--- a/pyanaconda/ui/tui/spokes/time_spoke.py ++++ b/pyanaconda/ui/tui/spokes/time_spoke.py +@@ -16,7 +16,10 @@ + # License and may only be used or replicated with the express permission of + # Red Hat, Inc. + # ++from pyanaconda.core.constants import TIME_SOURCE_SERVER + from pyanaconda.modules.common.constants.services import TIMEZONE ++from pyanaconda.modules.common.structures.timezone import TimeSourceData ++from pyanaconda.ntp import NTPServerStatusCache + from pyanaconda.ui.categories.localization import LocalizationCategory + from pyanaconda.ui.tui.spokes import NormalTUISpoke + from pyanaconda.ui.common import FirstbootSpokeMixIn +@@ -24,11 +27,9 @@ + from pyanaconda import ntp + from pyanaconda.core import constants + from pyanaconda.core.i18n import N_, _, C_ +-from pyanaconda.threading import threadMgr, AnacondaThread + from pyanaconda.flags import flags + +-from collections import OrderedDict, namedtuple +-from threading import RLock ++from collections import namedtuple + + from simpleline.render.containers import ListColumnContainer + from simpleline.render.screen import InputState +@@ -39,22 +40,10 @@ + from pyanaconda.anaconda_loggers import get_module_logger + log = get_module_logger(__name__) + +-CallbackTimezoneArgs = namedtuple("CallbackTimezoneArgs", ["region", "timezone"]) +- +- +-def format_ntp_status_list(servers): +- ntp_server_states = { +- constants.NTP_SERVER_OK: _("status: working"), +- constants.NTP_SERVER_NOK: _("status: not working"), +- constants.NTP_SERVER_QUERY: _("checking status") +- } +- status_list = [] +- for server, server_state in servers.items(): +- status_list.append("%s (%s)" % (server, ntp_server_states[server_state])) +- return status_list ++__all__ = ["TimeSpoke"] + + +-__all__ = ["TimeSpoke"] ++CallbackTimezoneArgs = namedtuple("CallbackTimezoneArgs", ["region", "timezone"]) + + + class TimeSpoke(FirstbootSpokeMixIn, NormalTUISpoke): +@@ -66,10 +55,8 @@ def __init__(self, data, storage, payload): + self.title = N_("Time settings") + self._timezone_spoke = None + self._container = None +- # we use an ordered dict to keep the NTP server insertion order +- self._ntp_servers = OrderedDict() +- self._ntp_servers_lock = RLock() +- ++ self._ntp_servers = [] ++ self._ntp_servers_states = NTPServerStatusCache() + self._timezone_module = TIMEZONE.get_proxy() + + @property +@@ -83,103 +70,24 @@ def initialize(self): + # during the installation + # - from config files when running in Initial Setup + # after the installation +- ntp_servers = [] +- + if constants.ANACONDA_ENVIRON in flags.environs: +- ntp_servers = self._timezone_module.NTPServers ++ self._ntp_servers = TimeSourceData.from_structure_list( ++ self._timezone_module.TimeSources ++ ) + elif constants.FIRSTBOOT_ENVIRON in flags.environs: +- ntp_servers = ntp.get_servers_from_config()[1] # returns a (NPT pools, NTP servers) tupple ++ self._ntp_servers = ntp.get_servers_from_config() + else: + log.error("tui time spoke: unsupported environment configuration %s," + "can't decide where to get initial NTP servers", flags.environs) + +- # check if the NTP servers appear to be working or not +- if ntp_servers: +- for server in ntp_servers: +- self._ntp_servers[server] = constants.NTP_SERVER_QUERY +- +- # check if the newly added NTP servers work fine +- self._check_ntp_servers_async(self._ntp_servers.keys()) ++ # check if the newly added NTP servers work fine ++ for server in self._ntp_servers: ++ self._ntp_servers_states.check_status(server) + + # we assume that the NTP spoke is initialized enough even if some NTP + # server check threads might still be running + self.initialize_done() + +- def _check_ntp_servers_async(self, servers): +- """Asynchronously check if given NTP servers appear to be working. +- +- :param list servers: list of servers to check +- """ +- for server in servers: +- threadMgr.add(AnacondaThread(prefix=constants.THREAD_NTP_SERVER_CHECK, +- target=self._check_ntp_server, +- args=(server,))) +- +- def _check_ntp_server(self, server): +- """Check if an NTP server appears to be working. +- +- :param str server: NTP server address +- :returns: True if the server appears to be working, False if not +- :rtype: bool +- """ +- log.debug("checking NTP server %s", server) +- result = ntp.ntp_server_working(server) +- if result: +- log.debug("NTP server %s appears to be working", server) +- self.set_ntp_server_status(server, constants.NTP_SERVER_OK) +- else: +- log.debug("NTP server %s appears not to be working", server) +- self.set_ntp_server_status(server, constants.NTP_SERVER_NOK) +- +- @property +- def ntp_servers(self): +- """Return a list of NTP servers known to the Time spoke. +- +- :returns: a list of NTP servers +- :rtype: list of strings +- """ +- return self._ntp_servers +- +- def add_ntp_server(self, server): +- """Add NTP server address to our internal NTP server tracking dictionary. +- +- :param str server: NTP server address to add +- """ +- # the add & remove operations should (at least at the moment) be never +- # called from different threads at the same time, but lets just use +- # a lock there when we are at it +- with self._ntp_servers_lock: +- if server not in self._ntp_servers: +- self._ntp_servers[server] = constants.NTP_SERVER_QUERY +- self._check_ntp_servers_async([server]) +- +- def remove_ntp_server(self, server): +- """Remove NTP server address from our internal NTP server tracking dictionary. +- +- :param str server: NTP server address to remove +- """ +- # the remove-server and set-server-status operations need to be atomic, +- # so that we avoid reintroducing removed servers by setting their status +- with self._ntp_servers_lock: +- if server in self._ntp_servers: +- del self._ntp_servers[server] +- +- def set_ntp_server_status(self, server, status): +- """Set status for an NTP server in the NTP server dict. +- +- The status can be "working", "not working" or "check in progress", +- and is defined by three constants defined in constants.py. +- +- :param str server: an NTP server +- :param int status: status of the NTP server +- """ +- +- # the remove-server and set-server-status operations need to be atomic, +- # so that we avoid reintroducing removed server by setting their status +- with self._ntp_servers_lock: +- if server in self._ntp_servers: +- self._ntp_servers[server] = status +- + @property + def timezone_spoke(self): + if not self._timezone_spoke: +@@ -210,6 +118,7 @@ def _summary_text(self): + :rtype: str + """ + msg = "" ++ + # timezone + kickstart_timezone = self._timezone_module.Timezone + timezone_msg = _("not set") +@@ -222,12 +131,10 @@ def _summary_text(self): + msg += "\n" + + # NTP +- msg += _("NTP servers:") +- if self._ntp_servers: +- for status in format_ntp_status_list(self._ntp_servers): +- msg += "\n%s" % status +- else: +- msg += _("not configured") ++ msg += ntp.get_ntp_servers_summary( ++ self._ntp_servers, ++ self._ntp_servers_states ++ ) + + return msg + +@@ -244,8 +151,15 @@ def refresh(self, args=None): + + self._container = ListColumnContainer(1, columns_width=78, spacing=1) + +- self._container.add(TextWidget(timezone_option), callback=self._timezone_callback) +- self._container.add(TextWidget(_("Configure NTP servers")), callback=self._configure_ntp_server_callback) ++ self._container.add( ++ TextWidget(timezone_option), ++ callback=self._timezone_callback ++ ) ++ ++ self._container.add( ++ TextWidget(_("Configure NTP servers")), ++ callback=self._configure_ntp_server_callback ++ ) + + self.window.add_with_separator(self._container) + +@@ -254,7 +168,13 @@ def _timezone_callback(self, data): + self.close() + + def _configure_ntp_server_callback(self, data): +- new_spoke = NTPServersSpoke(self.data, self.storage, self.payload, self) ++ new_spoke = NTPServersSpoke( ++ self.data, ++ self.storage, ++ self.payload, ++ self._ntp_servers, ++ self._ntp_servers_states ++ ) + ScreenHandler.push_screen_modal(new_spoke) + self.apply() + self.close() +@@ -268,7 +188,9 @@ def input(self, args, key): + + def apply(self): + # update the NTP server list in kickstart +- self._timezone_module.SetNTPServers(list(self.ntp_servers.keys())) ++ self._timezone_module.SetTimeSources( ++ TimeSourceData.to_structure_list(self._ntp_servers) ++ ) + + + class TimeZoneSpoke(NormalTUISpoke): +@@ -375,49 +297,55 @@ def apply(self): + class NTPServersSpoke(NormalTUISpoke): + category = LocalizationCategory + +- def __init__(self, data, storage, payload, time_spoke): ++ def __init__(self, data, storage, payload, servers, states): + super().__init__(data, storage, payload) + self.title = N_("NTP configuration") + self._container = None +- self._time_spoke = time_spoke ++ self._servers = servers ++ self._states = states + + @property + def indirect(self): + return True + +- def _summary_text(self): +- """Return summary of NTP configuration.""" +- msg = _("NTP servers:") +- if self._time_spoke.ntp_servers: +- for status in format_ntp_status_list(self._time_spoke.ntp_servers): +- msg += "\n%s" % status +- else: +- msg += _("no NTP servers have been configured") +- return msg +- + def refresh(self, args=None): + super().refresh(args) + +- summary = self._summary_text() ++ summary = ntp.get_ntp_servers_summary( ++ self._servers, ++ self._states ++ ) ++ + self.window.add_with_separator(TextWidget(summary)) + + self._container = ListColumnContainer(1, columns_width=78, spacing=1) +- + self._container.add(TextWidget(_("Add NTP server")), self._add_ntp_server) + + # only add the remove option when we can remove something +- if self._time_spoke.ntp_servers: ++ if self._servers: + self._container.add(TextWidget(_("Remove NTP server")), self._remove_ntp_server) + + self.window.add_with_separator(self._container) + + def _add_ntp_server(self, data): +- new_spoke = AddNTPServerSpoke(self.data, self.storage, self.payload, self._time_spoke) ++ new_spoke = AddNTPServerSpoke( ++ self.data, ++ self.storage, ++ self.payload, ++ self._servers, ++ self._states ++ ) + ScreenHandler.push_screen_modal(new_spoke) + self.redraw() + + def _remove_ntp_server(self, data): +- new_spoke = RemoveNTPServerSpoke(self.data, self.storage, self.payload, self._time_spoke) ++ new_spoke = RemoveNTPServerSpoke( ++ self.data, ++ self.storage, ++ self.payload, ++ self._servers, ++ self._states ++ ) + ScreenHandler.push_screen_modal(new_spoke) + self.redraw() + +@@ -434,12 +362,12 @@ def apply(self): + class AddNTPServerSpoke(NormalTUISpoke): + category = LocalizationCategory + +- def __init__(self, data, storage, payload, time_spoke): ++ def __init__(self, data, storage, payload, servers, states): + super().__init__(data, storage, payload) + self.title = N_("Add NTP server address") +- self._time_spoke = time_spoke +- self._new_ntp_server = None +- self.value = None ++ self._servers = servers ++ self._states = states ++ self._value = None + + @property + def indirect(self): +@@ -447,76 +375,80 @@ def indirect(self): + + def refresh(self, args=None): + super().refresh(args) +- self.value = None ++ self._value = None + + def prompt(self, args=None): + # the title is enough, no custom prompt is needed +- if self.value is None: # first run or nothing entered ++ if self._value is None: # first run or nothing entered + return Prompt(_("Enter an NTP server address and press %s") % Prompt.ENTER) + + # an NTP server address has been entered +- self._new_ntp_server = self.value ++ self._add_ntp_server(self._value) + +- self.apply() + self.close() + ++ def _add_ntp_server(self, server_hostname): ++ for server in self._servers: ++ if server.hostname == server_hostname: ++ return ++ ++ server = TimeSourceData() ++ server.type = TIME_SOURCE_SERVER ++ server.hostname = server_hostname ++ server.options = ["iburst"] ++ ++ self._servers.append(server) ++ self._states.check_status(server) ++ + def input(self, args, key): + # we accept any string as NTP server address, as we do an automatic + # working/not-working check on the address later +- self.value = key ++ self._value = key + return InputState.DISCARDED + + def apply(self): +- if self._new_ntp_server: +- self._time_spoke.add_ntp_server(self._new_ntp_server) ++ pass + + + class RemoveNTPServerSpoke(NormalTUISpoke): + category = LocalizationCategory + +- def __init__(self, data, storage, payload, timezone_spoke): ++ def __init__(self, data, storage, payload, servers, states): + super().__init__(data, storage, payload) + self.title = N_("Select an NTP server to remove") +- self._time_spoke = timezone_spoke +- self._ntp_server_index = None ++ self._servers = servers ++ self._states = states ++ self._container = None + + @property + def indirect(self): + return True + +- def _summary_text(self): +- """Return a numbered listing of NTP servers.""" +- msg = "" +- for index, status in enumerate(format_ntp_status_list(self._time_spoke.ntp_servers), start=1): +- msg += "%d) %s" % (index, status) +- if index < len(self._time_spoke.ntp_servers): +- msg += "\n" +- return msg +- + def refresh(self, args=None): + super().refresh(args) +- summary = self._summary_text() +- self.window.add_with_separator(TextWidget(summary)) ++ self._container = ListColumnContainer(1) + +- def input(self, args, key): +- try: +- num = int(key) +- except ValueError: +- return super().input(args, key) ++ for server in self._servers: ++ description = ntp.get_ntp_server_summary( ++ server, self._states ++ ) + +- # we expect a number corresponding to one of the NTP servers +- # in the listing - the server corresponding to the number will be +- # removed from the NTP server tracking (ordered) dict +- if num > 0 and num <= len(self._time_spoke.ntp_servers): +- self._ntp_server_index = num - 1 +- self.apply() ++ self._container.add( ++ TextWidget(description), ++ self._remove_ntp_server, ++ server ++ ) ++ ++ self.window.add_with_separator(self._container) ++ ++ def _remove_ntp_server(self, server): ++ self._servers.remove(server) ++ ++ def input(self, args, key): ++ if self._container.process_user_input(key): + return InputState.PROCESSED_AND_CLOSE +- else: +- # the user enter a number that is out of range of the +- # available NTP servers, ignore it and stay in spoke +- return InputState.DISCARDED ++ ++ return super().input(args, key) + + def apply(self): +- if self._ntp_server_index is not None: +- ntp_server_address = list(self._time_spoke.ntp_servers.keys())[self._ntp_server_index] +- self._time_spoke.remove_ntp_server(ntp_server_address) ++ pass +-- +2.23.0 diff --git a/ntp-servers-improve-009-Use-the-structure-for-time-sources-in-GUI.patch b/ntp-servers-improve-009-Use-the-structure-for-time-sources-in-GUI.patch new file mode 100644 index 0000000..304e4d2 --- /dev/null +++ b/ntp-servers-improve-009-Use-the-structure-for-time-sources-in-GUI.patch @@ -0,0 +1,620 @@ +From 15d2b2fb568df2c1a77cfb2baa703ae9f3da0f30 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 13:38:10 +0200 +Subject: [PATCH] Use the structure for time sources in GUI + +Modify GUI to work with TimeSourceData instead of strings. +--- + pyanaconda/ui/gui/spokes/datetime_spoke.glade | 4 + + pyanaconda/ui/gui/spokes/datetime_spoke.py | 385 +++++++++--------- + 2 files changed, 187 insertions(+), 202 deletions(-) + +diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.glade b/pyanaconda/ui/gui/spokes/datetime_spoke.glade +index 37c7c6edc0..49e33776f5 100644 +--- a/pyanaconda/ui/gui/spokes/datetime_spoke.glade ++++ b/pyanaconda/ui/gui/spokes/datetime_spoke.glade +@@ -87,6 +87,8 @@ + + + ++ ++ + + + +@@ -242,6 +244,8 @@ + + True + ++ ++ + + + 0 +diff --git a/pyanaconda/ui/gui/spokes/datetime_spoke.py b/pyanaconda/ui/gui/spokes/datetime_spoke.py +index 00b1bd9d56..ea121e7e4d 100644 +--- a/pyanaconda/ui/gui/spokes/datetime_spoke.py ++++ b/pyanaconda/ui/gui/spokes/datetime_spoke.py +@@ -16,47 +16,48 @@ + # License and may only be used or replicated with the express permission of + # Red Hat, Inc. + # ++import datetime ++import re ++import time ++import locale as locale_mod ++import functools ++import copy + ++from pyanaconda import isys ++from pyanaconda import network ++from pyanaconda import ntp ++from pyanaconda import flags + from pyanaconda.anaconda_loggers import get_module_logger +-log = get_module_logger(__name__) +- +-import gi +-gi.require_version("Gdk", "3.0") +-gi.require_version("Gtk", "3.0") +-gi.require_version("TimezoneMap", "1.0") +- +-from gi.repository import Gdk, Gtk, TimezoneMap +- ++from pyanaconda.core import util, constants ++from pyanaconda.core.async_utils import async_action_wait, async_action_nowait ++from pyanaconda.core.configuration.anaconda import conf ++from pyanaconda.core.constants import TIME_SOURCE_POOL, TIME_SOURCE_SERVER ++from pyanaconda.core.i18n import _, CN_ ++from pyanaconda.core.timer import Timer ++from pyanaconda.localization import get_xlated_timezone, resolve_date_format ++from pyanaconda.modules.common.structures.timezone import TimeSourceData ++from pyanaconda.modules.common.constants.services import TIMEZONE, NETWORK ++from pyanaconda.ntp import NTPServerStatusCache + from pyanaconda.ui.communication import hubQ + from pyanaconda.ui.common import FirstbootSpokeMixIn + from pyanaconda.ui.gui import GUIObject + from pyanaconda.ui.gui.spokes import NormalSpoke + from pyanaconda.ui.categories.localization import LocalizationCategory +-from pyanaconda.ui.gui.utils import gtk_call_once, override_cell_property ++from pyanaconda.ui.gui.utils import override_cell_property + from pyanaconda.ui.gui.utils import blockedHandler + from pyanaconda.ui.gui.helpers import GUIDialogInputCheckHandler + from pyanaconda.ui.helpers import InputCheck +- +-from pyanaconda.core import util, constants +-from pyanaconda.core.configuration.anaconda import conf +-from pyanaconda import isys +-from pyanaconda import network +-from pyanaconda import ntp +-from pyanaconda import flags +-from pyanaconda.modules.common.constants.services import TIMEZONE, NETWORK +-from pyanaconda.threading import threadMgr, AnacondaThread +-from pyanaconda.core.i18n import _, CN_ +-from pyanaconda.core.async_utils import async_action_wait, async_action_nowait + from pyanaconda.timezone import NTP_SERVICE, get_all_regions_and_timezones, get_timezone, is_valid_timezone +-from pyanaconda.localization import get_xlated_timezone, resolve_date_format +-from pyanaconda.core.timer import Timer ++from pyanaconda.threading import threadMgr, AnacondaThread + +-import datetime +-import re +-import threading +-import time +-import locale as locale_mod +-import functools ++import gi ++gi.require_version("Gdk", "3.0") ++gi.require_version("Gtk", "3.0") ++gi.require_version("TimezoneMap", "1.0") ++ ++from gi.repository import Gdk, Gtk, TimezoneMap ++ ++log = get_module_logger(__name__) + + __all__ = ["DatetimeSpoke"] + +@@ -64,6 +65,7 @@ + SERVER_POOL = 1 + SERVER_WORKING = 2 + SERVER_USE = 3 ++SERVER_OBJECT = 4 + + DEFAULT_TZ = "Asia/Shanghai" + +@@ -156,97 +158,49 @@ def _new_date_field_box(store): + return (box, combo, suffix_label) + + +-class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): ++class NTPConfigDialog(GUIObject, GUIDialogInputCheckHandler): + builderObjects = ["ntpConfigDialog", "addImage", "serversStore"] + mainWidgetName = "ntpConfigDialog" + uiFile = "spokes/datetime_spoke.glade" + +- def __init__(self, data, timezone_module): ++ def __init__(self, data, servers, states): + GUIObject.__init__(self, data) ++ self._servers = servers ++ self._active_server = None ++ self._states = states + + # Use GUIDIalogInputCheckHandler to manipulate the sensitivity of the + # add button, and check for valid input in on_entry_activated + add_button = self.builder.get_object("addButton") + GUIDialogInputCheckHandler.__init__(self, add_button) + +- #epoch is increased when serversStore is repopulated +- self._epoch = 0 +- self._epoch_lock = threading.Lock() +- self._timezone_module = timezone_module +- +- @property +- def working_server(self): +- for row in self._serversStore: +- if row[SERVER_WORKING] == constants.NTP_SERVER_OK and row[SERVER_USE]: +- #server is checked and working +- return row[SERVER_HOSTNAME] +- +- return None +- +- @property +- def pools_servers(self): +- pools = list() +- servers = list() +- +- for used_row in (row for row in self._serversStore if row[SERVER_USE]): +- if used_row[SERVER_POOL]: +- pools.append(used_row[SERVER_HOSTNAME]) +- else: +- servers.append(used_row[SERVER_HOSTNAME]) +- +- return (pools, servers) +- +- def _render_working(self, column, renderer, model, itr, user_data=None): +- value = model[itr][SERVER_WORKING] +- +- if value == constants.NTP_SERVER_QUERY: +- return "dialog-question" +- elif value == constants.NTP_SERVER_OK: +- return "emblem-default" +- else: +- return "dialog-error" +- +- def initialize(self): + self.window.set_size_request(500, 400) + +- workingColumn = self.builder.get_object("workingColumn") +- workingRenderer = self.builder.get_object("workingRenderer") +- override_cell_property(workingColumn, workingRenderer, "icon-name", +- self._render_working) ++ working_column = self.builder.get_object("workingColumn") ++ working_renderer = self.builder.get_object("workingRenderer") ++ override_cell_property(working_column, working_renderer, "icon-name", self._render_working) + + self._serverEntry = self.builder.get_object("serverEntry") + self._serversStore = self.builder.get_object("serversStore") +- + self._addButton = self.builder.get_object("addButton") +- + self._poolCheckButton = self.builder.get_object("poolCheckButton") + +- # Validate the server entry box +- self._serverCheck = self.add_check(self._serverEntry, self._validateServer) ++ self._serverCheck = self.add_check(self._serverEntry, self._validate_server) + self._serverCheck.update_check_status() + +- self._initialize_store_from_config() +- +- def _initialize_store_from_config(self): +- self._serversStore.clear() ++ self._update_timer = Timer() + +- kickstart_ntp_servers = self._timezone_module.NTPServers ++ def _render_working(self, column, renderer, model, itr, user_data=None): ++ value = self._serversStore[itr][SERVER_WORKING] + +- if kickstart_ntp_servers: +- pools, servers = ntp.internal_to_pools_and_servers(kickstart_ntp_servers) ++ if value == constants.NTP_SERVER_QUERY: ++ return "dialog-question" ++ elif value == constants.NTP_SERVER_OK: ++ return "emblem-default" + else: +- try: +- pools, servers = ntp.get_servers_from_config() +- except ntp.NTPconfigError: +- log.warning("Failed to load NTP servers configuration") +- return +- +- for pool in pools: +- self._add_server(pool, True) +- for server in servers: +- self._add_server(server, False) ++ return "dialog-error" + +- def _validateServer(self, inputcheck): ++ def _validate_server(self, inputcheck): + server = self.get_input(inputcheck.input_obj) + + # If not set, fail the check to keep the button insensitive, but don't +@@ -261,108 +215,97 @@ def _validateServer(self, inputcheck): + return InputCheck.CHECK_OK + + def refresh(self): +- self._initialize_store_from_config() +- self._serverEntry.grab_focus() ++ # Update the store. ++ self._serversStore.clear() + +- def refresh_servers_state(self): +- itr = self._serversStore.get_iter_first() +- while itr: +- self._refresh_server_working(itr) +- itr = self._serversStore.iter_next(itr) ++ for server in self._servers: ++ self._add_row(server) ++ ++ # Start to update the status. ++ self._update_timer.timeout_sec(1, self._update_rows) ++ ++ # Focus on the server entry. ++ self._serverEntry.grab_focus() + + def run(self): + self.window.show() + rc = self.window.run() + self.window.hide() + +- #OK clicked ++ # OK clicked + if rc == 1: +- new_pools, new_servers = self.pools_servers ++ # Remove servers. ++ for row in self._serversStore: ++ if not row[SERVER_USE]: ++ server = row[SERVER_OBJECT] ++ self._servers.remove(server) + ++ # Restart the NTP service. + if conf.system.can_set_time_synchronization: +- ntp.save_servers_to_config(new_pools, new_servers) ++ ntp.save_servers_to_config(self._servers) + util.restart_service(NTP_SERVICE) + +- #Cancel clicked, window destroyed... +- else: +- self._epoch_lock.acquire() +- self._epoch += 1 +- self._epoch_lock.release() +- + return rc + +- def _set_server_ok_nok(self, itr, epoch_started): +- """ +- If the server is working, set its data to NTP_SERVER_OK, otherwise set its +- data to NTP_SERVER_NOK. +- +- :param itr: iterator of the $server's row in the self._serversStore ++ def _add_row(self, server): ++ """Add a new row for the given NTP server. + ++ :param server: an NTP server ++ :type server: an instance of TimeSourceData + """ ++ itr = self._serversStore.append([ ++ "", ++ False, ++ constants.NTP_SERVER_QUERY, ++ True, ++ server ++ ]) ++ ++ self._refresh_row(itr) ++ ++ def _refresh_row(self, itr): ++ """Refresh the given row.""" ++ server = self._serversStore[itr][SERVER_OBJECT] ++ self._serversStore.set_value(itr, SERVER_HOSTNAME, server.hostname) ++ self._serversStore.set_value(itr, SERVER_POOL, server.type == TIME_SOURCE_POOL) ++ ++ def _update_rows(self): ++ """Periodically update the status of all rows. ++ ++ :return: True to repeat, otherwise False ++ """ ++ for row in self._serversStore: ++ server = row[SERVER_OBJECT] + +- @async_action_nowait +- def set_store_value(arg_tuple): +- """ +- We need a function for this, because this way it can be added to +- the MainLoop with thread-safe async_action_nowait (but only with one +- argument). +- +- :param arg_tuple: (store, itr, column, value) +- +- """ +- +- (store, itr, column, value) = arg_tuple +- store.set_value(itr, column, value) +- +- orig_hostname = self._serversStore[itr][SERVER_HOSTNAME] +- server_working = ntp.ntp_server_working(self._serversStore[itr][SERVER_HOSTNAME]) +- +- #do not let dialog change epoch while we are modifying data +- self._epoch_lock.acquire() +- +- #check if we are in the same epoch as the dialog (and the serversStore) +- #and if the server wasn't changed meanwhile +- if epoch_started == self._epoch: +- actual_hostname = self._serversStore[itr][SERVER_HOSTNAME] ++ if server is self._active_server: ++ continue + +- if orig_hostname == actual_hostname: +- if server_working: +- set_store_value((self._serversStore, +- itr, SERVER_WORKING, constants.NTP_SERVER_OK)) +- else: +- set_store_value((self._serversStore, +- itr, SERVER_WORKING, constants.NTP_SERVER_NOK)) +- self._epoch_lock.release() ++ status = self._states.get_status(server) ++ row[SERVER_WORKING] = status + +- @async_action_nowait +- def _refresh_server_working(self, itr): +- """ Runs a new thread with _set_server_ok_nok(itr) as a taget. """ +- +- self._serversStore.set_value(itr, SERVER_WORKING, constants.NTP_SERVER_QUERY) +- threadMgr.add(AnacondaThread(prefix=constants.THREAD_NTP_SERVER_CHECK, +- target=self._set_server_ok_nok, +- args=(itr, self._epoch))) ++ return True + +- def _add_server(self, server, pool=False): +- """ +- Checks if a given server is a valid hostname and if yes, adds it +- to the list of servers. ++ def on_entry_activated(self, entry, *args): ++ # Check that the input check has passed ++ if self._serverCheck.check_status != InputCheck.CHECK_OK: ++ return + +- :param server: string containing hostname ++ server = TimeSourceData() + +- """ ++ if self._poolCheckButton.get_active(): ++ server.type = TIME_SOURCE_POOL ++ else: ++ server.type = TIME_SOURCE_SERVER + +- itr = self._serversStore.append([server, pool, constants.NTP_SERVER_QUERY, True]) ++ server.hostname = entry.get_text() ++ server.options = ["iburst"] + +- #do not block UI while starting thread (may take some time) +- self._refresh_server_working(itr) ++ self._servers.append(server) ++ self._states.check_status(server) ++ self._add_row(server) + +- def on_entry_activated(self, entry, *args): +- # Check that the input check has passed +- if self._serverCheck.check_status == InputCheck.CHECK_OK: +- self._add_server(entry.get_text(), self._poolCheckButton.get_active()) +- entry.set_text("") +- self._poolCheckButton.set_active(False) ++ entry.set_text("") ++ self._poolCheckButton.set_active(False) + + def on_add_clicked(self, *args): + self._serverEntry.emit("activate") +@@ -370,16 +313,29 @@ def on_add_clicked(self, *args): + def on_use_server_toggled(self, renderer, path, *args): + itr = self._serversStore.get_iter(path) + old_value = self._serversStore[itr][SERVER_USE] +- + self._serversStore.set_value(itr, SERVER_USE, not old_value) + + def on_pool_toggled(self, renderer, path, *args): + itr = self._serversStore.get_iter(path) +- old_value = self._serversStore[itr][SERVER_POOL] ++ server = self._serversStore[itr][SERVER_OBJECT] ++ ++ if server.type == TIME_SOURCE_SERVER: ++ server.type = TIME_SOURCE_POOL ++ else: ++ server.type = TIME_SOURCE_SERVER ++ ++ self._refresh_row(itr) ++ ++ def on_server_editing_started(self, renderer, editable, path): ++ itr = self._serversStore.get_iter(path) ++ self._active_server = self._serversStore[itr][SERVER_OBJECT] + +- self._serversStore.set_value(itr, SERVER_POOL, not old_value) ++ def on_server_editing_canceled(self, renderer): ++ self._active_server = None + + def on_server_edited(self, renderer, path, new_text, *args): ++ self._active_server = None ++ + if not path: + return + +@@ -389,14 +345,14 @@ def on_server_edited(self, renderer, path, new_text, *args): + return + + itr = self._serversStore.get_iter(path) ++ server = self._serversStore[itr][SERVER_OBJECT] + +- if self._serversStore[itr][SERVER_HOSTNAME] == new_text: ++ if server.hostname == new_text: + return + +- self._serversStore.set_value(itr, SERVER_HOSTNAME, new_text) +- self._serversStore.set_value(itr, SERVER_WORKING, constants.NTP_SERVER_QUERY) +- +- self._refresh_server_working(itr) ++ server.hostname = new_text ++ self._states.check_status(server) ++ self._refresh_row(itr) + + + class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): +@@ -440,6 +396,9 @@ def __init__(self, *args): + self._timezone_module = TIMEZONE.get_proxy() + self._network_module = NETWORK.get_proxy() + ++ self._ntp_servers = [] ++ self._ntp_servers_states = NTPServerStatusCache() ++ + def initialize(self): + NormalSpoke.initialize(self) + self.initialize_start() +@@ -512,9 +471,6 @@ def initialize(self): + if not conf.system.can_set_system_clock: + self._hide_date_time_setting() + +- self._config_dialog = NTPconfigDialog(self.data, self._timezone_module) +- self._config_dialog.initialize() +- + threadMgr.add(AnacondaThread(name=constants.THREAD_DATE_TIME, + target=self._initialize)) + +@@ -634,12 +590,27 @@ def refresh(self): + + self._update_datetime() + ++ # update the ntp configuration ++ self._ntp_servers = TimeSourceData.from_structure_list( ++ self._timezone_module.TimeSources ++ ) ++ ++ if not self._ntp_servers: ++ try: ++ self._ntp_servers = ntp.get_servers_from_config() ++ except ntp.NTPconfigError: ++ log.warning("Failed to load NTP servers configuration") ++ ++ self._ntp_servers_states = NTPServerStatusCache() + has_active_network = self._network_module.Connected ++ + if not has_active_network: + self._show_no_network_warning() + else: + self.clear_info() +- gtk_call_once(self._config_dialog.refresh_servers_state) ++ ++ for server in self._ntp_servers: ++ self._ntp_servers_states.check_status(server) + + if conf.system.can_set_time_synchronization: + ntp_working = has_active_network and util.service_running(NTP_SERVICE) +@@ -867,13 +838,10 @@ def _set_combo_selection(self, combo, item): + return False + + def _get_combo_selection(self, combo): +- """ +- Get the selected item of the combobox. ++ """Get the selected item of the combobox. + + :return: selected item or None +- + """ +- + model = combo.get_model() + itr = combo.get_active_iter() + if not itr or not model: +@@ -946,9 +914,7 @@ def on_updown_ampm_clicked(self, *args): + def on_region_changed(self, combo, *args): + """ + :see: on_city_changed +- + """ +- + region = self._get_active_region() + + if not region or region == self._old_region: +@@ -974,9 +940,7 @@ def on_city_changed(self, combo, *args): + hit etc.; 'London' chosen in the expanded combobox => update timezone + map and do all necessary actions). Fortunately when entry is being + edited, self._get_active_city returns None. +- + """ +- + timezone = None + + region = self._get_active_region() +@@ -1107,8 +1071,17 @@ def _set_date_time_setting_sensitive(self, sensitive): + footer_alignment = self.builder.get_object("footerAlignment") + footer_alignment.set_sensitive(sensitive) + ++ def _get_working_server(self): ++ """Get a working NTP server.""" ++ for server in self._ntp_servers: ++ status = self._ntp_servers_states.get_status(server) ++ if status == constants.NTP_SERVER_OK: ++ return server ++ ++ return None ++ + def _show_no_network_warning(self): +- self.set_warning(_("You need to set up networking first if you "\ ++ self.set_warning(_("You need to set up networking first if you " + "want to use NTP")) + + def _show_no_ntp_server_warning(self): +@@ -1127,13 +1100,13 @@ def on_ntp_switched(self, switch, *args): + return + else: + self.clear_info() ++ working_server = self._get_working_server() + +- working_server = self._config_dialog.working_server + if working_server is None: + self._show_no_ntp_server_warning() + else: +- #we need a one-time sync here, because chronyd would not change +- #the time as drastically as we need ++ # We need a one-time sync here, because chronyd would ++ # not change the time as drastically as we need. + ntp.one_time_sync_async(working_server) + + ret = util.start_service(NTP_SERVICE) +@@ -1161,16 +1134,24 @@ def on_ntp_switched(self, switch, *args): + self.clear_info() + + def on_ntp_config_clicked(self, *args): +- self._config_dialog.refresh() ++ servers = copy.deepcopy(self._ntp_servers) ++ states = self._ntp_servers_states + +- with self.main_window.enlightbox(self._config_dialog.window): +- response = self._config_dialog.run() ++ dialog = NTPConfigDialog(self.data, servers, states) ++ dialog.refresh() ++ ++ with self.main_window.enlightbox(dialog.window): ++ response = dialog.run() + + if response == 1: +- pools, servers = self._config_dialog.pools_servers +- self._timezone_module.SetNTPServers(ntp.pools_servers_to_internal(pools, servers)) ++ self._timezone_module.SetTimeSources( ++ TimeSourceData.to_structure_list(servers) ++ ) ++ ++ self._ntp_servers = servers ++ working_server = self._get_working_server() + +- if self._config_dialog.working_server is None: ++ if working_server is None: + self._show_no_ntp_server_warning() + else: + self.clear_info() +-- +2.23.0 diff --git a/ntp-servers-improve-010-Add-support-for-the-timesource-kickstart-command.patch b/ntp-servers-improve-010-Add-support-for-the-timesource-kickstart-command.patch new file mode 100644 index 0000000..b5fb862 --- /dev/null +++ b/ntp-servers-improve-010-Add-support-for-the-timesource-kickstart-command.patch @@ -0,0 +1,284 @@ +From 61fe3f12215bceebde71c35dc7ef14dbc17bb4d7 Mon Sep 17 00:00:00 2001 +From: Vendula Poncova +Date: Fri, 3 Jul 2020 18:29:33 +0200 +Subject: [PATCH] Add support for the timesource kickstart command + +The Timezone module should handle the timesource kickstart command. +--- + anaconda.spec.in | 2 +- + pyanaconda/core/kickstart/commands.py | 4 +- + pyanaconda/kickstart.py | 1 + + pyanaconda/modules/timezone/kickstart.py | 5 ++ + pyanaconda/modules/timezone/timezone.py | 69 +++++++++++++--- + .../pyanaconda_tests/module_timezone_test.py | 79 ++++++++++++++++++- + 6 files changed, 141 insertions(+), 19 deletions(-) + +diff --git a/anaconda.spec.in b/anaconda.spec.in +index 83adeb9089..c76181d363 100644 +--- a/anaconda.spec.in ++++ b/anaconda.spec.in +@@ -33,7 +33,7 @@ Source0: %{name}-%{version}.tar.bz2 + %define libxklavierver 5.4 + %define mehver 0.23-1 + %define nmver 1.0 +-%define pykickstartver 3.25-1 ++%define pykickstartver 3.27-1 + %define pypartedver 2.5-2 + %define rpmver 4.10.0 + %define simplelinever 1.1-1 +diff --git a/pyanaconda/core/kickstart/commands.py b/pyanaconda/core/kickstart/commands.py +index 590027dd33..3c3eed03e2 100644 +--- a/pyanaconda/core/kickstart/commands.py ++++ b/pyanaconda/core/kickstart/commands.py +@@ -76,7 +76,8 @@ + from pykickstart.commands.sshpw import F24_SshPw as SshPw + from pykickstart.commands.sshkey import F22_SshKey as SshKey + from pykickstart.commands.syspurpose import RHEL8_Syspurpose as Syspurpose +-from pykickstart.commands.timezone import F32_Timezone as Timezone ++from pykickstart.commands.timezone import F33_Timezone as Timezone ++from pykickstart.commands.timesource import F33_Timesource as Timesource + from pykickstart.commands.updates import F7_Updates as Updates + from pykickstart.commands.url import F30_Url as Url + from pykickstart.commands.user import F24_User as User +@@ -107,6 +108,7 @@ + from pykickstart.commands.snapshot import F26_SnapshotData as SnapshotData + from pykickstart.commands.sshpw import F24_SshPwData as SshPwData + from pykickstart.commands.sshkey import F22_SshKeyData as SshKeyData ++from pykickstart.commands.timesource import F33_TimesourceData as TimesourceData + from pykickstart.commands.user import F19_UserData as UserData + from pykickstart.commands.volgroup import F21_VolGroupData as VolGroupData + from pykickstart.commands.zfcp import F14_ZFCPData as ZFCPData +diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py +index d2fcaab44d..946da8bc95 100644 +--- a/pyanaconda/kickstart.py ++++ b/pyanaconda/kickstart.py +@@ -372,6 +372,7 @@ def finalize(self): + "sshkey" : UselessCommand, + "skipx": UselessCommand, + "snapshot": UselessCommand, ++ "timesource": UselessCommand, + "timezone": UselessCommand, + "url": UselessCommand, + "user": UselessCommand, +diff --git a/pyanaconda/modules/timezone/kickstart.py b/pyanaconda/modules/timezone/kickstart.py +index 7115322677..b94e4129c3 100644 +--- a/pyanaconda/modules/timezone/kickstart.py ++++ b/pyanaconda/modules/timezone/kickstart.py +@@ -24,4 +24,9 @@ class TimezoneKickstartSpecification(KickstartSpecification): + + commands = { + "timezone": COMMANDS.Timezone, ++ "timesource": COMMANDS.Timesource, ++ } ++ ++ commands_data = { ++ "TimesourceData": COMMANDS.TimesourceData, + } +diff --git a/pyanaconda/modules/timezone/timezone.py b/pyanaconda/modules/timezone/timezone.py +index ff89d1ea77..b7fd5b6430 100644 +--- a/pyanaconda/modules/timezone/timezone.py ++++ b/pyanaconda/modules/timezone/timezone.py +@@ -17,8 +17,11 @@ + # License and may only be used or replicated with the express permission of + # Red Hat, Inc. + # ++from pykickstart.errors import KickstartParseError ++ ++from pyanaconda.core.i18n import _ + from pyanaconda.core.configuration.anaconda import conf +-from pyanaconda.core.constants import TIME_SOURCE_SERVER ++from pyanaconda.core.constants import TIME_SOURCE_SERVER, TIME_SOURCE_POOL + from pyanaconda.core.dbus import DBus + from pyanaconda.core.signal import Signal + from pyanaconda.modules.common.base import KickstartService +@@ -73,29 +76,69 @@ def process_kickstart(self, data): + self.set_is_utc(data.timezone.isUtc) + self.set_ntp_enabled(not data.timezone.nontp) + +- servers = [] ++ sources = [] + + for hostname in data.timezone.ntpservers: +- server = TimeSourceData() +- server.type = TIME_SOURCE_SERVER +- server.hostname = hostname +- server.options = ["iburst"] +- servers.append(server) +- +- self.set_time_sources(servers) ++ source = TimeSourceData() ++ source.type = TIME_SOURCE_SERVER ++ source.hostname = hostname ++ source.options = ["iburst"] ++ sources.append(source) ++ ++ for source_data in data.timesource.dataList(): ++ if source_data.ntp_disable: ++ self.set_ntp_enabled(False) ++ continue ++ ++ source = TimeSourceData() ++ source.options = ["iburst"] ++ ++ if source_data.ntp_server: ++ source.type = TIME_SOURCE_SERVER ++ source.hostname = source_data.ntp_server ++ elif source_data.ntp_pool: ++ source.type = TIME_SOURCE_POOL ++ source.hostname = source_data.ntp_pool ++ else: ++ KickstartParseError( ++ _("Invalid time source."), ++ lineno=source_data.lineno ++ ) ++ ++ if source_data.nts: ++ source.options.append("nts") ++ ++ sources.append(source) ++ ++ self.set_time_sources(sources) + + def setup_kickstart(self, data): + """Set up the kickstart data.""" + data.timezone.timezone = self.timezone + data.timezone.isUtc = self.is_utc +- data.timezone.nontp = not self.ntp_enabled ++ source_data_list = data.timesource.dataList() + + if not self.ntp_enabled: ++ source_data = data.TimesourceData() ++ source_data.ntp_disable = True ++ source_data_list.append(source_data) + return + +- data.timezone.ntpservers = [ +- server.hostname for server in self.time_sources +- ] ++ for source in self.time_sources: ++ source_data = data.TimesourceData() ++ ++ if source.type == TIME_SOURCE_SERVER: ++ source_data.ntp_server = source.hostname ++ elif source.type == TIME_SOURCE_POOL: ++ source_data.ntp_pool = source.hostname ++ else: ++ log.warning("Skipping %s.", source) ++ continue ++ ++ if "nts" in source.options: ++ source_data.nts = True ++ ++ source_data_list.append(source_data) + + @property + def timezone(self): +diff --git a/tests/nosetests/pyanaconda_tests/module_timezone_test.py b/tests/nosetests/pyanaconda_tests/module_timezone_test.py +index bb751d6f4b..dab857e034 100644 +--- a/tests/nosetests/pyanaconda_tests/module_timezone_test.py ++++ b/tests/nosetests/pyanaconda_tests/module_timezone_test.py +@@ -65,7 +65,7 @@ def _check_dbus_property(self, *args, **kwargs): + + def kickstart_properties_test(self): + """Test kickstart properties.""" +- self.assertEqual(self.timezone_interface.KickstartCommands, ["timezone"]) ++ self.assertEqual(self.timezone_interface.KickstartCommands, ["timezone", "timesource"]) + self.assertEqual(self.timezone_interface.KickstartSections, []) + self.assertEqual(self.timezone_interface.KickstartAddons, []) + self.callback.assert_not_called() +@@ -143,19 +143,90 @@ def kickstart2_test(self): + timezone --utc --nontp Europe/Prague + """ + ks_out = """ ++ timesource --ntp-disable + # System timezone +- timezone Europe/Prague --utc --nontp ++ timezone Europe/Prague --utc + """ + self._test_kickstart(ks_in, ks_out) + + def kickstart3_test(self): +- """Test the timezone command with ntp servers..""" ++ """Test the timezone command with ntp servers.""" + ks_in = """ + timezone --ntpservers ntp.cesnet.cz Europe/Prague + """ + ks_out = """ ++ timesource --ntp-server=ntp.cesnet.cz + # System timezone +- timezone Europe/Prague --ntpservers=ntp.cesnet.cz ++ timezone Europe/Prague ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timesource_ntp_disabled_test(self): ++ """Test the timesource command with ntp disabled.""" ++ ks_in = """ ++ timesource --ntp-disable ++ """ ++ ks_out = """ ++ timesource --ntp-disable ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timesource_ntp_server_test(self): ++ """Test the timesource command with ntp servers.""" ++ ks_in = """ ++ timesource --ntp-server ntp.cesnet.cz ++ """ ++ ks_out = """ ++ timesource --ntp-server=ntp.cesnet.cz ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timesource_ntp_pool_test(self): ++ """Test the timesource command with ntp pools.""" ++ ks_in = """ ++ timesource --ntp-pool ntp.cesnet.cz ++ """ ++ ks_out = """ ++ timesource --ntp-pool=ntp.cesnet.cz ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timesource_nts_test(self): ++ """Test the timesource command with the nts option.""" ++ ks_in = """ ++ timesource --ntp-pool ntp.cesnet.cz --nts ++ """ ++ ks_out = """ ++ timesource --ntp-pool=ntp.cesnet.cz --nts ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timesource_all_test(self): ++ """Test the timesource commands.""" ++ ks_in = """ ++ timesource --ntp-server ntp.cesnet.cz ++ timesource --ntp-pool 0.fedora.pool.ntp.org ++ """ ++ ks_out = """ ++ timesource --ntp-server=ntp.cesnet.cz ++ timesource --ntp-pool=0.fedora.pool.ntp.org ++ """ ++ self._test_kickstart(ks_in, ks_out) ++ ++ def kickstart_timezone_timesource_test(self): ++ """Test the combination of timezone and timesource commands.""" ++ ks_in = """ ++ timezone --ntpservers ntp.cesnet.cz,0.fedora.pool.ntp.org Europe/Prague ++ timesource --ntp-server ntp.cesnet.cz --nts ++ timesource --ntp-pool 0.fedora.pool.ntp.org ++ """ ++ ks_out = """ ++ timesource --ntp-server=ntp.cesnet.cz ++ timesource --ntp-server=0.fedora.pool.ntp.org ++ timesource --ntp-server=ntp.cesnet.cz --nts ++ timesource --ntp-pool=0.fedora.pool.ntp.org ++ # System timezone ++ timezone Europe/Prague + """ + self._test_kickstart(ks_in, ks_out) + +-- +2.23.0