From c1005629027fc74efd9df95042cd354ff480a9c5 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Mon, 23 Jul 2012 11:43:02 -0500
Subject: [PATCH 16/16] Add support for creating device based on a top-down
 specification.

General Approach:

Instead of an all-or-nothing approach, the goal is to come as close
as possible to satisfying the new device specification. Once the
container has been allocated, we simply adjust the new device's
size as needed to fit in the container.

This uses growable partition requests, but it then fixes their size
as soon as they have been allocated. It also changes doPartitioning
to reallocate container member devices each time instead of fixing
them on disk once the container is defined. Since the members are
not growable anymore once the container is defined, this is less
disruptive than it would otherwise be. It allows for maximum
flexibility to allocate the set of requests while still preserving
already-defined containers' sizes.

Notes:

Creation of md devices is completely untested.

Creation any device (md, lvm, btrfs) with striping, mirroring, or
any other RAID-like features is completely untested.

There is no support for container members of any type other than
normal partitions.
---
 pyanaconda/storage/__init__.py     |  230 +++++++++++++++++++++++++++++++++++-
 pyanaconda/storage/partitioning.py |    3 +-
 pyanaconda/ui/gui/spokes/custom.py |  112 ++----------------
 3 files changed, 239 insertions(+), 106 deletions(-)

diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py
index 48fba60..de878a0 100644
--- a/pyanaconda/storage/__init__.py
+++ b/pyanaconda/storage/__init__.py
@@ -49,6 +49,11 @@ from devicelibs.dm import name_from_dm_node
 from devicelibs.crypto import generateBackupPassphrase
 from devicelibs.mpath import MultipathConfigWriter
 from devicelibs.edd import get_edd_dict
+from devicelibs.mdraid import get_member_space
+from devicelibs.lvm import get_pv_space
+from .partitioning import SameSizeSet
+from .partitioning import TotalSizeSet
+from .partitioning import doPartitioning
 from udev import *
 import iscsi
 import fcoe
@@ -1391,9 +1396,6 @@ class Storage(object):
         # also include names of any lvs in the parent for the case of the
         # temporary vg in the lvm dialogs, which can contain lvs that are
         # not yet in the devicetree and therefore not in self.names
-        if hasattr(parent, "lvs"):
-            names.extend([full_name(d.lvname, parent) for d in parent.lvs])
-
         if full_name(name, parent) in names or not body:
             for i in range(100):
                 name = "%s%02d" % (template, i)
@@ -1866,6 +1868,228 @@ class Storage(object):
 
         return 0
 
