From cef1d2907260cdd57fcb105a69f7551bb5538cd7 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Thu, 26 Jul 2012 21:13:32 -0500
Subject: [PATCH 6/6] Add an IR for the devices in the custom UI.

---
 pyanaconda/ui/gui/spokes/custom.py |  369 +++++++++++++++++++++++++++++-------
 1 files changed, 303 insertions(+), 66 deletions(-)

diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py
index 2793957..2bddb9d 100644
--- a/pyanaconda/ui/gui/spokes/custom.py
+++ b/pyanaconda/ui/gui/spokes/custom.py
@@ -125,8 +125,45 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         self._current_selector = None
         self._when_create_text = ""
+        self.ui_devices = []
+        self._deleted_devices = []
+        self._free_space = Size(bytes=0)
 
     def apply(self):
+        # schedule actions for device removals, resizes
+        for removed in self._deleted_devices:
+            if removed.device_id is None:
+                # created here, destroyed here -> NOOP
+                continue
+
+            device = self.storage.devicetree.getDeviceByName(removed.name)
+            self.storage.destroyDevice(device)
+
+        for ui_device in self.ui_devices:
+            if ui_device.device_id is None:
+                # we're creating a device
+
+                # this isn't a leaf device
+                if ui_device.create_type is None:
+                    continue
+
+                self.storage.newUIDevice(ui_device.create_type,
+                                         ui_device.size,
+                                         encrypted=ui_device.encrypted,
+                                         level=ui_device.raid_level,
+                                         disks=self.storage.disks)
+
+            # this is a device that's already in the tree
+            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
+
+        # XXX How can we handle edits?
+        #
+        #       Doesn't matter for now since we don't have support for editing
+        #       devices.
+
         self.storage.setUpBootLoader()
         StorageChecker.run(self)
 
@@ -147,7 +184,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)
@@ -205,7 +241,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         from pyanaconda.storage.devices import 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)
 
@@ -214,7 +250,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             for chunk in tup:
                 totalFree += chunk
 
-        return totalFree
+        self._free_space = totalFree
 
     def _currentTotalSpace(self):
         """Add up the sizes of all selected disks and return it as a Size."""
@@ -231,7 +267,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         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 +281,12 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
     def refresh(self):
         NormalSpoke.refresh(self)
+        self.ui_devices = get_ui_devices(self.storage)
         self._do_refresh()
+
+        # update our free space number based on Storage
+        self._setCurrentFreeSpace()
+
         self._updateSpaceDisplay()
 
     def _do_refresh(self):
@@ -258,12 +299,43 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         # Now it's time to populate the accordion.
 
+        # set up a dict with name keys for the ui device list
+        ui_device_dict = {}
+        for device in self.ui_devices:
+            ui_device_dict[device.name] = device
+
         # We can only have one page expanded at a time.
         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]
+        unused = [ui_device_dict[d.name] for d in self._unusedDevices()
+                    if d.name in ui_device_dict and
+                       ui_device_dict[d.name].device_id is not None]
+        new_devices = [d for d in self.ui_devices if d.device_id is None]
+
         roots = self.storage.roots[:]
+        ui_roots = []
+        for root in roots:
+            name = root.name
+            swaps = []
+            mounts = {}
+            for (mountpoint, device) in root.mounts.items():
+                ui_device = ui_device_dict.get(device.name)
+                if not ui_device or ui_device.id is None:
+                    continue
+
+                mounts[mountpoint] = ui_device
+
+            for device in root.swaps:
+                ui_device = ui_device_dict.get(device.name)
+                if not ui_device or ui_device.id is None:
+                    continue
+
+                swaps.append(ui_device)
+
+            if not swaps and not mounts:
+                continue
+
+            ui_roots.append(UIRoot(name=name, mounts=mounts, swaps=swaps))
 
         # If we've not yet run autopart, add an instance of CreateNewPage.  This
         # ensures it's only added once.
@@ -278,32 +350,23 @@ 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)])
-            new_root = Root(mounts=mounts, swaps=swaps, name=new_install_name)
-            roots.insert(0, new_root)
+            swaps = [d for d in new_devices if d.fstype == "swap"]
+            mounts = dict([(d.mountpoint, d) for d in new_devices if d.mountpoint])
+            new_root = UIRoot(mounts=mounts, swaps=swaps, name=new_install_name)
+            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._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.fstype, device.size, mountpoint, self.on_selector_clicked)
                 selector._device = device
                 selector._root = root
 
@@ -320,14 +383,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.fstype, 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 and self._current_selector and self._current_selector._root is None:
                 did_expand = True
                 self._accordion.expandPage(page.pageTitle)
 
