470 lines
15 KiB
Python
470 lines
15 KiB
Python
# 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
|