+    def getFSType(self, mountpoint=None):
+        fstype = self.defaultFSType
+        if not mountpoint:
+            # just return the default
+            pass
+        elif mountpoint.lower() == "swap":
+            fstype = "swap"
+        elif mountpoint == "/boot":
+            fstype = self.defaultBootFSType
+        elif mountpoint == "/boot/efi":
+            if iutil.isMactel():
+                fstype = "hfs+"
+            else:
+                fstype = "efi"
+
+        return fstype
+
+    def newUIDevice(self, device_type, size, **kwargs):
+        mountpoint = kwargs.get("mountpoint")
+        fstype = kwargs.get("fstype")
+        device = kwargs.get("device")
+        disks = kwargs.get("disks")
+        encrypted = kwargs.get("encrypted", self.data.autopart.encrypted)
+
+        # md, btrfs
+        raid_level = kwargs.get("level")
+
+        # lvm
+        striped = kwargs.get("striped")
+        mirrored = kwargs.get("mirrored")
+
+        if not fstype:
+            fstype = self.getFSType(mountpoint=mountpoint)
+            if fstype == "swap":
+                mountpoint = None
+
+        if fstype == "swap" and device_type == AUTOPART_TYPE_BTRFS:
+            device_type = AUTOPART_TYPE_PLAIN
+        elif device_type != AUTOPART_TYPE_PLAIN and \
+             mountpoint and mountpoint.startswith("/boot"):
+            device_type = AUTOPART_TYPE_PLAIN
+
+        # TODO: striping, mirroring, &c
+        # TODO: non-partition members (pv-on-md)
+        if device_type == AUTOPART_TYPE_PLAIN:
+            # XXX FIXME: for a size of 500MB this is growing unbounded
+            device = self.newPartition(grow=True, maxsize=size,
+                                       fmt_type=fstype, mountpoint=mountpoint)
+            self.createDevice(device)
+            try:
+                doPartitioning(self)
+            except StorageError as e:
+                log.error("failed to allocate partitions: %s" % e)
+                actions = self.devicetree.findActions(device=device)
+                for a in reversed(actions):
+                    self.devicetree.cancelAction(a)
+            else:
+                # fix the sizes of all allocated partitions
+                for partition in self.partitions:
+                    partition.req_grow = False
+                    partition.req_base_size = partition.size
+                    partition.req_size = partition.size
+
+            return
+        elif device_type == AUTOPART_TYPE_BTRFS:
+            def btrfs_size(size, disks, level):
+                if level in ("single", "raid0"):
+                    return size
+                elif level in ("raid1", "raid10"):
+                    return size * len(disks)
+
+            type_desc = "btrfs"
+            member_format = "btrfs"
+            size_func = btrfs_size
+            size_func_kwargs = {"level": raid_level or "single"}
+            new_container = self.newBTRFS
+            new_device = self.newBTRFSSubVolume
+            container_list = self.btrfsVolumes
+            container_size_func = lambda c: c.size
+            if raid_level in ("raid1", "raid10"):
+                set_class = SameSizeSet
+            else:
+                set_class = TotalSizeSet
+        elif device_type == AUTOPART_TYPE_LVM:
+            type_desc = "lvm"
+            member_format = "lvmpv"
+            size_func = get_pv_space
+            size_func_kwargs = {"striped": striped,
+                                "mirrored": mirrored}
+            new_container = self.newVG
+            new_device = self.newLV
+            container_list = self.vgs
+            container_size_func = lambda c: c.size - c.freeSpace
+            if mirrored:
+                set_class = SameSizeSet
+            else:
+                set_class = TotalSizeSet
+        #elif device_type == AUTOPART_TYPE_MD:
+        else:
+            type_desc = "md"
+            member_format = "mdmember"
+            size_func = get_member_space
+            size_func_kwargs = {"level": raid_level}
+            new_container = None
+            new_device = self.newMD
+            container_list = []
+            container_size_func = lambda c: c.size
+            if raid_level != "linear":
+                set_class = SameSizeSet
+            else:
+                set_class = TotalSizeSet
+
+        container = None
+        containers = [c for c in container_list if not c.exists]
+        if containers:
+            container = containers[0]
+
+        # If we have a container and also a disk list, how do we reconcile the
+        # two to arrive at a member device count?
+        # For now, existing container wins since we will only modify one layer
+        # at a time.
+
+        # set up member devices
+        disks = disks
+        container_size = 0
+        if container:
+            log.debug("using container %s with %d devices" % (container.name,
+                                len(self.devicetree.getChildren(container))))
+            container_size = container_size_func(container)
+            log.debug("raw container size reported as %d" % container_size)
+            disks = list(set([d for m in container.parents for d in m.disks]))
+            disks.sort(key=lambda d: d.name, cmp=self.compareDisks)
+
+        device_space = size_func(size, len(disks), **size_func_kwargs)
+        log.debug("device requires %d" % device_space)
+        container_size += device_space
+
+        # XXX TODO: multiple member devices per disk
+
+        # XXX Never try to reuse member devices. They can be converted by the
+        #     user into free space.
+        if container:
+            members = container.parents[:]
+            for member in members[:]:
+                if isinstance(member, LUKSDevice):
+                    member = member.slave
+
+                member.req_base_size = PartitionDevice.defaultSize
+                member.req_size = member.req_base_size
+                member.req_grow = True
+
+                for ss in self.size_sets[:]:
+                    if member in ss.devices:
+                        self.size_sets.remove(ss)
+        else:
+            # set up members as needed to accommodate the device
+            members = []
+            for disk in disks:
+                if encrypted:
+                    luks_format_type = member_type
+                    member_type = "luks"
+
+                member = self.newPartition(parents=[disk], grow=True,
+                                           fmt_type=member_format)
+                self.createDevice(member)
+                if encrypted:
+                    fmt = getFormat(luks_format_type)
+                    member = LUKSDevice("luks-%s" % member.name,
+                                        parents=[member], format=fmt)
+                    self.createDevice(member)
+
+                members.append(member)
+
+        log.debug("adding a %s with size %d" % (set_class.__name__,
+                                                container_size))
+        size_set = set_class(members, container_size)
+        self.size_sets.append(size_set)
+
+        try:
+            doPartitioning(self)
+        except StorageError as e:
+            log.error("UI: failed to allocate partitions: %s" % e)
+            for device in members:
+                actions = self.devicetree.findActions(device=device)
+                for a in reversed(actions):
+                    self.devicetree.cancelAction(a)
+            # FIXME: revert container to its original state
+            return
+        else:
+            # fix the sizes of all allocated partitions
+            for partition in self.partitions:
+                partition.req_grow = False
+                partition.req_base_size = partition.size
+                partition.req_size = partition.size
+
+            parents = members
+
+        # set up container
+        if not container and new_container:
+            log.debug("creating new container")
+            container = new_container(parents=parents)
+            self.createDevice(container)
+
+        if container:
+            parents = [container]
+            log.debug("%r" % container)
+
+        # add the new device to the container
+        if new_device:
+            free = getattr(container, "freeSpace", size)
+            if free < size:
+                log.info("adjusting device size from %.2f to %.2f so it fits "
+                         "in container" % (size, free))
+                size = free
+
+            log.debug("creating new device")
+            device = new_device(parents=parents,
+                                size=size,
+                                fmt_type=fstype,
+                                mountpoint=mountpoint)
+            self.createDevice(device)
+
 def mountExistingSystem(fsset, rootEnt,
                         allowDirty=None, dirtyCB=None,
                         readOnly=None):
