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