add real path of device mapper to partition dict
[tools/mic.git] / mic / utils / partitionedfs.py
1 #!/usr/bin/python -tt
2 #
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
7 #
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
11 #
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
15 # for more details.
16 #
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.
20
21 import os
22
23 from mic import msger
24 from mic.utils import runner
25 from mic.utils.errors import MountError, CreatorError
26 from mic.utils.fs_related import *
27 from mic.utils.gpt_parser import GptParser
28
29 # Overhead of the MBR partitioning scheme (just one sector)
30 MBR_OVERHEAD = 1
31 # Overhead of the GPT partitioning scheme
32 GPT_OVERHEAD = 34
33
34 # Size of a sector in bytes
35 SECTOR_SIZE = 512
36
37 def resolve_ref(ref):
38     real = os.readlink(ref)
39     if not real.startswith('/'):
40         return os.path.realpath(os.path.join(ref, real))
41     else:
42         return real
43
44 class PartitionedMount(Mount):
45     def __init__(self, mountdir, skipformat = False):
46         Mount.__init__(self, mountdir)
47         self.disks = {}
48         self.partitions = []
49         self.subvolumes = []
50         self.mapped = False
51         self.mount_order = []
52         self.unmount_order = []
53         self.parted = find_binary_path("parted")
54         self.kpartx = find_binary_path("kpartx")
55         self.mkswap = find_binary_path("mkswap")
56         self.dmsetup = find_binary_path("dmsetup")
57         self.btrfscmd = None
58         self.mountcmd = find_binary_path("mount")
59         self.umountcmd = find_binary_path("umount")
60         self.skipformat = skipformat
61         self.snapshot_created = self.skipformat
62         # Size of a sector used in calculations
63         self.sector_size = SECTOR_SIZE
64         self._partitions_layed_out = False
65
66     def __add_disk(self, disk_name):
67         """ Add a disk 'disk_name' to the internal list of disks. Note,
68         'disk_name' is the name of the disk in the target system
69         (e.g., sdb). """
70
71         if disk_name in self.disks:
72             # We already have this disk
73             return
74
75         assert not self._partitions_layed_out
76
77         self.disks[disk_name] = \
78                 { 'disk': None,     # Disk object
79                   'mapped': False,  # True if kpartx mapping exists
80                   'numpart': 0,     # Number of allocate partitions
81                   'partitions': [], # Indexes to self.partitions
82                   'offset': 0,      # Offset of next partition (in sectors)
83                   # Minimum required disk size to fit all partitions (in bytes)
84                   'min_size': 0,
85                   'ptable_format': "msdos" } # Partition table format
86
87     def add_disk(self, disk_name, disk_obj):
88         """ Add a disk object which have to be partitioned. More than one disk
89         can be added. In case of multiple disks, disk partitions have to be
90         added for each disk separately with 'add_partition()". """
91
92         self.__add_disk(disk_name)
93         self.disks[disk_name]['disk'] = disk_obj
94
95     def __add_partition(self, part):
96         """ This is a helper function for 'add_partition()' which adds a
97         partition to the internal list of partitions. """
98
99         assert not self._partitions_layed_out
100
101         self.partitions.append(part)
102         self.__add_disk(part['disk_name'])
103
104     def add_partition(self, size, disk_name, mountpoint, fstype = None,
105                       label=None, fsopts = None, boot = False, align = None,
106                       part_type = None):
107         """ Add the next partition. Partitions have to be added in the
108         first-to-last order. """
109
110         ks_pnum = len(self.partitions)
111
112         # Converting MB to sectors for parted
113         size = size * 1024 * 1024 / self.sector_size
114
115         # We need to handle subvolumes for btrfs
116         if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
117             self.btrfscmd = find_binary_path("btrfs")
118             subvol = None
119             opts = fsopts.split(",")
120             for opt in opts:
121                 if opt.find("subvol=") != -1:
122                     subvol = opt.replace("subvol=", "").strip()
123                     break
124             if not subvol:
125                 raise MountError("No subvolume: %s" % fsopts)
126             self.subvolumes.append({'size': size, # In sectors
127                                     'mountpoint': mountpoint, # Mount relative to chroot
128                                     'fstype': fstype, # Filesystem type
129                                     'fsopts': fsopts, # Filesystem mount options
130                                     'disk_name': disk_name, # physical disk name holding partition
131                                     'device': None, # kpartx device node for partition
132                                     'mapper_device': None, # mapper device node
133                                     'mpath_device': None, # multipath device of device mapper
134                                     'mount': None, # Mount object
135                                     'subvol': subvol, # Subvolume name
136                                     'boot': boot, # Bootable flag
137                                     'mounted': False # Mount flag
138                                    })
139
140         # We still need partition for "/" or non-subvolume
141         if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
142             # Don't need subvolume for "/" because it will be set as default subvolume
143             if fsopts and fsopts.find("subvol=") != -1:
144                 opts = fsopts.split(",")
145                 for opt in opts:
146                     if opt.strip().startswith("subvol="):
147                         opts.remove(opt)
148                         break
149                 fsopts = ",".join(opts)
150
151             part = { 'ks_pnum' : ks_pnum, # Partition number in the KS file
152                      'size': size, # In sectors
153                      'mountpoint': mountpoint, # Mount relative to chroot
154                      'fstype': fstype, # Filesystem type
155                      'fsopts': fsopts, # Filesystem mount options
156                      'label': label, # Partition label
157                      'disk_name': disk_name, # physical disk name holding partition
158                      'device': None, # kpartx device node for partition
159                      'mapper_device': None, # mapper device node
160                      'mpath_device': None, # multipath device of device mapper
161                      'mount': None, # Mount object
162                      'num': None, # Partition number
163                      'boot': boot, # Bootable flag
164                      'align': align, # Partition alignment
165                      'part_type' : part_type, # Partition type
166                      'uuid': None, # Partition UUID (no-GPT use)
167                      'partuuid': None } # Partition UUID (GPT-only)
168
169             self.__add_partition(part)
170
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". """
175
176         msger.debug("Assigning %s partitions to disks" % ptable_format)
177
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)
181
182         if self._partitions_layed_out:
183             return
184
185         self._partitions_layed_out = True
186
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]
190
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']))
194
195             if p['part_type'] and ptable_format != 'gpt':
196                 # The --part-type can also be implemented for MBR partitions,
197                 # in which case it would map to the 1-byte "partition type"
198                 # filed at offset 3 of the partition entry.
199                 raise MountError("setting custom partition type is only " \
200                                  "implemented for GPT partitions")
201
202             # Get the disk where the partition is located
203             d = self.disks[p['disk_name']]
204             d['numpart'] += 1
205             d['ptable_format'] = ptable_format
206
207             if d['numpart'] == 1:
208                 if ptable_format == "msdos":
209                     overhead = MBR_OVERHEAD
210                 else:
211                     overhead = GPT_OVERHEAD
212
213                 # Skip one sector required for the partitioning scheme overhead
214                 d['offset'] += overhead
215                 # Steal few sectors from the first partition to offset for the
216                 # partitioning overhead
217                 p['size'] -= overhead
218
219             if p['align']:
220                 # If not first partition and we do have alignment set we need
221                 # to align the partition.
222                 # FIXME: This leaves a empty spaces to the disk. To fill the
223                 # gaps we could enlarge the previous partition?
224
225                 # Calc how much the alignment is off.
226                 align_sectors = d['offset'] % (p['align'] * 1024 / self.sector_size)
227                 # We need to move forward to the next alignment point
228                 align_sectors = (p['align'] * 1024 / self.sector_size) - align_sectors
229
230                 msger.debug("Realignment for %s%s with %s sectors, original"
231                             " offset %s, target alignment is %sK." %
232                             (p['disk_name'], d['numpart'], align_sectors,
233                              d['offset'], p['align']))
234
235                 # increase the offset so we actually start the partition on right alignment
236                 d['offset'] += align_sectors
237
238             p['start'] = d['offset']
239             d['offset'] += p['size']
240
241             p['type'] = 'primary'
242             p['num'] = d['numpart']
243
244             if d['ptable_format'] == "msdos":
245                 if d['numpart'] > 2:
246                     # Every logical partition requires an additional sector for
247                     # the EBR, so steal the last sector from the end of each
248                     # partition starting from the 3rd one for the EBR. This
249                     # will make sure the logical partitions are aligned
250                     # correctly.
251                     p['size'] -= 1
252
253                 if d['numpart'] > 3:
254                     p['type'] = 'logical'
255                     p['num'] = d['numpart'] + 1
256
257             d['partitions'].append(n)
258             msger.debug("Assigned %s to %s%d, sectors range %d-%d size %d "
259                         "sectors (%d bytes)." \
260                             % (p['mountpoint'], p['disk_name'], p['num'],
261                                p['start'], p['start'] + p['size'] - 1,
262                                p['size'], p['size'] * self.sector_size))
263
264         # Once all the partitions have been laid out, we can calculate the
265         # minimum disk sizes.
266         for disk_name, d in self.disks.items():
267             d['min_size'] = d['offset']
268             if d['ptable_format'] == 'gpt':
269                 # Account for the backup partition table at the end of the disk
270                 d['min_size'] += GPT_OVERHEAD
271
272             d['min_size'] *= self.sector_size
273
274     def __run_parted(self, args):
275         """ Run parted with arguments specified in the 'args' list. """
276
277         args.insert(0, self.parted)
278         msger.debug(args)
279
280         rc, out = runner.runtool(args, catch = 3)
281         out = out.strip()
282         msger.debug("'parted': exitcode: %d, output: %s" % (rc, out))
283         # We don't throw exception when return code is not 0, because
284         # parted always fails to reload part table with loop devices. This
285         # prevents us from distinguishing real errors based on return
286         # code.
287         return rc, out
288
289     def __create_partition(self, device, parttype, fstype, start, size):
290         """ Create a partition on an image described by the 'device' object. """
291
292         # Start is included to the size so we need to subtract one from the end.
293         end = start + size - 1
294         msger.debug("Added '%s' partition, sectors %d-%d, size %d sectors" %
295                     (parttype, start, end, size))
296
297         args = ["-s", device, "unit", "s", "mkpart", parttype]
298         if fstype:
299             args.extend([fstype])
300         args.extend(["%d" % start, "%d" % end])
301
302         return self.__run_parted(args)
303
304     def __format_disks(self):
305         self.layout_partitions()
306
307         if self.skipformat:
308             msger.debug("Skipping disk format, because skipformat flag is set.")
309             return
310
311         for dev in self.disks.keys():
312             d = self.disks[dev]
313             msger.debug("Initializing partition table for %s" % \
314                         (d['disk'].device))
315             self.__run_parted(["-s", d['disk'].device, "mklabel",
316                                d['ptable_format']])
317
318         msger.debug("Creating partitions")
319
320         for p in self.partitions:
321             d = self.disks[p['disk_name']]
322             if d['ptable_format'] == "msdos" and p['num'] == 5:
323                 # The last sector of the 3rd partition was reserved for the EBR
324                 # of the first _logical_ partition. This is why the extended
325                 # partition should start one sector before the first logical
326                 # partition.
327                 self.__create_partition(d['disk'].device, "extended",
328                                         None, p['start'] - 1,
329                                         d['offset'] - p['start'])
330
331             if p['fstype'] == "swap":
332                 parted_fs_type = "linux-swap"
333             elif p['fstype'] == "vfat":
334                 parted_fs_type = "fat32"
335             elif p['fstype'] == "msdos":
336                 parted_fs_type = "fat16"
337             else:
338                 # Type for ext2/ext3/ext4/btrfs
339                 parted_fs_type = "ext2"
340
341             # Boot ROM of OMAP boards require vfat boot partition to have an
342             # even number of sectors.
343             if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat", "msdos"] \
344                and p['size'] % 2:
345                 msger.debug("Substracting one sector from '%s' partition to " \
346                             "get even number of sectors for the partition" % \
347                             p['mountpoint'])
348                 p['size'] -= 1
349
350             self.__create_partition(d['disk'].device, p['type'],
351                                     parted_fs_type, p['start'], p['size'])
352
353             if p['boot']:
354                 if d['ptable_format'] == 'gpt':
355                     flag_name = "legacy_boot"
356                 else:
357                     flag_name = "boot"
358                 msger.debug("Set '%s' flag for partition '%s' on disk '%s'" % \
359                             (flag_name, p['num'], d['disk'].device))
360                 cmd = ["-s", d['disk'].device,
361                        "set", "%d" % p['num'],
362                        flag_name, "on"]
363                 exitcode, output = self.__run_parted(cmd)
364                 if exitcode != 0:
365                     msger.warning(
366                         "partition '%s' is marked with --active, "
367                         "but flag '%s' can't be set: "
368                         "exitcode: %s, output: %s"
369                         % (p['mountpoint'], flag_name, exitcode, output))
370
371         # If the partition table format is "gpt", find out PARTUUIDs for all
372         # the partitions. And if users specified custom parition type UUIDs,
373         # set them.
374         for disk_name, disk in self.disks.items():
375             if disk['ptable_format'] != 'gpt':
376                 continue
377
378             pnum = 0
379             gpt_parser = GptParser(disk['disk'].device, SECTOR_SIZE)
380             # Iterate over all GPT partitions on this disk
381             for entry in gpt_parser.get_partitions():
382                 pnum += 1
383                 # Find the matching partition in the 'self.partitions' list
384                 for n in disk['partitions']:
385                     p = self.partitions[n]
386                     if p['num'] == pnum:
387                         # Found, fetch PARTUUID (partition's unique ID)
388                         p['partuuid'] = entry['part_uuid']
389                         msger.debug("PARTUUID for partition %d on disk '%s' " \
390                                     "(mount point '%s') is '%s'" % (pnum, \
391                                     disk_name, p['mountpoint'], p['partuuid']))
392                         if p['part_type']:
393                             entry['type_uuid'] = p['part_type']
394                             msger.debug("Change type of partition %d on disk " \
395                                         "'%s' (mount point '%s') to '%s'" % \
396                                         (pnum, disk_name, p['mountpoint'],
397                                          p['part_type']))
398                             gpt_parser.change_partition(entry)
399
400             del gpt_parser
401
402     def __map_partitions(self):
403         """Load it if dm_snapshot isn't loaded. """
404         load_module("dm_snapshot")
405
406         for dev in self.disks.keys():
407             d = self.disks[dev]
408             if d['mapped']:
409                 continue
410
411             msger.debug("Running kpartx on %s" % d['disk'].device )
412             rc, kpartx_output = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
413             kpartx_output = kpartx_output.splitlines()
414
415             if rc != 0:
416                 raise MountError("Failed to query partition mapping for '%s'" %
417                                  d['disk'].device)
418
419             # Strip trailing blank and mask verbose output
420             i = 0
421             while i < len(kpartx_output) and kpartx_output[i][0:4] != "loop":
422                 i = i + 1
423             kpartx_output = kpartx_output[i:]
424
425             # Make sure kpartx reported the right count of partitions
426             if len(kpartx_output) != d['numpart']:
427                 # If this disk has more than 3 partitions, then in case of MBR
428                 # paritions there is an extended parition. Different versions
429                 # of kpartx behave differently WRT the extended partition -
430                 # some map it, some ignore it. This is why we do the below hack
431                 # - if kpartx reported one more partition and the partition
432                 # table type is "msdos" and the amount of partitions is more
433                 # than 3, we just assume kpartx mapped the extended parition
434                 # and we remove it.
435                 if len(kpartx_output) == d['numpart'] + 1 \
436                    and d['ptable_format'] == 'msdos' and len(kpartx_output) > 3:
437                     kpartx_output.pop(3)
438                 else:
439                     raise MountError("Unexpected number of partitions from " \
440                                      "kpartx: %d != %d" % \
441                                         (len(kpartx_output), d['numpart']))
442
443             for i in range(len(kpartx_output)):
444                 line = kpartx_output[i]
445                 newdev = line.split()[0]
446                 mapperdev = "/dev/mapper/" + newdev
447                 loopdev = d['disk'].device + newdev[-1]
448
449                 msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
450                 pnum = d['partitions'][i]
451                 self.partitions[pnum]['device'] = loopdev
452                 self.partitions[pnum]['mapper_device'] = mapperdev
453
454                 # grub's install wants partitions to be named
455                 # to match their parent device + partition num
456                 # kpartx doesn't work like this, so we add compat
457                 # symlinks to point to /dev/mapper
458                 if os.path.lexists(loopdev):
459                     os.unlink(loopdev)
460                 os.symlink(mapperdev, loopdev)
461
462             msger.debug("Adding partx mapping for %s" % d['disk'].device)
463             rc = runner.show([self.kpartx, "-v", "-sa", d['disk'].device])
464
465             if rc != 0:
466                 # Make sure that the device maps are also removed on error case.
467                 # The d['mapped'] isn't set to True if the kpartx fails so
468                 # failed mapping will not be cleaned on cleanup either.
469                 runner.quiet([self.kpartx, "-sd", d['disk'].device])
470                 raise MountError("Failed to map partitions for '%s'" %
471                                  d['disk'].device)
472
473             for p in self.partitions:
474                 if p['mapper_device'] and os.path.islink(p['mapper_device']):
475                     p['mpath_device'] = resolve_ref(p['mapper_device'])
476                 else:
477                     p['mpath_device'] = ''
478
479             # FIXME: need a better way to fix the latency
480             import time
481             time.sleep(1)
482
483             if not os.path.exists(mapperdev):
484                 # load mapper device if not updated
485                 runner.quiet([self.dmsetup, "mknodes"])
486                 # still not updated, roll back
487                 if not os.path.exists(mapperdev):
488                     runner.quiet([self.kpartx, "-sd", d['disk'].device])
489                     raise MountError("Failed to load mapper devices for '%s'" %
490                                      d['disk'].device)
491
492             d['mapped'] = True
493
494     def __unmap_partitions(self):
495         for dev in self.disks.keys():
496             d = self.disks[dev]
497             if not d['mapped']:
498                 continue
499
500             msger.debug("Removing compat symlinks")
501             for pnum in d['partitions']:
502                 if self.partitions[pnum]['device'] != None:
503                     os.unlink(self.partitions[pnum]['device'])
504                     self.partitions[pnum]['device'] = None
505
506             msger.debug("Unmapping %s" % d['disk'].device)
507             rc = runner.quiet([self.kpartx, "-sd", d['disk'].device])
508             if rc != 0:
509                 raise MountError("Failed to unmap partitions for '%s'" %
510                                  d['disk'].device)
511
512             d['mapped'] = False
513
514     def __calculate_mountorder(self):
515         msger.debug("Calculating mount order")
516         for p in self.partitions:
517             if p['mountpoint']:
518                 self.mount_order.append(p['mountpoint'])
519                 self.unmount_order.append(p['mountpoint'])
520
521         self.mount_order.sort()
522         self.unmount_order.sort()
523         self.unmount_order.reverse()
524
525     def cleanup(self):
526         Mount.cleanup(self)
527         if self.disks:
528             self.__unmap_partitions()
529             for dev in self.disks.keys():
530                 d = self.disks[dev]
531                 try:
532                     d['disk'].cleanup()
533                 except:
534                     pass
535
536     def unmount(self):
537         self.__unmount_subvolumes()
538         for mp in self.unmount_order:
539             if mp == 'swap':
540                 continue
541             p = None
542             for p1 in self.partitions:
543                 if p1['mountpoint'] == mp:
544                     p = p1
545                     break
546
547             if p['mount'] != None:
548                 try:
549                     # Create subvolume snapshot here
550                     if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and \
551                             not self.snapshot_created:
552                         self.__create_subvolume_snapshots(p, p["mount"])
553                     p['mount'].cleanup()
554                 except:
555                     pass
556                 p['mount'] = None
557
558     # Only for btrfs
559     def __get_subvolume_id(self, rootpath, subvol):
560         if not self.btrfscmd:
561             self.btrfscmd = find_binary_path("btrfs")
562         argv = [ self.btrfscmd, "subvolume", "list", rootpath ]
563
564         rc, out = runner.runtool(argv)
565         msger.debug(out)
566
567         if rc != 0:
568             raise MountError("Failed to get subvolume id from %s',"
569                     "return code: %d." % (rootpath, rc))
570
571         subvolid = -1
572         for line in out.splitlines():
573             if line.endswith(" path %s" % subvol):
574                 subvolid = line.split()[1]
575                 if not subvolid.isdigit():
576                     raise MountError("Invalid subvolume id: %s" % subvolid)
577                 subvolid = int(subvolid)
578                 break
579         return subvolid
580
581     def __create_subvolume_metadata(self, p, pdisk):
582         if len(self.subvolumes) == 0:
583             return
584
585         argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
586         rc, out = runner.runtool(argv)
587         msger.debug(out)
588
589         if rc != 0:
590             raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))
591
592         subvolid_items = out.splitlines()
593         subvolume_metadata = ""
594         for subvol in self.subvolumes:
595             for line in subvolid_items:
596                 if line.endswith(" path %s" % subvol["subvol"]):
597                     subvolid = line.split()[1]
598                     if not subvolid.isdigit():
599                         raise MountError("Invalid subvolume id: %s" % subvolid)
600
601                     subvolid = int(subvolid)
602                     opts = subvol["fsopts"].split(",")
603                     for opt in opts:
604                         if opt.strip().startswith("subvol="):
605                             opts.remove(opt)
606                             break
607                     fsopts = ",".join(opts)
608                     subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"],
609                             subvol['mountpoint'], fsopts)
610
611         if subvolume_metadata:
612             fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
613             fd.write(subvolume_metadata)
614             fd.close()
615
616     def __get_subvolume_metadata(self, p, pdisk):
617         subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
618         if not os.path.exists(subvolume_metadata_file):
619             return
620
621         fd = open(subvolume_metadata_file, "r")
622         content = fd.read()
623         fd.close()
624
625         for line in content.splitlines():
626             items = line.split("\t")
627             if items and len(items) == 4:
628                 self.subvolumes.append({'size': 0, # In sectors
629                                         'mountpoint': items[2], # Mount relative to chroot
630                                         'fstype': "btrfs", # Filesystem type
631                                         'fsopts': items[3] + ",subvol=%s" %  items[1], # Filesystem mount options
632                                         'disk_name': p['disk_name'], # physical disk name holding partition
633                                         'device': None, # kpartx device node for partition
634                                         'mount': None, # Mount object
635                                         'subvol': items[1], # Subvolume name
636                                         'boot': False, # Bootable flag
637                                         'mounted': False # Mount flag
638                                    })
639
640     def __create_subvolumes(self, p, pdisk):
641         """ Create all the subvolumes. """
642
643         for subvol in self.subvolumes:
644             argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]
645
646             rc = runner.show(argv)
647             if rc != 0:
648                 raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))
649
650         # Set default subvolume, subvolume for "/" is default
651         subvol = None
652         for subvolume in self.subvolumes:
653             if subvolume["mountpoint"] == "/" and p['disk_name'] == subvolume['disk_name']:
654                 subvol = subvolume
655                 break
656
657         if subvol:
658             # Get default subvolume id
659             subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
660             # Set default subvolume
661             if subvolid != -1:
662                 rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" %
663                         subvolid, pdisk.mountdir])
664                 if rc != 0:
665                     raise MountError("Failed to set default subvolume id: %d', return code: %d."
666                             % (subvolid, rc))
667
668         self.__create_subvolume_metadata(p, pdisk)
669
670     def __mount_subvolumes(self, p, pdisk):
671         if self.skipformat:
672             # Get subvolume info
673             self.__get_subvolume_metadata(p, pdisk)
674             # Set default mount options
675             if len(self.subvolumes) != 0:
676                 for subvol in self.subvolumes:
677                     if subvol["mountpoint"] == p["mountpoint"] == "/":
678                         opts = subvol["fsopts"].split(",")
679                         for opt in opts:
680                             if opt.strip().startswith("subvol="):
681                                 opts.remove(opt)
682                                 break
683                         pdisk.fsopts = ",".join(opts)
684                         break
685
686         if len(self.subvolumes) == 0:
687             # Return directly if no subvolumes
688             return
689
690         # Remount to make default subvolume mounted
691         rc = runner.show([self.umountcmd, pdisk.mountdir])
692         if rc != 0:
693             raise MountError("Failed to umount %s" % pdisk.mountdir)
694
695         rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
696         if rc != 0:
697             raise MountError("Failed to umount %s" % pdisk.mountdir)
698
699         for subvol in self.subvolumes:
700             if subvol["mountpoint"] == "/":
701                 continue
702             subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
703             if subvolid == -1:
704                 msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
705                 continue
706             # Replace subvolume name with subvolume ID
707             opts = subvol["fsopts"].split(",")
708             for opt in opts:
709                 if opt.strip().startswith("subvol="):
710                     opts.remove(opt)
711                     break
712
713             opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
714             fsopts = ",".join(opts)
715             subvol['fsopts'] = fsopts
716             mountpoint = self.mountdir + subvol['mountpoint']
717             makedirs(mountpoint)
718             rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
719             if rc != 0:
720                 raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
721             subvol["mounted"] = True
722
723     def __unmount_subvolumes(self):
724         """ It may be called multiple times, so we need to chekc if it is still mounted. """
725         for subvol in self.subvolumes:
726             if subvol["mountpoint"] == "/":
727                 continue
728             if not subvol["mounted"]:
729                 continue
730             mountpoint = self.mountdir + subvol['mountpoint']
731             rc = runner.show([self.umountcmd, mountpoint])
732             if rc != 0:
733                 raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
734             subvol["mounted"] = False
735
736     def __create_subvolume_snapshots(self, p, pdisk):
737         import time
738
739         if self.snapshot_created:
740             return
741
742         # Remount with subvolid=0
743         rc = runner.show([self.umountcmd, pdisk.mountdir])
744         if rc != 0:
745             raise MountError("Failed to umount %s" % pdisk.mountdir)
746         if pdisk.fsopts:
747             mountopts = pdisk.fsopts + ",subvolid=0"
748         else:
749             mountopts = "subvolid=0"
750         rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
751         if rc != 0:
752             raise MountError("Failed to umount %s" % pdisk.mountdir)
753
754         # Create all the subvolume snapshots
755         snapshotts = time.strftime("%Y%m%d-%H%M")
756         for subvol in self.subvolumes:
757             subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
758             snapshotpath = subvolpath + "_%s-1" % snapshotts
759             rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
760             if rc != 0:
761                 raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code:"
762                         "%d." % (snapshotpath, subvolpath, rc))
763
764         self.snapshot_created = True
765
766     def mount(self):
767         for dev in self.disks.keys():
768             d = self.disks[dev]
769             d['disk'].create()
770
771         self.__format_disks()
772         self.__map_partitions()
773         self.__calculate_mountorder()
774
775         for mp in self.mount_order:
776             p = None
777             for p1 in self.partitions:
778                 if p1['mountpoint'] == mp:
779                     p = p1
780                     break
781
782             if not p['label']:
783                 if p['mountpoint'] == "/":
784                     p['label'] = 'platform'
785                 else:
786                     p['label'] = mp.split('/')[-1]
787
788             if mp == 'swap':
789                 import uuid
790                 p['uuid'] = str(uuid.uuid1())
791                 runner.show([self.mkswap,
792                              '-L', p['label'],
793                              '-U', p['uuid'],
794                              p['device']])
795                 continue
796
797             rmmountdir = False
798             if p['mountpoint'] == "/":
799                 rmmountdir = True
800             if p['fstype'] == "vfat" or p['fstype'] == "msdos":
801                 my_disk_mount = VfatDiskMount
802             elif p['fstype'] in ("ext2", "ext3", "ext4"):
803                 my_disk_mount = ExtDiskMount
804             elif p['fstype'] == "btrfs":
805                 my_disk_mount = BtrfsDiskMount
806             else:
807                 raise MountError("Fail to support file system " + p['fstype'])
808
809             if p['fstype'] == "btrfs" and not p['fsopts']:
810                 p['fsopts'] = "subvolid=0"
811
812             pdisk = my_disk_mount(RawDisk(p['size'] * self.sector_size, p['device']),
813                                  self.mountdir + p['mountpoint'],
814                                  p['fstype'],
815                                  4096,
816                                  p['label'],
817                                  rmmountdir,
818                                  self.skipformat,
819                                  fsopts = p['fsopts'])
820             pdisk.mount(pdisk.fsopts)
821             if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
822                 if not self.skipformat:
823                     self.__create_subvolumes(p, pdisk)
824                 self.__mount_subvolumes(p, pdisk)
825             p['mount'] = pdisk
826             p['uuid'] = pdisk.uuid
827
828     def resparse(self, size = None):
829         # Can't re-sparse a disk image - too hard
830         pass