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 # Lenght of MBR in sectors
31 # Size of a sector in bytes
34 class PartitionedMount(Mount):
35 def __init__(self, mountdir, skipformat = False):
36 Mount.__init__(self, mountdir)
42 self.unmountOrder = []
43 self.parted=find_binary_path("parted")
44 self.kpartx=find_binary_path("kpartx")
45 self.mkswap=find_binary_path("mkswap")
47 self.mountcmd=find_binary_path("mount")
48 self.umountcmd=find_binary_path("umount")
49 self.skipformat = skipformat
50 self.snapshot_created = self.skipformat
51 # Size of a sector used in calculations
52 self.sector_size = SECTOR_SIZE
53 self._partitions_layed_out = False
55 def __add_disk(self, disk_name):
56 """ Add a disk 'disk_name' to the internal list of disks. Note,
57 'disk_name' is the name of the disk in the target system
60 if disk_name in self.disks:
61 # We already have this disk
64 assert not self._partitions_layed_out
66 self.disks[disk_name] = \
67 { 'disk': None, # Disk object
68 'mapped': False, # True if kpartx mapping exists
69 'numpart': 0, # Number of allocate partitions
70 'partitions': [], # Indexes to self.partitions
71 # Partitions with part num higher than 3 will
72 # be put to the extended partition.
73 'extended': 0, # Size of extended partition
74 'offset': 0, # Offset of next partition (in sectors)
75 # Minimum required disk size to fit all partitions (in bytes)
78 def add_disks(self, disks):
79 """ Add the disks which have to be partitioned. """
81 for name in disks.keys():
83 self.disks[name]['disk'] = disks[name]
85 def __add_partition(self, part):
86 """ This is a helper function for 'add_partition()' which adds a
87 partition to the internal list of partitions. """
89 assert not self._partitions_layed_out
91 self.partitions.append(part)
92 self.__add_disk(part['disk_name'])
94 def add_partition(self, size, disk_name, mountpoint, fstype = None,
95 label=None, fsopts = None, boot = False, align = None):
96 # Converting MB to sectors for parted
97 size = size * 1024 * 1024 / self.sector_size
99 """ We need to handle subvolumes for btrfs """
100 if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
101 self.btrfscmd=find_binary_path("btrfs")
103 opts = fsopts.split(",")
105 if opt.find("subvol=") != -1:
106 subvol = opt.replace("subvol=", "").strip()
109 raise MountError("No subvolume: %s" % fsopts)
110 self.subvolumes.append({'size': size, # In sectors
111 'mountpoint': mountpoint, # Mount relative to chroot
112 'fstype': fstype, # Filesystem type
113 'fsopts': fsopts, # Filesystem mount options
114 'disk_name': disk, # physical disk name holding partition
115 'device': None, # kpartx device node for partition
116 'mount': None, # Mount object
117 'subvol': subvol, # Subvolume name
118 'boot': boot, # Bootable flag
119 'mounted': False # Mount flag
122 """ We still need partition for "/" or non-subvolume """
123 if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
124 """ Don't need subvolume for "/" because it will be set as default subvolume """
125 if fsopts and fsopts.find("subvol=") != -1:
126 opts = fsopts.split(",")
128 if opt.strip().startswith("subvol="):
131 fsopts = ",".join(opts)
133 part = { 'size': size, # In sectors
134 'mountpoint': mountpoint, # Mount relative to chroot
135 'fstype': fstype, # Filesystem type
136 'fsopts': fsopts, # Filesystem mount options
137 'label': label, # Partition label
138 'disk_name': disk_name, # physical disk name holding partition
139 'device': None, # kpartx device node for partition
140 'mount': None, # Mount object
141 'num': None, # Partition number
142 'boot': boot, # Bootable flag
143 'align': align } # Partition alignment
145 self.__add_partition(part)
147 def __create_part_to_image(self, device, parttype, fstype, start, size):
148 # Start is included to the size so we need to substract one from the end.
150 msger.debug("Added '%s' part at Sector %d with size %d sectors" %
151 (parttype, start, end))
152 part_cmd = [self.parted, "-s", device, "unit", "s", "mkpart", parttype]
154 part_cmd.extend([fstype])
155 part_cmd.extend(["%d" % start, "%d" % end])
157 msger.debug(part_cmd)
158 rc, out = runner.runtool(part_cmd, catch=3)
161 msger.debug('"parted" output: %s' % out)
164 def layout_partitions(self):
165 """ Layout the partitions, meaning calculate the position of every
166 partition on the disk. """
168 msger.debug("Assigning partitions to disks")
170 if self._partitions_layed_out:
173 self._partitions_layed_out = True
175 # Go through partitions in the order they are added in .ks file
176 for n in range(len(self.partitions)):
177 p = self.partitions[n]
179 if not self.disks.has_key(p['disk_name']):
180 raise MountError("No disk %s for partition %s" \
181 % (p['disk_name'], p['mountpoint']))
183 # Get the disk where the partition is located
184 d = self.disks[p['disk_name']]
187 if d['numpart'] == 1:
188 # Skip one sector required for the MBR
189 d['offset'] += MBR_SECTOR_LEN
190 # Steal one sector from the first partition to offset for the
192 p['size'] -= MBR_SECTOR_LEN
195 # If not first partition and we do have alignment set we need
196 # to align the partition.
197 # FIXME: This leaves a empty spaces to the disk. To fill the
198 # gaps we could enlargea the previous partition?
200 # Calc how much the alignment is off.
201 align_sectors = d['offset'] % (p['align'] * 1024 / self.sector_size)
202 # We need to move forward to the next alignment point
203 align_sectors = (p['align'] * 1024 / self.sector_size) - align_sectors
205 msger.debug("Realignment for %s%s with %s sectors, original"
206 " offset %s, target alignment is %sK." %
207 (p['disk_name'], d['numpart'], align_sectors,
208 d['offset'], p['align']))
210 # p['size'] already converted in secctors
211 if p['size'] <= align_sectors:
212 raise MountError("Partition for %s is too small to handle "
213 "the alignment change." % p['mountpoint'])
215 # increase the offset so we actually start the partition on right alignment
216 d['offset'] += align_sectors
219 # Increase allocation of extended partition to hold this partition
220 d['extended'] += p['size']
221 p['type'] = 'logical'
222 p['num'] = d['numpart'] + 1
224 p['type'] = 'primary'
225 p['num'] = d['numpart']
227 p['start'] = d['offset']
228 d['offset'] += p['size']
229 d['partitions'].append(n)
230 msger.debug("Assigned %s to %s%d at Sector %d with size %d sectors "
231 "/ %d bytes." % (p['mountpoint'], p['disk_name'],
232 p['num'], p['start'], p['size'],
233 p['size'] * self.sector_size))
235 # Once all the partitions have been layed out, we can calculate the
236 # minumim disk sizes.
237 for disk_name, disk in self.disks.items():
238 last_partition = self.partitions[disk['partitions'][-1]]
239 disk['min_size'] = last_partition['start'] + last_partition['size']
240 disk['min_size'] *= self.sector_size
242 def __format_disks(self):
243 self.layout_partitions()
246 msger.debug("Skipping disk format, because skipformat flag is set.")
249 for dev in self.disks.keys():
251 msger.debug("Initializing partition table for %s" % (d['disk'].device))
252 rc, out = runner.runtool([self.parted, "-s", d['disk'].device, "mklabel", "msdos"], catch=3)
255 msger.debug('"parted" output: %s' % out)
258 # NOTE: We don't throw exception when return code is not 0, because
259 # parted always fails to reload part table with loop devices.
260 # This prevents us from distinguishing real errors based on return code.
261 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition-table for disk '%s'." % (rc, d['disk'].device))
263 msger.debug("Creating partitions")
265 for p in self.partitions:
266 d = self.disks[p['disk_name']]
268 self.__create_part_to_image(d['disk'].device,"extended",None,p['start'],d['extended'])
270 if p['fstype'] == "swap":
271 parted_fs_type = "linux-swap"
272 elif p['fstype'] == "vfat":
273 parted_fs_type = "fat32"
274 elif p['fstype'] == "msdos":
275 parted_fs_type = "fat16"
277 # Type for ext2/ext3/ext4/btrfs
278 parted_fs_type = "ext2"
280 # Boot ROM of OMAP boards require vfat boot partition to have an
281 # even number of sectors.
282 if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat","msdos"] and p['size'] % 2:
283 msger.debug("Substracting one sector from '%s' partition to get even number of sectors for the partition." % (p['mountpoint']))
286 ret = self.__create_part_to_image(d['disk'].device,p['type'],
287 parted_fs_type, p['start'],
291 # NOTE: We don't throw exception when return code is not 0, because
292 # parted always fails to reload part table with loop devices.
293 # This prevents us from distinguishing real errors based on return code.
294 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition '%s' for disk '%s'." % (ret, p['mountpoint'], d['disk'].device))
297 msger.debug("Setting boot flag for partition '%s' on disk '%s'." % (p['num'],d['disk'].device))
298 boot_cmd = [self.parted, "-s", d['disk'].device, "set", "%d" % p['num'], "boot", "on"]
299 msger.debug(boot_cmd)
300 rc = runner.show(boot_cmd)
303 # NOTE: We don't throw exception when return code is not 0, because
304 # parted always fails to reload part table with loop devices.
305 # This prevents us from distinguishing real errors based on return code.
306 msger.warning("parted returned '%s' instead of 0 when adding boot flag for partition '%s' disk '%s'." % (rc,p['num'],d['disk'].device))
308 def __map_partitions(self):
309 """Load it if dm_snapshot isn't loaded"""
310 load_module("dm_snapshot")
312 for dev in self.disks.keys():
317 msger.debug("Running kpartx on %s" % d['disk'].device )
318 rc, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
319 kpartxOutput = kpartxOutput.splitlines()
322 raise MountError("Failed to query partition mapping for '%s'" %
325 # Strip trailing blank and mask verbose output
327 while i < len(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
329 kpartxOutput = kpartxOutput[i:]
331 # Quick sanity check that the number of partitions matches
332 # our expectation. If it doesn't, someone broke the code
334 if len(kpartxOutput) != d['numpart']:
335 raise MountError("Unexpected number of partitions from kpartx: %d != %d" %
336 (len(kpartxOutput), d['numpart']))
338 for i in range(len(kpartxOutput)):
339 line = kpartxOutput[i]
340 newdev = line.split()[0]
341 mapperdev = "/dev/mapper/" + newdev
342 loopdev = d['disk'].device + newdev[-1]
344 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
345 pnum = d['partitions'][i]
346 self.partitions[pnum]['device'] = loopdev
348 # grub's install wants partitions to be named
349 # to match their parent device + partition num
350 # kpartx doesn't work like this, so we add compat
351 # symlinks to point to /dev/mapper
352 if os.path.lexists(loopdev):
354 os.symlink(mapperdev, loopdev)
356 msger.debug("Adding partx mapping for %s" % d['disk'].device)
357 rc = runner.show([self.kpartx, "-v", "-a", d['disk'].device])
360 # Make sure that the device maps are also removed on error case.
361 # The d['mapped'] isn't set to True if the kpartx fails so
362 # failed mapping will not be cleaned on cleanup either.
363 runner.quiet([self.kpartx, "-d", d['disk'].device])
364 raise MountError("Failed to map partitions for '%s'" %
369 def __unmap_partitions(self):
370 for dev in self.disks.keys():
375 msger.debug("Removing compat symlinks")
376 for pnum in d['partitions']:
377 if self.partitions[pnum]['device'] != None:
378 os.unlink(self.partitions[pnum]['device'])
379 self.partitions[pnum]['device'] = None
381 msger.debug("Unmapping %s" % d['disk'].device)
382 rc = runner.quiet([self.kpartx, "-d", d['disk'].device])
384 raise MountError("Failed to unmap partitions for '%s'" %
389 def __calculate_mountorder(self):
390 msger.debug("Calculating mount order")
391 for p in self.partitions:
392 self.mountOrder.append(p['mountpoint'])
393 self.unmountOrder.append(p['mountpoint'])
395 self.mountOrder.sort()
396 self.unmountOrder.sort()
397 self.unmountOrder.reverse()
402 self.__unmap_partitions()
403 for dev in self.disks.keys():
411 self.__unmount_subvolumes()
412 for mp in self.unmountOrder:
416 for p1 in self.partitions:
417 if p1['mountpoint'] == mp:
421 if p['mount'] != None:
423 """ Create subvolume snapshot here """
424 if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and not self.snapshot_created:
425 self.__create_subvolume_snapshots(p, p["mount"])
431 """ Only for btrfs """
432 def __get_subvolume_id(self, rootpath, subvol):
433 if not self.btrfscmd:
434 self.btrfscmd=find_binary_path("btrfs")
435 argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
437 rc, out = runner.runtool(argv)
441 raise MountError("Failed to get subvolume id from %s', return code: %d." % (rootpath, rc))
444 for line in out.splitlines():
445 if line.endswith(" path %s" % subvol):
446 subvolid = line.split()[1]
447 if not subvolid.isdigit():
448 raise MountError("Invalid subvolume id: %s" % subvolid)
449 subvolid = int(subvolid)
453 def __create_subvolume_metadata(self, p, pdisk):
454 if len(self.subvolumes) == 0:
457 argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
458 rc, out = runner.runtool(argv)
462 raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
464 subvolid_items = out.splitlines()
465 subvolume_metadata = ""
466 for subvol in self.subvolumes:
467 for line in subvolid_items:
468 if line.endswith(" path %s" % subvol["subvol"]):
469 subvolid = line.split()[1]
470 if not subvolid.isdigit():
471 raise MountError("Invalid subvolume id: %s" % subvolid)
473 subvolid = int(subvolid)
474 opts = subvol["fsopts"].split(",")
476 if opt.strip().startswith("subvol="):
479 fsopts = ",".join(opts)
480 subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"], subvol['mountpoint'], fsopts)
482 if subvolume_metadata:
483 fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
484 fd.write(subvolume_metadata)
487 def __get_subvolume_metadata(self, p, pdisk):
488 subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
489 if not os.path.exists(subvolume_metadata_file):
492 fd = open(subvolume_metadata_file, "r")
496 for line in content.splitlines():
497 items = line.split("\t")
498 if items and len(items) == 4:
499 self.subvolumes.append({'size': 0, # In sectors
500 'mountpoint': items[2], # Mount relative to chroot
501 'fstype': "btrfs", # Filesystem type
502 'fsopts': items[3] + ",subvol=%s" % items[1], # Filesystem mount options
503 'disk_name': p['disk_name'], # physical disk name holding partition
504 'device': None, # kpartx device node for partition
505 'mount': None, # Mount object
506 'subvol': items[1], # Subvolume name
507 'boot': False, # Bootable flag
508 'mounted': False # Mount flag
511 def __create_subvolumes(self, p, pdisk):
512 """ Create all the subvolumes """
514 for subvol in self.subvolumes:
515 argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
517 rc = runner.show(argv)
519 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
521 """ Set default subvolume, subvolume for "/" is default """
523 for subvolume in self.subvolumes:
524 if subvolume["mountpoint"] == "/" and p['disk_name'] == subvolume['disk_name']:
529 """ Get default subvolume id """
530 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
531 """ Set default subvolume """
533 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" % subvolid, pdisk.mountdir])
535 raise MountError("Failed to set default subvolume id: %d', return code: %d." % (subvolid, rc))
537 self.__create_subvolume_metadata(p, pdisk)
539 def __mount_subvolumes(self, p, pdisk):
541 """ Get subvolume info """
542 self.__get_subvolume_metadata(p, pdisk)
543 """ Set default mount options """
544 if len(self.subvolumes) != 0:
545 for subvol in self.subvolumes:
546 if subvol["mountpoint"] == p["mountpoint"] == "/":
547 opts = subvol["fsopts"].split(",")
549 if opt.strip().startswith("subvol="):
552 pdisk.fsopts = ",".join(opts)
555 if len(self.subvolumes) == 0:
556 """ Return directly if no subvolumes """
559 """ Remount to make default subvolume mounted """
560 rc = runner.show([self.umountcmd, pdisk.mountdir])
562 raise MountError("Failed to umount %s" % pdisk.mountdir)
564 rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
566 raise MountError("Failed to umount %s" % pdisk.mountdir)
568 for subvol in self.subvolumes:
569 if subvol["mountpoint"] == "/":
571 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
573 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
575 """ Replace subvolume name with subvolume ID """
576 opts = subvol["fsopts"].split(",")
578 if opt.strip().startswith("subvol="):
582 opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
583 fsopts = ",".join(opts)
584 subvol['fsopts'] = fsopts
585 mountpoint = self.mountdir + subvol['mountpoint']
587 rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
589 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
590 subvol["mounted"] = True
592 def __unmount_subvolumes(self):
593 """ It may be called multiple times, so we need to chekc if it is still mounted. """
594 for subvol in self.subvolumes:
595 if subvol["mountpoint"] == "/":
597 if not subvol["mounted"]:
599 mountpoint = self.mountdir + subvol['mountpoint']
600 rc = runner.show([self.umountcmd, mountpoint])
602 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
603 subvol["mounted"] = False
605 def __create_subvolume_snapshots(self, p, pdisk):
608 if self.snapshot_created:
611 """ Remount with subvolid=0 """
612 rc = runner.show([self.umountcmd, pdisk.mountdir])
614 raise MountError("Failed to umount %s" % pdisk.mountdir)
616 mountopts = pdisk.fsopts + ",subvolid=0"
618 mountopts = "subvolid=0"
619 rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
621 raise MountError("Failed to umount %s" % pdisk.mountdir)
623 """ Create all the subvolume snapshots """
624 snapshotts = time.strftime("%Y%m%d-%H%M")
625 for subvol in self.subvolumes:
626 subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
627 snapshotpath = subvolpath + "_%s-1" % snapshotts
628 rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
630 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code: %d." % (snapshotpath, subvolpath, rc))
632 self.snapshot_created = True
635 for dev in self.disks.keys():
639 self.__format_disks()
640 self.__map_partitions()
641 self.__calculate_mountorder()
643 for mp in self.mountOrder:
645 for p1 in self.partitions:
646 if p1['mountpoint'] == mp:
651 if p['mountpoint'] == "/":
652 p['label'] = 'platform'
654 p['label'] = mp.split('/')[-1]
658 p['uuid'] = str(uuid.uuid1())
659 runner.show([self.mkswap,
666 if p['mountpoint'] == "/":
668 if p['fstype'] == "vfat" or p['fstype'] == "msdos":
669 myDiskMount = VfatDiskMount
670 elif p['fstype'] in ("ext2", "ext3", "ext4"):
671 myDiskMount = ExtDiskMount
672 elif p['fstype'] == "btrfs":
673 myDiskMount = BtrfsDiskMount
675 raise MountError("Fail to support file system " + p['fstype'])
677 if p['fstype'] == "btrfs" and not p['fsopts']:
678 p['fsopts'] = "subvolid=0"
680 pdisk = myDiskMount(RawDisk(p['size'] * self.sector_size, p['device']),
681 self.mountdir + p['mountpoint'],
687 fsopts = p['fsopts'])
688 pdisk.mount(pdisk.fsopts)
689 if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
690 if not self.skipformat:
691 self.__create_subvolumes(p, pdisk)
692 self.__mount_subvolumes(p, pdisk)
694 p['uuid'] = pdisk.uuid
696 def resparse(self, size = None):
697 # Can't re-sparse a disk image - too hard