From 48f4fcd3980e1a26b935e7340beba10afa3c8403 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Thu, 26 Jul 2012 16:41:13 -0500
Subject: [PATCH 04/14] Add size-set managers to keep a set of growable
 requests in sync.

SameSizeSet is for mirrored container devices, and ensures that
all members in the set end up the same size.

TotalSizeSet is for non-mirrored container devices, and tries to
achieve a target combined size for devices in the set.
---
 pyanaconda/storage/__init__.py     |    1 +
 pyanaconda/storage/partitioning.py |  183 +++++++++++++++++++++++++++++++++---
 2 files changed, 170 insertions(+), 14 deletions(-)

diff --git a/pyanaconda/storage/__init__.py b/pyanaconda/storage/__init__.py
index 901c50b..fb08bd1 100644
--- a/pyanaconda/storage/__init__.py
+++ b/pyanaconda/storage/__init__.py
@@ -351,6 +351,7 @@ class Storage(object):
         self.eddDict = {}
 
         self.__luksDevs = {}
+        self.size_sets = []
 
         self.iscsi = iscsi.iscsi()
         self.fcoe = fcoe.fcoe()
diff --git a/pyanaconda/storage/partitioning.py b/pyanaconda/storage/partitioning.py
index e9c06f6..4c25928 100644
--- a/pyanaconda/storage/partitioning.py
+++ b/pyanaconda/storage/partitioning.py
@@ -789,7 +789,7 @@ def doPartitioning(storage):
     free = getFreeRegions(disks)
     try:
         allocatePartitions(storage, disks, partitions, free)
-        growPartitions(disks, partitions, free)
+        growPartitions(disks, partitions, free, size_sets=storage.size_sets)
     finally:
         # The number and thus the name of partitions may have changed now,
         # allocatePartitions() takes care of this for new partitions, but not
@@ -1188,6 +1188,8 @@ class Chunk(object):
             for req in requests:
                 self.addRequest(req)
 