diff --git a/pyanaconda/storage/partitioning.py b/pyanaconda/storage/partitioning.py
index 1d6c38f..765d824 100644
--- a/pyanaconda/storage/partitioning.py
+++ b/pyanaconda/storage/partitioning.py
@@ -762,8 +762,7 @@ def doPartitioning(storage):
     for part in storage.partitions:
         part.req_bootable = False
 
-        if part.exists or \
-           (storage.deviceImmutable(part) and part.partedPartition):
+        if part.exists:
             # if the partition is preexisting or part of a complex device
             # then we shouldn't modify it
             partitions.remove(part)
diff --git a/pyanaconda/ui/gui/spokes/custom.py b/pyanaconda/ui/gui/spokes/custom.py
index f66842f..c0713b4 100644
--- a/pyanaconda/ui/gui/spokes/custom.py
+++ b/pyanaconda/ui/gui/spokes/custom.py
@@ -20,8 +20,6 @@
 #
 
 # TODO:
-# - Add button doesn't do anything.  It may need to ask for what kind of thing is being
-#   added, too.
 # - Deleting an LV is not reflected in available space in the bottom left.
 # - Device descriptions, suggested sizes, etc. should be moved out into a support file.
 # - Newly created devices can not be resized (because self.resizable requires self.exists).
@@ -38,8 +36,6 @@ from pyanaconda.product import productName, productVersion
 from pyanaconda.storage.formats import device_formats
 from pyanaconda.storage.size import Size
 from pyanaconda.storage import Root
-from pyanaconda.storage.partitioning import doPartitioning
-from pyanaconda.storage.errors import StorageError
 
 from pyanaconda.ui.gui import UIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
@@ -443,21 +439,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
         # appropriate fstype and mountpoint
         mountpoint = dialog.mountpoint
         size = dialog.size
-        fstype = self.storage.defaultFSType
-        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
-
         log.info("UI: create new %s %s" % (size, mountpoint))
 
         # nothing in storage uses Size, so convert to float now
