# Timezone text spoke # # Copyright (C) 2012 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. # 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.modules.common.util import is_module_available 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 from pyanaconda import timezone from pyanaconda import ntp from pyanaconda.core import constants from pyanaconda.core.i18n import N_, _ from pyanaconda.flags import flags from collections import namedtuple from simpleline.render.containers import ListColumnContainer from simpleline.render.screen import InputState from simpleline.render.widgets import TextWidget from simpleline.render.screen_handler import ScreenHandler from simpleline.render.prompt import Prompt from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) __all__ = ["TimeSpoke"] # TRANSLATORS: 'b' to go back to region list PROMPT_BACK_DESCRIPTION = N_("to go back to region list") PROMPT_BACK_KEY = 'b' CallbackTimezoneArgs = namedtuple("CallbackTimezoneArgs", ["region", "timezone"]) class TimeSpoke(FirstbootSpokeMixIn, NormalTUISpoke): category = LocalizationCategory @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "date-time-configuration" @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(TIMEZONE): return False return FirstbootSpokeMixIn.should_run(environment, data) def __init__(self, data, storage, payload): NormalTUISpoke.__init__(self, data, storage, payload) self.title = N_("Time settings") self._timezone_spoke = None self._container = None self._ntp_servers = [] self._ntp_servers_states = NTPServerStatusCache() self._timezone_module = TIMEZONE.get_proxy() @property def indirect(self): return False def initialize(self): self.initialize_start() # We get the initial NTP servers (if any): # - from kickstart when running inside of Anaconda # during the installation # - from config files when running in Initial Setup # after the installation if constants.ANACONDA_ENVIRON in flags.environs: self._ntp_servers = TimeSourceData.from_structure_list( self._timezone_module.TimeSources ) elif constants.FIRSTBOOT_ENVIRON in flags.environs: 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 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() @property def timezone_spoke(self): if not self._timezone_spoke: self._timezone_spoke = TimeZoneSpoke(self.data, self.storage, self.payload) return self._timezone_spoke @property def completed(self): return bool(self._timezone_module.Timezone) @property def mandatory(self): return True @property def status(self): kickstart_timezone = self._timezone_module.Timezone if kickstart_timezone: return _("%s timezone") % kickstart_timezone else: return _("Timezone is not set.") def _summary_text(self): """Return summary of current timezone & NTP configuration. :returns: current status :rtype: str """ msg = "" # timezone kickstart_timezone = self._timezone_module.Timezone timezone_msg = _("not set") if kickstart_timezone: timezone_msg = kickstart_timezone msg += _("Timezone: %s\n") % timezone_msg # newline section separator msg += "\n" # NTP msg += ntp.get_ntp_servers_summary( self._ntp_servers, self._ntp_servers_states ) return msg def refresh(self, args=None): NormalTUISpoke.refresh(self, args) summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) if self._timezone_module.Timezone: timezone_option = _("Change timezone") else: timezone_option = _("Set timezone") 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.window.add_with_separator(self._container) def _timezone_callback(self, data): ScreenHandler.push_screen_modal(self.timezone_spoke) self.close() def _configure_ntp_server_callback(self, data): 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() def input(self, args, key): """ Handle the input - visit a sub spoke or go back to hub.""" if self._container.process_user_input(key): return InputState.PROCESSED else: return super().input(args, key) def apply(self): # update the NTP server list in kickstart self._timezone_module.TimeSources = \ TimeSourceData.to_structure_list(self._ntp_servers) class TimeZoneSpoke(NormalTUISpoke): """ .. inheritance-diagram:: TimeZoneSpoke :parts: 3 """ category = LocalizationCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Timezone settings") self._container = None # it's stupid to call get_all_regions_and_timezones twice, but regions # needs to be unsorted in order to display in the same order as the GUI # so whatever self._regions = list(timezone.get_all_regions_and_timezones().keys()) self._timezones = dict((k, sorted(v)) for k, v in timezone.get_all_regions_and_timezones().items()) self._lower_regions = [r.lower() for r in self._regions] self._zones = ["%s/%s" % (region, z) for region in self._timezones for z in self._timezones[region]] # for lowercase lookup self._lower_zones = [z.lower().replace("_", " ") for region in self._timezones for z in self._timezones[region]] self._selection = "" self._timezone_module = TIMEZONE.get_proxy() @property def indirect(self): return True def refresh(self, args=None): """args is None if we want a list of zones or "zone" to show all timezones in that zone.""" super().refresh(args) self._container = ListColumnContainer(3, columns_width=24) if args and args in self._timezones: self.window.add(TextWidget(_("Available timezones in region %s") % args)) for tz in self._timezones[args]: self._container.add(TextWidget(tz), self._select_timezone_callback, CallbackTimezoneArgs(args, tz)) else: self.window.add(TextWidget(_("Available regions"))) for region in self._regions: self._container.add(TextWidget(region), self._select_region_callback, region) self.window.add_with_separator(self._container) def _select_timezone_callback(self, data): self._selection = "%s/%s" % (data.region, data.timezone) self.apply() self.close() def _select_region_callback(self, data): region = data selected_timezones = self._timezones[region] if len(selected_timezones) == 1: self._selection = "%s/%s" % (region, selected_timezones[0]) self.apply() self.close() else: ScreenHandler.replace_screen(self, region) def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED else: if key.lower().replace("_", " ") in self._lower_zones: index = self._lower_zones.index(key.lower().replace("_", " ")) self._selection = self._zones[index] self.apply() return InputState.PROCESSED_AND_CLOSE elif key.lower() in self._lower_regions: index = self._lower_regions.index(key.lower()) if len(self._timezones[self._regions[index]]) == 1: self._selection = "%s/%s" % (self._regions[index], self._timezones[self._regions[index]][0]) self.apply() self.close() else: ScreenHandler.replace_screen(self, self._regions[index]) return InputState.PROCESSED elif key.lower() == PROMPT_BACK_KEY: ScreenHandler.replace_screen(self) return InputState.PROCESSED else: return key def prompt(self, args=None): """ Customize default prompt. """ prompt = NormalTUISpoke.prompt(self, args) prompt.set_message(_("Please select the timezone. Use numbers or type names directly")) prompt.add_option(PROMPT_BACK_KEY, _(PROMPT_BACK_DESCRIPTION)) return prompt def apply(self): self._timezone_module.SetTimezoneWithPriority( self._selection, constants.TIMEZONE_PRIORITY_USER ) self._timezone_module.Kickstarted = False class NTPServersSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, servers, states): super().__init__(data, storage, payload) self.title = N_("NTP configuration") self._container = None self._servers = servers self._states = states @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) 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._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._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._servers, self._states ) ScreenHandler.push_screen_modal(new_spoke) self.redraw() def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED else: return super().input(args, key) def apply(self): pass class AddNTPServerSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, servers, states): super().__init__(data, storage, payload) self.title = N_("Add NTP server address") self._servers = servers self._states = states self._value = None @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) 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 return Prompt(_("Enter an NTP server address and press %s") % Prompt.ENTER) # an NTP server address has been entered self._add_ntp_server(self._value) 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 return InputState.DISCARDED def apply(self): pass class RemoveNTPServerSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, servers, states): super().__init__(data, storage, payload) self.title = N_("Select an NTP server to remove") self._servers = servers self._states = states self._container = None @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for server in self._servers: description = ntp.get_ntp_server_summary( server, self._states ) 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 return super().input(args, key) def apply(self): pass