@@ -361,8 +424,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
 
         device = selector._device
 
-        if hasattr(device.format, "label") and labelEntry.get_text():
-            device.format.label = labelEntry.get_text()
+        if labelEntry.get_text():
+            device.label = labelEntry.get_text()
 
     def _populate_right_side(self, selector):
         encryptCheckbox = self.builder.get_object("encryptCheckbox")
@@ -378,15 +441,16 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         selectedDeviceLabel.set_text(selector.props.name)
         selectedDeviceDescLabel.set_text(self._description(selector.props.name))
 
-        labelEntry.set_text(getattr(device.format, "label", "") or "")
-        labelEntry.set_sensitive(getattr(device.format, "labelfsProg", "") != "")
+        labelEntry.set_text(device.label or "")
+        can_label = getattr(getFormat(device.fstype), "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.min_size, device.max_size)
         sizeSpinner.set_value(device.size)
         sizeSpinner.set_sensitive(device.resizable)
 
@@ -398,17 +462,19 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         encryptCheckbox.set_active(device.encrypted)
 
         # FIXME:  What do we do if we can't figure it out?
-        if device.type == "lvmlv":
+        if device.type == "LVM Logical Volume":
             typeCombo.set_active(1)
-        elif device.type in ["dm-raid array", "mdarray"]:
+        elif device.type == "Software RAID":
             typeCombo.set_active(2)
-        elif device.type == "partition":
+        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.fstype:
                 fsCombo.set_active(i)
                 break
 
@@ -455,32 +521,32 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
              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()
+        if self.data.autopart.type == AUTOPART_TYPE_LVM and \
+             mountpoint != "/boot/efi":
+            device_class = UILogicalVolume
+        elif self.data.autopart.type == AUTOPART_TYPE_BTRFS and \
+             fstype != "swap" and \
+             not mountpoint.startswith("/boot"):
+            device_class = UIBTRFSSubVolume
+        else:
+            device_class = UIPartition
+
+        device = device_class(mountpoint=mountpoint, size=size, fstype=fstype)
+        self.ui_devices.append(device)
+        self._free_space -= Size(spec="%f MB" % device.disk_space)
+        self._do_refresh()
 
     def _destroy_device(self, device):
+        self.ui_devices.remove(device)
+        self._deleted_devices.append(device)
+
+        if device.type == "Partition":
+            self._free_space += Size(spec="%f MB" % device.size)
+
         # if this device has parents with no other children, remove them too
-        parents = device.parents[:]
-        self.storage.destroyDevice(device)
-        for parent in parents:
-            if parent.kids == 0 and not parent.isDisk:
+        for parent in device.parents:
+            all_parents = [p for d in self.ui_devices for p in d.parents]
+            if parent not in all_parents and parent.type != "Disk":
                 self._destroy_device(parent)
 
     def _remove_from_root(self, root, device):
@@ -530,12 +596,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,11 +622,6 @@ 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):
@@ -616,6 +671,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         # 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.
+        # UIDeviceFIXME: we can't run autopart. we have to pretend.
         self.data.autopart.autopart = True
         self.data.autopart.execute(self.storage, self.data, self.instclass)
         self.data.autopart.autopart = False
@@ -641,3 +697,184 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             self._optionsNotebook.set_current_page(2)
         elif text == _("Standard Partition"):
             self._optionsNotebook.hide()
