552 lines
19 KiB
Python
552 lines
19 KiB
Python
|
# 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)
|