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

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

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


Revision 1.1 - (show 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 # 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