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
54 def add_disks(self, disks):
55 """ Add the disks which have to be partitioned. """
57 for name in disks.keys():
58 self.disks[name] = { 'disk': disks[name], # Disk object
59 'mapped': False, # True if kpartx mapping exists
60 'numpart': 0, # Number of allocate partitions
61 'partitions': [], # indexes to self.partitions
62 # Partitions with part num higher than 3 will
63 # be put inside extended partition.
64 'extended': 0, # Size of extended partition
65 # Offset of next partition (in sectors)
69 def add_partition(self, size, disk, mountpoint, fstype = None, label=None, fsopts = None, boot = False, align = None):
70 # Converting MB to sectors for parted
71 size = size * 1024 * 1024 / self.sector_size
73 """ We need to handle subvolumes for btrfs """
74 if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
75 self.btrfscmd=find_binary_path("btrfs")
77 opts = fsopts.split(",")
79 if opt.find("subvol=") != -1:
80 subvol = opt.replace("subvol=", "").strip()
83 raise MountError("No subvolume: %s" % fsopts)
84 self.subvolumes.append({'size': size, # In sectors
85 'mountpoint': mountpoint, # Mount relative to chroot
86 'fstype': fstype, # Filesystem type
87 'fsopts': fsopts, # Filesystem mount options
88 'disk': disk, # physical disk name holding partition
89 'device': None, # kpartx device node for partition
90 'mount': None, # Mount object
91 'subvol': subvol, # Subvolume name
92 'boot': boot, # Bootable flag
93 'mounted': False # Mount flag
96 """ We still need partition for "/" or non-subvolume """
97 if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
98 """ Don't need subvolume for "/" because it will be set as default subvolume """
99 if fsopts and fsopts.find("subvol=") != -1:
100 opts = fsopts.split(",")
102 if opt.strip().startswith("subvol="):
105 fsopts = ",".join(opts)
106 self.partitions.append({'size': size, # In sectors
107 'mountpoint': mountpoint, # Mount relative to chroot
108 'fstype': fstype, # Filesystem type
109 'fsopts': fsopts, # Filesystem mount options
110 'label': label, # Partition label
111 'disk': disk, # physical disk name holding partition
112 'device': None, # kpartx device node for partition
113 'mount': None, # Mount object
114 'num': None, # Partition number
115 'boot': boot, # Bootable flag
116 'align': align}) # Partition alignment
118 def __create_part_to_image(self, device, parttype, fstype, start, size):
119 # Start is included to the size so we need to substract one from the end.
121 msger.debug("Added '%s' part at Sector %d with size %d sectors" %
122 (parttype, start, end))
123 part_cmd = [self.parted, "-s", device, "unit", "s", "mkpart", parttype]
125 part_cmd.extend([fstype])
126 part_cmd.extend(["%d" % start, "%d" % end])
128 msger.debug(part_cmd)
129 rc, out = runner.runtool(part_cmd, catch=3)
132 msger.debug('"parted" output: %s' % out)
135 def __format_disks(self):
136 msger.debug("Assigning partitions to disks")
138 mbr_sector_skipped = False
140 # Go through partitions in the order they are added in .ks file
141 for n in range(len(self.partitions)):
142 p = self.partitions[n]
144 if not self.disks.has_key(p['disk']):
145 raise MountError("No disk %s for partition %s" % (p['disk'], p['mountpoint']))
147 if not mbr_sector_skipped:
148 # This hack is used to remove one sector from the first partition,
149 # that is the used to the MBR.
151 mbr_sector_skipped = True
153 # Get the disk where the partition is located
154 d = self.disks[p['disk']]
157 # alignment in sectors
159 # if first partition then we need to skip the first sector
160 # where the MBR is located, if the alignment isn't set
161 # See: https://wiki.linaro.org/WorkingGroups/Kernel/Projects/FlashCardSurvey
162 if d['numpart'] == 1:
163 if p['align'] and p['align'] > 0:
164 align_sectors = p['align'] * 1024 / self.sector_size
166 align_sectors = MBR_SECTOR_LEN
168 # If not first partition and we do have alignment set we need
169 # to align the partition.
170 # FIXME: This leaves a empty spaces to the disk. To fill the
171 # gaps we could enlargea the previous partition?
173 # Calc how much the alignment is off.
174 align_sectors = d['offset'] % (p['align'] * 1024 / self.sector_size)
175 # We need to move forward to the next alignment point
176 align_sectors = (p['align'] * 1024 / self.sector_size) - align_sectors
179 if p['align'] and p['align'] > 0:
180 msger.debug("Realignment for %s%s with %s sectors, original"
181 " offset %s, target alignment is %sK." %
182 (p['disk'], d['numpart'], align_sectors,
183 d['offset'], p['align']))
184 # p['size'] already converted in secctors
185 if p['size'] <= align_sectors:
186 raise MountError("Partition for %s is too small to handle "
187 "the alignment change." % p['mountpoint'])
189 # increase the offset so we actually start the partition on right alignment
190 d['offset'] += align_sectors
193 # Increase allocation of extended partition to hold this partition
194 d['extended'] += p['size']
195 p['type'] = 'logical'
196 p['num'] = d['numpart'] + 1
198 p['type'] = 'primary'
199 p['num'] = d['numpart']
201 p['start'] = d['offset']
202 d['offset'] += p['size']
203 d['partitions'].append(n)
204 msger.debug("Assigned %s to %s%d at Sector %d with size %d sectors "
205 "/ %d bytes." % (p['mountpoint'], p['disk'], p['num'],
206 p['start'], p['size'],
207 p['size'] * self.sector_size))
210 msger.debug("Skipping disk format, because skipformat flag is set.")
213 for dev in self.disks.keys():
215 msger.debug("Initializing partition table for %s" % (d['disk'].device))
216 rc, out = runner.runtool([self.parted, "-s", d['disk'].device, "mklabel", "msdos"], catch=3)
219 msger.debug('"parted" output: %s' % out)
222 # NOTE: We don't throw exception when return code is not 0, because
223 # parted always fails to reload part table with loop devices.
224 # This prevents us from distinguishing real errors based on return code.
225 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition-table for disk '%s'." % (rc, d['disk'].device))
227 msger.debug("Creating partitions")
229 for p in self.partitions:
230 d = self.disks[p['disk']]
232 self.__create_part_to_image(d['disk'].device,"extended",None,p['start'],d['extended'])
234 if p['fstype'] == "swap":
235 parted_fs_type = "linux-swap"
236 elif p['fstype'] == "vfat":
237 parted_fs_type = "fat32"
238 elif p['fstype'] == "msdos":
239 parted_fs_type = "fat16"
241 # Type for ext2/ext3/ext4/btrfs
242 parted_fs_type = "ext2"
244 # Boot ROM of OMAP boards require vfat boot partition to have an
245 # even number of sectors.
246 if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat","msdos"] and p['size'] % 2:
247 msger.debug("Substracting one sector from '%s' partition to get even number of sectors for the partition." % (p['mountpoint']))
250 ret = self.__create_part_to_image(d['disk'].device,p['type'],
251 parted_fs_type, p['start'],
255 # NOTE: We don't throw exception when return code is not 0, because
256 # parted always fails to reload part table with loop devices.
257 # This prevents us from distinguishing real errors based on return code.
258 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition '%s' for disk '%s'." % (ret, p['mountpoint'], d['disk'].device))
261 msger.debug("Setting boot flag for partition '%s' on disk '%s'." % (p['num'],d['disk'].device))
262 boot_cmd = [self.parted, "-s", d['disk'].device, "set", "%d" % p['num'], "boot", "on"]
263 msger.debug(boot_cmd)
264 rc = runner.show(boot_cmd)
267 # NOTE: We don't throw exception when return code is not 0, because
268 # parted always fails to reload part table with loop devices.
269 # This prevents us from distinguishing real errors based on return code.
270 msger.warning("parted returned '%s' instead of 0 when adding boot flag for partition '%s' disk '%s'." % (rc,p['num'],d['disk'].device))
272 def __map_partitions(self):
273 """Load it if dm_snapshot isn't loaded"""
274 load_module("dm_snapshot")
276 for dev in self.disks.keys():
281 msger.debug("Running kpartx on %s" % d['disk'].device )
282 rc, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
283 kpartxOutput = kpartxOutput.splitlines()
286 raise MountError("Failed to query partition mapping for '%s'" %
289 # Strip trailing blank and mask verbose output
291 while i < len(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
293 kpartxOutput = kpartxOutput[i:]
295 # Quick sanity check that the number of partitions matches
296 # our expectation. If it doesn't, someone broke the code
298 if len(kpartxOutput) != d['numpart']:
299 raise MountError("Unexpected number of partitions from kpartx: %d != %d" %
300 (len(kpartxOutput), d['numpart']))
302 for i in range(len(kpartxOutput)):
303 line = kpartxOutput[i]
304 newdev = line.split()[0]
305 mapperdev = "/dev/mapper/" + newdev
306 loopdev = d['disk'].device + newdev[-1]
308 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
309 pnum = d['partitions'][i]
310 self.partitions[pnum]['device'] = loopdev
312 # grub's install wants partitions to be named
313 # to match their parent device + partition num
314 # kpartx doesn't work like this, so we add compat
315 # symlinks to point to /dev/mapper
316 if os.path.lexists(loopdev):
318 os.symlink(mapperdev, loopdev)
320 msger.debug("Adding partx mapping for %s" % d['disk'].device)
321 rc = runner.show([self.kpartx, "-v", "-a", d['disk'].device])
324 # Make sure that the device maps are also removed on error case.
325 # The d['mapped'] isn't set to True if the kpartx fails so
326 # failed mapping will not be cleaned on cleanup either.
327 runner.quiet([self.kpartx, "-d", d['disk'].device])
328 raise MountError("Failed to map partitions for '%s'" %
333 def __unmap_partitions(self):
334 for dev in self.disks.keys():
339 msger.debug("Removing compat symlinks")
340 for pnum in d['partitions']:
341 if self.partitions[pnum]['device'] != None:
342 os.unlink(self.partitions[pnum]['device'])
343 self.partitions[pnum]['device'] = None
345 msger.debug("Unmapping %s" % d['disk'].device)
346 rc = runner.quiet([self.kpartx, "-d", d['disk'].device])
348 raise MountError("Failed to unmap partitions for '%s'" %
353 def __calculate_mountorder(self):
354 msger.debug("Calculating mount order")
355 for p in self.partitions:
356 self.mountOrder.append(p['mountpoint'])
357 self.unmountOrder.append(p['mountpoint'])
359 self.mountOrder.sort()
360 self.unmountOrder.sort()
361 self.unmountOrder.reverse()
366 self.__unmap_partitions()
367 for dev in self.disks.keys():
375 self.__unmount_subvolumes()
376 for mp in self.unmountOrder:
380 for p1 in self.partitions:
381 if p1['mountpoint'] == mp:
385 if p['mount'] != None:
387 """ Create subvolume snapshot here """
388 if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and not self.snapshot_created:
389 self.__create_subvolume_snapshots(p, p["mount"])
395 """ Only for btrfs """
396 def __get_subvolume_id(self, rootpath, subvol):
397 if not self.btrfscmd:
398 self.btrfscmd=find_binary_path("btrfs")
399 argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
401 rc, out = runner.runtool(argv)
405 raise MountError("Failed to get subvolume id from %s', return code: %d." % (rootpath, rc))
408 for line in out.splitlines():
409 if line.endswith(" path %s" % subvol):
410 subvolid = line.split()[1]
411 if not subvolid.isdigit():
412 raise MountError("Invalid subvolume id: %s" % subvolid)
413 subvolid = int(subvolid)
417 def __create_subvolume_metadata(self, p, pdisk):
418 if len(self.subvolumes) == 0:
421 argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
422 rc, out = runner.runtool(argv)
426 raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
428 subvolid_items = out.splitlines()
429 subvolume_metadata = ""
430 for subvol in self.subvolumes:
431 for line in subvolid_items:
432 if line.endswith(" path %s" % subvol["subvol"]):
433 subvolid = line.split()[1]
434 if not subvolid.isdigit():
435 raise MountError("Invalid subvolume id: %s" % subvolid)
437 subvolid = int(subvolid)
438 opts = subvol["fsopts"].split(",")
440 if opt.strip().startswith("subvol="):
443 fsopts = ",".join(opts)
444 subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"], subvol['mountpoint'], fsopts)
446 if subvolume_metadata:
447 fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
448 fd.write(subvolume_metadata)
451 def __get_subvolume_metadata(self, p, pdisk):
452 subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
453 if not os.path.exists(subvolume_metadata_file):
456 fd = open(subvolume_metadata_file, "r")
460 for line in content.splitlines():
461 items = line.split("\t")
462 if items and len(items) == 4:
463 self.subvolumes.append({'size': 0, # In sectors
464 'mountpoint': items[2], # Mount relative to chroot
465 'fstype': "btrfs", # Filesystem type
466 'fsopts': items[3] + ",subvol=%s" % items[1], # Filesystem mount options
467 'disk': p['disk'], # physical disk name holding partition
468 'device': None, # kpartx device node for partition
469 'mount': None, # Mount object
470 'subvol': items[1], # Subvolume name
471 'boot': False, # Bootable flag
472 'mounted': False # Mount flag
475 def __create_subvolumes(self, p, pdisk):
476 """ Create all the subvolumes """
478 for subvol in self.subvolumes:
479 argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
481 rc = runner.show(argv)
483 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
485 """ Set default subvolume, subvolume for "/" is default """
487 for subvolume in self.subvolumes:
488 if subvolume["mountpoint"] == "/" and p["disk"] == subvolume["disk"]:
493 """ Get default subvolume id """
494 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
495 """ Set default subvolume """
497 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" % subvolid, pdisk.mountdir])
499 raise MountError("Failed to set default subvolume id: %d', return code: %d." % (subvolid, rc))
501 self.__create_subvolume_metadata(p, pdisk)
503 def __mount_subvolumes(self, p, pdisk):
505 """ Get subvolume info """
506 self.__get_subvolume_metadata(p, pdisk)
507 """ Set default mount options """
508 if len(self.subvolumes) != 0:
509 for subvol in self.subvolumes:
510 if subvol["mountpoint"] == p["mountpoint"] == "/":
511 opts = subvol["fsopts"].split(",")
513 if opt.strip().startswith("subvol="):
516 pdisk.fsopts = ",".join(opts)
519 if len(self.subvolumes) == 0:
520 """ Return directly if no subvolumes """
523 """ Remount to make default subvolume mounted """
524 rc = runner.show([self.umountcmd, pdisk.mountdir])
526 raise MountError("Failed to umount %s" % pdisk.mountdir)
528 rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
530 raise MountError("Failed to umount %s" % pdisk.mountdir)
532 for subvol in self.subvolumes:
533 if subvol["mountpoint"] == "/":
535 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
537 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
539 """ Replace subvolume name with subvolume ID """
540 opts = subvol["fsopts"].split(",")
542 if opt.strip().startswith("subvol="):
546 opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
547 fsopts = ",".join(opts)
548 subvol['fsopts'] = fsopts
549 mountpoint = self.mountdir + subvol['mountpoint']
551 rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
553 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
554 subvol["mounted"] = True
556 def __unmount_subvolumes(self):
557 """ It may be called multiple times, so we need to chekc if it is still mounted. """
558 for subvol in self.subvolumes:
559 if subvol["mountpoint"] == "/":
561 if not subvol["mounted"]:
563 mountpoint = self.mountdir + subvol['mountpoint']
564 rc = runner.show([self.umountcmd, mountpoint])
566 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
567 subvol["mounted"] = False
569 def __create_subvolume_snapshots(self, p, pdisk):
572 if self.snapshot_created:
575 """ Remount with subvolid=0 """
576 rc = runner.show([self.umountcmd, pdisk.mountdir])
578 raise MountError("Failed to umount %s" % pdisk.mountdir)
580 mountopts = pdisk.fsopts + ",subvolid=0"
582 mountopts = "subvolid=0"
583 rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
585 raise MountError("Failed to umount %s" % pdisk.mountdir)
587 """ Create all the subvolume snapshots """
588 snapshotts = time.strftime("%Y%m%d-%H%M")
589 for subvol in self.subvolumes:
590 subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
591 snapshotpath = subvolpath + "_%s-1" % snapshotts
592 rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
594 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code: %d." % (snapshotpath, subvolpath, rc))
596 self.snapshot_created = True
599 for dev in self.disks.keys():
603 self.__format_disks()
604 self.__map_partitions()
605 self.__calculate_mountorder()
607 for mp in self.mountOrder:
609 for p1 in self.partitions:
610 if p1['mountpoint'] == mp:
615 if p['mountpoint'] == "/":
616 p['label'] = 'platform'
618 p['label'] = mp.split('/')[-1]
622 p['uuid'] = str(uuid.uuid1())
623 runner.show([self.mkswap,
630 if p['mountpoint'] == "/":
632 if p['fstype'] == "vfat" or p['fstype'] == "msdos":
633 myDiskMount = VfatDiskMount
634 elif p['fstype'] in ("ext2", "ext3", "ext4"):
635 myDiskMount = ExtDiskMount
636 elif p['fstype'] == "btrfs":
637 myDiskMount = BtrfsDiskMount
639 raise MountError("Fail to support file system " + p['fstype'])
641 if p['fstype'] == "btrfs" and not p['fsopts']:
642 p['fsopts'] = "subvolid=0"
644 pdisk = myDiskMount(RawDisk(p['size'] * self.sector_size, p['device']),
645 self.mountdir + p['mountpoint'],
651 fsopts = p['fsopts'])
652 pdisk.mount(pdisk.fsopts)
653 if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
654 if not self.skipformat:
655 self.__create_subvolumes(p, pdisk)
656 self.__mount_subvolumes(p, pdisk)
658 p['uuid'] = pdisk.uuid
660 def resparse(self, size = None):
661 # Can't re-sparse a disk image - too hard