From b576c201ce2dae3585b41ba13736fae27844cf0d Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Thu, 26 Jul 2012 21:13:32 -0500
Subject: [PATCH 09/14] Use a copy of the main Storage instance during custom
 partitioning.

---
 pyanaconda/storage/__init__.py            |    3 -
 pyanaconda/ui/gui/spokes/custom.py        |  291 +++++++++++++++++++----------
 pyanaconda/ui/gui/spokes/lib/accordion.py |    5 +-
 3 files changed, 195 insertions(+), 104 deletions(-)

diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py
index 3193ca2..7f52c25 100644
--- a/pyanaconda/storage/__init__.py
+++ b/pyanaconda/storage/__init__.py
@@ -1875,9 +1875,6 @@ class Storage(object):
         disks = kwargs.get("disks")
         encrypted = kwargs.get("encrypted", self.data.autopart.encrypted)
 
-        # XXX temporary?
-        size = float(size.convertTo(spec="mb"))
-
         # md, btrfs
         raid_level = kwargs.get("level")
 
diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py
index 2793957..29aae8f 100644
--- a/pyanaconda/ui/gui/spokes/custom.py
+++ b/pyanaconda/ui/gui/spokes/custom.py
@@ -32,13 +32,17 @@ _ = lambda x: gettext.ldgettext("anaconda", x)
 N_ = lambda x: x
 P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z)
 
+from contextlib import contextmanager
+
 from pykickstart.constants import *
 
 from pyanaconda.product import productName, productVersion
 from pyanaconda.storage.formats import device_formats
+from pyanaconda.storage.formats import getFormat
 from pyanaconda.storage.size import Size
 from pyanaconda.storage import Root
 from pyanaconda.storage.partitioning import doPartitioning
+from pyanaconda.storage.partitioning import doAutoPartition
 from pyanaconda.storage.errors import StorageError
 
 from pyanaconda.ui.gui import UIObject
@@ -51,10 +55,26 @@ from pyanaconda.ui.gui.categories.storage import StorageCategory
 
 from gi.repository import Gtk
 
+import logging
+log = logging.getLogger("anaconda")
+
 __all__ = ["CustomPartitioningSpoke"]
 
 new_install_name = _("New %s %s Installation") % (productName, productVersion)
 
+class UIStorageFilter(logging.Filter):
+    def filter(self, record):
+        record.name = "storage.ui"
+        return True
+
+@contextmanager
+def ui_storage_logger():
+    storage_log = logging.getLogger("storage")
+    f = UIStorageFilter()
+    storage_log.addFilter(f)
+    yield
+    storage_log.removeFilter(f)
+
 class AddDialog(UIObject):
     builderObjects = ["addDialog"]
     mainWidgetName = "addDialog"
@@ -125,8 +145,67 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         self._current_selector = None
         self._when_create_text = ""
+        self._devices = []
+        self._free_space = Size(bytes=0)
 
     def apply(self):
+        log.debug("custom spoke apply")
+        # schedule actions for device removals, resizes
+        for removal in self.__storage.devicetree.findActions(type="destroy",
+                                                             object="device"):
+            removed = removal.device
+            if removed.id not in [d.id for d in self.storage.devices]:
+                # created here, destroyed here -> NOOP
+                continue
+
+            device = self.storage.devicetree.getDeviceByName(removed.name)
+            self.storage.destroyDevice(device)
+
+        for ui_device in self._devices:
+            log.debug("checking for changes to device %s (%d)" % (ui_device.name,
+                                                              ui_device.id))
+            if ui_device not in self.storage.devices:
+                # we're creating a device
+                log.debug("we created %s" % ui_device.name)
+
+                if not ui_device.isleaf:
+                    log.debug("%s is not a leaf device" % ui_device.name)
+                    continue
+
+                mountpoint = getattr(ui_device.format, "mountpoint", None)
+
+                if ui_device.type == "partition":
+                    device_type = AUTOPART_TYPE_PLAIN
+                elif ui_device.type == "lvmlv":
+                    device_type = AUTOPART_TYPE_LVM
+                elif ui_device.type == "btrfs subvolume":
+                    device_type = AUTOPART_TYPE_BTRFS
+                else:
+                    log.error("don't know what to do with '%s'" % ui_device)
+                    continue
+
+                # needs work
+                encrypted = self.data.autopart.encrypted
+                disks = self.storage.partitioned
+
+                log.debug("recreating %s" % ui_device.name)
+                self.storage.newUIDevice(device_type,
+                                         ui_device.size,
+                                         mountpoint=mountpoint,
+                                         fstype=ui_device.format.type,
+                                         encrypted=encrypted,
+                                         level=None,
+                                         disks=disks)
+            else:
+                # this is a device that's already in the tree
+                log.debug("%s is already in the tree" % ui_device.name)
+                device = self.storage.devicetree.getDeviceByName(ui_device.name)
+                if ui_device.resizable and ui_device.size != device.size:
+                    # schedule a resize action for the device and its format
+                    pass
+
+                # TODO: handle edits to a device in the tree
+
         self.storage.setUpBootLoader()
         StorageChecker.run(self)
 
