# Source repo text spoke # # Copyright (C) 2013 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the # GNU General Public License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.payload import parse_nfs_url from pyanaconda.modules.common.constants.objects import DEVICE_TREE from pyanaconda.modules.common.constants.services import STORAGE from pyanaconda.modules.common.structures.payload import RepoConfigurationData from pyanaconda.payload.utils import get_device_path from pyanaconda.ui.categories.software import SoftwareCategory from pyanaconda.ui.context import context from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.tuiobject import Dialog from pyanaconda.core.threads import thread_manager from pyanaconda.payload import utils as payload_utils from pyanaconda.payload.manager import payloadMgr from pyanaconda.core.i18n import N_, _ from pyanaconda.ui.lib.payload import find_potential_hdiso_sources, get_hdiso_source_info, \ get_hdiso_source_description from pyanaconda.core.constants import THREAD_SOURCE_WATCHER, THREAD_PAYLOAD, PAYLOAD_TYPE_DNF, \ SOURCE_TYPE_URL, SOURCE_TYPE_NFS, SOURCE_TYPE_HMC, PAYLOAD_STATUS_SETTING_SOURCE, \ PAYLOAD_STATUS_INVALID_SOURCE from pyanaconda.core.constants import THREAD_STORAGE_WATCHER from pyanaconda.core.constants import THREAD_CHECK_SOFTWARE, ISO_DIR, DRACUT_ISODIR from pyanaconda.core.constants import PAYLOAD_STATUS_PROBING_STORAGE from pyanaconda.ui.helpers import SourceSwitchHandler from simpleline.render.containers import ListColumnContainer from simpleline.render.prompt import Prompt from simpleline.render.screen import InputState from simpleline.render.screen_handler import ScreenHandler from simpleline.render.widgets import TextWidget, EntryWidget import os import fnmatch from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) __all__ = ["SourceSpoke"] class SourceSpoke(NormalTUISpoke, SourceSwitchHandler): """ Spoke used to customize the install source repo. .. inheritance-diagram:: SourceSpoke :parts: 3 """ 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() # Register callbacks to signals of the payload manager. payloadMgr.failed_signal.connect(self._on_payload_failed) # It is possible that the payload manager is finished by now. In that case, # trigger the failed callback manually to set up the error messages. if not payloadMgr.is_running and not payloadMgr.report.is_valid(): self._on_payload_failed() # Finish the initialization. thread_manager.add_thread( name=THREAD_SOURCE_WATCHER, target=self._initialize ) def _initialize(self): """ Private initialize. """ thread_manager.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 _on_payload_failed(self): self._error = True @property def status(self): """The status of the spoke.""" if not self.ready: return _(PAYLOAD_STATUS_SETTING_SOURCE) if not self.completed: return _(PAYLOAD_STATUS_INVALID_SOURCE) source_proxy = self.payload.get_source_proxy() return source_proxy.Description @property def completed(self): """Is the spoke complete?""" return self.ready and not self._error and self.payload.is_ready() def refresh(self, args=None): NormalTUISpoke.refresh(self, args) thread_manager.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 thread_manager.get(THREAD_PAYLOAD) and not thread_manager.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 # Restart the payload setup. payloadMgr.start(self.payload) 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.Configuration ) 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 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: configuration = RepoConfigurationData.from_structure( source_proxy.Configuration ) return parse_nfs_url(configuration.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 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 thread_manager.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 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)