[PATCH] Add HDD ISO support for TUI (#1000327)
Vratislav Podzimek
vpodzime at redhat.com
Wed Oct 30 14:57:20 UTC 2013
On Tue, 2013-10-29 at 23:00 +0100, Martin Kolman wrote:
> It is possible to:
> * select a mountable device
> * select a *.iso from the root folder of the device
>
> Signed-off-by: Martin Kolman <mkolman at redhat.com>
> ---
> pyanaconda/ui/tui/spokes/source.py | 336 ++++++++++++++++++++++++++++++++++---
> 1 file changed, 316 insertions(+), 20 deletions(-)
>
> diff --git a/pyanaconda/ui/tui/spokes/source.py b/pyanaconda/ui/tui/spokes/source.py
> index 62241a0..2fe286a 100644
> --- a/pyanaconda/ui/tui/spokes/source.py
> +++ b/pyanaconda/ui/tui/spokes/source.py
> @@ -20,18 +20,24 @@
> #
>
> from pyanaconda.flags import flags
> -from pyanaconda.ui.tui.spokes import EditTUISpoke
> +from pyanaconda.ui.tui.spokes import EditTUISpoke, NormalTUISpoke
> from pyanaconda.ui.tui.spokes import EditTUISpokeEntry as Entry
> from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget
> from pyanaconda.threads import threadMgr, AnacondaThread
> from pyanaconda.packaging import PayloadError, MetadataError
> from pyanaconda.i18n import _
> -from pyanaconda.image import opticalInstallMedia
> +from pyanaconda.image import opticalInstallMedia, potentialHdisoSources
>
> from pyanaconda.constants import THREAD_SOURCE_WATCHER, THREAD_SOFTWARE_WATCHER, THREAD_PAYLOAD
> -from pyanaconda.constants import THREAD_PAYLOAD_MD, THREAD_STORAGE, THREAD_CHECK_SOFTWARE
> +from pyanaconda.constants import THREAD_PAYLOAD_MD, THREAD_STORAGE, THREAD_STORAGE_WATCHER
> +from pyanaconda.constants import THREAD_CHECK_SOFTWARE, ISO_DIR, DRACUT_ISODIR
> +
> +from blivet.util import get_mount_paths
>
> import re
> +import os
> +import fnmatch
> +import copy
>
> import logging
> LOG = logging.getLogger("anaconda")
> @@ -39,7 +45,89 @@ LOG = logging.getLogger("anaconda")
>
> __all__ = ["SourceSpoke"]
>
> -class SourceSpoke(EditTUISpoke):
> +class SourceSwitchHandler(object):
> + """ A class that can be used as a mixin handling
> + installation source switching.
> + It will correctly switch to the new method
> + and cleanup any previous method set.
> + """
I'd like to see some whitespace around docstrings and also between the
"title" and params, types and things like that.
> + def __init__(self):
> + self._device = None
> + self._current_iso_path = None
> +
> + def _clean_hdd_iso(self):
> + """ Clean HDD ISO usage
> + This means unmounting the partition and unprotecting it,
> + so it can be used for the installation.
> + """
> + if self.data.method.method == "harddrive" and self.data.method.partition:
> + part = self.data.method.partition
> + dev = self.storage.devicetree.getDeviceByName(part)
> + if dev:
> + dev.protected = False
> + self.storage.config.protectedDevSpecs.remove(part)
> +
> + def set_source_hdd_iso(self, device, iso_path):
> + """ Switch to the HDD ISO install source
> + :param partition: name of the partition hosting the ISO
> + :type partition: string
> + :param iso_path: full path to the source ISO file
> + :type iso_path: string
> + """
> + partition = device.name
> + # the GUI source spoke also does the copy
> + old_source = copy.copy(self.data.method)
> +
> + # if a different partition was used previously, unprotect it
> + if old_source.method == "harddrive" and old_source.partition != partition:
> + self._clean_hdd_iso()
> +
> + # protect current device
> + if device:
> + device.protected = True
> + self.storage.config.protectedDevSpecs.append(device.name)
> +
> + self.data.method.method = "harddrive"
> + self.data.method.partition = partition
> + # the / gets stripped off by payload.ISOImage
> + self.data.method.dir = "/" + iso_path
> +
> + # as we already made the device protected when
> + # switching to it, we don't need to protect it here
> +
> + def set_source_url(self, url=None):
> + """ Switch to install source specified by URL """
> + # clean any old HDD ISO sources
> + self._clean_hdd_iso()
> +
> + self.data.method.method = "url"
> + if url is not None:
> + self.data.method.url = url
> +
> + def set_source_nfs(self, opts=None):
> + """ Switch to NFS install source """
> + # clean any old HDD ISO sources
> + self._clean_hdd_iso()
> +
> + self.data.method.method = "nfs"
> + if opts is not None:
> + self.data.method.opts = opts
> +
> + def set_source_cdrom(self):
> + """ Switch to cdrom install source """
> + # clean any old HDD ISO sources
> + self._clean_hdd_iso()
> +
> + self.data.method.method = "cdrom"
> +
> + def set_source_closest_mirror(self):
> + """ Switch to the closest mirror install source """
> + # clean any old HDD ISO sources
> + self._clean_hdd_iso()
> +
> + self.data.method.method = None
> +
> +class SourceSpoke(EditTUISpoke, SourceSwitchHandler):
> """ Spoke used to customize the install source repo. """
> title = _("Installation source")
> category = "source"
> @@ -51,6 +139,7 @@ class SourceSpoke(EditTUISpoke):
>
> def __init__(self, app, data, storage, payload, instclass):
> EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
> + SourceSwitchHandler.__init__(self)
> self._ready = False
> self.errors = []
> self._cdrom = None
> @@ -88,6 +177,10 @@ class SourceSpoke(EditTUISpoke):
> return _("NFS server %s") % self.data.method.server
> elif self.data.method.method == "cdrom":
> return _("Local media")
> + elif self.data.method.method == "harddrive":
> + if not self.data.method.dir:
> + return _("Error setting up software source")
> + return os.path.basename(self.data.method.dir)
> elif self.payload.baseRepo:
> return _("Closest mirror")
> else:
> @@ -123,8 +216,8 @@ class SourceSpoke(EditTUISpoke):
>
> threadMgr.wait(THREAD_PAYLOAD_MD)
>
> - _methods = [_("CD/DVD"), _("Network")]
> - if args == 2:
> + _methods = [_("CD/DVD"), _("local ISO file"), _("Network")]
I think "Local ISO file" would look better.
> + if args == 3:
> text = [TextWidget(p) for p in self._protocols]
> else:
> self._window += [TextWidget(_("Choose an installation source type."))]
> @@ -144,14 +237,15 @@ class SourceSpoke(EditTUISpoke):
> return True
>
> def input(self, args, key):
> - """ Handle the input; this decides the repo protocol. """
> + """ Handle the input; this decides the repo source. """
> try:
> num = int(key)
> - if args == 2:
> + if args == 3:
> # network install
> self._selection = num
> if 0 < self._selection < 4:
> - self.data.method.method = "url"
> + # preliminary URL source switch
> + self.set_source_url()
> newspoke = SpecifyRepoSpoke(self.app, self.data, self.storage,
> self.payload, self.instclass, self._selection)
> self.app.switch_screen_modal(newspoke)
> @@ -160,7 +254,8 @@ class SourceSpoke(EditTUISpoke):
> return True
> elif self._selection == 4:
> # nfs
> - self.data.method.method = "nfs"
> + # preliminary NFS source switch
> + self.set_source_nfs()
> newspoke = SpecifyNFSRepoSpoke(self.app, self.data, self.storage,
> self.payload, self.instclass, self._selection, self.errors)
> self.app.switch_screen_modal(newspoke)
> @@ -169,14 +264,25 @@ class SourceSpoke(EditTUISpoke):
> return True
> elif self._selection == 5 and self.payload.mirrorEnabled:
> # closest mirror
> - self.data.method.method = None
> + self.set_source_closest_mirror()
> self.apply()
> self.close()
> return True
> + elif num == 2:
> + # local ISO file (HDD ISO)
> + self._selection = num
> + newspoke = SelectDeviceSpoke(self.app, self.data,
> + self.storage, self.payload,
> + self.instclass)
> + self.app.switch_screen_modal(newspoke)
> + self.apply()
> + self.close()
> + return True
> else:
> + # mounted ISO
> if num == 1:
> # iso selected, just set some vars and return to main hub
> - self.data.method.method = "cdrom"
> + self.set_source_cdrom()
> self.payload.install_device = self._cdrom
> self.apply()
> self.close()
> @@ -216,10 +322,19 @@ class SourceSpoke(EditTUISpoke):
>
> def apply(self):
> """ Execute the selections made. """
> + # If askmethod was provided on the command line, entering the source
> + # spoke wipes that out.
> + if flags.askmethod:
> + flags.askmethod = False
> +
> + # when a payload refresh is not needed
> + # (nothing was changed in the source spoke)
> + # the _no_refresh flag is set and payload refresh
> + # is not done
> threadMgr.add(AnacondaThread(name=THREAD_PAYLOAD_MD,
> - target=self.getRepoMetadata))
> + target=self.getRepoMetadata))
This changes nothing, just modifies git history. And the comment is a
remnant of an old version of the patch (discussed with mkolman).
>
> -class SpecifyRepoSpoke(EditTUISpoke):
> +class SpecifyRepoSpoke(EditTUISpoke, SourceSwitchHandler):
> """ Specify the repo URL here if closest mirror not selected. """
> title = _("Specify Repo Options")
> category = "source"
> @@ -230,6 +345,7 @@ class SpecifyRepoSpoke(EditTUISpoke):
>
> def __init__(self, app, data, storage, payload, instclass, selection):
> EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
> + SourceSwitchHandler.__init__(self)
> self.selection = selection
> self.args = self.data.method
>
> @@ -243,18 +359,20 @@ class SpecifyRepoSpoke(EditTUISpoke):
>
> def apply(self):
> """ Apply all of our changes. """
> + url = None
> if self.selection == 2 and not self.args.url.startswith("http://"):
> - self.data.method.url = "http://" + self.args.url
> + url = "http://" + self.args.url
> elif self.selection == 3 and not self.args.url.startswith("https://"):
> - self.data.method.url = "https://" + self.args.url
> + url = "https://" + self.args.url
> elif self.selection == 4 and not self.args.url.startswith("ftp://"):
> - self.data.method.url = "ftp://" + self.args.url
> + url = "ftp://" + self.args.url
> else:
> # protocol either unknown or entry already starts with a protocol
> # specification
> - self.data.method.url = self.args.url
> + url = self.args.url
> + self.set_source_url(url)
>
> -class SpecifyNFSRepoSpoke(EditTUISpoke):
> +class SpecifyNFSRepoSpoke(EditTUISpoke, SourceSwitchHandler):
> """ Specify server and mount opts here if NFS selected. """
> title = _("Specify Repo Options")
> category = "source"
> @@ -266,6 +384,7 @@ class SpecifyNFSRepoSpoke(EditTUISpoke):
>
> def __init__(self, app, data, storage, payload, instclass, selection, errors):
> EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
> + SourceSwitchHandler.__init__(self)
> self.selection = selection
> self.args = self.data.method
> self.errors = errors
> @@ -293,4 +412,181 @@ class SpecifyNFSRepoSpoke(EditTUISpoke):
> self.errors.append(_("Failed to set up installation source. Check the source address."))
> return
>
> - self.data.method.opts = self.args.opts or ""
> + opts = self.args.opts or ""
> + self.set_source_nfs(opts)
> +
> +class SelectDeviceSpoke(NormalTUISpoke):
> + """ Select device containing the install source ISO file. """
> + title = _("Select device containing the ISO file")
> + category = "source"
> +
> + def __init__(self, app, data, storage, payload, instclass):
> + NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
> + 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 = _("Probing storage...")
> + self._window += [TextWidget(message), ""]
> + return True
> +
> + # check if there are any mountable devices
> + if self._mountable_devices:
> + def _prep(i, w):
> + """ Mangle our text to make it look pretty on screen. """
> + number = TextWidget("%2d)" % (i + 1))
> + return ColumnWidget([(4, [number]), (None, [w])], 1)
> +
> + devices = [TextWidget(d[1]) for d in self._mountable_devices]
> +
> + # gnarl and mangle all of our widgets so things look pretty on
> + # screen
> + choices = [_prep(i, w) for i, w in enumerate(devices)]
> +
> + displayed = ColumnWidget([(78, choices)], 1)
> + self._window.append(displayed)
> +
> + else:
> + message = _("No mountable devices found")
> + self._window += [TextWidget(message), ""]
> + return True
> +
> + def input(self, args, key):
> + try:
> + # try to switch to one of the mountable devices
> + # to look for ISOs
> + num = int(key)
> + device = self._mountable_devices[num-1][0] # get the device object
> + self._device = device
> + newspoke = SelectISOSpoke(self.app, self.data,
> + self.storage, self.payload,
> + self.instclass, device)
> + self.app.switch_screen_modal(newspoke)
> + self.close()
> + return True
> + except (IndexError, ValueError):
> + # either the input was not a number or
> + # we don't have the disk for the given number
> + return key
> +
> +class SelectISOSpoke(NormalTUISpoke, SourceSwitchHandler):
> + """ Select an ISO to use as install source. """
> + title = _("Select an ISO to use as install source")
> + category = "source"
> +
> + def __init__(self, app, data, storage, payload, instclass, device):
> + NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
> + SourceSwitchHandler.__init__(self)
> + self.selection = 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:
> + isos = [TextWidget(iso) for iso in self._isos]
> +
> + def _prep(i, w):
> + """ Mangle our text to make it look pretty on screen. """
> + number = TextWidget("%2d)" % (i + 1))
> + return ColumnWidget([(4, [number]), (None, [w])], 1)
> +
> + # gnarl and mangle all of our widgets so things look pretty on screen
> + choices = [_prep(i, w) for i, w in enumerate(isos)]
> +
> + displayed = ColumnWidget([(78, choices)], 1)
> + self._window.append(displayed)
> + else:
> + message = _("No *.iso files found in device root folder")
> + self._window += [TextWidget(message), ""]
> +
> + return True
> +
> + def input(self, args, key):
> + if key == "c":
> + self.apply()
> + self.close()
> + return key
> + try:
> + num = int(key)
> + # get the ISO path
> + self._current_iso_path = self._isos[num-1]
> + self.apply()
> + self.close()
> + return True
> + except (IndexError, ValueError):
> + return key
> +
> + @property
> + def indirect(self):
> + return True
> +
> + def _mount_device(self):
> + """ Mount the device so we can search it for ISOs. """
> + mounts = get_mount_paths(self._device.path)
> + # We have to check both ISO_DIR and the DRACUT_ISODIR because we
> + # still reference both, even though /mnt/install is a symlink to
> + # /run/install. Finding mount points doesn't handle the symlink
> + if ISO_DIR not in mounts and DRACUT_ISODIR not in mounts:
> + # We're not mounted to either location, so do the mount
> + self._device.format.mount(mountpoint=ISO_DIR)
> +
> + def _unmount_device(self):
> + self._device.format.unmount()
> +
> + def _getISOs(self):
> + """List all *.iso files in the root folder
> + of the currently selected device.
> +
> + TODO: advanced ISO file selection
> + :returns: a list of *.iso file paths
> + :rtype: list
> + """
> + isos = []
> + for filename in os.listdir(ISO_DIR):
> + if fnmatch.fnmatch(filename.lower(), "*.iso"):
> + isos.append(filename)
> + return isos
You can replace this with:
return glob.glob(ISO_DIR + "*.iso")
we don't have to support '.ISO', '.iSO' and things like that.
Otherwise this looks good to me as a first version of the spoke. We can
work on it later if needed and requested.
--
Vratislav Podzimek
Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic
More information about the anaconda-patches
mailing list