class SpecifyRepoSpoke(NormalTUISpoke, SourceSwitchHandler): """ Specify the repo URL here if closest mirror not selected. """ category = SoftwareCategory HTTP = 1 HTTPS = 2 FTP = 3 def __init__(self, data, storage, payload, instclass, protocol): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Specify Repo Options") self.protocol = protocol self._container = None self._url = self.data.url.url def refresh(self, args=None): """ Refresh window. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) dialog = Dialog(_("Repo URL")) self._container.add(EntryWidget(dialog.title, self.data.method.url), self._set_repo_url, dialog) self.window.add_with_separator(self._container) def _set_repo_url(self, dialog): self._url = dialog.run() def input(self, args, key): if self._container.process_user_input(key): self.apply() self.redraw() return InputState.PROCESSED else: return NormalTUISpoke.input(self, args, key) @property def indirect(self): return True def apply(self): """ Apply all of our changes. """ if self.protocol == SpecifyRepoSpoke.HTTP and not self._url.startswith( "http://"): url = "http://" + self._url elif self.protocol == SpecifyRepoSpoke.HTTPS and not self._url.startswith( "https://"): url = "https://" + self._url elif self.protocol == SpecifyRepoSpoke.FTP and not self._url.startswith( "ftp://"): url = "ftp://" + self._url else: # protocol either unknown or entry already starts with a protocol # specification url = self._url self.set_source_url(url)
class NTPServersSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, instclass, time_spoke): super().__init__(data, storage, payload, instclass) self.title = N_("NTP configuration") self._container = None self._time_spoke = time_spoke @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() 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: 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.instclass, self._time_spoke) ScreenHandler.push_screen_modal(new_spoke) self.redraw() def _remove_ntp_server(self, data): new_spoke = RemoveNTPServerSpoke(self.data, self.storage, self.payload, self.instclass, self._time_spoke) 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 PartitionSchemeSpoke(NormalTUISpoke): """ Spoke to select what partitioning scheme to use on disk(s). """ category = SystemCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Partition Scheme Options") self._container = None self.part_schemes = OrderedDict() self._auto_part_proxy = STORAGE.get_proxy(AUTO_PARTITIONING) pre_select = self._auto_part_proxy.Type supported_choices = get_supported_autopart_choices() if supported_choices: # Fallback value (eg when default is not supported) self._selected_scheme_value = supported_choices[0][1] for item in supported_choices: self.part_schemes[item[0]] = item[1] if item[1] == pre_select: self._selected_scheme_value = item[1] @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for scheme, value in self.part_schemes.items(): box = CheckboxWidget(title=_(scheme), completed=(value == self._selected_scheme_value)) self._container.add(box, self._set_part_scheme_callback, value) self.window.add_with_separator(self._container) message = _("Select a partition scheme configuration.") self.window.add_with_separator(TextWidget(message)) def _set_part_scheme_callback(self, data): self._selected_scheme_value = data def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) return InputState.PROCESSED_AND_REDRAW def apply(self): """ Apply our selections. """ self._auto_part_proxy.SetType(self._selected_scheme_value)
class PartitionSchemeSpoke(NormalTUISpoke): """ Spoke to select what partitioning scheme to use on disk(s). """ category = SystemCategory def __init__(self, data, storage, payload, instclass): super().__init__(data, storage, payload, instclass) self.title = N_("Partition Scheme Options") self._container = None self.part_schemes = OrderedDict() self._auto_part_proxy = STORAGE.get_proxy(AUTO_PARTITIONING) pre_select = self._auto_part_proxy.Type if pre_select == AUTOPART_TYPE_DEFAULT: pre_select = DEFAULT_AUTOPART_TYPE for item in AUTOPART_CHOICES: self.part_schemes[item[0]] = item[1] if item[1] == pre_select: self._selected_scheme_value = item[1] @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for scheme, value in self.part_schemes.items(): box = CheckboxWidget( title=_(scheme), completed=(value == self._selected_scheme_value)) self._container.add(box, self._set_part_scheme_callback, value) self.window.add_with_separator(self._container) message = _("Select a partition scheme configuration.") self.window.add_with_separator(TextWidget(message)) def _set_part_scheme_callback(self, data): self._selected_scheme_value = data def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) return InputState.PROCESSED_AND_REDRAW def apply(self): """ Apply our selections. """ self._auto_part_proxy.SetType(self._selected_scheme_value)
class NTPServersSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, time_spoke): super().__init__(data, storage, payload) self.title = N_("NTP configuration") self._container = None self._time_spoke = time_spoke @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() 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: 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) 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) 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 RootSelectionSpoke(NormalTUISpoke): """UI for selection of installed system root to be mounted.""" def __init__(self, roots): super().__init__(data=None, storage=None, payload=None) self.title = N_("Root Selection") self._roots = roots self._selection = roots[0] self._container = None @property def selection(self): """The selected root fs to mount.""" return self._selection @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for root in self._roots: box = CheckboxWidget( title="{} on {}".format(root.name, root.device.path), completed=(self._selection == root) ) self._container.add(box, self._select_root, root) message = _("The following installations were discovered on your system.") self.window.add_with_separator(TextWidget(message)) self.window.add_with_separator(self._container) def _select_root(self, root): self._selection = root def prompt(self, args=None): """ Override the default TUI prompt.""" prompt = Prompt() prompt.add_continue_option() return prompt def input(self, args, key): """Override any input so we can launch rescue mode.""" if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW elif key == Prompt.CONTINUE: return InputState.PROCESSED_AND_CLOSE else: return key def apply(self): """Define the abstract method.""" pass
class AdditionalSoftwareSpoke(NormalTUISpoke): """The spoke for choosing the additional software.""" category = SoftwareCategory def __init__(self, data, storage, payload, selection_cache): super().__init__(data, storage, payload) self.title = N_("Software selection") self._container = None self._selection_cache = selection_cache @property def _dnf_manager(self): """The DNF manager.""" return self.payload.dnf_manager def refresh(self, args=None): """Refresh the screen.""" NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(columns=2, columns_width=38, spacing=2) for group in self._selection_cache.available_groups: data = self._dnf_manager.get_group_data(group) selected = self._selection_cache.is_group_selected(group) widget = CheckboxWidget(title=data.name, completed=selected) self._container.add(widget, callback=self._select_group, data=data.id) if self._selection_cache.available_groups: msg = _("Additional software for selected environment") else: msg = _("No additional software to select.") self.window.add_with_separator(TextWidget(msg)) self.window.add_with_separator(self._container) def _select_group(self, group): if not self._selection_cache.is_group_selected(group): self._selection_cache.select_group(group) else: self._selection_cache.deselect_group(group) def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW else: return super().input(args, key) def apply(self): pass
class SpecifyRepoSpoke(NormalTUISpoke, SourceSwitchHandler): """ Specify the repo URL here if closest mirror not selected. """ category = SoftwareCategory HTTP = 1 HTTPS = 2 FTP = 3 def __init__(self, data, storage, payload, instclass, protocol): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Specify Repo Options") self.protocol = protocol self._container = None self._url = self.data.url.url def refresh(self, args=None): """ Refresh window. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) dialog = Dialog(_("Repo URL")) self._container.add(EntryWidget(dialog.title, self._url), self._set_repo_url, dialog) self.window.add_with_separator(self._container) def _set_repo_url(self, dialog): self._url = dialog.run() def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return NormalTUISpoke.input(self, args, key) @property def indirect(self): return True def apply(self): """ Apply all of our changes. """ if self.protocol == SpecifyRepoSpoke.HTTP and not self._url.startswith("http://"): url = "http://" + self._url elif self.protocol == SpecifyRepoSpoke.HTTPS and not self._url.startswith("https://"): url = "https://" + self._url elif self.protocol == SpecifyRepoSpoke.FTP and not self._url.startswith("ftp://"): url = "ftp://" + self._url else: # protocol either unknown or entry already starts with a protocol # specification url = self._url self.set_source_url(url)
class PartitionSchemeSpoke(NormalTUISpoke): """ Spoke to select what partitioning scheme to use on disk(s). """ category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Partition Scheme Options") self._container = None self.part_schemes = OrderedDict() pre_select = self.data.autopart.type or DEFAULT_AUTOPART_TYPE for item in AUTOPART_CHOICES: self.part_schemes[item[0]] = item[1] if item[1] == pre_select: self._selected_scheme_value = item[1] @property def indirect(self): return True def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) for scheme, value in self.part_schemes.items(): box = CheckboxWidget(title=_(scheme), completed=(value == self._selected_scheme_value)) self._container.add(box, self._set_part_scheme_callback, value) self.window.add_with_separator(self._container) message = _("Select a partition scheme configuration.") self.window.add_with_separator(TextWidget(message)) def _set_part_scheme_callback(self, data): self._selected_scheme_value = data def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() self.close() return InputState.PROCESSED else: return super(PartitionSchemeSpoke, self).input(args, key) self.redraw() return InputState.PROCESSED def apply(self): """ Apply our selections. """ self.data.autopart.type = self._selected_scheme_value
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 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
class TimeZoneSpoke(NormalTUISpoke): """ .. inheritance-diagram:: TimeZoneSpoke :parts: 3 """ category = LocalizationCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) 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 = "" @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.""" NormalTUISpoke.refresh(self, 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() self.close() return InputState.PROCESSED 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 # TRANSLATORS: 'b' to go back elif key.lower() == C_('TUI|Spoke Navigation|Time Settings', 'b'): 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") ) # TRANSLATORS: 'b' to go back prompt.add_option(C_('TUI|Spoke Navigation|Time Settings', 'b'), _("back to region list")) return prompt def apply(self): self.data.timezone.timezone = self._selection self.data.timezone.seen = False
class PartTypeSpoke(NormalTUISpoke): """ Partitioning options are presented here. .. inheritance-diagram:: PartTypeSpoke :parts: 3 """ category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Partitioning Options") self._container = None self.parttypelist = sorted(PARTTYPES.keys()) # remember the original values so that we can detect a change self._orig_clearpart_type = self.data.clearpart.type self._orig_mount_assign = len(self.data.mount.dataList()) != 0 # default to mount point assignment if it is already (partially) # configured self._do_mount_assign = self._orig_mount_assign if not self._do_mount_assign: self.clearPartType = self.data.clearpart.type or CLEARPART_TYPE_ALL else: self.clearPartType = CLEARPART_TYPE_NONE @property def indirect(self): return True def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) for part_type in self.parttypelist: c = CheckboxWidget( title=_(part_type), completed=(not self._do_mount_assign and PARTTYPES[part_type] == self.clearPartType)) self._container.add(c, self._select_partition_type_callback, part_type) c = CheckboxWidget(title=_("Manually assign mount points") + _(" (EXPERIMENTAL)"), completed=self._do_mount_assign) self._container.add(c, self._select_mount_assign) self.window.add_with_separator(self._container) message = _( "Installation requires partitioning of your hard drive. " "Select what space to use for the install target or manually assign mount points." ) self.window.add_with_separator(TextWidget(message)) def _select_mount_assign(self, data=None): self.clearPartType = CLEARPART_TYPE_NONE self._do_mount_assign = True self.apply() def _select_partition_type_callback(self, data): self._do_mount_assign = False self.clearPartType = PARTTYPES[data] self.apply() def apply(self): # kind of a hack, but if we're actually getting to this spoke, there # is no doubt that we are doing autopartitioning, so set autopart to # True. In the case of ks installs which may not have defined any # partition options, autopart was never set to True, causing some # issues. (rhbz#1001061) if not self._do_mount_assign: self.data.autopart.autopart = True self.data.clearpart.type = self.clearPartType self.data.clearpart.initAll = True self.data.mount.clear_mount_data() else: self.data.autopart.autopart = False self.data.clearpart.type = CLEARPART_TYPE_NONE self.data.clearpart.initAll = False def _ensure_init_storage(self): """ If a different clearpart type was chosen or mount point assignment was chosen instead, we need to reset/rescan storage to revert all changes done by the previous run of doKickstartStorage() and get everything into the initial state. """ # the only safe options are: # 1) if nothing was set before (self._orig_clearpart_type is None) or # 2) mount point assignment was done before and user just wants to tweak it if self._orig_clearpart_type is None or (self._orig_mount_assign and self._do_mount_assign): return # else print(_("Reverting previous configuration. This may take a moment...")) # unset self.data.ignoredisk.onlyuse temporarily so that # storage_initialize() processes all devices ignoredisk = self.data.ignoredisk.onlyuse self.data.ignoredisk.onlyuse = [] storage_initialize(self.storage, self.data, self.storage.devicetree.protected_dev_names) self.data.ignoredisk.onlyuse = ignoredisk self.data.mount.clear_mount_data() def input(self, args, key): """Grab the choice and update things""" if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() self._ensure_init_storage() if self._do_mount_assign: new_spoke = MountPointAssignSpoke(self.data, self.storage, self.payload, self.instclass) else: new_spoke = PartitionSchemeSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.close() return InputState.PROCESSED else: return super(PartTypeSpoke, self).input(args, key) self.redraw() return InputState.PROCESSED
class StorageSpoke(NormalTUISpoke): """Storage spoke where users proceed to customize storage features such as disk selection, partitioning, and fs type. .. inheritance-diagram:: StorageSpoke :parts: 3 """ helpFile = "StorageSpoke.txt" category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Installation Destination") self._ready = False self._container = None self.selected_disks = self.data.ignoredisk.onlyuse[:] self.select_all = False self.autopart = None # This list gets set up once in initialize and should not be modified # except perhaps to add advanced devices. It will remain the full list # of disks that can be included in the install. self.disks = [] self.errors = [] self.warnings = [] if self.data.zerombr.zerombr and arch.is_s390(): # if zerombr is specified in a ks file and there are unformatted # dasds, automatically format them. pass in storage.devicetree here # instead of storage.disks since media_present is checked on disks; # a dasd needing dasdfmt will fail this media check though to_format = [ d for d in getDisks(self.storage.devicetree) if d.type == "dasd" and blockdev.s390.dasd_needs_format(d.busid) ] if to_format: self.run_dasdfmt(to_format) if not flags.automatedInstall: # default to using autopart for interactive installs self.data.autopart.autopart = True @property def completed(self): retval = bool(self.storage.root_device and not self.errors) return retval @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready and not threadMgr.get(THREAD_STORAGE_WATCHER) @property def mandatory(self): return True @property def showable(self): return not flags.dirInstall @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") if flags.automatedInstall and not self.storage.root_device: msg = _("Kickstart insufficient") elif self.data.ignoredisk.onlyuse: msg = P_(("%d disk selected"), ("%d disks selected"), len(self.data.ignoredisk.onlyuse)) % len( self.data.ignoredisk.onlyuse) if self.errors: msg = _("Error checking storage configuration") elif self.warnings: msg = _("Warning checking storage configuration") # Maybe show what type of clearpart and which disks selected? elif self.data.autopart.autopart: msg = _("Automatic partitioning selected") else: msg = _("Custom partitioning selected") return msg def _update_disk_list(self, disk): """ Update self.selected_disks based on the selection.""" name = disk.name # if the disk isn't already selected, select it. if name not in self.selected_disks: self.selected_disks.append(name) # If the disk is already selected, deselect it. elif name in self.selected_disks: self.selected_disks.remove(name) def _update_summary(self): """ Update the summary based on the UI. """ count = 0 capacity = 0 free = Size(0) # pass in our disk list so hidden disks' free space is available free_space = self.storage.get_free_space(disks=self.disks) selected = [d for d in self.disks if d.name in self.selected_disks] for disk in selected: capacity += disk.size free += free_space[disk.name][0] count += 1 summary = (P_(("%d disk selected; %s capacity; %s free ..."), ("%d disks selected; %s capacity; %s free ..."), count) % (count, str(Size(capacity)), free)) if len(self.disks) == 0: summary = _( "No disks detected. Please shut down the computer, connect at least one disk, and restart to complete installation." ) elif count == 0: summary = (_( "No disks selected; please select at least one disk to install to." )) # Append storage errors to the summary if self.errors: summary = summary + "\n" + "\n".join(self.errors) elif self.warnings: summary = summary + "\n" + "\n".join(self.warnings) return summary def refresh(self, args=None): NormalTUISpoke.refresh(self, args) # Join the initialization thread to block on it # This print is foul. Need a better message display print(_(PAYLOAD_STATUS_PROBING_STORAGE)) threadMgr.wait(THREAD_STORAGE_WATCHER) if not any(d in self.storage.disks for d in self.disks): # something happened to self.storage (probably reset), need to # reinitialize the list of disks self._initialize(reinit=True) # synchronize our local data store with the global ksdata # Commment out because there is no way to select a disk right # now without putting it in ksdata. Seems wrong? #self.selected_disks = self.data.ignoredisk.onlyuse[:] self.autopart = self.data.autopart.autopart self._container = ListColumnContainer(1, spacing=1) message = self._update_summary() # loop through the disks and present them. for disk in self.disks: disk_info = self._format_disk_info(disk) c = CheckboxWidget(title=disk_info, completed=(disk.name in self.selected_disks)) self._container.add(c, self._update_disk_list_callback, disk) # if we have more than one disk, present an option to just # select all disks if len(self.disks) > 1: c = CheckboxWidget(title=_("Select all"), completed=self.select_all) self._container.add(c, self._select_all_disks_callback) self.window.add_with_separator(self._container) self.window.add_with_separator(TextWidget(message)) def _select_all_disks_callback(self, data): """ Mark all disks as selected for use in partitioning. """ self.select_all = True for disk in self.disks: if disk.name not in self.selected_disks: self._update_disk_list(disk) def _update_disk_list_callback(self, data): disk = data self.select_all = False self._update_disk_list(disk) def _format_disk_info(self, disk): """ Some specialized disks are difficult to identify in the storage spoke, so add and return extra identifying information about them. Since this is going to be ugly to do within the confines of the CheckboxWidget, pre-format the display string right here. """ # show this info for all disks format_str = "%s: %s (%s)" % (disk.model, disk.size, disk.name) disk_attrs = [] # now check for/add info about special disks if (isinstance(disk, MultipathDevice) or isinstance(disk, iScsiDiskDevice) or isinstance(disk, FcoeDiskDevice)): if hasattr(disk, "wwid"): disk_attrs.append(disk.wwid) elif isinstance(disk, DASDDevice): if hasattr(disk, "busid"): disk_attrs.append(disk.busid) elif isinstance(disk, ZFCPDiskDevice): if hasattr(disk, "fcp_lun"): disk_attrs.append(disk.fcp_lun) if hasattr(disk, "wwpn"): disk_attrs.append(disk.wwpn) if hasattr(disk, "hba_id"): disk_attrs.append(disk.hba_id) # now append all additional attributes to our string for attr in disk_attrs: format_str += ", %s" % attr return format_str def input(self, args, key): """Grab the disk choice and update things""" self.errors = [] if self._container.process_user_input(key): self.redraw() return InputState.PROCESSED else: # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): if self.selected_disks: # check selected disks to see if we have any unformatted DASDs # if we're on s390x, since they need to be formatted before we # can use them. if arch.is_s390(): _disks = [ d for d in self.disks if d.name in self.selected_disks ] to_format = [ d for d in _disks if d.type == "dasd" and blockdev.s390.dasd_needs_format(d.busid) ] if to_format: self.run_dasdfmt(to_format) self.redraw() return InputState.PROCESSED # make sure no containers were split up by the user's disk # selection self.errors.extend( checkDiskSelection(self.storage, self.selected_disks)) if self.errors: # The disk selection has to make sense before we can # proceed. self.redraw() return InputState.PROCESSED self.apply() new_spoke = PartTypeSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.execute() self.close() return InputState.PROCESSED else: return super(StorageSpoke, self).input(args, key) def run_dasdfmt(self, to_format): """ This generates the list of DASDs requiring dasdfmt and runs dasdfmt against them. """ # if the storage thread is running, wait on it to complete before taking # any further actions on devices; most likely to occur if user has # zerombr in their ks file threadMgr.wait(THREAD_STORAGE) # ask user to verify they want to format if zerombr not in ks file if not self.data.zerombr.zerombr: # prepare our msg strings; copied directly from dasdfmt.glade summary = _( "The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs cannot be used during installation.\n\n" ) warntext = _( "Warning: All storage changes made using the installer will be lost when you choose to format.\n\nProceed to run dasdfmt?\n" ) displaytext = summary + "\n".join( "/dev/" + d.name for d in to_format) + "\n" + warntext # now show actual prompt; note -- in cmdline mode, auto-answer for # this is 'no', so unformatted DASDs will remain so unless zerombr # is added to the ks file question_window = YesNoDialog(displaytext) ScreenHandler.push_screen_modal(question_window) if not question_window.answer: # no? well fine then, back to the storage spoke with you; return None for disk in to_format: try: print( _("Formatting /dev/%s. This may take a moment.") % disk.name) blockdev.s390.dasd_format(disk.name) except blockdev.S390Error as err: # Log errors if formatting fails, but don't halt the installer log.error(str(err)) continue def apply(self): self.autopart = self.data.autopart.autopart self.data.ignoredisk.onlyuse = self.selected_disks[:] self.data.clearpart.drives = self.selected_disks[:] if self.autopart and self.data.autopart.type is None: self.data.autopart.type = AUTOPART_TYPE_LVM for disk in self.disks: if disk.name not in self.selected_disks and \ disk in self.storage.devices: self.storage.devicetree.hide(disk) elif disk.name in self.selected_disks and \ disk not in self.storage.devices: self.storage.devicetree.unhide(disk) self.data.bootloader.location = "mbr" if self.data.bootloader.bootDrive and \ self.data.bootloader.bootDrive not in self.selected_disks: self.data.bootloader.bootDrive = "" self.storage.bootloader.reset() self.storage.config.update(self.data) # If autopart is selected we want to remove whatever has been # created/scheduled to make room for autopart. # If custom is selected, we want to leave alone any storage layout the # user may have set up before now. self.storage.config.clear_non_existent = self.data.autopart.autopart def execute(self): print(_("Generating updated storage configuration")) try: doKickstartStorage(self.storage, self.data, self.instclass) except (StorageError, KickstartParseError) as e: log.error("storage configuration failed: %s", e) print(_("storage configuration failed: %s") % e) self.errors = [str(e)] self.data.bootloader.bootDrive = "" self.data.clearpart.type = CLEARPART_TYPE_ALL self.data.clearpart.initAll = False self.storage.config.update(self.data) self.storage.autopart_type = self.data.autopart.type self.storage.reset() # now set ksdata back to the user's specified config applyDiskSelection(self.storage, self.data, self.selected_disks) except BootLoaderError as e: log.error("BootLoader setup failed: %s", e) print(_("storage configuration failed: %s") % e) self.errors = [str(e)] self.data.bootloader.bootDrive = "" else: print(_("Checking storage configuration...")) report = storage_checker.check(self.storage) print("\n".join(report.all_errors)) report.log(log) self.errors = report.errors self.warnings = report.warnings finally: resetCustomStorageData(self.data) self._ready = True def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() threadMgr.add( AnacondaThread(name=THREAD_STORAGE_WATCHER, target=self._initialize)) self.selected_disks = self.data.ignoredisk.onlyuse[:] # Probably need something here to track which disks are selected? def _initialize(self, reinit=False): """ Secondary initialize so wait for the storage thread to complete before populating our disk list """ threadMgr.wait(THREAD_STORAGE) self.disks = sorted(getDisks(self.storage.devicetree), key=lambda d: d.name) # if only one disk is available, go ahead and mark it as selected if len(self.disks) == 1: self._update_disk_list(self.disks[0]) self._update_summary() if not reinit: self._ready = True # report that the storage spoke has been initialized self.initialize_done()
class SpecifyNFSRepoSpoke(NormalTUISpoke, SourceSwitchHandler): """ Specify server and mount opts here if NFS selected. """ category = SoftwareCategory def __init__(self, data, storage, payload, instclass, error): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Specify Repo Options") self._container = None self._error = error nfs = self.data.method self._nfs_opts = "" self._nfs_server = "" if nfs.method == "nfs" and (nfs.server and nfs.dir): self._nfs_server = "%s:%s" % (nfs.server, nfs.dir) self._nfs_opts = nfs.opts def refresh(self, args=None): """ Refresh window. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) dialog = Dialog(title=_("SERVER:/PATH"), conditions=[self._check_nfs_server]) self._container.add(EntryWidget(dialog.title, self._nfs_server), self._set_nfs_server, dialog) dialog = Dialog(title=_("NFS mount options")) self._container.add(EntryWidget(dialog.title, self._nfs_opts), self._set_nfs_opts, dialog) self.window.add_with_separator(self._container) def _set_nfs_server(self, dialog): self._nfs_server = dialog.run() def _check_nfs_server(self, user_input, report_func): if ":" not in user_input or len(user_input.split(":")) != 2: report_func(_("Server must be specified as SERVER:/PATH")) return False return True def _set_nfs_opts(self, dialog): self._nfs_opts = dialog.run() def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return NormalTUISpoke.input(self, args, key) @property def indirect(self): return True def apply(self): """ Apply our changes. """ if self._nfs_server == "" or ':' not in self._nfs_server: return False if self._nfs_server.startswith("nfs://"): self._nfs_server = self._nfs_server[6:] try: (self.data.method.server, self.data.method.dir) = self._nfs_server.split(":", 2) except ValueError as err: log.error("ValueError: %s", err) self._error = True return opts = self._nfs_opts or "" self.set_source_nfs(opts)
class SelectISOSpoke(NormalTUISpoke, SourceSwitchHandler): """ Select an ISO to use as install source. """ category = SoftwareCategory def __init__(self, data, storage, payload, instclass, device): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Select an ISO to use as install source") self._container = None self.args = self.data.method self._device = device self._mount_device() self._isos = self._getISOs() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) if self._isos: self._container = ListColumnContainer(1, columns_width=78, spacing=1) for iso in self._isos: self._container.add(TextWidget(iso), callback=self._select_iso_callback, data=iso) self.window.add_with_separator(self._container) else: message = _("No *.iso files found in device root folder") self.window.add_with_separator(TextWidget(message)) def _select_iso_callback(self, data): self._current_iso_path = data self.apply() self.close() def input(self, args, key): if self._container is not None and self._container.process_user_input(key): return InputState.PROCESSED # TRANSLATORS: 'c' to continue elif key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) @property def indirect(self): return True def _mount_device(self): """ Mount the device so we can search it for ISOs. """ mounts = get_mount_paths(self._device.path) # We have to check both ISO_DIR and the DRACUT_ISODIR because we # still reference both, even though /mnt/install is a symlink to # /run/install. Finding mount points doesn't handle the symlink if ISO_DIR not in mounts and DRACUT_ISODIR not in mounts: # We're not mounted to either location, so do the mount self._device.format.mount(mountpoint=ISO_DIR) def _unmount_device(self): self._device.format.unmount() def _getISOs(self): """List all *.iso files in the root folder of the currently selected device. TODO: advanced ISO file selection :returns: a list of *.iso file paths :rtype: list """ isos = [] for filename in os.listdir(ISO_DIR): if fnmatch.fnmatch(filename.lower(), "*.iso"): isos.append(filename) return isos def apply(self): """ Apply all of our changes. """ if self._current_iso_path: # If a hdd iso source has already been selected previously we need # to clear it now. # Otherwise we would get a crash if the same iso was selected again # as _unmount_device() would try to unmount a partition that is in use # due to the payload still holding on to the ISO file. if self.data.method.method == "harddrive": self.unset_source() self.set_source_hdd_iso(self._device, self._current_iso_path) # unmount the device - the payload will remount it anyway # (if it uses it) self._unmount_device()
class SoftwareSpoke(NormalTUISpoke): """ Spoke used to read new value of text to represent source repo. .. inheritance-diagram:: SoftwareSpoke :parts: 3 """ helpFile = "SoftwareSpoke.txt" category = SoftwareCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Software selection") self._container = None self.errors = [] self._tx_id = None self._selected_environment = None self.environment = None self._addons_selection = set() self.addons = set() # for detecting later whether any changes have been made self._origEnv = None self._origAddons = set() # are we taking values (package list) from a kickstart file? self._kickstarted = flags.automatedInstall and self.data.packages.seen # Register event listeners to update our status on payload events payloadMgr.addListener(payloadMgr.STATE_START, self._payload_start) payloadMgr.addListener(payloadMgr.STATE_FINISHED, self._payload_finished) payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error) def initialize(self): # Start a thread to wait for the payload and run the first, automatic # dependency check self.initialize_start() super(SoftwareSpoke, self).initialize() threadMgr.add(AnacondaThread(name=THREAD_SOFTWARE_WATCHER, target=self._initialize)) def _initialize(self): threadMgr.wait(THREAD_PAYLOAD) if not self._kickstarted: # If an environment was specified in the instclass, use that. # Otherwise, select the first environment. if self.payload.environments: environments = self.payload.environments instclass = self.payload.instclass if instclass and instclass.defaultPackageEnvironment and \ instclass.defaultPackageEnvironment in environments: self._selected_environment = environments.index(instclass.defaultPackageEnvironment) else: self._selected_environment = 0 # Apply the initial selection self._apply() # Wait for the software selection thread that might be started by _apply(). # We are already running in a thread, so it should not needlessly block anything # and only like this we can be sure we are really initialized. threadMgr.wait(THREAD_CHECK_SOFTWARE) # report that the software spoke has been initialized self.initialize_done() def _payload_start(self): # Source is changing, invalidate the software selection and clear the # errors self._selected_environment = None self._addons_selection = set() self.errors = [] def _payload_finished(self): self.environment = self.data.packages.environment self.addons = self._get_selected_addons() def _payload_error(self): self.errors = [payloadMgr.error] def _translate_env_selection_to_name(self, selection): """ Return the selected environment name or None. Selection can be None during kickstart installation. """ if selection is not None: return self.payload.environments[selection] else: return None def _translate_env_name_to_id(self, environment): """ Return the id of the selected environment or None. """ if environment is None: return None try: return self.payload.environmentId(environment) except NoSuchGroup: return None def _get_available_addons(self, environment_id): """ Return all add-ons of the specific environment. """ addons = [] if environment_id in self.payload.environmentAddons: for addons_list in self.payload.environmentAddons[environment_id]: addons.extend(addons_list) return addons def _get_selected_addons(self): """ Return selected add-ons. """ return {group.name for group in self.payload.data.packages.groupList} @property def showable(self): return isinstance(self.payload, PackagePayload) @property def status(self): """ Where we are in the process """ if self.errors: return _("Error checking software selection") if not self.ready: return _("Processing...") if not self.payload.baseRepo: return _("Installation source not set up") if not self.txid_valid: return _("Source changed - please verify") if not self.environment: # Ks installs with %packages will have an env selected, unless # they did an install without a desktop environment. This should # catch that one case. if self._kickstarted: return _("Custom software selected") return _("Nothing selected") return self.payload.environmentDescription(self.environment)[0] @property def completed(self): """ Make sure our threads are done running and vars are set. WARNING: This can be called before the spoke is finished initializing if the spoke starts a thread. It should make sure it doesn't access things until they are completely setup. """ processing_done = self.ready and not self.errors and self.txid_valid if flags.automatedInstall or self._kickstarted: return processing_done and self.payload.baseRepo and self.data.packages.seen else: return processing_done and self.payload.baseRepo and self.environment is not None def refresh(self, args=None): """ Refresh screen. """ NormalTUISpoke.refresh(self, args) threadMgr.wait(THREAD_PAYLOAD) self._container = None if not self.payload.baseRepo: message = TextWidget(_("Installation source needs to be set up first.")) self.window.add_with_separator(message) return threadMgr.wait(THREAD_CHECK_SOFTWARE) self._container = ListColumnContainer(2, columns_width=38, spacing=2) # Display the environments if args is None: environments = self.payload.environments msg = _("Base environment") for env in environments: name = self.payload.environmentDescription(env)[0] selected = (env == self._selected_environment) widget = CheckboxWidget(title="%s" % name, completed=selected) self._container.add(widget, callback=self._set_environment_callback, data=env) # Display the add-ons else: length = len(args) if length > 0: msg = _("Add-ons for selected environment") else: msg = _("No add-ons to select.") for addon_id in args: name = self.payload.groupDescription(addon_id)[0] selected = addon_id in self._addons_selection widget = CheckboxWidget(title="%s" % name, completed=selected) self._container.add(widget, callback=self._set_addons_callback, data=addon_id) self.window.add_with_separator(TextWidget(msg)) self.window.add_with_separator(self._container) def _set_environment_callback(self, data): self._selected_environment = data def _set_addons_callback(self, data): addon = data if addon not in self._addons_selection: self._addons_selection.add(addon) else: self._addons_selection.remove(addon) def input(self, args, key): """ Handle the input; this chooses the desktop environment. """ if self._container is not None and self._container.process_user_input(key): self.redraw() else: # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): # No environment was selected, close if self._selected_environment is None: self.close() # The environment was selected, switch screen elif args is None: # Get addons for the selected environment environment = self._translate_env_selection_to_name(self._selected_environment) environment_id = self._translate_env_name_to_id(environment) addons = self._get_available_addons(environment_id) # Switch the screen ScreenHandler.replace_screen(self, addons) # The addons were selected, apply and close else: self.apply() self.close() return InputState.PROCESSED else: return super(SoftwareSpoke, self).input(args, key) return InputState.PROCESSED @property def ready(self): """ If we're ready to move on. """ return (not threadMgr.get(THREAD_PAYLOAD) and not threadMgr.get(THREAD_CHECK_SOFTWARE) and not threadMgr.get(THREAD_SOFTWARE_WATCHER)) def apply(self): """ Apply our selections """ # no longer using values from kickstart self._kickstarted = False self.data.packages.seen = True # _apply depends on a value of _kickstarted self._apply() def _apply(self): """ Private apply. """ self.environment = self._translate_env_selection_to_name(self._selected_environment) self.addons = self._addons_selection if self.environment is not None else set() if self.environment is None: return changed = False # Not a kickstart with packages, setup the selected environment and addons if not self._kickstarted: # Changed the environment or addons, clear and setup if not self._origEnv \ or self._origEnv != self.environment \ or set(self._origAddons) != set(self.addons): self.payload.data.packages.packageList = [] self.data.packages.groupList = [] self.payload.selectEnvironment(self.environment) environment_id = self._translate_env_name_to_id(self.environment) available_addons = self._get_available_addons(environment_id) for addon_id in available_addons: if addon_id in self.addons: self.payload.selectGroup(addon_id) changed = True self._origEnv = self.environment self._origAddons = set(self.addons) # Check the software selection if changed or self._kickstarted: threadMgr.add(AnacondaThread(name=THREAD_CHECK_SOFTWARE, target=self.checkSoftwareSelection)) def checkSoftwareSelection(self): """ Depsolving """ try: self.payload.checkSoftwareSelection() except DependencyError as e: self.errors = [str(e)] self._tx_id = None else: self._tx_id = self.payload.txID @property def txid_valid(self): """ Whether we have a valid dnf tx id. """ return self._tx_id == self.payload.txID
class NetworkSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ Spoke used to configure network settings. .. inheritance-diagram:: NetworkSpoke :parts: 3 """ helpFile = "NetworkSpoke.txt" category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Network configuration") self._network_module = NETWORK.get_observer() self._network_module.connect() self._container = None self._value = self._network_module.proxy.Hostname self.supported_devices = [] self.errors = [] self._apply = False def initialize(self): self.initialize_start() self._load_new_devices() NormalTUISpoke.initialize(self) if not self._network_module.proxy.Kickstarted: self._update_network_data() self.initialize_done() def _load_new_devices(self): devices = nm.nm_devices() intf_dumped = network.dumpMissingDefaultIfcfgs() if intf_dumped: log.debug("dumped interfaces: %s", intf_dumped) for name in devices: if name in self.supported_devices: continue if network.is_ibft_configured_device(name): continue if network.device_type_is_supported_wired(name): # ignore slaves try: if nm.nm_device_setting_value(name, "connection", "slave-type"): continue except nm.MultipleSettingsFoundError as e: log.debug("%s during initialization", e) self.supported_devices.append(name) @property def completed(self): """ Check whether this spoke is complete or not.""" # If we can't configure network, don't require it return (not conf.system.can_configure_network or nm.nm_activated_devices()) @property def mandatory(self): # the network spoke should be mandatory only if it is running # during the installation and if the installation source requires network return ANACONDA_ENVIRON in flags.environs and self.payload.needsNetwork @property def status(self): """ Short msg telling what devices are active. """ return network.status_message() def _summary_text(self): """Devices cofiguration shown to user.""" msg = "" activated_devs = nm.nm_activated_devices() for name in self.supported_devices: if name in activated_devs: msg += self._activated_device_msg(name) else: msg += _("Wired (%(interface_name)s) disconnected\n") \ % {"interface_name": name} return msg def _activated_device_msg(self, devname): msg = _("Wired (%(interface_name)s) connected\n") \ % {"interface_name": devname} ipv4config = nm.nm_device_ip_config(devname, version=4) ipv6config = nm.nm_device_ip_config(devname, version=6) if ipv4config and ipv4config[0]: addr_str, prefix, gateway_str = ipv4config[0][0] netmask_str = network.prefix2netmask(prefix) dnss_str = ",".join(ipv4config[1]) else: addr_str = dnss_str = gateway_str = netmask_str = "" msg += _(" IPv4 Address: %(addr)s Netmask: %(netmask)s Gateway: %(gateway)s\n") % \ {"addr": addr_str, "netmask": netmask_str, "gateway": gateway_str} msg += _(" DNS: %s\n") % dnss_str if ipv6config and ipv6config[0]: for ipv6addr in ipv6config[0]: addr_str, prefix, gateway_str = ipv6addr # Do not display link-local addresses if not addr_str.startswith("fe80:"): msg += _(" IPv6 Address: %(addr)s/%(prefix)d\n") % \ {"addr": addr_str, "prefix": prefix} return msg def refresh(self, args=None): """ Refresh screen. """ self._load_new_devices() NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) hostname = _("Host Name: %s\n") % self._network_module.proxy.Hostname self.window.add_with_separator(TextWidget(hostname)) current_hostname = _( "Current host name: %s\n" ) % self._network_module.proxy.GetCurrentHostname() self.window.add_with_separator(TextWidget(current_hostname)) # if we have any errors, display them while len(self.errors) > 0: self.window.add_with_separator(TextWidget(self.errors.pop())) dialog = Dialog(_("Host Name")) self._container.add(TextWidget(_("Set host name")), callback=self._set_hostname_callback, data=dialog) for dev_name in self.supported_devices: text = (_("Configure device %s") % dev_name) self._container.add(TextWidget(text), callback=self._configure_network_interface, data=dev_name) self.window.add_with_separator(self._container) def _set_hostname_callback(self, dialog): # set hostname self._value = dialog.run() self.redraw() self.apply() def _configure_network_interface(self, data): devname = data ndata = network.ksdata_from_ifcfg(devname) if not ndata: # There is no ifcfg file for the device. # Make sure there is just one connection for the device. try: nm.nm_device_setting_value(devname, "connection", "uuid") except nm.SettingsNotFoundError: log.debug("can't find any connection for %s", devname) return except nm.MultipleSettingsFoundError: log.debug("multiple non-ifcfg connections found for %s", devname) return log.debug("dumping ifcfg file for in-memory connection %s", devname) nm.nm_update_settings_of_device( devname, [['connection', 'id', devname, None]]) ndata = network.ksdata_from_ifcfg(devname) new_spoke = ConfigureNetworkSpoke(self.data, self.storage, self.payload, self.instclass, ndata) ScreenHandler.push_screen_modal(new_spoke) self.redraw() if ndata.ip == "dhcp": ndata.bootProto = "dhcp" ndata.ip = "" else: ndata.bootProto = "static" if not ndata.netmask: self.errors.append( _("Configuration not saved: netmask missing in static configuration" )) return if ndata.ipv6 == "ignore": ndata.noipv6 = True ndata.ipv6 = "" else: ndata.noipv6 = False uuid = network.update_settings_with_ksdata(devname, ndata) network.update_onboot_value(devname, ndata.onboot, ksdata=None, root_path="") network.logIfcfgFiles("settings of %s updated in tui" % devname) if new_spoke.apply_configuration: self._apply = True try: nm.nm_activate_device_connection(devname, uuid) except (nm.UnmanagedDeviceError, nm.UnknownConnectionError): self.errors.append( _("Can't apply configuration, device activation failed.")) self.apply() def input(self, args, key): """ Handle the input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: return super().input(args, key) def apply(self): """Apply all of our settings.""" self._update_network_data() log.debug("apply ksdata %s", self.data.network) if self._apply: self._apply = False if ANACONDA_ENVIRON in flags.environs: from pyanaconda.payload import payloadMgr payloadMgr.restartThread(self.storage, self.data, self.payload, self.instclass, checkmount=False) def _update_network_data(self): hostname = self._network_module.proxy.Hostname self.data.network.network = [] for i, name in enumerate(nm.nm_devices()): if network.is_ibft_configured_device(name): continue nd = network.ksdata_from_ifcfg(name) if not nd: continue if name in nm.nm_activated_devices(): nd.activate = True else: # First network command defaults to --activate so we must # use --no-activate explicitly to prevent the default if i == 0: nd.activate = False self.data.network.network.append(nd) (valid, error) = network.sanityCheckHostname(self._value) if valid: hostname = self._value else: self.errors.append(_("Host name is not valid: %s") % error) self._value = hostname network.update_hostname_data(self.data, hostname)
class AskVNCSpoke(NormalTUISpoke): """ .. inheritance-diagram:: AskVNCSpoke :parts: 3 """ title = N_("VNC") # This spoke is kinda standalone, not meant to be used with a hub # We pass in some fake data just to make our parents happy def __init__(self, data, storage=None, payload=None, instclass=None, message=""): super().__init__(data, storage, payload, instclass) self.input_required = True self.initialize_start() self._container = None # The TUI hasn't been initialized with the message handlers yet. Add an # exception message handler so that the TUI exits if anything goes wrong # at this stage. loop = App.get_event_loop() loop.register_signal_handler(ExceptionSignal, exception_msg_handler_and_exit) self._message = message self._usevnc = False self.initialize_done() @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self.window.add_with_separator(TextWidget(self._message)) self._container = ListColumnContainer(1, spacing=1) # choices are # USE VNC self._container.add(TextWidget(_(USEVNC)), self._use_vnc_callback) # USE TEXT self._container.add(TextWidget(_(USETEXT)), self._use_text_callback) self.window.add_with_separator(self._container) def _use_vnc_callback(self, data): self._usevnc = True new_spoke = VNCPassSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) def _use_text_callback(self, data): self._usevnc = False def input(self, args, key): """Override input so that we can launch the VNC password spoke""" if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_CLOSE else: # TRANSLATORS: 'q' to quit if key.lower() == C_('TUI|Spoke Navigation', 'q'): d = YesNoDialog(_(u"Do you really want to quit?")) ScreenHandler.push_screen_modal(d) if d.answer: ipmi_abort(scripts=self.data.scripts) if conf.system.can_reboot: execWithRedirect("systemctl", ["--no-wall", "reboot"]) else: sys.exit(1) else: return super().input(args, key) def apply(self): self.data.vnc.enabled = self._usevnc
class RescueModeSpoke(NormalTUISpoke): """UI offering mounting existing installation roots in rescue mode.""" # If it acts like a spoke and looks like a spoke, is it a spoke? Not # always. This is independent of any hub(s), so pass in some fake data def __init__(self, rescue): super().__init__(data=None, storage=None, payload=None) self.title = N_("Rescue") self._container = None self._rescue = rescue def refresh(self, args=None): super().refresh(args) msg = _("The rescue environment will now attempt " "to find your Linux installation and mount it under " "the directory : %s. You can then make any changes " "required to your system. Choose '1' to proceed with " "this step.\nYou can choose to mount your file " "systems read-only instead of read-write by choosing " "'2'.\nIf for some reason this process does not work " "choose '3' to skip directly to a shell.\n\n") % (util.getSysroot()) self.window.add_with_separator(TextWidget(msg)) self._container = ListColumnContainer(1) self._container.add(TextWidget(_("Continue")), self._read_write_mount_callback) self._container.add(TextWidget(_("Read-only mount")), self._read_only_mount_callback) self._container.add(TextWidget(_("Skip to shell")), self._skip_to_shell_callback) self._container.add(TextWidget(_("Quit (Reboot)")), self._quit_callback) self.window.add_with_separator(self._container) def _read_write_mount_callback(self, data): self._mount_and_prompt_for_shell() def _read_only_mount_callback(self, data): self._rescue.ro = True self._mount_and_prompt_for_shell() def _skip_to_shell_callback(self, data): self._show_result_and_prompt_for_shell() def _quit_callback(self, data): d = YesNoDialog(_(QUIT_MESSAGE)) ScreenHandler.push_screen_modal(d) self.redraw() if d.answer: self._rescue.reboot = True self._rescue.finish() def _mount_and_prompt_for_shell(self): self._rescue.mount = True self._mount_root() self._show_result_and_prompt_for_shell() def prompt(self, args=None): """ Override the default TUI prompt.""" if self._rescue.automated: if self._rescue.mount: self._mount_root() self._show_result_and_prompt_for_shell() return None return Prompt() def input(self, args, key): """Override any input so we can launch rescue mode.""" if self._container.process_user_input(key): return InputState.PROCESSED else: return InputState.DISCARDED def _mount_root(self): # decrypt all luks devices self._unlock_devices() roots = self._rescue.find_roots() if not roots: return if len(roots) == 1: root = roots[0] else: # have to prompt user for which root to mount root_spoke = RootSelectionSpoke(roots) ScreenHandler.push_screen_modal(root_spoke) self.redraw() root = root_spoke.selection self._rescue.mount_root(root) def _show_result_and_prompt_for_shell(self): new_spoke = RescueStatusAndShellSpoke(self._rescue) ScreenHandler.push_screen_modal(new_spoke) self.close() def _unlock_devices(self): """Attempt to unlock all locked LUKS devices. Returns true if all devices were unlocked. """ try_passphrase = None passphrase = None for device_name in self._rescue.get_locked_device_names(): skip = False unlocked = False while not (skip or unlocked): if try_passphrase is None: p = PasswordDialog(device_name) ScreenHandler.push_screen_modal(p) if p.answer: passphrase = p.answer.strip() else: passphrase = try_passphrase if passphrase is None: # cancelled skip = True else: unlocked = self._rescue.unlock_device(device_name, passphrase) try_passphrase = passphrase if unlocked else None return not self._rescue.get_locked_device_names() def apply(self): """Move along home.""" pass
class StorageSpoke(NormalTUISpoke): """Storage spoke where users proceed to customize storage features such as disk selection, partitioning, and fs type. .. inheritance-diagram:: StorageSpoke :parts: 3 """ helpFile = "StorageSpoke.txt" category = SystemCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Installation Destination") self._container = None self._bootloader_observer = STORAGE.get_observer(BOOTLOADER) self._bootloader_observer.connect() self._disk_init_observer = STORAGE.get_observer(DISK_INITIALIZATION) self._disk_init_observer.connect() self._disk_select_observer = STORAGE.get_observer(DISK_SELECTION) self._disk_select_observer.connect() self._auto_part_observer = STORAGE.get_observer(AUTO_PARTITIONING) self._auto_part_observer.connect() self._selected_disks = self._disk_select_observer.proxy.SelectedDisks # This list gets set up once in initialize and should not be modified # except perhaps to add advanced devices. It will remain the full list # of disks that can be included in the install. self._available_disks = [] if not flags.automatedInstall: # default to using autopart for interactive installs self._auto_part_observer.proxy.SetEnabled(True) self._ready = False self._select_all = False self._auto_part_enabled = None self.errors = [] self.warnings = [] @property def completed(self): return bool(self.storage.root_device and not self.errors) @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready and not threadMgr.get(THREAD_STORAGE_WATCHER) @property def mandatory(self): return True @property def showable(self): return not conf.target.is_directory @property def status(self): """ A short string describing the current status of storage setup. """ if flags.automatedInstall and not self.storage.root_device: return _("Kickstart insufficient") elif not self._disk_select_observer.proxy.SelectedDisks: return _("No disks selected") if self.errors: return _("Error checking storage configuration") elif self.warnings: return _("Warning checking storage configuration") elif self._auto_part_observer.proxy.Enabled: return _("Automatic partitioning selected") else: return _("Custom partitioning selected") def _update_disk_list(self, disk): """ Update self.selected_disks based on the selection.""" name = disk.name # if the disk isn't already selected, select it. if name not in self._selected_disks: self._selected_disks.append(name) # If the disk is already selected, deselect it. elif name in self._selected_disks: self._selected_disks.remove(name) def _update_summary(self): """ Update the summary based on the UI. """ # Get the summary message. if not self._available_disks: summary = _(WARNING_NO_DISKS_DETECTED) elif not self._selected_disks: summary = _(WARNING_NO_DISKS_SELECTED) else: disks = filter_disks_by_names(self._available_disks, self._selected_disks) summary = get_disks_summary(self.storage, disks) # Append storage errors to the summary if self.errors or self.warnings: summary = summary + "\n" + "\n".join(self.errors or self.warnings) return summary def refresh(self, args=None): super().refresh(args) # Join the initialization thread to block on it # This print is foul. Need a better message display print(_(PAYLOAD_STATUS_PROBING_STORAGE)) threadMgr.wait(THREAD_STORAGE_WATCHER) if not any(d in self.storage.disks for d in self._available_disks): # something happened to self.storage (probably reset), need to # reinitialize the list of disks self.update_disks() # synchronize our local data store with the global ksdata # Commment out because there is no way to select a disk right # now without putting it in ksdata. Seems wrong? # self.selected_disks = self.data.ignoredisk.onlyuse[:] self._auto_part_enabled = self._auto_part_observer.proxy.Enabled self._container = ListColumnContainer(1, spacing=1) message = self._update_summary() # loop through the disks and present them. for disk in self._available_disks: disk_info = self._format_disk_info(disk) c = CheckboxWidget(title=disk_info, completed=(disk.name in self._selected_disks)) self._container.add(c, self._update_disk_list_callback, disk) # if we have more than one disk, present an option to just # select all disks if len(self._available_disks) > 1: c = CheckboxWidget(title=_("Select all"), completed=self._select_all) self._container.add(c, self._select_all_disks_callback) self.window.add_with_separator(self._container) self.window.add_with_separator(TextWidget(message)) def _select_all_disks_callback(self, data): """ Mark all disks as selected for use in partitioning. """ self._select_all = True for disk in self._available_disks: if disk.name not in self._selected_disks: self._update_disk_list(disk) def _update_disk_list_callback(self, data): disk = data self._select_all = False self._update_disk_list(disk) def _format_disk_info(self, disk): """ Some specialized disks are difficult to identify in the storage spoke, so add and return extra identifying information about them. Since this is going to be ugly to do within the confines of the CheckboxWidget, pre-format the display string right here. """ # show this info for all disks format_str = "%s: %s (%s)" % (disk.model, disk.size, disk.name) disk_attrs = [] # now check for/add info about special disks if (isinstance(disk, MultipathDevice) or isinstance(disk, iScsiDiskDevice) or isinstance(disk, FcoeDiskDevice)): if hasattr(disk, "wwn"): disk_attrs.append(disk.wwn) elif isinstance(disk, DASDDevice): if hasattr(disk, "busid"): disk_attrs.append(disk.busid) elif isinstance(disk, ZFCPDiskDevice): if hasattr(disk, "fcp_lun"): disk_attrs.append(disk.fcp_lun) if hasattr(disk, "wwpn"): disk_attrs.append(disk.wwpn) if hasattr(disk, "hba_id"): disk_attrs.append(disk.hba_id) # now append all additional attributes to our string for attr in disk_attrs: format_str += ", %s" % attr return format_str def input(self, args, key): """Grab the disk choice and update things""" self.errors = [] if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW else: # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): if self._selected_disks: # Is DASD formatting supported? if DasdFormatting.is_supported(): # Wait for storage. threadMgr.wait(THREAD_STORAGE) # Get selected disks. disks = filter_disks_by_names(self._available_disks, self._selected_disks) # Check if some of the disks should be formatted. dasd_formatting = DasdFormatting() dasd_formatting.search_disks(disks) if dasd_formatting.should_run(): # We want to apply current selection before running dasdfmt to # prevent this information from being lost afterward apply_disk_selection(self.storage, self._selected_disks) # Run the dialog. self.run_dasdfmt_dialog(dasd_formatting) return InputState.PROCESSED_AND_REDRAW # make sure no containers were split up by the user's disk # selection self.errors.extend(check_disk_selection(self.storage, self._selected_disks)) if self.errors: # The disk selection has to make sense before we can # proceed. return InputState.PROCESSED_AND_REDRAW self.apply() new_spoke = PartTypeSpoke(self.data, self.storage, self.payload) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.execute() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) def run_dasdfmt_dialog(self, dasd_formatting): """Do DASD formatting if user agrees.""" # Prepare text of the dialog. text = "" text += _("The following unformatted or LDL DASDs have been " "detected on your system. You can choose to format them " "now with dasdfmt or cancel to leave them unformatted. " "Unformatted DASDs cannot be used during installation.\n\n") text += dasd_formatting.dasds_summary + "\n\n" text += _("Warning: All storage changes made using the installer will " "be lost when you choose to format.\n\nProceed to run dasdfmt?\n") # Run the dialog. question_window = YesNoDialog(text) ScreenHandler.push_screen_modal(question_window) if not question_window.answer: return None print(_("This may take a moment."), flush=True) # Do the DASD formatting. dasd_formatting.report.connect(self._show_dasdfmt_report) dasd_formatting.run(self.storage, self.data) dasd_formatting.report.disconnect(self._show_dasdfmt_report) self.update_disks() def _show_dasdfmt_report(self, msg): print(msg, flush=True) def run_passphrase_dialog(self): """Ask user for a default passphrase.""" data_without_passphrase = self._get_data_without_passphrase() if not data_without_passphrase: return dialog = PasswordDialog( title=_("Passphrase"), message=_("Please provide a default LUKS passphrase for all devices " "you want to encrypt. You will have to type it twice."), secret_type=SecretType.PASSPHRASE, policy=get_policy(self.data, "luks"), process_func=lambda x: x ) passphrase = None while passphrase is None: passphrase = dialog.run() self._set_data_without_passphrase(data_without_passphrase, passphrase) def _get_data_without_passphrase(self): """Collect kickstart data and DBus proxies that require a passphrase.""" result = [] if self._auto_part_observer.proxy.Encrypted \ and not self._auto_part_observer.proxy.Passphrase: result.append(self._auto_part_observer.proxy) for data in self.data.partition.dataList(): if data.encrypted and not data.passphrase: result.append(data) for data in self.data.logvol.dataList(): if data.encrypted and not data.passphrase: result.append(data) for data in self.data.raid.dataList(): if data.encrypted and not data.passphrase: result.append(data) return result def _set_data_without_passphrase(self, data_without_passphrase, passphrase): """Set a passphrase to the collected kickstart data and DBus proxies.""" for data in data_without_passphrase: if isinstance(data, BaseData): data.passphrase = passphrase else: data.SetPassphrase(passphrase) def apply(self): self._auto_part_enabled = self._auto_part_observer.proxy.Enabled self.storage.select_disks(self._selected_disks) self._bootloader_observer.proxy.SetPreferredLocation(BOOTLOADER_LOCATION_MBR) boot_drive = self._bootloader_observer.proxy.Drive if boot_drive and boot_drive not in self._selected_disks: reset_bootloader(self.storage) apply_disk_selection(self.storage, self._selected_disks) def execute(self): print(_("Generating updated storage configuration")) try: configure_storage(self.storage, self.data) except StorageConfigurationError as e: print(_("storage configuration failed: %s") % e) self.errors = [str(e)] reset_bootloader(self.storage) reset_storage(self.storage, scan_all=True) except BootloaderConfigurationError as e: print(_("storage configuration failed: %s") % e) self.errors = [str(e)] reset_bootloader(self.storage) else: print(_("Checking storage configuration...")) report = storage_checker.check(self.storage) print("\n".join(report.all_errors)) report.log(log) self.errors = report.errors self.warnings = report.warnings finally: reset_custom_storage_data(self.data) self._ready = True def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() # Ask for a default passphrase. if flags.automatedInstall and flags.ksprompt: self.run_passphrase_dialog() threadMgr.add(AnacondaThread(name=THREAD_STORAGE_WATCHER, target=self._initialize)) self._selected_disks = self._disk_select_observer.proxy.SelectedDisks # Probably need something here to track which disks are selected? def _initialize(self): """ Secondary initialize so wait for the storage thread to complete before populating our disk list """ # Wait for storage. threadMgr.wait(THREAD_STORAGE) # Automatically format DASDs if allowed. DasdFormatting.run_automatically(self.storage, self.data) # Update the selected disks. if flags.automatedInstall: self._selected_disks = select_all_disks_by_default(self.storage) # Update disk list. self.update_disks() # Storage is ready. self._ready = True # Report that the storage spoke has been initialized. self.initialize_done() def update_disks(self): threadMgr.wait(THREAD_STORAGE) self._available_disks = self.storage.usable_disks # if only one disk is available, go ahead and mark it as selected if len(self._available_disks) == 1: self._update_disk_list(self._available_disks[0])
class MountPointAssignSpoke(NormalTUISpoke): """ Assign mount points to block devices. """ category = SystemCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Assign mount points") self._container = None self._disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) self._manual_part_proxy = STORAGE.get_proxy(MANUAL_PARTITIONING) self._mount_info = self._gather_mount_info() @property def indirect(self): return True def refresh(self, args=None): """Refresh the window.""" super().refresh(args) self._container = ListColumnContainer(2) for info in self._mount_info: widget = TextWidget(self._get_mount_info_description(info)) self._container.add(widget, self._configure_mount_info, info) message = _( "Choose device from above to assign mount point and set format.\n" "Formats marked with * are new formats meaning ALL DATA on the " "original format WILL BE LOST!" ) self.window.add_with_separator(self._container) self.window.add_with_separator(TextWidget(message)) def prompt(self, args=None): prompt = super().prompt(args) # TRANSLATORS: 's' to rescan devices prompt.add_option(C_('TUI|Spoke Navigation|Partitioning', 's'), _("rescan devices")) return prompt def input(self, args, key): """ Grab the choice and update things. """ if self._container.process_user_input(key): return InputState.PROCESSED # TRANSLATORS: 's' to rescan devices if key.lower() == C_('TUI|Spoke Navigation|Partitioning', 's'): self._rescan_devices() return InputState.PROCESSED_AND_REDRAW # TRANSLATORS: 'c' to continue elif key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() return super().input(args, key) def apply(self): """ Apply our selections. """ mount_points = [] for _device, data in self._mount_info: if data[MOUNT_POINT_REFORMAT] or data[MOUNT_POINT_PATH]: mount_points.append({ MOUNT_POINT_PATH: get_variant(Str, data[MOUNT_POINT_PATH] or "none"), MOUNT_POINT_DEVICE: get_variant(Str, data[MOUNT_POINT_DEVICE]), MOUNT_POINT_REFORMAT: get_variant(Bool, data[MOUNT_POINT_REFORMAT]), MOUNT_POINT_FORMAT: get_variant(Str, data[MOUNT_POINT_FORMAT]) }) self._manual_part_proxy.SetMountPoints(mount_points) def _gather_mount_info(self): """Gather info about mount points.""" selected_disks = self._disk_select_proxy.SelectedDisks mount_points = self._manual_part_proxy.MountPoints mount_info = [] for device in self.storage.devicetree.leaves: # Is the device usable? if device.protected or device.size == Size(0): continue # All device's disks have to be in selected disks. device_disks = {d.name for d in device.disks} if selected_disks and not set(selected_disks).issuperset(device_disks): continue # Append new info about this device. data = self._get_mount_point_data(device, mount_points) mount_info.append((device, data)) # Use the data only once. if data in mount_points: mount_points.remove(data) return mount_info def _get_mount_point_data(self, device, mount_points): """Get the mount point data for the given device.""" # Try to find existing assignment for this device. for data in mount_points: if device is self.storage.devicetree.resolve_device(data[MOUNT_POINT_DEVICE]): return data # Or create a new assignment. if device.format.mountable and device.format.mountpoint: mount_point = device.format.mountpoint else: mount_point = "" return { MOUNT_POINT_DEVICE: device.path, MOUNT_POINT_PATH: mount_point, MOUNT_POINT_FORMAT: device.format.type, MOUNT_POINT_REFORMAT: False } def _get_mount_info_description(self, info): """Get description of the given mount info.""" device, data = info description = "{} ({})".format(data[MOUNT_POINT_DEVICE], device.size) if data[MOUNT_POINT_FORMAT]: description += "\n {}".format(data[MOUNT_POINT_FORMAT]) if data[MOUNT_POINT_REFORMAT]: description += "*" if data[MOUNT_POINT_PATH]: description += ", {}".format(data[MOUNT_POINT_PATH]) return description def _configure_mount_info(self, info): """Configure the given mount info.""" spoke = ConfigureDeviceSpoke(self.data, self.storage, self.payload, *info) ScreenHandler.push_screen(spoke) def _rescan_devices(self): """Rescan devices.""" text = _("Warning: This will revert all changes done so far.\n" "Do you want to proceed?\n") question_window = YesNoDialog(text) ScreenHandler.push_screen_modal(question_window) if not question_window.answer: return print(_("Scanning disks. This may take a moment...")) reset_storage(self.storage, scan_all=True) self._manual_part_proxy.SetMountPoints([]) self._mount_info = self._gather_mount_info()
class NetworkSpoke(FirstbootSpokeMixIn, EditTUISpoke): """ Spoke used to configure network settings. .. inheritance-diagram:: NetworkSpoke :parts: 3 """ helpFile = "NetworkSpoke.txt" category = SystemCategory def __init__(self, data, storage, payload, instclass): EditTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Network configuration") self._container = None self.hostname_dialog = OneShotEditTUIDialog(data, storage, payload, instclass) self.hostname_dialog.value = self.data.network.hostname self.supported_devices = [] self.errors = [] self._apply = False def initialize(self): self.initialize_start() self._load_new_devices() EditTUISpoke.initialize(self) if not self.data.network.seen: self._update_network_data() self.initialize_done() def _load_new_devices(self): devices = nm.nm_devices() intf_dumped = network.dumpMissingDefaultIfcfgs() if intf_dumped: log.debug("dumped interfaces: %s", intf_dumped) for name in devices: if name in self.supported_devices: continue if network.is_ibft_configured_device(name): continue if network.device_type_is_supported_wired(name): # ignore slaves try: if nm.nm_device_setting_value(name, "connection", "slave-type"): continue except nm.MultipleSettingsFoundError as e: log.debug("%s during initialization", e) self.supported_devices.append(name) @property def completed(self): """ Check whether this spoke is complete or not. Do an additional check if we're installing from CD/DVD, since a network connection should not be required in this case. """ return (not can_touch_runtime_system("require network connection") or nm.nm_activated_devices()) @property def mandatory(self): # the network spoke should be mandatory only if it is running # during the installation and if the installation source requires network return ANACONDA_ENVIRON in flags.environs and self.payload.needsNetwork @property def status(self): """ Short msg telling what devices are active. """ return network.status_message() def _summary_text(self): """Devices cofiguration shown to user.""" msg = "" activated_devs = nm.nm_activated_devices() for name in self.supported_devices: if name in activated_devs: msg += self._activated_device_msg(name) else: msg += _("Wired (%(interface_name)s) disconnected\n") \ % {"interface_name": name} return msg def _activated_device_msg(self, devname): msg = _("Wired (%(interface_name)s) connected\n") \ % {"interface_name": devname} ipv4config = nm.nm_device_ip_config(devname, version=4) ipv6config = nm.nm_device_ip_config(devname, version=6) if ipv4config and ipv4config[0]: addr_str, prefix, gateway_str = ipv4config[0][0] netmask_str = network.prefix2netmask(prefix) dnss_str = ",".join(ipv4config[1]) else: addr_str = dnss_str = gateway_str = netmask_str = "" msg += _(" IPv4 Address: %(addr)s Netmask: %(netmask)s Gateway: %(gateway)s\n") % \ {"addr": addr_str, "netmask": netmask_str, "gateway": gateway_str} msg += _(" DNS: %s\n") % dnss_str if ipv6config and ipv6config[0]: for ipv6addr in ipv6config[0]: addr_str, prefix, gateway_str = ipv6addr # Do not display link-local addresses if not addr_str.startswith("fe80:"): msg += _(" IPv6 Address: %(addr)s/%(prefix)d\n") % \ {"addr": addr_str, "prefix": prefix} dnss_str = ",".join(ipv6config[1]) return msg def refresh(self, args=None): """ Refresh screen. """ self._load_new_devices() EditTUISpoke.refresh(self, args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) hostname = _("Host Name: %s\n") % self.data.network.hostname self.window.add_with_separator(TextWidget(hostname)) current_hostname = _("Current host name: %s\n") % network.current_hostname() self.window.add_with_separator(TextWidget(current_hostname)) # if we have any errors, display them while len(self.errors) > 0: self.window.add_with_separator(TextWidget(self.errors.pop())) self._container.add(TextWidget(_("Set host name")), callback=self._set_hostname_callback) for dev_name in self.supported_devices: text = (_("Configure device %s") % dev_name) self._container.add(TextWidget(text), callback=self._configure_network_interface, data=dev_name) self.window.add_with_separator(self._container) def _set_hostname_callback(self, data): # set hostname entry = Entry(_("Host Name"), "hostname", re.compile(".*$"), True) ScreenHandler.push_screen_modal(self.hostname_dialog, entry) self.redraw() self.apply() def _configure_network_interface(self, data): devname = data ndata = network.ksdata_from_ifcfg(devname) if not ndata: # There is no ifcfg file for the device. # Make sure there is just one connection for the device. try: nm.nm_device_setting_value(devname, "connection", "uuid") except nm.SettingsNotFoundError: log.debug("can't find any connection for %s", devname) return except nm.MultipleSettingsFoundError: log.debug("multiple non-ifcfg connections found for %s", devname) return log.debug("dumping ifcfg file for in-memory connection %s", devname) nm.nm_update_settings_of_device(devname, [['connection', 'id', devname, None]]) ndata = network.ksdata_from_ifcfg(devname) new_spoke = ConfigureNetworkSpoke(self.data, self.storage, self.payload, self.instclass, ndata) ScreenHandler.push_screen_modal(new_spoke) self.redraw() if ndata.ip == "dhcp": ndata.bootProto = "dhcp" ndata.ip = "" else: ndata.bootProto = "static" if not ndata.netmask: self.errors.append(_("Configuration not saved: netmask missing in static configuration")) return if ndata.ipv6 == "ignore": ndata.noipv6 = True ndata.ipv6 = "" else: ndata.noipv6 = False uuid = network.update_settings_with_ksdata(devname, ndata) network.update_onboot_value(devname, ndata.onboot, ksdata=None, root_path="") network.logIfcfgFiles("settings of %s updated in tui" % devname) if ndata._apply: self._apply = True try: nm.nm_activate_device_connection(devname, uuid) except (nm.UnmanagedDeviceError, nm.UnknownConnectionError): self.errors.append(_("Can't apply configuration, device activation failed.")) self.apply() def input(self, args, key): """ Handle the input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: return super(NetworkSpoke, self).input(args, key) def apply(self): """Apply all of our settings.""" self._update_network_data() log.debug("apply ksdata %s", self.data.network) if self._apply: self._apply = False if ANACONDA_ENVIRON in flags.environs: from pyanaconda.payload import payloadMgr payloadMgr.restartThread(self.storage, self.data, self.payload, self.instclass, checkmount=False) def _update_network_data(self): hostname = self.data.network.hostname self.data.network.network = [] for i, name in enumerate(nm.nm_devices()): if network.is_ibft_configured_device(name): continue nd = network.ksdata_from_ifcfg(name) if not nd: continue if name in nm.nm_activated_devices(): nd.activate = True else: # First network command defaults to --activate so we must # use --no-activate explicitly to prevent the default if i == 0: nd.activate = False self.data.network.network.append(nd) (valid, error) = network.sanityCheckHostname(self.hostname_dialog.value) if valid: hostname = self.hostname_dialog.value else: self.errors.append(_("Host name is not valid: %s") % error) self.hostname_dialog.value = hostname network.update_hostname_data(self.data, hostname)
class TimeSpoke(FirstbootSpokeMixIn, NormalTUISpoke): helpFile = "DateTimeSpoke.txt" category = LocalizationCategory def __init__(self, data, storage, payload): NormalTUISpoke.__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._timezone_module = TIMEZONE.get_observer() self._timezone_module.connect() @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 ntp_servers = [] if constants.ANACONDA_ENVIRON in flags.environs: ntp_servers = self._timezone_module.proxy.NTPServers elif constants.FIRSTBOOT_ENVIRON in flags.environs: ntp_servers = ntp.get_servers_from_config()[1] # returns a (NPT pools, NTP servers) tupple 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()) # 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: self._timezone_spoke = TimeZoneSpoke(self.data, self.storage, self.payload) return self._timezone_spoke @property def completed(self): return bool(self._timezone_module.proxy.Timezone) @property def mandatory(self): return True @property def status(self): kickstart_timezone = self._timezone_module.proxy.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.proxy.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 servers:") if self._ntp_servers: for status in format_ntp_status_list(self._ntp_servers): msg += "\n%s" % status else: msg += _("not configured") 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.proxy.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) 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.proxy.SetNTPServers(list(self.ntp_servers.keys()))
class ConfigureDeviceSpoke(NormalTUISpoke): """ Assign mount point to a block device and (optionally) reformat it. """ category = SystemCategory def __init__(self, data, storage, payload, device, mount_data): super().__init__(data, storage, payload) self.title = N_("Configure device: %s") % mount_data[MOUNT_POINT_DEVICE] self._container = None self._supported_filesystems = [fmt.type for fmt in get_supported_filesystems()] self._mount_data = mount_data self._device = device @property def indirect(self): return True def refresh(self, args=None): """Refresh window.""" super().refresh(args) self._container = ListColumnContainer(1) self._add_mount_point_widget() self._add_format_widget() self._add_reformat_widget() self.window.add_with_separator(self._container) self.window.add_with_separator(TextWidget( _("Choose from above to assign mount point and/or set format.") )) def input(self, args, key): """Grab the choice and update things.""" if not self._container.process_user_input(key): return super().input(args, key) return InputState.PROCESSED_AND_REDRAW def apply(self): """Nothing to apply here.""" pass def _add_mount_point_widget(self): """Add a widget for mount point assignment.""" title = _("Mount point") fmt = get_format(self._mount_data[MOUNT_POINT_FORMAT]) if fmt and fmt.mountable: # mount point can be set value = self._mount_data[MOUNT_POINT_PATH] or _("none") callback = self._assign_mount_point elif fmt and fmt.type is None: # mount point cannot be set for no format # (fmt.name = "Unknown" in this case which would look weird) value = _("none") callback = None else: # mount point cannot be set for format that is not mountable, just # show the format's name in square brackets instead value = fmt.name callback = None dialog = Dialog(title, conditions=[self._check_assign_mount_point]) widget = EntryWidget(dialog.title, value) self._container.add(widget, callback, dialog) def _check_assign_mount_point(self, user_input, report_func): """Check the mount point assignment.""" # a valid mount point must start with / or user set nothing if user_input == "" or user_input.startswith("/"): return True else: report_func(_("Invalid mount point given")) return False def _assign_mount_point(self, dialog): """Change the mount point assignment.""" self._mount_data[MOUNT_POINT_PATH] = dialog.run() # Always reformat root. if self._mount_data[MOUNT_POINT_PATH] == "/": self._mount_data[MOUNT_POINT_REFORMAT] = True def _add_format_widget(self): """Add a widget for format.""" dialog = Dialog(_("Format"), conditions=[self._check_format]) widget = EntryWidget(dialog.title, self._mount_data[MOUNT_POINT_FORMAT] or _("none")) self._container.add(widget, self._set_format, dialog) def _check_format(self, user_input, report_func): """Check value of format.""" user_input = user_input.lower() if user_input in self._supported_filesystems: return True else: msg = _("Invalid or unsupported format given") msg += "\n" msg += (_("Supported formats: %s") % ", ".join(self._supported_filesystems)) report_func(msg) return False def _set_format(self, dialog): """Change value of format.""" old_format = self._mount_data[MOUNT_POINT_FORMAT] new_format = dialog.run() # Reformat to a new format. if new_format != old_format: self._mount_data[MOUNT_POINT_FORMAT] = new_format self._mount_data[MOUNT_POINT_REFORMAT] = True def _add_reformat_widget(self): """Add a widget for reformat.""" widget = CheckboxWidget( title=_("Reformat"), completed=self._mount_data[MOUNT_POINT_REFORMAT] ) self._container.add(widget, self._switch_reformat) def _switch_reformat(self, data): """Change value of reformat.""" device_format = self._device.format.type if device_format and device_format != self._mount_data[MOUNT_POINT_FORMAT]: reformat = True elif self._mount_data[MOUNT_POINT_PATH] == "/": reformat = True else: reformat = not self._mount_data[MOUNT_POINT_REFORMAT] self._mount_data[MOUNT_POINT_REFORMAT] = reformat
class LangSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ This spoke allows a user to select their installed language. Note that this does not affect the display of the installer, it only will affect the system post-install, because it's too much of a pain to make other languages work in text-mode. Also this doesn't allow for selection of multiple languages like in the GUI. .. inheritance-diagram:: LangSpoke :parts: 3 """ helpFile = "LangSupportSpoke.txt" category = LocalizationCategory def __init__(self, data, storage, payload): NormalTUISpoke.__init__(self, data, storage, payload) self.title = N_("Language settings") self.initialize_start() self._container = None self._langs = [ localization.get_english_name(lang) for lang in localization.get_available_translations() ] self._langs_and_locales = dict( (localization.get_english_name(lang), lang) for lang in localization.get_available_translations()) self._locales = dict((lang, localization.get_language_locales(lang)) for lang in self._langs_and_locales.values()) self._l12_module = LOCALIZATION.get_proxy() self._selected = self._l12_module.Language self.initialize_done() @property def completed(self): return self._l12_module.Language @property def mandatory(self): return False @property def showable(self): # don't show the language support spoke in single language mode return not flags.singlelang @property def status(self): if self._l12_module.Language: return localization.get_english_name(self._selected) else: return _("Language is not set.") def refresh(self, args=None): """ args is None if we want a list of languages; or, it is a list of all locales for a language. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(3) if args: self.window.add(TextWidget(_("Available locales"))) for locale in args: widget = TextWidget(localization.get_english_name(locale)) self._container.add(widget, self._set_locales_callback, locale) else: self.window.add(TextWidget(_("Available languages"))) for lang in self._langs: langs_and_locales = self._langs_and_locales[lang] locales = self._locales[langs_and_locales] self._container.add(TextWidget(lang), self._show_locales_callback, locales) self.window.add_with_separator(self._container) def _set_locales_callback(self, data): locale = data self._selected = locale self.apply() self.close() def _show_locales_callback(self, data): locales = data ScreenHandler.replace_screen(self, locales) def input(self, args, key): """ Handle user input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: # TRANSLATORS: 'b' to go back if key.lower() == C_("TUI|Spoke Navigation|Language Support", "b"): ScreenHandler.replace_screen(self) return InputState.PROCESSED else: return super().input(args, key) def prompt(self, args=None): """ Customize default prompt. """ prompt = NormalTUISpoke.prompt(self, args) prompt.set_message(_("Please select language support to install")) # TRANSLATORS: 'b' to go back prompt.add_option(C_("TUI|Spoke Navigation|Language Support", "b"), _("to return to language list")) return prompt def apply(self): """ Store the selected lang support locales """ self._l12_module.SetLanguage(self._selected)
class PartTypeSpoke(NormalTUISpoke): """ Partitioning options are presented here. .. inheritance-diagram:: PartTypeSpoke :parts: 3 """ category = SystemCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Partitioning Options") self._container = None self._part_type_list = sorted(PARTTYPES.keys()) # remember the original values so that we can detect a change self._disk_init_proxy = STORAGE.get_proxy(DISK_INITIALIZATION) self._orig_init_mode = self._disk_init_proxy.InitializationMode self._manual_part_proxy = STORAGE.get_proxy(MANUAL_PARTITIONING) self._orig_mount_assign = self._manual_part_proxy.Enabled # Create the auto partitioning proxy self._auto_part_proxy = STORAGE.get_proxy(AUTO_PARTITIONING) # default to mount point assignment if it is already (partially) # configured self._do_mount_assign = self._orig_mount_assign if not self._do_mount_assign: self._init_mode = self._disk_init_proxy.InitializationMode else: self._init_mode = CLEAR_PARTITIONS_NONE @property def indirect(self): return True def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for part_type in self._part_type_list: c = CheckboxWidget(title=_(part_type), completed=(not self._do_mount_assign and PARTTYPES[part_type] == self._init_mode) ) self._container.add(c, self._select_partition_type_callback, part_type) c = CheckboxWidget(title=_("Manually assign mount points"), completed=self._do_mount_assign) self._container.add(c, self._select_mount_assign) self.window.add_with_separator(self._container) message = _("Installation requires partitioning of your hard drive. " "Select what space to use for the install target or " "manually assign mount points.") self.window.add_with_separator(TextWidget(message)) def _select_mount_assign(self, data=None): self._init_mode = CLEAR_PARTITIONS_NONE self._do_mount_assign = True self.apply() def _select_partition_type_callback(self, data): self._do_mount_assign = False self._init_mode = PARTTYPES[data] self.apply() def apply(self): # kind of a hack, but if we're actually getting to this spoke, there # is no doubt that we are doing autopartitioning, so set autopart to # True. In the case of ks installs which may not have defined any # partition options, autopart was never set to True, causing some # issues. (rhbz#1001061) if not self._do_mount_assign: self._auto_part_proxy.SetEnabled(True) self._manual_part_proxy.SetEnabled(False) self._disk_init_proxy.SetInitializationMode(self._init_mode) self._disk_init_proxy.SetInitializeLabelsEnabled(True) else: self._auto_part_proxy.SetEnabled(False) self._manual_part_proxy.SetEnabled(True) self._disk_init_proxy.SetInitializationMode(CLEAR_PARTITIONS_NONE) self._disk_init_proxy.SetInitializeLabelsEnabled(False) def _ensure_init_storage(self): """ If a different clearpart type was chosen or mount point assignment was chosen instead, we need to reset/rescan storage to revert all changes done by the previous run of doKickstartStorage() and get everything into the initial state. """ # the only safe options are: # 1) if nothing was set before (self._orig_clearpart_type is None) or if self._orig_init_mode == CLEAR_PARTITIONS_DEFAULT: return # 2) mount point assignment was done before and user just wants to tweak it if self._orig_mount_assign and self._do_mount_assign: return # else print(_("Reverting previous configuration. This may take a moment...")) reset_storage(self.storage, scan_all=True) self._manual_part_proxy.SetMountPoints([]) def input(self, args, key): """Grab the choice and update things""" if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() self._ensure_init_storage() if self._do_mount_assign: new_spoke = MountPointAssignSpoke(self.data, self.storage, self.payload) else: new_spoke = PartitionSchemeSpoke(self.data, self.storage, self.payload) ScreenHandler.push_screen_modal(new_spoke) return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) return InputState.PROCESSED_AND_REDRAW
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_observer() self._timezone_module.connect() @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 # TRANSLATORS: 'b' to go back elif key.lower() == C_('TUI|Spoke Navigation|Time Settings', 'b'): 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")) # TRANSLATORS: 'b' to go back prompt.add_option(C_('TUI|Spoke Navigation|Time Settings', 'b'), _("back to region list")) return prompt def apply(self): self._timezone_module.proxy.SetTimezone(self._selection) self._timezone_module.proxy.SetKickstarted(False)
class UserSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ helpFile = "UserSpoke.txt" category = UserSettingsCategory @classmethod def should_run(cls, environment, data): if FirstbootSpokeMixIn.should_run(environment, data): return True # the user spoke should run always in the anaconda and in firstboot only # when doing reconfig or if no user has been created in the installation if environment == FIRSTBOOT_ENVIRON and data and not data.user.userList: return True return False def __init__(self, data, storage, payload, instclass): FirstbootSpokeMixIn.__init__(self) NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.initialize_start() self.title = N_("User creation") self._container = None if self.data.user.userList: self._user_data = self.data.user.userList[0] self._create_user = True else: self._user_data = self.data.UserData() self._create_user = False self._use_password = self._user_data.isCrypted or self._user_data.password self._groups = "" self._is_admin = False self._policy = self.data.anaconda.pwpolicy.get_policy("user", fallback_to_default=True) self.errors = [] self._users_module = USERS.get_observer() self._users_module.connect() self.initialize_done() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._is_admin = "wheel" in self._user_data.groups self._groups = ", ".join(self._user_data.groups) self._container = ListColumnContainer(1) w = CheckboxWidget(title=_("Create user"), completed=self._create_user) self._container.add(w, self._set_create_user) if self._create_user: dialog = Dialog(title=_("Full name"), conditions=[self._check_fullname]) self._container.add(EntryWidget(dialog.title, self._user_data.gecos), self._set_fullname, dialog) dialog = Dialog(title=_("User name"), conditions=[self._check_username]) self._container.add(EntryWidget(dialog.title, self._user_data.name), self._set_username, dialog) w = CheckboxWidget(title=_("Use password"), completed=self._use_password) self._container.add(w, self._set_use_password) if self._use_password: password_dialog = PasswordDialog(title=_("Password"), policy=self._policy) if self._user_data.password: entry = EntryWidget(password_dialog.title, _(PASSWORD_SET)) else: entry = EntryWidget(password_dialog.title) self._container.add(entry, self._set_password, password_dialog) msg = _("Administrator") w = CheckboxWidget(title=msg, completed=self._is_admin) self._container.add(w, self._set_administrator) dialog = Dialog(title=_("Groups"), conditions=[self._check_groups]) self._container.add(EntryWidget(dialog.title, self._groups), self._set_groups, dialog) self.window.add_with_separator(self._container) @report_if_failed(message=FULLNAME_ERROR_MSG) def _check_fullname(self, user_input, report_func): return GECOS_VALID.match(user_input) is not None @report_check_func() def _check_username(self, user_input, report_func): return check_username(user_input) @report_check_func() def _check_groups(self, user_input, report_func): return check_grouplist(user_input) def _set_create_user(self, args): self._create_user = not self._create_user def _set_fullname(self, dialog): self._user_data.gecos = dialog.run() def _set_username(self, dialog): self._user_data.name = dialog.run() def _set_use_password(self, args): self._use_password = not self._use_password def _set_password(self, password_dialog): password = password_dialog.run() while password is None: password = password_dialog.run() self._user_data.password = password def _set_administrator(self, args): self._is_admin = not self._is_admin def _set_groups(self, dialog): self._groups = dialog.run() def show_all(self): NormalTUISpoke.show_all(self) # if we have any errors, display them while self.errors: print(self.errors.pop()) @property def completed(self): """ Verify a user is created; verify pw is set if option checked. """ if len(self.data.user.userList) > 0: if self._use_password and not bool(self._user_data.password or self._user_data.isCrypted): return False else: return True else: return False @property def showable(self): return not (self.completed and flags.automatedInstall and self.data.user.seen and not self._policy.changesok) @property def mandatory(self): """ Only mandatory if the root pw hasn't been set in the UI eg. not mandatory if the root account was locked in a kickstart """ return not self._users_module.proxy.IsRootPasswordSet and not self._users_module.proxy.IsRootAccountLocked @property def status(self): if len(self.data.user.userList) == 0: return _("No user will be created") elif self._use_password and not bool(self._user_data.password or self._user_data.isCrypted): return _("You must set a password") elif "wheel" in self.data.user.userList[0].groups: return _("Administrator %s will be created") % self.data.user.userList[0].name else: return _("User %s will be created") % self.data.user.userList[0].name def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW return super().input(args, key) def apply(self): if self._user_data.gecos and not self._user_data.name: username = guess_username(self._user_data.gecos) valid, msg = check_username(username) if not valid: self.errors.append(_("Invalid user name: %(name)s.\n%(error_message)s") % {"name": username, "error_message": msg}) else: self._user_data.name = guess_username(self._user_data.gecos) self._user_data.groups = [g.strip() for g in self._groups.split(",") if g] # Add or remove the user from wheel group if self._is_admin and "wheel" not in self._user_data.groups: self._user_data.groups.append("wheel") elif not self._is_admin and "wheel" in self._user_data.groups: self._user_data.groups.remove("wheel") # Add or remove the user from userlist as needed if self._create_user and (self._user_data not in self.data.user.userList and self._user_data.name): self.data.user.userList.append(self._user_data) elif (not self._create_user) and (self._user_data in self.data.user.userList): self.data.user.userList.remove(self._user_data) # encrypt and store password only if user entered anything; this should # preserve passwords set via kickstart if self._use_password and self._user_data.password and len(self._user_data.password) > 0: self._user_data.password = self._user_data.password self._user_data.isCrypted = True # clear pw when user unselects to use pw else: self._user_data.password = "" self._user_data.isCrypted = False
class ConfigureNetworkSpoke(NormalTUISpoke): """ Spoke to set various configuration options for net devices. """ category = "network" def __init__(self, data, storage, payload, instclass, network_data): super().__init__(data, storage, payload, instclass) self.title = N_("Device configuration") self.network_data = network_data if self.network_data.bootProto == "dhcp": self.network_data.ip = "dhcp" if self.network_data.noipv6: self.network_data.ipv6 = "ignore" self.apply_configuration = False self._container = None def refresh(self, args=None): """ Refresh window. """ super().refresh(args) self._container = ListColumnContainer(1) dialog = Dialog(title=(_('IPv4 address or %s for DHCP') % '"dhcp"'), conditions=[self._check_ipv4_or_dhcp]) self._container.add(EntryWidget(dialog.title, self.network_data.ip), self._set_ipv4_or_dhcp, dialog) dialog = Dialog(title=_("IPv4 netmask"), conditions=[self._check_netmask]) self._container.add( EntryWidget(dialog.title, self.network_data.netmask), self._set_netmask, dialog) dialog = Dialog(title=_("IPv4 gateway"), conditions=[self._check_ipv4]) self._container.add( EntryWidget(dialog.title, self.network_data.gateway), self._set_ipv4_gateway, dialog) msg = (_( 'IPv6 address[/prefix] or %(auto)s for automatic, %(dhcp)s for DHCP, ' '%(ignore)s to turn off') % { "auto": '"auto"', "dhcp": '"dhcp"', "ignore": '"ignore"' }) dialog = Dialog(title=msg, conditions=[self._check_ipv6_config]) self._container.add(EntryWidget(dialog.title, self.network_data.ipv6), self._set_ipv6, dialog) dialog = Dialog(title=_("IPv6 default gateway"), conditions=[self._check_ipv6]) self._container.add( EntryWidget(dialog.title, self.network_data.ipv6gateway), self._set_ipv6_gateway, dialog) dialog = Dialog(title=_("Nameservers (comma separated)"), conditions=[self._check_nameservers]) self._container.add( EntryWidget(dialog.title, self.network_data.nameserver), self._set_nameservers, dialog) msg = _("Connect automatically after reboot") w = CheckboxWidget(title=msg, completed=self.network_data.onboot) self._container.add(w, self._set_onboot_handler) msg = _("Apply configuration in installer") w = CheckboxWidget(title=msg, completed=self.apply_configuration) self._container.add(w, self._set_apply_handler) self.window.add_with_separator(self._container) message = _("Configuring device %s.") % self.network_data.device self.window.add_with_separator(TextWidget(message)) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4_or_dhcp(self, user_input, report_func): return IPV4_OR_DHCP_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4(self, user_input, report_func): return IPV4_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=NETMASK_ERROR_MSG) def _check_netmask(self, user_input, report_func): return IPV4_NETMASK_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6(self, user_input, report_func): return network.check_ip_address(user_input, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6_config(self, user_input, report_func): if user_input in ["auto", "dhcp", "ignore"]: return True addr, _slash, prefix = user_input.partition("/") if prefix: try: if not 1 <= int(prefix) <= 128: return False except ValueError: return False return network.check_ip_address(addr, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_nameservers(self, user_input, report_func): if user_input.strip(): addresses = [str.strip(i) for i in user_input.split(",")] for ip in addresses: if not network.check_ip_address(ip): return False return True def _set_ipv4_or_dhcp(self, dialog): self.network_data.ip = dialog.run() def _set_netmask(self, dialog): self.network_data.netmask = dialog.run() def _set_ipv4_gateway(self, dialog): self.network_data.gateway = dialog.run() def _set_ipv6(self, dialog): self.network_data.ipv6 = dialog.run() def _set_ipv6_gateway(self, dialog): self.network_data.ipv6gateway = dialog.run() def _set_nameservers(self, dialog): self.network_data.nameserver = dialog.run() def _set_apply_handler(self, args): self.apply_configuration = not self.apply_configuration def _set_onboot_handler(self, args): self.network_data.onboot = not self.network_data.onboot def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return super().input(args, key) @property def indirect(self): return True def apply(self): """ Apply our changes. """ # save this back to network data, this will be applied in upper layer pass
class RescueModeSpoke(NormalTUISpoke): """UI offering mounting existing installation roots in rescue mode.""" # If it acts like a spoke and looks like a spoke, is it a spoke? Not # always. This is independent of any hub(s), so pass in some fake data def __init__(self, rescue): super().__init__(data=None, storage=None, payload=None) self.title = N_("Rescue") self._container = None self._rescue = rescue def refresh(self, args=None): super().refresh(args) msg = _("The rescue environment will now attempt " "to find your Linux installation and mount it under " "the directory : %s. You can then make any changes " "required to your system. Choose '1' to proceed with " "this step.\nYou can choose to mount your file " "systems read-only instead of read-write by choosing " "'2'.\nIf for some reason this process does not work " "choose '3' to skip directly to a shell.\n\n") % ( conf.target.system_root) self.window.add_with_separator(TextWidget(msg)) self._container = ListColumnContainer(1) self._container.add(TextWidget(_("Continue")), self._read_write_mount_callback) self._container.add(TextWidget(_("Read-only mount")), self._read_only_mount_callback) self._container.add(TextWidget(_("Skip to shell")), self._skip_to_shell_callback) self._container.add(TextWidget(_("Quit (Reboot)")), self._quit_callback) self.window.add_with_separator(self._container) def _read_write_mount_callback(self, data): self._mount_and_prompt_for_shell() def _read_only_mount_callback(self, data): self._rescue.ro = True self._mount_and_prompt_for_shell() def _skip_to_shell_callback(self, data): self._show_result_and_prompt_for_shell() def _quit_callback(self, data): d = YesNoDialog(_(QUIT_MESSAGE)) ScreenHandler.push_screen_modal(d) self.redraw() if d.answer: self._rescue.reboot = True self._rescue.finish() def _mount_and_prompt_for_shell(self): self._rescue.mount = True self._mount_root() self._show_result_and_prompt_for_shell() def prompt(self, args=None): """ Override the default TUI prompt.""" if self._rescue.automated: if self._rescue.mount: self._mount_root() self._show_result_and_prompt_for_shell() return None return Prompt() def input(self, args, key): """Override any input so we can launch rescue mode.""" if self._container.process_user_input(key): return InputState.PROCESSED else: return InputState.DISCARDED def _mount_root(self): # decrypt all luks devices self._unlock_devices() roots = self._rescue.find_roots() if not roots: return if len(roots) == 1: root = roots[0] else: # have to prompt user for which root to mount root_spoke = RootSelectionSpoke(roots) ScreenHandler.push_screen_modal(root_spoke) self.redraw() root = root_spoke.selection self._rescue.mount_root(root) def _show_result_and_prompt_for_shell(self): new_spoke = RescueStatusAndShellSpoke(self._rescue) ScreenHandler.push_screen_modal(new_spoke) self.close() def _unlock_devices(self): """Attempt to unlock all locked LUKS devices. Returns true if all devices were unlocked. """ try_passphrase = None passphrase = None for device_name in self._rescue.get_locked_device_names(): skip = False unlocked = False while not (skip or unlocked): if try_passphrase is None: p = PasswordDialog(device_name) ScreenHandler.push_screen_modal(p) if p.answer: passphrase = p.answer.strip() else: passphrase = try_passphrase if passphrase is None: # cancelled skip = True else: unlocked = self._rescue.unlock_device( device_name, passphrase) try_passphrase = passphrase if unlocked else None return not self._rescue.get_locked_device_names() def apply(self): """Move along home.""" pass
class StorageSpoke(NormalTUISpoke): """Storage spoke where users proceed to customize storage features such as disk selection, partitioning, and fs type. .. inheritance-diagram:: StorageSpoke :parts: 3 """ helpFile = "StorageSpoke.txt" category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Installation Destination") self._ready = False self._container = None self.selected_disks = self.data.ignoredisk.onlyuse[:] self.select_all = False self.autopart = None self.clearPartType = None # This list gets set up once in initialize and should not be modified # except perhaps to add advanced devices. It will remain the full list # of disks that can be included in the install. self.disks = [] self.errors = [] self.warnings = [] if self.data.zerombr.zerombr and arch.is_s390(): # if zerombr is specified in a ks file and there are unformatted # dasds, automatically format them. pass in storage.devicetree here # instead of storage.disks since media_present is checked on disks; # a dasd needing dasdfmt will fail this media check though to_format = [d for d in getDisks(self.storage.devicetree) if d.type == "dasd" and blockdev.s390.dasd_needs_format(d.busid)] if to_format: self.run_dasdfmt(to_format) if not flags.automatedInstall: # default to using autopart for interactive installs self.data.autopart.autopart = True @property def completed(self): retval = bool(self.storage.root_device and not self.errors) return retval @property def ready(self): # By default, the storage spoke is not ready. We have to wait until # storageInitialize is done. return self._ready and not threadMgr.get(THREAD_STORAGE_WATCHER) @property def mandatory(self): return True @property def showable(self): return not flags.dirInstall @property def status(self): """ A short string describing the current status of storage setup. """ msg = _("No disks selected") if flags.automatedInstall and not self.storage.root_device: msg = _("Kickstart insufficient") elif self.data.ignoredisk.onlyuse: msg = P_(("%d disk selected"), ("%d disks selected"), len(self.data.ignoredisk.onlyuse)) % len(self.data.ignoredisk.onlyuse) if self.errors: msg = _("Error checking storage configuration") elif self.warnings: msg = _("Warning checking storage configuration") # Maybe show what type of clearpart and which disks selected? elif self.data.autopart.autopart: msg = _("Automatic partitioning selected") else: msg = _("Custom partitioning selected") return msg def _update_disk_list(self, disk): """ Update self.selected_disks based on the selection.""" name = disk.name # if the disk isn't already selected, select it. if name not in self.selected_disks: self.selected_disks.append(name) # If the disk is already selected, deselect it. elif name in self.selected_disks: self.selected_disks.remove(name) def _update_summary(self): """ Update the summary based on the UI. """ count = 0 capacity = 0 free = Size(0) # pass in our disk list so hidden disks' free space is available free_space = self.storage.get_free_space(disks=self.disks) selected = [d for d in self.disks if d.name in self.selected_disks] for disk in selected: capacity += disk.size free += free_space[disk.name][0] count += 1 summary = (P_(("%d disk selected; %s capacity; %s free ..."), ("%d disks selected; %s capacity; %s free ..."), count) % (count, str(Size(capacity)), free)) if len(self.disks) == 0: summary = _("No disks detected. Please shut down the computer, connect at least one disk, and restart to complete installation.") elif count == 0: summary = (_("No disks selected; please select at least one disk to install to.")) # Append storage errors to the summary if self.errors: summary = summary + "\n" + "\n".join(self.errors) elif self.warnings: summary = summary + "\n" + "\n".join(self.warnings) return summary def refresh(self, args=None): NormalTUISpoke.refresh(self, args) # Join the initialization thread to block on it # This print is foul. Need a better message display print(_(PAYLOAD_STATUS_PROBING_STORAGE)) threadMgr.wait(THREAD_STORAGE_WATCHER) # synchronize our local data store with the global ksdata # Commment out because there is no way to select a disk right # now without putting it in ksdata. Seems wrong? #self.selected_disks = self.data.ignoredisk.onlyuse[:] self.autopart = self.data.autopart.autopart self._container = ListColumnContainer(1, spacing=1) message = self._update_summary() # loop through the disks and present them. for disk in self.disks: disk_info = self._format_disk_info(disk) c = CheckboxWidget(title=disk_info, completed=(disk.name in self.selected_disks)) self._container.add(c, self._update_disk_list_callback, disk) # if we have more than one disk, present an option to just # select all disks if len(self.disks) > 1: c = CheckboxWidget(title=_("Select all"), completed=self.select_all) self._container.add(c, self._select_all_disks_callback) self.window.add_with_separator(self._container) self.window.add_with_separator(TextWidget(message)) def _select_all_disks_callback(self, data): """ Mark all disks as selected for use in partitioning. """ self.select_all = True for disk in self.disks: if disk.name not in self.selected_disks: self._update_disk_list(disk) def _update_disk_list_callback(self, data): disk = data self.select_all = False self._update_disk_list(disk) def _format_disk_info(self, disk): """ Some specialized disks are difficult to identify in the storage spoke, so add and return extra identifying information about them. Since this is going to be ugly to do within the confines of the CheckboxWidget, pre-format the display string right here. """ # show this info for all disks format_str = "%s: %s (%s)" % (disk.model, disk.size, disk.name) disk_attrs = [] # now check for/add info about special disks if (isinstance(disk, MultipathDevice) or isinstance(disk, iScsiDiskDevice) or isinstance(disk, FcoeDiskDevice)): if hasattr(disk, "wwid"): disk_attrs.append(disk.wwid) elif isinstance(disk, DASDDevice): if hasattr(disk, "busid"): disk_attrs.append(disk.busid) elif isinstance(disk, ZFCPDiskDevice): if hasattr(disk, "fcp_lun"): disk_attrs.append(disk.fcp_lun) if hasattr(disk, "wwpn"): disk_attrs.append(disk.wwpn) if hasattr(disk, "hba_id"): disk_attrs.append(disk.hba_id) # now append all additional attributes to our string for attr in disk_attrs: format_str += ", %s" % attr return format_str def input(self, args, key): """Grab the disk choice and update things""" self.errors = [] if self._container.process_user_input(key): self.redraw() return InputState.PROCESSED else: # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): if self.selected_disks: # check selected disks to see if we have any unformatted DASDs # if we're on s390x, since they need to be formatted before we # can use them. if arch.is_s390(): _disks = [d for d in self.disks if d.name in self.selected_disks] to_format = [d for d in _disks if d.type == "dasd" and blockdev.s390.dasd_needs_format(d.busid)] if to_format: self.run_dasdfmt(to_format) self.redraw() return InputState.PROCESSED # make sure no containers were split up by the user's disk # selection self.errors.extend(checkDiskSelection(self.storage, self.selected_disks)) if self.errors: # The disk selection has to make sense before we can # proceed. self.redraw() return InputState.PROCESSED new_spoke = AutoPartSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.execute() self.close() return InputState.PROCESSED else: return super(StorageSpoke, self).input(args, key) def run_dasdfmt(self, to_format): """ This generates the list of DASDs requiring dasdfmt and runs dasdfmt against them. """ # if the storage thread is running, wait on it to complete before taking # any further actions on devices; most likely to occur if user has # zerombr in their ks file threadMgr.wait(THREAD_STORAGE) # ask user to verify they want to format if zerombr not in ks file if not self.data.zerombr.zerombr: # prepare our msg strings; copied directly from dasdfmt.glade summary = _("The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs cannot be used during installation.\n\n") warntext = _("Warning: All storage changes made using the installer will be lost when you choose to format.\n\nProceed to run dasdfmt?\n") displaytext = summary + "\n".join("/dev/" + d.name for d in to_format) + "\n" + warntext # now show actual prompt; note -- in cmdline mode, auto-answer for # this is 'no', so unformatted DASDs will remain so unless zerombr # is added to the ks file question_window = YesNoDialog(displaytext) ScreenHandler.push_screen_modal(question_window) if not question_window.answer: # no? well fine then, back to the storage spoke with you; return None for disk in to_format: try: print(_("Formatting /dev/%s. This may take a moment.") % disk.name) blockdev.s390.dasd_format(disk.name) except blockdev.S390Error as err: # Log errors if formatting fails, but don't halt the installer log.error(str(err)) continue def apply(self): self.autopart = self.data.autopart.autopart self.data.ignoredisk.onlyuse = self.selected_disks[:] self.data.clearpart.drives = self.selected_disks[:] if self.data.autopart.type is None: self.data.autopart.type = AUTOPART_TYPE_LVM if self.autopart: self.clearPartType = CLEARPART_TYPE_ALL else: self.clearPartType = CLEARPART_TYPE_NONE for disk in self.disks: if disk.name not in self.selected_disks and \ disk in self.storage.devices: self.storage.devicetree.hide(disk) elif disk.name in self.selected_disks and \ disk not in self.storage.devices: self.storage.devicetree.unhide(disk) self.data.bootloader.location = "mbr" if self.data.bootloader.bootDrive and \ self.data.bootloader.bootDrive not in self.selected_disks: self.data.bootloader.bootDrive = "" self.storage.bootloader.reset() self.storage.config.update(self.data) # If autopart is selected we want to remove whatever has been # created/scheduled to make room for autopart. # If custom is selected, we want to leave alone any storage layout the # user may have set up before now. self.storage.config.clear_non_existent = self.data.autopart.autopart def execute(self): print(_("Generating updated storage configuration")) try: doKickstartStorage(self.storage, self.data, self.instclass) except (StorageError, KickstartParseError) as e: log.error("storage configuration failed: %s", e) print(_("storage configuration failed: %s") % e) self.errors = [str(e)] self.data.bootloader.bootDrive = "" self.data.clearpart.type = CLEARPART_TYPE_ALL self.data.clearpart.initAll = False self.storage.config.update(self.data) self.storage.autopart_type = self.data.autopart.type self.storage.reset() # now set ksdata back to the user's specified config applyDiskSelection(self.storage, self.data, self.selected_disks) except BootLoaderError as e: log.error("BootLoader setup failed: %s", e) print(_("storage configuration failed: %s") % e) self.errors = [str(e)] self.data.bootloader.bootDrive = "" else: print(_("Checking storage configuration...")) report = storage_checker.check(self.storage) print("\n".join(report.all_errors)) report.log(log) self.errors = report.errors self.warnings = report.warnings finally: resetCustomStorageData(self.data) self._ready = True def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() threadMgr.add(AnacondaThread(name=THREAD_STORAGE_WATCHER, target=self._initialize)) self.selected_disks = self.data.ignoredisk.onlyuse[:] # Probably need something here to track which disks are selected? def _initialize(self): """ Secondary initialize so wait for the storage thread to complete before populating our disk list """ threadMgr.wait(THREAD_STORAGE) self.disks = sorted(getDisks(self.storage.devicetree), key=lambda d: d.name) # if only one disk is available, go ahead and mark it as selected if len(self.disks) == 1: self._update_disk_list(self.disks[0]) self._update_summary() self._ready = True # report that the storage spoke has been initialized self.initialize_done()
class SpecifyRepoSpoke(NormalTUISpoke, SourceSwitchHandler): """ Specify the repo URL here if closest mirror not selected. """ category = SoftwareCategory HTTP = 1 HTTPS = 2 FTP = 3 def __init__(self, data, storage, payload, protocol): NormalTUISpoke.__init__(self, data, storage, payload) SourceSwitchHandler.__init__(self) self.title = N_("Specify Repo Options") self.protocol = protocol self._container = None self._url = self._get_url() def _get_url(self): """Get the URL of the current source.""" source_proxy = self.payload.get_source_proxy() if source_proxy.Type == SOURCE_TYPE_URL: repo_configuration = RepoConfigurationData.from_structure( source_proxy.RepoConfiguration ) return repo_configuration.url return "" def refresh(self, args=None): """ Refresh window. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) dialog = Dialog(_("Repo URL")) self._container.add(EntryWidget(dialog.title, self._url), self._set_repo_url, dialog) self.window.add_with_separator(self._container) def _set_repo_url(self, dialog): self._url = dialog.run() def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return NormalTUISpoke.input(self, args, key) @property def indirect(self): return True def apply(self): """ Apply all of our changes. """ if self.protocol == SpecifyRepoSpoke.HTTP and not self._url.startswith("http://"): url = "http://" + self._url elif self.protocol == SpecifyRepoSpoke.HTTPS and not self._url.startswith("https://"): url = "https://" + self._url elif self.protocol == SpecifyRepoSpoke.FTP and not self._url.startswith("ftp://"): url = "ftp://" + self._url else: # protocol either unknown or entry already starts with a protocol # specification url = self._url self.set_source_url(url)
class SelectDeviceSpoke(NormalTUISpoke): """ Select device containing the install source ISO file. """ category = SoftwareCategory def __init__(self, data, storage, payload, instclass): super().__init__(data, storage, payload, instclass) self.title = N_("Select device containing the ISO file") self._container = None self._mountable_devices = self._get_mountable_devices() self._device = None @property def indirect(self): return True def _sanitize_model(self, model): return model.replace("_", " ") def _get_mountable_devices(self): disks = [] fstring = "%(model)s %(path)s (%(size)s MB) %(format)s %(label)s" for dev in potentialHdisoSources(self.storage.devicetree): # path model size format type uuid of format dev_info = {"model": self._sanitize_model(dev.disk.model), "path": dev.path, "size": dev.size, "format": dev.format.name or "", "label": dev.format.label or dev.format.uuid or "" } disks.append([dev, fstring % dev_info]) return disks def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) # check if the storage refresh thread is running if threadMgr.get(THREAD_STORAGE_WATCHER): # storage refresh is running - just report it # so that the user can refresh until it is done # TODO: refresh once the thread is done ? message = _(PAYLOAD_STATUS_PROBING_STORAGE) self.window.add_with_separator(TextWidget(message)) # check if there are any mountable devices if self._mountable_devices: for d in self._mountable_devices: self._container.add(TextWidget(d[1]), callback=self._select_mountable_device, data=d[0]) self.window.add_with_separator(self._container) else: message = _("No mountable devices found") self.window.add_with_separator(TextWidget(message)) def _select_mountable_device(self, data): self._device = data new_spoke = SelectISOSpoke(self.data, self.storage, self.payload, self.instclass, self._device) ScreenHandler.push_screen_modal(new_spoke) self.close() def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED else: # either the input was not a number or # we don't have the disk for the given number return super().input(args, key) # Override Spoke.apply def apply(self): pass
class SelectDeviceSpoke(NormalTUISpoke): """ Select device containing the install source ISO file. """ category = SoftwareCategory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Select device containing the ISO file") self._container = None self._device_tree = STORAGE.get_proxy(DEVICE_TREE) self._mountable_devices = self._get_mountable_devices() self._device = None @property def indirect(self): return True def _get_mountable_devices(self): disks = [] for device_name in find_potential_hdiso_sources(): device_info = get_hdiso_source_info(self._device_tree, device_name) device_desc = get_hdiso_source_description(device_info) disks.append([device_name, device_desc]) return disks def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) # check if the storage refresh thread is running if threadMgr.get(THREAD_STORAGE_WATCHER): # storage refresh is running - just report it # so that the user can refresh until it is done # TODO: refresh once the thread is done ? message = _(PAYLOAD_STATUS_PROBING_STORAGE) self.window.add_with_separator(TextWidget(message)) # check if there are any mountable devices if self._mountable_devices: for d in self._mountable_devices: self._container.add(TextWidget(d[1]), callback=self._select_mountable_device, data=d[0]) self.window.add_with_separator(self._container) else: message = _("No mountable devices found") self.window.add_with_separator(TextWidget(message)) def _select_mountable_device(self, data): self._device = data new_spoke = SelectISOSpoke(self.data, self.storage, self.payload, self._device) ScreenHandler.push_screen_modal(new_spoke) self.close() def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED else: # either the input was not a number or # we don't have the disk for the given number return super().input(args, key) # Override Spoke.apply def apply(self): pass
class SourceSpoke(NormalTUISpoke, SourceSwitchHandler): """ Spoke used to customize the install source repo. .. inheritance-diagram:: SourceSpoke :parts: 3 """ helpFile = "SourceSpoke.txt" category = SoftwareCategory SET_NETWORK_INSTALL_MODE = "network_install" def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Installation source") self._container = None self._ready = False self._error = False self._cdrom = None self._hmc = False def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() threadMgr.add(AnacondaThread(name=THREAD_SOURCE_WATCHER, target=self._initialize)) payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error) def _initialize(self): """ Private initialize. """ threadMgr.wait(THREAD_PAYLOAD) # If we've previously set up to use a CD/DVD method, the media has # already been mounted by payload.setup. We can't try to mount it # again. So just use what we already know to create the selector. # Otherwise, check to see if there's anything available. if self.data.method.method == "cdrom": self._cdrom = self.payload.install_device elif not flags.automatedInstall: self._cdrom = opticalInstallMedia(self.storage.devicetree) # Enable the SE/HMC option. if flags.hmc: self._hmc = True self._ready = True # report that the source spoke has been initialized self.initialize_done() def _payload_error(self): self._error = True def _repo_status(self): """ Return a string describing repo url or lack of one. """ method = self.data.method if method.method == "url": return method.url or method.mirrorlist or method.metalink elif method.method == "nfs": return _("NFS server %s") % method.server elif method.method == "cdrom": return _("Local media") elif method.method == "hmc": return _("Local media via SE/HMC") elif method.method == "harddrive": if not method.dir: return _("Error setting up software source") return os.path.basename(method.dir) elif self.payload.baseRepo: return _("Closest mirror") else: return _("Nothing selected") @property def showable(self): return isinstance(self.payload, PackagePayload) @property def status(self): if self._error: return _("Error setting up software source") elif not self.ready: return _("Processing...") else: return self._repo_status() @property def completed(self): if flags.automatedInstall and self.ready and not self.payload.baseRepo: return False else: return not self._error and self.ready and (self.data.method.method or self.payload.baseRepo) def refresh(self, args=None): NormalTUISpoke.refresh(self, args) threadMgr.wait(THREAD_PAYLOAD) self._container = ListColumnContainer(1, columns_width=78, spacing=1) if self.data.method.method == "harddrive" and \ get_mount_device(DRACUT_ISODIR) == get_mount_device(DRACUT_REPODIR): message = _("The installation source is in use by the installer and cannot be changed.") self.window.add_with_separator(TextWidget(message)) return if args == self.SET_NETWORK_INSTALL_MODE: if self.payload.mirrors_available: self._container.add(TextWidget(_("Closest mirror")), self._set_network_close_mirror) self._container.add(TextWidget("http://"), self._set_network_url, SpecifyRepoSpoke.HTTP) self._container.add(TextWidget("https://"), self._set_network_url, SpecifyRepoSpoke.HTTPS) self._container.add(TextWidget("ftp://"), self._set_network_url, SpecifyRepoSpoke.FTP) self._container.add(TextWidget("nfs"), self._set_network_nfs) else: self.window.add(TextWidget(_("Choose an installation source type."))) self._container.add(TextWidget(_("CD/DVD")), self._set_cd_install_source) self._container.add(TextWidget(_("local ISO file")), self._set_iso_install_source) self._container.add(TextWidget(_("Network")), self._set_network_install_source) if self._hmc: self._container.add(TextWidget(_("SE/HMC")), self._set_hmc_install_source) self.window.add_with_separator(self._container) # Set installation source callbacks def _set_cd_install_source(self, data): self.set_source_cdrom() self.payload.install_device = self._cdrom self.apply() self.close() def _set_hmc_install_source(self, data): self.set_source_hmc() self.apply() self.close() def _set_iso_install_source(self, data): new_spoke = SelectDeviceSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_install_source(self, data): ScreenHandler.replace_screen(self, self.SET_NETWORK_INSTALL_MODE) # Set network source callbacks def _set_network_close_mirror(self, data): self.set_source_closest_mirror() self.apply() self.close() def _set_network_url(self, data): new_spoke = SpecifyRepoSpoke(self.data, self.storage, self.payload, self.instclass, data) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_nfs(self, data): self.set_source_nfs() new_spoke = SpecifyNFSRepoSpoke(self.data, self.storage, self.payload, self.instclass, self._error) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def input(self, args, key): """ Handle the input; this decides the repo source. """ if not self._container.process_user_input(key): return super().input(args, key) return InputState.PROCESSED @property def ready(self): """ Check if the spoke is ready. """ return (self._ready and not threadMgr.get(THREAD_PAYLOAD) and not threadMgr.get(THREAD_CHECK_SOFTWARE)) def apply(self): """ Execute the selections made. """ # If askmethod was provided on the command line, entering the source # spoke wipes that out. if flags.askmethod: flags.askmethod = False # if we had any errors, e.g. from a previous attempt to set the source, # clear them at this point self._error = False payloadMgr.restartThread(self.storage, self.data, self.payload, self.instclass, checkmount=False)
class SourceSpoke(NormalTUISpoke, SourceSwitchHandler): """ Spoke used to customize the install source repo. .. inheritance-diagram:: SourceSpoke :parts: 3 """ helpFile = "SourceSpoke.txt" category = SoftwareCategory SET_NETWORK_INSTALL_MODE = "network_install" @classmethod def should_run(cls, environment, data): """Don't run for any non-package payload.""" if not NormalTUISpoke.should_run(environment, data): return False return context.payload_type == PAYLOAD_TYPE_DNF def __init__(self, data, storage, payload): NormalTUISpoke.__init__(self, data, storage, payload) SourceSwitchHandler.__init__(self) self.title = N_("Installation source") self._container = None self._ready = False self._error = False self._hmc = False def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() threadMgr.add(AnacondaThread(name=THREAD_SOURCE_WATCHER, target=self._initialize)) payloadMgr.add_listener(PayloadState.ERROR, self._payload_error) def _initialize(self): """ Private initialize. """ threadMgr.wait(THREAD_PAYLOAD) # Enable the SE/HMC option. if self.payload.source_type == SOURCE_TYPE_HMC: self._hmc = True self._ready = True # report that the source spoke has been initialized self.initialize_done() def _payload_error(self): self._error = True @property def status(self): if self._error: return _("Error setting up software source") elif not self.ready: return _("Processing...") elif not self.payload.is_complete(): return _("Nothing selected") else: source_proxy = self.payload.get_source_proxy() return source_proxy.Description @property def completed(self): if flags.automatedInstall and self.ready and not self.payload.base_repo: return False return not self._error and self.ready and self.payload.is_complete() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) threadMgr.wait(THREAD_PAYLOAD) self._container = ListColumnContainer(1, columns_width=78, spacing=1) if args == self.SET_NETWORK_INSTALL_MODE: if conf.payload.enable_closest_mirror: self._container.add(TextWidget(_("Closest mirror")), self._set_network_close_mirror) self._container.add(TextWidget("http://"), self._set_network_url, SpecifyRepoSpoke.HTTP) self._container.add(TextWidget("https://"), self._set_network_url, SpecifyRepoSpoke.HTTPS) self._container.add(TextWidget("ftp://"), self._set_network_url, SpecifyRepoSpoke.FTP) self._container.add(TextWidget("nfs"), self._set_network_nfs) else: self.window.add(TextWidget(_("Choose an installation source type."))) self._container.add(TextWidget(_("CD/DVD")), self._set_cd_install_source) self._container.add(TextWidget(_("local ISO file")), self._set_iso_install_source) self._container.add(TextWidget(_("Network")), self._set_network_install_source) if self._hmc: self._container.add(TextWidget(_("SE/HMC")), self._set_hmc_install_source) self.window.add_with_separator(self._container) # Set installation source callbacks def _set_cd_install_source(self, data): self.set_source_cdrom() self.apply() self.close() def _set_hmc_install_source(self, data): self.set_source_hmc() self.apply() self.close() def _set_iso_install_source(self, data): new_spoke = SelectDeviceSpoke(self.data, self.storage, self.payload) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_install_source(self, data): ScreenHandler.replace_screen(self, self.SET_NETWORK_INSTALL_MODE) # Set network source callbacks def _set_network_close_mirror(self, data): self.set_source_closest_mirror() self.apply() self.close() def _set_network_url(self, data): new_spoke = SpecifyRepoSpoke(self.data, self.storage, self.payload, data) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_nfs(self, data): new_spoke = SpecifyNFSRepoSpoke(self.data, self.storage, self.payload, self._error) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def input(self, args, key): """ Handle the input; this decides the repo source. """ if not self._container.process_user_input(key): return super().input(args, key) return InputState.PROCESSED @property def ready(self): """ Check if the spoke is ready. """ return (self._ready and not threadMgr.get(THREAD_PAYLOAD) and not threadMgr.get(THREAD_CHECK_SOFTWARE)) def apply(self): """ Execute the selections made. """ # if we had any errors, e.g. from a previous attempt to set the source, # clear them at this point self._error = False payloadMgr.restart_thread(self.payload, checkmount=False)
class MountPointAssignSpoke(NormalTUISpoke): """ Assign mount points to block devices. """ category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Assign mount points") self._container = None self._mds = None self._gather_mount_data_info() def _is_dev_usable(self, dev): maybe = not dev.protected and dev.size != Size(0) if maybe and self.data.ignoredisk.onlyuse: # all device's disks have to be in ignoredisk.onlyuse maybe = set(self.data.ignoredisk.onlyuse).issuperset( {d.name for d in dev.disks}) return maybe def _gather_mount_data_info(self): self._mds = OrderedDict() for device in filter(self._is_dev_usable, self.storage.devicetree.leaves): fmt = device.format.type for ks_md in self.data.mount.dataList(): if device is self.storage.devicetree.resolve_device( ks_md.device): # already have a configuration for the device in ksdata, # let's just copy it mdrec = MountDataRecorder(device=ks_md.device, mount_point=ks_md.mount_point, format=ks_md.format, reformat=ks_md.reformat) # and make sure the new version is put back self.data.mount.remove_mount_data(ks_md) mdrec.modified = True break else: if device.format.mountable and device.format.mountpoint: mpoint = device.format.mountpoint else: mpoint = None mdrec = MountDataRecorder(device=device.path, mount_point=mpoint, format=fmt, reformat=False) mdrec.orig_format = fmt self._mds[device.name] = mdrec @property def indirect(self): return True def prompt(self, args=None): prompt = super(MountPointAssignSpoke, self).prompt(args) # TRANSLATORS: 's' to rescan devices prompt.add_option(C_('TUI|Spoke Navigation|Partitioning', 's'), _("rescan devices")) return prompt def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(2) for md in self._mds.values(): device = self.storage.devicetree.resolve_device(md.device) devspec = "%s (%s)" % (md.device, device.size) if md.format: devspec += "\n %s" % md.format if md.reformat: devspec += "*" if md.mount_point: devspec += ", %s" % md.mount_point w = TextWidget(devspec) self._container.add(w, self._configure_device, device) self.window.add_with_separator(self._container) message = _( "Choose device from above to assign mount point and set format.\n" + "Formats marked with * are new formats meaning ALL DATA on the original format WILL BE LOST!" ) self.window.add_with_separator(TextWidget(message)) def _configure_device(self, device): md = self._mds[device.name] new_spoke = ConfigureDeviceSpoke(self.data, self.storage, self.payload, self.instclass, md) ScreenHandler.push_screen(new_spoke) def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): # TRANSLATORS: 's' to rescan devices if key.lower() == C_('TUI|Spoke Navigation|Partitioning', 's'): text = _( "Warning: This will revert all changes done so far.\nDo you want to proceed?\n" ) question_window = YesNoDialog(text) ScreenHandler.push_screen_modal(question_window) if question_window.answer: # unset self.data.ignoredisk.onlyuse temporarily so that # storage_initialize() processes all devices ignoredisk = self.data.ignoredisk.onlyuse self.data.ignoredisk.onlyuse = [] print(_("Scanning disks. This may take a moment...")) storage_initialize( self.storage, self.data, self.storage.devicetree.protected_dev_names) self.data.ignoredisk.onlyuse = ignoredisk self.data.mount.clear_mount_data() self._gather_mount_data_info() self.redraw() return InputState.PROCESSED # TRANSLATORS: 'c' to continue elif key.lower() == C_('TUI|Spoke Navigation', 'c'): self.apply() return super(MountPointAssignSpoke, self).input(args, key) return InputState.PROCESSED def apply(self): """ Apply our selections. """ for mount_data in self._mds.values(): if mount_data.modified and (mount_data.reformat or mount_data.mount_point): self.data.mount.add_mount_data(mount_data)
class NetworkSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ Spoke used to configure network settings. .. inheritance-diagram:: NetworkSpoke :parts: 3 """ helpFile = "NetworkSpoke.txt" category = SystemCategory configurable_device_types = [ NM.DeviceType.ETHERNET, NM.DeviceType.INFINIBAND, ] def __init__(self, data, storage, payload): NormalTUISpoke.__init__(self, data, storage, payload) self.title = N_("Network configuration") self._network_module = NETWORK.get_proxy() self.nm_client = network.get_nm_client() if not self.nm_client and conf.system.provides_system_bus: self.nm_client = NM.Client.new(None) self._container = None self.hostname = self._network_module.Hostname self.editable_configurations = [] self.errors = [] self._apply = False @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not FirstbootSpokeMixIn.should_run(environment, data): return False return conf.system.can_configure_network def initialize(self): self.initialize_start() NormalTUISpoke.initialize(self) self._update_editable_configurations() self._network_module.DeviceConfigurationChanged.connect( self._device_configurations_changed) self.initialize_done() def _device_configurations_changed(self, device_configurations): log.debug("device configurations changed: %s", device_configurations) self._update_editable_configurations() def _update_editable_configurations(self): device_configurations = NetworkDeviceConfiguration.from_structure_list( self._network_module.GetDeviceConfigurations()) self.editable_configurations = [ dc for dc in device_configurations if dc.device_type in self.configurable_device_types ] @property def completed(self): """ Check whether this spoke is complete or not.""" # If we can't configure network, don't require it return (not conf.system.can_configure_network or self._network_module.GetActivatedInterfaces()) @property def mandatory(self): # the network spoke should be mandatory only if it is running # during the installation and if the installation source requires network return ANACONDA_ENVIRON in flags.environs and self.payload.needs_network @property def status(self): """ Short msg telling what devices are active. """ return network.status_message(self.nm_client) def _summary_text(self): """Devices cofiguration shown to user.""" msg = "" activated_devs = self._network_module.GetActivatedInterfaces() for device_configuration in self.editable_configurations: name = device_configuration.device_name if name in activated_devs: msg += self._activated_device_msg(name) else: msg += _("Wired (%(interface_name)s) disconnected\n") \ % {"interface_name": name} return msg def _activated_device_msg(self, devname): msg = _("Wired (%(interface_name)s) connected\n") \ % {"interface_name": devname} device = self.nm_client.get_device_by_iface(devname) if device: addr_str = dnss_str = gateway_str = netmask_str = "" ipv4config = device.get_ip4_config() if ipv4config: addresses = ipv4config.get_addresses() if addresses: a0 = addresses[0] addr_str = a0.get_address() prefix = a0.get_prefix() netmask_str = network.prefix_to_netmask(prefix) gateway_str = ipv4config.get_gateway() or '' dnss_str = ",".join(ipv4config.get_nameservers()) msg += _(" IPv4 Address: %(addr)s Netmask: %(netmask)s Gateway: %(gateway)s\n") % \ {"addr": addr_str, "netmask": netmask_str, "gateway": gateway_str} msg += _(" DNS: %s\n") % dnss_str ipv6config = device.get_ip6_config() if ipv6config: for address in ipv6config.get_addresses(): addr_str = address.get_address() prefix = address.get_prefix() # Do not display link-local addresses if not addr_str.startswith("fe80:"): msg += _(" IPv6 Address: %(addr)s/%(prefix)d\n") % \ {"addr": addr_str, "prefix": prefix} return msg def refresh(self, args=None): """ Refresh screen. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) if not self.nm_client: self.window.add_with_separator( TextWidget(_("Network configuration is not available."))) return summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) hostname = _("Host Name: %s\n") % self._network_module.Hostname self.window.add_with_separator(TextWidget(hostname)) current_hostname = _("Current host name: %s\n" ) % self._network_module.GetCurrentHostname() self.window.add_with_separator(TextWidget(current_hostname)) # if we have any errors, display them while len(self.errors) > 0: self.window.add_with_separator(TextWidget(self.errors.pop())) dialog = Dialog(_("Host Name")) self._container.add(TextWidget(_("Set host name")), callback=self._set_hostname_callback, data=dialog) for device_configuration in self.editable_configurations: iface = device_configuration.device_name text = (_("Configure device %s") % iface) self._container.add(TextWidget(text), callback=self._ensure_connection_and_configure, data=iface) self.window.add_with_separator(self._container) def _set_hostname_callback(self, dialog): self.hostname = dialog.run() self.redraw() self.apply() def _ensure_connection_and_configure(self, iface): for device_configuration in self.editable_configurations: if device_configuration.device_name == iface: connection_uuid = device_configuration.connection_uuid if connection_uuid: self._configure_connection(iface, connection_uuid) else: device_type = self.nm_client.get_device_by_iface( iface).get_device_type() connection = get_default_connection(iface, device_type) connection_uuid = connection.get_uuid() log.debug("adding default connection %s for %s", connection_uuid, iface) data = (iface, connection_uuid) self.nm_client.add_connection2( connection.to_dbus( NM.ConnectionSerializationFlags.ALL), (NM.SettingsAddConnection2Flags.TO_DISK | NM.SettingsAddConnection2Flags.BLOCK_AUTOCONNECT), None, False, None, self._default_connection_added_cb, data) return log.error("device configuration for %s not found", iface) def _default_connection_added_cb(self, client, result, data): iface, connection_uuid = data try: _connection, result = client.add_connection2_finish(result) except Exception as e: # pylint: disable=broad-except msg = "adding default connection {} from {} failed: {}".format( connection_uuid, iface, str(e)) log.error(msg) self.errors.append(msg) self.redraw() else: log.debug("added default connection %s for %s: %s", connection_uuid, iface, result) self._configure_connection(iface, connection_uuid) def _configure_connection(self, iface, connection_uuid): connection = self.nm_client.get_connection_by_uuid(connection_uuid) new_spoke = ConfigureDeviceSpoke(self.data, self.storage, self.payload, self._network_module, iface, connection) ScreenHandler.push_screen_modal(new_spoke) if new_spoke.errors: self.errors.extend(new_spoke.errors) self.redraw() return if new_spoke.apply_configuration: self._apply = True device = self.nm_client.get_device_by_iface(iface) log.debug("activating connection %s with device %s", connection_uuid, iface) self.nm_client.activate_connection_async(connection, device, None, None) self._network_module.LogConfigurationState( "Settings of {} updated in TUI.".format(iface)) self.redraw() self.apply() def input(self, args, key): """ Handle the input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: return super().input(args, key) def apply(self): """Apply all of our settings.""" # Inform network module that device configurations might have been changed # and we want to generate kickstart from device configurations # (persistent NM / config files configuration), instead of using original kickstart. self._network_module.NetworkDeviceConfigurationChanged() (valid, error) = network.is_valid_hostname(self.hostname, local=True) if not self.hostname or valid: self._network_module.SetHostname(self.hostname) else: self.errors.append(_("Host name is not valid: %s") % error) self.hostname = self._network_module.Hostname if self._apply: self._apply = False if ANACONDA_ENVIRON in flags.environs: from pyanaconda.payload.manager import payloadMgr payloadMgr.restart_thread(self.payload, checkmount=False)
class ConfigureDeviceSpoke(NormalTUISpoke): """ Assign mount point to a block device and (optionally) reformat it. """ category = SystemCategory def __init__(self, data, storage, payload, instclass, mount_data): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self._container = None self._mount_data = mount_data self.title = N_("Configure device: %s") % mount_data.device self._supported_filesystems = [ fmt.type for fmt in get_supported_filesystems() ] @property def indirect(self): return True def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) mount_point_title = _("Mount point") reformat_title = _("Reformat") none_msg = _("none") fmt = get_format(self._mount_data.format) if fmt and fmt.mountable: dialog = Dialog(mount_point_title, conditions=[self._check_assign_mount_point]) value = self._mount_data.mount_point or none_msg self._container.add(EntryWidget(dialog.title, value), self._assign_mount_point, dialog) elif fmt and fmt.type is None: # mount point cannot be set for no format # (fmt.name = "Uknown" in this case which would look weird) self._container.add(EntryWidget(mount_point_title, none_msg), lambda x: self.redraw()) else: # mount point cannot be set for format that is not mountable, just # show the format's name in square brackets instead self._container.add(EntryWidget(mount_point_title, fmt.name), lambda x: self.redraw()) dialog = Dialog(_("Format"), conditions=[self._check_format]) value = self._mount_data.format or none_msg self._container.add(EntryWidget(dialog.title, value), self._set_format, dialog) if ((self._mount_data.orig_format and self._mount_data.orig_format != self._mount_data.format) or self._mount_data.mount_point == "/"): # changing format implies reformat and so does "/" mount point self._container.add( CheckboxWidget(title=reformat_title, completed=self._mount_data.reformat)) else: self._container.add( CheckboxWidget(title=reformat_title, completed=self._mount_data.reformat), self._switch_reformat) self.window.add_with_separator(self._container) self.window.add_with_separator( TextWidget( _("Choose from above to assign mount point and/or set format.") )) def _check_format(self, user_input, report_func): user_input = user_input.lower() if user_input in self._supported_filesystems: return True else: msg = _("Invalid or unsupported format given") msg += "\n" msg += (_("Supported formats: %s") % ", ".join(self._supported_filesystems)) report_func(msg) return False def _check_assign_mount_point(self, user_input, report_func): # a valid mount point must start with / or user set nothing if user_input == "" or user_input.startswith("/"): return True else: report_func(_("Invalid mount point given")) return False def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): return super(ConfigureDeviceSpoke, self).input(args, key) self.redraw() return InputState.PROCESSED def apply(self): # nothing to do here, the callbacks below directly modify the data pass def _switch_reformat(self, args): self._mount_data.modified = True self._mount_data.reformat = not self._mount_data.reformat def _set_format(self, dialog): self._mount_data.modified = True value = dialog.run() if value != self._mount_data.format: self._mount_data.reformat = True self._mount_data.format = value def _assign_mount_point(self, dialog): self._mount_data.modified = True value = dialog.run() if value: self._mount_data.mount_point = value else: self._mount_data.mount_point = None if self._mount_data.mount_point == "/": self._mount_data.reformat = True
class ConfigureNetworkSpoke(NormalTUISpoke): """ Spoke to set various configuration options for net devices. """ category = "network" def __init__(self, data, storage, payload, instclass, network_data): super().__init__(data, storage, payload, instclass) self.title = N_("Device configuration") self.network_data = network_data if self.network_data.bootProto == "dhcp": self.network_data.ip = "dhcp" if self.network_data.noipv6: self.network_data.ipv6 = "ignore" self.apply_configuration = False self._container = None def refresh(self, args=None): """ Refresh window. """ super().refresh(args) self._container = ListColumnContainer(1) dialog = Dialog(title=(_('IPv4 address or %s for DHCP') % '"dhcp"'), conditions=[self._check_ipv4_or_dhcp]) self._container.add(EntryWidget(dialog.title, self.network_data.ip), self._set_ipv4_or_dhcp, dialog) dialog = Dialog(title=_("IPv4 netmask"), conditions=[self._check_netmask]) self._container.add(EntryWidget(dialog.title, self.network_data.netmask), self._set_netmask, dialog) dialog = Dialog(title=_("IPv4 gateway"), conditions=[self._check_ipv4]) self._container.add(EntryWidget(dialog.title, self.network_data.gateway), self._set_ipv4_gateway, dialog) msg = (_('IPv6 address[/prefix] or %(auto)s for automatic, %(dhcp)s for DHCP, ' '%(ignore)s to turn off') % {"auto": '"auto"', "dhcp": '"dhcp"', "ignore": '"ignore"'}) dialog = Dialog(title=msg, conditions=[self._check_ipv6_config]) self._container.add(EntryWidget(dialog.title, self.network_data.ipv6), self._set_ipv6, dialog) dialog = Dialog(title=_("IPv6 default gateway"), conditions=[self._check_ipv6]) self._container.add(EntryWidget(dialog.title, self.network_data.ipv6gateway), self._set_ipv6_gateway, dialog) dialog = Dialog(title=_("Nameservers (comma separated)"), conditions=[self._check_nameservers]) self._container.add(EntryWidget(dialog.title, self.network_data.nameserver), self._set_nameservers, dialog) msg = _("Connect automatically after reboot") w = CheckboxWidget(title=msg, completed=self.network_data.onboot) self._container.add(w, self._set_onboot_handler) msg = _("Apply configuration in installer") w = CheckboxWidget(title=msg, completed=self.apply_configuration) self._container.add(w, self._set_apply_handler) self.window.add_with_separator(self._container) message = _("Configuring device %s.") % self.network_data.device self.window.add_with_separator(TextWidget(message)) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4_or_dhcp(self, user_input, report_func): return IPV4_OR_DHCP_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4(self, user_input, report_func): return IPV4_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=NETMASK_ERROR_MSG) def _check_netmask(self, user_input, report_func): return IPV4_NETMASK_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6(self, user_input, report_func): return network.check_ip_address(user_input, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6_config(self, user_input, report_func): if user_input in ["auto", "dhcp", "ignore"]: return True addr, _slash, prefix = user_input.partition("/") if prefix: try: if not 1 <= int(prefix) <= 128: return False except ValueError: return False return network.check_ip_address(addr, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_nameservers(self, user_input, report_func): if user_input.strip(): addresses = [str.strip(i) for i in user_input.split(",")] for ip in addresses: if not network.check_ip_address(ip): return False return True def _set_ipv4_or_dhcp(self, dialog): self.network_data.ip = dialog.run() def _set_netmask(self, dialog): self.network_data.netmask = dialog.run() def _set_ipv4_gateway(self, dialog): self.network_data.gateway = dialog.run() def _set_ipv6(self, dialog): self.network_data.ipv6 = dialog.run() def _set_ipv6_gateway(self, dialog): self.network_data.ipv6gateway = dialog.run() def _set_nameservers(self, dialog): self.network_data.nameserver = dialog.run() def _set_apply_handler(self, args): self.apply_configuration = not self.apply_configuration def _set_onboot_handler(self, args): self.network_data.onboot = not self.network_data.onboot def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return super().input(args, key) @property def indirect(self): return True def apply(self): """ Apply our changes. """ # save this back to network data, this will be applied in upper layer pass
class TimeSpoke(FirstbootSpokeMixIn, NormalTUISpoke): helpFile = "DateTimeSpoke.txt" category = LocalizationCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) 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() @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 ntp_servers = [] if constants.ANACONDA_ENVIRON in flags.environs: ntp_servers = self.data.timezone.ntpservers elif constants.FIRSTBOOT_ENVIRON in flags.environs: ntp_servers = ntp.get_servers_from_config()[ 1] # returns a (NPT pools, NTP servers) tupple 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()) # 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: self._timezone_spoke = TimeZoneSpoke(self.data, self.storage, self.payload, self.instclass) return self._timezone_spoke @property def completed(self): return bool(self.data.timezone.timezone) @property def mandatory(self): return True @property def status(self): if self.data.timezone.timezone: return _("%s timezone") % self.data.timezone.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 timezone_msg = _("not set") if self.data.timezone.timezone: timezone_msg = self.data.timezone.timezone msg += _("Timezone: %s\n") % timezone_msg # newline section separator 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") return msg def refresh(self, args=None): NormalTUISpoke.refresh(self, args) summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) if self.data.timezone.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.instclass, self) 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(TimeSpoke, self).input(args, key) def apply(self): # update the NTP server list in kickstart self.data.timezone.ntpservers = list(self.ntp_servers.keys())
class UserSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ helpFile = "UserSpoke.txt" category = UserSettingsCategory @classmethod def should_run(cls, environment, data): # the user spoke should run always in the anaconda and in firstboot only # when doing reconfig or if no user has been created in the installation if environment == ANACONDA_ENVIRON: return True elif environment == FIRSTBOOT_ENVIRON and data is None: # cannot decide, stay in the game and let another call with data # available (will come) decide return True elif environment == FIRSTBOOT_ENVIRON and data and \ (data.firstboot.firstboot == FIRSTBOOT_RECONFIG or len(data.user.userList) == 0): return True else: return False def __init__(self, data, storage, payload, instclass): FirstbootSpokeMixIn.__init__(self) NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.initialize_start() self.title = N_("User creation") self._container = None if self.data.user.userList: self._user_data = self.data.user.userList[0] self._create_user = True else: self._user_data = self.data.UserData() self._create_user = False self._use_password = self._user_data.isCrypted or self._user_data.password self._groups = "" self._is_admin = False self._policy = self.data.anaconda.pwpolicy.get_policy( "user", fallback_to_default=True) self.errors = [] self.initialize_done() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._is_admin = "wheel" in self._user_data.groups self._groups = ", ".join(self._user_data.groups) self._container = ListColumnContainer(1) w = CheckboxWidget(title=_("Create user"), completed=self._create_user) self._container.add(w, self._set_create_user) if self._create_user: dialog = Dialog(title=_("Full name"), conditions=[self._check_fullname]) self._container.add( EntryWidget(dialog.title, self._user_data.gecos), self._set_fullname, dialog) dialog = Dialog(title=_("User name"), conditions=[self._check_username]) self._container.add( EntryWidget(dialog.title, self._user_data.name), self._set_username, dialog) w = CheckboxWidget(title=_("Use password"), completed=self._use_password) self._container.add(w, self._set_use_password) if self._use_password: password_dialog = PasswordDialog(title=_("Password"), policy=self._policy) if self._user_data.password: entry = EntryWidget(password_dialog.title, _(PASSWORD_SET)) else: entry = EntryWidget(password_dialog.title) self._container.add(entry, self._set_password, password_dialog) msg = _("Administrator") w = CheckboxWidget(title=msg, completed=self._is_admin) self._container.add(w, self._set_administrator) dialog = Dialog(title=_("Groups"), conditions=[self._check_groups]) self._container.add(EntryWidget(dialog.title, self._groups), self._set_groups, dialog) self.window.add_with_separator(self._container) @report_if_failed(message=FULLNAME_ERROR_MSG) def _check_fullname(self, user_input, report_func): return GECOS_VALID.match(user_input) is not None @report_check_func() def _check_username(self, user_input, report_func): return check_username(user_input) @report_check_func() def _check_groups(self, user_input, report_func): return check_grouplist(user_input) def _set_create_user(self, args): self._create_user = not self._create_user def _set_fullname(self, dialog): self._user_data.gecos = dialog.run() def _set_username(self, dialog): self._user_data.name = dialog.run() def _set_use_password(self, args): self._use_password = not self._use_password def _set_password(self, password_dialog): password = password_dialog.run() while password is None: password = password_dialog.run() self._user_data.password = password def _set_administrator(self, args): self._is_admin = not self._is_admin def _set_groups(self, dialog): self._groups = dialog.run() def show_all(self): NormalTUISpoke.show_all(self) # if we have any errors, display them while self.errors: print(self.errors.pop()) @property def completed(self): """ Verify a user is created; verify pw is set if option checked. """ if len(self.data.user.userList) > 0: if self._use_password and not bool(self._user_data.password or self._user_data.isCrypted): return False else: return True else: return False @property def showable(self): return not (self.completed and flags.automatedInstall and self.data.user.seen and not self._policy.changesok) @property def mandatory(self): """ Only mandatory if the root pw hasn't been set in the UI eg. not mandatory if the root account was locked in a kickstart """ return not self.data.rootpw.password and not self.data.rootpw.lock @property def status(self): if len(self.data.user.userList) == 0: return _("No user will be created") elif self._use_password and not bool(self._user_data.password or self._user_data.isCrypted): return _("You must set a password") elif "wheel" in self.data.user.userList[0].groups: return _("Administrator %s will be created" ) % self.data.user.userList[0].name else: return _( "User %s will be created") % self.data.user.userList[0].name def input(self, args, key): if self._container.process_user_input(key): self.apply() self.redraw() return InputState.PROCESSED return super(UserSpoke, self).input(args, key) def apply(self): if self._user_data.gecos and not self._user_data.name: username = guess_username(self._user_data.gecos) valid, msg = check_username(username) if not valid: self.errors.append( _("Invalid user name: %(name)s.\n%(error_message)s") % { "name": username, "error_message": msg }) else: self._user_data.name = guess_username(self._user_data.gecos) self._user_data.groups = [ g.strip() for g in self._groups.split(",") if g ] # Add or remove the user from wheel group if self._is_admin and "wheel" not in self._user_data.groups: self._user_data.groups.append("wheel") elif not self._is_admin and "wheel" in self._user_data.groups: self._user_data.groups.remove("wheel") # Add or remove the user from userlist as needed if self._create_user and (self._user_data not in self.data.user.userList and self._user_data.name): self.data.user.userList.append(self._user_data) elif (not self._create_user) and (self._user_data in self.data.user.userList): self.data.user.userList.remove(self._user_data) # encrypt and store password only if user entered anything; this should # preserve passwords set via kickstart if self._use_password and self._user_data.password and len( self._user_data.password) > 0: self._user_data.password = self._user_data.password self._user_data.isCrypted = True self._user_data.password_kickstarted = False # clear pw when user unselects to use pw else: self._user_data.password = "" self._user_data.isCrypted = False self._user_data.password_kickstarted = False
class UserSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ .. inheritance-diagram:: UserSpoke :parts: 3 """ category = UserSettingsCategory @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "user-configuration" @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(USERS): return False if FirstbootSpokeMixIn.should_run(environment, data): return True # the user spoke should run always in the anaconda and in firstboot only # when doing reconfig or if no user has been created in the installation users_module = USERS.get_proxy() user_list = get_user_list(users_module) if environment == FIRSTBOOT_ENVIRON and data and not user_list: return True return False def __init__(self, data, storage, payload): FirstbootSpokeMixIn.__init__(self) NormalTUISpoke.__init__(self, data, storage, payload) self.initialize_start() # connect to the Users DBus module self._users_module = USERS.get_proxy() self.title = N_("User creation") self._container = None # was user creation requested by the Users DBus module # - at the moment this basically means user creation was # requested via kickstart # - note that this does not currently update when user # list is changed via DBus self._user_requested = False self._user_cleared = False # should a user be created ? self._create_user = False self._user_list = get_user_list(self._users_module, add_default=True) # if user has a name, it's an actual user that has been requested, # rather than a default user added by us if self.user.name: self._user_requested = True self._create_user = True self._use_password = self.user.is_crypted or self.user.password self._groups = "" self._is_admin = False self.errors = [] self._users_module = USERS.get_proxy() self.initialize_done() @property def user(self): """The user that is manipulated by the User spoke. This user is always the first one in the user list. :return: a UserData instance """ return self._user_list[0] def refresh(self, args=None): NormalTUISpoke.refresh(self, args) # refresh the user list self._user_list = get_user_list(self._users_module, add_default=True, add_if_not_empty=self._user_cleared) self._is_admin = self.user.has_admin_priviledges() self._groups = ", ".join(self.user.groups) self._container = ListColumnContainer(1) w = CheckboxWidget(title=_("Create user"), completed=self._create_user) self._container.add(w, self._set_create_user) if self._create_user: dialog = Dialog(title=_("Full name"), conditions=[self._check_fullname]) self._container.add(EntryWidget(dialog.title, self.user.gecos), self._set_fullname, dialog) dialog = Dialog(title=_("User name"), conditions=[self._check_username]) self._container.add(EntryWidget(dialog.title, self.user.name), self._set_username, dialog) w = CheckboxWidget(title=_("Use password"), completed=self._use_password) self._container.add(w, self._set_use_password) if self._use_password: password_dialog = PasswordDialog( title=_("Password"), policy_name=PASSWORD_POLICY_USER) if self.user.password: entry = EntryWidget(password_dialog.title, _(PASSWORD_SET)) else: entry = EntryWidget(password_dialog.title) self._container.add(entry, self._set_password, password_dialog) msg = _("Administrator") w = CheckboxWidget(title=msg, completed=self._is_admin) self._container.add(w, self._set_administrator) dialog = Dialog(title=_("Groups"), conditions=[self._check_groups]) self._container.add(EntryWidget(dialog.title, self._groups), self._set_groups, dialog) self.window.add_with_separator(self._container) @report_if_failed(message=FULLNAME_ERROR_MSG) def _check_fullname(self, user_input, report_func): return GECOS_VALID.match(user_input) is not None @report_check_func() def _check_username(self, user_input, report_func): return check_username(user_input) @report_check_func() def _check_groups(self, user_input, report_func): return check_grouplist(user_input) def _set_create_user(self, args): self._create_user = not self._create_user def _set_fullname(self, dialog): self.user.gecos = dialog.run() def _set_username(self, dialog): self.user.name = dialog.run() def _set_use_password(self, args): self._use_password = not self._use_password def _set_password(self, password_dialog): password = password_dialog.run() while password is None: password = password_dialog.run() self.user.password = password def _set_administrator(self, args): self._is_admin = not self._is_admin def _set_groups(self, dialog): self._groups = dialog.run() def show_all(self): NormalTUISpoke.show_all(self) # if we have any errors, display them while self.errors: print(self.errors.pop()) @property def completed(self): """ Verify a user is created; verify pw is set if option checked. """ user_list = get_user_list(self._users_module) if user_list: if self._use_password and not bool(self.user.password or self.user.is_crypted): return False else: return True else: return False @property def showable(self): return not (self.completed and flags.automatedInstall and self._user_requested and not conf.ui.can_change_users) @property def mandatory(self): """The spoke is mandatory only if some input is missing. Possible reasons to be mandatory: - No admin user has been created - Password has been requested but not entered """ return (not self._users_module.CheckAdminUserExists() or (self._use_password and not bool(self.user.password or self.user.is_crypted))) @property def status(self): user_list = get_user_list(self._users_module) if not user_list: return _("No user will be created") elif self._use_password and not bool(self.user.password or self.user.is_crypted): return _("You must set a password") elif user_list[0].has_admin_priviledges(): return _("Administrator %s will be created") % user_list[0].name else: return _("User %s will be created") % user_list[0].name def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW return super().input(args, key) def apply(self): if self.user.gecos and not self.user.name: username = guess_username(self.user.gecos) valid, msg = check_username(username) if not valid: self.errors.append( _("Invalid user name: %(name)s.\n%(error_message)s") % { "name": username, "error_message": msg }) else: self.user.name = guess_username(self.user.gecos) self.user.groups = [g.strip() for g in self._groups.split(",") if g] # Add or remove user admin status self.user.set_admin_priviledges(self._is_admin) # encrypt and store password only if user entered anything; this should # preserve passwords set via kickstart if self._use_password and self.user.password and len( self.user.password) > 0: self.user.password = self.user.password self.user.is_crypted = True # clear pw when user unselects to use pw else: self.user.password = "" self.user.is_crypted = False # Turning user creation off clears any already configured user, # regardless of origin (kickstart, user, DBus). if not self._create_user and self.user.name: self.user.name = "" self._user_cleared = True # An the other hand, if we have a user with name set, # it is valid and should be used if the spoke is re-visited. if self.user.name: self._user_cleared = False # Set the user list while removing any unset users, where unset # means the user has nema == "". set_user_list(self._users_module, self._user_list, remove_unset=True)
class HelloWorldEditSpoke(NormalTUISpoke): """Example class demonstrating usage of editing in TUI""" category = HelloWorldCategory def __init__(self, data, storage, payload, instclass): """ :see: simpleline.render.screen.UIScreen :param data: data object passed to every spoke to load/store data from/to it :type data: pykickstart.base.BaseHandler :param storage: object storing storage-related information (disks, partitioning, bootloader, etc.) :type storage: blivet.Blivet :param payload: object storing packaging-related information :type payload: pyanaconda.packaging.Payload :param instclass: distribution-specific information :type instclass: pyanaconda.installclass.BaseInstallClass """ NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Hello World Edit") self._container = None # values for user to set self._checked = False self._unconditional_input = "" self._conditional_input = "" def refresh(self, args=None): """ The refresh method that is called every time the spoke is displayed. It should update the UI elements according to the contents of self.data. :see: pyanaconda.ui.common.UIObject.refresh :see: simpleline.render.screen.UIScreen.refresh :param args: optional argument that may be used when the screen is scheduled :type args: anything """ super().refresh(args) self._container = ListColumnContainer(columns=1) # add ListColumnContainer to window (main window container) # this will automatically add numbering and will call callbacks when required self.window.add(self._container) self._container.add(CheckboxWidget(title="Simple checkbox", completed=self._checked), callback=self._checkbox_called) self._container.add(EntryWidget(title="Unconditional input", value=self._unconditional_input), callback=self._get_unconditional_input) # show conditional input only if the checkbox is checked if self._checked: self._container.add(EntryWidget(title="Conditional input", value="Password set" if self._conditional_input else ""), callback=self._get_conditional_input) self._window.add_separator() def _checkbox_called(self, data): """Callback when user wants to switch checkbox. :param data: can be passed when adding callback in container (not used here) :type data: anything """ self._checked = not self._checked def _get_unconditional_input(self, data): """Callback when user wants to set unconditional input. :param data: can be passed when adding callback in container (not used here) :type data: anything """ dialog = Dialog("Unconditional input", conditions=[self._check_user_input]) self._unconditional_input = dialog.run() def _get_conditional_input(self, data): """Callback when user wants to set conditional input. :param data: can be passed when adding callback in container (not used here) :type data: anything """ # password policy for setting root password password_policy = self.data.anaconda.pwpolicy.get_policy("root", fallback_to_default=True) dialog = PasswordDialog("Unconditional input", policy=password_policy) self._conditional_input = dialog.run() def _check_user_input(self, user_input, report_func): """Check if user has wrote a valid value. :param user_input: user input for validation :type user_input: str :param report_func: function for reporting errors on user input :type report_func: func with one param """ if re.match(r'^\w+$', user_input): return True else: report_func("You must set a one word") return False def input(self, args, key): """ The input method that is called by the main loop on user's input. :param args: optional argument that may be used when the screen is scheduled :type args: anything :param key: user's input :type key: unicode :return: if the input should not be handled here, return it, otherwise return InputState.PROCESSED or InputState.DISCARDED if the input was processed successfully or not respectively :rtype: enum InputState """ if self._container.process_user_input(key): # redraw or close must be called before PROCESSED self.redraw() return InputState.PROCESSED else: return super().input(args=args, key=key) @property def completed(self): # completed if user entered something non-empty to the Conditioned input return bool(self._conditional_input) @property def status(self): return "Hidden input %s" % ("entered" if self._conditional_input else "not entered") def apply(self): # nothing to do here pass
class AskVNCSpoke(NormalTUISpoke): """ .. inheritance-diagram:: AskVNCSpoke :parts: 3 """ title = N_("VNC") # This spoke is kinda standalone, not meant to be used with a hub # We pass in some fake data just to make our parents happy def __init__(self, data, storage=None, payload=None, instclass=None, message=""): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.input_required = True self.initialize_start() self._container = None # The TUI hasn't been initialized with the message handlers yet. Add an # exception message handler so that the TUI exits if anything goes wrong # at this stage. loop = App.get_event_loop() loop.register_signal_handler(ExceptionSignal, exception_msg_handler_and_exit) self._message = message self._usevnc = False self.initialize_done() @property def indirect(self): return True def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self.window.add_with_separator(TextWidget(self._message)) self._container = ListColumnContainer(1, spacing=1) # choices are # USE VNC self._container.add(TextWidget(_(USEVNC)), self._use_vnc_callback) # USE TEXT self._container.add(TextWidget(_(USETEXT)), self._use_text_callback) self.window.add_with_separator(self._container) def _use_vnc_callback(self, data): self._usevnc = True new_spoke = VNCPassSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) def _use_text_callback(self, data): self._usevnc = False def input(self, args, key): """Override input so that we can launch the VNC password spoke""" if self._container.process_user_input(key): self.apply() self.close() return InputState.PROCESSED else: # TRANSLATORS: 'q' to quit if key.lower() == C_('TUI|Spoke Navigation', 'q'): d = YesNoDialog(_(u"Do you really want to quit?")) ScreenHandler.push_screen_modal(d) if d.answer: ipmi_abort(scripts=self.data.scripts) if can_touch_runtime_system("Quit and Reboot"): execWithRedirect("systemctl", ["--no-wall", "reboot"]) else: sys.exit(1) else: return super(AskVNCSpoke, self).input(args, key) def apply(self): self.data.vnc.enabled = self._usevnc
class NetworkSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ Spoke used to configure network settings. .. inheritance-diagram:: NetworkSpoke :parts: 3 """ helpFile = "NetworkSpoke.txt" category = SystemCategory configurable_device_types = [ NM.DeviceType.ETHERNET, NM.DeviceType.INFINIBAND, ] def __init__(self, data, storage, payload): NormalTUISpoke.__init__(self, data, storage, payload) self.title = N_("Network configuration") self._network_module = NETWORK.get_observer() self._network_module.connect() self.nm_client = network.get_nm_client() if not self.nm_client and conf.system.provides_system_bus: self.nm_client = NM.Client.new(None) self._container = None self.hostname = self._network_module.proxy.Hostname self.editable_configurations = [] self.errors = [] self._apply = False @classmethod def should_run(cls, environment, data): return conf.system.can_configure_network def initialize(self): self.initialize_start() NormalTUISpoke.initialize(self) self._update_editable_configurations() self._network_module.proxy.DeviceConfigurationChanged.connect(self._device_configurations_changed) self.initialize_done() def _device_configurations_changed(self, device_configurations): log.debug("device configurations changed: %s", device_configurations) self._update_editable_configurations() def _update_editable_configurations(self): device_configurations = self._network_module.proxy.GetDeviceConfigurations() self.editable_configurations = [NetworkDeviceConfiguration.from_structure(dc) for dc in device_configurations if dc['device-type'] in self.configurable_device_types] @property def completed(self): """ Check whether this spoke is complete or not.""" # If we can't configure network, don't require it return (not conf.system.can_configure_network or self._network_module.proxy.GetActivatedInterfaces()) @property def mandatory(self): # the network spoke should be mandatory only if it is running # during the installation and if the installation source requires network return ANACONDA_ENVIRON in flags.environs and self.payload.needs_network @property def status(self): """ Short msg telling what devices are active. """ return network.status_message(self.nm_client) def _summary_text(self): """Devices cofiguration shown to user.""" msg = "" activated_devs = self._network_module.proxy.GetActivatedInterfaces() for device_configuration in self.editable_configurations: name = device_configuration.device_name if name in activated_devs: msg += self._activated_device_msg(name) else: msg += _("Wired (%(interface_name)s) disconnected\n") \ % {"interface_name": name} return msg def _activated_device_msg(self, devname): msg = _("Wired (%(interface_name)s) connected\n") \ % {"interface_name": devname} device = self.nm_client.get_device_by_iface(devname) if device: ipv4config = device.get_ip4_config() if ipv4config: addresses = ipv4config.get_addresses() if addresses: a0 = addresses[0] addr_str = a0.get_address() prefix = a0.get_prefix() netmask_str = network.prefix_to_netmask(prefix) gateway_str = ipv4config.get_gateway() or '' dnss_str = ",".join(ipv4config.get_nameservers()) else: addr_str = dnss_str = gateway_str = netmask_str = "" msg += _(" IPv4 Address: %(addr)s Netmask: %(netmask)s Gateway: %(gateway)s\n") % \ {"addr": addr_str, "netmask": netmask_str, "gateway": gateway_str} msg += _(" DNS: %s\n") % dnss_str ipv6config = device.get_ip6_config() if ipv6config: for address in ipv6config.get_addresses(): addr_str = address.get_address() prefix = address.get_prefix() # Do not display link-local addresses if not addr_str.startswith("fe80:"): msg += _(" IPv6 Address: %(addr)s/%(prefix)d\n") % \ {"addr": addr_str, "prefix": prefix} return msg def refresh(self, args=None): """ Refresh screen. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1, columns_width=78, spacing=1) if not self.nm_client: self.window.add_with_separator(TextWidget(_("Network configuration is not available."))) return summary = self._summary_text() self.window.add_with_separator(TextWidget(summary)) hostname = _("Host Name: %s\n") % self._network_module.proxy.Hostname self.window.add_with_separator(TextWidget(hostname)) current_hostname = _("Current host name: %s\n") % self._network_module.proxy.GetCurrentHostname() self.window.add_with_separator(TextWidget(current_hostname)) # if we have any errors, display them while len(self.errors) > 0: self.window.add_with_separator(TextWidget(self.errors.pop())) dialog = Dialog(_("Host Name")) self._container.add(TextWidget(_("Set host name")), callback=self._set_hostname_callback, data=dialog) for device_configuration in self.editable_configurations: iface = device_configuration.device_name text = (_("Configure device %s") % iface) self._container.add(TextWidget(text), callback=self._ensure_connection_and_configure, data=iface) self.window.add_with_separator(self._container) def _set_hostname_callback(self, dialog): self.hostname = dialog.run() self.redraw() self.apply() def _ensure_connection_and_configure(self, iface): for device_configuration in self.editable_configurations: if device_configuration.device_name == iface: connection_uuid = device_configuration.connection_uuid if connection_uuid: self._configure_connection(iface, connection_uuid) else: device_type = self.nm_client.get_device_by_iface(iface).get_device_type() connection = get_default_connection(iface, device_type) connection_uuid = connection.get_uuid() log.debug("adding default connection %s for %s", connection_uuid, iface) persistent = False data = (iface, connection_uuid) self.nm_client.add_connection_async(connection, persistent, None, self._default_connection_added_cb, data) return log.error("device configuration for %s not found", iface) def _default_connection_added_cb(self, client, result, data): client.add_connection_finish(result) iface, connection_uuid = data log.debug("added default connection %s for %s", connection_uuid, iface) self._configure_connection(iface, connection_uuid) def _configure_connection(self, iface, connection_uuid): connection = self.nm_client.get_connection_by_uuid(connection_uuid) new_spoke = ConfigureDeviceSpoke(self.data, self.storage, self.payload, self._network_module, iface, connection) ScreenHandler.push_screen_modal(new_spoke) if new_spoke.errors: self.errors.extend(new_spoke.errors) self.redraw() return if new_spoke.apply_configuration: self._apply = True device = self.nm_client.get_device_by_iface(iface) log.debug("activating connection %s with device %s", connection_uuid, iface) self.nm_client.activate_connection_async(connection, device, None, None) self._network_module.proxy.LogConfigurationState( "Settings of {} updated in TUI.".format(iface) ) self.redraw() self.apply() def input(self, args, key): """ Handle the input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: return super().input(args, key) def apply(self): """Apply all of our settings.""" # Inform network module that device configurations might have been changed # and we want to generate kickstart from device configurations # (persistent NM / ifcfg configuration), instead of using original kickstart. self._network_module.proxy.NetworkDeviceConfigurationChanged() (valid, error) = network.is_valid_hostname(self.hostname) if valid: self._network_module.proxy.SetHostname(self.hostname) else: self.errors.append(_("Host name is not valid: %s") % error) self.hostname = self._network_module.proxy.Hostname if self._apply: self._apply = False if ANACONDA_ENVIRON in flags.environs: from pyanaconda.payload.manager import payloadMgr payloadMgr.restart_thread(self.storage, self.data, self.payload, checkmount=False)
class SpecifyNFSRepoSpoke(NormalTUISpoke, SourceSwitchHandler): """ Specify server and mount opts here if NFS selected. """ category = SoftwareCategory def __init__(self, data, storage, payload, error): NormalTUISpoke.__init__(self, data, storage, payload) SourceSwitchHandler.__init__(self) self.title = N_("Specify Repo Options") self._container = None self._error = error options, host, path = self._get_nfs() self._nfs_opts = options self._nfs_server = "{}:{}".format(host, path) if host else "" def _get_nfs(self): """Get the NFS options, host and path of the current source.""" source_proxy = self.payload.get_source_proxy() if source_proxy.Type == SOURCE_TYPE_NFS: return parse_nfs_url(source_proxy.URL) return "", "", "" def refresh(self, args=None): """ Refresh window. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(1) dialog = Dialog(title=_("SERVER:/PATH"), conditions=[self._check_nfs_server]) self._container.add(EntryWidget(dialog.title, self._nfs_server), self._set_nfs_server, dialog) dialog = Dialog(title=_("NFS mount options")) self._container.add(EntryWidget(dialog.title, self._nfs_opts), self._set_nfs_opts, dialog) self.window.add_with_separator(self._container) def _set_nfs_server(self, dialog): self._nfs_server = dialog.run() def _check_nfs_server(self, user_input, report_func): if ":" not in user_input or len(user_input.split(":")) != 2: report_func(_("Server must be specified as SERVER:/PATH")) return False return True def _set_nfs_opts(self, dialog): self._nfs_opts = dialog.run() def input(self, args, key): if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW else: return NormalTUISpoke.input(self, args, key) @property def indirect(self): return True def apply(self): """ Apply our changes. """ if self._nfs_server == "" or ':' not in self._nfs_server: return False if self._nfs_server.startswith("nfs://"): self._nfs_server = self._nfs_server[6:] try: (server, directory) = self._nfs_server.split(":", 2) except ValueError as err: log.error("ValueError: %s", err) self._error = True return opts = self._nfs_opts or "" self.set_source_nfs(server, directory, opts)
class ConfigureDeviceSpoke(NormalTUISpoke): """ Spoke to set various configuration options for net devices. """ category = "network" def __init__(self, data, storage, payload, network_module, iface, connection): super().__init__(data, storage, payload) self.title = N_("Device configuration") self._network_module = network_module self._container = None self._connection = connection self._iface = iface self._connection_uuid = connection.get_uuid() self.errors = [] self.apply_configuration = False self._data = WiredTUIConfigurationData() self._data.set_from_connection(self._connection) # ONBOOT workaround - changing autoconnect connection value would # activate the device self._data.onboot = self._get_onboot(self._connection_uuid) log.debug("Configure iface %s: connection %s -> %s", self._iface, self._connection_uuid, self._data) def _get_onboot(self, connection_uuid): return self._network_module.proxy.GetConnectionOnbootValue(connection_uuid) def _set_onboot(self, connection_uuid, onboot): return self._network_module.proxy.SetConnectionOnbootValue(connection_uuid, onboot) def refresh(self, args=None): """ Refresh window. """ super().refresh(args) self._container = ListColumnContainer(1) dialog = Dialog(title=(_('IPv4 address or %s for DHCP') % '"dhcp"'), conditions=[self._check_ipv4_or_dhcp]) self._container.add(EntryWidget(dialog.title, self._data.ip), self._set_ipv4_or_dhcp, dialog) dialog = Dialog(title=_("IPv4 netmask"), conditions=[self._check_netmask]) self._container.add(EntryWidget(dialog.title, self._data.netmask), self._set_netmask, dialog) dialog = Dialog(title=_("IPv4 gateway"), conditions=[self._check_ipv4]) self._container.add(EntryWidget(dialog.title, self._data.gateway), self._set_ipv4_gateway, dialog) msg = (_('IPv6 address[/prefix] or %(auto)s for automatic, %(dhcp)s for DHCP, ' '%(ignore)s to turn off') % {"auto": '"auto"', "dhcp": '"dhcp"', "ignore": '"ignore"'}) dialog = Dialog(title=msg, conditions=[self._check_ipv6_config]) self._container.add(EntryWidget(dialog.title, self._data.ipv6), self._set_ipv6, dialog) dialog = Dialog(title=_("IPv6 default gateway"), conditions=[self._check_ipv6]) self._container.add(EntryWidget(dialog.title, self._data.ipv6gateway), self._set_ipv6_gateway, dialog) dialog = Dialog(title=_("Nameservers (comma separated)"), conditions=[self._check_nameservers]) self._container.add(EntryWidget(dialog.title, self._data.nameserver), self._set_nameservers, dialog) msg = _("Connect automatically after reboot") w = CheckboxWidget(title=msg, completed=self._data.onboot) self._container.add(w, self._set_onboot_handler) msg = _("Apply configuration in installer") w = CheckboxWidget(title=msg, completed=self.apply_configuration) self._container.add(w, self._set_apply_handler) self.window.add_with_separator(self._container) message = _("Configuring device %s.") % self._iface self.window.add_with_separator(TextWidget(message)) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4_or_dhcp(self, user_input, report_func): return IPV4_OR_DHCP_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4(self, user_input, report_func): return IPV4_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=NETMASK_ERROR_MSG) def _check_netmask(self, user_input, report_func): return IPV4_NETMASK_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6(self, user_input, report_func): return network.check_ip_address(user_input, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6_config(self, user_input, report_func): if user_input in ["auto", "dhcp", "ignore"]: return True addr, _slash, prefix = user_input.partition("/") if prefix: try: if not 1 <= int(prefix) <= 128: return False except ValueError: return False return network.check_ip_address(addr, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_nameservers(self, user_input, report_func): if user_input.strip(): addresses = [str.strip(i) for i in user_input.split(",")] for ip in addresses: if not network.check_ip_address(ip): return False return True def _set_ipv4_or_dhcp(self, dialog): self._data.ip = dialog.run() def _set_netmask(self, dialog): self._data.netmask = dialog.run() def _set_ipv4_gateway(self, dialog): self._data.gateway = dialog.run() def _set_ipv6(self, dialog): self._data.ipv6 = dialog.run() def _set_ipv6_gateway(self, dialog): self._data.ipv6gateway = dialog.run() def _set_nameservers(self, dialog): self._data.nameserver = dialog.run() def _set_apply_handler(self, args): self.apply_configuration = not self.apply_configuration def _set_onboot_handler(self, args): self._data.onboot = not self._data.onboot def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW else: # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): if self._data.ip != "dhcp" and not self._data.netmask: self.errors.append(_("Configuration not saved: netmask missing in static configuration")) else: self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) @property def indirect(self): return True def apply(self): """Apply changes to NM connection.""" log.debug("updating connection %s:\n%s", self._connection_uuid, self._connection.to_dbus(NM.ConnectionSerializationFlags.ALL)) self._data.update_connection(self._connection) # ONBOOT workaround s_con = self._connection.get_setting_connection() s_con.set_property(NM.SETTING_CONNECTION_AUTOCONNECT, False) self._connection.commit_changes(True, None) log.debug("updated connection %s:\n%s", self._connection_uuid, self._connection.to_dbus(NM.ConnectionSerializationFlags.ALL)) # ONBOOT workaround self._set_onboot(self._connection_uuid, self._data.onboot)
class SelectISOSpoke(NormalTUISpoke, SourceSwitchHandler): """ Select an ISO to use as install source. """ category = SoftwareCategory def __init__(self, data, storage, payload, device): NormalTUISpoke.__init__(self, data, storage, payload) SourceSwitchHandler.__init__(self) self.title = N_("Select an ISO to use as install source") self._container = None self._device = device self._isos = self._collect_iso_files() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) if self._isos: self._container = ListColumnContainer(1, columns_width=78, spacing=1) for iso in self._isos: self._container.add(TextWidget(iso), callback=self._select_iso_callback, data=iso) self.window.add_with_separator(self._container) else: message = _("No *.iso files found in device root folder") self.window.add_with_separator(TextWidget(message)) def _select_iso_callback(self, data): self._current_iso_path = data self.apply() self.close() def input(self, args, key): if self._container is not None and self._container.process_user_input(key): return InputState.PROCESSED elif key.lower() == Prompt.CONTINUE: self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) @property def indirect(self): return True def _collect_iso_files(self): """Collect *.iso files.""" try: self._mount_device() return self._getISOs() finally: self._unmount_device() def _mount_device(self): """ Mount the device so we can search it for ISOs. """ # FIXME: Use a unique mount point. device_path = get_device_path(self._device) mounts = payload_utils.get_mount_paths(device_path) # We have to check both ISO_DIR and the DRACUT_ISODIR because we # still reference both, even though /mnt/install is a symlink to # /run/install. Finding mount points doesn't handle the symlink if ISO_DIR not in mounts and DRACUT_ISODIR not in mounts: # We're not mounted to either location, so do the mount payload_utils.mount_device(self._device, ISO_DIR) def _unmount_device(self): # FIXME: Unmount a specific mount point. payload_utils.unmount_device(self._device, mount_point=None) def _getISOs(self): """List all *.iso files in the root folder of the currently selected device. TODO: advanced ISO file selection :returns: a list of *.iso file paths :rtype: list """ isos = [] for filename in os.listdir(ISO_DIR): if fnmatch.fnmatch(filename.lower(), "*.iso"): isos.append(filename) return isos def apply(self): """ Apply all of our changes. """ if self._current_iso_path: self.set_source_hdd_iso(self._device, self._current_iso_path)
class SourceSpoke(NormalTUISpoke, SourceSwitchHandler): """ Spoke used to customize the install source repo. .. inheritance-diagram:: SourceSpoke :parts: 3 """ helpFile = "SourceSpoke.txt" category = SoftwareCategory SET_NETWORK_INSTALL_MODE = "network_install" def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.title = N_("Installation source") self._container = None self._ready = False self._error = False self._cdrom = None self._hmc = False def initialize(self): NormalTUISpoke.initialize(self) self.initialize_start() threadMgr.add( AnacondaThread(name=THREAD_SOURCE_WATCHER, target=self._initialize)) payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error) def _initialize(self): """ Private initialize. """ threadMgr.wait(THREAD_PAYLOAD) # If we've previously set up to use a CD/DVD method, the media has # already been mounted by payload.setup. We can't try to mount it # again. So just use what we already know to create the selector. # Otherwise, check to see if there's anything available. if self.data.method.method == "cdrom": self._cdrom = self.payload.install_device elif not flags.automatedInstall: self._cdrom = opticalInstallMedia(self.storage.devicetree) # Enable the SE/HMC option. if flags.hmc: self._hmc = True self._ready = True # report that the source spoke has been initialized self.initialize_done() def _payload_error(self): self._error = True def _repo_status(self): """ Return a string describing repo url or lack of one. """ method = self.data.method if method.method == "url": return method.url or method.mirrorlist or method.metalink elif method.method == "nfs": return _("NFS server %s") % method.server elif method.method == "cdrom": return _("Local media") elif method.method == "hmc": return _("Local media via SE/HMC") elif method.method == "harddrive": if not method.dir: return _("Error setting up software source") return os.path.basename(method.dir) elif self.payload.baseRepo: return _("Closest mirror") else: return _("Nothing selected") @property def showable(self): return isinstance(self.payload, PackagePayload) @property def status(self): if self._error: return _("Error setting up software source") elif not self.ready: return _("Processing...") else: return self._repo_status() @property def completed(self): if flags.automatedInstall and self.ready and not self.payload.baseRepo: return False else: return not self._error and self.ready and ( self.data.method.method or self.payload.baseRepo) def refresh(self, args=None): NormalTUISpoke.refresh(self, args) threadMgr.wait(THREAD_PAYLOAD) self._container = ListColumnContainer(1, columns_width=78, spacing=1) if self.data.method.method == "harddrive" and \ get_mount_device(DRACUT_ISODIR) == get_mount_device(DRACUT_REPODIR): message = _( "The installation source is in use by the installer and cannot be changed." ) self.window.add_with_separator(TextWidget(message)) return if args == self.SET_NETWORK_INSTALL_MODE: if self.payload.mirrors_available: self._container.add(TextWidget(_("Closest mirror")), self._set_network_close_mirror) self._container.add(TextWidget("http://"), self._set_network_url, SpecifyRepoSpoke.HTTP) self._container.add(TextWidget("https://"), self._set_network_url, SpecifyRepoSpoke.HTTPS) self._container.add(TextWidget("ftp://"), self._set_network_url, SpecifyRepoSpoke.FTP) self._container.add(TextWidget("nfs"), self._set_network_nfs) else: self.window.add( TextWidget(_("Choose an installation source type."))) self._container.add(TextWidget(_("CD/DVD")), self._set_cd_install_source) self._container.add(TextWidget(_("local ISO file")), self._set_iso_install_source) self._container.add(TextWidget(_("Network")), self._set_network_install_source) if self._hmc: self._container.add(TextWidget(_("SE/HMC")), self._set_hmc_install_source) self.window.add_with_separator(self._container) # Set installation source callbacks def _set_cd_install_source(self, data): self.set_source_cdrom() self.payload.install_device = self._cdrom self.apply() self.close() def _set_hmc_install_source(self, data): self.set_source_hmc() self.apply() self.close() def _set_iso_install_source(self, data): new_spoke = SelectDeviceSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_install_source(self, data): ScreenHandler.replace_screen(self, self.SET_NETWORK_INSTALL_MODE) # Set network source callbacks def _set_network_close_mirror(self, data): self.set_source_closest_mirror() self.apply() self.close() def _set_network_url(self, data): new_spoke = SpecifyRepoSpoke(self.data, self.storage, self.payload, self.instclass, data) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def _set_network_nfs(self, data): self.set_source_nfs() new_spoke = SpecifyNFSRepoSpoke(self.data, self.storage, self.payload, self.instclass, self._error) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() def input(self, args, key): """ Handle the input; this decides the repo source. """ if not self._container.process_user_input(key): return super().input(args, key) return InputState.PROCESSED @property def ready(self): """ Check if the spoke is ready. """ return (self._ready and not threadMgr.get(THREAD_PAYLOAD) and not threadMgr.get(THREAD_CHECK_SOFTWARE)) def apply(self): """ Execute the selections made. """ # If askmethod was provided on the command line, entering the source # spoke wipes that out. if flags.askmethod: flags.askmethod = False # if we had any errors, e.g. from a previous attempt to set the source, # clear them at this point self._error = False payloadMgr.restartThread(self.storage, self.data, self.payload, self.instclass, checkmount=False)
class LangSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ This spoke allows a user to select their installed language. Note that this does not affect the display of the installer, it only will affect the system post-install, because it's too much of a pain to make other languages work in text-mode. Also this doesn't allow for selection of multiple languages like in the GUI. .. inheritance-diagram:: LangSpoke :parts: 3 """ helpFile = "LangSupportSpoke.txt" category = LocalizationCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Language settings") self.initialize_start() self._container = None self._langs = [localization.get_english_name(lang) for lang in localization.get_available_translations()] self._langs_and_locales = dict((localization.get_english_name(lang), lang) for lang in localization.get_available_translations()) self._locales = dict((lang, localization.get_language_locales(lang)) for lang in self._langs_and_locales.values()) self._l12_module = LOCALIZATION.get_observer() self._l12_module.connect() self._selected = self._l12_module.proxy.Language self.initialize_done() @property def completed(self): return self._l12_module.proxy.Language @property def mandatory(self): return False @property def showable(self): # don't show the language support spoke in single language mode return not flags.singlelang @property def status(self): if self._l12_module.proxy.Language: return localization.get_english_name(self._selected) else: return _("Language is not set.") def refresh(self, args=None): """ args is None if we want a list of languages; or, it is a list of all locales for a language. """ NormalTUISpoke.refresh(self, args) self._container = ListColumnContainer(3) if args: self.window.add(TextWidget(_("Available locales"))) for locale in args: widget = TextWidget(localization.get_english_name(locale)) self._container.add(widget, self._set_locales_callback, locale) else: self.window.add(TextWidget(_("Available languages"))) for lang in self._langs: langs_and_locales = self._langs_and_locales[lang] locales = self._locales[langs_and_locales] self._container.add(TextWidget(lang), self._show_locales_callback, locales) self.window.add_with_separator(self._container) def _set_locales_callback(self, data): locale = data self._selected = locale self.apply() self.close() def _show_locales_callback(self, data): locales = data ScreenHandler.replace_screen(self, locales) def input(self, args, key): """ Handle user input. """ if self._container.process_user_input(key): return InputState.PROCESSED else: # TRANSLATORS: 'b' to go back if key.lower() == C_("TUI|Spoke Navigation|Language Support", "b"): ScreenHandler.replace_screen(self) return InputState.PROCESSED else: return super().input(args, key) def prompt(self, args=None): """ Customize default prompt. """ prompt = NormalTUISpoke.prompt(self, args) prompt.set_message(_("Please select language support to install")) # TRANSLATORS: 'b' to go back prompt.add_option(C_("TUI|Spoke Navigation|Language Support", "b"), _("to return to language list")) return prompt def apply(self): """ Store the selected lang support locales """ self._l12_module.proxy.SetLanguage(self._selected)
class KdumpSpoke(NormalTUISpoke): category = SystemCategory def __init__(self, data, storage, payload, instclass): super().__init__(data, storage, payload, instclass) self.title = N_("Kdump") self._addon_data = self.data.addons.com_redhat_kdump self._lower, self._upper, self._step = getMemoryBounds() # Allow a string of digits optionally followed by 'M' self._reserve_check_re = re.compile(r'^(\d+M?)$') self._container = None @classmethod def should_run(cls, environment, data): # the KdumpSpoke should run only if requested return flags.cmdline.getbool("kdump_addon", default=False) def apply(self): pass @property def completed(self): return True @property def status(self): if self._addon_data.enabled: state = _("Kdump is enabled") else: state = _("Kdump is disabled") return state def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) self.window.add(self._container) self._create_enable_checkbox() if self._addon_data.enabled: self._create_fadump_checkbox() self._create_reserve_amount_text_widget() self.window.add_separator() def _create_enable_checkbox(self): enable_kdump_checkbox = CheckboxWidget(title=_("Enable kdump"), completed=self._addon_data.enabled) self._container.add(enable_kdump_checkbox, self._set_enabled) def _create_fadump_checkbox(self): if not os.path.exists(FADUMP_CAPABLE_FILE): return enable_fadump_checkbox = CheckboxWidget(title=_("Enable dump mode fadump"), completed=self._addon_data.enablefadump) self._container.add(enable_fadump_checkbox, self._set_fadump_enable) def _create_reserve_amount_text_widget(self): title = _("Reserve amount (%d - %d MB)" % (self._lower, self._upper)) reserve_amount_entry = EntryWidget(title=title, value=self._addon_data.reserveMB) self._container.add(reserve_amount_entry, self._get_reserve_amount) def _set_enabled(self, data): self._addon_data.enabled = not self._addon_data.enabled def _set_fadump_enable(self, data): self._addon_data.enablefadump = not self._addon_data.enablefadump def _get_reserve_amount(self, data): text = "Reserve amount (%d - %d MB)" % (self._lower, self._upper) dialog = Dialog(title=text, conditions=[self._check_reserve_valid]) self._addon_data.reserveMB = dialog.run() def _check_reserve_valid(self, key, report_func): if self._reserve_check_re.match(key): if key[-1] == 'M': key = key[:-1] key = int(key) if self._upper >= key >= self._lower: return True return False def input(self, args, key): if self._container.process_user_input(key): self.redraw() return InputState.PROCESSED else: return super().input(args, key)
class ConfigureDeviceSpoke(NormalTUISpoke): """ Spoke to set various configuration options for net devices. """ category = "network" def __init__(self, data, storage, payload, network_module, iface, connection): super().__init__(data, storage, payload) self.title = N_("Device configuration") self._network_module = network_module self._container = None self._connection = connection self._iface = iface self._connection_uuid = connection.get_uuid() self.errors = [] self.apply_configuration = False self._data = WiredTUIConfigurationData() self._data.set_from_connection(self._connection) log.debug("Configure iface %s: connection %s -> %s", self._iface, self._connection_uuid, self._data) def refresh(self, args=None): """ Refresh window. """ super().refresh(args) self._container = ListColumnContainer(1) dialog = Dialog(title=(_('IPv4 address or %s for DHCP') % '"dhcp"'), conditions=[self._check_ipv4_or_dhcp]) self._container.add(EntryWidget(dialog.title, self._data.ip), self._set_ipv4_or_dhcp, dialog) dialog = Dialog(title=_("IPv4 netmask"), conditions=[self._check_netmask]) self._container.add(EntryWidget(dialog.title, self._data.netmask), self._set_netmask, dialog) dialog = Dialog(title=_("IPv4 gateway"), conditions=[self._check_ipv4]) self._container.add(EntryWidget(dialog.title, self._data.gateway), self._set_ipv4_gateway, dialog) msg = (_( 'IPv6 address[/prefix] or %(auto)s for automatic, %(dhcp)s for DHCP, ' '%(ignore)s to turn off') % { "auto": '"auto"', "dhcp": '"dhcp"', "ignore": '"ignore"' }) dialog = Dialog(title=msg, conditions=[self._check_ipv6_config]) self._container.add(EntryWidget(dialog.title, self._data.ipv6), self._set_ipv6, dialog) dialog = Dialog(title=_("IPv6 default gateway"), conditions=[self._check_ipv6]) self._container.add(EntryWidget(dialog.title, self._data.ipv6gateway), self._set_ipv6_gateway, dialog) dialog = Dialog(title=_("Nameservers (comma separated)"), conditions=[self._check_nameservers]) self._container.add(EntryWidget(dialog.title, self._data.nameserver), self._set_nameservers, dialog) msg = _("Connect automatically after reboot") w = CheckboxWidget(title=msg, completed=self._data.onboot) self._container.add(w, self._set_onboot_handler) msg = _("Apply configuration in installer") w = CheckboxWidget(title=msg, completed=self.apply_configuration) self._container.add(w, self._set_apply_handler) self.window.add_with_separator(self._container) message = _("Configuring device %s.") % self._iface self.window.add_with_separator(TextWidget(message)) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4_or_dhcp(self, user_input, report_func): return IPV4_OR_DHCP_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv4(self, user_input, report_func): return IPV4_PATTERN_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=NETMASK_ERROR_MSG) def _check_netmask(self, user_input, report_func): return IPV4_NETMASK_WITH_ANCHORS.match(user_input) is not None @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6(self, user_input, report_func): return network.check_ip_address(user_input, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_ipv6_config(self, user_input, report_func): if user_input in ["auto", "dhcp", "ignore"]: return True addr, _slash, prefix = user_input.partition("/") if prefix: try: if not 1 <= int(prefix) <= 128: return False except ValueError: return False return network.check_ip_address(addr, version=6) @report_if_failed(message=IP_ERROR_MSG) def _check_nameservers(self, user_input, report_func): if user_input.strip(): addresses = [str.strip(i) for i in user_input.split(",")] for ip in addresses: if not network.check_ip_address(ip): return False return True def _set_ipv4_or_dhcp(self, dialog): self._data.ip = dialog.run() def _set_netmask(self, dialog): self._data.netmask = dialog.run() def _set_ipv4_gateway(self, dialog): self._data.gateway = dialog.run() def _set_ipv6(self, dialog): self._data.ipv6 = dialog.run() def _set_ipv6_gateway(self, dialog): self._data.ipv6gateway = dialog.run() def _set_nameservers(self, dialog): self._data.nameserver = dialog.run() def _set_apply_handler(self, args): self.apply_configuration = not self.apply_configuration def _set_onboot_handler(self, args): self._data.onboot = not self._data.onboot def input(self, args, key): if self._container.process_user_input(key): return InputState.PROCESSED_AND_REDRAW else: if key.lower() == Prompt.CONTINUE: if self._data.ip != "dhcp" and not self._data.netmask: self.errors.append( _("Configuration not saved: netmask missing in static configuration" )) else: self.apply() return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) @property def indirect(self): return True def apply(self): """Apply changes to NM connection.""" log.debug( "updating connection %s:\n%s", self._connection_uuid, self._connection.to_dbus(NM.ConnectionSerializationFlags.ALL)) updated_connection = NM.SimpleConnection.new_clone(self._connection) self._data.update_connection(updated_connection) # Commit the changes self._connection.update2( updated_connection.to_dbus(NM.ConnectionSerializationFlags.ALL), NM.SettingsUpdate2Flags.TO_DISK | NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT, None, None, self._connection_updated_cb, self._connection_uuid) def _connection_updated_cb(self, connection, result, connection_uuid): connection.update2_finish(result) log.debug("updated connection %s:\n%s", connection_uuid, connection.to_dbus(NM.ConnectionSerializationFlags.ALL))
class AutoPartSpoke(NormalTUISpoke): """ Autopartitioning options are presented here. .. inheritance-diagram:: AutoPartSpoke :parts: 3 """ category = SystemCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Autopartitioning Options") self._container = None self.clearPartType = self.data.clearpart.type self.parttypelist = sorted(PARTTYPES.keys()) @property def indirect(self): return True def refresh(self, args=None): NormalTUISpoke.refresh(self, args) # synchronize our local data store with the global ksdata self.clearPartType = self.data.clearpart.type self._container = ListColumnContainer(1) # I dislike "is None", but bool(0) returns false :( if self.clearPartType is None: # Default to clearing everything. self.clearPartType = CLEARPART_TYPE_ALL for part_type in self.parttypelist: c = CheckboxWidget(title=_(part_type), completed=(PARTTYPES[part_type] == self.clearPartType)) self._container.add(c, self._select_partition_type_callback, part_type) self.window.add_with_separator(self._container) message = _("Installation requires partitioning of your hard drive. " "Select what space to use for the install target.") self.window.add_with_separator(TextWidget(message)) def _select_partition_type_callback(self, data): self.clearPartType = PARTTYPES[data] self.apply() def apply(self): # kind of a hack, but if we're actually getting to this spoke, there # is no doubt that we are doing autopartitioning, so set autopart to # True. In the case of ks installs which may not have defined any # partition options, autopart was never set to True, causing some # issues. (rhbz#1001061) self.data.autopart.autopart = True self.data.clearpart.type = self.clearPartType self.data.clearpart.initAll = True def input(self, args, key): """Grab the choice and update things""" if not self._container.process_user_input(key): # TRANSLATORS: 'c' to continue if key.lower() == C_('TUI|Spoke Navigation', 'c'): new_spoke = PartitionSchemeSpoke(self.data, self.storage, self.payload, self.instclass) ScreenHandler.push_screen_modal(new_spoke) self.apply() self.close() return InputState.PROCESSED else: return super(AutoPartSpoke, self).input(args, key) self.redraw() return InputState.PROCESSED