3 # Copyright (c) 2009, 2010, 2011 Intel, Inc.
4 # Copyright (c) 2007, 2008 Red Hat, Inc.
5 # Copyright (c) 2008 Daniel P. Berrange
6 # Copyright (c) 2008 David P. Huff
8 # This program is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation; version 2 of the License
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc., 59
19 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 from mic.utils import runner
25 from mic.utils.errors import MountError
26 from mic.utils.fs_related import *
28 # Overhead of the MBR partitioning scheme (just one sector)
30 # Overhead of the GPT partitioning scheme
33 # Size of a sector in bytes
36 class PartitionedMount(Mount):
37 def __init__(self, mountdir, skipformat = False):
38 Mount.__init__(self, mountdir)
44 self.unmountOrder = []
45 self.parted=find_binary_path("parted")
46 self.kpartx=find_binary_path("kpartx")
47 self.mkswap=find_binary_path("mkswap")
49 self.mountcmd=find_binary_path("mount")
50 self.umountcmd=find_binary_path("umount")
51 self.skipformat = skipformat
52 self.snapshot_created = self.skipformat
53 # Size of a sector used in calculations
54 self.sector_size = SECTOR_SIZE
55 self._partitions_layed_out = False
57 def __add_disk(self, disk_name):
58 """ Add a disk 'disk_name' to the internal list of disks. Note,
59 'disk_name' is the name of the disk in the target system
62 if disk_name in self.disks:
63 # We already have this disk
66 assert not self._partitions_layed_out
68 self.disks[disk_name] = \
69 { 'disk': None, # Disk object
70 'mapped': False, # True if kpartx mapping exists
71 'numpart': 0, # Number of allocate partitions
72 'partitions': [], # Indexes to self.partitions
73 # Partitions with part num higher than 3 will
74 # be put to the extended partition.
75 'extended': 0, # Size of extended partition
76 'offset': 0, # Offset of next partition (in sectors)
77 # Minimum required disk size to fit all partitions (in bytes)
79 'ptable_format': "msdos" } # Partition table format
81 def add_disk(self, disk_name, disk_obj):
82 """ Add a disk object which have to be partitioned. More than one disk
83 can be added. In case of multiple disks, disk partitions have to be
84 added for each disk separately with 'add_partition()". """
86 self.__add_disk(disk_name)
87 self.disks[disk_name]['disk'] = disk_obj
89 def __add_partition(self, part):
90 """ This is a helper function for 'add_partition()' which adds a
91 partition to the internal list of partitions. """
93 assert not self._partitions_layed_out
95 self.partitions.append(part)
96 self.__add_disk(part['disk_name'])
98 def add_partition(self, size, disk_name, mountpoint, fstype = None,
99 label=None, fsopts = None, boot = False, align = None):
100 """ Add the next partition. Prtitions have to be added in the
101 first-to-last order. """
103 # Converting MB to sectors for parted
104 size = size * 1024 * 1024 / self.sector_size
106 # We need to handle subvolumes for btrfs
107 if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
108 self.btrfscmd=find_binary_path("btrfs")
110 opts = fsopts.split(",")
112 if opt.find("subvol=") != -1:
113 subvol = opt.replace("subvol=", "").strip()
116 raise MountError("No subvolume: %s" % fsopts)
117 self.subvolumes.append({'size': size, # In sectors
118 'mountpoint': mountpoint, # Mount relative to chroot
119 'fstype': fstype, # Filesystem type
120 'fsopts': fsopts, # Filesystem mount options
121 'disk_name': disk, # physical disk name holding partition
122 'device': None, # kpartx device node for partition
123 'mount': None, # Mount object
124 'subvol': subvol, # Subvolume name
125 'boot': boot, # Bootable flag
126 'mounted': False # Mount flag
129 # We still need partition for "/" or non-subvolume
130 if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
131 # Don't need subvolume for "/" because it will be set as default subvolume
132 if fsopts and fsopts.find("subvol=") != -1:
133 opts = fsopts.split(",")
135 if opt.strip().startswith("subvol="):
138 fsopts = ",".join(opts)
140 part = { 'size': size, # In sectors
141 'mountpoint': mountpoint, # Mount relative to chroot
142 'fstype': fstype, # Filesystem type
143 'fsopts': fsopts, # Filesystem mount options
144 'label': label, # Partition label
145 'disk_name': disk_name, # physical disk name holding partition
146 'device': None, # kpartx device node for partition
147 'mount': None, # Mount object
148 'num': None, # Partition number
149 'boot': boot, # Bootable flag
150 'align': align } # Partition alignment
152 self.__add_partition(part)
154 def __create_part_to_image(self, device, parttype, fstype, start, size):
155 # Start is included to the size so we need to substract one from the end.
157 msger.debug("Added '%s' part at Sector %d with size %d sectors" %
158 (parttype, start, end))
159 part_cmd = [self.parted, "-s", device, "unit", "s", "mkpart", parttype]
161 part_cmd.extend([fstype])
162 part_cmd.extend(["%d" % start, "%d" % end])
164 msger.debug(part_cmd)
165 rc, out = runner.runtool(part_cmd, catch=3)
168 msger.debug('"parted" output: %s' % out)
171 def layout_partitions(self, ptable_format = "msdos"):
172 """ Layout the partitions, meaning calculate the position of every
173 partition on the disk. The 'ptable_format' parameter defines the
174 partition table format, and may be either "msdos" or "gpt". """
176 msger.debug("Assigning %s partitions to disks" % ptable_format)
178 if ptable_format not in ('msdos', 'gpt'):
179 raise MountError("Unknown partition table format '%s', supported " \
180 "formats are: 'msdos' and 'gpt'" % ptable_format)
182 if self._partitions_layed_out:
185 self._partitions_layed_out = True
187 # Go through partitions in the order they are added in .ks file
188 for n in range(len(self.partitions)):
189 p = self.partitions[n]
191 if not self.disks.has_key(p['disk_name']):
192 raise MountError("No disk %s for partition %s" \
193 % (p['disk_name'], p['mountpoint']))
195 # Get the disk where the partition is located
196 d = self.disks[p['disk_name']]
198 d['ptable_format'] = ptable_format
200 if d['numpart'] == 1:
201 if ptable_format == "msdos":
202 overhead = MBR_OVERHEAD
204 overhead = GPT_OVERHEAD
206 # Skip one sector required for the partitioning scheme overhead
207 d['offset'] += overhead
208 # Steal few sectors from the first partition to offset for the
209 # partitioning overhead
210 p['size'] -= overhead
213 # If not first partition and we do have alignment set we need
214 # to align the partition.
215 # FIXME: This leaves a empty spaces to the disk. To fill the
216 # gaps we could enlargea the previous partition?
218 # Calc how much the alignment is off.
219 align_sectors = d['offset'] % (p['align'] * 1024 / self.sector_size)
220 # We need to move forward to the next alignment point
221 align_sectors = (p['align'] * 1024 / self.sector_size) - align_sectors
223 msger.debug("Realignment for %s%s with %s sectors, original"
224 " offset %s, target alignment is %sK." %
225 (p['disk_name'], d['numpart'], align_sectors,
226 d['offset'], p['align']))
228 # p['size'] already converted in secctors
229 if p['size'] <= align_sectors:
230 raise MountError("Partition for %s is too small to handle "
231 "the alignment change." % p['mountpoint'])
233 # increase the offset so we actually start the partition on right alignment
234 d['offset'] += align_sectors
237 # Increase allocation of extended partition to hold this partition
238 d['extended'] += p['size']
239 p['type'] = 'logical'
240 p['num'] = d['numpart'] + 1
242 p['type'] = 'primary'
243 p['num'] = d['numpart']
245 p['start'] = d['offset']
246 d['offset'] += p['size']
247 d['partitions'].append(n)
248 msger.debug("Assigned %s to %s%d at Sector %d with size %d sectors "
249 "/ %d bytes." % (p['mountpoint'], p['disk_name'],
250 p['num'], p['start'], p['size'],
251 p['size'] * self.sector_size))
253 # Once all the partitions have been layed out, we can calculate the
254 # minumim disk sizes.
255 for disk_name, disk in self.disks.items():
256 last_partition = self.partitions[disk['partitions'][-1]]
257 disk['min_size'] = last_partition['start'] + last_partition['size']
259 if disk['ptable_format'] == 'gpt':
260 # Account for the backup partition table at the end of the disk
261 disk['min_size'] += GPT_OVERHEAD
263 disk['min_size'] *= self.sector_size
265 def __format_disks(self):
266 self.layout_partitions()
269 msger.debug("Skipping disk format, because skipformat flag is set.")
272 for dev in self.disks.keys():
274 msger.debug("Initializing partition table for %s" % (d['disk'].device))
275 rc, out = runner.runtool([self.parted, "-s", d['disk'].device, "mklabel", "msdos"], catch=3)
278 msger.debug('"parted" output: %s' % out)
281 # NOTE: We don't throw exception when return code is not 0, because
282 # parted always fails to reload part table with loop devices.
283 # This prevents us from distinguishing real errors based on return code.
284 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition-table for disk '%s'." % (rc, d['disk'].device))
286 msger.debug("Creating partitions")
288 for p in self.partitions:
289 d = self.disks[p['disk_name']]
291 self.__create_part_to_image(d['disk'].device, "extended",None,p['start'], d['extended'])
293 if p['fstype'] == "swap":
294 parted_fs_type = "linux-swap"
295 elif p['fstype'] == "vfat":
296 parted_fs_type = "fat32"
297 elif p['fstype'] == "msdos":
298 parted_fs_type = "fat16"
300 # Type for ext2/ext3/ext4/btrfs
301 parted_fs_type = "ext2"
303 # Boot ROM of OMAP boards require vfat boot partition to have an
304 # even number of sectors.
305 if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat","msdos"] and p['size'] % 2:
306 msger.debug("Substracting one sector from '%s' partition to get even number of sectors for the partition." % (p['mountpoint']))
309 ret = self.__create_part_to_image(d['disk'].device, p['type'],
310 parted_fs_type, p['start'],
314 # NOTE: We don't throw exception when return code is not 0, because
315 # parted always fails to reload part table with loop devices.
316 # This prevents us from distinguishing real errors based on return code.
317 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition '%s' for disk '%s'." % (ret, p['mountpoint'], d['disk'].device))
320 msger.debug("Setting boot flag for partition '%s' on disk '%s'." % (p['num'],d['disk'].device))
321 boot_cmd = [self.parted, "-s", d['disk'].device, "set", "%d" % p['num'], "boot", "on"]
322 msger.debug(boot_cmd)
323 rc = runner.show(boot_cmd)
326 # NOTE: We don't throw exception when return code is not 0, because
327 # parted always fails to reload part table with loop devices.
328 # This prevents us from distinguishing real errors based on return code.
329 msger.warning("parted returned '%s' instead of 0 when adding boot flag for partition '%s' disk '%s'." % (rc,p['num'],d['disk'].device))
331 def __map_partitions(self):
332 """Load it if dm_snapshot isn't loaded. """
333 load_module("dm_snapshot")
335 for dev in self.disks.keys():
340 msger.debug("Running kpartx on %s" % d['disk'].device )
341 rc, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
342 kpartxOutput = kpartxOutput.splitlines()
345 raise MountError("Failed to query partition mapping for '%s'" %
348 # Strip trailing blank and mask verbose output
350 while i < len(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
352 kpartxOutput = kpartxOutput[i:]
354 # Quick sanity check that the number of partitions matches
355 # our expectation. If it doesn't, someone broke the code
357 if len(kpartxOutput) != d['numpart']:
358 raise MountError("Unexpected number of partitions from kpartx: %d != %d" %
359 (len(kpartxOutput), d['numpart']))
361 for i in range(len(kpartxOutput)):
362 line = kpartxOutput[i]
363 newdev = line.split()[0]
364 mapperdev = "/dev/mapper/" + newdev
365 loopdev = d['disk'].device + newdev[-1]
367 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
368 pnum = d['partitions'][i]
369 self.partitions[pnum]['device'] = loopdev
371 # grub's install wants partitions to be named
372 # to match their parent device + partition num
373 # kpartx doesn't work like this, so we add compat
374 # symlinks to point to /dev/mapper
375 if os.path.lexists(loopdev):
377 os.symlink(mapperdev, loopdev)
379 msger.debug("Adding partx mapping for %s" % d['disk'].device)
380 rc = runner.show([self.kpartx, "-v", "-a", d['disk'].device])
383 # Make sure that the device maps are also removed on error case.
384 # The d['mapped'] isn't set to True if the kpartx fails so
385 # failed mapping will not be cleaned on cleanup either.
386 runner.quiet([self.kpartx, "-d", d['disk'].device])
387 raise MountError("Failed to map partitions for '%s'" %
392 def __unmap_partitions(self):
393 for dev in self.disks.keys():
398 msger.debug("Removing compat symlinks")
399 for pnum in d['partitions']:
400 if self.partitions[pnum]['device'] != None:
401 os.unlink(self.partitions[pnum]['device'])
402 self.partitions[pnum]['device'] = None
404 msger.debug("Unmapping %s" % d['disk'].device)
405 rc = runner.quiet([self.kpartx, "-d", d['disk'].device])
407 raise MountError("Failed to unmap partitions for '%s'" %
412 def __calculate_mountorder(self):
413 msger.debug("Calculating mount order")
414 for p in self.partitions:
415 self.mountOrder.append(p['mountpoint'])
416 self.unmountOrder.append(p['mountpoint'])
418 self.mountOrder.sort()
419 self.unmountOrder.sort()
420 self.unmountOrder.reverse()
425 self.__unmap_partitions()
426 for dev in self.disks.keys():
434 self.__unmount_subvolumes()
435 for mp in self.unmountOrder:
439 for p1 in self.partitions:
440 if p1['mountpoint'] == mp:
444 if p['mount'] != None:
446 # Create subvolume snapshot here
447 if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and not self.snapshot_created:
448 self.__create_subvolume_snapshots(p, p["mount"])
455 def __get_subvolume_id(self, rootpath, subvol):
456 if not self.btrfscmd:
457 self.btrfscmd=find_binary_path("btrfs")
458 argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
460 rc, out = runner.runtool(argv)
464 raise MountError("Failed to get subvolume id from %s', return code: %d." % (rootpath, rc))
467 for line in out.splitlines():
468 if line.endswith(" path %s" % subvol):
469 subvolid = line.split()[1]
470 if not subvolid.isdigit():
471 raise MountError("Invalid subvolume id: %s" % subvolid)
472 subvolid = int(subvolid)
476 def __create_subvolume_metadata(self, p, pdisk):
477 if len(self.subvolumes) == 0:
480 argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
481 rc, out = runner.runtool(argv)
485 raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
487 subvolid_items = out.splitlines()
488 subvolume_metadata = ""
489 for subvol in self.subvolumes:
490 for line in subvolid_items:
491 if line.endswith(" path %s" % subvol["subvol"]):
492 subvolid = line.split()[1]
493 if not subvolid.isdigit():
494 raise MountError("Invalid subvolume id: %s" % subvolid)
496 subvolid = int(subvolid)
497 opts = subvol["fsopts"].split(",")
499 if opt.strip().startswith("subvol="):
502 fsopts = ",".join(opts)
503 subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"], subvol['mountpoint'], fsopts)
505 if subvolume_metadata:
506 fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
507 fd.write(subvolume_metadata)
510 def __get_subvolume_metadata(self, p, pdisk):
511 subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
512 if not os.path.exists(subvolume_metadata_file):
515 fd = open(subvolume_metadata_file, "r")
519 for line in content.splitlines():
520 items = line.split("\t")
521 if items and len(items) == 4:
522 self.subvolumes.append({'size': 0, # In sectors
523 'mountpoint': items[2], # Mount relative to chroot
524 'fstype': "btrfs", # Filesystem type
525 'fsopts': items[3] + ",subvol=%s" % items[1], # Filesystem mount options
526 'disk_name': p['disk_name'], # physical disk name holding partition
527 'device': None, # kpartx device node for partition
528 'mount': None, # Mount object
529 'subvol': items[1], # Subvolume name
530 'boot': False, # Bootable flag
531 'mounted': False # Mount flag
534 def __create_subvolumes(self, p, pdisk):
535 """ Create all the subvolumes. """
537 for subvol in self.subvolumes:
538 argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
540 rc = runner.show(argv)
542 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
544 # Set default subvolume, subvolume for "/" is default
546 for subvolume in self.subvolumes:
547 if subvolume["mountpoint"] == "/" and p['disk_name'] == subvolume['disk_name']:
552 # Get default subvolume id
553 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
554 # Set default subvolume
556 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" % subvolid, pdisk.mountdir])
558 raise MountError("Failed to set default subvolume id: %d', return code: %d." % (subvolid, rc))
560 self.__create_subvolume_metadata(p, pdisk)
562 def __mount_subvolumes(self, p, pdisk):
565 self.__get_subvolume_metadata(p, pdisk)
566 # Set default mount options
567 if len(self.subvolumes) != 0:
568 for subvol in self.subvolumes:
569 if subvol["mountpoint"] == p["mountpoint"] == "/":
570 opts = subvol["fsopts"].split(",")
572 if opt.strip().startswith("subvol="):
575 pdisk.fsopts = ",".join(opts)
578 if len(self.subvolumes) == 0:
579 # Return directly if no subvolumes
582 # Remount to make default subvolume mounted
583 rc = runner.show([self.umountcmd, pdisk.mountdir])
585 raise MountError("Failed to umount %s" % pdisk.mountdir)
587 rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
589 raise MountError("Failed to umount %s" % pdisk.mountdir)
591 for subvol in self.subvolumes:
592 if subvol["mountpoint"] == "/":
594 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
596 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
598 # Replace subvolume name with subvolume ID
599 opts = subvol["fsopts"].split(",")
601 if opt.strip().startswith("subvol="):
605 opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
606 fsopts = ",".join(opts)
607 subvol['fsopts'] = fsopts
608 mountpoint = self.mountdir + subvol['mountpoint']
610 rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
612 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
613 subvol["mounted"] = True
615 def __unmount_subvolumes(self):
616 """ It may be called multiple times, so we need to chekc if it is still mounted. """
617 for subvol in self.subvolumes:
618 if subvol["mountpoint"] == "/":
620 if not subvol["mounted"]:
622 mountpoint = self.mountdir + subvol['mountpoint']
623 rc = runner.show([self.umountcmd, mountpoint])
625 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
626 subvol["mounted"] = False
628 def __create_subvolume_snapshots(self, p, pdisk):
631 if self.snapshot_created:
634 # Remount with subvolid=0
635 rc = runner.show([self.umountcmd, pdisk.mountdir])
637 raise MountError("Failed to umount %s" % pdisk.mountdir)
639 mountopts = pdisk.fsopts + ",subvolid=0"
641 mountopts = "subvolid=0"
642 rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
644 raise MountError("Failed to umount %s" % pdisk.mountdir)
646 # Create all the subvolume snapshots
647 snapshotts = time.strftime("%Y%m%d-%H%M")
648 for subvol in self.subvolumes:
649 subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
650 snapshotpath = subvolpath + "_%s-1" % snapshotts
651 rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
653 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code: %d." % (snapshotpath, subvolpath, rc))
655 self.snapshot_created = True
658 for dev in self.disks.keys():
662 self.__format_disks()
663 self.__map_partitions()
664 self.__calculate_mountorder()
666 for mp in self.mountOrder:
668 for p1 in self.partitions:
669 if p1['mountpoint'] == mp:
674 if p['mountpoint'] == "/":
675 p['label'] = 'platform'
677 p['label'] = mp.split('/')[-1]
681 p['uuid'] = str(uuid.uuid1())
682 runner.show([self.mkswap,
689 if p['mountpoint'] == "/":
691 if p['fstype'] == "vfat" or p['fstype'] == "msdos":
692 myDiskMount = VfatDiskMount
693 elif p['fstype'] in ("ext2", "ext3", "ext4"):
694 myDiskMount = ExtDiskMount
695 elif p['fstype'] == "btrfs":
696 myDiskMount = BtrfsDiskMount
698 raise MountError("Fail to support file system " + p['fstype'])
700 if p['fstype'] == "btrfs" and not p['fsopts']:
701 p['fsopts'] = "subvolid=0"
703 pdisk = myDiskMount(RawDisk(p['size'] * self.sector_size, p['device']),
704 self.mountdir + p['mountpoint'],
710 fsopts = p['fsopts'])
711 pdisk.mount(pdisk.fsopts)
712 if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
713 if not self.skipformat:
714 self.__create_subvolumes(p, pdisk)
715 self.__mount_subvolumes(p, pdisk)
717 p['uuid'] = pdisk.uuid
719 def resparse(self, size = None):
720 # Can't re-sparse a disk image - too hard