@@ -465,92 +446,21 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker):
             size = float(size.convertTo(spec="mb"))
         else:
             # no size specified, so use the default size
+            # FIXME: this only makes sense for partitions, if ever
             size = None
 
-        # swap can't be on btrfs.
-        # /boot* must be on partition or md
-        if (self.data.autopart.type == AUTOPART_TYPE_PLAIN or
-            (self.data.autopart.type == AUTOPART_TYPE_BTRFS and
-             fstype == "swap") or
-            (mountpoint and mountpoint.startswith("/boot"))):
-            log.info("UI: using a partition")
-            # create a partition for this new filesystem
-            device = self.storage.newPartition(size=size,
-                                               fmt_type=fstype,
-                                               mountpoint=mountpoint)
-            self.storage.createDevice(device)
-            try:
-                doPartitioning(self.storage)
-            except StorageError as e:
-                log.error("UI: failed to allocate new partition: %s" % e)
-                actions = self.storage.devicetree.findActions(device=device)
-                for a in reversed(actions):
-                    self.storage.devicetree.cancelAction(a)
-            else:
-                self._do_refresh()
-                self._updateSpaceDisplay()
-        elif self.data.autopart.type == AUTOPART_TYPE_BTRFS:
-            log.info("UI: using a btrfs subvolume")
-            # see if we've already created a btrfs volume
-            container = None
-            for c in self.storage.btrfsVolumes:
-                if not c.exists:
-                    container = c
-                    break
-
-            # XXX for now we are ignoring raid
-
-            # if we have a container, we need to grow it
-            # figure out this device's space needs based on it and the container
-            if container:
-                log.debug("UI: found a btrfs volume with %d subvolumes" % len(container.subvolumes))
-                new_size = container.size + size
-                # this is always rounding down
-                member_size = int(new_size / len(container.parents))
-                log.debug("setting member device size to %d" % member_size)
-                for member in container.parents:
-                    # btrfs members can only be partitions
-                    # FIXME: grab the slave if the member is a DMCryptDevice
-                    member.req_base_size = member_size
-            else:
-                log.debug("UI: going to create a btrfs volume")
-                # if we don't have a container we need to create member devices
-                # and then create the container itself
-                # figure out this devices's space needs
-                usable_disks = set(self.storage.disks) & set(self.storage.partitioned)
-                member_size = size / len(usable_disks)
-                members = []
-                for disk in usable_disks:
-                    member = self.storage.newPartition(parents=[disk],
-                                                       size=member_size,
-                                                       fmt_type="btrfs")
-                    self.storage.createDevice(member)
-                    members.append(member)
+        # This should be encapsulated somewhere as the default type for new
+        # devices.
+        device_type = self.data.autopart.type
+        encrypted = self.data.autopart.encrypted
+        disks = list(set(self.storage.disks) & set(self.storage.partitioned))
+        disks.sort(key=lambda d: d.name, cmp=self.storage.compareDisks)
 
-            try:
-                doPartitioning(self.storage)
-            except StorageError as e:
-                log.error("UI: failed to allocate new members: %s" % e)
-                for device in members:
-                    actions = self.storage.devicetree.findActions(device=device)
-                    for a in reversed(actions):
-                        self.storage.devicetree.cancelAction(a)
-                # FIXME: revert container to its original state
-                return
+        self.storage.newUIDevice(device_type, size, disks=disks,
+                                 mountpoint=mountpoint, encrypted=encrypted)
 
-            if not container:
-                log.debug("UI: creating new btrfs volume")
-                container = self.storage.newBTRFS(parents=members)
-                self.storage.createDevice(container)
-
-            # add the new device to the container
-            log.debug("UI: creating new btrfs subvolume")
-            device = self.storage.newBTRFS(subvol=True,
-                                           parents=[container],
-                                           mountpoint=mountpoint)
-            self.storage.createDevice(device)
-            self._do_refresh()
-            self._updateSpaceDisplay()
+        self._do_refresh()
+        self._updateSpaceDisplay()
 
     def _destroy_device(self, device):
         # if this device has parents with no other children, remove them too
-- 
1.7.7.6