+        self.skip_list = []
+
     def __repr__(self):
         s = ("%(type)s instance --\n"
              "device = %(device)s  length = %(length)d  size = %(size)d\n"
@@ -1212,6 +1214,22 @@ class Chunk(object):
         if not req.done:
             self.base += req.base
 
+    def reclaim(self, request, amount):
+        """ Reclaim units from a request and return them to the pool. """
+        log.debug("reclaim: %s %d (%d MB)" % (request, amount, self.lengthToSize(amount)))
+        if request.growth < amount:
+            log.error("tried to reclaim %d from request with %d of growth"
+                        % (amount, request.growth))
+            raise ValueError("cannot reclaim more than request has grown")
+
+        request.growth -= amount
+        self.pool += amount
+
+        # put this request in the skip list so we don't try to grow it the
+        # next time we call growRequests to allocate the newly re-acquired pool
+        if request not in self.skip_list:
+            self.skip_list.append(request)
+
     @property
     def growth(self):
         """ Sum of growth for all requests in this chunk. """
@@ -1291,7 +1309,7 @@ class Chunk(object):
             log.debug("%d requests and %d (%dMB) left in chunk" %
                         (self.remaining, self.pool, self.lengthToSize(self.pool)))
             for p in self.requests:
-                if p.done:
+                if p.done or p in self.skip_list:
                     continue
 
                 if not uniform:
@@ -1336,6 +1354,8 @@ class Chunk(object):
                 if self.pool == 0:
                     break
 
+        self.skip_list = []
+
 
 class DiskChunk(Chunk):
     """ A free region on disk from which partitions will be allocated """
@@ -1549,7 +1569,46 @@ def getDiskChunks(disk, partitions, free):
 
     return chunks
 
-def growPartitions(disks, partitions, free):
+class TotalSizeSet(object):
+    """ Set of device requests with a target combined size.
+
+        This will be handled by growing the requests until the desired combined
+        size has been achieved.
+    """
+    def __init__(self, devices, size):
+        self.devices = devices
+        self.size = size
+
+        self.requests = []
+
+        self.allocated = sum([d.req_base_size for d in self.devices])
+        log.debug("set.allocated = %d" % self.allocated)
+
+    def allocate(self, amount):
+        log.debug("allocating %d to TotalSizeSet with %d/%d (%d needed)"
+                    % (amount, self.allocated, self.size, self.needed))
+        self.allocated += amount
+
+    @property
+    def needed(self):
+        return self.size - self.allocated
+
+    def deallocate(self, amount):
+        log.debug("deallocating %d from TotalSizeSet with %d/%d (%d needed)"
+                    % (amount, self.allocated, self.size, self.needed))
+        self.allocated -= amount
+
+class SameSizeSet(object):
+    """ Set of device requests with a common target size. """
+    def __init__(self, devices, size, grow=False, max_size=None):
+        self.devices = devices
+        self.size = int(size / len(devices))
+        self.grow = grow
+        self.max_size = max_size
+
+        self.requests = []
+
+def growPartitions(disks, partitions, free, size_sets=None):
     """ Grow all growable partition requests.
 
         Partitions have already been allocated from chunks of free space on
@@ -1575,36 +1634,132 @@ def growPartitions(disks, partitions, free):
         log.debug("no growable partitions")
         return
 
+    if size_sets is None:
+        size_sets = []
+
     log.debug("growable partitions are %s" % [p.name for p in all_growable])
 
+    #
+    # collect info about each disk and the requests it contains
+    #
+    requests_by_device = {}
+    chunks_by_request = {}
+    chunks = []
     for disk in disks:
-        log.debug("growing partitions on %s" % disk.name)
         sector_size = disk.format.partedDevice.sectorSize
 
-        # find any extended partition on this disk
-        extended_geometry = getattr(disk.format.extendedPartition,
-                                    "geometry",
-                                    None)  # parted.Geometry
-
         # list of free space regions on this disk prior to partition allocation
         disk_free = [f for f in free if f.device.path == disk.path]
         if not disk_free:
             log.debug("no free space on %s" % disk.name)
             continue
 
-        chunks = getDiskChunks(disk, partitions, disk_free)
-        log.debug("disk %s has %d chunks" % (disk.name, len(chunks)))
-        # grow the partitions in each chunk as a group
+        disk_chunks = getDiskChunks(disk, partitions, disk_free)
+        for chunk in disk_chunks:
+            for request in chunk.requests:
+                requests_by_device[request.device] = request
+                chunks_by_request[request] = chunk
+
+        chunks.extend(disk_chunks)
+
+        log.debug("disk %s has %d chunks" % (disk.name, len(disk_chunks)))
+
+    #
+    # grow the partitions in each chunk as a group
+    #
+    for chunk in chunks:
+        if not chunk.hasGrowable:
+            # no growable partitions in this chunk
+            continue
+
+        chunk.growRequests()
+
+    # adjust set members' growth amounts as needed
+
+    # TotalSizeSet members are trimmed to meet the total size requested
+    reclaimed = dict([(chunk, 0) for chunk in chunks])
+    for ss in size_sets:
+        if not isinstance(ss, TotalSizeSet):
+            continue
+
+        log.debug("set: %s %d/%d" % ([d.name for d in ss.devices],
+                                      ss.allocated, ss.size))
+
+        for device in ss.devices:
+            request = requests_by_device[device]
+            chunk = chunks_by_request[request]
+            ss.allocate(chunk.lengthToSize(request.growth))
+
+        # decide how much to take back from each request
+        # We may assume that all requests have the same base size.
+        # We're shooting for a roughly equal distribution by trimming growth
+        # from the requests that have grown the most first.
+        requests = sorted([requests_by_device[d] for d in ss.devices],
+                          key=lambda r: r.growth, reverse=True)
+        for request in requests:
+            chunk = chunks_by_request[request]
+            log.debug("%s" % request)
+            log.debug("needed: %d" % ss.needed)
+
+            if ss.needed < 0:
+                # it would be good to take back some from each device instead
+                # of taking all from the last one(s)
+                extra = min(-chunk.sizeToLength(ss.needed), request.growth)
+                reclaimed[chunk] += extra
+                chunk.reclaim(request, extra)
+                ss.deallocate(chunk.lengthToSize(extra))
+
+            if ss.needed <= 0:
+                request.done = True
+
+    # XXX maybe we need to be converting this back to size in case of
+    #     multiple disks with varying sector sizes.
+    for chunk in chunks:
+        if reclaimed[chunk] > chunk.sizeToLength(len(disks)):
+            chunk.growRequests()
+
+    # SameSizeSet members all have the same size as the smallest member
+    reclaimed = dict([(chunk, 0) for chunk in chunks])
+    for ss in size_sets:
+        if not isinstance(ss, SameSizeSet):
+            continue
+        requests = [requests_by_device[d] for d in ss.devices]
+        min_growth = min([r.growth for r in requests])
+        for request in requests:
+            if request.growth > min_growth:
+                chunk = chunks_by_request[request]
+                extra = request.growth - min_growth
+                reclaimed[chunk] += extra
+                chunk.reclaim(request, extra)
+                request.done = True
+            elif request.growth == min_growth:
+                request.done = True
+
+    # XXX maybe we need to be converting this back to size in case of
+    #     multiple disks with varying sector sizes.
+    for chunk in chunks:
+        if reclaimed[chunk] > chunk.sizeToLength(len(disks)):
+            chunk.growRequests()
+
+    for disk in disks:
+        log.debug("growing partitions on %s" % disk.name)
         for chunk in chunks:
+            if chunk.path != disk.path:
+                continue
+
             if not chunk.hasGrowable:
                 # no growable partitions in this chunk
                 continue
 
-            chunk.growRequests()
-
             # recalculate partition geometries
             disklabel = disk.format
             start = chunk.geometry.start
+
+            # find any extended partition on this disk
+            extended_geometry = getattr(disklabel.extendedPartition,
+                                        "geometry",
+                                        None)  # parted.Geometry
+
             # align start sector as needed
             if not disklabel.alignment.isAligned(chunk.geometry, start):
                 start = disklabel.alignment.alignUp(chunk.geometry, start)
-- 
1.7.7.6

