From c5f2874e9c1f36f9a4127014e2ef921155f34d51 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Thu, 1 Nov 2012 11:25:59 -0500
Subject: [PATCH 12/12] Enable specification of disk(s) for individual
 mountpoints. (#870569)

For a partition, the selected disks indicate acceptable options for
where the partition ends up. For btrfs, raid, and lvm it indicates
the set of disks on which member/pv devices will be created.
---
 pyanaconda/storage/__init__.py        |   67 +++++++++---
 pyanaconda/ui/gui/spokes/custom.glade |  201 +++++++++++++++++++++++++++++++++
 pyanaconda/ui/gui/spokes/custom.py    |  121 ++++++++++++++++++--
 3 files changed, 361 insertions(+), 28 deletions(-)

diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py
index 8691bef..d3b334c 100644
--- a/pyanaconda/storage/__init__.py
+++ b/pyanaconda/storage/__init__.py
@@ -1782,8 +1782,9 @@ class Storage(object):
             return []
 
         # set up member devices
-        container_size = 0
+        container_size = factory.device_size
         add_disks = []
+        remove_disks = []
 
         if members is None:
             members = []
@@ -1791,23 +1792,19 @@ class Storage(object):
         if container:
             members = container.parents[:]
 
-        if container:
-            log.debug("using container %s with %d devices" % (container.name,
-                                len(self.devicetree.getChildren(container))))
-            container_size = factory.container_size_func(container, device)
-            log.debug("raw container size reported as %d" % container_size)
-
-        if members:
+        # XXX how can we detect/handle failure to use one or more of the disks?
+        if container and device:
+            # See if we need to add/remove any disks, but only if we are
+            # adjusting a device. When adding a new device to a container we do
+            # not want to modify the container's disk set.
             _disks = list(set([d for m in members for d in m.disks]))
 
-            # see if factory.disks contains disks not already in use
             add_disks = [d for d in factory.disks if d not in _disks]
-        else:
+            remove_disks = [d for d in _disks if d not in factory.disks]
+        elif not container:
+            # new container, so use the factory's disk set
             add_disks = factory.disks
 
-        device_space = factory.device_size
-        log.debug("device requires %d" % device_space)
-        container_size += device_space
         base_size = max(1, getFormat(factory.member_format).minSize)
 
         # XXX TODO: multiple member devices per disk
@@ -1815,8 +1812,21 @@ class Storage(object):
         # prepare already-defined member partitions for reallocation
         for member in members[:]:
             if isinstance(member, LUKSDevice):
+                if member.slave.disk in remove_disks:
+                    container.removeMember(member)
+                    self.destroyDevice(member)
+                    members.remove(member)
+
                 member = member.slave
 
+            if member.disk in remove_disks:
+                if member in container.parents:
+                    container.removeMember(member)
+                    members.remove(member)
+
+                self.destroyDevice(member)
+                continue
+
             member.req_base_size = base_size
             member.req_size = member.req_base_size
             member.req_grow = True
@@ -1850,6 +1860,12 @@ class Storage(object):
             if container:
                 container.addMember(member)
 
+        if container:
+            log.debug("using container %s with %d devices" % (container.name,
+                                len(self.devicetree.getChildren(container))))
+            container_size = factory.container_size_func(container, device)
+            log.debug("raw container size reported as %d" % container_size)
+
         log.debug("adding a %s with size %d" % (factory.set_class.__name__,
                                                 container_size))
         size_set = factory.set_class(members, container_size)
@@ -1988,6 +2004,13 @@ class Storage(object):
         # TODO: striping, mirroring, &c
         # TODO: non-partition members (pv-on-md)
 
+        # setContainerMembers can modify these, so save them now
+        old_size = None
+        old_disks = []
+        if device:
+            old_size = device.size
+            old_disks = device.disks[:]
+
         members = []
         if device and device.type == "mdarray":
             members = device.parents
@@ -2045,19 +2068,27 @@ class Storage(object):
         if device:
             # We are adjusting the size of a device. The StorageDevice instance
             # exists, but the underlying device does not.
-            old_size = device.size
             e = None
             try:
                 factory.post_create()
             except StorageError as e:
                 log.error("device post-create method failed: %s" % e)
             else:
-                if not device.size:
-                    e = StorageError("failed to adjust device")
+                if device.size <= device.format.minSize:
+                    e = StorageError("failed to adjust device -- not enough free space in specified disks?")
 
             if e:
                 # Clean up by reverting the device to its previous size.
                 factory.size = old_size
+                factory.disks = old_disks
+                try:
+                    self.setContainerMembers(container, factory,
+                                             members=members, device=device)
+                except StorageError as e:
+                    # yes, we're replacing e here.
+                    log.error("failed to revert device size: %s" % e)
+                    raise ErrorRecoveryFailure("failed to revert device size")
+
                 factory.set_device_size(container, device=device)
                 try:
                     factory.post_create()
@@ -3208,6 +3239,10 @@ class PartitionFactory(DeviceFactory):
             device.req_max_size = size
             device.req_grow = size > base_size
 
+            # this probably belongs somewhere else but this is our chance to
+            # update the disk set
+            device.req_disks = self.disks[:]
+
         return size
 
 class BTRFSFactory(DeviceFactory):
diff --git a/pyanaconda/ui/gui/spokes/custom.glade b/pyanaconda/ui/gui/spokes/custom.glade
index 5134c77..1aaede0 100644
--- a/pyanaconda/ui/gui/spokes/custom.glade
+++ b/pyanaconda/ui/gui/spokes/custom.glade
@@ -1615,6 +1615,207 @@ you'll be able to view their details here.</property>
       </object>
     </child>
   </object>
+  <object class="GtkListStore" id="disk_store">
+    <columns>
+      <!-- column-name disk_col -->
+      <column type="gchararray"/>
+      <!-- column-name capacity_col -->
+      <column type="gchararray"/>
+      <!-- column-name free_col -->
+      <column type="gchararray"/>
+      <!-- column-name serial_col -->
+      <column type="gchararray"/>
+      <!-- column-name id_col -->
+      <column type="gint"/>
+    </columns>
+  </object>
+  <object class="GtkDialog" id="disks_dialog">
+    <property name="width_request">400</property>
+    <property name="height_request">200</property>
+    <property name="can_focus">False</property>
+    <property name="border_width">5</property>
+    <property name="type_hint">dialog</property>
+    <property name="decorated">False</property>
+
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox1">
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+            <property name="layout_style">end</property>
+            <child>
+              <object class="GtkButton" id="cancel_button">
+                <property name="label">_Cancel</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+                <signal name="clicked" handler="on_cancel_clicked" swapped="no"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="save_button">
+                <property name="label">_Save</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_underline">True</property>
+                <signal name="clicked" handler="on_save_clicked" swapped="no"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="box1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkLabel" id="disk_selection_title_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">CONFIGURE MOUNT POINT</property>
+                <attributes>
+                  <attribute name="weight" value="bold"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">Select one or more disks this device may reside on.</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledwindow1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="shadow_type">in</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <child>
+                  <object class="GtkViewport" id="viewport1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkTreeView" id="disk_view">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="model">disk_store</property>
+                        <child internal-child="selection">
+                          <object class="GtkTreeSelection" id="disk_view_selection">
+                            <property name="mode">multiple</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="disk_column">
+                            <property name="spacing">6</property>
+                            <property name="title" translatable="yes">Disk</property>
+                            <property name="clickable">True</property>
+                            <child>
+                              <object class="GtkCellRendererText" id="disk_renderer"/>
+                              <attributes>
+                                <attribute name="text">0</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="capacity_column">
+                            <property name="spacing">6</property>
+                            <property name="title" translatable="yes">Capacity</property>
+                            <property name="clickable">True</property>
+                            <child>
+                              <object class="GtkCellRendererText" id="capacity_renderer"/>
+                              <attributes>
+                                <attribute name="text">1</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="free_column">
+                            <property name="spacing">6</property>
+                            <property name="title" translatable="yes">Free</property>
+                            <property name="clickable">True</property>
+                            <child>
+                              <object class="GtkCellRendererText" id="free_renderer"/>
+                              <attributes>
+                                <attribute name="text">2</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="id_column">
+                            <property name="spacing">6</property>
+                            <property name="title" translatable="yes">ID</property>
+                            <property name="clickable">True</property>
+                            <child>
+                              <object class="GtkCellRendererText" id="id_renderer"/>
+                              <attributes>
+                                <attribute name="text">3</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="0">cancel_button</action-widget>
+      <action-widget response="1">save_button</action-widget>
+    </action-widgets>
+  </object>
   <object class="GtkImage" id="removeImage">
     <property name="visible">True</property>
     <property name="can_focus">False</property>
diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py
index dfa7b52..06cc894 100644
--- a/pyanaconda/ui/gui/spokes/custom.py
+++ b/pyanaconda/ui/gui/spokes/custom.py
@@ -279,6 +279,59 @@ class ConfirmDeleteDialog(GUIObject):
     def run(self):
         return self.window.run()
 
+class DisksDialog(GUIObject):
+    builderObjects = ["disks_dialog", "disk_store", "disk_view"]
+    mainWidgetName = "disks_dialog"
+    uiFile = "spokes/custom.glade"
+
+    def __init__(self, *args, **kwargs):
+        self._disks = kwargs.pop("disks")
+        free = kwargs.pop("free")
+        self.selected = kwargs.pop("selected")[:]
+        GUIObject.__init__(self, *args, **kwargs)
+        self._store = self.builder.get_object("disk_store")
+        # populate the store
+        for disk in self._disks:
+            self._store.append([disk.description,
+                                str(Size(spec="%dMB" % disk.size)),
+                                str(free[disk.name][0]),
+                                disk.serial,
+                                disk.id])
+
+        treeview = self.builder.get_object("disk_view")
+        model = treeview.get_model()
+        itr = model.get_iter_first()
+        selected_ids = [d.id for d in self.selected]
+        selection = treeview.get_selection()
+        while itr:
+            disk_id = model.get_value(itr, 4)
+            if disk_id in selected_ids:
+                selection.select_iter(itr)
+
+            itr = model.iter_next(itr)
+
+    def on_cancel_clicked(self, button):
+        self.window.destroy()
+
+    def _get_disk_by_id(self, disk_id):
+        for disk in self._disks:
+            if disk.id == disk_id:
+                return disk
+
+    def on_save_clicked(self, button):
+        treeview = self.builder.get_object("disk_view")
+        model, paths = treeview.get_selection().get_selected_rows()
+        self.selected = []
+        for path in paths:
+            itr = model.get_iter(path)
+            disk_id = model.get_value(itr, 4)
+            self.selected.append(self._get_disk_by_id(disk_id))
+
+        self.window.destroy()
+
+    def run(self):
+        return self.window.run()
+
 class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
     builderObjects = ["customStorageWindow", "sizeAdjustment",
                       "partitionStore",
@@ -298,6 +351,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         self._fs_types = []             # list of supported fstypes
         self._unused_devices = None     # None indicates uninitialized
         self._free_space = Size(bytes=0)
+        self._device_disks = []
 
         self._initialized = False
 
@@ -1004,15 +1058,22 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         changed_raid_level = (current_device_type == device_type and
                               device_type in (None, AUTOPART_TYPE_BTRFS) and
                               old_raid_level != raid_level)
+        changed_disk_set = (set(device.disks) != set(self._device_disks))
 
         if changed_device_type or changed_raid_level:
             if changed_device_type:
                 log.info("changing device type from %s to %s"
                             % (current_device_type, device_type))
-            else:
+
+            if changed_raid_level:
                 log.info("changing raid level from %s to %s"
                             % (old_raid_level, raid_level))
 
+            if changed_disk_set:
+                log.info("changing disk set from %s to %s"
+                            % ([d.name for d in device.disks],
+                               [d.name for d in self._device_disks]))
+
             # remove the current device
             self.clear_errors()
             root = self._current_selector._root
@@ -1022,13 +1083,11 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
                 return
 
             with ui_storage_logger():
-                # Use any disks with space in addition to any disks used by
-                # a defined container.
-                disks = [d for d in self._clearpartDevices
-                            if getattr(d.format, "free", 0) > 500]
+                disks = self._device_disks[:]
                 container = self.__storage.getContainer(factory)
-                if container:
-                    disks = list(set(disks).union(container.disks))
+                if container and changed_device_type:
+                    log.debug("overriding disk set with container's")
+                    disks = container.disks[:]
                 log.debug("disks: %s" % [d.name for d in disks])
                 try:
                     self._replace_device(device_type, size, fstype=fs_type,
@@ -1101,7 +1160,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         ## SIZE
         ##
         # new size means resize for existing devices and adjust for new ones
-        if int(size) != int(device.size):
+        if int(size) != int(device.size) or changed_disk_set:
             self.clear_errors()
             old_size = device.size
             if device.exists and device.resizable:
@@ -1121,11 +1180,16 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
                         log.debug("new size: %s" % device.size)
                         log.debug("target size: %s" % device.targetSize)
             else:
+                if changed_disk_set:
+                    log.info("changing disk set from %s to %s"
+                                % ([d.name for d in device.disks],
+                                   [d.name for d in self._device_disks]))
+
                 with ui_storage_logger():
                     try:
                         self.__storage.newDevice(device_type, size,
                                                  device=device,
-                                                 disks=device.disks,
+                                                 disks=self._device_disks[:],
                                                  raid_level=raid_level)
                     except ErrorRecoveryFailure as e:
                         self._error = e
@@ -1320,7 +1384,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
     def _get_raid_disabled_features(self, raid_level):
         """ Return a list of disabled features based on raid level. """
         disabled = raid_disabled_features.get(raid_level, [])
-        disk_count = len(self._clearpartDevices)
+        disk_count = len(self._device_disks)
         # go through each feature's reference raid level, filtering any that
         # require more disks than we have available
         for feature in raid_features:
@@ -1365,7 +1429,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         # this device with various raid features enabled.
         with ui_storage_logger():
             factory = self.__storage.getDeviceFactory(factory_type, size,
-                                                      disks=self._clearpartDevices, 
+                                                      disks=self._device_disks,
                                                       raid_level=base_level)
 
         widget_dict = self._get_raid_widget_dict(device_type)
@@ -1437,6 +1501,9 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         else:
             use_dev = device
 
+        self._device_disks = getattr(use_dev, "req_disks", use_dev.disks)
+        log.debug("udpated device_disks to %s" % [d.name for d in self._device_disks])
+
         selectedDeviceLabel.set_text(selector.props.name)
         selectedDeviceDescLabel.set_text(self._description(selector.props.name))
 
@@ -1816,7 +1883,37 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             dialog.run()
 
     def on_configure_clicked(self, button):
-        pass
+        selector = self._current_selector
+        if not selector:
+            return
+
+        device = selector._device
+        if device.exists:
+            return
+
+        self.clear_errors()
+
+        dialog = DisksDialog(self.data,
+                             disks=self._clearpartDevices,
+                             free=self._currentFreeInfo,
+                             selected=self._device_disks)
+        with enlightbox(self.window, dialog.window):
+            rc = dialog.run()
+
+        if rc == 0:
+            return
+
+        disks = dialog.selected
+        log.debug("new disks for %s: %s" % (device.name,
+                                            [d.name for d in disks]))
+        if not disks:
+            self._error = "No disks selected. Keeping previous disk set."
+            self.window.set_info(Gtk.MessageType.INFO,
+                                 self._error)
+            self.window.show_all()
+            return
+
+        self._device_disks = disks
 
     def on_selector_clicked(self, selector):
         if not self._initialized:
-- 
1.7.7.6

