/[smeserver]/cdrom.image/sme9/updates/storage/partitioning.py
ViewVC logotype

Annotation of /cdrom.image/sme9/updates/storage/partitioning.py

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (hide annotations) (download) (as text)
Tue Jul 30 21:01:52 2013 UTC (10 years, 11 months ago) by charliebrady
Branch: MAIN
Content type: text/x-python
Add storage module from anaconda to updates directory, so that parts of it can be modified.

1 charliebrady 1.1 # partitioning.py
2     # Disk partitioning functions.
3     #
4     # Copyright (C) 2009 Red Hat, Inc.
5     #
6     # This copyrighted material is made available to anyone wishing to use,
7     # modify, copy, or redistribute it subject to the terms and conditions of
8     # the GNU General Public License v.2, or (at your option) any later version.
9     # This program is distributed in the hope that it will be useful, but WITHOUT
10     # ANY WARRANTY expressed or implied, including the implied warranties of
11     # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12     # Public License for more details. You should have received a copy of the
13     # GNU General Public License along with this program; if not, write to the
14     # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15     # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
16     # source code or documentation are not subject to the GNU General Public
17     # License and may only be used or replicated with the express permission of
18     # Red Hat, Inc.
19     #
20     # Red Hat Author(s): Dave Lehman <dlehman@redhat.com>
21     #
22    
23     import sys
24     import os
25     from operator import add, sub, gt, lt
26    
27     import parted
28     from pykickstart.constants import *
29    
30     from constants import *
31     import platform
32    
33     from errors import *
34     from deviceaction import *
35     from devices import PartitionDevice, LUKSDevice, devicePathToName
36     from formats import getFormat
37    
38     import gettext
39     _ = lambda x: gettext.ldgettext("anaconda", x)
40    
41     import logging
42     log = logging.getLogger("storage")
43    
44     def _createFreeSpacePartitions(anaconda):
45     # get a list of disks that have at least one free space region of at
46     # least 100MB
47     disks = []
48     for disk in anaconda.id.storage.partitioned:
49     if anaconda.id.storage.clearPartDisks and \
50     (disk.name not in anaconda.id.storage.clearPartDisks):
51     continue
52    
53     part = disk.format.firstPartition
54     while part:
55     if not part.type & parted.PARTITION_FREESPACE:
56     part = part.nextPartition()
57     continue
58    
59     if part.getSize(unit="MB") > 100:
60     disks.append(disk)
61     break
62    
63     part = part.nextPartition()
64    
65     # create a separate pv partition for each disk with free space
66     devs = []
67     for disk in disks:
68     if anaconda.id.storage.encryptedAutoPart:
69     fmt_type = "luks"
70     fmt_args = {"escrow_cert": anaconda.id.storage.autoPartEscrowCert,
71     "cipher": anaconda.id.storage.encryptionCipher,
72     "add_backup_passphrase": anaconda.id.storage.autoPartAddBackupPassphrase}
73     else:
74     fmt_type = "lvmpv"
75     fmt_args = {}
76     part = anaconda.id.storage.newPartition(fmt_type=fmt_type,
77     fmt_args=fmt_args,
78     size=1,
79     grow=True,
80     disks=[disk])
81     anaconda.id.storage.createDevice(part)
82     devs.append(part)
83    
84     return (disks, devs)
85    
86     def _schedulePartitions(anaconda, disks):
87     #
88     # Convert storage.autoPartitionRequests into Device instances and
89     # schedule them for creation
90     #
91     # First pass is for partitions only. We'll do LVs later.
92     #
93     for request in anaconda.id.storage.autoPartitionRequests:
94     if request.asVol:
95     continue
96    
97     if request.fstype is None:
98     request.fstype = anaconda.id.storage.defaultFSType
99     elif request.fstype == "prepboot":
100     # make sure there never is more than one prepboot per disk
101     bootdev = anaconda.platform.bootDevice()
102     if (bootdev and
103     anaconda.id.bootloader.drivelist and
104     anaconda.id.bootloader.drivelist[0] == bootdev.disk.name):
105     # do not allow creating the new PReP boot on the same drive
106     log.info("partitioning: skipping a PReP boot "
107     "partition request on %s" % bootdev.disk.name)
108     continue
109     log.debug("partitioning: allowing a PReP boot partition request")
110     elif request.fstype == "efi":
111     # make sure there never is more than one efi system partition per disk
112     bootdev = anaconda.platform.bootDevice()
113     if (bootdev and
114     anaconda.id.bootloader.drivelist and
115     anaconda.id.bootloader.drivelist[0] == bootdev.disk.name):
116     log.info("partitioning: skipping a EFI System "
117     "Partition request on %s" % bootdev.disk.name)
118     bootdev.format.mountpoint = "/boot/efi"
119     continue
120     log.debug("partitioning: allowing a EFI System Partition request")
121    
122    
123     # This is a little unfortunate but let the backend dictate the rootfstype
124     # so that things like live installs can do the right thing
125     if request.mountpoint == "/" and anaconda.backend.rootFsType != None:
126     request.fstype = anaconda.backend.rootFsType
127    
128     dev = anaconda.id.storage.newPartition(fmt_type=request.fstype,
129     size=request.size,
130     grow=request.grow,
131     maxsize=request.maxSize,
132     mountpoint=request.mountpoint,
133     disks=disks,
134     weight=request.weight)
135    
136     # schedule the device for creation
137     anaconda.id.storage.createDevice(dev)
138    
139     # make sure preexisting broken lvm/raid configs get out of the way
140     return
141    
142     def _scheduleLVs(anaconda, devs):
143     if anaconda.id.storage.encryptedAutoPart:
144     pvs = []
145     for dev in devs:
146     pv = LUKSDevice("luks-%s" % dev.name,
147     format=getFormat("lvmpv", device=dev.path),
148     size=dev.size,
149     parents=dev)
150     pvs.append(pv)
151     anaconda.id.storage.createDevice(pv)
152     else:
153     pvs = devs
154    
155     # create a vg containing all of the autopart pvs
156     vg = anaconda.id.storage.newVG(pvs=pvs)
157     anaconda.id.storage.createDevice(vg)
158    
159     initialVGSize = vg.size
160    
161     #
162     # Convert storage.autoPartitionRequests into Device instances and
163     # schedule them for creation.
164     #
165     # Second pass, for LVs only.
166     for request in anaconda.id.storage.autoPartitionRequests:
167     if not request.asVol:
168     continue
169    
170     if request.requiredSpace and request.requiredSpace > initialVGSize:
171     continue
172    
173     if request.fstype is None:
174     request.fstype = anaconda.id.storage.defaultFSType
175    
176     # This is a little unfortunate but let the backend dictate the rootfstype
177     # so that things like live installs can do the right thing
178     if request.mountpoint == "/" and anaconda.backend.rootFsType != None:
179     request.fstype = anaconda.backend.rootFsType
180    
181     # FIXME: move this to a function and handle exceptions
182     dev = anaconda.id.storage.newLV(vg=vg,
183     fmt_type=request.fstype,
184     mountpoint=request.mountpoint,
185     grow=request.grow,
186     maxsize=request.maxSize,
187     size=request.size,
188     singlePV=request.singlePV)
189    
190     # schedule the device for creation
191     anaconda.id.storage.createDevice(dev)
192    
193    
194     def doAutoPartition(anaconda):
195     log.debug("doAutoPartition(%s)" % anaconda)
196     log.debug("doAutoPart: %s" % anaconda.id.storage.doAutoPart)
197     log.debug("clearPartType: %s" % anaconda.id.storage.clearPartType)
198     log.debug("clearPartDisks: %s" % anaconda.id.storage.clearPartDisks)
199     log.debug("autoPartitionRequests: %s" % anaconda.id.storage.autoPartitionRequests)
200     log.debug("storage.disks: %s" % [d.name for d in anaconda.id.storage.disks])
201     log.debug("storage.partitioned: %s" % [d.name for d in anaconda.id.storage.partitioned])
202     log.debug("all names: %s" % [d.name for d in anaconda.id.storage.devices])
203     if anaconda.dir == DISPATCH_BACK:
204     # temporarily unset storage.clearPartType so that all devices will be
205     # found during storage reset
206     clearPartType = anaconda.id.storage.clearPartType
207     anaconda.id.storage.clearPartType = None
208     anaconda.id.storage.reset()
209     anaconda.id.storage.clearPartType = clearPartType
210     return
211    
212     disks = []
213     devs = []
214    
215     if anaconda.id.storage.doAutoPart:
216     clearPartitions(anaconda.id.storage)
217     # update the bootloader's drive list to add disks which have their
218     # whole disk format replaced by a disklabel. Make sure to keep any
219     # previous boot order selection from clearpart_gui or kickstart
220     anaconda.id.bootloader.updateDriveList(anaconda.id.bootloader.drivelist)
221    
222     if anaconda.id.storage.doAutoPart:
223     (disks, devs) = _createFreeSpacePartitions(anaconda)
224    
225     if disks == []:
226     if anaconda.isKickstart:
227     msg = _("Could not find enough free space for automatic "
228     "partitioning. Press 'OK' to exit the installer.")
229     else:
230     msg = _("Could not find enough free space for automatic "
231     "partitioning, please use another partitioning method.")
232    
233     anaconda.intf.messageWindow(_("Error Partitioning"), msg,
234     custom_icon='error')
235    
236     if anaconda.isKickstart:
237     sys.exit(0)
238    
239     anaconda.id.storage.reset()
240     return DISPATCH_BACK
241    
242     _schedulePartitions(anaconda, disks)
243    
244     # sanity check the individual devices
245     log.warning("not sanity checking devices because I don't know how yet")
246    
247     # run the autopart function to allocate and grow partitions
248     try:
249     doPartitioning(anaconda.id.storage,
250     exclusiveDisks=anaconda.id.storage.clearPartDisks)
251    
252     if anaconda.id.storage.doAutoPart:
253     _scheduleLVs(anaconda, devs)
254    
255     # grow LVs
256     growLVM(anaconda.id.storage)
257     except PartitioningWarning as msg:
258     if not anaconda.isKickstart:
259     anaconda.intf.messageWindow(_("Warnings During Automatic "
260     "Partitioning"),
261     _("Following warnings occurred during automatic "
262     "partitioning:\n\n%s") % (msg,),
263     custom_icon='warning')
264     else:
265     log.warning(msg)
266     except PartitioningError as msg:
267     # restore drives to original state
268     anaconda.id.storage.reset()
269     if not anaconda.isKickstart:
270     extra = ""
271    
272     if anaconda.id.displayMode != "t":
273     anaconda.dispatch.skipStep("partition", skip = 0)
274     else:
275     extra = _("\n\nPress 'OK' to exit the installer.")
276     anaconda.intf.messageWindow(_("Error Partitioning"),
277     _("Could not allocate requested partitions: \n\n"
278     "%(msg)s.%(extra)s") % {'msg': msg, 'extra': extra},
279     custom_icon='error')
280    
281     if anaconda.isKickstart:
282     sys.exit(0)
283     else:
284     return DISPATCH_BACK
285    
286     # sanity check the collection of devices
287     log.warning("not sanity checking storage config because I don't know how yet")
288     # now do a full check of the requests
289     (errors, warnings) = anaconda.id.storage.sanityCheck()
290     if warnings:
291     for warning in warnings:
292     log.warning(warning)
293     if errors:
294     errortxt = "\n".join(errors)
295     if anaconda.isKickstart:
296     extra = _("\n\nPress 'OK' to exit the installer.")
297     else:
298     extra = _("\n\nPress 'OK' to choose a different partitioning option.")
299    
300     anaconda.intf.messageWindow(_("Automatic Partitioning Errors"),
301     _("The following errors occurred with your "
302     "partitioning:\n\n%(errortxt)s\n\n"
303     "This can happen if there is not enough "
304     "space on your hard drive(s) for the "
305     "installation. %(extra)s")
306     % {'errortxt': errortxt, 'extra': extra},
307     custom_icon='error')
308     #
309     # XXX if in kickstart we reboot
310     #
311     if anaconda.isKickstart:
312     anaconda.intf.messageWindow(_("Unrecoverable Error"),
313     _("The system will now reboot."))
314     sys.exit(0)
315     anaconda.id.storage.reset()
316     return DISPATCH_BACK
317    
318     def shouldClear(device, clearPartType, clearPartDisks=None):
319     if clearPartType not in [CLEARPART_TYPE_LINUX, CLEARPART_TYPE_ALL]:
320     return False
321    
322     if isinstance(device, PartitionDevice):
323     # Never clear the special first partition on a Mac disk label, as that
324     # holds the partition table itself.
325     if device.disk.format.partedDisk.type == "mac" and \
326     device.partedPartition.number == 1 and \
327     device.partedPartition.name == "Apple":
328     return False
329    
330     # If we got a list of disks to clear, make sure this one's on it
331     if clearPartDisks and device.disk.name not in clearPartDisks:
332     return False
333    
334     # We don't want to fool with extended partitions, freespace, &c
335     if device.partType not in [parted.PARTITION_NORMAL,
336     parted.PARTITION_LOGICAL]:
337     return False
338    
339     if clearPartType == CLEARPART_TYPE_LINUX and \
340     not device.format.linuxNative and \
341     not device.getFlag(parted.PARTITION_LVM) and \
342     not device.getFlag(parted.PARTITION_RAID) and \
343     not device.getFlag(parted.PARTITION_SWAP):
344     return False
345     elif device.isDisk and not device.partitioned:
346     # If we got a list of disks to clear, make sure this one's on it
347     if clearPartDisks and device.name not in clearPartDisks:
348     return False
349    
350     # Never clear disks with hidden formats
351     if device.format.hidden:
352     return False
353    
354     if clearPartType == CLEARPART_TYPE_LINUX and \
355     not device.format.linuxNative:
356     return False
357    
358     # Don't clear devices holding install media.
359     if device.protected:
360     return False
361    
362     # Don't clear immutable devices.
363     if device.immutable:
364     return False
365    
366     # TODO: do platform-specific checks on ia64, pSeries, iSeries, mac
367    
368     return True
369    
370     def clearPartitions(storage):
371     """ Clear partitions and dependent devices from disks.
372    
373     Arguments:
374    
375     storage -- a storage.Storage instance
376    
377     Keyword arguments:
378    
379     None
380    
381     NOTES:
382    
383     - Needs some error handling, especially for the parted bits.
384    
385     """
386     if storage.clearPartType is None or storage.clearPartType == CLEARPART_TYPE_NONE:
387     # not much to do
388     return
389    
390     _platform = storage.anaconda.platform
391    
392     if not hasattr(_platform, "diskLabelTypes"):
393     raise StorageError("can't clear partitions without platform data")
394    
395     # we are only interested in partitions that physically exist
396     partitions = [p for p in storage.partitions if p.exists]
397     # Sort partitions by descending partition number to minimize confusing
398     # things like multiple "destroy sda5" actions due to parted renumbering
399     # partitions. This can still happen through the UI but it makes sense to
400     # avoid it where possible.
401     partitions.sort(key=lambda p: p.partedPartition.number, reverse=True)
402     for part in partitions:
403     log.debug("clearpart: looking at %s" % part.name)
404     if not shouldClear(part, storage.clearPartType, storage.clearPartDisks):
405     continue
406    
407     log.debug("clearing %s" % part.name)
408    
409     # XXX is there any argument for not removing incomplete devices?
410     # -- maybe some RAID devices
411     devices = storage.deviceDeps(part)
412     while devices:
413     log.debug("devices to remove: %s" % ([d.name for d in devices],))
414     leaves = [d for d in devices if d.isleaf]
415     log.debug("leaves to remove: %s" % ([d.name for d in leaves],))
416     for leaf in leaves:
417     storage.destroyDevice(leaf)
418     devices.remove(leaf)
419    
420     log.debug("partitions: %s" % [p.getDeviceNodeName() for p in part.partedPartition.disk.partitions])
421     storage.destroyDevice(part)
422    
423     # now remove any empty extended partitions
424     removeEmptyExtendedPartitions(storage)
425    
426     # make sure that the the boot device, along with any other disk we are
427     # supposed to reinitialize, has the correct disklabel type if we're going
428     # to completely clear it.
429     for disk in storage.partitioned:
430     if not storage.anaconda.id.bootloader.drivelist and \
431     not storage.reinitializeDisks:
432     break
433    
434     if not storage.reinitializeDisks and \
435     disk.name != storage.anaconda.id.bootloader.drivelist[0]:
436     continue
437    
438     if storage.clearPartType != CLEARPART_TYPE_ALL or \
439     (storage.clearPartDisks and disk.name not in storage.clearPartDisks):
440     continue
441    
442     # Don't touch immutable disks
443     if disk.immutable:
444     continue
445    
446     # don't reinitialize the disklabel if the disk contains install media
447     if filter(lambda p: p.dependsOn(disk), storage.protectedDevices):
448     continue
449    
450     nativeLabelType = _platform.bestDiskLabelType(disk)
451     if disk.format.labelType == nativeLabelType:
452     continue
453    
454     if disk.format.labelType == "mac":
455     # remove the magic apple partition
456     for part in storage.partitions:
457     if part.disk == disk and part.partedPartition.number == 1:
458     log.debug("clearing %s" % part.name)
459     # We can't schedule the apple map partition for removal
460     # because parted will not allow us to remove it from the
461     # disk. Still, we need it out of the devicetree.
462     storage.devicetree._removeDevice(part, moddisk=False)
463    
464     destroy_action = ActionDestroyFormat(disk)
465     newLabel = getFormat("disklabel", device=disk.path,
466     labelType=nativeLabelType)
467     create_action = ActionCreateFormat(disk, format=newLabel)
468     storage.devicetree.registerAction(destroy_action)
469     storage.devicetree.registerAction(create_action)
470    
471     def removeEmptyExtendedPartitions(storage):
472     for disk in storage.partitioned:
473     log.debug("checking whether disk %s has an empty extended" % disk.name)
474     extended = disk.format.extendedPartition
475     logical_parts = disk.format.logicalPartitions
476     log.debug("extended is %s ; logicals is %s" % (extended, [p.getDeviceNodeName() for p in logical_parts]))
477     if extended and not logical_parts:
478     log.debug("removing empty extended partition from %s" % disk.name)
479     extended_name = devicePathToName(extended.getDeviceNodeName())
480     extended = storage.devicetree.getDeviceByName(extended_name)
481     storage.destroyDevice(extended)
482     #disk.partedDisk.removePartition(extended.partedPartition)
483    
484     for disk in [d for d in storage.disks if d not in storage.partitioned]:
485     # clear any whole-disk formats that need clearing
486     if shouldClear(disk, storage.clearPartType, storage.clearPartDisks):
487     log.debug("clearing %s" % disk.name)
488     devices = storage.deviceDeps(disk)
489     while devices:
490     log.debug("devices to remove: %s" % ([d.name for d in devices],))
491     leaves = [d for d in devices if d.isleaf]
492     log.debug("leaves to remove: %s" % ([d.name for d in leaves],))
493     for leaf in leaves:
494     storage.destroyDevice(leaf)
495     devices.remove(leaf)
496    
497     destroy_action = ActionDestroyFormat(disk)
498     newLabel = getFormat("disklabel", device=disk.path)
499     create_action = ActionCreateFormat(disk, format=newLabel)
500     storage.devicetree.registerAction(destroy_action)
501     storage.devicetree.registerAction(create_action)
502    
503     def partitionCompare(part1, part2):
504     """ More specifically defined partitions come first.
505    
506     < 1 => x < y
507     0 => x == y
508     > 1 => x > y
509     """
510     ret = 0
511    
512     if part1.req_base_weight:
513     ret -= part1.req_base_weight
514    
515     if part2.req_base_weight:
516     ret += part2.req_base_weight
517    
518     # more specific disk specs to the front of the list
519     # req_disks being empty is equivalent to it being an infinitely long list
520     if part1.req_disks and not part2.req_disks:
521     ret -= 500
522     elif not part1.req_disks and part2.req_disks:
523     ret += 500
524     else:
525     ret += cmp(len(part1.req_disks), len(part2.req_disks)) * 500
526    
527     # primary-only to the front of the list
528     ret -= cmp(part1.req_primary, part2.req_primary) * 200
529    
530     # fixed size requests to the front
531     ret += cmp(part1.req_grow, part2.req_grow) * 100
532    
533     # larger requests go to the front of the list
534     ret -= cmp(part1.req_base_size, part2.req_base_size) * 50
535    
536     # potentially larger growable requests go to the front
537     if part1.req_grow and part2.req_grow:
538     if not part1.req_max_size and part2.req_max_size:
539     ret -= 25
540     elif part1.req_max_size and not part2.req_max_size:
541     ret += 25
542     else:
543     ret -= cmp(part1.req_max_size, part2.req_max_size) * 25
544    
545     # give a little bump based on mountpoint
546     if hasattr(part1.format, "mountpoint") and \
547     hasattr(part2.format, "mountpoint"):
548     ret += cmp(part1.format.mountpoint, part2.format.mountpoint) * 10
549    
550     if ret > 0:
551     ret = 1
552     elif ret < 0:
553     ret = -1
554    
555     return ret
556    
557     def getNextPartitionType(disk, no_primary=None):
558     """ Find the type of partition to create next on a disk.
559    
560     Return a parted partition type value representing the type of the
561     next partition we will create on this disk.
562    
563     If there is only one free primary partition and we can create an
564     extended partition, we do that.
565    
566     If there are free primary slots and an extended partition we will
567     recommend creating a primary partition. This can be overridden
568     with the keyword argument no_primary.
569    
570     Arguments:
571    
572     disk -- a parted.Disk instance representing the disk
573    
574     Keyword arguments:
575    
576     no_primary -- given a choice between primary and logical
577     partitions, prefer logical
578    
579     """
580     part_type = None
581     extended = disk.getExtendedPartition()
582     supports_extended = disk.supportsFeature(parted.DISK_TYPE_EXTENDED)
583     logical_count = len(disk.getLogicalPartitions())
584     max_logicals = disk.getMaxLogicalPartitions()
585     primary_count = disk.primaryPartitionCount
586    
587     if primary_count < disk.maxPrimaryPartitionCount:
588     if primary_count == disk.maxPrimaryPartitionCount - 1:
589     # can we make an extended partition? now's our chance.
590     if not extended and supports_extended:
591     part_type = parted.PARTITION_EXTENDED
592     elif not extended:
593     # extended partitions not supported. primary or nothing.
594     if not no_primary:
595     part_type = parted.PARTITION_NORMAL
596     else:
597     # there is an extended and a free primary
598     if not no_primary:
599     part_type = parted.PARTITION_NORMAL
600     elif logical_count < max_logicals:
601     # we have an extended with logical slots, so use one.
602     part_type = parted.PARTITION_LOGICAL
603     else:
604     # there are two or more primary slots left. use one unless we're
605     # not supposed to make primaries.
606     if not no_primary:
607     part_type = parted.PARTITION_NORMAL
608     elif extended and logical_count < max_logicals:
609     part_type = parted.PARTITION_LOGICAL
610     elif extended and logical_count < max_logicals:
611     part_type = parted.PARTITION_LOGICAL
612    
613     return part_type
614    
615     def getBestFreeSpaceRegion(disk, part_type, req_size,
616     boot=None, best_free=None, grow=None):
617     """ Return the "best" free region on the specified disk.
618    
619     For non-boot partitions, we return the largest free region on the
620     disk. For boot partitions, we return the first region that is
621     large enough to hold the partition.
622    
623     Partition type (parted's PARTITION_NORMAL, PARTITION_LOGICAL) is
624     taken into account when locating a suitable free region.
625    
626     For locating the best region from among several disks, the keyword
627     argument best_free allows the specification of a current "best"
628     free region with which to compare the best from this disk. The
629     overall best region is returned.
630    
631     Arguments:
632    
633     disk -- the disk (a parted.Disk instance)
634     part_type -- the type of partition we want to allocate
635     (one of parted's partition type constants)
636     req_size -- the requested size of the partition (in MB)
637    
638     Keyword arguments:
639    
640     boot -- indicates whether this will be a bootable partition
641     (boolean)
642     best_free -- current best free region for this partition
643     grow -- indicates whether this is a growable request
644    
645     """
646     log.debug("getBestFreeSpaceRegion: disk=%s part_type=%d req_size=%dMB "
647     "boot=%s best=%s grow=%s" %
648     (disk.device.path, part_type, req_size, boot, best_free, grow))
649     extended = disk.getExtendedPartition()
650    
651     for _range in disk.getFreeSpaceRegions():
652     if extended:
653     # find out if there is any overlap between this region and the
654     # extended partition
655     log.debug("looking for intersection between extended (%d-%d) and free (%d-%d)" %
656     (extended.geometry.start, extended.geometry.end, _range.start, _range.end))
657    
658     # parted.Geometry.overlapsWith can handle this
659     try:
660     free_geom = extended.geometry.intersect(_range)
661     except ArithmeticError, e:
662     # this freespace region does not lie within the extended
663     # partition's geometry
664     free_geom = None
665    
666     if (free_geom and part_type == parted.PARTITION_NORMAL) or \
667     (not free_geom and part_type == parted.PARTITION_LOGICAL):
668     log.debug("free region not suitable for request")
669     continue
670    
671     if part_type == parted.PARTITION_NORMAL:
672     # we're allocating a primary and the region is not within
673     # the extended, so we use the original region
674     free_geom = _range
675     else:
676     free_geom = _range
677    
678     if free_geom.start > disk.maxPartitionStartSector:
679     log.debug("free range start sector beyond max for new partitions")
680     continue
681    
682     if boot:
683     free_start_mb = sectorsToSize(free_geom.start,
684     disk.device.sectorSize)
685     req_end_mb = free_start_mb + req_size
686     if req_end_mb > 2*1024*1024:
687     log.debug("free range position would place boot req above 2TB")
688     continue
689    
690     log.debug("current free range is %d-%d (%dMB)" % (free_geom.start,
691     free_geom.end,
692     free_geom.getSize()))
693     free_size = free_geom.getSize()
694    
695     # For boot partitions, we want the first suitable region we find.
696     # For growable or extended partitions, we want the largest possible
697     # free region.
698     # For all others, we want the smallest suitable free region.
699     if grow or part_type == parted.PARTITION_EXTENDED:
700     op = gt
701     else:
702     op = lt
703     if req_size <= free_size:
704     if not best_free or op(free_geom.length, best_free.length):
705     best_free = free_geom
706    
707     if boot:
708     # if this is a bootable partition we want to
709     # use the first freespace region large enough
710     # to satisfy the request
711     break
712    
713     return best_free
714    
715     def sectorsToSize(sectors, sectorSize):
716     """ Convert length in sectors to size in MB.
717    
718     Arguments:
719    
720     sectors - sector count
721     sectorSize - sector size for the device, in bytes
722     """
723     return (sectors * sectorSize) / (1024.0 * 1024.0)
724    
725     def sizeToSectors(size, sectorSize):
726     """ Convert size in MB to length in sectors.
727    
728     Arguments:
729    
730     size - size in MB
731     sectorSize - sector size for the device, in bytes
732     """
733     return (size * 1024.0 * 1024.0) / sectorSize
734    
735     def removeNewPartitions(disks, partitions):
736     """ Remove newly added input partitions from input disks.
737    
738     Arguments:
739    
740     disks -- list of StorageDevice instances with DiskLabel format
741     partitions -- list of PartitionDevice instances
742    
743     """
744     log.debug("removing all non-preexisting partitions %s from disk(s) %s"
745     % (["%s(id %d)" % (p.name, p.id) for p in partitions
746     if not p.exists],
747     [d.name for d in disks]))
748     for part in partitions:
749     if part.partedPartition and part.disk in disks:
750     if part.exists:
751     # we're only removing partitions that don't physically exist
752     continue
753    
754     if part.isExtended:
755     # these get removed last
756     continue
757    
758     part.disk.format.partedDisk.removePartition(part.partedPartition)
759     part.partedPartition = None
760     part.disk = None
761    
762     for disk in disks:
763     # remove empty extended so it doesn't interfere
764     extended = disk.format.extendedPartition
765     if extended and not disk.format.logicalPartitions:
766     log.debug("removing empty extended partition from %s" % disk.name)
767     disk.format.partedDisk.removePartition(extended)
768    
769     def addPartition(disklabel, free, part_type, size):
770     """ Return new partition after adding it to the specified disk.
771    
772     Arguments:
773    
774     disklabel -- disklabel instance to add partition to
775     free -- where to add the partition (parted.Geometry instance)
776     part_type -- partition type (parted.PARTITION_* constant)
777     size -- size (in MB) of the new partition
778    
779     The new partition will be aligned.
780    
781     Return value is a parted.Partition instance.
782    
783     """
784     start = free.start
785     if not disklabel.alignment.isAligned(free, start):
786     start = disklabel.alignment.alignNearest(free, start)
787    
788     if part_type == parted.PARTITION_LOGICAL:
789     # make room for logical partition's metadata
790     start += disklabel.alignment.grainSize
791    
792     if start != free.start:
793     log.debug("adjusted start sector from %d to %d" % (free.start, start))
794    
795     if part_type == parted.PARTITION_EXTENDED:
796     end = free.end
797     length = end - start + 1
798     else:
799     # size is in MB
800     length = sizeToSectors(size, disklabel.partedDevice.sectorSize)
801     end = start + length - 1
802    
803     if not disklabel.endAlignment.isAligned(free, end):
804     end = disklabel.endAlignment.alignNearest(free, end)
805     log.debug("adjusted length from %d to %d" % (length, end - start + 1))
806     if start > end:
807     raise PartitioningError("unable to allocate aligned partition")
808    
809     new_geom = parted.Geometry(device=disklabel.partedDevice,
810     start=start,
811     end=end)
812    
813     max_length = disklabel.partedDisk.maxPartitionLength
814     if max_length and new_geom.length > max_length:
815     raise PartitioningError("requested size exceeds maximum allowed")
816    
817     # create the partition and add it to the disk
818     partition = parted.Partition(disk=disklabel.partedDisk,
819     type=part_type,
820     geometry=new_geom)
821     constraint = parted.Constraint(exactGeom=new_geom)
822     disklabel.partedDisk.addPartition(partition=partition,
823     constraint=constraint)
824     return partition
825    
826     def getFreeRegions(disks):
827     """ Return a list of free regions on the specified disks.
828    
829     Arguments:
830    
831     disks -- list of parted.Disk instances
832    
833     Return value is a list of unaligned parted.Geometry instances.
834    
835     """
836     free = []
837     for disk in disks:
838     for f in disk.format.partedDisk.getFreeSpaceRegions():
839     if f.length > 0:
840     free.append(f)
841    
842     return free
843    
844     def updateExtendedPartitions(storage, disks):
845     # XXX hack -- if we created any extended partitions we need to add
846     # them to the tree now
847     for disk in disks:
848     extended = disk.format.extendedPartition
849     if not extended:
850     # remove any obsolete extended partitions
851     for part in storage.partitions:
852     if part.disk == disk and part.isExtended:
853     if part.exists:
854     storage.destroyDevice(part)
855     else:
856     storage.devicetree._removeDevice(part, moddisk=False)
857     continue
858    
859     extendedName = devicePathToName(extended.getDeviceNodeName())
860     # remove any obsolete extended partitions
861     for part in storage.partitions:
862     if part.disk == disk and part.isExtended and \
863     part.partedPartition not in disk.format.partitions:
864     if part.exists:
865     storage.destroyDevice(part)
866     else:
867     storage.devicetree._removeDevice(part, moddisk=False)
868    
869     device = storage.devicetree.getDeviceByName(extendedName)
870     if device:
871     if not device.exists:
872     # created by us, update partedPartition
873     device.partedPartition = extended
874     continue
875    
876     # This is a little odd because normally instantiating a partition
877     # that does not exist means leaving self.parents empty and instead
878     # populating self.req_disks. In this case, we need to skip past
879     # that since this partition is already defined.
880     device = PartitionDevice(extendedName, parents=disk)
881     device.parents = [disk]
882     device.partedPartition = extended
883     # just add the device for now -- we'll handle actions at the last
884     # moment to simplify things
885     storage.devicetree._addDevice(device)
886    
887     def doPartitioning(storage, exclusiveDisks=None):
888     """ Allocate and grow partitions.
889    
890     When this function returns without error, all PartitionDevice
891     instances must have their parents set to the disk they are
892     allocated on, and their partedPartition attribute set to the
893     appropriate parted.Partition instance from their containing
894     disk. All req_xxxx attributes must be unchanged.
895    
896     Arguments:
897    
898     storage - Main anaconda Storage instance
899    
900     Keyword arguments:
901    
902     exclusiveDisks -- list of names of disks to use
903    
904     """
905     anaconda = storage.anaconda
906     if not hasattr(anaconda.platform, "diskLabelTypes"):
907     raise StorageError("can't allocate partitions without platform data")
908    
909     disks = storage.partitioned
910     if exclusiveDisks:
911     disks = [d for d in disks if d.name in exclusiveDisks]
912    
913     for disk in disks:
914     try:
915     disk.setup()
916     except DeviceError as (msg, name):
917     log.error("failed to set up disk %s: %s" % (name, msg))
918     raise PartitioningError("disk %s inaccessible" % disk.name)
919    
920     partitions = storage.partitions[:]
921     for part in storage.partitions:
922     part.req_bootable = False
923    
924     if part.exists or \
925     (storage.deviceImmutable(part) and part.partedPartition):
926     # if the partition is preexisting or part of a complex device
927     # then we shouldn't modify it
928     partitions.remove(part)
929     continue
930    
931     if not part.exists:
932     # start over with flexible-size requests
933     part.req_size = part.req_base_size
934    
935     # FIXME: isn't there a better place for this to happen?
936     try:
937     bootDev = anaconda.platform.bootDevice()
938     except DeviceError:
939     bootDev = None
940    
941     if bootDev:
942     bootDev.req_bootable = True
943    
944     # turn off cylinder alignment
945     for partedDisk in [d.format.partedDisk for d in disks]:
946     if partedDisk.isFlagAvailable(parted.DISK_CYLINDER_ALIGNMENT):
947     partedDisk.unsetFlag(parted.DISK_CYLINDER_ALIGNMENT)
948    
949     removeNewPartitions(disks, partitions)
950     free = getFreeRegions(disks)
951    
952     try:
953     allocatePartitions(storage, disks, partitions, free)
954     growPartitions(disks, partitions, free)
955     finally:
956     # The number and thus the name of partitions may have changed now,
957     # allocatePartitions() takes care of this for new partitions, but not
958     # for pre-existing ones, so we update the name of all partitions here
959     for part in storage.partitions:
960     # leave extended partitions as-is -- we'll handle them separately
961     if part.isExtended:
962     continue
963     part.updateName()
964    
965     updateExtendedPartitions(storage, disks)
966    
967     def allocatePartitions(storage, disks, partitions, freespace):
968     """ Allocate partitions based on requested features.
969    
970     Non-existing partitions are sorted according to their requested
971     attributes, and then allocated.
972    
973     The basic approach to sorting is that the more specifically-
974     defined a request is, the earlier it will be allocated. See
975     the function partitionCompare for details on the sorting
976     criteria.
977    
978     The PartitionDevice instances will have their name and parents
979     attributes set once they have been allocated.
980     """
981     log.debug("allocatePartitions: disks=%s ; partitions=%s" %
982     ([d.name for d in disks],
983     ["%s(id %d)" % (p.name, p.id) for p in partitions]))
984    
985     new_partitions = [p for p in partitions if not p.exists]
986     new_partitions.sort(cmp=partitionCompare)
987    
988     # the following dicts all use device path strings as keys
989     disklabels = {} # DiskLabel instances for each disk
990     all_disks = {} # StorageDevice for each disk
991     for disk in disks:
992     if disk.path not in disklabels.keys():
993     disklabels[disk.path] = disk.format
994     all_disks[disk.path] = disk
995    
996     removeNewPartitions(disks, new_partitions)
997    
998     for _part in new_partitions:
999     if _part.partedPartition and _part.isExtended:
1000     # ignore new extendeds as they are implicit requests
1001     continue
1002    
1003     # obtain the set of candidate disks
1004     req_disks = []
1005     if _part.disk:
1006     # we have a already selected a disk for this request
1007     req_disks = [_part.disk]
1008     elif _part.req_disks:
1009     # use the requested disk set
1010     req_disks = _part.req_disks
1011     else:
1012     # no disks specified means any disk will do
1013     req_disks = disks
1014    
1015     # sort the disks, making sure the boot disk is first
1016     req_disks.sort(key=lambda d: d.name, cmp=storage.compareDisks)
1017     boot_index = None
1018     for disk in req_disks:
1019     if disk.name in storage.anaconda.id.bootloader.drivelist and \
1020     disk.name == storage.anaconda.id.bootloader.drivelist[0]:
1021     boot_index = req_disks.index(disk)
1022    
1023     if boot_index is not None and len(req_disks) > 1:
1024     boot_disk = req_disks.pop(boot_index)
1025     req_disks.insert(0, boot_disk)
1026    
1027     boot = _part.req_base_weight > 1000
1028    
1029     log.debug("allocating partition: %s ; id: %d ; disks: %s ;\n"
1030     "boot: %s ; primary: %s ; size: %dMB ; grow: %s ; "
1031     "max_size: %s" % (_part.name, _part.id,
1032     [d.name for d in req_disks],
1033     boot, _part.req_primary,
1034     _part.req_size, _part.req_grow,
1035     _part.req_max_size))
1036     free = None
1037     use_disk = None
1038     part_type = None
1039     growth = 0
1040     # loop through disks
1041     for _disk in req_disks:
1042     disklabel = disklabels[_disk.path]
1043     sectorSize = disklabel.partedDevice.sectorSize
1044     best = None
1045     current_free = free
1046    
1047     # for growable requests, we don't want to pass the current free
1048     # geometry to getBestFreeRegion -- this allows us to try the
1049     # best region from each disk and choose one based on the total
1050     # growth it allows
1051     if _part.req_grow:
1052     current_free = None
1053    
1054     problem = _part.checkSize()
1055     if problem:
1056     raise PartitioningError("partition is too %s for %s formatting "
1057     "(allowable size is %d MB to %d MB)"
1058     % (problem, _part.format.name,
1059     _part.format.minSize,
1060     _part.format.maxSize))
1061    
1062     log.debug("checking freespace on %s" % _disk.name)
1063    
1064     new_part_type = getNextPartitionType(disklabel.partedDisk)
1065     if new_part_type is None:
1066     # can't allocate any more partitions on this disk
1067     log.debug("no free partition slots on %s" % _disk.name)
1068     continue
1069    
1070     if _part.req_primary and new_part_type != parted.PARTITION_NORMAL:
1071     if (disklabel.partedDisk.primaryPartitionCount <
1072     disklabel.partedDisk.maxPrimaryPartitionCount):
1073     # don't fail to create a primary if there are only three
1074     # primary partitions on the disk (#505269)
1075     new_part_type = parted.PARTITION_NORMAL
1076     else:
1077     # we need a primary slot and none are free on this disk
1078     log.debug("no primary slots available on %s" % _disk.name)
1079     continue
1080    
1081     best = getBestFreeSpaceRegion(disklabel.partedDisk,
1082     new_part_type,
1083     _part.req_size,
1084     best_free=current_free,
1085     boot=boot,
1086     grow=_part.req_grow)
1087    
1088     if best == free and not _part.req_primary and \
1089     new_part_type == parted.PARTITION_NORMAL:
1090     # see if we can do better with a logical partition
1091     log.debug("not enough free space for primary -- trying logical")
1092     new_part_type = getNextPartitionType(disklabel.partedDisk,
1093     no_primary=True)
1094     if new_part_type:
1095     best = getBestFreeSpaceRegion(disklabel.partedDisk,
1096     new_part_type,
1097     _part.req_size,
1098     best_free=current_free,
1099     boot=boot,
1100     grow=_part.req_grow)
1101    
1102     if best and free != best:
1103     update = True
1104     if _part.req_grow:
1105     log.debug("evaluating growth potential for new layout")
1106     new_growth = 0
1107     for disk_path in disklabels.keys():
1108     log.debug("calculating growth for disk %s" % disk_path)
1109     # Now we check, for growable requests, which of the two
1110     # free regions will allow for more growth.
1111    
1112     # set up chunks representing the disks' layouts
1113     temp_parts = []
1114     for _p in new_partitions[:new_partitions.index(_part)]:
1115     if _p.disk.path == disk_path:
1116     temp_parts.append(_p)
1117    
1118     # add the current request to the temp disk to set up
1119     # its partedPartition attribute with a base geometry
1120     if disk_path == _disk.path:
1121     temp_part = addPartition(disklabel,
1122     best,
1123     new_part_type,
1124     _part.req_size)
1125     _part.partedPartition = temp_part
1126     _part.disk = _disk
1127     temp_parts.append(_part)
1128    
1129     chunks = getDiskChunks(all_disks[disk_path],
1130     temp_parts, freespace)
1131    
1132     # grow all growable requests
1133     disk_growth = 0
1134     disk_sector_size = disklabels[disk_path].partedDevice.sectorSize
1135     for chunk in chunks:
1136     chunk.growRequests()
1137     # record the growth for this layout
1138     new_growth += chunk.growth
1139     disk_growth += chunk.growth
1140     for req in chunk.requests:
1141     log.debug("request %d (%s) growth: %d (%dMB) "
1142     "size: %dMB" %
1143     (req.partition.id,
1144     req.partition.name,
1145     req.growth,
1146     sectorsToSize(req.growth,
1147     disk_sector_size),
1148     sectorsToSize(req.growth + req.base,
1149     disk_sector_size)))
1150     log.debug("disk %s growth: %d (%dMB)" %
1151     (disk_path, disk_growth,
1152     sectorsToSize(disk_growth,
1153     disk_sector_size)))
1154    
1155     disklabel.partedDisk.removePartition(temp_part)
1156     _part.partedPartition = None
1157     _part.disk = None
1158    
1159     log.debug("total growth: %d sectors" % new_growth)
1160    
1161     # update the chosen free region unless the previous
1162     # choice yielded greater total growth
1163     if new_growth < growth:
1164     log.debug("keeping old free: %d < %d" % (new_growth,
1165     growth))
1166     update = False
1167     else:
1168     growth = new_growth
1169    
1170     if update:
1171     # now we know we are choosing a new free space,
1172     # so update the disk and part type
1173     log.debug("updating use_disk to %s (%s), type: %s"
1174     % (_disk, _disk.name, new_part_type))
1175     part_type = new_part_type
1176     use_disk = _disk
1177     log.debug("new free: %s (%d-%d / %dMB)" % (best,
1178     best.start,
1179     best.end,
1180     best.getSize()))
1181     log.debug("new free allows for %d sectors of growth" %
1182     growth)
1183     free = best
1184    
1185     if free and boot:
1186     # if this is a bootable partition we want to
1187     # use the first freespace region large enough
1188     # to satisfy the request
1189     log.debug("found free space for bootable request")
1190     break
1191    
1192     if free is None:
1193     raise PartitioningError("not enough free space on disks")
1194    
1195     _disk = use_disk
1196     disklabel = _disk.format
1197    
1198     # create the extended partition if needed
1199     if part_type == parted.PARTITION_EXTENDED:
1200     log.debug("creating extended partition")
1201     addPartition(disklabel, free, part_type, None)
1202    
1203     # now the extended partition exists, so set type to logical
1204     part_type = parted.PARTITION_LOGICAL
1205    
1206     # recalculate freespace
1207     log.debug("recalculating free space")
1208     free = getBestFreeSpaceRegion(disklabel.partedDisk,
1209     part_type,
1210     _part.req_size,
1211     boot=boot,
1212     grow=_part.req_grow)
1213     if not free:
1214     raise PartitioningError("not enough free space after "
1215     "creating extended partition")
1216    
1217     partition = addPartition(disklabel, free, part_type, _part.req_size)
1218     log.debug("created partition %s of %dMB and added it to %s" %
1219     (partition.getDeviceNodeName(), partition.getSize(),
1220     disklabel.device))
1221    
1222     # this one sets the name
1223     _part.partedPartition = partition
1224     _part.disk = _disk
1225    
1226     # parted modifies the partition in the process of adding it to
1227     # the disk, so we need to grab the latest version...
1228     _part.partedPartition = disklabel.partedDisk.getPartitionByPath(_part.path)
1229    
1230    
1231     class Request(object):
1232     """ A partition request.
1233    
1234     Request instances are used for calculating how much to grow
1235     partitions.
1236     """
1237     def __init__(self, partition):
1238     """ Create a Request instance.
1239    
1240     Arguments:
1241    
1242     partition -- a PartitionDevice instance
1243    
1244     """
1245     self.partition = partition # storage.devices.PartitionDevice
1246     self.growth = 0 # growth in sectors
1247     self.max_growth = 0 # max growth in sectors
1248     self.done = not partition.req_grow # can we grow this request more?
1249     self.base = partition.partedPartition.geometry.length # base sectors
1250    
1251     sector_size = partition.partedPartition.disk.device.sectorSize
1252    
1253     if partition.req_grow:
1254     limits = filter(lambda l: l > 0,
1255     [sizeToSectors(partition.req_max_size, sector_size),
1256     sizeToSectors(partition.format.maxSize, sector_size),
1257     partition.partedPartition.disk.maxPartitionLength])
1258    
1259     if limits:
1260     max_sectors = min(limits)
1261     self.max_growth = max_sectors - self.base
1262     if self.max_growth <= 0:
1263     # max size is less than or equal to base, so we're done
1264     self.done = True
1265    
1266     @property
1267     def growable(self):
1268     """ True if this request is growable. """
1269     return self.partition.req_grow
1270    
1271     @property
1272     def id(self):
1273     """ The id of the PartitionDevice this request corresponds to. """
1274     return self.partition.id
1275    
1276     def __str__(self):
1277     s = ("%(type)s instance --\n"
1278     "id = %(id)s name = %(name)s growable = %(growable)s\n"
1279     "base = %(base)d growth = %(growth)d max_grow = %(max_grow)d\n"
1280     "done = %(done)s" %
1281     {"type": self.__class__.__name__, "id": self.id,
1282     "name": self.partition.name, "growable": self.growable,
1283     "base": self.base, "growth": self.growth,
1284     "max_grow": self.max_growth, "done": self.done})
1285     return s
1286    
1287    
1288     class Chunk(object):
1289     """ A free region on disk from which partitions will be allocated """
1290     def __init__(self, geometry, requests=None):
1291     """ Create a Chunk instance.
1292    
1293     Arguments:
1294    
1295     geometry -- parted.Geometry instance describing the free space
1296    
1297    
1298     Keyword Arguments:
1299    
1300     requests -- list of Request instances allocated from this chunk
1301    
1302    
1303     Note: We will limit partition growth based on disklabel
1304     limitations for partition end sector, so a 10TB disk with an
1305     msdos disklabel will be treated like a 2TB disk.
1306    
1307     """
1308     self.geometry = geometry # parted.Geometry
1309     self.pool = self.geometry.length # free sector count
1310     self.sectorSize = self.geometry.device.sectorSize
1311     self.base = 0 # sum of growable requests' base
1312     # sizes, in sectors
1313     self.requests = [] # list of Request instances
1314     if isinstance(requests, list):
1315     for req in requests:
1316     self.addRequest(req)
1317    
1318     def __str__(self):
1319     s = ("%(type)s instance --\n"
1320     "device = %(device)s start = %(start)d end = %(end)d\n"
1321     "length = %(length)d size = %(size)d pool = %(pool)d\n"
1322     "remaining = %(rem)d sectorSize = %(sectorSize)d" %
1323     {"type": self.__class__.__name__,
1324     "device": self.geometry.device.path,
1325     "start": self.geometry.start, "end": self.geometry.end,
1326     "length": self.geometry.length, "size": self.geometry.getSize(),
1327     "pool": self.pool, "rem": self.remaining,
1328     "sectorSize": self.sectorSize})
1329    
1330     return s
1331    
1332     def addRequest(self, req):
1333     """ Add a Request to this chunk. """
1334     log.debug("adding request %d to chunk %s" % (req.partition.id, self))
1335     if not self.requests:
1336     # when adding the first request to the chunk, adjust the pool
1337     # size to reflect any disklabel-specific limits on end sector
1338     max_sector = req.partition.partedPartition.disk.maxPartitionStartSector
1339     chunk_end = min(max_sector, self.geometry.end)
1340     if chunk_end <= self.geometry.start:
1341     # this should clearly never be possible, but if the chunk's
1342     # start sector is beyond the maximum allowed end sector, we
1343     # cannot continue
1344     log.error("chunk start sector is beyond disklabel maximum")
1345     raise PartitioningError("partitions allocated outside "
1346     "disklabel limits")
1347    
1348     new_pool = chunk_end - self.geometry.start + 1
1349     if new_pool != self.pool:
1350     log.debug("adjusting pool to %d based on disklabel limits"
1351     % new_pool)
1352     self.pool = new_pool
1353    
1354     self.requests.append(req)
1355     self.pool -= req.base
1356    
1357     if not req.done:
1358     self.base += req.base
1359    
1360     def getRequestByID(self, id):
1361     """ Retrieve a request from this chunk based on its id. """
1362     for request in self.requests:
1363     if request.id == id:
1364     return request
1365    
1366     @property
1367     def growth(self):
1368     """ Sum of growth in sectors for all requests in this chunk. """
1369     return sum(r.growth for r in self.requests)
1370    
1371     @property
1372     def hasGrowable(self):
1373     """ True if this chunk contains at least one growable request. """
1374     for req in self.requests:
1375     if req.growable:
1376     return True
1377     return False
1378    
1379     @property
1380     def remaining(self):
1381     """ Number of requests still being grown in this chunk. """
1382     return len([d for d in self.requests if not d.done])
1383    
1384     @property
1385     def done(self):
1386     """ True if we are finished growing all requests in this chunk. """
1387     return self.remaining == 0
1388    
1389     def trimOverGrownRequest(self, req, base=None):
1390     """ Enforce max growth and return extra sectors to the pool. """
1391     req_end = req.partition.partedPartition.geometry.end
1392     req_start = req.partition.partedPartition.geometry.start
1393    
1394     # Establish the current total number of sectors of growth for requests
1395     # that lie before this one within this chunk. We add the total count
1396     # to this request's end sector to obtain the end sector for this
1397     # request, including growth of earlier requests but not including
1398     # growth of this request. Maximum growth values are obtained using
1399     # this end sector and various values for maximum end sector.
1400     growth = 0
1401     for request in self.requests:
1402     if request.partition.partedPartition.geometry.start < req_start:
1403     growth += request.growth
1404     req_end += growth
1405    
1406     # obtain the set of possible maximum sectors-of-growth values for this
1407     # request and use the smallest
1408     limits = []
1409    
1410     # disklabel-specific maximum sector
1411     max_sector = req.partition.partedPartition.disk.maxPartitionStartSector
1412     limits.append(max_sector - req_end)
1413    
1414     # 2TB limit on bootable partitions, regardless of disklabel
1415     if req.partition.req_bootable:
1416     limits.append(sizeToSectors(2*1024*1024, self.sectorSize) - req_end)
1417    
1418     # request-specific maximum (see Request.__init__, above, for details)
1419     if req.max_growth:
1420     limits.append(req.max_growth)
1421    
1422     max_growth = min(limits)
1423    
1424     if max_growth and req.growth >= max_growth:
1425     if req.growth > max_growth:
1426     # we've grown beyond the maximum. put some back.
1427     extra = req.growth - max_growth
1428     log.debug("taking back %d (%dMB) from %d (%s)" %
1429     (extra,
1430     sectorsToSize(extra, self.sectorSize),
1431     req.partition.id, req.partition.name))
1432     self.pool += extra
1433     req.growth = max_growth
1434    
1435     # We're done growing this partition, so it no longer
1436     # factors into the growable base used to determine
1437     # what fraction of the pool each request gets.
1438     if base is not None:
1439     base -= req.base
1440     req.done = True
1441    
1442     return base
1443    
1444     def growRequests(self):
1445     """ Calculate growth amounts for requests in this chunk. """
1446     log.debug("Chunk.growRequests: %s" % self)
1447    
1448     # sort the partitions by start sector
1449     self.requests.sort(key=lambda r: r.partition.partedPartition.geometry.start)
1450    
1451     # we use this to hold the base for the next loop through the
1452     # chunk's requests since we want the base to be the same for
1453     # all requests in any given growth iteration
1454     new_base = self.base
1455     last_pool = 0 # used to track changes to the pool across iterations
1456     while not self.done and self.pool and last_pool != self.pool:
1457     last_pool = self.pool # to keep from getting stuck
1458     self.base = new_base
1459     log.debug("%d partitions and %d (%dMB) left in chunk" %
1460     (self.remaining, self.pool,
1461     sectorsToSize(self.pool, self.sectorSize)))
1462     for p in self.requests:
1463     if p.done:
1464     continue
1465    
1466     # Each partition is allocated free sectors from the pool
1467     # based on the relative _base_ sizes of the remaining
1468     # growable partitions.
1469     share = p.base / float(self.base)
1470     growth = int(share * last_pool) # truncate, don't round
1471     p.growth += growth
1472     self.pool -= growth
1473     log.debug("adding %d (%dMB) to %d (%s)" %
1474     (growth,
1475     sectorsToSize(growth, self.sectorSize),
1476     p.partition.id, p.partition.name))
1477    
1478     new_base = self.trimOverGrownRequest(p, base=new_base)
1479     log.debug("new grow amount for partition %d (%s) is %d "
1480     "sectors, or %dMB" %
1481     (p.partition.id, p.partition.name, p.growth,
1482     sectorsToSize(p.growth, self.sectorSize)))
1483    
1484     if self.pool:
1485     # allocate any leftovers in pool to the first partition
1486     # that can still grow
1487     for p in self.requests:
1488     if p.done:
1489     continue
1490    
1491     p.growth += self.pool
1492     self.pool = 0
1493    
1494     self.trimOverGrownRequest(p)
1495     if self.pool == 0:
1496     break
1497    
1498    
1499     def getDiskChunks(disk, partitions, free):
1500     """ Return a list of Chunk instances representing a disk.
1501    
1502     Arguments:
1503    
1504     disk -- a StorageDevice with a DiskLabel format
1505     partitions -- list of PartitionDevice instances
1506     free -- list of parted.Geometry instances representing free space
1507    
1508     Partitions and free regions not on the specified disk are ignored.
1509    
1510     """
1511     # list of all new partitions on this disk
1512     disk_parts = [p for p in partitions if p.disk == disk and not p.exists]
1513     disk_free = [f for f in free if f.device.path == disk.path]
1514    
1515    
1516     chunks = [Chunk(f) for f in disk_free]
1517    
1518     for p in disk_parts:
1519     if p.isExtended:
1520     # handle extended partitions specially since they are
1521     # indeed very special
1522     continue
1523    
1524     for i, f in enumerate(disk_free):
1525     if f.contains(p.partedPartition.geometry):
1526     chunks[i].addRequest(Request(p))
1527     break
1528    
1529     return chunks
1530    
1531     def growPartitions(disks, partitions, free):
1532     """ Grow all growable partition requests.
1533    
1534     Partitions have already been allocated from chunks of free space on
1535     the disks. This function does not modify the ordering of partitions
1536     or the free chunks from which they are allocated.
1537    
1538     Free space within a given chunk is allocated to each growable
1539     partition allocated from that chunk in an amount corresponding to
1540     the ratio of that partition's base size to the sum of the base sizes
1541     of all growable partitions allocated from the chunk.
1542    
1543     Arguments:
1544    
1545     disks -- a list of all usable disks (DiskDevice instances)
1546     partitions -- a list of all partitions (PartitionDevice instances)
1547     free -- a list of all free regions (parted.Geometry instances)
1548     """
1549     log.debug("growPartitions: disks=%s, partitions=%s" %
1550     ([d.name for d in disks],
1551     ["%s(id %d)" % (p.name, p.id) for p in partitions]))
1552     all_growable = [p for p in partitions if p.req_grow]
1553     if not all_growable:
1554     log.debug("no growable partitions")
1555     return
1556    
1557     log.debug("growable partitions are %s" % [p.name for p in all_growable])
1558    
1559     for disk in disks:
1560     log.debug("growing partitions on %s" % disk.name)
1561     sector_size = disk.format.partedDevice.sectorSize
1562    
1563     # find any extended partition on this disk
1564     extended_geometry = getattr(disk.format.extendedPartition,
1565     "geometry",
1566     None) # parted.Geometry
1567    
1568     # list of free space regions on this disk prior to partition allocation
1569     disk_free = [f for f in free if f.device.path == disk.path]
1570     if not disk_free:
1571     log.debug("no free space on %s" % disk.name)
1572     continue
1573    
1574     chunks = getDiskChunks(disk, partitions, disk_free)
1575     log.debug("disk %s has %d chunks" % (disk.name, len(chunks)))
1576     # grow the partitions in each chunk as a group
1577     for chunk in chunks:
1578     if not chunk.hasGrowable:
1579     # no growable partitions in this chunk
1580     continue
1581    
1582     chunk.growRequests()
1583    
1584     # recalculate partition geometries
1585     disklabel = disk.format
1586     start = chunk.geometry.start
1587     # align start sector as needed
1588     if not disklabel.alignment.isAligned(chunk.geometry, start):
1589     start = disklabel.alignment.alignUp(chunk.geometry, start)
1590     new_partitions = []
1591     for p in chunk.requests:
1592     ptype = p.partition.partedPartition.type
1593     log.debug("partition %s (%d): %s" % (p.partition.name,
1594     p.partition.id, ptype))
1595     if ptype == parted.PARTITION_EXTENDED:
1596     continue
1597    
1598     # XXX since we need one metadata sector before each
1599     # logical partition we burn one logical block to
1600     # safely align the start of each logical partition
1601     if ptype == parted.PARTITION_LOGICAL:
1602     start += disklabel.alignment.grainSize
1603    
1604     old_geometry = p.partition.partedPartition.geometry
1605     new_length = p.base + p.growth
1606     end = start + new_length - 1
1607     # align end sector as needed
1608     if not disklabel.endAlignment.isAligned(chunk.geometry, end):
1609     end = disklabel.endAlignment.alignDown(chunk.geometry, end)
1610     new_geometry = parted.Geometry(device=disklabel.partedDevice,
1611     start=start,
1612     end=end)
1613     log.debug("new geometry for %s: %s" % (p.partition.name,
1614     new_geometry))
1615     start = end + 1
1616     new_partition = parted.Partition(disk=disklabel.partedDisk,
1617     type=ptype,
1618     geometry=new_geometry)
1619     new_partitions.append((new_partition, p.partition))
1620    
1621     # remove all new partitions from this chunk
1622     removeNewPartitions([disk], [r.partition for r in chunk.requests])
1623     log.debug("back from removeNewPartitions")
1624    
1625     # adjust the extended partition as needed
1626     # we will ony resize an extended partition that we created
1627     log.debug("extended: %s" % extended_geometry)
1628     if extended_geometry and \
1629     chunk.geometry.contains(extended_geometry):
1630     log.debug("setting up new geometry for extended on %s" % disk.name)
1631     ext_start = 0
1632     for (partition, device) in new_partitions:
1633     if partition.type != parted.PARTITION_LOGICAL:
1634     continue
1635    
1636     if not ext_start or partition.geometry.start < ext_start:
1637     # account for the logical block difference in start
1638     # sector for the extended -v- first logical
1639     # (partition.geometry.start is already aligned)
1640     ext_start = partition.geometry.start - disklabel.alignment.grainSize
1641    
1642     new_geometry = parted.Geometry(device=disklabel.partedDevice,
1643     start=ext_start,
1644     end=chunk.geometry.end)
1645     log.debug("new geometry for extended: %s" % new_geometry)
1646     new_extended = parted.Partition(disk=disklabel.partedDisk,
1647     type=parted.PARTITION_EXTENDED,
1648     geometry=new_geometry)
1649     ptypes = [p.type for (p, d) in new_partitions]
1650     for pt_idx, ptype in enumerate(ptypes):
1651     if ptype == parted.PARTITION_LOGICAL:
1652     new_partitions.insert(pt_idx, (new_extended, None))
1653     break
1654    
1655     # add the partitions with their new geometries to the disk
1656     for (partition, device) in new_partitions:
1657     if device:
1658     name = device.name
1659     else:
1660     # If there was no extended partition on this disk when
1661     # doPartitioning was called we won't have a
1662     # PartitionDevice instance for it.
1663     name = partition.getDeviceNodeName()
1664    
1665     log.debug("setting %s new geometry: %s" % (name,
1666     partition.geometry))
1667     constraint = parted.Constraint(exactGeom=partition.geometry)
1668     disklabel.partedDisk.addPartition(partition=partition,
1669     constraint=constraint)
1670     path = partition.path
1671     if device:
1672     # set the device's name
1673     device.partedPartition = partition
1674     # without this, the path attr will be a basename. eek.
1675     device.disk = disk
1676    
1677     # make sure we store the disk's version of the partition
1678     newpart = disklabel.partedDisk.getPartitionByPath(path)
1679     device.partedPartition = newpart
1680    
1681    
1682     def lvCompare(lv1, lv2):
1683     """ More specifically defined lvs come first.
1684    
1685     < 1 => x < y
1686     0 => x == y
1687     > 1 => x > y
1688     """
1689     ret = 0
1690    
1691     # larger requests go to the front of the list
1692     ret -= cmp(lv1.size, lv2.size) * 100
1693    
1694     # fixed size requests to the front
1695     ret += cmp(lv1.req_grow, lv2.req_grow) * 50
1696    
1697     # potentially larger growable requests go to the front
1698     if lv1.req_grow and lv2.req_grow:
1699     if not lv1.req_max_size and lv2.req_max_size:
1700     ret -= 25
1701     elif lv1.req_max_size and not lv2.req_max_size:
1702     ret += 25
1703     else:
1704     ret -= cmp(lv1.req_max_size, lv2.req_max_size) * 25
1705    
1706     if ret > 0:
1707     ret = 1
1708     elif ret < 0:
1709     ret = -1
1710    
1711     return ret
1712    
1713     def growLVM(storage):
1714     """ Grow LVs according to the sizes of the PVs. """
1715     for vg in storage.vgs:
1716     total_free = vg.freeSpace
1717     if total_free < 0:
1718     # by now we have allocated the PVs so if there isn't enough
1719     # space in the VG we have a real problem
1720     raise PartitioningError("not enough space for LVM requests")
1721     elif not total_free:
1722     log.debug("vg %s has no free space" % vg.name)
1723     continue
1724    
1725     log.debug("vg %s: %dMB free ; lvs: %s" % (vg.name, vg.freeSpace,
1726     [l.lvname for l in vg.lvs]))
1727    
1728     # figure out how much to grow each LV
1729     grow_amounts = {}
1730     lv_total = vg.size - total_free
1731     log.debug("used: %dMB ; vg.size: %dMB" % (lv_total, vg.size))
1732    
1733     # This first loop is to calculate percentage-based growth
1734     # amounts. These are based on total free space.
1735     lvs = vg.lvs
1736     lvs.sort(cmp=lvCompare)
1737     for lv in lvs:
1738     if not lv.req_grow or not lv.req_percent:
1739     continue
1740    
1741     portion = (lv.req_percent * 0.01)
1742     grow = portion * vg.freeSpace
1743     new_size = lv.req_size + grow
1744     if lv.req_max_size and new_size > lv.req_max_size:
1745     grow -= (new_size - lv.req_max_size)
1746    
1747     if lv.format.maxSize and lv.format.maxSize < new_size:
1748     grow -= (new_size - lv.format.maxSize)
1749    
1750     # clamp growth amount to a multiple of vg extent size
1751     grow_amounts[lv.name] = vg.align(grow)
1752     total_free -= grow
1753     lv_total += grow
1754    
1755     # This second loop is to calculate non-percentage-based growth
1756     # amounts. These are based on free space remaining after
1757     # calculating percentage-based growth amounts.
1758    
1759     # keep a tab on space not allocated due to format or requested
1760     # maximums -- we'll dole it out to subsequent requests
1761     leftover = 0
1762     for lv in lvs:
1763     log.debug("checking lv %s: req_grow: %s ; req_percent: %s"
1764     % (lv.name, lv.req_grow, lv.req_percent))
1765     if not lv.req_grow or lv.req_percent:
1766     continue
1767    
1768     portion = float(lv.req_size) / float(lv_total)
1769     grow = portion * total_free
1770     log.debug("grow is %dMB" % grow)
1771    
1772     todo = lvs[lvs.index(lv):]
1773     unallocated = reduce(lambda x,y: x+y,
1774     [l.req_size for l in todo
1775     if l.req_grow and not l.req_percent])
1776     extra_portion = float(lv.req_size) / float(unallocated)
1777     extra = extra_portion * leftover
1778     log.debug("%s getting %dMB (%d%%) of %dMB leftover space"
1779     % (lv.name, extra, extra_portion * 100, leftover))
1780     leftover -= extra
1781     grow += extra
1782     log.debug("grow is now %dMB" % grow)
1783     max_size = lv.req_size + grow
1784     if lv.req_max_size and max_size > lv.req_max_size:
1785     max_size = lv.req_max_size
1786    
1787     if lv.format.maxSize and max_size > lv.format.maxSize:
1788     max_size = lv.format.maxSize
1789    
1790     log.debug("max size is %dMB" % max_size)
1791     max_size = max_size
1792     leftover += (lv.req_size + grow) - max_size
1793     grow = max_size - lv.req_size
1794     log.debug("lv %s gets %dMB" % (lv.name, vg.align(grow)))
1795     grow_amounts[lv.name] = vg.align(grow)
1796    
1797     if not grow_amounts:
1798     log.debug("no growable lvs in vg %s" % vg.name)
1799     continue
1800    
1801     # now grow the lvs by the amounts we've calculated above
1802     for lv in lvs:
1803     if lv.name not in grow_amounts.keys():
1804     continue
1805     lv.size += grow_amounts[lv.name]
1806    
1807     # now there shouldn't be any free space left, but if there is we
1808     # should allocate it to one of the LVs
1809     vg_free = vg.freeSpace
1810     log.debug("vg %s has %dMB free" % (vg.name, vg_free))
1811     if vg_free:
1812     for lv in lvs:
1813     if not lv.req_grow:
1814     continue
1815    
1816     if lv.req_percent > 0:
1817     continue
1818    
1819     if lv.req_max_size and lv.size == lv.req_max_size:
1820     continue
1821    
1822     if lv.format.maxSize and lv.size == lv.format.maxSize:
1823     continue
1824    
1825     # first come, first served
1826     projected = lv.size + vg.freeSpace
1827     if lv.req_max_size and projected > lv.req_max_size:
1828     projected = lv.req_max_size
1829    
1830     if lv.format.maxSize and projected > lv.format.maxSize:
1831     projected = lv.format.maxSize
1832    
1833     log.debug("giving leftover %dMB to %s" % (projected - lv.size,
1834     lv.name))
1835     lv.size = projected
1836    

admin@koozali.org
ViewVC Help
Powered by ViewVC 1.2.1 RSS 2.0 feed