@@ -147,7 +226,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         self._configButton = self.builder.get_object("configureButton")
 
     def initialize(self):
-        from pyanaconda.storage.devices import DiskDevice
         from pyanaconda.storage.formats.fs import FS
 
         NormalSpoke.initialize(self)
@@ -198,40 +276,36 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         else:
             return None
 
+    @property
     def _clearpartDevices(self):
-        return [d for d in self.storage.devicetree.devices if d.name in self.data.clearpart.drives]
+        return [d for d in self._devices if d.name in self.data.clearpart.drives]
 
     def _unusedDevices(self):
         from pyanaconda.storage.devices import DiskDevice
-        return [d for d in self.storage.unusedDevices if d.disks and not isinstance(d, DiskDevice)]
+        return [d for d in self.__storage.unusedDevices if d.disks and not isinstance(d, DiskDevice)]
 
-    def _currentFreeSpace(self):
+    def _setCurrentFreeSpace(self):
         """Add up all the free space on selected disks and return it as a Size."""
-        totalFree = Size(bytes=0)
-
-        freeDisks = self.storage.getFreeSpace(disks=self._clearpartDevices())
-        for tup in freeDisks.values():
-            for chunk in tup:
-                totalFree += chunk
-
-        return totalFree
+        freeDisks = self.__storage.getFreeSpace(clearPartType=CLEARPART_TYPE_NONE)
+        self._free_space = sum([f[0] for f in freeDisks.values()])
 
     def _currentTotalSpace(self):
         """Add up the sizes of all selected disks and return it as a Size."""
         totalSpace = 0
 
-        for disk in self._clearpartDevices():
+        for disk in self._clearpartDevices:
             totalSpace += disk.size
 
         return Size(spec="%s MB" % totalSpace)
 
     def _updateSpaceDisplay(self):
         # Set up the free space/available space displays in the bottom left.
+        self._setCurrentFreeSpace()
         self._availableSpaceLabel = self.builder.get_object("availableSpaceLabel")
         self._totalSpaceLabel = self.builder.get_object("totalSpaceLabel")
         self._summaryButton = self.builder.get_object("summary_button")
 
-        self._availableSpaceLabel.set_text(str(self._currentFreeSpace()))
+        self._availableSpaceLabel.set_text(str(self._free_space))
         self._totalSpaceLabel.set_text(str(self._currentTotalSpace()))
 
         summaryLabel = self._summaryButton.get_children()[0]
@@ -245,7 +319,13 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
     def refresh(self):
         NormalSpoke.refresh(self)
+        self.__storage = self.storage.copy()
+        self._devices = self.__storage.devices
         self._do_refresh()
+
+        # update our free space number based on Storage
+        self._setCurrentFreeSpace()
+
         self._updateSpaceDisplay()
 
     def _do_refresh(self):
@@ -262,8 +342,13 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         did_expand = False
 
         unused = self._unusedDevices()
-        new_devices = [d for d in self.storage.devicetree.leaves if d not in unused and not d.exists]
-        roots = self.storage.roots[:]
+        new_devices = [d for d in self._devices if not d.exists]
+
+        log.debug("ui: unused=%s" % [d.name for d in unused])
+        log.debug("ui: new_devices=%s" % [d.name for d in new_devices])
+
+        ui_roots = [r for r in self.__storage.roots
+                        if r.device in self._devices]
 
         # If we've not yet run autopart, add an instance of CreateNewPage.  This
         # ensures it's only added once.
@@ -278,39 +363,33 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             label = self.builder.get_object("whenCreateLabel")
             label.set_text(self._when_create_text % (productName, productVersion))
         else:
