anaconda/anaconda-40.22.3.13/pyanaconda/ui/tui/spokes/installation_source.py

552 lines
19 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
# 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)