diff --git a/.flake8 b/.flake8 index 5cd19e5..daa88e3 100644 --- a/.flake8 +++ b/.flake8 @@ -8,6 +8,7 @@ max-line-length = 88 # Include scripts to check in addition to the default *.py. filename = *.py, + ./eos-esp-generator, ./eos-migrate-chromium-profile, ./eos-migrate-firefox-profile, ./eos-update-flatpak-repos, diff --git a/Makefile.am b/Makefile.am index 186db4e..ab4307e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,6 +38,7 @@ dist_systemduserunit_DATA = \ $(NULL) dist_systemdgenerator_SCRIPTS = \ + eos-esp-generator \ eos-live-boot-generator \ eos-vm-generator \ $(NULL) diff --git a/eos-esp-generator b/eos-esp-generator new file mode 100755 index 0000000..520e832 --- /dev/null +++ b/eos-esp-generator @@ -0,0 +1,991 @@ +#!/usr/bin/env python3 + +# Copyright © 2023 Endless OS Foundation, LLC +# +# 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; either version 2 of the License, or (at +# your option) any later version. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""\ +Endless OS EFI System Partition (ESP) mount generator + +This program is responsible for mounting the EFI System Partition (ESP) on EOS +systems. It's heavily inspired by systemd-gpt-auto-generator with a few policy +changes that are not easily expressed there: + +* Both GPT and MBR partition tables are supported. + +* The EFI LoaderDevicePartUUID variable is preferred but not required. This + allows for usage with bootloaders such as GRUB that do not implement the boot + loader interface. + +* Mounting at /boot is allowed even when the /efi directory exists. This allows + use of a generic OS commit that can be used on systems where /boot data + exists in the ESP or not. + +* When the ESP is mounted at /boot, it is made world readable to allow + unprivileged ostree admin operations to succeed. + +* When the endless.image.device kernel command line argument is set, the + ESP from that disk is mounted. + +The units created with this generator take precedence over +systemd-gpt-auto-generator. Output from the generator can be read with +"journalctl -t eos-esp-generator". When invoked as eos-esp-generator-gather, +the generator will only gather data and store it in a tarball in /run. +""" + +from argparse import ArgumentParser, RawDescriptionHelpFormatter +from dataclasses import dataclass +import json +import logging +from logging.handlers import SysLogHandler +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tarfile +import time +from tempfile import TemporaryDirectory +from textwrap import dedent + +progname = os.path.basename(__file__) +logger = logging.getLogger(progname) + +ESP_GPT_PARTTYPE = 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b' +ESP_MBR_PARTTYPE = '0xef' +XBOOTLDR_GPT_PARTTYPE = 'bc13c2ff-59e6-4262-a352-b275fd6f7172' +LOADER_EFI_VENDOR = '4a67b082-0a4c-41cf-b6c7-440b29bb8c4f' +LOADER_DEVICE_PART_UUID_NAME = 'LoaderDevicePartUUID' +LOADER_DEVICE_PART_UUID_EFIVAR = f'{LOADER_DEVICE_PART_UUID_NAME}-{LOADER_EFI_VENDOR}' + +# Program name used to automatically enable gather mode. +GATHER_MODE_PROGNAME = 'eos-esp-generator-gather' + + +class EspError(Exception): + """ESP generator errors""" + pass + + +class KmsgHandler(logging.StreamHandler): + """Logging Handler using /dev/kmsg""" + PRIORITY_MAP = { + logging.DEBUG: SysLogHandler.LOG_DEBUG, + logging.INFO: SysLogHandler.LOG_INFO, + logging.WARNING: SysLogHandler.LOG_WARNING, + logging.ERROR: SysLogHandler.LOG_ERR, + logging.CRITICAL: SysLogHandler.LOG_CRIT, + } + + def __init__(self, kmsg=None): + if kmsg is None: + kmsg = open('/dev/kmsg', 'wb', buffering=0) + self.kmsg = kmsg + self.pid = os.getpid() + super().__init__(stream=self.kmsg) + + def close(self): + self.acquire() + try: + try: + self.flush() + finally: + self.kmsg.close() + finally: + self.release() + + def emit(self, record): + try: + message = self.format(record).encode('utf-8', errors='replace') + + # kmsg recognizes a syslog style identifier[pid]: prefix. + priority = self.PRIORITY_MAP.get(record.levelno, SysLogHandler.LOG_INFO) + prefix = ( + f'<{priority:d}>{progname}[{self.pid:d}]: ' + .encode('utf-8', errors='replace') + ) + + # kmsg allows a maximum of 1024 bytes per message, so split the + # message up if needed, without splitting multibyte-encoded + # characters. + start = 0 + length = len(message) + size = 1024 - len(prefix) + while start < length: + end = start + size + while end < length and (message[end] & 0b1100_0000) == 0b1000_0000: + end -= 1 + + buf = prefix + message[start:end] + start = end + self.kmsg.write(buf) + self.flush() + except: # noqa: E722 + self.handleError(record) + + +def in_generator_env(): + """Whether the process is executing in a systemd generator environment""" + return 'SYSTEMD_SCOPE' in os.environ + + +def run(cmd, *args, **kwargs): + """Run a command with logging + + By default, subprocess.run is called with check and text set to True and + encoding set to utf-8. In a systemd generator environment, stdout is set to + subprocess.DEVNULL and stderr is set to subprocess.PIPE so that all output + is captured by default. + """ + kwargs.setdefault('check', True) + kwargs.setdefault('text', True) + kwargs.setdefault('encoding', 'utf-8') + if in_generator_env(): + kwargs.setdefault('stdout', subprocess.DEVNULL) + kwargs.setdefault('stderr', subprocess.PIPE) + + logger.debug(f'> {shlex.join(cmd)}') + return subprocess.run(cmd, *args, **kwargs) + + +def _get_root_path(): + """Get the path to the root directory + + For testing, the ESPGEN_ROOT_PATH variable is read. If not set or empty, / + is returned. + """ + return Path(os.getenv('ESPGEN_ROOT_PATH') or '/') + + +def get_mount_data(): + """Gather mounted filesystems + + Returns a list of mounted filesystem dicts. + """ + cmd = ( + 'findmnt', + '--real', + '--json', + '--list', + '--canonicalize', + '--evaluate', + '--nofsroot', + '--output', + 'TARGET,SOURCE,MAJ:MIN,FSTYPE,FSROOT,OPTIONS', + ) + + proc = run(cmd, stdout=subprocess.PIPE) + data = json.loads(proc.stdout).get('filesystems', {}) + logger.debug('mounts:') + for entry in data: + logger.debug(f' {entry}') + return data + + +def get_fstab_data(): + """Gather filesystem mount configuration + + Returns a list of fstab entry dicts. + """ + cmd = ( + 'findmnt', + '--fstab', + '--json', + '--list', + '--canonicalize', + '--evaluate', + '--nofsroot', + '--output', + 'TARGET,SOURCE,MAJ:MIN,FSTYPE,FSROOT,OPTIONS', + ) + + proc = run(cmd, stdout=subprocess.PIPE) + data = json.loads(proc.stdout).get('filesystems', {}) + logger.debug('fstab:') + for entry in data: + logger.debug(f' {entry}') + return data + + +def get_partition_data(): + """Gather disk partition data + + Returns a list of partition dicts. Ideally this would use lsblk --json, but + that just collects udev attributes. Since this will be run as a generator, + udev won't be running yet. In that case, blkid is used to gather partition + data just like udev itself does. + """ + partitions = [] + + # Get the partition devices, stripping any empty lines in the output. + proc = run( + ['blkid', '-d', '-c', '/dev/null', '-o', 'device'], + stdout=subprocess.PIPE, + ) + partition_devs = [line for line in proc.stdout.splitlines() if line] + logger.debug(f'partition devices: {partition_devs}') + + # Probe all the partition devices. + for dev in partition_devs: + try: + proc = run( + ['blkid', '--probe', '-d', '-c', '/dev/null', '-o', 'export', dev], + stdout=subprocess.PIPE, + ) + except subprocess.CalledProcessError as err: + # blkid will exit with code 2 if it can't gather information about + # the device. Just carry on in that case. + if err.returncode == 2: + logger.warning(f'Ignoring blkid --probe exit code 2 for {dev}') + continue + else: + raise + + entry = {} + for line in proc.stdout.splitlines(): + tag, sep, value = line.partition('=') + if not sep or not tag: + continue + entry[tag] = value + + if entry: + partitions.append(entry) + else: + logger.warning(f'No blkid probe output for {dev}') + + logger.debug('partitions:') + for entry in partitions: + logger.debug(f' {entry}') + return partitions + + +def read_efivar(name): + """Read an EFI variable value + + Reads the data from efivarfs mounted at /sys/firmware/efi/efivars. + Only the variable value is returned, not the attributes. + """ + root = _get_root_path() + varpath = root / 'sys/firmware/efi/efivars' / name + logger.debug(f'Reading EFI variable {varpath}') + try: + with open(varpath, 'rb') as f: + value = f.read() + except FileNotFoundError: + logger.debug(f'EFI variable {name} not set') + return None + + # Skip the first 4 bytes, those are the 32 bit attribute mask. + if len(value) < 4: + logger.warning(f'Invalid EFI variable {name} is less than 4 bytes') + return None + return value[4:] + + +def read_efivar_utf16_string(name): + """Read an EFI variable UTF-16 string + + If the EFI variable doesn't exist, None is returned. Any nul + terminating bytes will be removed. + """ + value = read_efivar(name) + if value is None: + return None + + logger.debug(f'EFI variable {name} contents: {value}') + + # Systemd appends 3 nul bytes for some reason. If there are an odd + # number of bytes, ignore the last one so there are an appropriate + # number of utf16 bytes. + end = len(value) + if end % 2 == 1: + end -= 1 + + # Ignore any trailing nul byte pairs. + while end > 0: + if value[end - 2:end] != b'\0\0': + break + end -= 2 + + return value[:end].decode('utf-16', errors='replace') + + +def parse_kernel_command_line(): + """Read and parse the kernel command line from /proc/cmdline + + Returns a dictionary of arguments names and values. A value of None is used + if the argument has no = sign. + """ + path = _get_root_path() / 'proc/cmdline' + cmdline = path.read_text('utf-8') + + arguments = {} + for arg in shlex.split(cmdline): + name, eq, value = arg.partition('=') + if not eq: + # If there was no = in the argument, use None as the value to + # differentiate from an empty value. + value = None + arguments[name] = value + return arguments + + +def systemd_escape_path(path): + """Escape a path for usage in systemd unit names""" + proc = run( + ('systemd-escape', '--path', str(path)), + stdout=subprocess.PIPE, + ) + return proc.stdout.strip() + + +@dataclass +class EspMount: + """ESP mount specification + + Describes the parameters for mounting the ESP. The write_units() + method can be used to create the systemd units from a generator. + """ + source: str + target: str + type: str = 'vfat' + umask: str = '0077' + + def write_units(self, unit_dir): + source_escaped = systemd_escape_path(self.source) + target_escaped = systemd_escape_path(self.target) + automount_unit = unit_dir / f'{target_escaped}.automount' + mount_unit = unit_dir / f'{target_escaped}.mount' + local_fs_wants = unit_dir / 'local-fs.target.wants' / automount_unit.name + + logger.debug(f'Writing unit {automount_unit}') + automount_unit.write_text(dedent(f"""\ + # Automatically generated by {progname} + + [Unit] + Description=EFI System Partition Automount + + [Automount] + Where={self.target} + TimeoutIdleSec=2min + """)) + + logger.debug(f'Writing unit {mount_unit}') + mount_unit.write_text(dedent(f"""\ + # Automatically generated by {progname} + + [Unit] + Description=EFI System Partition Automount + Requires=systemd-fsck@{source_escaped}.service + After=systemd-fsck@{source_escaped}.service + After=blockdev@{source_escaped}.target + + [Mount] + What={self.source} + Where={self.target} + Type={self.type} + Options=umask={self.umask},noauto,rw + """)) + + # Create a symlink for the automount unit in local-fs.target.wants. + logger.debug(f'Creating {local_fs_wants} symlink') + local_fs_wants.parent.mkdir(parents=True, exist_ok=True) + link = os.path.relpath(automount_unit, local_fs_wants.parent) + os.symlink(link, local_fs_wants) + + +class EspGenerator: + """Generator for mounting the ESP + + Locates the ESP device and determines the path to mount it at. Call + get_esp_mount() to retrieve an EspMount instance describing the + configuration. + """ + def __init__(self): + self.root = _get_root_path() + self.mounts = get_mount_data() + self.fstab = get_fstab_data() + self.partitions = get_partition_data() + self.kcmdline = parse_kernel_command_line() + + self._root_part = self._get_root_partition() + self._endless_image_device, self._endless_image_part = ( + self._get_endless_image_device() + ) + self._loader_device_uuid, self._loader_device_part = self._get_loader_device() + + def get_esp_mount(self): + """Get the ESP mount specification + + Returns an EspMount or None. + """ + # Only mount units when booted with EFI. + efi_path = self.root / 'sys/firmware/efi' + if not efi_path.is_dir(): + logger.info('Skipping ESP mounting for non-EFI booted system') + return None + + # Don't mount units in the initrd. + if os.getenv('SYSTEMD_IN_INITRD', '0') == '1': + logger.info('Skipping ESP mounting in initrd') + return None + + esp_part = self._get_esp_part() + if not esp_part: + logger.info('No ESP partition found, skipping mounting') + return None + + esp_dev = esp_part['DEVNAME'] + logger.info(f'ESP device: {esp_dev}') + + esp_path = self._get_esp_path(esp_part) + if not esp_path: + logger.info('No ESP mount path determined, skipping mounting') + return None + logger.info(f'ESP mount path: {esp_path}') + + # If the ESP is mounted at /boot, it needs to be world readable + # since it's also accessed by ostree and some operations are + # expected to work unprivileged. + umask = '0022' if esp_path == '/boot' else '0077' + + mount = EspMount(source=esp_dev, target=esp_path, umask=umask) + logger.info(f'Created ESP mount instance {mount}') + return mount + + def print_data(self): + """Print gathered device data""" + print('mounts:\n{}'.format(json.dumps(self.mounts, indent=2))) + print('fstab:\n{}'.format(json.dumps(self.fstab, indent=2))) + print('partitions:\n{}'.format(json.dumps(self.partitions, indent=2))) + print('kcmdline:\n{}'.format(json.dumps(self.kcmdline, indent=2))) + + def save_data(self): + """Save gathered data""" + with TemporaryDirectory(dir='/run', prefix='espgen-') as savedir: + logger.debug(f'Writing gathered data to {savedir}') + mounts_path = os.path.join(savedir, 'mounts.json') + with open(mounts_path, 'w') as f: + json.dump(self.mounts, f, indent=2) + f.write('\n') + + fstab_path = os.path.join(savedir, 'fstab.json') + with open(fstab_path, 'w') as f: + json.dump(self.fstab, f, indent=2) + f.write('\n') + + partitions_path = os.path.join(savedir, 'partitions.json') + with open(partitions_path, 'w') as f: + json.dump(self.partitions, f, indent=2) + f.write('\n') + + kcmdline_path = os.path.join(savedir, 'kcmdline.json') + with open(kcmdline_path, 'w') as f: + json.dump(self.kcmdline, f, indent=2) + f.write('\n') + + now = time.strftime('%Y%m%d%H%M%S') + data_path = f'/run/espgen-data-{now}.tar.gz' + logger.info(f'Saving gathered data to {data_path}') + with tarfile.open(data_path, 'x:gz') as tf: + tf.add(savedir, 'espgen-data') + + def _get_esp_part(self): + """Determine the ESP partition to use""" + # If LoaderDevicePartUUID is set, that's always used. + if self._loader_device_uuid: + # If the partition wasn't found, bail out. + if not self._loader_device_part: + return None + + # Make sure it points to an ESP. I'm fairly certain this will only + # be set on GPT disks, but validate MBR, too. + loader_device_dev = self._loader_device_part['DEVNAME'] + loader_device_scheme = self._loader_device_part['PART_ENTRY_SCHEME'] + loader_device_parttype = self._loader_device_part['PART_ENTRY_TYPE'] + if loader_device_scheme == 'gpt': + if loader_device_parttype != ESP_GPT_PARTTYPE: + logger.info( + f'Ignoring LoaderDevicePartUUID device {loader_device_dev} ' + 'since it is not an EFI system partition' + ) + return None + elif loader_device_scheme == 'dos': + if loader_device_parttype != ESP_MBR_PARTTYPE: + logger.info( + f'Ignoring LoaderDevicePartUUID device {loader_device_dev} ' + 'since it is not an EFI system partition' + ) + return None + else: + logger.warning( + f'Unexpected partition type "{loader_device_scheme}" for ' + f'LoaderDevicePartUUID device {loader_device_dev} disk' + ) + return None + + # If it's on the root disk, we're done. + root_dev = self._root_part['DEVNAME'] + loader_on_root = self._partitions_on_same_disk( + self._loader_device_part, + self._root_part, + ) + if loader_on_root: + logger.debug( + f'Using LoaderDevicePartUUID device {loader_device_dev} for ESP ' + f'since it is on the same disk as root device {root_dev}' + ) + return self._loader_device_part + + # If it's on a different disk, it might not be our ESP. Only + # use it if it's on the endless.image.device disk. + if not self._endless_image_part: + return None + + endless_image_dev = self._endless_image_part["DEVNAME"] + + loader_on_endless_image = self._partitions_on_same_disk( + self._loader_device_part, + self._endless_image_part, + ) + if not loader_on_endless_image: + logger.info( + f'Ignoring LoaderDevicePartUUID device {loader_device_dev} since ' + f'it is not on the same disk as root device {root_dev} or ' + f'endless.image.device {endless_image_dev}' + ) + return None + + logger.debug( + f'Using LoaderDevicePartUUID device {loader_device_dev} for ESP since ' + f'it is on the same disk as endless.image.device {endless_image_dev}' + ) + return self._loader_device_part + + # At this point we should be done handling LoaderDevicePartUUID. + assert not self._loader_device_uuid, ( + 'LoaderDevicePartUUID handling did not complete' + ) + + # Look for an appropriate ESP partition. If endless.image.device + # is set, use that disk. Otherwise, use the root partition disk. + if self._endless_image_device: + esp_disk_part = self._endless_image_part + else: + esp_disk_part = self._root_part + + if not esp_disk_part: + # If the partition wasn't found, bail out. + return None + + # Look for the first ESP partition on the disk depending on partition + # type. + esp_part = None + esp_disk_part_dev = esp_disk_part['DEVNAME'] + esp_disk_scheme = esp_disk_part['PART_ENTRY_SCHEME'] + if esp_disk_scheme == 'gpt': + esp_part = self._get_partition( + PART_ENTRY_SCHEME=esp_disk_scheme, + PART_ENTRY_DISK=esp_disk_part['PART_ENTRY_DISK'], + PART_ENTRY_TYPE=ESP_GPT_PARTTYPE, + ) + if not esp_part: + logger.info( + f'No GPT partition with ESP type {ESP_GPT_PARTTYPE} found ' + f'on {esp_disk_part_dev} disk' + ) + return None + elif esp_disk_scheme == 'dos': + esp_part = self._get_partition( + PART_ENTRY_SCHEME=esp_disk_scheme, + PART_ENTRY_DISK=esp_disk_part['PART_ENTRY_DISK'], + PART_ENTRY_TYPE=ESP_MBR_PARTTYPE, + ) + if not esp_part: + logger.info( + f'No MBR partition with ESP type {ESP_MBR_PARTTYPE} found ' + f'on {esp_disk_part_dev} disk' + ) + return None + else: + logger.warning( + f'Unexpected partition type "{esp_disk_scheme}" for ' + f'{esp_disk_part_dev} disk' + ) + return None + + assert esp_part, 'Should have found esp_part' + esp_part_dev = esp_part['DEVNAME'] + + # Don't handle the ESP mount if the disk has an XBOOTLDR partition. We + # don't currently create an XBOOTLDR partition and supporting them + # would provide very little benefit. systemd-gpt-auto-generator should + # mostly DTRT, anyways. + if self._disk_has_xbootldr(esp_part): + logger.info( + f'Ignoring ESP device {esp_part_dev} since the disk has an ' + 'XBOOTLDR partition' + ) + return None + + esp_disk_use = ( + 'endless.image.device' if self._endless_image_device else 'root' + ) + logger.debug( + f'Using device {esp_part_dev} for ESP since it is on the same disk as ' + f'{esp_disk_use} device {esp_disk_part_dev}' + ) + return esp_part + + def _get_esp_path(self, esp_part): + """Determine the path to mount the ESP partition""" + esp_dev = esp_part['DEVNAME'] + boot_mount = self._get_mount(target='/boot') + boot_fstab = self._get_fstab(target='/boot') + efi_mount = self._get_mount(target='/efi') + efi_fstab = self._get_fstab(target='/efi') + + def _use_if_dir_exists(path): + rooted_path = self.root / path.lstrip('/') + if not rooted_path.exists(): + logger.info(f'Would use {path} for mount path, but it does not exist') + return None + return path + + # /boot is preferred unless some other boot directory/partition will be + # mounted there. + path = '/boot' + if boot_fstab: + # If the source is the ESP device, do nothing as the fstab + # generator will create the mount unit. + if boot_fstab['source'] == esp_dev: + logger.info(f'Skipping /boot since it is in fstab using {esp_dev}') + return None + + # Something else is setup to be mounted at /boot. Prefer /efi. + logger.debug( + f'/boot is in fstab using {boot_fstab["source"]}, ' + f'prefer /efi as mount path' + ) + path = '/efi' + + if path == '/boot': + if boot_mount: + # /boot is already mounted but not via fstab configuration. + if boot_mount['source'] == esp_dev: + # If it's mounted using the ESP device, then that probably + # happened from a mount unit created by a previous run of + # this generator. Keep using /boot. + logger.debug( + f'/boot is mounted using ESP device {esp_dev}, ' + 'use it as mount path' + ) + else: + # Some other device is mounted at /boot. Likely this is the + # bind mount created by ostree-prepare-root in the + # initramfs. Regardless, we don't want to mount the ESP + # over it. + logger.debug( + f'/boot is mounted using {boot_mount["source"]}, ' + f'prefer /efi as mount path' + ) + path = '/efi' + + if path == '/boot': + return _use_if_dir_exists(path) + + # At this point we should only be dealing with /efi. + assert path == '/efi', '/boot handling did not complete' + + # Validate potential /efi mount point. + if efi_fstab: + logger.info( + f'Skipping /efi since it is in fstab using {efi_fstab["source"]}' + ) + return None + + if efi_mount: + # /efi is already mounted but not via fstab configuration. + if efi_mount['source'] == esp_dev: + # If it's mounted using the ESP device, then that probably + # happened from a mount unit created by a previous run of this + # generator. Keep using /efi. + logger.debug( + f'/efi is mounted using ESP device {esp_dev}, ' + 'use it as mount path' + ) + else: + # Some other device is mounted at /efi. Don't want to mount the + # ESP over it. + logger.info( + f'Skipping /efi since it is mounted using {efi_mount["source"]}' + ) + return None + + return _use_if_dir_exists(path) + + def _get_root_partition(self): + root_mount = self._get_mount(target='/') + if not root_mount: + raise EspError(f'Could not find / mount in {self.mounts}') + root_dev = root_mount['source'] + root_part = self._get_partition(DEVNAME=root_dev) + if not root_part: + raise EspError(f'Could not find / device {root_dev} in {self.partitions}') + logger.debug(f'Determined root partition {root_part}') + return root_part + + @staticmethod + def _partitions_on_same_disk(a, b): + return a['PART_ENTRY_DISK'] == b['PART_ENTRY_DISK'] + + def _get_endless_image_device(self): + """Find the partition for the endless.image.device command line option + + Looks for the endless.image.device kernel command line option and tries + to find the corresponding partition. Returns a tuple of argument and + partition. + """ + image_device_arg = self.kcmdline.get('endless.image.device') + if not image_device_arg: + logger.debug('endless.image.device kernel command line arg not set') + return None, None + + logger.debug( + f'Found endless.image.device kernel command line arg is {image_device_arg}' + ) + tag, eq, value = image_device_arg.partition('=') + if not eq: + # No = sign, treat the argument as a device path. + part = self._get_partition(DEVNAME=image_device_arg) + else: + # Presumably a tag like UUID=. Make sure it's something we + # can handle. + if tag.upper() not in ('UUID', 'LABEL', 'PARTUUID', 'PARTLABEL'): + logger.warning( + f'Unrecognized tag "{tag}" in endless.image.device argument ' + f'{image_device_arg}' + ) + return image_device_arg, None + + tag = tag.upper() + if tag == 'PARTUUID': + tag = 'PART_ENTRY_UUID' + elif tag == 'PARTLABEL': + tag = 'PART_ENTRY_NAME' + part_kwargs = {tag: value} + part = self._get_partition(**part_kwargs) + + if not part: + logger.warning( + f'Could not locate endless.image.device "{image_device_arg}"' + ) + return image_device_arg, part + + def _get_loader_device(self): + """Find the partition for the LoaderDevicePartUUID EFI variable + + Looks for the LoaderDevicePartUUID EFI variable to find the + corresponding partition. Returns a tuple of value and partition. + """ + loader_part_uuid = read_efivar_utf16_string(LOADER_DEVICE_PART_UUID_EFIVAR) + if not loader_part_uuid: + return None, None + + logger.debug( + f'Found EFI var {LOADER_DEVICE_PART_UUID_NAME} is {loader_part_uuid}' + ) + + uuid = loader_part_uuid.lower() + part = self._get_partition(PART_ENTRY_UUID=uuid) + if not part: + logger.warning( + f'Could not locate {LOADER_DEVICE_PART_UUID_NAME} "{loader_part_uuid}"' + ) + return loader_part_uuid, part + + def _disk_has_xbootldr(self, partition): + """Whether the partition's disk has an XBOOTLDR partition""" + xbootldr_part = self._get_partition( + PART_ENTRY_SCHEME='gpt', + PART_ENTRY_DISK=partition['PART_ENTRY_DISK'], + PART_ENTRY_TYPE=XBOOTLDR_GPT_PARTTYPE, + ) + return bool(xbootldr_part) + + @staticmethod + def _filter_data(data, **kwargs): + """Filter iterable of dict data + + Returns an iterator of dicts where the dict entries match the keyword + arguments. + """ + if not kwargs: + raise ValueError('Must provide filter values') + + def _test(entry): + for name, value in kwargs.items(): + if name not in entry or entry[name] != value: + return False + return True + + return filter(_test, data) + + def _get_mount(self, **kwargs): + return next(self._filter_data(self.mounts, **kwargs), None) + + def _get_fstab(self, **kwargs): + return next(self._filter_data(self.fstab, **kwargs), None) + + def _get_partition(self, **kwargs): + return next(self._filter_data(self.partitions, **kwargs), None) + + +def main(): + """ESP generator main entry point""" + doclines = __doc__.splitlines() + ap = ArgumentParser( + description=doclines[0], + epilog='\n'.join(doclines[2:]), + formatter_class=RawDescriptionHelpFormatter, + ) + ap.add_argument( + 'normal_dir', + metavar='NORMAL', + nargs='?', + help='Normal generator output directory', + ) + ap.add_argument( + 'early_dir', + metavar='EARLY', + nargs='?', + help='Early generator output directory', + ) + ap.add_argument( + 'late_dir', + metavar='LATE', + nargs='?', + help='Late generator output directory', + ) + ap.add_argument( + '-n', + '--dry-run', + action='store_true', + help='only show what would be done', + ) + ap.add_argument( + '-g', + '--gather-data', + action='store_true', + help='only gather device data', + ) + ap.add_argument( + '-s', + '--save-data', + action='store_true', + help='save gathered data', + ) + log_level_group = ap.add_mutually_exclusive_group() + log_level_group.add_argument( + '--quiet', + dest='log_level', + action='store_const', + const=logging.WARNING, + help='only log warning messages', + ) + log_level_group.add_argument( + '--debug', + dest='log_level', + action='store_const', + const=logging.DEBUG, + help='log debug messages', + ) + + # Detect when being run as a generator. + run_as_generator = in_generator_env() + + # Default to debug logging when run as a generator and debug is in the + # kernel command line. + if run_as_generator and 'debug' in parse_kernel_command_line(): + default_log_level = logging.DEBUG + else: + default_log_level = logging.INFO + ap.set_defaults(log_level=default_log_level) + + # Default to gather mode when invoked using the special gather name. + if progname == GATHER_MODE_PROGNAME: + ap.set_defaults(gather_data=True, save_data=True) + + # Generators are run with stdout/stderr connected to /dev/null, so + # reconnect them to /dev/kmsg and use the KmsgHandler logging handler. + if run_as_generator: + with open('/dev/kmsg', 'wb', buffering=0) as kmsg: + kmsg_fd = kmsg.fileno() + stdout_fd = sys.stdout.fileno() + stderr_fd = sys.stderr.fileno() + os.dup2(kmsg_fd, stdout_fd) + os.dup2(kmsg_fd, stderr_fd) + + log_handler = KmsgHandler() + + # Use a format string with just the message since the name, level and + # time are already handled. + log_format = '%(message)s' + else: + # Use a normal stderr handler. + log_handler = logging.StreamHandler(sys.stderr) + log_format = '%(name)s:%(levelname)s:%(message)s' + + args = ap.parse_args() + logging.basicConfig(level=args.log_level, handlers=[log_handler], format=log_format) + + # When not in gather mode, at least the normal unit directory is needed. + if not args.gather_data and not args.normal_dir: + logger.error('Normal generator directory not specified') + sys.exit(1) + + generator = EspGenerator() + if args.gather_data: + # In gather mode, just collect the data and exit. + if args.save_data: + generator.save_data() + else: + generator.print_data() + return + + esp_mount = generator.get_esp_mount() + if esp_mount and not args.dry_run: + # The normal generator directory is used so that we take precedence + # over systemd-gpt-auto-generator. However, this is the same level as + # systemd-fstab-generator, so we need to take care to not override + # fstab entries. + unit_dir = Path(args.normal_dir) + esp_mount.write_units(unit_dir) + + +if __name__ == '__main__': + # Print exceptions through logging so it gets associated to this program in + # kmsg. + try: + main() + except SystemExit: + pass + except subprocess.CalledProcessError as err: + logger.exception(f'Executing {err.cmd[0]} failed:\n{err.stderr}') + sys.exit(err.returncode) + except: # noqa: E722 + logger.exception('Generator failed:') + sys.exit(1) diff --git a/tests/Makefile.am b/tests/Makefile.am index 9efd4f6..2d2ac6d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -19,6 +19,8 @@ EXTRA_DIST = \ __init__.py \ conftest.py \ efivars \ + espgen \ + test_esp_generator.py \ test_image_boot.py \ test_live_storage.py \ test_migrate_chromium_profile.py \ diff --git a/tests/espgen/README.md b/tests/espgen/README.md new file mode 100644 index 0000000..e0eab11 --- /dev/null +++ b/tests/espgen/README.md @@ -0,0 +1,16 @@ +Test data directory for use with `test_esp_generator.py`. The data here is +created by running `eos-esp-generator` in gather mode. The easiest way to do +that is to install it as +`/etc/systemd/system-generators/eos-esp-generator-gather`. + +Reboot so that it runs as an early boot generator. After booting, run it again +as a generator by calling `systemctl daemon-reload`. There will then be 2 +tarballs at `/run/espgen-data-*.tar.gz`. Unpack the tarballs and add the data +files to this directory with a semi-descriptive prefix. The `mounts.json` and +`partitions.json` data files should be differentiated by adding `-init` or +`-reload` suffixes as appropriate. The `fstab.json` and `kcmdline.json` files +don't need to be differentiated as they won't change between the 2 executions +of the generator. + +Finally, wire up the data in the `ESP_MOUNT_TEST_DATA` dictionary in +`test_esp_generator.py`. The key is the prefix added to the data files above. diff --git a/tests/espgen/grub-gpt-fstab-efi-fstab.json b/tests/espgen/grub-gpt-fstab-efi-fstab.json new file mode 100644 index 0000000..f716579 --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-fstab.json @@ -0,0 +1,18 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/vda1", + "maj:min": null, + "fstype": "vfat", + "fsroot": null, + "options": "umask=0077" + } +] diff --git a/tests/espgen/grub-gpt-fstab-efi-kcmdline.json b/tests/espgen/grub-gpt-fstab-efi-kcmdline.json new file mode 100644 index 0000000..ca045bd --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-kcmdline.json @@ -0,0 +1,10 @@ +{ + "BOOT_IMAGE": "(hd0,gpt3)/boot/ostree/eos-078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/vmlinuz-6.5.0-10-generic", + "root": "UUID=cecb0576-cf95-48bf-8b72-aced790e83e9", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.1/eos/078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/0" +} diff --git a/tests/espgen/grub-gpt-fstab-efi-mounts-init.json b/tests/espgen/grub-gpt-fstab-efi-mounts-init.json new file mode 100644 index 0000000..575a5fd --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-mounts-init.json @@ -0,0 +1,34 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0", + "options": "ro,relatime" + }, + { + "target": "/boot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0/usr", + "options": "ro,relatime" + }, + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/grub-gpt-fstab-efi-mounts-reload.json b/tests/espgen/grub-gpt-fstab-efi-mounts-reload.json new file mode 100644 index 0000000..39e8577 --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-mounts-reload.json @@ -0,0 +1,50 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/vda1", + "maj:min": "253:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/grub-gpt-fstab-efi-partitions-init.json b/tests/espgen/grub-gpt-fstab-efi-partitions-init.json new file mode 100644 index 0000000..ff16736 --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-partitions-init.json @@ -0,0 +1,44 @@ +[ + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "9eaab1f8-c210-2b4e-8656-e8ed0beb1ab2", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "cecb0576-cf95-48bf-8b72-aced790e83e9", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "eb219ed1-9e98-4048-9288-1c16db3dea72", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "52297695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "CD28-379D", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "7969ae8c-416d-6f4d-9bff-9e2047c79857", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/grub-gpt-fstab-efi-partitions-reload.json b/tests/espgen/grub-gpt-fstab-efi-partitions-reload.json new file mode 100644 index 0000000..685b921 --- /dev/null +++ b/tests/espgen/grub-gpt-fstab-efi-partitions-reload.json @@ -0,0 +1,51 @@ +[ + { + "DEVNAME": "/dev/zram0", + "UUID": "1293710f-3872-47e8-a9a8-be9545021994", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "9eaab1f8-c210-2b4e-8656-e8ed0beb1ab2", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "cecb0576-cf95-48bf-8b72-aced790e83e9", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "eb219ed1-9e98-4048-9288-1c16db3dea72", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "52297695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "CD28-379D", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "7969ae8c-416d-6f4d-9bff-9e2047c79857", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/grub-gpt-fstab.json b/tests/espgen/grub-gpt-fstab.json new file mode 100644 index 0000000..826ab23 --- /dev/null +++ b/tests/espgen/grub-gpt-fstab.json @@ -0,0 +1,10 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + } +] diff --git a/tests/espgen/grub-gpt-kcmdline.json b/tests/espgen/grub-gpt-kcmdline.json new file mode 100644 index 0000000..ca045bd --- /dev/null +++ b/tests/espgen/grub-gpt-kcmdline.json @@ -0,0 +1,10 @@ +{ + "BOOT_IMAGE": "(hd0,gpt3)/boot/ostree/eos-078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/vmlinuz-6.5.0-10-generic", + "root": "UUID=cecb0576-cf95-48bf-8b72-aced790e83e9", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.1/eos/078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/0" +} diff --git a/tests/espgen/grub-gpt-mounts-init.json b/tests/espgen/grub-gpt-mounts-init.json new file mode 100644 index 0000000..575a5fd --- /dev/null +++ b/tests/espgen/grub-gpt-mounts-init.json @@ -0,0 +1,34 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0", + "options": "ro,relatime" + }, + { + "target": "/boot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0/usr", + "options": "ro,relatime" + }, + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/grub-gpt-mounts-reload.json b/tests/espgen/grub-gpt-mounts-reload.json new file mode 100644 index 0000000..39e8577 --- /dev/null +++ b/tests/espgen/grub-gpt-mounts-reload.json @@ -0,0 +1,50 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/03b591bb31c1d4cb69df9e4a7d7097a48012354d4a3777179d90016ec1893738.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/vda1", + "maj:min": "253:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/grub-gpt-partitions-init.json b/tests/espgen/grub-gpt-partitions-init.json new file mode 100644 index 0000000..ff16736 --- /dev/null +++ b/tests/espgen/grub-gpt-partitions-init.json @@ -0,0 +1,44 @@ +[ + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "9eaab1f8-c210-2b4e-8656-e8ed0beb1ab2", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "cecb0576-cf95-48bf-8b72-aced790e83e9", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "eb219ed1-9e98-4048-9288-1c16db3dea72", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "52297695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "CD28-379D", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "7969ae8c-416d-6f4d-9bff-9e2047c79857", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/grub-gpt-partitions-reload.json b/tests/espgen/grub-gpt-partitions-reload.json new file mode 100644 index 0000000..685b921 --- /dev/null +++ b/tests/espgen/grub-gpt-partitions-reload.json @@ -0,0 +1,51 @@ +[ + { + "DEVNAME": "/dev/zram0", + "UUID": "1293710f-3872-47e8-a9a8-be9545021994", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "9eaab1f8-c210-2b4e-8656-e8ed0beb1ab2", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "cecb0576-cf95-48bf-8b72-aced790e83e9", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "eb219ed1-9e98-4048-9288-1c16db3dea72", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "52297695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "CD28-379D", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "7969ae8c-416d-6f4d-9bff-9e2047c79857", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/grub-mbr-fstab.json b/tests/espgen/grub-mbr-fstab.json new file mode 100644 index 0000000..27c097d --- /dev/null +++ b/tests/espgen/grub-mbr-fstab.json @@ -0,0 +1,10 @@ +[ + { + "target": "/", + "source": "/dev/vda2", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + } +] diff --git a/tests/espgen/grub-mbr-kcmdline.json b/tests/espgen/grub-mbr-kcmdline.json new file mode 100644 index 0000000..102f70e --- /dev/null +++ b/tests/espgen/grub-mbr-kcmdline.json @@ -0,0 +1,10 @@ +{ + "BOOT_IMAGE": "(hd0,msdos2)/boot/ostree/eos-078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/vmlinuz-6.5.0-10-generic", + "root": "UUID=dde5e094-c22d-4b11-86de-e6c880085f93", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.0/eos/078584ee3e2b33ee29896c7ebd1533b40f7f7a6d551dcc39cfecd4d8e8f75d51/0" +} diff --git a/tests/espgen/grub-mbr-mounts-init.json b/tests/espgen/grub-mbr-mounts-init.json new file mode 100644 index 0000000..9c69f2e --- /dev/null +++ b/tests/espgen/grub-mbr-mounts-init.json @@ -0,0 +1,34 @@ +[ + { + "target": "/", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/587b6f5ddcd32337a8b27ac3fe07f6141965fc455200f07f32d2a4ae7c089028.0", + "options": "ro,relatime" + }, + { + "target": "/boot", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime" + }, + { + "target": "/usr", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/587b6f5ddcd32337a8b27ac3fe07f6141965fc455200f07f32d2a4ae7c089028.0/usr", + "options": "ro,relatime" + }, + { + "target": "/sysroot", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/grub-mbr-mounts-reload.json b/tests/espgen/grub-mbr-mounts-reload.json new file mode 100644 index 0000000..81eb4af --- /dev/null +++ b/tests/espgen/grub-mbr-mounts-reload.json @@ -0,0 +1,50 @@ +[ + { + "target": "/", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/587b6f5ddcd32337a8b27ac3fe07f6141965fc455200f07f32d2a4ae7c089028.0", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/587b6f5ddcd32337a8b27ac3fe07f6141965fc455200f07f32d2a4ae7c089028.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/sysroot", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/vda2", + "maj:min": "253:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/vda1", + "maj:min": "253:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/grub-mbr-partitions-init.json b/tests/espgen/grub-mbr-partitions-init.json new file mode 100644 index 0000000..b8fce04 --- /dev/null +++ b/tests/espgen/grub-mbr-partitions-init.json @@ -0,0 +1,35 @@ +[ + { + "DEVNAME": "/dev/vda2", + "LABEL": "ostree", + "UUID": "dde5e094-c22d-4b11-86de-e6c880085f93", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "dos", + "PART_ENTRY_UUID": "680dae45-02", + "PART_ENTRY_TYPE": "0x83", + "PART_ENTRY_FLAGS": "0x80", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "88487695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "6D78-30F7", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "dos", + "PART_ENTRY_UUID": "680dae45-01", + "PART_ENTRY_TYPE": "0xef", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/grub-mbr-partitions-reload.json b/tests/espgen/grub-mbr-partitions-reload.json new file mode 100644 index 0000000..3b55abf --- /dev/null +++ b/tests/espgen/grub-mbr-partitions-reload.json @@ -0,0 +1,42 @@ +[ + { + "DEVNAME": "/dev/zram0", + "UUID": "dc44bec6-3776-441e-96f5-b77aac2a7505", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + }, + { + "DEVNAME": "/dev/vda2", + "LABEL": "ostree", + "UUID": "dde5e094-c22d-4b11-86de-e6c880085f93", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "dos", + "PART_ENTRY_UUID": "680dae45-02", + "PART_ENTRY_TYPE": "0x83", + "PART_ENTRY_FLAGS": "0x80", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "88487695", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "6D78-30F7", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "dos", + "PART_ENTRY_UUID": "680dae45-01", + "PART_ENTRY_TYPE": "0xef", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/sdboot-fstab.json b/tests/espgen/sdboot-fstab.json new file mode 100644 index 0000000..826ab23 --- /dev/null +++ b/tests/espgen/sdboot-fstab.json @@ -0,0 +1,10 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + } +] diff --git a/tests/espgen/sdboot-kcmdline.json b/tests/espgen/sdboot-kcmdline.json new file mode 100644 index 0000000..4e50875 --- /dev/null +++ b/tests/espgen/sdboot-kcmdline.json @@ -0,0 +1,11 @@ +{ + "eospayg": null, + "efi_no_storage_paranoia": null, + "rd.shell": "0", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.0/eos/de6ecb1835fa999f3d337c4b719b6b1259f829d4e938703a1316949622612ba6/0" +} diff --git a/tests/espgen/sdboot-mounts-init.json b/tests/espgen/sdboot-mounts-init.json new file mode 100644 index 0000000..d8969f8 --- /dev/null +++ b/tests/espgen/sdboot-mounts-init.json @@ -0,0 +1,26 @@ +[ + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "rw,relatime" + }, + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0", + "options": "rw,relatime" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0/usr", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/sdboot-mounts-reload.json b/tests/espgen/sdboot-mounts-reload.json new file mode 100644 index 0000000..a7d36c0 --- /dev/null +++ b/tests/espgen/sdboot-mounts-reload.json @@ -0,0 +1,42 @@ +[ + { + "target": "/sysroot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/vda1", + "maj:min": "253:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/sdboot-partitions-init.json b/tests/espgen/sdboot-partitions-init.json new file mode 100644 index 0000000..3f82a69 --- /dev/null +++ b/tests/espgen/sdboot-partitions-init.json @@ -0,0 +1,44 @@ +[ + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "B5E9-EA49", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "0189f938-7e9e-194d-8757-e85edaaca5b3", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "1024000", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "e7fb50ac-d074-5c4d-8d1e-92f91b4f0718", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "1026048", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "2bad2e5e-d97c-4c87-ae37-aa8ed2279c9a", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "b47e35b3-8d0d-7347-b2e1-6303cc628984", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "499090063", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/sdboot-partitions-reload.json b/tests/espgen/sdboot-partitions-reload.json new file mode 100644 index 0000000..2a1615a --- /dev/null +++ b/tests/espgen/sdboot-partitions-reload.json @@ -0,0 +1,51 @@ +[ + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "B5E9-EA49", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "0189f938-7e9e-194d-8757-e85edaaca5b3", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "1024000", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "e7fb50ac-d074-5c4d-8d1e-92f91b4f0718", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "1026048", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "LABEL": "ostree", + "UUID": "2bad2e5e-d97c-4c87-ae37-aa8ed2279c9a", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "b47e35b3-8d0d-7347-b2e1-6303cc628984", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "499090063", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/zram0", + "UUID": "6965bcd9-5f8b-42d2-8c2e-12d0e8d22163", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + } +] diff --git a/tests/espgen/sdboot-xbootldr-fstab.json b/tests/espgen/sdboot-xbootldr-fstab.json new file mode 100644 index 0000000..826ab23 --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-fstab.json @@ -0,0 +1,10 @@ +[ + { + "target": "/", + "source": "/dev/vda3", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + } +] diff --git a/tests/espgen/sdboot-xbootldr-kcmdline.json b/tests/espgen/sdboot-xbootldr-kcmdline.json new file mode 100644 index 0000000..4e50875 --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-kcmdline.json @@ -0,0 +1,11 @@ +{ + "eospayg": null, + "efi_no_storage_paranoia": null, + "rd.shell": "0", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.0/eos/de6ecb1835fa999f3d337c4b719b6b1259f829d4e938703a1316949622612ba6/0" +} diff --git a/tests/espgen/sdboot-xbootldr-mounts-init.json b/tests/espgen/sdboot-xbootldr-mounts-init.json new file mode 100644 index 0000000..a61cc79 --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-mounts-init.json @@ -0,0 +1,26 @@ +[ + { + "target": "/sysroot", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/", + "options": "rw,relatime" + }, + { + "target": "/", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0", + "options": "rw,relatime" + }, + { + "target": "/usr", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0/usr", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/sdboot-xbootldr-mounts-reload.json b/tests/espgen/sdboot-xbootldr-mounts-reload.json new file mode 100644 index 0000000..20eeb90 --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-mounts-reload.json @@ -0,0 +1,50 @@ +[ + { + "target": "/sysroot", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/d590ffda3b7eebc41121fde0a05c10d5638c5a0facb51ec7437268399dfc51dd.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/vda4", + "maj:min": "253:4", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "rw,relatime,errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/vda1", + "maj:min": "253:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/vda3", + "maj:min": "253:3", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/sdboot-xbootldr-partitions-init.json b/tests/espgen/sdboot-xbootldr-partitions-init.json new file mode 100644 index 0000000..dab31ec --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-partitions-init.json @@ -0,0 +1,60 @@ +[ + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "B5E9-EA49", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "0189f938-7e9e-194d-8757-e85edaaca5b3", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "1024000", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "4c4f3323-e487-f145-9a01-2afa67956dc6", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "part_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "1026048", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "SEC_TYPE": "msdos", + "UUID": "3C18-31F6", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "c3c8d283-ec36-534c-be18-1b41377f3d40", + "PART_ENTRY_TYPE": "bc13c2ff-59e6-4262-a352-b275fd6f7172", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "409600", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda4", + "LABEL": "ostree", + "UUID": "2bad2e5e-d97c-4c87-ae37-aa8ed2279c9a", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "b47e35b3-8d0d-7347-b2e1-6303cc628984", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "4", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "499090063", + "PART_ENTRY_DISK": "253:0" + } +] diff --git a/tests/espgen/sdboot-xbootldr-partitions-reload.json b/tests/espgen/sdboot-xbootldr-partitions-reload.json new file mode 100644 index 0000000..746740c --- /dev/null +++ b/tests/espgen/sdboot-xbootldr-partitions-reload.json @@ -0,0 +1,67 @@ +[ + { + "DEVNAME": "/dev/vda1", + "SEC_TYPE": "msdos", + "UUID": "B5E9-EA49", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "0189f938-7e9e-194d-8757-e85edaaca5b3", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "1024000", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "4c4f3323-e487-f145-9a01-2afa67956dc6", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "part_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "1026048", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda3", + "SEC_TYPE": "msdos", + "UUID": "3C18-31F6", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "c3c8d283-ec36-534c-be18-1b41377f3d40", + "PART_ENTRY_TYPE": "bc13c2ff-59e6-4262-a352-b275fd6f7172", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "409600", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/vda4", + "LABEL": "ostree", + "UUID": "2bad2e5e-d97c-4c87-ae37-aa8ed2279c9a", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "b47e35b3-8d0d-7347-b2e1-6303cc628984", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "4", + "PART_ENTRY_OFFSET": "1028096", + "PART_ENTRY_SIZE": "499090063", + "PART_ENTRY_DISK": "253:0" + }, + { + "DEVNAME": "/dev/zram0", + "UUID": "6965bcd9-5f8b-42d2-8c2e-12d0e8d22163", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + } +] diff --git a/tests/espgen/windows-efi-fstab.json b/tests/espgen/windows-efi-fstab.json new file mode 100644 index 0000000..a04ee1d --- /dev/null +++ b/tests/espgen/windows-efi-fstab.json @@ -0,0 +1,10 @@ +[ + { + "target": "/", + "source": "/dev/loop0p3", + "maj:min": null, + "fstype": "ext4", + "fsroot": null, + "options": "errors=remount-ro" + } +] diff --git a/tests/espgen/windows-efi-kcmdline.json b/tests/espgen/windows-efi-kcmdline.json new file mode 100644 index 0000000..209dbd1 --- /dev/null +++ b/tests/espgen/windows-efi-kcmdline.json @@ -0,0 +1,12 @@ +{ + "BOOT_IMAGE": "(loop_img,gpt3)/boot/ostree/eos-05199b9a534975d167eff9621ff44b04342dff52cb692b76ab3f2cd2f5d6bb5e/vmlinuz-6.5.0-10-generic", + "rw": null, + "splash": null, + "plymouth.ignore-serial-consoles": null, + "quiet": null, + "loglevel": "0", + "ostree": "/ostree/boot.1/eos/05199b9a534975d167eff9621ff44b04342dff52cb692b76ab3f2cd2f5d6bb5e/0", + "endless.image.device": "UUID=107EB4247EB4048E", + "endless.image.path": "/endless/endless.img", + "nohibernate": null +} diff --git a/tests/espgen/windows-efi-mounts-init.json b/tests/espgen/windows-efi-mounts-init.json new file mode 100644 index 0000000..fce9f5f --- /dev/null +++ b/tests/espgen/windows-efi-mounts-init.json @@ -0,0 +1,34 @@ +[ + { + "target": "/", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/2b89dbb29d8a02e2fcc96b9812dcb996222f7b38d064e303c899bc04f845cbc3.0", + "options": "ro,relatime" + }, + { + "target": "/boot", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime" + }, + { + "target": "/usr", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/2b89dbb29d8a02e2fcc96b9812dcb996222f7b38d064e303c899bc04f845cbc3.0/usr", + "options": "ro,relatime" + }, + { + "target": "/sysroot", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime" + } +] diff --git a/tests/espgen/windows-efi-mounts-reload.json b/tests/espgen/windows-efi-mounts-reload.json new file mode 100644 index 0000000..01685a7 --- /dev/null +++ b/tests/espgen/windows-efi-mounts-reload.json @@ -0,0 +1,50 @@ +[ + { + "target": "/", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/2b89dbb29d8a02e2fcc96b9812dcb996222f7b38d064e303c899bc04f845cbc3.0", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/boot", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/boot", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/usr", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/deploy/2b89dbb29d8a02e2fcc96b9812dcb996222f7b38d064e303c899bc04f845cbc3.0/usr", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/sysroot", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/var", + "source": "/dev/loop0p3", + "maj:min": "259:2", + "fstype": "ext4", + "fsroot": "/ostree/deploy/eos/var", + "options": "ro,relatime,errors=remount-ro" + }, + { + "target": "/efi", + "source": "/dev/sda1", + "maj:min": "8:1", + "fstype": "vfat", + "fsroot": "/", + "options": "rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro" + } +] diff --git a/tests/espgen/windows-efi-partitions-init.json b/tests/espgen/windows-efi-partitions-init.json new file mode 100644 index 0000000..82f8ca8 --- /dev/null +++ b/tests/espgen/windows-efi-partitions-init.json @@ -0,0 +1,103 @@ +[ + { + "DEVNAME": "/dev/loop0p3", + "LABEL": "ostree", + "UUID": "4bdeb166-7be1-40d8-a73c-6555db153bf5", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "cae19154-af25-7b4c-972c-ad7e1bf16501", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "33933927", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/loop0p1", + "SEC_TYPE": "msdos", + "UUID": "AE27-C340", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "f4293fff-3134-2a49-a106-3381175ea360", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/loop0p2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "68b1bc0f-1961-ac4f-81cc-1b22ba659ce0", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/sda4", + "BLOCK_SIZE": "512", + "UUID": "ECACABC7ACAB8AA2", + "TYPE": "ntfs", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "8236a774-8b07-41fc-b213-c426fe500760", + "PART_ENTRY_TYPE": "de94bba4-06d1-4d40-a16a-bfd50179d6ac", + "PART_ENTRY_FLAGS": "0x8000000000000001", + "PART_ENTRY_NUMBER": "4", + "PART_ENTRY_OFFSET": "103784448", + "PART_ENTRY_SIZE": "1069056", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "Microsoft reserved partition", + "PART_ENTRY_UUID": "db00106a-064f-46ff-8310-ad78b8830282", + "PART_ENTRY_TYPE": "e3c9e316-0b5c-4db8-817d-f92df00215ae", + "PART_ENTRY_FLAGS": "0x8000000000000000", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "206848", + "PART_ENTRY_SIZE": "32768", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda3", + "BLOCK_SIZE": "512", + "UUID": "107EB4247EB4048E", + "TYPE": "ntfs", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "Basic data partition", + "PART_ENTRY_UUID": "6a418dd1-ef6e-420e-8473-c2c081649374", + "PART_ENTRY_TYPE": "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "239616", + "PART_ENTRY_SIZE": "103543269", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda1", + "UUID": "A8B2-3D19", + "VERSION": "FAT32", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "EFI system partition", + "PART_ENTRY_UUID": "7e2d5124-6070-4167-8952-6cc10812caa0", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_FLAGS": "0x8000000000000000", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "204800", + "PART_ENTRY_DISK": "8:0" + } +] diff --git a/tests/espgen/windows-efi-partitions-reload.json b/tests/espgen/windows-efi-partitions-reload.json new file mode 100644 index 0000000..c1deb2a --- /dev/null +++ b/tests/espgen/windows-efi-partitions-reload.json @@ -0,0 +1,110 @@ +[ + { + "DEVNAME": "/dev/loop0p3", + "LABEL": "ostree", + "UUID": "4bdeb166-7be1-40d8-a73c-6555db153bf5", + "VERSION": "1.0", + "BLOCK_SIZE": "4096", + "TYPE": "ext4", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "cae19154-af25-7b4c-972c-ad7e1bf16501", + "PART_ENTRY_TYPE": "4f68bce3-e8cd-4db1-96e7-fbcaf984b709", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "131072", + "PART_ENTRY_SIZE": "33933927", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/loop0p1", + "SEC_TYPE": "msdos", + "UUID": "AE27-C340", + "VERSION": "FAT16", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "f4293fff-3134-2a49-a106-3381175ea360", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "126976", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/loop0p2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "68b1bc0f-1961-ac4f-81cc-1b22ba659ce0", + "PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "129024", + "PART_ENTRY_SIZE": "2048", + "PART_ENTRY_DISK": "7:0" + }, + { + "DEVNAME": "/dev/sda4", + "BLOCK_SIZE": "512", + "UUID": "ECACABC7ACAB8AA2", + "TYPE": "ntfs", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_UUID": "8236a774-8b07-41fc-b213-c426fe500760", + "PART_ENTRY_TYPE": "de94bba4-06d1-4d40-a16a-bfd50179d6ac", + "PART_ENTRY_FLAGS": "0x8000000000000001", + "PART_ENTRY_NUMBER": "4", + "PART_ENTRY_OFFSET": "103784448", + "PART_ENTRY_SIZE": "1069056", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda2", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "Microsoft reserved partition", + "PART_ENTRY_UUID": "db00106a-064f-46ff-8310-ad78b8830282", + "PART_ENTRY_TYPE": "e3c9e316-0b5c-4db8-817d-f92df00215ae", + "PART_ENTRY_FLAGS": "0x8000000000000000", + "PART_ENTRY_NUMBER": "2", + "PART_ENTRY_OFFSET": "206848", + "PART_ENTRY_SIZE": "32768", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda3", + "BLOCK_SIZE": "512", + "UUID": "107EB4247EB4048E", + "TYPE": "ntfs", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "Basic data partition", + "PART_ENTRY_UUID": "6a418dd1-ef6e-420e-8473-c2c081649374", + "PART_ENTRY_TYPE": "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7", + "PART_ENTRY_NUMBER": "3", + "PART_ENTRY_OFFSET": "239616", + "PART_ENTRY_SIZE": "103543269", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/sda1", + "UUID": "A8B2-3D19", + "VERSION": "FAT32", + "BLOCK_SIZE": "512", + "TYPE": "vfat", + "USAGE": "filesystem", + "PART_ENTRY_SCHEME": "gpt", + "PART_ENTRY_NAME": "EFI system partition", + "PART_ENTRY_UUID": "7e2d5124-6070-4167-8952-6cc10812caa0", + "PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "PART_ENTRY_FLAGS": "0x8000000000000000", + "PART_ENTRY_NUMBER": "1", + "PART_ENTRY_OFFSET": "2048", + "PART_ENTRY_SIZE": "204800", + "PART_ENTRY_DISK": "8:0" + }, + { + "DEVNAME": "/dev/zram0", + "UUID": "10c3a2ce-da4a-4173-bd6b-b4b2a913dcc6", + "VERSION": "1", + "TYPE": "swap", + "USAGE": "other" + } +] diff --git a/tests/test_esp_generator.py b/tests/test_esp_generator.py new file mode 100644 index 0000000..6a46eb4 --- /dev/null +++ b/tests/test_esp_generator.py @@ -0,0 +1,498 @@ +# Copyright © 2023 Endless OS Foundation, LLC +# +# 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; either version 2 of the License, or (at +# your option) any later version. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +""" +Tests for eos-esp-generator +""" + +from contextlib import contextmanager +from io import BytesIO +import json +import logging +import os +from pathlib import Path +import pytest +import sys +from textwrap import dedent + +from .util import import_script_as_module, TESTS_PATH + +espgen = import_script_as_module('espgen', 'eos-esp-generator') + +logger = logging.getLogger(__name__) +TESTSDATADIR = TESTS_PATH / 'espgen' + + +@pytest.fixture(autouse=True) +def generator_environment(monkeypatch): + """Generator environment fixture""" + monkeypatch.setenv('SYSTEMD_IN_INITRD', '0') + + +@pytest.fixture +def root_dir(tmp_path, monkeypatch): + """Temporary root directory""" + path = tmp_path / 'root' + monkeypatch.setenv('ESPGEN_ROOT_PATH', str(path)) + path.mkdir() + path.joinpath('boot').mkdir() + path.joinpath('efi').mkdir() + return path + + +@pytest.fixture +def unit_dir(root_dir): + """Temporary systemd generator unit directory""" + path = root_dir / 'run/systemd/generator' + path.mkdir(parents=True) + return path + + +@pytest.fixture +def mock_system_data(monkeypatch, root_dir): + """System data mocking factory fixture + + Provides a function that can be called with the desired mocked data. + This will override the generator calls that use findmnt and blkid. + """ + def _mock_system_data(system, reload=False): + phase = 'reload' if reload else 'init' + + firmware_efi_path = root_dir / 'sys/firmware/efi' + firmware_efi_path.mkdir(parents=True, exist_ok=True) + + mounts = TESTSDATADIR / f'{system}-mounts-{phase}.json' + with mounts.open() as f: + mounts_data = json.load(f) + logger.debug(f'mounts: {mounts_data}') + + def _fake_mount_data(root=Path('/')): + return mounts_data + + monkeypatch.setattr(espgen, 'get_mount_data', _fake_mount_data) + + fstab = TESTSDATADIR / f'{system}-fstab.json' + with fstab.open() as f: + fstab_data = json.load(f) + logger.debug(f'fstab: {fstab_data}') + + def _fake_fstab_data(): + return fstab_data + + monkeypatch.setattr(espgen, 'get_fstab_data', _fake_fstab_data) + + partitions = TESTSDATADIR / f'{system}-partitions-{phase}.json' + with partitions.open() as f: + partitions_data = json.load(f) + logger.debug(f'partitions: {partitions_data}') + + def _fake_partition_data(): + return partitions_data + + monkeypatch.setattr(espgen, 'get_partition_data', _fake_partition_data) + + kcmdline = TESTSDATADIR / f'{system}-kcmdline.json' + with kcmdline.open() as f: + kcmdline_data = json.load(f) + logger.debug(f'kcmdline: {kcmdline_data}') + + entries = [] + for name, value in kcmdline_data.items(): + if value is not None: + entry = f'{name}={value}' + else: + entry = name + entries.append(entry) + kcmdline_str = ' '.join(entries) + '\n' + kcmdline_path = root_dir / 'proc/cmdline' + kcmdline_path.parent.mkdir(parents=True, exist_ok=True) + with open(kcmdline_path, 'w') as f: + f.write(kcmdline_str) + + return _mock_system_data + + +@contextmanager +def loader_device_part_uuid(value, root): + """LoaderDevicePartUUID EFI variable context manager""" + varpath = root / 'sys/firmware/efi/efivars' / espgen.LOADER_DEVICE_PART_UUID_EFIVAR + varpath.parent.mkdir(parents=True, exist_ok=True) + + # Using the utf-16 codec for writing will add the byte order mark + # (BOM). Select the native endian version. + utf_16 = 'utf-16-le' if sys.byteorder == 'little' else 'utf-16-be' + + with open(varpath, 'wb') as f: + f.write(b'\0\0\0\0') + f.write(value.encode(utf_16)) + + # Add 3 nul byte terminators like systemd-boot does. + f.write(b'\0\0\0') + + try: + yield + finally: + varpath.unlink() + + +# Dictionary of mocked data to use with the test_get_esp_mount() test. The keys +# are names and the values are expected EspMount instances. +ESP_MOUNT_TEST_DATA = { + # Standard EOS using grub with GPT partitioning. /boot gets mounted in the + # initramfs by ostree-prepare-root. + 'grub-gpt': espgen.EspMount( + source='/dev/vda1', + target='/efi', + type='vfat', + umask='0077', + ), + + # Standard EOS using grub with MBR partitioning. /boot gets mounted in the + # initramfs by ostree-prepare-root. + 'grub-mbr': espgen.EspMount( + source='/dev/vda1', + target='/efi', + type='vfat', + umask='0077', + ), + + # Standard EOS using grub with GPT partitioning and /efi in fstab. /boot + # gets mounted in the initramfs by ostree-prepare-root. It's not typical + # that there would be a /efi entry in fstab, but this test ensures no /efi + # mount units are created. + 'grub-gpt-fstab-efi': None, + + # EOS using systemd-boot with GPT partitioning. This represents PAYG + # systemd. There is no /boot mounted by ostree-prepare-root since the root + # partition's /boot directory is empty. The ESP should be mounted at /boot + # world readable so that ostree deployments can be resolved unprivileged. + 'sdboot': espgen.EspMount( + source='/dev/vda1', + target='/boot', + type='vfat', + umask='0022', + ), + + # EOS using systemd-boot with GPT partitioning and an XBOOTLDR partition. + # This is not something we currently do or try to support since it would + # provide little benefit. Mostly this is here to ensure we don't add + # incorrect mounts if the situation starts happening. + 'sdboot-xbootldr': None, + + # Windows dual boot using EFI. The Endless disk (including an ESP + # partition) is a file on the NTFS partition that gets loop mounted. The + # real disk's ESP should be mounted instead of the loop disk's ESP + # partition. The real disk is found from the endless.image.device kernel + # command line parameter. + 'windows-efi': espgen.EspMount( + source='/dev/sda1', + target='/efi', + type='vfat', + umask='0077', + ), +} + + +@pytest.mark.parametrize('system', sorted(ESP_MOUNT_TEST_DATA.keys())) +def test_get_esp_mount(system, root_dir, mock_system_data, monkeypatch): + """EspGenerator.get_esp_mount tests""" + expected_mount = ESP_MOUNT_TEST_DATA[system] + mock_system_data(system) + + # Running during init. + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount == expected_mount + + # If there's no /sys/firmware/efi directory, it should do nothing. + firmware_efi_path = root_dir / 'sys/firmware/efi' + firmware_efi_path.rmdir() + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount is None + firmware_efi_path.mkdir() + + # If the generator is run in the initrd, it should do nothing. + with monkeypatch.context() as mctx: + mctx.setenv('SYSTEMD_IN_INITRD', '1') + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount is None + + # If the expected mount directory is /efi but there's no /efi + # directory, nothing should be mounted since there's already a mount + # on /boot. + root_dir.joinpath('efi').rmdir() + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + if expected_mount is None or expected_mount.target == '/efi': + assert esp_mount is None + else: + assert esp_mount == expected_mount + root_dir.joinpath('efi').mkdir() + + expected_esp_part = {} + if expected_mount: + partitions = espgen.get_partition_data() + expected_esp_part = next( + filter(lambda p: p['DEVNAME'] == expected_mount.source, partitions), + ) + + # If the ESP disk is GPT partitioned, test the LoaderDevicePartUUID + # EFI variable handling. + if expected_esp_part.get('disk_label') == 'gpt': + # Set it to the expected value. + with loader_device_part_uuid(expected_esp_part['uuid'].upper(), root_dir): + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount == expected_mount + + # Set it to a different value. This should cause the mount to be + # skipped. + with loader_device_part_uuid('4859CD39-28DC-4C60-B509-C2A63A80483B', root_dir): + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount is None + + # Load the post-init mounts data to check that running during a + # reload still creates the mount unit. + mock_system_data(system, reload=True) + generator = espgen.EspGenerator() + esp_mount = generator.get_esp_mount() + assert esp_mount == expected_mount + + +def test_write_units(unit_dir): + """EspMount.write_units tests""" + esp_mount = espgen.EspMount( + source='/dev/vda1', + target='/efi', + type='vfat', + umask='0077', + ) + esp_mount.write_units(unit_dir) + automount_unit = unit_dir / 'efi.automount' + mount_unit = unit_dir / 'efi.mount' + local_fs_wants = unit_dir / 'local-fs.target.wants/efi.automount' + units = set() + for dirpath, dirnames, filenames in os.walk(unit_dir): + units.update([unit_dir / dirpath / f for f in filenames]) + assert units == {automount_unit, mount_unit, local_fs_wants} + assert not os.path.isabs(os.readlink(local_fs_wants)) + assert local_fs_wants.resolve() == automount_unit + + automount_contents = automount_unit.read_text() + assert automount_contents == dedent("""\ + # Automatically generated by eos-esp-generator + + [Unit] + Description=EFI System Partition Automount + + [Automount] + Where=/efi + TimeoutIdleSec=2min + """) + + mount_contents = mount_unit.read_text() + assert mount_contents == dedent("""\ + # Automatically generated by eos-esp-generator + + [Unit] + Description=EFI System Partition Automount + Requires=systemd-fsck@dev-vda1.service + After=systemd-fsck@dev-vda1.service + After=blockdev@dev-vda1.target + + [Mount] + What=/dev/vda1 + Where=/efi + Type=vfat + Options=umask=0077,noauto,rw + """) + + +def test_read_efivar(root_dir, caplog): + """read_efivar tests""" + var = 'Foo-7553c1d3-754b-47f3-a613-b9e2b860e8c1' + varpath = root_dir / 'sys/firmware/efi/efivars' / var + varpath.parent.mkdir(parents=True) + + # The first 32 bits are an attribute mask. Use all 1s so we can + # better detect it leaking into the value. + attr = b'\xff\xff\xff\xff' + + # Missing variable should return None. + varpath.unlink(missing_ok=True) + value = espgen.read_efivar(var) + assert value is None + + # Empty value. + with open(varpath, 'wb') as f: + f.write(attr) + value = espgen.read_efivar(var) + assert value == b'' + + # Actual contents. + with open(varpath, 'wb') as f: + f.write(attr) + f.write(b'hello') + value = espgen.read_efivar(var) + assert value == b'hello' + + # Invalid contents should log a warning and return None. + with open(varpath, 'wb') as f: + pass + caplog.clear() + with caplog.at_level(logging.WARNING): + value = espgen.read_efivar(var) + assert value is None + assert caplog.record_tuples == [( + espgen.logger.name, + logging.WARNING, + f'Invalid EFI variable {var} is less than 4 bytes', + )] + + +def test_efivar_utf16_string(root_dir): + """read_efivar_utf16_string tests""" + var = 'Bar-7553c1d3-754b-47f3-a613-b9e2b860e8c1' + varpath = root_dir / 'sys/firmware/efi/efivars' / var + varpath.parent.mkdir(parents=True) + + # The first 32 bits are an attribute mask. Use all 1s so we can + # better detect it leaking into the value. + attr = b'\xff\xff\xff\xff' + + # Using the utf-16 codec for writing will add the byte order mark + # (BOM). Select the native endian version. + utf_16 = 'utf-16-le' if sys.byteorder == 'little' else 'utf-16-be' + + # Missing variable should return None. + varpath.unlink(missing_ok=True) + value = espgen.read_efivar_utf16_string(var) + assert value is None + + # Empty value. + with open(varpath, 'wb') as f: + f.write(attr) + value = espgen.read_efivar_utf16_string(var) + assert value == '' + + # Actual contents. + with open(varpath, 'wb') as f: + f.write(attr) + f.write('hello'.encode(utf_16)) + value = espgen.read_efivar_utf16_string(var) + assert value == 'hello' + + # Only nul terminator. + with open(varpath, 'wb') as f: + f.write(attr) + f.write(b'\0\0') + value = espgen.read_efivar_utf16_string(var) + assert value == '' + + # Various nul byte terminators. + for n in range(6): + term = b'\0' * n + with open(varpath, 'wb') as f: + f.write(attr) + f.write('hello'.encode(utf_16)) + f.write(term) + value = espgen.read_efivar_utf16_string(var) + assert value == 'hello' + + # Unicode. + uface = '\N{UPSIDE-DOWN FACE}' # 🙃 + for term in (b'', b'\0\0'): + with open(varpath, 'wb') as f: + f.write(attr) + f.write(uface.encode(utf_16)) + f.write(term) + value = espgen.read_efivar_utf16_string(var) + assert value == uface + + +def test_kmsg_handler(): + """KmsgHandler tests""" + pid = os.getpid() + buf = BytesIO() + formatter = logging.Formatter('%(message)s') + handler = espgen.KmsgHandler(buf) + handler.setFormatter(formatter) + + def _expected_prefix(priority): + return f'<{priority:d}>eos-esp-generator[{pid:d}]: '.encode('utf-8') + + # Default log level corresponds to LOG_INFO. + buf.truncate(0) + buf.seek(0) + record = logging.makeLogRecord({'msg': 'msg'}) + handler.emit(record) + assert buf.getvalue() == _expected_prefix(6) + b'msg' + + # Error message. + buf.truncate(0) + buf.seek(0) + record = logging.makeLogRecord({'msg': 'msg', 'levelno': logging.ERROR}) + handler.emit(record) + assert buf.getvalue() == _expected_prefix(3) + b'msg' + + # Debug message. + buf.truncate(0) + buf.seek(0) + record = logging.makeLogRecord({'msg': 'msg', 'levelno': logging.DEBUG}) + handler.emit(record) + assert buf.getvalue() == _expected_prefix(7) + b'msg' + + # Unicode. + buf.truncate(0) + buf.seek(0) + msg = '\N{UPSIDE-DOWN FACE}' + record = logging.makeLogRecord({'msg': msg}) + handler.emit(record) + assert buf.getvalue() == _expected_prefix(6) + msg.encode('utf-8') + + # Splitting at 1024 bytes, non-unicode. + buf.truncate(0) + buf.seek(0) + prefix = _expected_prefix(6) + size = 1024 - len(prefix) + msg_len = 2000 + msg = 'x' * msg_len + record = logging.makeLogRecord({'msg': msg}) + handler.emit(record) + assert buf.getvalue() == ( + prefix + b'x' * size + + prefix + b'x' * size + + prefix + b'x' * (msg_len - 2 * size) + ) + + # Splitting at 1024 bytes, multibyte unicode. Add a multibyte unicode + # character at the 1024th byte to check that it gets split to a separate + # record. + buf.truncate(0) + buf.seek(0) + prefix = _expected_prefix(6) + size = 1024 - len(prefix) + msg = 'x' * (size - 1) + '\N{UPSIDE-DOWN FACE}' + record = logging.makeLogRecord({'msg': msg}) + handler.emit(record) + assert buf.getvalue() == ( + prefix + b'x' * (size - 1) + + prefix + '\N{UPSIDE-DOWN FACE}'.encode('utf-8') + )