-            swaps = [d for d in new_devices if d in self.storage.swaps]
-            mounts = dict([(d.format.mountpoint, d) for d in new_devices if getattr(d.format, "mountpoint", None)])
+            swaps = [d for d in new_devices if d.format.type == "swap"]
+            mounts = dict([(d.format.mountpoint, d) for d in new_devices
+                                if getattr(d.format, "mountpoint", None)])
             new_root = Root(mounts=mounts, swaps=swaps, name=new_install_name)
-            roots.insert(0, new_root)
+            ui_roots.insert(0, new_root)
 
         # Add in all the existing (or autopart-created) operating systems.
-        for root in roots:
-            if root.device and root.device not in self.storage.devices:
-                continue
-
+        for root in ui_roots:
             page = Page()
             page.pageTitle = root.name
 
             for device in root.swaps:
-                if device not in self.storage.devices:
-                    continue
-
-                selector = page.addDevice("Swap", device.size, None, self.on_selector_clicked)
+                selector = page.addDevice("Swap",
+                                          Size(spec="%f MB" % device.size),
+                                          None, self.on_selector_clicked)
                 selector._device = device
                 selector._root = root
 
             for (mountpoint, device) in root.mounts.iteritems():
-                if device not in self.storage.devices:
-                    continue
-
-                selector = page.addDevice(self._mountpointName(mountpoint) or device.format.name, device.size, mountpoint, self.on_selector_clicked)
+                selector = page.addDevice(self._mountpointName(mountpoint) or device.format.name, Size(spec="%f MB" % device.size), mountpoint, self.on_selector_clicked)
                 selector._device = device
                 selector._root = root
 
             page.show_all()
             self._accordion.addPage(page, cb=self.on_page_clicked)
 
-            if not did_expand and getattr(self._current_selector, "_root", None) and root.name == self._current_selector._root.name:
+            if not did_expand:
                 did_expand = True
                 self._accordion.expandPage(root.name)
 
@@ -320,14 +399,14 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             page.pageTitle = _("Unknown")
 
             for u in unused:
-                selector = page.addDevice(u.format.name, u.size, None, self.on_selector_clicked)
+                selector = page.addDevice(u.format.name, Size(spec="%f MB" % u.size), None, self.on_selector_clicked)
                 selector._device = u
                 selector._root = None
 
             page.show_all()
             self._accordion.addPage(page, cb=self.on_page_clicked)
 
-            if not did_expand and self._current_selector and unused == self._current_selector._root:
+            if not did_expand:
                 did_expand = True
                 self._accordion.expandPage(page.pageTitle)
 
@@ -361,7 +440,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         device = selector._device
 
-        if hasattr(device.format, "label") and labelEntry.get_text():
+        if labelEntry.get_text() and hasattr(device.format, "label"):
             device.format.label = labelEntry.get_text()
 
     def _populate_right_side(self, selector):
@@ -379,14 +458,16 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         selectedDeviceDescLabel.set_text(self._description(selector.props.name))
 
         labelEntry.set_text(getattr(device.format, "label", "") or "")
-        labelEntry.set_sensitive(getattr(device.format, "labelfsProg", "") != "")
+        can_label = getattr(device.format, "labelfsProg", "") != ""
+        labelEntry.set_sensitive(can_label)
 
         if labelEntry.get_sensitive():
             labelEntry.props.has_tooltip = False
         else:
             labelEntry.set_tooltip_text(_("This file system does not support labels."))
 
-        sizeSpinner.set_range(device.minSize, device.maxSize)
+        sizeSpinner.set_range(device.minSize,
+                              device.maxSize)
         sizeSpinner.set_value(device.size)
         sizeSpinner.set_sensitive(device.resizable)
 
@@ -400,15 +481,17 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         # FIXME:  What do we do if we can't figure it out?
         if device.type == "lvmlv":
             typeCombo.set_active(1)
-        elif device.type in ["dm-raid array", "mdarray"]:
+        elif device.type == "mdarray":
             typeCombo.set_active(2)
         elif device.type == "partition":
             typeCombo.set_active(3)
+        elif device.type.startswith("btrfs"):
+            typeCombo.set_active(0)
 
         # FIXME:  What do we do if we can't figure it out?
         model = fsCombo.get_model()
         for i in range(0, len(model)):
-            if model[i][0] == device.format.name:
+            if model[i][0] == device.format.type:
                 fsCombo.set_active(i)
                 break
 
@@ -439,47 +522,56 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         # create a device of the default type, using any disks, with an
         # appropriate fstype and mountpoint
         mountpoint = dialog.mountpoint
