2 # partitionedfs.py: partitioned files system class, extends fs.py
4 # Copyright 2007-2008, Red Hat Inc.
5 # Copyright 2008, Daniel P. Berrange
6 # Copyright 2008, David P. Huff
8 # Copyright 2009, 2010, 2011 Intel, Inc.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; version 2 of the License.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Library General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26 from mic.utils import runner
27 from mic.utils.errors import MountError
28 from mic.utils.fs_related import *
30 class PartitionedMount(Mount):
31 def __init__(self, disks, mountdir, skipformat = False):
32 Mount.__init__(self, mountdir)
34 for name in disks.keys():
35 self.disks[name] = { 'disk': disks[name], # Disk object
36 'mapped': False, # True if kpartx mapping exists
37 'numpart': 0, # Number of allocate partitions
38 'partitions': [], # indexes to self.partitions
39 # Partitions with part num higher than 3 will
40 # be put inside extended partition.
41 'extended': 0, # Size of extended partition
42 # Sector 0 is used by the MBR and can't be used
43 # as the start, so setting offset to 1.
44 'offset': 1 } # Offset of next partition (in sectors)
50 self.unmountOrder = []
51 self.parted=find_binary_path("parted")
52 self.kpartx=find_binary_path("kpartx")
53 self.mkswap=find_binary_path("mkswap")
55 self.mountcmd=find_binary_path("mount")
56 self.umountcmd=find_binary_path("umount")
57 self.skipformat = skipformat
58 self.snapshot_created = self.skipformat
59 # Size of a sector used in calculations
60 self.sector_size = 512
62 def add_partition(self, size, disk, mountpoint, fstype = None, fsopts = None, boot = False):
63 # Converting M to s for parted
64 size = size * 1024 * 1024 / self.sector_size
66 """ We need to handle subvolumes for btrfs """
67 if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
68 self.btrfscmd=find_binary_path("btrfs")
70 opts = fsopts.split(",")
72 if opt.find("subvol=") != -1:
73 subvol = opt.replace("subvol=", "").strip()
76 raise MountError("No subvolume: %s" % fsopts)
77 self.subvolumes.append({'size': size, # In sectors
78 'mountpoint': mountpoint, # Mount relative to chroot
79 'fstype': fstype, # Filesystem type
80 'fsopts': fsopts, # Filesystem mount options
81 'disk': disk, # physical disk name holding partition
82 'device': None, # kpartx device node for partition
83 'mount': None, # Mount object
84 'subvol': subvol, # Subvolume name
85 'boot': boot, # Bootable flag
86 'mounted': False # Mount flag
89 """ We still need partition for "/" or non-subvolume """
90 if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
91 """ Don't need subvolume for "/" because it will be set as default subvolume """
92 if fsopts and fsopts.find("subvol=") != -1:
93 opts = fsopts.split(",")
95 if opt.strip().startswith("subvol="):
98 fsopts = ",".join(opts)
99 self.partitions.append({'size': size, # In sectors
100 'mountpoint': mountpoint, # Mount relative to chroot
101 'fstype': fstype, # Filesystem type
102 'fsopts': fsopts, # Filesystem mount options
103 'disk': disk, # physical disk name holding partition
104 'device': None, # kpartx device node for partition
105 'mount': None, # Mount object
106 'num': None, # Partition number
107 'boot': boot}) # Bootable flag
109 def __create_part_to_image(self,device, parttype, fstype, start, size):
110 # Start is included to the size so we need to substract one from the end.
112 msger.debug("Added '%s' part at %d of size %d" % (parttype,start,end))
113 part_cmd = [self.parted, "-s", device, "unit", "s", "mkpart", parttype]
115 part_cmd.extend([fstype])
116 part_cmd.extend(["%d" % start, "%d" % end])
118 msger.debug(part_cmd)
119 rc, out = runner.runtool(part_cmd, catch=3)
122 msger.debug('"parted" output: %s' % out)
125 def __format_disks(self):
126 msger.debug("Assigning partitions to disks")
128 mbr_sector_skipped = False
130 for n in range(len(self.partitions)):
131 p = self.partitions[n]
133 if not self.disks.has_key(p['disk']):
134 raise MountError("No disk %s for partition %s" % (p['disk'], p['mountpoint']))
136 if not mbr_sector_skipped:
137 # This hack is used to remove one sector from the first partition,
138 # that is the used to the MBR.
140 mbr_sector_skipped = True
142 d = self.disks[p['disk']]
145 # Increase allocation of extended partition to hold this partition
146 d['extended'] += p['size']
147 p['type'] = 'logical'
148 p['num'] = d['numpart'] + 1
150 p['type'] = 'primary'
151 p['num'] = d['numpart']
153 p['start'] = d['offset']
154 d['offset'] += p['size']
155 d['partitions'].append(n)
156 msger.debug("Assigned %s to %s%d at %d at size %d" % (p['mountpoint'], p['disk'], p['num'], p['start'], p['size']))
159 msger.debug("Skipping disk format, because skipformat flag is set.")
162 for dev in self.disks.keys():
164 msger.debug("Initializing partition table for %s" % (d['disk'].device))
165 rc, out = runner.runtool([self.parted, "-s", d['disk'].device, "mklabel", "msdos"], catch=3)
168 msger.debug('"parted" output: %s' % out)
171 # NOTE: We don't throw exception when return code is not 0, because
172 # parted always fails to reload part table with loop devices.
173 # This prevents us from distinguishing real errors based on return code.
174 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition-table for disk '%s'." % (p1.returncode,d['disk'].device))
176 msger.debug("Creating partitions")
178 for p in self.partitions:
179 d = self.disks[p['disk']]
181 self.__create_part_to_image(d['disk'].device,"extended",None,p['start'],d['extended'])
183 if p['fstype'] == "swap":
184 parted_fs_type = "linux-swap"
185 elif p['fstype'] == "vfat":
186 parted_fs_type = "fat32"
187 elif p['fstype'] == "msdos":
188 parted_fs_type = "fat16"
190 # Type for ext2/ext3/ext4/btrfs
191 parted_fs_type = "ext2"
193 # Boot ROM of OMAP boards require vfat boot partition to have an
194 # even number of sectors.
195 if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat","msdos"] and p['size'] % 2:
196 msger.debug("Substracting one sector from '%s' partition to get even number of sectors for the partition." % (p['mountpoint']))
199 ret = self.__create_part_to_image(d['disk'].device,p['type'],
200 parted_fs_type, p['start'],
204 # NOTE: We don't throw exception when return code is not 0, because
205 # parted always fails to reload part table with loop devices.
206 # This prevents us from distinguishing real errors based on return code.
207 msger.debug("WARNING: parted returned '%s' instead of 0 when creating partition '%s' for disk '%s'." % (p1.returncode,p['mountpoint'],d['disk'].device))
210 msger.debug("Setting boot flag for partition '%s' on disk '%s'." % (p['num'],d['disk'].device))
211 boot_cmd = [self.parted, "-s", d['disk'].device, "set", "%d" % p['num'], "boot", "on"]
212 msger.debug(boot_cmd)
213 rc = runner.show(boot_cmd)
216 # NOTE: We don't throw exception when return code is not 0, because
217 # parted always fails to reload part table with loop devices.
218 # This prevents us from distinguishing real errors based on return code.
219 msger.warning("parted returned '%s' instead of 0 when adding boot flag for partition '%s' disk '%s'." % (rc,p['num'],d['disk'].device))
221 def __map_partitions(self):
222 """Load it if dm_snapshot isn't loaded"""
223 load_module("dm_snapshot")
225 for dev in self.disks.keys():
230 msger.debug("Running kpartx on %s" % d['disk'].device )
231 rc, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
232 kpartxOutput = kpartxOutput.splitlines()
235 raise MountError("Failed to query partition mapping for '%s'" %
238 # Strip trailing blank and mask verbose output
240 while i < len(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
242 kpartxOutput = kpartxOutput[i:]
244 # Quick sanity check that the number of partitions matches
245 # our expectation. If it doesn't, someone broke the code
247 if len(kpartxOutput) != d['numpart']:
248 raise MountError("Unexpected number of partitions from kpartx: %d != %d" %
249 (len(kpartxOutput), d['numpart']))
251 for i in range(len(kpartxOutput)):
252 line = kpartxOutput[i]
253 newdev = line.split()[0]
254 mapperdev = "/dev/mapper/" + newdev
255 loopdev = d['disk'].device + newdev[-1]
257 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
258 pnum = d['partitions'][i]
259 self.partitions[pnum]['device'] = loopdev
261 # grub's install wants partitions to be named
262 # to match their parent device + partition num
263 # kpartx doesn't work like this, so we add compat
264 # symlinks to point to /dev/mapper
265 if os.path.lexists(loopdev):
267 os.symlink(mapperdev, loopdev)
269 msger.debug("Adding partx mapping for %s" % d['disk'].device)
270 rc = runner.show([self.kpartx, "-v", "-a", d['disk'].device])
273 # Make sure that the device maps are also removed on error case.
274 # The d['mapped'] isn't set to True if the kpartx fails so
275 # failed mapping will not be cleaned on cleanup either.
276 runner.quiet([self.kpartx, "-d", d['disk'].device])
277 raise MountError("Failed to map partitions for '%s'" %
282 def __unmap_partitions(self):
283 for dev in self.disks.keys():
288 msger.debug("Removing compat symlinks")
289 for pnum in d['partitions']:
290 if self.partitions[pnum]['device'] != None:
291 os.unlink(self.partitions[pnum]['device'])
292 self.partitions[pnum]['device'] = None
294 msger.debug("Unmapping %s" % d['disk'].device)
295 rc = runner.quiet([self.kpartx, "-d", d['disk'].device])
297 raise MountError("Failed to unmap partitions for '%s'" %
302 def __calculate_mountorder(self):
303 msger.debug("Calculating mount order")
304 for p in self.partitions:
305 self.mountOrder.append(p['mountpoint'])
306 self.unmountOrder.append(p['mountpoint'])
308 self.mountOrder.sort()
309 self.unmountOrder.sort()
310 self.unmountOrder.reverse()
314 self.__unmap_partitions()
315 for dev in self.disks.keys():
323 self.__unmount_subvolumes()
324 for mp in self.unmountOrder:
328 for p1 in self.partitions:
329 if p1['mountpoint'] == mp:
333 if p['mount'] != None:
335 """ Create subvolume snapshot here """
336 if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and not self.snapshot_created:
337 self.__create_subvolume_snapshots(p, p["mount"])
343 """ Only for btrfs """
344 def __get_subvolume_id(self, rootpath, subvol):
345 if not self.btrfscmd:
346 self.btrfscmd=find_binary_path("btrfs")
347 argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
349 rc, out = runner.runtool(argv)
353 raise MountError("Failed to get subvolume id from %s', return code: %d." % (rootpath, rc))
356 for line in out.splitlines():
357 if line.endswith(" path %s" % subvol):
358 subvolid = line.split()[1]
359 if not subvolid.isdigit():
360 raise MountError("Invalid subvolume id: %s" % subvolid)
361 subvolid = int(subvolid)
365 def __create_subvolume_metadata(self, p, pdisk):
366 if len(self.subvolumes) == 0:
369 argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
370 rc, out = runner.runtool(argv)
374 raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
376 subvolid_items = out.splitlines()
377 subvolume_metadata = ""
378 for subvol in self.subvolumes:
379 for line in subvolid_items:
380 if line.endswith(" path %s" % subvol["subvol"]):
381 subvolid = line.split()[1]
382 if not subvolid.isdigit():
383 raise MountError("Invalid subvolume id: %s" % subvolid)
385 subvolid = int(subvolid)
386 opts = subvol["fsopts"].split(",")
388 if opt.strip().startswith("subvol="):
391 fsopts = ",".join(opts)
392 subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"], subvol['mountpoint'], fsopts)
394 if subvolume_metadata:
395 fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
396 fd.write(subvolume_metadata)
399 def __get_subvolume_metadata(self, p, pdisk):
400 subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
401 if not os.path.exists(subvolume_metadata_file):
404 fd = open(subvolume_metadata_file, "r")
408 for line in content.splitlines():
409 items = line.split("\t")
410 if items and len(items) == 4:
411 self.subvolumes.append({'size': 0, # In sectors
412 'mountpoint': items[2], # Mount relative to chroot
413 'fstype': "btrfs", # Filesystem type
414 'fsopts': items[3] + ",subvol=%s" % items[1], # Filesystem mount options
415 'disk': p['disk'], # physical disk name holding partition
416 'device': None, # kpartx device node for partition
417 'mount': None, # Mount object
418 'subvol': items[1], # Subvolume name
419 'boot': False, # Bootable flag
420 'mounted': False # Mount flag
423 def __create_subvolumes(self, p, pdisk):
424 """ Create all the subvolumes """
426 for subvol in self.subvolumes:
427 argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
429 rc = runner.show(argv)
431 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
433 """ Set default subvolume, subvolume for "/" is default """
435 for subvolume in self.subvolumes:
436 if subvolume["mountpoint"] == "/" and p["disk"] == subvolume["disk"]:
441 """ Get default subvolume id """
442 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
443 """ Set default subvolume """
445 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" % subvolid, pdisk.mountdir])
447 raise MountError("Failed to set default subvolume id: %d', return code: %d." % (subvolid, rc))
449 self.__create_subvolume_metadata(p, pdisk)
451 def __mount_subvolumes(self, p, pdisk):
453 """ Get subvolume info """
454 self.__get_subvolume_metadata(p, pdisk)
455 """ Set default mount options """
456 if len(self.subvolumes) != 0:
457 for subvol in self.subvolumes:
458 if subvol["mountpoint"] == p["mountpoint"] == "/":
459 opts = subvol["fsopts"].split(",")
461 if opt.strip().startswith("subvol="):
464 pdisk.fsopts = ",".join(opts)
467 if len(self.subvolumes) == 0:
468 """ Return directly if no subvolumes """
471 """ Remount to make default subvolume mounted """
472 rc = runner.show([self.umountcmd, pdisk.mountdir])
474 raise MountError("Failed to umount %s" % pdisk.mountdir)
476 rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
478 raise MountError("Failed to umount %s" % pdisk.mountdir)
480 for subvol in self.subvolumes:
481 if subvol["mountpoint"] == "/":
483 subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
485 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
487 """ Replace subvolume name with subvolume ID """
488 opts = subvol["fsopts"].split(",")
490 if opt.strip().startswith("subvol="):
493 #opts.append("subvolid=%d" % subvolid)
494 opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
495 fsopts = ",".join(opts)
496 subvol['fsopts'] = fsopts
497 mountpoint = self.mountdir + subvol['mountpoint']
499 rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
501 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
502 subvol["mounted"] = True
504 def __unmount_subvolumes(self):
505 """ It may be called multiple times, so we need to chekc if it is still mounted. """
506 for subvol in self.subvolumes:
507 if subvol["mountpoint"] == "/":
509 if not subvol["mounted"]:
511 mountpoint = self.mountdir + subvol['mountpoint']
512 rc = runner.show([self.umountcmd, mountpoint])
514 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
515 subvol["mounted"] = False
517 def __create_subvolume_snapshots(self, p, pdisk):
520 if self.snapshot_created:
523 """ Remount with subvolid=0 """
524 rc = runner.show([self.umountcmd, pdisk.mountdir])
526 raise MountError("Failed to umount %s" % pdisk.mountdir)
528 mountopts = pdisk.fsopts + ",subvolid=0"
530 mountopts = "subvolid=0"
531 rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
533 raise MountError("Failed to umount %s" % pdisk.mountdir)
535 """ Create all the subvolume snapshots """
536 snapshotts = time.strftime("%Y%m%d-%H%M")
537 for subvol in self.subvolumes:
538 subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
539 snapshotpath = subvolpath + "_%s-1" % snapshotts
540 rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
542 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code: %d." % (snapshotpath, subvolpath, rc))
544 self.snapshot_created = True
547 for dev in self.disks.keys():
551 self.__format_disks()
552 self.__map_partitions()
553 self.__calculate_mountorder()
555 for mp in self.mountOrder:
557 for p1 in self.partitions:
558 if p1['mountpoint'] == mp:
563 runner.show([self.mkswap, p['device']])
567 if p['mountpoint'] == "/":
569 if p['fstype'] == "vfat" or p['fstype'] == "msdos":
570 myDiskMount = VfatDiskMount
571 elif p['fstype'] in ("ext2", "ext3", "ext4"):
572 myDiskMount = ExtDiskMount
573 elif p['fstype'] == "btrfs":
574 myDiskMount = BtrfsDiskMount
576 raise MountError("Fail to support file system " + p['fstype'])
578 if p['fstype'] == "btrfs" and not p['fsopts']:
579 p['fsopts'] = "subvolid=0"
581 pdisk = myDiskMount(RawDisk(p['size'] * self.sector_size, p['device']),
582 self.mountdir + p['mountpoint'],
588 fsopts = p['fsopts'])
589 pdisk.mount(pdisk.fsopts)
590 if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
591 if not self.skipformat:
592 self.__create_subvolumes(p, pdisk)
593 self.__mount_subvolumes(p, pdisk)
596 def resparse(self, size = None):
597 # Can't re-sparse a disk image - too hard