[PATCH] Add HDD ISO support for TUI (#1000327)

Martin Kolman mkolman at redhat.com
Tue Oct 29 22:00:44 UTC 2013


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.
+    """
+    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")]
+        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))
 
-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
+
+    def apply(self):
+        """ Apply all of our changes. """
+
+        if self._current_iso_path:
+            self.set_source_hdd_iso(self._device, self._current_iso_path)
+        else:
+            self._no_refresh = True
+        # unmount the device - the (YUM) payload will remount it anyway
+        # (if it uses it)
+        self._unmount_device()
-- 
1.8.3.1



More information about the anaconda-patches mailing list