-        size = dialog.size
-        fstype = self.storage.defaultFSType
+        log.debug("requested size = %s  ; available space = %s"
+                    % (dialog.size, self._free_space))
+        size = min(dialog.size, self._free_space)
+        fstype = self.storage.getFSType(mountpoint)
+        encrypted = self.data.autopart.encrypted
+
+        # we're doing nothing here to ensure that bootable requests end up on
+        # the boot disk, but the weight from platform should take care of this
+
         if mountpoint.lower() == "swap":
-            fstype = "swap"
             mountpoint = None
-        elif mountpoint == "/boot":
-            fstype = self.storage.defaultBootFSType
-        elif mountpoint == "/boot/efi":
-            if iutil.isMactel():
-                fstype = "hfs+"
-            else:
-                fstype = "efi"
-        elif not mountpoint or not mountpoint.startswith("/") or \
-             mountpoint in self.storage.mountpoints.keys():
-            return
 
-        if not size:
-            # no size specified, so use the default size
-            size = None
-
-        if self.data.autopart.type == AUTOPART_TYPE_PLAIN:
-            # create a partition for this new filesystem
-            size_mb = float(size.convertTo(spec="mb"))
-            device = self.storage.newPartition(size=size_mb,
-                                               fmt_type=fstype,
-                                               mountpoint=mountpoint)
-            self.storage.createDevice(device)
-            try:
-                doPartitioning(self.storage)
-            except StorageError as e:
-                actions = self.storage.devicetree.findActions(device=device)
-                for a in reversed(actions):
-                    self.storage.devicetree.cancelAction(a)
-            else:
-                self._do_refresh()
+        # TODO: validate the mountpoint for sanity and uniqueness
+
+        device_type = self.data.autopart.type
+        if device_type == AUTOPART_TYPE_LVM and \
+             mountpoint == "/boot/efi":
+            device_type = AUTOPART_TYPE_PLAIN
+        elif device_type == AUTOPART_TYPE_BTRFS and \
+             fstype == "swap" or \
+             mountpoint and mountpoint.startswith("/boot"):
+            device_type = AUTOPART_TYPE_PLAIN
+
+        disks = self._clearpartDevices
+
+        with ui_storage_logger():
+            self.__storage.newUIDevice(device_type,
+                                       size=float(size.convertTo(spec="mb")),
+                                       fstype=fstype,
+                                       mountpoint=mountpoint,
+                                       encrypted=encrypted,
+                                       disks=disks)
+        self._devices = self.__storage.devices
+        self._do_refresh()
+        self._updateSpaceDisplay()
 
     def _destroy_device(self, device):
+        with ui_storage_logger():
+            self.__storage.destroyDevice(device)
+
+        self._devices = self.__storage.devices
+
+        # should this be in DeviceTree._removeDevice?
+        if hasattr(device, "vg"):
+            device.vg._removeLogVol(device)
+        elif hasattr(device, "volume"):
+            device.volume._removeSubVolume(device.name)
+
         # if this device has parents with no other children, remove them too
-        parents = device.parents[:]
-        self.storage.destroyDevice(device)
-        for parent in parents:
+        for parent in device.parents:
             if parent.kids == 0 and not parent.isDisk:
                 self._destroy_device(parent)
 
@@ -488,7 +580,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             pass    # unused device
         elif device in root.swaps:
             root.swaps.remove(device)
-        elif hasattr(device.format, "mountpoint") and \
+        elif getattr(device.format, "mountpoint", None) and \
              device in root.mounts.values():
             mountpoints = [m for (m,d) in root.mounts.items() if d == device]
             for mountpoint in mountpoints:
@@ -512,6 +604,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         self._updateSpaceDisplay()
 
     def on_remove_clicked(self, button):
+        # FIXME: removing a btrfs subvol doesn't cause its size to be reduced
         if self._current_selector:
             device = self._current_selector._device
             if device.exists:
@@ -520,7 +613,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
                 # schedule actions to delete the thing.
                 dialog = ConfirmDeleteDialog(self.data)
                 with enlightbox(self.window, dialog.window):
-                    dialog.refresh(getattr(device.format, "mountpoint", None), device.name)
+                    dialog.refresh(getattr(device.format, "mountpoint", ""),
+                                   device.name)
                     rc = dialog.run()
 
                     if rc == 0:
@@ -530,12 +624,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
             self._remove_from_root(root, device)
             self._destroy_device(device)
-
-            # If the root is now empty, remove it. Devices from the Unused page
-            # will have no root.
-            if root and root in self.storage.roots and len(root.swaps + root.mounts.values()) == 0:
-                self.storage.roots.remove(root)
-
             self._update_ui_for_removals()
         elif self._accordion.currentPage():
             # This is a complete installed system.  Thus, we first need to confirm
