[PATCH] Add HDD ISO support for TUI (#1000327)
Martin Kolman
mkolman at redhat.com
Wed Oct 30 15:47:11 UTC 2013
On Wed, 2013-10-30 at 15:57 +0100, Vratislav Podzimek wrote:
> 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.
>
OK, fixing locally and waiting for any other feedback. :)
More information about the anaconda-patches
mailing list