777 lines
32 KiB
Python
777 lines
32 KiB
Python
#!/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
|
|
from mic.utils.fs_related import *
|
|
from mic.utils.gpt_parser import GptParser
|
|
from mic.utils.oe.misc import *
|
|
|
|
# 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
|
|
|
|
class PartitionedMount(Mount):
|
|
def __init__(self, mountdir, skipformat = False):
|
|
Mount.__init__(self, mountdir)
|
|
self.disks = {}
|
|
self.partitions = []
|
|
self.subvolumes = []
|
|
self.mapped = False
|
|
self.mountOrder = []
|
|
self.unmountOrder = []
|
|
self.parted = find_binary_path("parted")
|
|
self.kpartx = find_binary_path("kpartx")
|
|
self.mkswap = find_binary_path("mkswap")
|
|
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, source_file = None, fstype = None,
|
|
label=None, fsopts = None, boot = False, align = None,
|
|
part_type = None):
|
|
""" Add the next partition. Prtitions 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
|
|
'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
|
|
'source_file': source_file, # partition contents
|
|
'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
|
|
'mount': None, # Mount object
|
|
'num': None, # Partition number
|
|
'boot': boot, # Bootable flag
|
|
'align': align, # Partition alignment
|
|
'part_type' : part_type, # Partition type
|
|
'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 " \
|
|
"imlemented 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 enlargea 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 layed out, we can calculate the
|
|
# minumim 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()
|
|
if out:
|
|
msger.debug('"parted" output: %s' % out)
|
|
|
|
if rc != 0:
|
|
# 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.
|
|
msger.debug("WARNING: parted returned '%s' instead of 0" % rc)
|
|
|
|
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 substract 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))
|
|
self.__run_parted(["-s", d['disk'].device, "set",
|
|
"%d" % p['num'], flag_name, "on"])
|
|
|
|
# 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(d['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 d['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, kpartxOutput = runner.runtool([self.kpartx, "-l", "-v", d['disk'].device])
|
|
kpartxOutput = kpartxOutput.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(kpartxOutput) and kpartxOutput[i][0:4] != "loop":
|
|
i = i + 1
|
|
kpartxOutput = kpartxOutput[i:]
|
|
|
|
# Make sure kpartx reported the right count of partitions
|
|
if len(kpartxOutput) != 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(kpartxOutput) == d['numpart'] + 1 \
|
|
and d['ptable_format'] == 'msdos' and len(kpartxOutput) > 3:
|
|
kpartxOutput.pop(3)
|
|
else:
|
|
raise MountError("Unexpected number of partitions from " \
|
|
"kpartx: %d != %d" % \
|
|
(len(kpartxOutput), d['numpart']))
|
|
|
|
for i in range(len(kpartxOutput)):
|
|
line = kpartxOutput[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
|
|
|
|
# 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", "-a", 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, "-d", d['disk'].device])
|
|
raise MountError("Failed to map partitions for '%s'" %
|
|
d['disk'].device)
|
|
|
|
# FIXME: there is a bit delay for multipath device setup,
|
|
# wait 10ms for the setup
|
|
import time
|
|
time.sleep(10)
|
|
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, "-d", 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.mountOrder.append(p['mountpoint'])
|
|
self.unmountOrder.append(p['mountpoint'])
|
|
|
|
self.mountOrder.sort()
|
|
self.unmountOrder.sort()
|
|
self.unmountOrder.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.unmountOrder:
|
|
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 __install_partition(self, num, source_file, start, size):
|
|
"""
|
|
Install source_file contents into a partition.
|
|
"""
|
|
if not source_file: # nothing to install
|
|
return
|
|
|
|
# Start is included in the size so need to substract one from the end.
|
|
end = start + size - 1
|
|
msger.debug("Installed %s in partition %d, sectors %d-%d, size %d sectors" % (source_file, num, start, end, size))
|
|
|
|
dd_cmd = "dd if=%s of=%s bs=%d seek=%d count=%d conv=notrunc" % \
|
|
(source_file, self.image_file, self.sector_size, start, size)
|
|
rc, out = exec_cmd(dd_cmd)
|
|
|
|
|
|
def install(self, image_file):
|
|
msger.debug("Installing partitions")
|
|
|
|
self.image_file = image_file
|
|
|
|
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.__install_partition(p['num'], p['source_file'],
|
|
p['start'] - 1,
|
|
d['offset'] - p['start'])
|
|
|
|
self.__install_partition(p['num'], p['source_file'],
|
|
p['start'], p['size'])
|
|
|
|
def mount(self):
|
|
for dev in self.disks.keys():
|
|
d = self.disks[dev]
|
|
d['disk'].create()
|
|
|
|
self.__format_disks()
|
|
|
|
self.__calculate_mountorder()
|
|
|
|
return
|
|
|
|
def resparse(self, size = None):
|
|
# Can't re-sparse a disk image - too hard
|
|
pass
|