@@ -562,19 +650,14 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             for device in root.swaps + root.mounts.values():
                 self._destroy_device(device)
 
-            # Remove the root entirely.  This will cause the empty Page to be
-            # deleted from the left hand side.
-            if root in self.storage.roots:
-                self.storage.roots.remove(root)
-
             self._update_ui_for_removals()
 
     def on_summary_clicked(self, button):
-        free = self.storage.getFreeSpace()
+        free = self._free_space
         dialog = SelectedDisksDialog(self.data)
 
         with enlightbox(self.window, dialog.window):
-            dialog.refresh(self._clearpartDevices(), free, showRemove=False)
+            dialog.refresh(self._clearpartDevices, free, showRemove=False)
             dialog.run()
 
     def on_configure_clicked(self, button):
@@ -608,20 +691,30 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         self._removeButton.set_sensitive(True)
 
-    def on_create_clicked(self, button):
-        from pyanaconda.storage import Root
-        from pyanaconda.storage.devices import DiskDevice
-        from pyanaconda.storage.partitioning import doAutoPartition
+    def _do_autopart(self):
+        # There are never any non-existent devices around when this runs.
+        disks = self._clearpartDevices
+        partitions = [d for d in self._devices if d.type == "partition"]
+        vgs = [d for d in self._devices if d.type == "lvmvg"]
 
+        log.debug("running automatic partitioning")
+        with ui_storage_logger():
+            doAutoPartition(self.__storage, self.data)
+        self._devices = self.__storage.devices
+        log.debug("finished automatic partitioning")
+
+    def on_create_clicked(self, button):
         # Then do autopartitioning.  We do not do any clearpart first.  This is
         # custom partitioning, so you have to make your own room.
-        # FIXME:  Handle all the autopart exns here.
-        self.data.autopart.autopart = True
-        self.data.autopart.execute(self.storage, self.data, self.instclass)
-        self.data.autopart.autopart = False
+        self._do_autopart()
 
         # Refresh the spoke to make the new partitions appear.
-        self.refresh()
+        log.debug("refreshing ui")
+        self._do_refresh()
+        log.debug("finished refreshing ui")
+        log.debug("updating space display")
+        self._updateSpaceDisplay()
+        log.debug("finished updating space display")
         self._accordion.expandPage(new_install_name)
 
         # And then display the first filesystem on the RHS.
@@ -641,3 +734,5 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             self._optionsNotebook.set_current_page(2)
         elif text == _("Standard Partition"):
             self._optionsNotebook.hide()
+
+
diff --git a/pyanaconda/ui/gui/spokes/lib/accordion.py b/pyanaconda/ui/gui/spokes/lib/accordion.py
index c2da71e..09b87e9 100644
--- a/pyanaconda/ui/gui/spokes/lib/accordion.py
+++ b/pyanaconda/ui/gui/spokes/lib/accordion.py
@@ -23,7 +23,6 @@ import gettext
 _ = lambda x: gettext.ldgettext("anaconda", x)
 
 from pyanaconda.product import productName, productVersion
-from pyanaconda.storage.size import Size
 
 from gi.repository.AnacondaWidgets import MountpointSelector
 from gi.repository import Gtk
@@ -137,7 +136,7 @@ class Page(Gtk.Box):
 
     def addDevice(self, name, size, mountpoint, cb):
         selector = MountpointSelector()
-        selector = MountpointSelector(name, str(Size(spec="%s MB" % size)), mountpoint or "")
+        selector = MountpointSelector(name, str(size).upper(), mountpoint or "")
         selector.connect("button-press-event", self._onSelectorClicked, cb)
         selector.connect("key-release-event", self._onSelectorClicked, cb)
         selector.connect("focus-in-event", self._onSelectorClicked, cb)
@@ -182,7 +181,7 @@ class UnknownPage(Page):
 
     def addDevice(self, name, size, mountpoint, cb):
         selector = MountpointSelector()
-        selector = MountpointSelector(name, str(Size(spec="%s MB" % size)), mountpoint or "")
+        selector = MountpointSelector(name, str(size).upper(), mountpoint or "")
         selector.connect("button-press-event", self._onSelectorClicked, cb)
         selector.connect("key-release-event", self._onSelectorClicked, cb)
         selector.connect("focus-in-event", self._onSelectorClicked, cb)
-- 
1.7.7.6

