# # Kickstart module for date and time settings. # # Copyright (C) 2018 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. # import datetime 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, TIME_SOURCE_POOL, \ TIMEZONE_PRIORITY_USER, TIMEZONE_PRIORITY_DEFAULT, TIMEZONE_PRIORITY_KICKSTART 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, GeolocationData from pyanaconda.timezone import NTP_PACKAGE from pyanaconda.modules.common.containers import TaskContainer from pyanaconda.modules.common.structures.requirement import Requirement from pyanaconda.modules.timezone.initialization import GeolocationTask from pyanaconda.modules.timezone.installation import ConfigureHardwareClockTask, \ ConfigureNTPTask, ConfigureTimezoneTask from pyanaconda.modules.timezone.kickstart import TimezoneKickstartSpecification from pyanaconda.modules.timezone.timezone_interface import TimezoneInterface from pyanaconda.timezone import get_timezone, set_system_date_time, get_all_regions_and_timezones from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) class TimezoneService(KickstartService): """The Timezone service.""" def __init__(self): super().__init__() self.timezone_changed = Signal() self._timezone = "America/New_York" self._priority = TIMEZONE_PRIORITY_DEFAULT self.geolocation_result_changed = Signal() self._geoloc_result = GeolocationData() self.is_utc_changed = Signal() self._is_utc = False self.ntp_enabled_changed = Signal() self._ntp_enabled = True self.time_sources_changed = Signal() self._time_sources = [] def publish(self): """Publish the module.""" TaskContainer.set_namespace(TIMEZONE.namespace) DBus.publish_object(TIMEZONE.object_path, TimezoneInterface(self)) DBus.register_service(TIMEZONE.service_name) @property def kickstart_specification(self): """Return the kickstart specification.""" return TimezoneKickstartSpecification def process_kickstart(self, data): """Process the kickstart data.""" self.set_timezone_with_priority(data.timezone.timezone, TIMEZONE_PRIORITY_KICKSTART) self.set_is_utc(data.timezone.isUtc) sources = [] 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: raise 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 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 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): """Return the timezone.""" return self._timezone def set_timezone(self, timezone): """Set the timezone.""" self.set_timezone_with_priority(timezone, TIMEZONE_PRIORITY_USER) def set_timezone_with_priority(self, timezone, priority): """Set the timezone with priority. Sets the timezone only if the priority is higher than the previous priority. """ if priority < self._priority: log.debug("Timezone did not change %s -> %s due to too low priority: %d > %d.", self._timezone, timezone, self._priority, priority) return self._timezone = timezone self._priority = priority self.timezone_changed.emit() log.debug("Timezone is set to %s.", timezone) def get_all_valid_timezones(self): """Get all valid timezones. :return: list of valid timezones :rtype: list of str """ timezone_dict = get_all_regions_and_timezones() # convert to a dict of lists for easier transfer over DBus # - change the nested sets to lists new_timezone_dict = {} for region in timezone_dict: new_timezone_dict[region] = list(timezone_dict[region]) return new_timezone_dict @property def is_utc(self): """Is the hardware clock set to UTC?""" return self._is_utc def set_is_utc(self, is_utc): """Set if the hardware clock is set to UTC.""" self._is_utc = is_utc self.is_utc_changed.emit() log.debug("UTC is set to %s.", is_utc) @property def ntp_enabled(self): """Enable automatic starting of NTP service.""" return self._ntp_enabled def set_ntp_enabled(self, ntp_enabled): """Enable or disable automatic starting of NTP service.""" self._ntp_enabled = ntp_enabled self.ntp_enabled_changed.emit() log.debug("NTP is set to %s.", ntp_enabled) @property def time_sources(self): """Return a list of time sources.""" return self._time_sources 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. :return: a list of requirements """ requirements = [] # Add ntp service requirements. if self._ntp_enabled: requirements.append( Requirement.for_package(NTP_PACKAGE, reason="Needed to run NTP service.") ) return requirements def install_with_tasks(self): """Return the installation tasks of this module. :return: list of installation tasks """ return [ ConfigureHardwareClockTask( is_utc=self.is_utc ), ConfigureTimezoneTask( sysroot=conf.target.system_root, timezone=self.timezone, is_utc=self.is_utc ), ConfigureNTPTask( sysroot=conf.target.system_root, ntp_enabled=self.ntp_enabled, ntp_servers=self.time_sources ) ] def _set_geolocation_result(self, result): """Set geolocation result when the task finished.""" self._geoloc_result = result self.geolocation_result_changed.emit() log.debug("Geolocation result is set, valid=%s", not self._geoloc_result.is_empty()) def start_geolocation_with_task(self): """Start geolocation. :return: task to run for geolocation :rtype: Task """ task = GeolocationTask() task.succeeded_signal.connect(lambda: self._set_geolocation_result(task.get_result())) return task @property def geolocation_result(self): """Get geolocation result. :return GeolocationData: result of the lookup, empty if not ready yet """ return self._geoloc_result def get_system_date_time(self): """Get system time as a ISO 8601 formatted string. :return: system time as ISO 8601 formatted string :rtype: str """ # convert to the expected tzinfo format via get_timezone() return datetime.datetime.now(get_timezone(self._timezone)).isoformat() def set_system_date_time(self, date_time_spec): """Set system time based on a ISO 8601 formatted string. :param str date_time_spec: ISO 8601 time specification to use """ log.debug("Setting system time to: %s, with timezone: %s", date_time_spec, self._timezone) # first convert the ISO 8601 time string to a Python date object date = datetime.datetime.fromisoformat(date_time_spec) # set the date to the system set_system_date_time( year=date.year, month=date.month, day=date.day, hour=date.hour, minute=date.minute, tz=self._timezone )