#!/usr/bin/python -tt
#
# Copyright (c) 2009, 2010, 2011 Intel, Inc.
# Copyright (c) 2007, 2008 Red Hat, Inc.
# Copyright (c) 2008 Daniel P. Berrange
# Copyright (c) 2008 David P. Huff
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; version 2 of the License
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc., 59
# Temple Place - Suite 330, Boston, MA 02111-1307, USA.

import os

from mic import msger
from mic.utils import runner
from mic.utils.errors import MountError, CreatorError
from mic.utils.fs_related import *
from mic.utils.gpt_parser import GptParser

# Overhead of the MBR partitioning scheme (just one sector)
MBR_OVERHEAD = 1
# Overhead of the GPT partitioning scheme
GPT_OVERHEAD = 34

# Size of a sector in bytes
SECTOR_SIZE = 512

def resolve_ref(ref):
    real = os.readlink(ref)
    if not real.startswith('/'):
        return os.path.realpath(os.path.join(ref, real))
    else:
        return real

class PartitionedMount(Mount):
    def __init__(self, mountdir, skipformat = False):
        Mount.__init__(self, mountdir)
        self.disks = {}
        self.partitions = []
        self.subvolumes = []
        self.mapped = False
        self.mount_order = []
        self.unmount_order = []
        self.parted = find_binary_path("parted")
        self.kpartx = find_binary_path("kpartx")
        self.mkswap = find_binary_path("mkswap")
        self.dmsetup = find_binary_path("dmsetup")
        self.btrfscmd = None
        self.mountcmd = find_binary_path("mount")
        self.umountcmd = find_binary_path("umount")
        self.skipformat = skipformat
        self.snapshot_created = self.skipformat
        # Size of a sector used in calculations
        self.sector_size = SECTOR_SIZE
        self._partitions_layed_out = False

    def __add_disk(self, disk_name):
        """ Add a disk 'disk_name' to the internal list of disks. Note,
        'disk_name' is the name of the disk in the target system
        (e.g., sdb). """

        if disk_name in self.disks:
            # We already have this disk
            return

        assert not self._partitions_layed_out

        self.disks[disk_name] = \
                { 'disk': None,     # Disk object
                  'mapped': False,  # True if kpartx mapping exists
                  'numpart': 0,     # Number of allocate partitions
                  'partitions': [], # Indexes to self.partitions
                  'offset': 0,      # Offset of next partition (in sectors)
                  # Minimum required disk size to fit all partitions (in bytes)
                  'min_size': 0,
                  'ptable_format': "msdos" } # Partition table format

    def add_disk(self, disk_name, disk_obj):
        """ Add a disk object which have to be partitioned. More than one disk
        can be added. In case of multiple disks, disk partitions have to be
        added for each disk separately with 'add_partition()". """

        self.__add_disk(disk_name)
        self.disks[disk_name]['disk'] = disk_obj

    def __add_partition(self, part):
        """ This is a helper function for 'add_partition()' which adds a
        partition to the internal list of partitions. """

        assert not self._partitions_layed_out

        self.partitions.append(part)
        self.__add_disk(part['disk_name'])

    def add_partition(self, size, disk_name, mountpoint, fstype = None,
                      label=None, fsopts = None, boot = False, align = None,
                      part_type = None):
        """ Add the next partition. Partitions have to be added in the
        first-to-last order. """

        ks_pnum = len(self.partitions)

        # Converting MB to sectors for parted
        size = size * 1024 * 1024 / self.sector_size

        # We need to handle subvolumes for btrfs
        if fstype == "btrfs" and fsopts and fsopts.find("subvol=") != -1:
            self.btrfscmd = find_binary_path("btrfs")
            subvol = None
            opts = fsopts.split(",")
            for opt in opts:
                if opt.find("subvol=") != -1:
                    subvol = opt.replace("subvol=", "").strip()
                    break
            if not subvol:
                raise MountError("No subvolume: %s" % fsopts)
            self.subvolumes.append({'size': size, # In sectors
                                    'mountpoint': mountpoint, # Mount relative to chroot
                                    'fstype': fstype, # Filesystem type
                                    'fsopts': fsopts, # Filesystem mount options
                                    'disk_name': disk_name, # physical disk name holding partition
                                    'device': None, # kpartx device node for partition
                                    'mapper_device': None, # mapper device node
                                    'mpath_device': None, # multipath device of device mapper
                                    'mount': None, # Mount object
                                    'subvol': subvol, # Subvolume name
                                    'boot': boot, # Bootable flag
                                    'mounted': False # Mount flag
                                   })

        # We still need partition for "/" or non-subvolume
        if mountpoint == "/" or not fsopts or fsopts.find("subvol=") == -1:
            # Don't need subvolume for "/" because it will be set as default subvolume
            if fsopts and fsopts.find("subvol=") != -1:
                opts = fsopts.split(",")
                for opt in opts:
                    if opt.strip().startswith("subvol="):
                        opts.remove(opt)
                        break
                fsopts = ",".join(opts)

            part = { 'ks_pnum' : ks_pnum, # Partition number in the KS file
                     'size': size, # In sectors
                     'mountpoint': mountpoint, # Mount relative to chroot
                     'fstype': fstype, # Filesystem type
                     'fsopts': fsopts, # Filesystem mount options
                     'label': label, # Partition label
                     'disk_name': disk_name, # physical disk name holding partition
                     'device': None, # kpartx device node for partition
                     'mapper_device': None, # mapper device node
                     'mpath_device': None, # multipath device of device mapper
                     'mount': None, # Mount object
                     'num': None, # Partition number
                     'boot': boot, # Bootable flag
                     'align': align, # Partition alignment
                     'part_type' : part_type, # Partition type
                     'uuid': None, # Partition UUID (no-GPT use)
                     'partuuid': None } # Partition UUID (GPT-only)

            self.__add_partition(part)

    def layout_partitions(self, ptable_format = "msdos"):
        """ Layout the partitions, meaning calculate the position of every
        partition on the disk. The 'ptable_format' parameter defines the
        partition table format, and may be either "msdos" or "gpt". """

        msger.debug("Assigning %s partitions to disks" % ptable_format)

        if ptable_format not in ('msdos', 'gpt'):
            raise MountError("Unknown partition table format '%s', supported " \
                             "formats are: 'msdos' and 'gpt'" % ptable_format)

        if self._partitions_layed_out:
            return

        self._partitions_layed_out = True

        # Go through partitions in the order they are added in .ks file
        for n in range(len(self.partitions)):
            p = self.partitions[n]

            if not self.disks.has_key(p['disk_name']):
                raise MountError("No disk %s for partition %s" \
                                 % (p['disk_name'], p['mountpoint']))

            if p['part_type'] and ptable_format != 'gpt':
                # The --part-type can also be implemented for MBR partitions,
                # in which case it would map to the 1-byte "partition type"
                # filed at offset 3 of the partition entry.
                raise MountError("setting custom partition type is only " \
                                 "implemented for GPT partitions")

            # Get the disk where the partition is located
            d = self.disks[p['disk_name']]
            d['numpart'] += 1
            d['ptable_format'] = ptable_format

            if d['numpart'] == 1:
                if ptable_format == "msdos":
                    overhead = MBR_OVERHEAD
                else:
                    overhead = GPT_OVERHEAD

                # Skip one sector required for the partitioning scheme overhead
                d['offset'] += overhead
                # Steal few sectors from the first partition to offset for the
                # partitioning overhead
                p['size'] -= overhead

            if p['align']:
                # If not first partition and we do have alignment set we need
                # to align the partition.
                # FIXME: This leaves a empty spaces to the disk. To fill the
                # gaps we could enlarge the previous partition?

                # Calc how much the alignment is off.
                align_sectors = d['offset'] % (p['align'] * 1024 / self.sector_size)
                # We need to move forward to the next alignment point
                align_sectors = (p['align'] * 1024 / self.sector_size) - align_sectors

                msger.debug("Realignment for %s%s with %s sectors, original"
                            " offset %s, target alignment is %sK." %
                            (p['disk_name'], d['numpart'], align_sectors,
                             d['offset'], p['align']))

                # increase the offset so we actually start the partition on right alignment
                d['offset'] += align_sectors

            p['start'] = d['offset']
            d['offset'] += p['size']

            p['type'] = 'primary'
            p['num'] = d['numpart']

            if d['ptable_format'] == "msdos":
                if d['numpart'] > 2:
                    # Every logical partition requires an additional sector for
                    # the EBR, so steal the last sector from the end of each
                    # partition starting from the 3rd one for the EBR. This
                    # will make sure the logical partitions are aligned
                    # correctly.
                    p['size'] -= 1

                if d['numpart'] > 3:
                    p['type'] = 'logical'
                    p['num'] = d['numpart'] + 1

            d['partitions'].append(n)
            msger.debug("Assigned %s to %s%d, sectors range %d-%d size %d "
                        "sectors (%d bytes)." \
                            % (p['mountpoint'], p['disk_name'], p['num'],
                               p['start'], p['start'] + p['size'] - 1,
                               p['size'], p['size'] * self.sector_size))

        # Once all the partitions have been laid out, we can calculate the
        # minimum disk sizes.
        for disk_name, d in self.disks.items():
            d['min_size'] = d['offset']
            if d['ptable_format'] == 'gpt':
                # Account for the backup partition table at the end of the disk
                d['min_size'] += GPT_OVERHEAD

            d['min_size'] *= self.sector_size

    def __run_parted(self, args):
        """ Run parted with arguments specified in the 'args' list. """

        args.insert(0, self.parted)
        msger.debug(args)

        rc, out = runner.runtool(args, catch = 3)
        out = out.strip()
        msger.debug("'parted': exitcode: %d, output: %s" % (rc, out))
        # We don't throw exception when return code is not 0, because
        # parted always fails to reload part table with loop devices. This
        # prevents us from distinguishing real errors based on return
        # code.
        return rc, out

    def __create_partition(self, device, parttype, fstype, start, size):
        """ Create a partition on an image described by the 'device' object. """

        # Start is included to the size so we need to subtract one from the end.
        end = start + size - 1
        msger.debug("Added '%s' partition, sectors %d-%d, size %d sectors" %
                    (parttype, start, end, size))

        args = ["-s", device, "unit", "s", "mkpart", parttype]
        if fstype:
            args.extend([fstype])
        args.extend(["%d" % start, "%d" % end])

        return self.__run_parted(args)

    def __format_disks(self):
        self.layout_partitions()

        if self.skipformat:
            msger.debug("Skipping disk format, because skipformat flag is set.")
            return

        for dev in self.disks.keys():
            d = self.disks[dev]
            msger.debug("Initializing partition table for %s" % \
                        (d['disk'].device))
            self.__run_parted(["-s", d['disk'].device, "mklabel",
                               d['ptable_format']])

        msger.debug("Creating partitions")

        for p in self.partitions:
            d = self.disks[p['disk_name']]
            if d['ptable_format'] == "msdos" and p['num'] == 5:
                # The last sector of the 3rd partition was reserved for the EBR
                # of the first _logical_ partition. This is why the extended
                # partition should start one sector before the first logical
                # partition.
                self.__create_partition(d['disk'].device, "extended",
                                        None, p['start'] - 1,
                                        d['offset'] - p['start'])

            if p['fstype'] == "swap":
                parted_fs_type = "linux-swap"
            elif p['fstype'] == "vfat":
                parted_fs_type = "fat32"
            elif p['fstype'] == "msdos":
                parted_fs_type = "fat16"
            else:
                # Type for ext2/ext3/ext4/btrfs
                parted_fs_type = "ext2"

            # Boot ROM of OMAP boards require vfat boot partition to have an
            # even number of sectors.
            if p['mountpoint'] == "/boot" and p['fstype'] in ["vfat", "msdos"] \
               and p['size'] % 2:
                msger.debug("Substracting one sector from '%s' partition to " \
                            "get even number of sectors for the partition" % \
                            p['mountpoint'])
                p['size'] -= 1

            self.__create_partition(d['disk'].device, p['type'],
                                    parted_fs_type, p['start'], p['size'])

            if p['boot']:
                if d['ptable_format'] == 'gpt':
                    flag_name = "legacy_boot"
                else:
                    flag_name = "boot"
                msger.debug("Set '%s' flag for partition '%s' on disk '%s'" % \
                            (flag_name, p['num'], d['disk'].device))
                cmd = ["-s", d['disk'].device,
                       "set", "%d" % p['num'],
                       flag_name, "on"]
                exitcode, output = self.__run_parted(cmd)
                if exitcode != 0:
                    msger.warning(
                        "partition '%s' is marked with --active, "
                        "but flag '%s' can't be set: "
                        "exitcode: %s, output: %s"
                        % (p['mountpoint'], flag_name, exitcode, output))

        # If the partition table format is "gpt", find out PARTUUIDs for all
        # the partitions. And if users specified custom parition type UUIDs,
        # set them.
        for disk_name, disk in self.disks.items():
            if disk['ptable_format'] != 'gpt':
                continue

            pnum = 0
            gpt_parser = GptParser(disk['disk'].device, SECTOR_SIZE)
            # Iterate over all GPT partitions on this disk
            for entry in gpt_parser.get_partitions():
                pnum += 1
                # Find the matching partition in the 'self.partitions' list
                for n in disk['partitions']:
                    p = self.partitions[n]
                    if p['num'] == pnum:
                        # Found, fetch PARTUUID (partition's unique ID)
                        p['partuuid'] = entry['part_uuid']
                        msger.debug("PARTUUID for partition %d on disk '%s' " \
                                    "(mount point '%s') is '%s'" % (pnum, \
                                    disk_name, p['mountpoint'], p['partuuid']))
                        if p['part_type']:
                            entry['type_uuid'] = p['part_type']
                            msger.debug("Change type of partition %d on disk " \
                                        "'%s' (mount point '%s') to '%s'" % \
                                        (pnum, disk_name, p['mountpoint'],
                                         p['part_type']))
                            gpt_parser.change_partition(entry)

            del gpt_parser

    def __map_partitions(self):
        """Load it if dm_snapshot isn't loaded. """
        load_module("dm_snapshot")

        for dev in self.disks.keys():
            d = self.disks[dev]
            if d['mapped']:
                continue

            msger.debug("Running kpartx on %s" % d['disk'].device )
            rc, kpartx_output = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
            kpartx_output = kpartx_output.splitlines()

            if rc != 0:
                raise MountError("Failed to query partition mapping for '%s'" %
                                 d['disk'].device)

            # Strip trailing blank and mask verbose output
            i = 0
            while i < len(kpartx_output) and kpartx_output[i][0:4] != "loop":
                i = i + 1
            kpartx_output = kpartx_output[i:]

            # Make sure kpartx reported the right count of partitions
            if len(kpartx_output) != d['numpart']:
                # If this disk has more than 3 partitions, then in case of MBR
                # paritions there is an extended parition. Different versions
                # of kpartx behave differently WRT the extended partition -
                # some map it, some ignore it. This is why we do the below hack
                # - if kpartx reported one more partition and the partition
                # table type is "msdos" and the amount of partitions is more
                # than 3, we just assume kpartx mapped the extended parition
                # and we remove it.
                if len(kpartx_output) == d['numpart'] + 1 \
                   and d['ptable_format'] == 'msdos' and len(kpartx_output) > 3:
                    kpartx_output.pop(3)
                else:
                    raise MountError("Unexpected number of partitions from " \
                                     "kpartx: %d != %d" % \
                                        (len(kpartx_output), d['numpart']))

            for i in range(len(kpartx_output)):
                line = kpartx_output[i]
                newdev = line.split()[0]
                mapperdev = "/dev/mapper/" + newdev
                loopdev = d['disk'].device + newdev[-1]

                msger.debug("Dev %s: %s -> %s" % (newdev, loopdev, mapperdev))
                pnum = d['partitions'][i]
                self.partitions[pnum]['device'] = loopdev
                self.partitions[pnum]['mapper_device'] = mapperdev

                # grub's install wants partitions to be named
                # to match their parent device + partition num
                # kpartx doesn't work like this, so we add compat
                # symlinks to point to /dev/mapper
                if os.path.lexists(loopdev):
                    os.unlink(loopdev)
                os.symlink(mapperdev, loopdev)

            msger.debug("Adding partx mapping for %s" % d['disk'].device)
            rc = runner.show([self.kpartx, "-v", "-sa", d['disk'].device])

            if rc != 0:
                # Make sure that the device maps are also removed on error case.
                # The d['mapped'] isn't set to True if the kpartx fails so
                # failed mapping will not be cleaned on cleanup either.
                runner.quiet([self.kpartx, "-sd", d['disk'].device])
                raise MountError("Failed to map partitions for '%s'" %
                                 d['disk'].device)

            for p in self.partitions:
                if p['mapper_device'] and os.path.islink(p['mapper_device']):
                    p['mpath_device'] = resolve_ref(p['mapper_device'])
                else:
                    p['mpath_device'] = ''

            # FIXME: need a better way to fix the latency
            import time
            time.sleep(1)

            if not os.path.exists(mapperdev):
                # load mapper device if not updated
                runner.quiet([self.dmsetup, "mknodes"])
                # still not updated, roll back
                if not os.path.exists(mapperdev):
                    runner.quiet([self.kpartx, "-sd", d['disk'].device])
                    raise MountError("Failed to load mapper devices for '%s'" %
                                     d['disk'].device)

            d['mapped'] = True

    def __unmap_partitions(self):
        for dev in self.disks.keys():
            d = self.disks[dev]
            if not d['mapped']:
                continue

            msger.debug("Removing compat symlinks")
            for pnum in d['partitions']:
                if self.partitions[pnum]['device'] != None:
                    os.unlink(self.partitions[pnum]['device'])
                    self.partitions[pnum]['device'] = None

            msger.debug("Unmapping %s" % d['disk'].device)
            rc = runner.quiet([self.kpartx, "-sd", d['disk'].device])
            if rc != 0:
                raise MountError("Failed to unmap partitions for '%s'" %
                                 d['disk'].device)

            d['mapped'] = False

    def __calculate_mountorder(self):
        msger.debug("Calculating mount order")
        for p in self.partitions:
            if p['mountpoint']:
                self.mount_order.append(p['mountpoint'])
                self.unmount_order.append(p['mountpoint'])

        self.mount_order.sort()
        self.unmount_order.sort()
        self.unmount_order.reverse()

    def cleanup(self):
        Mount.cleanup(self)
        if self.disks:
            self.__unmap_partitions()
            for dev in self.disks.keys():
                d = self.disks[dev]
                try:
                    d['disk'].cleanup()
                except:
                    pass

    def unmount(self):
        self.__unmount_subvolumes()
        for mp in self.unmount_order:
            if mp == 'swap':
                continue
            p = None
            for p1 in self.partitions:
                if p1['mountpoint'] == mp:
                    p = p1
                    break

            if p['mount'] != None:
                try:
                    # Create subvolume snapshot here
                    if p['fstype'] == "btrfs" and p['mountpoint'] == "/" and \
                            not self.snapshot_created:
                        self.__create_subvolume_snapshots(p, p["mount"])
                    p['mount'].cleanup()
                except:
                    pass
                p['mount'] = None

    # Only for btrfs
    def __get_subvolume_id(self, rootpath, subvol):
        if not self.btrfscmd:
            self.btrfscmd = find_binary_path("btrfs")
        argv = [ self.btrfscmd, "subvolume", "list", rootpath ]

        rc, out = runner.runtool(argv)
        msger.debug(out)

        if rc != 0:
            raise MountError("Failed to get subvolume id from %s',"
                    "return code: %d." % (rootpath, rc))

        subvolid = -1
        for line in out.splitlines():
            if line.endswith(" path %s" % subvol):
                subvolid = line.split()[1]
                if not subvolid.isdigit():
                    raise MountError("Invalid subvolume id: %s" % subvolid)
                subvolid = int(subvolid)
                break
        return subvolid

    def __create_subvolume_metadata(self, p, pdisk):
        if len(self.subvolumes) == 0:
            return

        argv = [ self.btrfscmd, "subvolume", "list", pdisk.mountdir ]
        rc, out = runner.runtool(argv)
        msger.debug(out)

        if rc != 0:
            raise MountError("Failed to get subvolume id from %s', return code: %d." % (pdisk.mountdir, rc))

        subvolid_items = out.splitlines()
        subvolume_metadata = ""
        for subvol in self.subvolumes:
            for line in subvolid_items:
                if line.endswith(" path %s" % subvol["subvol"]):
                    subvolid = line.split()[1]
                    if not subvolid.isdigit():
                        raise MountError("Invalid subvolume id: %s" % subvolid)

                    subvolid = int(subvolid)
                    opts = subvol["fsopts"].split(",")
                    for opt in opts:
                        if opt.strip().startswith("subvol="):
                            opts.remove(opt)
                            break
                    fsopts = ",".join(opts)
                    subvolume_metadata += "%d\t%s\t%s\t%s\n" % (subvolid, subvol["subvol"],
                            subvol['mountpoint'], fsopts)

        if subvolume_metadata:
            fd = open("%s/.subvolume_metadata" % pdisk.mountdir, "w")
            fd.write(subvolume_metadata)
            fd.close()

    def __get_subvolume_metadata(self, p, pdisk):
        subvolume_metadata_file = "%s/.subvolume_metadata" % pdisk.mountdir
        if not os.path.exists(subvolume_metadata_file):
            return

        fd = open(subvolume_metadata_file, "r")
        content = fd.read()
        fd.close()

        for line in content.splitlines():
            items = line.split("\t")
            if items and len(items) == 4:
                self.subvolumes.append({'size': 0, # In sectors
                                        'mountpoint': items[2], # Mount relative to chroot
                                        'fstype': "btrfs", # Filesystem type
                                        'fsopts': items[3] + ",subvol=%s" %  items[1], # Filesystem mount options
                                        'disk_name': p['disk_name'], # physical disk name holding partition
                                        'device': None, # kpartx device node for partition
                                        'mount': None, # Mount object
                                        'subvol': items[1], # Subvolume name
                                        'boot': False, # Bootable flag
                                        'mounted': False # Mount flag
                                   })

    def __create_subvolumes(self, p, pdisk):
        """ Create all the subvolumes. """

        for subvol in self.subvolumes:
            argv = [ self.btrfscmd, "subvolume", "create", pdisk.mountdir + "/" + subvol["subvol"]]

            rc = runner.show(argv)
            if rc != 0:
                raise MountError("Failed to create subvolume '%s', return code: %d." % (subvol["subvol"], rc))

        # Set default subvolume, subvolume for "/" is default
        subvol = None
        for subvolume in self.subvolumes:
            if subvolume["mountpoint"] == "/" and p['disk_name'] == subvolume['disk_name']:
                subvol = subvolume
                break

        if subvol:
            # Get default subvolume id
            subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
            # Set default subvolume
            if subvolid != -1:
                rc = runner.show([ self.btrfscmd, "subvolume", "set-default", "%d" %
                        subvolid, pdisk.mountdir])
                if rc != 0:
                    raise MountError("Failed to set default subvolume id: %d', return code: %d."
                            % (subvolid, rc))

        self.__create_subvolume_metadata(p, pdisk)

    def __mount_subvolumes(self, p, pdisk):
        if self.skipformat:
            # Get subvolume info
            self.__get_subvolume_metadata(p, pdisk)
            # Set default mount options
            if len(self.subvolumes) != 0:
                for subvol in self.subvolumes:
                    if subvol["mountpoint"] == p["mountpoint"] == "/":
                        opts = subvol["fsopts"].split(",")
                        for opt in opts:
                            if opt.strip().startswith("subvol="):
                                opts.remove(opt)
                                break
                        pdisk.fsopts = ",".join(opts)
                        break

        if len(self.subvolumes) == 0:
            # Return directly if no subvolumes
            return

        # Remount to make default subvolume mounted
        rc = runner.show([self.umountcmd, pdisk.mountdir])
        if rc != 0:
            raise MountError("Failed to umount %s" % pdisk.mountdir)

        rc = runner.show([self.mountcmd, "-o", pdisk.fsopts, pdisk.disk.device, pdisk.mountdir])
        if rc != 0:
            raise MountError("Failed to umount %s" % pdisk.mountdir)

        for subvol in self.subvolumes:
            if subvol["mountpoint"] == "/":
                continue
            subvolid = self. __get_subvolume_id(pdisk.mountdir, subvol["subvol"])
            if subvolid == -1:
                msger.debug("WARNING: invalid subvolume %s" % subvol["subvol"])
                continue
            # Replace subvolume name with subvolume ID
            opts = subvol["fsopts"].split(",")
            for opt in opts:
                if opt.strip().startswith("subvol="):
                    opts.remove(opt)
                    break

            opts.extend(["subvolrootid=0", "subvol=%s" % subvol["subvol"]])
            fsopts = ",".join(opts)
            subvol['fsopts'] = fsopts
            mountpoint = self.mountdir + subvol['mountpoint']
            makedirs(mountpoint)
            rc = runner.show([self.mountcmd, "-o", fsopts, pdisk.disk.device, mountpoint])
            if rc != 0:
                raise MountError("Failed to mount subvolume %s to %s" % (subvol["subvol"], mountpoint))
            subvol["mounted"] = True

    def __unmount_subvolumes(self):
        """ It may be called multiple times, so we need to chekc if it is still mounted. """
        for subvol in self.subvolumes:
            if subvol["mountpoint"] == "/":
                continue
            if not subvol["mounted"]:
                continue
            mountpoint = self.mountdir + subvol['mountpoint']
            rc = runner.show([self.umountcmd, mountpoint])
            if rc != 0:
                raise MountError("Failed to unmount subvolume %s from %s" % (subvol["subvol"], mountpoint))
            subvol["mounted"] = False

    def __create_subvolume_snapshots(self, p, pdisk):
        import time

        if self.snapshot_created:
            return

        # Remount with subvolid=0
        rc = runner.show([self.umountcmd, pdisk.mountdir])
        if rc != 0:
            raise MountError("Failed to umount %s" % pdisk.mountdir)
        if pdisk.fsopts:
            mountopts = pdisk.fsopts + ",subvolid=0"
        else:
            mountopts = "subvolid=0"
        rc = runner.show([self.mountcmd, "-o", mountopts, pdisk.disk.device, pdisk.mountdir])
        if rc != 0:
            raise MountError("Failed to umount %s" % pdisk.mountdir)

        # Create all the subvolume snapshots
        snapshotts = time.strftime("%Y%m%d-%H%M")
        for subvol in self.subvolumes:
            subvolpath = pdisk.mountdir + "/" + subvol["subvol"]
            snapshotpath = subvolpath + "_%s-1" % snapshotts
            rc = runner.show([ self.btrfscmd, "subvolume", "snapshot", subvolpath, snapshotpath ])
            if rc != 0:
                raise MountError("Failed to create subvolume snapshot '%s' for '%s', return code:"
                        "%d." % (snapshotpath, subvolpath, rc))

        self.snapshot_created = True

    def mount(self):
        for dev in self.disks.keys():
            d = self.disks[dev]
            d['disk'].create()

        self.__format_disks()
        self.__map_partitions()
        self.__calculate_mountorder()

        for mp in self.mount_order:
            p = None
            for p1 in self.partitions:
                if p1['mountpoint'] == mp:
                    p = p1
                    break

            if not p['label']:
                if p['mountpoint'] == "/":
                    p['label'] = 'platform'
                else:
                    p['label'] = mp.split('/')[-1]

            if mp == 'swap':
                import uuid
                p['uuid'] = str(uuid.uuid1())
                runner.show([self.mkswap,
                             '-L', p['label'],
                             '-U', p['uuid'],
                             p['device']])
                continue

            rmmountdir = False
            if p['mountpoint'] == "/":
                rmmountdir = True
            if p['fstype'] == "vfat" or p['fstype'] == "msdos":
                my_disk_mount = VfatDiskMount
            elif p['fstype'] in ("ext2", "ext3", "ext4"):
                my_disk_mount = ExtDiskMount
            elif p['fstype'] == "btrfs":
                my_disk_mount = BtrfsDiskMount
            else:
                raise MountError("Fail to support file system " + p['fstype'])

            if p['fstype'] == "btrfs" and not p['fsopts']:
                p['fsopts'] = "subvolid=0"

            pdisk = my_disk_mount(RawDisk(p['size'] * self.sector_size, p['device']),
                                 self.mountdir + p['mountpoint'],
                                 p['fstype'],
                                 4096,
                                 p['label'],
                                 rmmountdir,
                                 self.skipformat,
                                 fsopts = p['fsopts'])
            pdisk.mount(pdisk.fsopts)
            if p['fstype'] == "btrfs" and p['mountpoint'] == "/":
                if not self.skipformat:
                    self.__create_subvolumes(p, pdisk)
                self.__mount_subvolumes(p, pdisk)
            p['mount'] = pdisk
            p['uuid'] = pdisk.uuid

    def resparse(self, size = None):
        # Can't re-sparse a disk image - too hard
        pass
