[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