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 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 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
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)
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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))
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 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) if args is None: msg = self._refresh_environments() else: msg = self._refresh_addons(args) self.window.add_with_separator(TextWidget(msg)) self.window.add_with_separator(self._container)
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 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)
class QubesOsSpoke(FirstbootOnlySpokeMixIn, NormalTUISpoke): """ Since this class inherits from the FirstbootOnlySpokeMixIn, it will only appear in the Initial Setup (successor of the Firstboot tool). :see: pyanaconda.ui.tui.TUISpoke :see: pyanaconda.ui.common.FirstbootSpokeMixIn :see: pyanaconda.ui.tui.tuiobject.TUIObject :see: pyaanconda.ui.tui.simpleline.Widget """ ### class attributes defined by API ### # category this spoke belongs to category = SystemCategory def __init__(self, data, storage, payload): """ :see: pyanaconda.ui.tui.base.UIScreen :see: pyanaconda.ui.tui.base.App :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 """ NormalTUISpoke.__init__(self, data, storage, payload) self.initialize_start() # title of the spoke self.title = N_("Qubes OS") self._container = None self.qubes_data = self.data.addons.org_qubes_os_initial_setup for attr in self.qubes_data.bool_options: setattr(self, '_' + attr, getattr(self.qubes_data, attr)) self.initialize_done() def initialize(self): """ The initialize method that is called after the instance is created. The difference between __init__ and this method is that this may take a long time and thus could be called in a separated thread. :see: pyanaconda.ui.common.UIObject.initialize """ NormalTUISpoke.initialize(self) def _add_checkbox(self, name, title): w = CheckboxWidget(title=title, completed=getattr(self, name)) self._container.add(w, self._set_checkbox, name) 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: pyanaconda.ui.tui.base.UIScreen.refresh :param args: optional argument that may be used when the screen is scheduled (passed to App.switch_screen* methods) :type args: anything :return: whether this screen requests input or not :rtype: bool """ super(QubesOsSpoke, self).refresh() self._container = ListColumnContainer(1) w = CheckboxWidget(title=_('Create default system qubes ' '(sys-net, sys-firewall, default DispVM)'), completed=self._system_vms) self._container.add(w, self._set_checkbox, '_system_vms') w = CheckboxWidget(title=_('Create default application qubes ' '(personal, work, untrusted, vault)'), completed=self._default_vms) self._container.add(w, self._set_checkbox, '_default_vms') if self.qubes_data.whonix_available: w = CheckboxWidget(title=_( 'Create Whonix Gateway and Workstation qubes ' '(sys-whonix, anon-whonix)'), completed=self._whonix_vms) self._container.add(w, self._set_checkbox, '_whonix_vms') if self._whonix_vms: w = CheckboxWidget(title=_( 'Enable system and template updates over the Tor anonymity ' 'network using Whonix'), completed=self._whonix_default) self._container.add(w, self._set_checkbox, '_whonix_default') if self.qubes_data.usbvm_available: w = CheckboxWidget(title=_( 'Create USB qube holding all USB controllers (sys-usb)'), completed=self._usbvm) self._container.add(w, self._set_checkbox, '_usbvm') if self._usbvm: w = CheckboxWidget(title=_( 'Use sys-net qube for both networking and USB devices'), completed=self._usbvm_with_netvm) self._container.add(w, self._set_checkbox, '_usbvm_with_netvm') self.window.add_with_separator(self._container) def _set_checkbox(self, name): setattr(self, name, not getattr(self, name)) def apply(self): """ The apply method that is called when the spoke is left. It should update the contents of self.data with values set in the spoke. """ for attr in self.qubes_data.bool_options: setattr(self.qubes_data, attr, getattr(self, '_' + attr)) self.qubes_data.seen = True def execute(self): """ The excecute method that is called when the spoke is left. It is supposed to do all changes to the runtime environment according to the values set in the spoke. """ # nothing to do here pass @property def completed(self): """ The completed property that tells whether all mandatory items on the spoke are set, or not. The spoke will be marked on the hub as completed or uncompleted acording to the returned value. :rtype: bool """ return self.qubes_data.seen @property def status(self): """ The status property that is a brief string describing the state of the spoke. It should describe whether all values are set and if possible also the values themselves. The returned value will appear on the hub below the spoke's title. :rtype: str """ return "" 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 (passed to App.switch_screen* methods) :type args: anything :param key: user's input :type key: unicode :return: if the input should not be handled here, return it, otherwise return INPUT_PROCESSED or INPUT_DISCARDED if the input was processed succesfully or not respectively :rtype: bool|unicode """ if self._container.process_user_input(key): self.apply() return InputState.PROCESSED_AND_REDRAW return super().input(args, key)
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 @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "language-configuration" @classmethod def should_run(cls, environment, data): """Should the spoke run?""" if not is_module_available(LOCALIZATION): return False return FirstbootSpokeMixIn.should_run(environment, data) 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 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: if key.lower() == PROMPT_BACK_KEY: 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")) prompt.add_option(PROMPT_BACK_KEY, _(PROMPT_BACK_DESCRIPTION)) return prompt def apply(self): """ Store the selected lang support locales """ self._l12_module.SetLanguage(self._selected)
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): super().__init__(data, storage, payload, instclass) 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.selected_disks = self._disk_select_observer.proxy.SelectedDisks self.title = N_("Installation Destination") self._ready = False self._container = None 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 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. """ 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.data.autopart.autopart: 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. """ 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): 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.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.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: # Is DASD formatting supported? if DasdFormatting.is_supported(): # Wait for storage. threadMgr.wait(THREAD_STORAGE) # Get selected disks. disks = getDisksByNames(self.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 applyDiskSelection(self.storage, self.data, self.selected_disks) # Run the dialog. self.run_dasdfmt_dialog(dasd_formatting) 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().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 apply(self): self.autopart = self.data.autopart.autopart self._disk_select_observer.proxy.SetSelectedDisks(self.selected_disks) self._disk_init_observer.proxy.SetDrivesToClear(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() # 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._disk_init_observer.proxy.SetInitializationMode( CLEAR_PARTITIONS_ALL) self._disk_init_observer.proxy.SetInitializeLabelsEnabled(False) self.storage.config.update() 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._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 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.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])
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 SoftwareSpoke(NormalTUISpoke): """ Spoke used to read new value of text to represent source repo. .. inheritance-diagram:: SoftwareSpoke :parts: 3 """ helpFile = "SoftwareSpoke.txt" category = SoftwareCategory @classmethod def should_run(cls, environment, data): """Don't run for any non-package payload.""" return context.payload.type == PAYLOAD_TYPE_DNF def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Software selection") self._container = None self.errors = [] self._tx_id = None # Get the packages configuration. self._selection = self.payload.get_packages_data() # are we taking values (package list) from a kickstart file? self._kickstarted = flags.automatedInstall and self.payload.proxy.PackagesKickstarted # Register event listeners to update our status on payload events payloadMgr.add_listener(PayloadState.STARTED, self._payload_start) payloadMgr.add_listener(PayloadState.ERROR, self._payload_error) def initialize(self): """Initialize the spoke.""" super().initialize() self.initialize_start() threadMgr.add( AnacondaThread(name=THREAD_SOFTWARE_WATCHER, target=self._initialize)) def _initialize(self): """Initialize the spoke in a separate thread.""" threadMgr.wait(THREAD_PAYLOAD) if not self._kickstarted: # Set the environment. self.set_default_environment() # Apply the initial selection. self.apply() # Check the initial software selection. self.execute() # Wait for the software selection thread that might be started by execute(). # 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 set_default_environment(self): # If an environment was specified in the configuration, use that. # Otherwise, select the first environment. if self.payload.environments: environments = self.payload.environments if conf.payload.default_environment in environments: self._selection.environment = conf.payload.default_environment else: self._selection.environment = environments[0] def _payload_start(self): self.errors = [] def _payload_error(self): self.errors = [payloadMgr.error] def _translate_env_name_to_id(self, environment): """ Return the id of the selected environment or None. """ if not environment: # None means environment is not set, no need to try translate that to an id return None try: return self.payload.environment_id(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.environment_addons: for addons_list in self.payload.environment_addons[environment_id]: addons.extend(addons_list) return addons @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.base_repo: return _("Installation source not set up") if not self.txid_valid: return _("Source changed - please verify") if not self._selection.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.environment_description( self._selection.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.base_repo and self.payload.proxy.PackagesKickstarted else: return processing_done and self.payload.base_repo and self._selection.environment def setup(self, args): """Set up the spoke right before it is used.""" super().setup(args) # Join the initialization thread to block on it threadMgr.wait(THREAD_SOFTWARE_WATCHER) # Get the packages configuration. self._selection = self.payload.get_packages_data() return True def refresh(self, args=None): """ Refresh screen. """ NormalTUISpoke.refresh(self, args) threadMgr.wait(THREAD_PAYLOAD) self._container = None if not self.payload.base_repo: 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) if args is None: msg = self._refresh_environments() else: msg = self._refresh_addons(args) self.window.add_with_separator(TextWidget(msg)) self.window.add_with_separator(self._container) def _refresh_environments(self): environments = self.payload.environments for env in environments: name = self.payload.environment_description(env)[0] selected = (env == self._selection.environment) widget = CheckboxWidget(title="%s" % name, completed=selected) self._container.add(widget, callback=self._set_environment_callback, data=env) return _("Base environment") def _refresh_addons(self, available_addons): for addon_id in available_addons: name = self.payload.group_description(addon_id)[0] selected = addon_id in self._selection.groups widget = CheckboxWidget(title="%s" % name, completed=selected) self._container.add(widget, callback=self._set_addons_callback, data=addon_id) if available_addons: return _("Additional software for selected environment") else: return _("No additional software to select.") def _set_environment_callback(self, data): self._selection.environment = data def _set_addons_callback(self, data): if data not in self._selection.groups: self._selection.groups.append(data) else: self._selection.groups.remove(data) 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 not self._selection.environment: self.close() # The environment was selected, switch screen elif args is None: # Get addons for the selected environment environment = self._selection.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.execute() self.close() else: return super().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 the changes.""" self._kickstarted = False # Clear packages data. self._selection.packages = [] self._selection.excluded_packages = [] # Clear groups data. self._selection.excluded_groups = [] self._selection.groups_package_types = {} # Select valid groups. # FIXME: Remove invalid groups from selected groups. log.debug("Setting new software selection: %s", self._selection) self.payload.set_packages_data(self._selection) def execute(self): """Execute the changes.""" threadMgr.add( AnacondaThread(name=THREAD_CHECK_SOFTWARE, target=self._check_software_selection)) def _check_software_selection(self): """Check the software selection.""" try: self.payload.check_software_selection() except DependencyError as e: self.errors = [str(e)] self._tx_id = None log.warning("Transaction error %s", str(e)) else: self._tx_id = self.payload.tx_id @property def txid_valid(self): """ Whether we have a valid dnf tx id. """ return self._tx_id == self.payload.tx_id
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.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 = payload_utils.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 payload_utils.mount_device(self._device, ISO_DIR) def _unmount_device(self): 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: # 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 SelectDeviceSpoke(NormalTUISpoke): """ Select device containing the install source ISO file. """ category = SoftwareCategory def __init__(self, data, storage, payload, instclass): NormalTUISpoke.__init__(self, data, storage, payload, instclass) self.title = N_("Select device containing the ISO file") self._container = None self._currentISOFile = 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): NormalTUISpoke.refresh(self, args) # 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: self._container = ListColumnContainer(1, columns_width=78, spacing=1) 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(SelectDeviceSpoke, self).input(args, key) # Override Spoke.apply def apply(self): pass
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 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 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" @staticmethod def get_screen_id(): """Return a unique id of this UI screen.""" return "software-source-configuration" @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 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 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 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): 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 = self._network_module.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.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 / ifcfg configuration), instead of using original kickstart. self._network_module.NetworkDeviceConfigurationChanged() (valid, error) = network.is_valid_hostname(self.hostname, local=True) if 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 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): NormalTUISpoke.__init__(self, data, storage, payload) 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.add_listener(PayloadState.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 = find_optical_install_media() # Enable the SE/HMC option. if self.payload.is_hmc_enabled: 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.base_repo: 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.base_repo: return False else: return (not self._error and self.ready and (self.data.method.method or self.payload.base_repo)) 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 \ payload_utils.get_mount_device_path(DRACUT_ISODIR) == \ payload_utils.get_mount_device_path(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) 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): self.set_source_nfs() 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 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.""" passphrase = None for device_name in self._rescue.get_locked_device_names(): while True: if passphrase is None: dialog = PasswordDialog(device_name) ScreenHandler.push_screen_modal(dialog) if not dialog.answer: break passphrase = dialog.answer.strip() if self._rescue.unlock_device(device_name, passphrase): break passphrase = None def apply(self): """Move along home.""" pass
class PartTypeSpoke(NormalTUISpoke): """ Partitioning options are presented here. .. inheritance-diagram:: PartTypeSpoke :parts: 3 """ category = SystemCategory def __init__(self, data, storage, payload, storage_module, partitioning): super().__init__(data, storage, payload) self.title = N_("Partitioning Options") self._container = None # Choose the initialization mode. self._disk_init_proxy = STORAGE.get_proxy(DISK_INITIALIZATION) self._orig_init_mode = self._disk_init_proxy.InitializationMode self._init_mode = self._orig_init_mode self._init_mode_list = sorted(INIT_MODES.keys()) if self._init_mode == CLEAR_PARTITIONS_DEFAULT: self._init_mode = CLEAR_PARTITIONS_ALL # Choose the partitioning method. self._storage_module = storage_module self._partitioning = partitioning self._orig_part_method = self._partitioning.PartitioningMethod self._part_method = self._orig_part_method if self._part_method == PARTITIONING_METHOD_MANUAL: self._init_mode = CLEAR_PARTITIONS_NONE @property def indirect(self): return True @property def partitioning(self): return self._partitioning def refresh(self, args=None): super().refresh(args) self._container = ListColumnContainer(1) for init_mode in self._init_mode_list: c = CheckboxWidget(title=_(init_mode), completed=( self._part_method == PARTITIONING_METHOD_AUTOMATIC and self._init_mode == INIT_MODES[init_mode] )) self._container.add(c, self._select_partition_type_callback, init_mode) c = CheckboxWidget(title=_("Manually assign mount points"), completed=( self._part_method == PARTITIONING_METHOD_MANUAL )) 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._part_method = PARTITIONING_METHOD_MANUAL self._init_mode = CLEAR_PARTITIONS_NONE def _select_partition_type_callback(self, data): self._part_method = PARTITIONING_METHOD_AUTOMATIC self._init_mode = INIT_MODES[data] 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._disk_init_proxy.SetInitializationMode(self._init_mode) self._disk_init_proxy.SetInitializeLabelsEnabled( self._part_method == PARTITIONING_METHOD_AUTOMATIC ) if self._orig_part_method != self._part_method: self._partitioning = create_partitioning(self._part_method) 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_part_method == self._part_method == PARTITIONING_METHOD_MANUAL: return # else print(_("Reverting previous configuration. This may take a moment...")) reset_storage(scan_all=True) def input(self, args, key): """Grab the choice and update things""" if not self._container.process_user_input(key): if key.lower() == Prompt.CONTINUE: self.apply() self._ensure_init_storage() if self._part_method == PARTITIONING_METHOD_MANUAL: new_spoke = MountPointAssignSpoke( self.data, self.storage, self.payload, self._partitioning ) else: new_spoke = PartitionSchemeSpoke( self.data, self.storage, self.payload, self._partitioning ) ScreenHandler.push_screen_modal(new_spoke) return InputState.PROCESSED_AND_CLOSE else: return super().input(args, key) return InputState.PROCESSED_AND_REDRAW
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 PartitionSchemeSpoke(NormalTUISpoke): """ Spoke to select what partitioning scheme to use on disk(s). """ category = SystemCategory def __init__(self, data, storage, payload, partitioning): super().__init__(data, storage, payload) self.title = N_("Partition Scheme Options") self._container = None self._part_schemes = OrderedDict() self._partitioning = partitioning self._request = PartitioningRequest.from_structure( self._partitioning.Request ) 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] selected_choice = self._request.partitioning_scheme for item in supported_choices: self._part_schemes[item[0]] = item[1] if item[1] == selected_choice: 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 self._request.partitioning_scheme = data def input(self, args, key): """ Grab the choice and update things. """ if not self._container.process_user_input(key): if key.lower() == Prompt.CONTINUE: 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._partitioning.SetRequest( PartitioningRequest.to_structure(self._request) )
class NTPServersSpoke(NormalTUISpoke): category = LocalizationCategory def __init__(self, data, storage, payload, instclass, time_spoke): NormalTUISpoke.__init__(self, 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): NormalTUISpoke.refresh(self, 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(NTPServersSpoke, self).input(args, key) def apply(self): pass
class MountPointAssignSpoke(NormalTUISpoke): """ Assign mount points to block devices. """ category = SystemCategory def __init__(self, data, storage, payload, partitioning): super().__init__(data, storage, payload) self.title = N_("Assign mount points") self._container = None self._partitioning = partitioning self._device_tree = STORAGE.get_proxy(self._partitioning.GetDeviceTree()) self._requests = self._gather_requests() @property def indirect(self): return True def refresh(self, args=None): """Refresh the window.""" super().refresh(args) self._container = ListColumnContainer(2) for request in self._requests: widget = TextWidget(self._get_request_description(request)) self._container.add(widget, self._configure_request, request) 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) prompt.add_option(PROMPT_SCAN_KEY, _(PROMPT_SCAN_DESCRIPTION)) return prompt def input(self, args, key): """ Grab the choice and update things. """ if self._container.process_user_input(key): return InputState.PROCESSED if key.lower() == PROMPT_SCAN_KEY: self._rescan_devices() return InputState.PROCESSED_AND_REDRAW elif key.lower() == Prompt.CONTINUE: self.apply() return super().input(args, key) def apply(self): """ Apply our selections. """ mount_points = [] for request in self._requests: if request.reformat or request.mount_point: if not request.mount_point: request.mount_point = "none" mount_points.append(request) self._partitioning.SetRequests( MountPointRequest.to_structure_list(mount_points) ) def _gather_requests(self): """Gather info about mount points.""" return MountPointRequest.from_structure_list( self._partitioning.GatherRequests() ) def _get_request_description(self, request): """Get description of the given mount info.""" # Get the device data. device_name = self._device_tree.ResolveDevice(request.device_spec) device_data = DeviceData.from_structure( self._device_tree.GetDeviceData(device_name) ) # Generate the description. description = "{} ({})".format(request.device_spec, Size(device_data.size)) if request.format_type: description += "\n {}".format(request.format_type) if request.reformat: description += "*" if request.mount_point: description += ", {}".format(request.mount_point) return description def _configure_request(self, request): """Configure the given mount request.""" spoke = ConfigureDeviceSpoke( self.data, self.storage, self.payload, self._device_tree, request ) 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(scan_all=True) # Forget the mount point requests. self._partitioning.SetRequests([]) self._requests = self._gather_requests()
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 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._policy = self.data.anaconda.pwpolicy.get_policy( "user", fallback_to_default=True) 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=self._policy) 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 self._policy.changesok) @property def mandatory(self): """Only mandatory if no admin user has been requested.""" return not self._users_module.CheckAdminUserExists() @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 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 @classmethod def should_run(cls, environment, data): """Don't run the storage spoke on dir installations.""" if not NormalTUISpoke.should_run(environment, data): return False return not conf.target.is_directory def __init__(self, data, storage, payload): super().__init__(data, storage, payload) self.title = N_("Installation Destination") self._container = None self._ready = False self._select_all = False self._storage_module = STORAGE.get_proxy() self._device_tree = STORAGE.get_proxy(DEVICE_TREE) self._bootloader_module = STORAGE.get_proxy(BOOTLOADER) self._disk_init_module = STORAGE.get_proxy(DISK_INITIALIZATION) self._disk_select_module = STORAGE.get_proxy(DISK_SELECTION) self._available_disks = [] self._selected_disks = [] # Is the partitioning already configured? self._is_preconfigured = bool(self._storage_module.CreatedPartitioning) # Find a partitioning to use. self._partitioning = find_partitioning() self.errors = [] self.warnings = [] @property def completed(self): return self.ready and not self.errors and self._device_tree.GetRootDevice() @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) \ and not threadMgr.get(THREAD_STORAGE_WATCHER) @property def mandatory(self): return True @property def status(self): """ A short string describing the current status of storage setup. """ if not self.ready: return _("Processing...") elif flags.automatedInstall and not self._device_tree.GetRootDevice(): return _("Kickstart insufficient") elif not self._disk_select_module.SelectedDisks: return _("No disks selected") if self.errors: return _("Error checking storage configuration") elif self.warnings: return _("Warning checking storage configuration") elif self._partitioning.PartitioningMethod == PARTITIONING_METHOD_AUTOMATIC: return _("Automatic partitioning selected") else: return _("Custom partitioning selected") def _update_disk_list(self, name): """ Update self.selected_disks based on the selection.""" # 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(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 setup(self, args=None): """Set up the spoke right before it is used.""" super().setup(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) self._available_disks = self._disk_select_module.GetUsableDisks() self._selected_disks = self._disk_select_module.SelectedDisks # Get the available selected disks. self._selected_disks = filter_disks_by_names(self._available_disks, self._selected_disks) # Get the available partitioning. object_path = self._storage_module.CreatedPartitioning[-1] self._partitioning = STORAGE.get_proxy(object_path) return True def refresh(self, args=None): """Prepare the content of the screen.""" super().refresh(args) # Create a new container. self._container = ListColumnContainer(1, spacing=1) # loop through the disks and present them. for disk_name in self._available_disks: disk_info = self._format_disk_info(disk_name) c = CheckboxWidget(title=disk_info, completed=(disk_name in self._selected_disks)) self._container.add(c, self._update_disk_list_callback, disk_name) # 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(self._update_summary())) def _select_all_disks_callback(self, data): """ Mark all disks as selected for use in partitioning. """ self._select_all = True for disk_name in self._available_disks: if disk_name not in self._selected_disks: self._update_disk_list(disk_name) 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. """ data = DeviceData.from_structure( self._device_tree.GetDeviceData(disk) ) # show this info for all disks format_str = "{}: {} ({})".format( data.attrs.get("model", "DISK"), Size(data.size), data.name ) # now append all additional attributes to our string disk_attrs = filter(None, map(data.attrs.get, ( "wwn", "bus-id", "fcp-lun", "wwpn", "hba-id" ))) 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: if key.lower() == Prompt.CONTINUE: if self._selected_disks: # Is DASD formatting supported? if DasdFormatting.is_supported(): # Wait for storage. threadMgr.wait(THREAD_STORAGE) # Allow to format DASDs. self._disk_init_module.SetFormatUnrecognizedEnabled(True) self._disk_init_module.SetFormatLDLEnabled(True) # 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._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 report = ValidationReport.from_structure( self._disk_select_module.ValidateSelectedDisks(self._selected_disks) ) self.errors.extend(report.get_messages()) 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, self._storage_module, self._partitioning) ScreenHandler.push_screen_modal(new_spoke) self._partitioning = new_spoke.partitioning 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() dasd_formatting.report.disconnect(self._show_dasdfmt_report) def _show_dasdfmt_report(self, msg): print(msg, flush=True) def run_passphrase_dialog(self): """Ask user for a default passphrase.""" if not self._is_passphrase_required(): 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_name=PASSWORD_POLICY_LUKS, process_func=lambda x: x ) passphrase = None while passphrase is None: passphrase = dialog.run() self._set_required_passphrase(passphrase) def _is_passphrase_required(self): """Is the default passphrase required?""" return self._partitioning.PartitioningMethod in ( PARTITIONING_METHOD_AUTOMATIC, PARTITIONING_METHOD_CUSTOM ) and self._partitioning.RequiresPassphrase() def _set_required_passphrase(self, passphrase): """Set the required passphrase.""" self._partitioning.SetPassphrase(passphrase) def apply(self): self._bootloader_module.SetPreferredLocation(BOOTLOADER_LOCATION_MBR) apply_disk_selection(self._selected_disks, reset_boot_drive=True) def execute(self): report = apply_partitioning(self._partitioning, self._show_execute_message) log.debug("Partitioning has been applied: %s", report) self.errors = list(report.error_messages) self.warnings = list(report.warning_messages) print("\n".join(report.get_messages())) self._ready = True def _show_execute_message(self, msg): print(msg) log.debug(msg) 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)) 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. disks = self._disk_select_module.GetUsableDisks() DasdFormatting.run_automatically(disks) # Update the selected disks. select_default_disks() # Automatically apply the preconfigured partitioning. if flags.automatedInstall and self._is_preconfigured: self.execute() # Storage is ready. self._ready = True # Report that the storage spoke has been initialized. self.initialize_done() def closed(self): """The spoke has been closed.""" super().closed() # Run the setup method again on entry. self.screen_ready = 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): super().__init__(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): super().refresh(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().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 ConfigureDeviceSpoke(NormalTUISpoke): """ Assign mount point to a block device and (optionally) reformat it. """ category = SystemCategory def __init__(self, data, storage, payload, device_tree, request): super().__init__(data, storage, payload) self.title = N_("Configure device: %s") % request.device_spec self._container = None self._device_tree = device_tree self._request = request self._supported_filesystems = set(device_tree.GetSupportedFileSystems()) @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 = DeviceFormatData.from_structure( self._device_tree.GetFormatTypeData(self._request.format_type) ) if fmt.mountable: # mount point can be set value = self._request.mount_point or _("none") callback = self._assign_mount_point elif not fmt.type: # 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.description 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._request.mount_point = dialog.run() # Always reformat root. if self._request.mount_point == "/": self._request.reformat = True def _add_format_widget(self): """Add a widget for format.""" dialog = Dialog(_("Format"), conditions=[self._check_format]) widget = EntryWidget(dialog.title, self._request.format_type 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._request.format_type new_format = dialog.run() # Reformat to a new format. if new_format != old_format: self._request.format_type = new_format self._request.reformat = True def _add_reformat_widget(self): """Add a widget for reformat.""" widget = CheckboxWidget( title=_("Reformat"), completed=self._request.reformat ) self._container.add(widget, self._switch_reformat) def _switch_reformat(self, data): """Change value of reformat.""" device_name = self._device_tree.ResolveDevice( self._request.device_spec ) format_data = DeviceFormatData.from_structure( self._device_tree.GetFormatData(device_name) ) device_format = format_data.type if device_format and device_format != self._request.format_type: reformat = True elif self._request.mount_point == "/": reformat = True else: reformat = not self._request.reformat self._request.reformat = reformat
class MountPointAssignSpoke(NormalTUISpoke): """ Assign mount points to block devices. """ category = SystemCategory def __init__(self, data, storage, payload, instclass): super().__init__(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, selected_disks): maybe = not dev.protected and dev.size != Size(0) if maybe and selected_disks: # all device's disks have to be in selected disks maybe = set(selected_disks).issuperset({d.name for d in dev.disks}) return maybe def _gather_mount_data_info(self): self._mds = OrderedDict() disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) selected_disks = disk_select_proxy.SelectedDisks for device in self.storage.devicetree.leaves: if not self._is_dev_usable(device, selected_disks): continue 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().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): super().refresh(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 selected disks temporarily so that # storage_initialize() processes all devices disk_select_proxy = STORAGE.get_proxy(DISK_SELECTION) selected_disks = disk_select_proxy.SelectedDisks disk_select_proxy.SetSelectedDisks([]) print(_("Scanning disks. This may take a moment...")) storage_initialize(self.storage, self.data, self.storage.protected_dev_names) disk_select_proxy.SetSelectedDisks(selected_disks) 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().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 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) # Forget the mount point requests. self._manual_part_proxy.SetRequests([]) 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 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 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 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 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: # 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)) 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))