From 025e1444551457b2fc3d9a048402e6bdfd6da7e5 Mon Sep 17 00:00:00 2001 From: Edward Cragg Date: Tue, 4 Aug 2015 12:21:57 +0100 Subject: Rawdisk partitioning v2: Add partitioning functions Add partitioning functions to the rawdisk.write deployment extension Change-Id: I45bcabc191951d086b8f4ae028a248f95a5e6f2e --- extensions/openstack.write | 2 +- extensions/partitioning.py | 163 +++++++++++++++++++++++ extensions/rawdisk.write | 73 +++++----- extensions/writeexts.py | 323 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 490 insertions(+), 71 deletions(-) create mode 100644 extensions/partitioning.py diff --git a/extensions/openstack.write b/extensions/openstack.write index f1233560..f0d2fc0b 100755 --- a/extensions/openstack.write +++ b/extensions/openstack.write @@ -51,7 +51,7 @@ class OpenStackWriteExtension(writeexts.WriteExtension): def set_extlinux_root_to_virtio(self, raw_disk): '''Re-configures extlinux to use virtio disks''' self.status(msg='Updating extlinux.conf') - with self.mount(raw_disk) as mp: + with self.find_and_mount_rootfs(raw_disk) as mp: path = os.path.join(mp, 'extlinux.conf') with open(path) as f: diff --git a/extensions/partitioning.py b/extensions/partitioning.py new file mode 100644 index 00000000..1c295697 --- /dev/null +++ b/extensions/partitioning.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# Copyright (C) 2015 Codethink Limited +# +# 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, see . + + +"""A module providing Baserock-specific partitioning functions""" + +import os +import pyfdisk +import re +import subprocess +import writeexts + +def do_partitioning(location, disk_size, temp_root, part_spec): + '''Perform partitioning + + Perform partitioning using the pyfdisk.py module. Documentation + for this, and guidance on how to create a partition specification can + be found in extensions/pyfdisk.README + + This function also validates essential parts of the partition layout + + Args: + location: Path to the target device or image + temp_root: Location of the unpacked Baserock rootfs + part_spec: Path to a YAML formatted partition specification + Returns: + A pyfdisk.py Device object + Raises: + writeexts.ExtensionError + ''' + # Create partition table and filesystems + try: + dev = pyfdisk.load_yaml(location, disk_size, part_spec) + writeexts.Extension.status(msg='Loaded partition specification: %s' % + part_spec) + + # FIXME: GPT currently not fully supported due to missing tools + if dev.partition_table_format.lower() == 'gpt': + writeexts.Extension.status(msg='WARNING: GPT partition tables ' + 'are not currently supported, ' + 'when using the extlinux ' + 'bootloader') + + writeexts.Extension.status(msg='Summary:\n' + str(dev.partitionlist)) + writeexts.Extension.status(msg='Writing partition table') + dev.commit() + dev.create_filesystems(skip=('/')) + except (pyfdisk.PartitioningError, pyfdisk.FdiskError) as e: + raise writeexts.ExtensionError(e.msg) + + mountpoints = set(part.mountpoint for part in dev.partitionlist + if hasattr(part, 'mountpoint')) + if '/' not in mountpoints: + raise writeexts.ExtensionError('No partition with root ' + 'mountpoint, please specify a ' + 'partition with \'mountpoint: /\' ' + 'in the partition specification') + + mounted_partitions = set(part for part in dev.partitionlist + if hasattr(part, 'mountpoint')) + + for part in mounted_partitions: + if not hasattr(part, 'filesystem'): + raise writeexts.ExtensionError('Cannot mount a partition ' + 'without filesystem, please specify one ' + 'for \'%s\' partition in the partition ' + 'specification' % part.mountpoint) + if part.mountpoint == '/': + # Check that bootable flag is set for MBR devices + if (hasattr(part, 'boot') + and str(part.boot).lower() not in ('yes', 'true') + and dev.partition_table_format.lower() == 'mbr'): + writeexts.Extension.status(msg='WARNING: Boot partition ' + 'needs bootable flag set to ' + 'boot with extlinux/syslinux') + + return dev + +def process_raw_files(dev, temp_root): + if hasattr(dev, 'raw_files'): + write_raw_files(dev.location, temp_root, dev) + for part in dev.partitionlist: + if hasattr(part, 'raw_files'): + # dd seek=n is used, which skips n blocks before writing, + # so we must skip n-1 sectors before writing in order to + # start writing files to the first block of the partition + write_raw_files(dev.location, temp_root, part, + (part.extent.start - 1) * dev.sector_size) + +def write_raw_files(location, temp_root, dev_or_part, start_offset=0): + '''Write files with `dd`''' + offset = 0 + for raw_args in dev_or_part.raw_files: + r = RawFile(temp_root, start_offset, offset, **raw_args) + offset = r.next_offset + r.dd(location) + + +class RawFile(object): + '''A class to hold information about a raw file to write to a device''' + + def __init__(self, source_root, + start_offset=0, wr_offset=0, + sector_size=512, **kwargs): + '''Initialisation function + + Args: + source_root: Base path for filenames + wr_offset: Offset to write to (and offset per-file offsets by) + sector_size: Device sector size (default: 512) + **kwargs: + file: A path to the file to write (combined with source_root) + offset_sectors: An offset to write to in sectors (optional) + offset_bytes: An offset to write to in bytes (optional) + ''' + if 'file' not in kwargs: + raise writeexts.ExtensionError('Missing file name or path') + self.path = os.path.join(source_root, + re.sub('^/', '', kwargs['file'])) + + if not os.path.exists(self.path): + raise writeexts.ExtensionError('File not found: %s' % self.path) + elif os.path.isdir(self.path): + raise writeexts.ExtensionError('Can only dd regular files') + else: + self.size = os.stat(self.path).st_size + + self.offset = start_offset + if 'offset_bytes' in kwargs: + self.offset += pyfdisk.human_size(kwargs['offset_bytes']) + elif 'offset_sectors' in kwargs: + self.offset += kwargs['offset_sectors'] * sector_size + else: + self.offset += wr_offset + + self.skip = pyfdisk.human_size(kwargs.get('skip_bytes', 0)) + self.count = pyfdisk.human_size(kwargs.get('count_bytes', self.size)) + + # Offset of the first free byte after this file (first byte of next) + self.next_offset = self.size + self.offset + + def dd(self, location): + writeexts.Extension.status(msg='Writing %s at %d bytes' % + (self.path, self.offset)) + subprocess.check_call(['dd', 'if=%s' % self.path, + 'of=%s' % location, 'bs=1', + 'seek=%d' % self.offset, + 'skip=%d' % self.skip, + 'count=%d' % self.count, + 'conv=notrunc']) + subprocess.check_call('sync') diff --git a/extensions/rawdisk.write b/extensions/rawdisk.write index 49d0a1e8..830d06a5 100755 --- a/extensions/rawdisk.write +++ b/extensions/rawdisk.write @@ -17,7 +17,10 @@ '''A Morph deployment write extension for raw disk images.''' +import contextlib import os +import pyfdisk +import re import subprocess import sys import time @@ -44,54 +47,64 @@ class RawDiskWriteExtension(writeexts.WriteExtension): try: if not self.is_device(location): with self.created_disk_image(location): - self.format_btrfs(location) - self.create_system(temp_root, location) + self.create_baserock_system(temp_root, location) self.status(msg='Disk image has been created at %s' % location) else: - self.format_btrfs(location) - self.create_system(temp_root, location) + self.create_baserock_system(temp_root, location) self.status(msg='System deployed to %s' % location) except Exception: self.status(msg='Failure to deploy system to %s' % location) raise - def upgrade_local_system(self, raw_disk, temp_root): + def upgrade_local_system(self, location, temp_root): self.complete_fstab_for_btrfs_layout(temp_root) - with self.mount(raw_disk) as mp: - version_label = self.get_version_label(mp) - self.status(msg='Updating image to a new version with label %s' % - version_label) + try: + with self.mount(location) as mp: + self.do_upgrade(mp, temp_root) + return + except subprocess.CalledProcessError: + pass - version_root = os.path.join(mp, 'systems', version_label) - os.mkdir(version_root) + # At this point, we have failed to mount a raw image, so instead + # search for a Baserock root filesystem in the device's partitions + with self.find_and_mount_rootfs(location) as mp: + self.do_upgrade(mp, temp_root) - old_orig = os.path.join(mp, 'systems', 'default', 'orig') - new_orig = os.path.join(version_root, 'orig') - subprocess.check_call( - ['btrfs', 'subvolume', 'snapshot', old_orig, new_orig]) + def do_upgrade(self, mp, temp_root): + version_label = self.get_version_label(mp) + self.status(msg='Updating image to a new version with label %s' % + version_label) - subprocess.check_call( - ['rsync', '-a', '--checksum', '--numeric-ids', '--delete', - temp_root + os.path.sep, new_orig]) + version_root = os.path.join(mp, 'systems', version_label) + os.mkdir(version_root) - self.create_run(version_root) + old_orig = os.path.join(mp, 'systems', 'default', 'orig') + new_orig = os.path.join(version_root, 'orig') + subprocess.check_call( + ['btrfs', 'subvolume', 'snapshot', old_orig, new_orig]) - default_path = os.path.join(mp, 'systems', 'default') - if os.path.exists(default_path): - os.remove(default_path) - else: - # we are upgrading and old system that does - # not have an updated extlinux config file - if self.bootloader_config_is_wanted(): - self.generate_bootloader_config(mp) - self.install_bootloader(mp) - os.symlink(version_label, default_path) + subprocess.check_call( + ['rsync', '-a', '--checksum', '--numeric-ids', '--delete', + temp_root + os.path.sep, new_orig]) + self.create_run(version_root) + + default_path = os.path.join(mp, 'systems', 'default') + if os.path.exists(default_path): + os.remove(default_path) + else: + # we are upgrading and old system that does + # not have an updated extlinux config file if self.bootloader_config_is_wanted(): - self.install_kernel(version_root, temp_root) + self.generate_bootloader_config(mp) + self.install_bootloader(mp) + os.symlink(version_label, default_path) + + if self.bootloader_config_is_wanted(): + self.install_kernel(version_root, temp_root) def get_version_label(self, mp): version_label = os.environ.get('VERSION_LABEL') diff --git a/extensions/writeexts.py b/extensions/writeexts.py index 000c8270..de7c0f2e 100644 --- a/extensions/writeexts.py +++ b/extensions/writeexts.py @@ -18,6 +18,8 @@ import errno import fcntl import logging import os +import partitioning +import pyfdisk import re import select import shutil @@ -228,7 +230,8 @@ class Extension(object): sys.stdout.write('ERROR: %s\n' % e) sys.exit(1) - def status(self, **kwargs): + @staticmethod + def status(**kwargs): '''Provide status output. The ``msg`` keyword argument is the actual message, @@ -264,12 +267,18 @@ class WriteExtension(Extension): 'Error: Btrfs is required for this deployment, but was not ' 'detected in the kernel of the machine that is running Morph.') - def create_local_system(self, temp_root, raw_disk): + def create_local_system(self, temp_root, location): '''Create a raw system image locally.''' - with self.created_disk_image(raw_disk): - self.format_btrfs(raw_disk) - self.create_system(temp_root, raw_disk) + with self.created_disk_image(location): + self.create_baserock_system(temp_root, location) + + def create_baserock_system(self, temp_root, location): + if self.get_environment_boolean('USE_PARTITIONING', 'no'): + self.create_partitioned_system(temp_root, location) + else: + self.format_btrfs(location) + self.create_system(temp_root, location) @contextlib.contextmanager def created_disk_image(self, location): @@ -295,7 +304,7 @@ class WriteExtension(Extension): try: self.create_btrfs_system_layout( temp_root, mp, version_label='factory', - disk_uuid=self.get_uuid(raw_disk)) + rootfs_uuid=self.get_uuid(raw_disk)) except BaseException as e: sys.stderr.write('Error creating Btrfs system layout') raise @@ -384,11 +393,20 @@ class WriteExtension(Extension): else: raise - def get_uuid(self, location): - '''Get the UUID of a block device's file system.''' - # Requires util-linux blkid; busybox one ignores options and - # lies by exiting successfully. - return subprocess.check_output(['blkid', '-s', 'UUID', '-o', 'value', + def get_uuid(self, location, offset=0): + '''Get the filesystem UUID of a block device's file system. + + Requires util-linux blkid; the busybox version ignores options and + lies by exiting successfully. + + Args: + location: Path of device or image to inspect + offset: A byte offset - which should point to the start of a + partition containing a filesystem + ''' + + return subprocess.check_output(['blkid', '-s', 'UUID', '-o', + 'value', '-p', '-O', str(offset), location]).strip() @contextlib.contextmanager @@ -413,7 +431,7 @@ class WriteExtension(Extension): os.rmdir(mount_point) def create_btrfs_system_layout(self, temp_root, mountpoint, version_label, - disk_uuid): + rootfs_uuid, device=None): '''Separate base OS versions from state using subvolumes. ''' @@ -424,11 +442,9 @@ class WriteExtension(Extension): os.makedirs(version_root) os.makedirs(state_root) - self.create_orig(version_root, temp_root) - system_dir = os.path.join(version_root, 'orig') - + system_dir = self.create_orig(version_root, temp_root) state_dirs = self.complete_fstab_for_btrfs_layout(system_dir, - disk_uuid) + rootfs_uuid, device) for state_dir in state_dirs: self.create_state_subvolume(system_dir, mountpoint, state_dir) @@ -444,12 +460,53 @@ class WriteExtension(Extension): self.install_dtb(version_root, temp_root) self.install_syslinux_menu(mountpoint, version_root) if initramfs is not None: + # Using initramfs - can boot a rootfs with a filesystem UUID self.install_initramfs(initramfs, version_root) - self.generate_bootloader_config(mountpoint, disk_uuid) + self.generate_bootloader_config(mountpoint, + rootfs_uuid=rootfs_uuid) else: - self.generate_bootloader_config(mountpoint) + if device: + # A partitioned disk or image - boot with partition UUID + root_part = device.get_partition_by_mountpoint('/') + root_guid = device.get_partition_uuid(root_part) + self.generate_bootloader_config(mountpoint, + root_guid=root_guid) + if self.get_bootloader_install() == 'extlinux': + self.install_syslinux_blob(device, system_dir) + else: + # Unpartitioned and no initramfs - cannot boot with a UUID + self.generate_bootloader_config(mountpoint) self.install_bootloader(mountpoint) + if device: + self.create_partition_mountpoints(device, system_dir) + + def create_partition_mountpoints(self, device, system_dir): + '''Create (or empty) partition mountpoints in the root filesystem + + Delete contents of partition mountpoints in the rootfs to leave an + empty mount drectory (files are copied to the actual partition in + create_partitioned_system()), or create an empty mount directory in + the rootfs if the mount path doesn't exist. + + Args: + device: A pyfdisk.py Device object describing the partitioning + system_dir: A path to the Baserock rootfs to be modified + ''' + + for part in device.partitionlist: + if hasattr(part, 'mountpoint') and part.mountpoint != '/': + part_mount_dir = os.path.join(system_dir, + re.sub('^/', '', part.mountpoint)) + if os.path.exists(part_mount_dir): + self.status(msg='Deleting files in mountpoint ' + 'for %s partition' % part.mountpoint) + self.empty_dir(part_mount_dir) + else: + self.status(msg='Creating empty mount directory ' + 'for %s partition' % part.mountpoint) + os.mkdir(part_mount_dir) + def create_orig(self, version_root, temp_root): '''Create the default "factory" system.''' @@ -460,6 +517,8 @@ class WriteExtension(Extension): self.status(msg='Copying files to orig subvolume') subprocess.check_call(['cp', '-a', temp_root + '/.', orig + '/.']) + return orig + def create_run(self, version_root): '''Create the 'run' snapshot.''' @@ -484,16 +543,41 @@ class WriteExtension(Extension): os.chmod(subvolume, 0o755) existing_state_dir = os.path.join(system_dir, state_subdir) + self.move_dir_contents(existing_state_dir, subvolume) + + def move_dir_contents(self, source_dir, target_dir): + '''Move all files source_dir, to target_dir''' + + n = self.__cmd_files_in_dir(['mv'], source_dir, target_dir) + if n: + self.status(msg='Moved %d files to %s' % (n, target_dir)) + + def copy_dir_contents(self, source_dir, target_dir): + '''Copy all files source_dir, to target_dir''' + + n = self.__cmd_files_in_dir(['cp', '-a', '-r'], source_dir, target_dir) + if n: + self.status(msg='Copied %d files to %s' % (n, target_dir)) + + def empty_dir(self, directory): + '''Empty the contents of a directory, but not the directory itself''' + + n = self.__cmd_files_in_dir(['rm', '-rf'], directory) + if n: + self.status(msg='Deleted %d files in %s' % (n, directory)) + + def __cmd_files_in_dir(self, cmd, source_dir, target_dir=None): files = [] - if os.path.exists(existing_state_dir): - files = os.listdir(existing_state_dir) - if len(files) > 0: - self.status(msg='Moving existing data to %s subvolume' % subvolume) + if os.path.exists(source_dir): + files = os.listdir(source_dir) for filename in files: - filepath = os.path.join(existing_state_dir, filename) - subprocess.check_call(['mv', filepath, subvolume]) + filepath = os.path.join(source_dir, filename) + add_params = [filepath, target_dir] if target_dir else [filepath] + subprocess.check_call(cmd + add_params) + return len(files) - def complete_fstab_for_btrfs_layout(self, system_dir, rootfs_uuid=None): + def complete_fstab_for_btrfs_layout(self, system_dir, + rootfs_uuid=None, device=None): '''Fill in /etc/fstab entries for the default Btrfs disk layout. In the future we should move this code out of the write extension and @@ -521,9 +605,33 @@ class WriteExtension(Extension): 'UUID=%s' % rootfs_uuid) fstab.add_line('%s / btrfs defaults,rw,noatime 0 1' % root_device) + # Add fstab entries for partitions + if device: + mount_parts = set(p for p in device.partitionlist + if hasattr(p, 'mountpoint') and p.mountpoint != '/') + part_mountpoints = set(p.mountpoint for p in mount_parts) + for part in mount_parts: + if part.mountpoint not in existing_mounts: + # Get filesystem UUID + part_uuid = self.get_uuid(device.location, + part.extent.start * + device.sector_size) + self.status(msg='Adding fstab entry for %s ' + 'partition' % part.mountpoint) + fstab.add_line('UUID=%s %s %s defaults,rw,noatime ' + '0 2' % (part_uuid, part.mountpoint, + part.filesystem)) + else: + self.status(msg='WARNING: an entry already exists in ' + 'fstab for %s partition, skipping' % + part.mountpoint) + + # Add entries for state dirs state_dirs_to_create = set() for state_dir in shared_state_dirs: - if '/' + state_dir not in existing_mounts: + mp = '/' + state_dir + if (mp not in existing_mounts and + (device and mp not in part_mountpoints)): state_dirs_to_create.add(state_dir) state_subvol = os.path.join('/state', state_dir) fstab.add_line( @@ -601,7 +709,7 @@ class WriteExtension(Extension): def get_root_device(self): return os.environ.get('ROOT_DEVICE', '/dev/sda') - def generate_bootloader_config(self, real_root, disk_uuid=None): + def generate_bootloader_config(self, *args, **kwargs): '''Install extlinux on the newly created disk image.''' config_function_dict = { 'extlinux': self.generate_extlinux_config, @@ -609,13 +717,22 @@ class WriteExtension(Extension): config_type = self.get_bootloader_config_format() if config_type in config_function_dict: - config_function_dict[config_type](real_root, disk_uuid) + config_function_dict[config_type](*args, **kwargs) else: raise ExtensionError( 'Invalid BOOTLOADER_CONFIG_FORMAT %s' % config_type) - def generate_extlinux_config(self, real_root, disk_uuid=None): - '''Install extlinux on the newly created disk image.''' + def generate_extlinux_config(self, real_root, + rootfs_uuid=None, root_guid=None): + '''Generate the extlinux configuration file + + Args: + real_root: Path to the mounted top level of the root filesystem + rootfs_uuid: Specify a filesystem UUID which can be loaded using + an initramfs aware of filesystems + root_guid: Specify a partition GUID, can be used without an + initramfs + ''' self.status(msg='Creating extlinux.conf') config = os.path.join(real_root, 'extlinux.conf') @@ -631,29 +748,40 @@ class WriteExtension(Extension): 'rootfstype=btrfs ' # required when using initramfs, also boots # faster when specified without initramfs 'rootflags=subvol=systems/default/run ') # boot runtime subvol - kernel_args += 'root=%s ' % (self.get_root_device() - if disk_uuid is None - else 'UUID=%s' % disk_uuid) + + # See init/do_mounts.c:182 in the kernel source, in the comment above + # function name_to_dev_t(), for an explanation of the available + # options for the kernel parameter 'root', particularly when using + # GUID/UUIDs + if rootfs_uuid: + root_device = 'UUID=%s' % rootfs_uuid + elif root_guid: + root_device = 'PARTUUID=%s' % root_guid + else: + # Fall back to the root partition named in the cluster + root_device = self.get_root_device() + kernel_args += 'root=%s ' % root_device + kernel_args += self.get_extra_kernel_args() with open(config, 'w') as f: f.write('default linux\n') f.write('timeout 1\n') f.write('label linux\n') f.write('kernel /systems/default/kernel\n') - if disk_uuid is not None: + if rootfs_uuid is not None: f.write('initrd /systems/default/initramfs\n') if self.get_dtb_path() != '': f.write('devicetree /systems/default/dtb\n') f.write('append %s\n' % kernel_args) - def install_bootloader(self, real_root): + def install_bootloader(self, *args, **kwargs): install_function_dict = { 'extlinux': self.install_bootloader_extlinux, } install_type = self.get_bootloader_install() if install_type in install_function_dict: - install_function_dict[install_type](real_root) + install_function_dict[install_type](*args, **kwargs) elif install_type != 'none': raise ExtensionError( 'Invalid BOOTLOADER_INSTALL %s' % install_type) @@ -666,6 +794,33 @@ class WriteExtension(Extension): subprocess.check_call(['sync']) time.sleep(2) + def install_syslinux_blob(self, device, orig_root): + '''Install Syslinux MBR blob + + This is the first stage of boot (for partitioned images) on x86 + machines. It is not required where there is no partition table. The + syslinux bootloader is written to the MBR, and is capable of loading + extlinux. This only works when the partition is set as bootable (MBR), + or the legacy boot flag is set (GPT). The blob is built with extlinux, + and found in the rootfs''' + + pt_format = device.partition_table_format.lower() + if pt_format in ('gpb', 'mbr'): + blob = 'mbr.bin' + elif pt_format == 'gpt': + blob = 'gptmbr.bin' + blob_name = 'usr/share/syslinux/' + blob + self.status(msg='Installing syslinux %s blob' % pt_format.upper()) + blob_location = os.path.join(orig_root, blob_name) + if os.path.exists(blob_location): + subprocess.check_call(['dd', 'if=%s' % blob_location, + 'of=%s' % device.location, + 'bs=440', 'count=1', 'conv=notrunc']) + else: + raise ExtensionError('MBR blob not found. Is this the correct' + 'architecture? The MBR blob will only be built for x86' + 'systems. You may wish to configure BOOTLOADER_INSTALL') + def install_syslinux_menu(self, real_root, version_root): '''Make syslinux/extlinux menu binary available. @@ -712,13 +867,13 @@ class WriteExtension(Extension): return True - def get_environment_boolean(self, variable): + def get_environment_boolean(self, variable, default='no'): '''Parse a yes/no boolean passed through the environment.''' - value = os.environ.get(variable, 'no').lower() - if value in ['no', '0', 'false']: + value = os.environ.get(variable, default).lower() + if value in ('no', '0', 'false'): return False - elif value in ['yes', '1', 'true']: + elif value in ('yes', '1', 'true'): return True else: raise ExtensionError('Unexpected value for %s: %s' % @@ -744,3 +899,91 @@ class WriteExtension(Extension): if e.errno == errno.ENOENT: return False raise + + def create_partitioned_system(self, temp_root, location): + '''Create a Baserock system in a partitioned disk image or device''' + + part_spec = os.environ.get('PARTITION_FILE', 'partitioning/default') + + disk_size = self.get_disk_size() + if not disk_size: + raise writeexts.ExtensionError('DISK_SIZE is not defined') + + dev = partitioning.do_partitioning(location, disk_size, + temp_root, part_spec) + + for part in dev.partitionlist: + if not hasattr(part, 'mountpoint'): + continue + if part.mountpoint == '/': + # Re-format the rootfs, to include needed extra features + with pyfdisk.create_loopback(location, + part.extent.start * + dev.sector_size, part.size) as l: + self.mkfs_btrfs(l) + + self.status(msg='Mounting partition %d' % part.number) + offset = part.extent.start * dev.sector_size + with self.mount_partition(location, + offset, part.size) as part_mount_dir: + if part.mountpoint == '/': + # Install root filesystem + rfs_uuid = self.get_uuid(location, part.extent.start * + dev.sector_size) + self.create_btrfs_system_layout(temp_root, part_mount_dir, + 'factory', rfs_uuid, dev) + else: + # Copy files to partition from unpacked rootfs + src_dir = os.path.join(temp_root, + re.sub('^/', '', part.mountpoint)) + self.status(msg='Copying files to %s partition' % + part.mountpoint) + self.copy_dir_contents(src_dir, part_mount_dir) + + # Write raw files to disk with dd + partitioning.process_raw_files(dev, temp_root) + + @contextlib.contextmanager + def mount_partition(self, location, offset_bytes, size_bytes): + '''Mount a partition in a partitioned device or image''' + + with pyfdisk.create_loopback(location, offset=offset_bytes, + size=size_bytes) as loop: + with self.mount(loop) as mountpoint: + yield mountpoint + + @contextlib.contextmanager + def find_and_mount_rootfs(self, location): + ''' + Mount a Baserock rootfs inside a partitioned device or image + + This function searches a disk image or device, with unknown + partitioning scheme, for a Baserock rootfs. This is done by finding + offsets and sizes of partitions in the partition table, mounting each + partition, and checking whether a known path exists in the mount. + + Args: + location: the location of the disk image or device to search + Returns: + A path to the mount point of the mounted Baserock rootfs + ''' + + if pyfdisk.get_pt_type(location) == 'none': + with self.mount(location) as mountpoint: + yield mountpoint + + sector_size = pyfdisk.get_sector_size(location) + partn_sizes = pyfdisk.get_partition_sector_sizes(location) + for i, offset in enumerate(pyfdisk.get_partition_offsets(location)): + try: + with self.mount_partition(location, offset * sector_size, + partn_sizes[i] * sector_size) as mp: + path = os.path.join(mp, 'systems/default/orig/baserock') + if os.path.exists(path): + self.status(msg='Found a Baserock rootfs at ' + 'offset %d sectors/%d bytes' % + (offset, offset * sector_size)) + yield mp + except BaseException: + # Probably a partition without a filesystem, carry on + pass -- cgit v1.2.1