+
+class UIDevice(object):
+    type = "Generic Device"
+    raid_levels = []
+    create_type = None
+
+    def __init__(self, size=None, mountpoint=None, fstype=None, name=None,
+                 raid_level=None, parents=None, exists=False, encrypted=False,
+                 resizable=False, min_size=None, max_size=None,
+                 device_id=None):
+        self.size = size
+        self.mountpoint = mountpoint
+        self.fstype = fstype
+        self.name = name
+        self.raid_level = raid_level
+        self.parents = parents or []
+        self.exists = exists
+        self.encrypted = encrypted
+
+        self.resizable = resizable
+        self.min_size = min_size
+        self.max_size = max_size
+
+        self.device_id = device_id
+
+    @property
+    def disk_space(self):
+        space = self.size
+        if self.raid_level in ("raid1", "raid10"):
+            space = self.size * len(self.parents)
+        elif self.raid_level == "raid5":
+            space = self.size * (float(len(parents)) / (len(parents) - 1))
+        elif self.raid_level == "raid6":
+            space = self.size * (float(len(parents)) / (len(parents) - 2))
+
+        return space
+
+class UIDisk(UIDevice):
+    type = "Disk"
+
+class UIPartition(UIDevice):
+    type = "Partition"
+    create_type = AUTOPART_TYPE_PLAIN
+
+class UIVolumeGroup(UIDevice):
+    type = "LVM Volume Group"
+
+class UILogicalVolume(UIDevice):
+    type = "LVM Logical Volume"
+    raid_levels = ["linear", "raid0", "raid1", "raid10"]
+    create_type = AUTOPART_TYPE_LVM
+
+class UIRAIDArray(UIDevice):
+    type = "Software RAID"
+    raid_levels = ["raid0", "raid1", "raid5", "raid10"]
+
+class UIBTRFSVolume(UIDevice):
+    type = "BTRFS"
+    raid_levels = ["linear", "raid0", "raid1", "raid10"]
+    create_type = AUTOPART_TYPE_BTRFS
+
+class UIBTRFSSubVolume(UIDevice):
+    type = "BTRFS Sub-Volume"
+    create_type = AUTOPART_TYPE_BTRFS
+
+"""
+    UI for device editing
+
+        - convert current StorageDevice tree to UIDevice tree
+        - modify UIDevice tree in UI
+        - generate a list of actions to represent the changes
+"""
+def get_ui_device_class(device):
+    ui_class = None
+    if device.isDisk:
+        ui_class = UIDisk
+    elif device.type == "partition":
+        ui_class = UIPartition
+    elif device.type == "mdarray":
+        ui_class = UIRAIDArray
+    elif device.type == "lvmvg":
+        ui_class = UIVolumeGroup
+    elif device.type == "lvmlv":
+        ui_class = UILogicalVolume
+    elif device.type == "luks/dm-crypt":
+        ui_class = get_ui_device_type(device.parents[0])
+
+    print "get_ui_device_class returning %s for device %s" % (ui_class, device)
+    return ui_class
+
+def get_ui_device_raid_level(device):
+    level = None
+    if device.type == "mdarray":
+        if device.level == mdraid.RAID0:
+            level = "raid0"
+        elif device.level == mdraid.RAID1:
+            level = "raid1"
+        elif device.level == mdraid.RAID5:
+            level = "raid5"
+        elif device.level == mdraid.RAID6:
+            level = "raid6"
+        elif device.level == mdraid.RAID10:
+            level = "raid10"
+    elif device.type == "lvmlv":
+        # TODO: implement striping and mirroring in LVMLogicalVolumeDevice
+        pass
+    elif device.type == "btrfs volume":
+        level = device.dataLevel
+
+    return level
+
+def get_ui_parents(storage_device, ui_devices):
+    ui_parents = []
+    for parent in storage_device.parents:
+        for u in ui_devices:
+            if u.name == parent.name:
+                ui_parents.append(u)
+
+    return ui_parents
+
+def get_ui_devices(storage):
+    devices = storage.devicetree.devices[:]
+    ui_devices = []
+    while devices:
+        roots = [d for d in devices if
+                        not [p for p in d.parents if p in devices]]
+        for device in roots:
+            ui_class = get_ui_device_class(device)
+            devices.remove(device)
+            if ui_class is None:
+                continue
+
+            min_size = device.size
+            max_size = device.size
+            resizable = False
+            if device.format.type == "luks" and \
+               storage.devicetree.getChildren(device):
+                continue
+            elif device.type == "luks/dm-crypt":
+                encrypted = True
+                raid_level = get_ui_device_raid_level(device.parents[0])
+                exists = device.parents[0].exists
+                devices.remove(device.parents[0])
+                parents = get_ui_parents(device.parents[0], ui_devices)
+            else:
+                encrypted = False
+                raid_level = get_ui_device_raid_level(device)
+                exists = device.exists
+                parents = get_ui_parents(device, ui_devices)
+                if device.resizable:
+                    resizable = True
+                    min_size = device.minSize
+                    max_size = device.maxSize
+
+            ui_device = ui_class(name=device.name,
+                                 size=device.size,
+                                 fstype=device.format.type,
+                                 mountpoint=getattr(device.format,
+                                                    "mountpoint", None),
+                                 raid_level=raid_level,
+                                 encrypted=encrypted,
+                                 parents=parents,
+                                 exists=exists,
+                                 resizable=resizable,
+                                 min_size=min_size,
+                                 max_size=max_size,
+                                 device_id=device.id)
+            ui_devices.append(ui_device)
+
+    return ui_devices
+
+class UIRoot(object):
+    def __init__(self, name=None, mounts=None, swaps=None):
+        self.name = name
+        self.mounts = mounts or {}
+        self.swaps = swaps or []
+
+    @property
+    def devices(self):
+        return self.mounts.values() + self.swaps[:]
+
-- 
1.7.7.6

