← Back to team overview

curtin-dev team mailing list archive

[Merge] ~sjg1/curtin:boot-schema-update-23.1 into curtin:master

 

Simon Glass has proposed merging ~sjg1/curtin:boot-schema-update-23.1 into curtin:master.

Commit message:
Curtin currently assumes that grub is used as the bootloader.

Generalise the schema to support multiple bootloaders

TBD: Add extlinux support

Requested reviews:
  Dan Bungert (dbungert)

For more details, see:
https://code.launchpad.net/~sjg1/curtin/+git/curtin/+merge/479586

The idea here is to allow MAAS to set up RISC-V clients without needing grub. They can use extlinux instead, meaning that U-Boot is enough to handle the full boot.

I have made the schema changes but I need to figure out how to get the device and mount point for the root and boot devices.
-- 
Your team curtin developers is subscribed to branch curtin:master.
diff --git a/.gitignore b/.gitignore
index 8f343e7..1c6bd5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ __pycache__
 .tox
 .coverage
 curtin.egg-info/
+doc/_build
diff --git a/curtin/commands/__init__.py b/curtin/commands/__init__.py
index 51b91c6..2d784db 100644
--- a/curtin/commands/__init__.py
+++ b/curtin/commands/__init__.py
@@ -1,5 +1,6 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
+
 class MutuallyExclusiveGroup:
     def __init__(self, entries) -> None:
         self.entries = entries
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index 7506c4a..918bd79 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -1,7 +1,11 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 from collections import OrderedDict, namedtuple
+<<<<<<< curtin/commands/block_meta.py
 from curtin import (block, compat, config, paths, storage_actions, util)
+=======
+from curtin import (block, compat, config, paths, util)
+>>>>>>> curtin/commands/block_meta.py
 from curtin.block import schemas
 from curtin.block import (bcache, clear_holders, dasd, iscsi, lvm, mdadm, mkfs,
                           multipath, zfs)
@@ -674,10 +678,16 @@ class Device:
 
 
 def image_handler(info, storage_config, context):
+<<<<<<< curtin/commands/block_meta.py
     image: Image = storage_actions.asobject(info)
     path = image.path
     size = image.size
     if image.preserve:
+=======
+    path = info['path']
+    size = int(util.human2bytes(info['size']))
+    if info.get('preserve', False):
+>>>>>>> curtin/commands/block_meta.py
         actual_size = os.stat(path).st_size
         if size != actual_size:
             raise RuntimeError(
@@ -695,7 +705,11 @@ def image_handler(info, storage_config, context):
             raise
 
     cmd = ['losetup', '--show', '--find', path]
+<<<<<<< curtin/commands/block_meta.py
     sector_size = image.sector_size
+=======
+    sector_size = int(util.human2bytes(info.get('sector_size', 512)))
+>>>>>>> curtin/commands/block_meta.py
     if sector_size != 512:
         compat.supports_large_sectors(fatal=True)
         cmd.extend(('--sector-size', str(sector_size)))
diff --git a/curtin/commands/block_meta_v2.py b/curtin/commands/block_meta_v2.py
index 946751a..c8a2ad9 100644
--- a/curtin/commands/block_meta_v2.py
+++ b/curtin/commands/block_meta_v2.py
@@ -1,10 +1,6 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 import os
-from typing import (
-    List,
-    Optional,
-    )
 
 import attr
 
@@ -27,6 +23,7 @@ from curtin.storage_config import (
 from curtin.udev import udevadm_settle
 
 
+<<<<<<< curtin/commands/block_meta_v2.py
 def to_utf8_hex_notation(string: str) -> str:
     ''' Convert a string into a valid ASCII string where all characters outside
     the alphanumerical range (according to bytes.isalnum()) are translated to
@@ -45,20 +42,23 @@ def to_utf8_hex_notation(string: str) -> str:
 
 
 @attr.s(auto_attribs=True)
+=======
+@attr.s()
+>>>>>>> curtin/commands/block_meta_v2.py
 class PartTableEntry:
     # The order listed here matches the order sfdisk represents these fields
     # when using the --dump argument.
-    number: int
-    start: int
-    size: int
-    type: str
-    uuid: Optional[str]
+    number = attr.ib(default=None)
+    start = attr.ib(default=None)
+    size = attr.ib(default=None)
+    type = attr.ib(default=None)
+    uuid = attr.ib(default=None)
     # name here is the sfdisk term - quoted descriptive text of the partition -
     # not to be confused with what make_dname() does.
     # Offered in the partition command as 'partition_name'.
-    name: Optional[str]
-    attrs: Optional[List[str]]
-    bootable: bool = False
+    name = attr.ib(default=None)
+    attrs = attr.ib(default=None)
+    bootable = attr.ib(default=False)
 
     def render(self):
         r = '{}: '.format(self.number)
@@ -149,8 +149,8 @@ class SFDiskPartTable:
         self._sector_bytes = sector_bytes
         if ONE_MIB_BYTES % sector_bytes != 0:
             raise Exception(
-                f"sector_bytes {sector_bytes} does not divide 1MiB, cannot "
-                "continue!")
+                "sector_bytes {} does not divide 1MiB, cannot "
+                "continue!".format(sector_bytes))
         self.one_mib_sectors = ONE_MIB_BYTES // sector_bytes
 
     def bytes2sectors(self, amount):
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index 82ebcab..6bd5d9d 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -32,6 +32,7 @@ from curtin.net import deps as ndeps
 from curtin.reporter import events
 from curtin.commands import apply_net, apt_config
 from curtin.commands.install_grub import install_grub
+from curtin.commands.install_extlinux import install_extlinux
 from curtin.url_helper import get_maas_version
 
 from . import populate_one_subcmd
@@ -439,7 +440,11 @@ def install_kernel(cfg, target):
                          " System may not boot.", package)
 
 
+<<<<<<< curtin/commands/curthooks.py
 def uefi_remove_old_loaders(grubcfg: config.GrubConfig, target: str):
+=======
+def uefi_remove_old_loaders(bootcfg, target):
+>>>>>>> curtin/commands/curthooks.py
     """Removes the old UEFI loaders from efibootmgr."""
     efi_state = util.get_efibootmgr(target)
 
@@ -450,6 +455,7 @@ def uefi_remove_old_loaders(grubcfg: config.GrubConfig, target: str):
         for number, entry in efi_state.entries.items()
         if 'File(\\EFI' in entry.path
     }
+<<<<<<< curtin/commands/curthooks.py
 
     if efi_state.current in old_efi_entries:
         del old_efi_entries[efi_state.current]
@@ -469,6 +475,18 @@ def uefi_remove_old_loaders(grubcfg: config.GrubConfig, target: str):
             len(old_efi_entries),
             '' if len(old_efi_entries) == 1 else 's')
         for entry in old_efi_entries.values():
+=======
+    old_efi_entries.pop(current_uefi_boot, None)
+    remove_old_loaders = bootcfg.get('remove_old_uefi_loaders', True)
+    if old_efi_entries:
+        if remove_old_loaders:
+            with util.ChrootableTarget(target) as in_chroot:
+                for entry, info in old_efi_entries.items():
+                    LOG.debug("removing old UEFI entry: %s" % info['name'])
+                    in_chroot.subp(
+                        ['efibootmgr', '-B', '-b', entry], capture=True)
+        else:
+>>>>>>> curtin/commands/curthooks.py
             LOG.debug(
                 "UEFI entry '%s' might no longer exist and "
                 "should be removed.", entry.name)
@@ -531,12 +549,16 @@ def _reorder_new_entry(
     return new_order
 
 
+<<<<<<< curtin/commands/curthooks.py
 def uefi_reorder_loaders(
         grubcfg: config.GrubConfig,
         target: str,
         efi_orig_state: util.EFIBootState,
         variant: str,
         ) -> None:
+=======
+def uefi_reorder_loaders(bootcfg, target, efi_orig=None, variant=None):
+>>>>>>> curtin/commands/curthooks.py
     """Reorders the UEFI BootOrder to place BootCurrent first.
 
     The specifically doesn't try to do to much. The order in which grub places
@@ -548,7 +570,48 @@ def uefi_reorder_loaders(
     is installed after the the previous first entry (before we installed grub).
 
     """
+<<<<<<< curtin/commands/curthooks.py
     if not grubcfg.reorder_uefi:
+=======
+    if bootcfg.get('reorder_uefi', True):
+        efi_output = util.get_efibootmgr(target=target)
+        LOG.debug('UEFI efibootmgr output after install:\n%s', efi_output)
+        currently_booted = efi_output.get('current', None)
+        boot_order = efi_output.get('order', [])
+        new_boot_order = None
+        force_fallback_reorder = config.value_as_boolean(
+            bootcfg.get('reorder_uefi_force_fallback', False))
+        if currently_booted and force_fallback_reorder is False:
+            if currently_booted in boot_order:
+                boot_order.remove(currently_booted)
+            boot_order = [currently_booted] + boot_order
+            new_boot_order = ','.join(boot_order)
+            LOG.debug(
+                "Setting currently booted %s as the first "
+                "UEFI loader.", currently_booted)
+        else:
+            reason = (
+                "config 'reorder_uefi_force_fallback' is True" if
+                force_fallback_reorder else "missing 'BootCurrent' value")
+            LOG.debug("Using fallback UEFI reordering: " + reason)
+            if len(boot_order) < 2:
+                LOG.debug(
+                    'UEFI BootOrder has less than 2 entries, cannot reorder')
+                return
+            # look at efi entries before we added one to find the new addition
+            new_order = _reorder_new_entry(
+                    copy.deepcopy(boot_order), efi_output, efi_orig, variant)
+            if new_order != boot_order:
+                new_boot_order = ','.join(new_order)
+            else:
+                LOG.debug("UEFI No changes to boot order.")
+        if new_boot_order:
+            LOG.debug(
+                "New UEFI boot order: %s", new_boot_order)
+            with util.ChrootableTarget(target) as in_chroot:
+                in_chroot.subp(['efibootmgr', '-o', new_boot_order])
+    else:
+>>>>>>> curtin/commands/curthooks.py
         LOG.debug("Skipped reordering of UEFI boot methods.")
         LOG.debug("Currently booted UEFI loader might no longer boot.")
         return
@@ -572,6 +635,7 @@ def uefi_reorder_loaders(
             reason = "missing 'BootCurrent' value"
         LOG.debug("Using fallback UEFI reordering: " + reason)
 
+<<<<<<< curtin/commands/curthooks.py
         if len(efi_state.order) < 2:
             LOG.debug(
                 'UEFI BootOrder has less than 2 entries, cannot reorder')
@@ -600,6 +664,14 @@ def uefi_remove_duplicate_entries(
         return
 
     to_remove = uefi_find_duplicate_entries(util.get_efibootmgr(target=target))
+=======
+def uefi_remove_duplicate_entries(bootcfg, target, to_remove=None):
+    if not bootcfg.get('remove_duplicate_entries', True):
+        LOG.debug("Skipped removing duplicate UEFI boot entries per config.")
+        return
+    if to_remove is None:
+        to_remove = uefi_find_duplicate_entries(bootcfg, target)
+>>>>>>> curtin/commands/curthooks.py
 
     # check so we don't run ChrootableTarget code unless we have things to do
     if not to_remove:
@@ -612,9 +684,13 @@ def uefi_remove_duplicate_entries(
                 ['efibootmgr', '--bootnum=%s' % bootnum, '--delete-bootnum'])
 
 
+<<<<<<< curtin/commands/curthooks.py
 def uefi_find_duplicate_entries(
         efi_state: util.EFIBootState,
         ) -> List[Tuple[str, util.EFIBootEntry]]:
+=======
+def uefi_find_duplicate_entries(bootcfg, target, efi_output=None):
+>>>>>>> curtin/commands/curthooks.py
     seen = set()
     to_remove = []
     # adding BootCurrent to seen first allows us to remove any other duplicate
@@ -752,6 +828,7 @@ def setup_grub(
     from curtin.commands.block_meta import (extract_storage_ordered_dict,
                                             get_path_to_storage_volume)
 
+<<<<<<< curtin/commands/curthooks.py
     grubcfg_d = cfg.get('grub', {})
 
     # copy legacy top level name
@@ -759,6 +836,9 @@ def setup_grub(
         grubcfg_d['install_devices'] = cfg['grub_install_devices']
 
     grubcfg = config.fromdict(config.GrubConfig, grubcfg_d)
+=======
+    bootcfg = cfg.get('boot', {})
+>>>>>>> curtin/commands/curthooks.py
 
     LOG.debug("setup grub on target %s", target)
     # if there is storage config, look for devices tagged with 'grub_device'
@@ -784,6 +864,7 @@ def setup_grub(
                     get_path_to_storage_volume(item_id, storage_cfg_odict))
 
         if len(storage_grub_devices) > 0:
+<<<<<<< curtin/commands/curthooks.py
             if grubcfg.install_devices and \
                grubcfg.install_devices is not grubcfg.install_devices_default:
                 LOG.warn("Storage Config grub device config takes precedence "
@@ -794,6 +875,19 @@ def setup_grub(
     LOG.debug("install_devices: %s", grubcfg.install_devices)
     if grubcfg.install_devices is not grubcfg.install_devices_default:
         instdevs = grubcfg.install_devices
+=======
+            if len(bootcfg.get('install_devices', [])):
+                LOG.warn("Storage Config grub device config takes precedence "
+                         "over grub 'install_devices' value, ignoring: %s",
+                         bootcfg['install_devices'])
+            bootcfg['install_devices'] = storage_grub_devices
+
+    LOG.debug("install_devices: %s", bootcfg.get('install_devices'))
+    if 'install_devices' in bootcfg:
+        instdevs = bootcfg.get('install_devices')
+        if isinstance(instdevs, str):
+            instdevs = [instdevs]
+>>>>>>> curtin/commands/curthooks.py
         if instdevs is None:
             LOG.debug("grub installation disabled by config")
     else:
@@ -843,16 +937,86 @@ def setup_grub(
     else:
         instdevs = ["none"]
 
+<<<<<<< curtin/commands/curthooks.py
     update_nvram = grubcfg.update_nvram
     if uefi_bootable and update_nvram:
         efi_orig_state = util.get_efibootmgr(target)
         uefi_remove_old_loaders(grubcfg, target)
+=======
+    update_nvram = bootcfg.get('update_nvram', True)
+    if uefi_bootable and update_nvram:
+        efi_orig_output = util.get_efibootmgr(target)
+        uefi_remove_old_loaders(bootcfg, target)
+>>>>>>> curtin/commands/curthooks.py
 
-    install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg)
+    install_grub(instdevs, target, uefi=uefi_bootable, bootcfg=bootcfg)
 
     if uefi_bootable and update_nvram:
+<<<<<<< curtin/commands/curthooks.py
         uefi_reorder_loaders(grubcfg, target, efi_orig_state, variant)
         uefi_remove_duplicate_entries(grubcfg, target)
+=======
+        uefi_reorder_loaders(bootcfg, target, efi_orig_output, variant)
+        uefi_remove_duplicate_entries(bootcfg, target)
+
+
+def translate_old_grub_schema(cfg):
+    """Translate the old top-level 'grub' configure to the new 'boot' one"""
+    grub_cfg = cfg.get('grub', {})
+
+    # Use the 'boot' key, if present
+    if 'boot' in cfg:
+        if grub_cfg:
+            raise ValueError("Configuration has both 'grub' and 'boot' keys")
+        return
+
+    # copy legacy top level name
+    if 'grub_install_devices' in cfg and 'install_devices' not in cfg:
+        grub_cfg['install_devices'] = cfg['grub_install_devices']
+
+    if 'grub' in cfg:
+        del cfg['grub']
+
+    # Create a bootloaders list with 'grub', which is implied in the old config
+    grub_cfg['bootloaders'] = ['grub']
+
+    cfg['boot'] = grub_cfg
+
+
+def setup_boot(
+        cfg: dict,
+        target: str,
+        machine: str,
+        stack_prefix: str,
+        osfamily: str,
+        variant: str,
+        ) -> None:
+    translate_old_grub_schema(cfg)
+
+    boot_cfg = cfg['boot']
+
+    # Manually check that the bootloaders are correct
+    config.check_bootcfg(boot_cfg)
+
+    bootloaders = boot_cfg['bootloaders']
+
+    # For now we have a hard-coded mechanism to determine whether grub should
+    # be installed or not. Even if the grub info is present in the config, we
+    # check the machine to decide whether or not to install it.
+    if 'grub' in bootloaders and uses_grub(machine):
+        with events.ReportEventStack(
+                name=stack_prefix + '/install-grub',
+                reporting_enabled=True, level="INFO",
+                description="installing grub to target devices"):
+            setup_grub(cfg, target, osfamily=osfamily, variant=variant)
+
+    if 'extlinux' in bootloaders:
+        with events.ReportEventStack(
+                name=stack_prefix + '/install-extlinux',
+                reporting_enabled=True, level="INFO",
+                description="installing extlinux to target devices"):
+            install_extlinux(cfg, target, osfamily=osfamily, variant=variant)
+>>>>>>> curtin/commands/curthooks.py
 
 
 def update_initramfs(target=None, all_kernels=False):
@@ -869,6 +1033,7 @@ def update_initramfs(target=None, all_kernels=False):
     # Ensure target is resolved even if it's None
     target = paths.target_path(target)
     boot = paths.target_path(target, 'boot')
+<<<<<<< curtin/commands/curthooks.py
 
     if util.which('update-initramfs', target=target):
         # We keep the all_kernels flag for callers, the implementation
@@ -934,6 +1099,18 @@ def update_initramfs(target=None, all_kernels=False):
             raise RuntimeError(
                     "cannot update the initramfs: neither update-initramfs or"
                     f" dracut found in target {target}")
+=======
+    for kfile, initrd, version in paths.get_kernel_list(target):
+        # -u == update, -c == create
+        mode = '-u' if os.path.exists(initrd) else '-c'
+        cmd = ['update-initramfs', mode, '-k', version]
+        with util.ChrootableTarget(target) as in_chroot:
+            in_chroot.subp(cmd)
+            if not os.path.exists(initrd):
+                files = os.listdir(target + '/boot')
+                LOG.debug('Failed to find initrd %s', initrd)
+                LOG.debug('Files in target /boot: %s', files)
+>>>>>>> curtin/commands/curthooks.py
 
 
 def copy_fstab(fstab, target):
@@ -1564,6 +1741,7 @@ def configure_mdadm(cfg, state_etcd, target, osfamily=DISTROS.debian):
                 data=None, target=target)
 
 
+<<<<<<< curtin/commands/curthooks.py
 def configure_nvme_over_tcp(cfg, target: pathlib.Path) -> None:
     """If any NVMe controller using the TCP transport is present in the storage
     configuration, create a nvme-stas configuration and configure the initramfs
@@ -1614,6 +1792,8 @@ Pin-Priority: -1
         nvme_tcp.configure_nvme_stas(cfg, target)
 
 
+=======
+>>>>>>> curtin/commands/curthooks.py
 def configure_kernel_crash_dumps(cfg, target: pathlib.Path) -> None:
     """Configure kernel crash dumps on target system.
 
@@ -1626,7 +1806,11 @@ def configure_kernel_crash_dumps(cfg, target: pathlib.Path) -> None:
     """
     kdump_cfg = cfg.get("kernel-crash-dumps", {})
 
+<<<<<<< curtin/commands/curthooks.py
     enabled: bool = kdump_cfg.get("enabled")
+=======
+    enabled = kdump_cfg.get("enabled")
+>>>>>>> curtin/commands/curthooks.py
     automatic = enabled is None
 
     if automatic:
@@ -2051,6 +2235,7 @@ def builtin_curthooks(cfg, target, state):
             reporting_enabled=True, level="INFO",
             description="configuring target system bootloader"):
 
+<<<<<<< curtin/commands/curthooks.py
         if uses_grub(machine):
             with events.ReportEventStack(
                     name=stack_prefix + '/install-grub',
@@ -2066,6 +2251,10 @@ def builtin_curthooks(cfg, target, state):
             description="copying metadata from /cdrom"):
         cdrom_loc: str = "/cdrom"
         copy_cdrom(cdrom_loc, target)
+=======
+        setup_boot(cfg, target, machine, stack_prefix, osfamily=osfamily,
+                   variant=distro_info.variant)
+>>>>>>> curtin/commands/curthooks.py
 
 
 def curthooks(args):
diff --git a/curtin/commands/install.py b/curtin/commands/install.py
index 9fb4116..f57ad23 100644
--- a/curtin/commands/install.py
+++ b/curtin/commands/install.py
@@ -112,16 +112,16 @@ def writeline(fname, output):
         pass
 
 
-@attr.s(auto_attribs=True)
+@attr.s()
 class WorkingDir:
-    target: str
-    top: str
-    scratch: str
-    interfaces: str
-    netconf: str
-    netstate: str
-    fstab: str
-    config_file: str
+    target = attr.ib()
+    top = attr.ib()
+    scratch = attr.ib()
+    interfaces = attr.ib()
+    netconf = attr.ib()
+    netstate = attr.ib()
+    fstab = attr.ib()
+    config_file = attr.ib()
 
     @classmethod
     def import_existing(cls, config):
diff --git a/curtin/commands/install_extlinux.py b/curtin/commands/install_extlinux.py
new file mode 100644
index 0000000..5048b06
--- /dev/null
+++ b/curtin/commands/install_extlinux.py
@@ -0,0 +1,92 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+"""This loosely follows the u-boot-update script in the u-boot-menu package"""
+
+import io
+import os
+
+from curtin import paths
+from curtin.log import LOG
+
+EXTLINUX_DIR = '/boot/extlinux'
+
+
+def build_content(target: str, bootcfg: dict):
+    """Build the content of the extlinux.conf file
+
+    For now this only supports x86, since it does not handle the 'fdt' option.
+    Rather than add that, the plan is to use a FIT (Flat Image Tree) which can
+    handle FDT selection automatically. This should avoid the complexity
+    associated with fdt and fdtdir options.
+
+    We assume that the boot/ directory is in the root partition, rather than
+    being in a separate partition. TBD.
+    """
+    def get_entry(label, params, menu_label_append=''):
+        return '''\
+label {label}
+\tmenu label {menu_label} {version}{menu_label_append}
+\tlinux /{kernel_path}
+\tinitrd /{initrd_path}
+\tappend root={root} {params}'''.format(
+        label=label,
+        menu_label=menu_label,
+        version=version,
+        menu_label_append=menu_label_append,
+        kernel_path=kernel_path,
+        initrd_path=initrd_path,
+        root=root,
+        params=params)
+
+    buf = io.StringIO()
+    params = 'ro quiet'
+    alternatives = ['default', 'recovery']
+    menu_label = 'Ubuntu 22.04.5 LTS'
+    root = '/dev/mapper/vgubuntu-root'  # TODO
+
+    # For the recovery option, remove 'quiet' and add 'single'
+    without_quiet = filter(lambda word: word != 'quiet', params.split())
+    rec_params = ' '.join(list(without_quiet) + ['single'])
+
+    print('''\
+## %s/extlinux.conf
+##
+## IMPORTANT WARNING
+##
+## The configuration of this file is generated automatically.
+## Do not edit this file manually, use: u-boot-update
+
+default l0
+menu title U-Boot menu
+prompt 0
+timeout 50''' % EXTLINUX_DIR, file=buf)
+    for seq, (kernel_path, full_initrd_path, version) in enumerate(
+            paths.get_kernel_list(target)):
+        LOG.debug('P: Writing config for %s...', kernel_path)
+        initrd_path = os.path.basename(full_initrd_path)
+        print(file=buf)
+        print(file=buf)
+        print(get_entry('l%d' % seq, params), file=buf)
+
+        if 'recovery' in alternatives:
+            print(file=buf)
+            print(get_entry('l%dr' % seq, rec_params, ' (rescue target)'),
+                  file=buf)
+
+    return buf.getvalue()
+
+
+def install_extlinux(
+        target: str,
+        bootcfg: dict,
+        ):
+    """Install extlinux to the target chroot.
+
+    :param: target: A string specifying the path to the chroot mountpoint.
+    :param: bootcfg: An config dict with grub config options.
+    """
+    content = build_content(target, bootcfg)
+    extlinux_path = paths.target_path(target, '/boot/extlinux')
+    os.makedirs(extlinux_path, exist_ok=True)
+    with open(extlinux_path + '/extlinux.conf', 'w', encoding='utf-8') as outf:
+        outf.write(content)
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
index 6d1d59e..5936ac8 100644
--- a/curtin/commands/install_grub.py
+++ b/curtin/commands/install_grub.py
@@ -2,8 +2,11 @@ import os
 import re
 import platform
 import shutil
+<<<<<<< curtin/commands/install_grub.py
 import sys
 from typing import List, Optional
+=======
+>>>>>>> curtin/commands/install_grub.py
 
 from curtin import block
 from curtin import config
@@ -11,8 +14,6 @@ from curtin import distro
 from curtin import util
 from curtin.log import LOG
 from curtin.paths import target_path
-from curtin.reporter import events
-from . import populate_one_subcmd
 
 CMD_ARGUMENTS = (
     ((('-t', '--target'),
@@ -207,6 +208,7 @@ def replace_grub_cmdline_linux_default(target, new_args):
     LOG.debug('updated %s to set: %s', target_grubconf, newcontent)
 
 
+<<<<<<< curtin/commands/install_grub.py
 def write_grub_config(
         target: str,
         grubcfg: config.GrubConfig,
@@ -218,6 +220,16 @@ def write_grub_config(
         replace_grub_cmdline_linux_default(target, new_params)
 
     probe_os = grubcfg.probe_additional_os
+=======
+def write_grub_config(target, bootcfg, grub_conf, new_params):
+    replace_default = config.value_as_boolean(
+        bootcfg.get('replace_linux_default', True))
+    if replace_default:
+        replace_grub_cmdline_linux_default(target, new_params)
+
+    probe_os = config.value_as_boolean(
+        bootcfg.get('probe_additional_os', False))
+>>>>>>> curtin/commands/install_grub.py
     if not probe_os:
         probe_content = [
             ('# Curtin disable grub os prober that might find other '
@@ -228,7 +240,14 @@ def write_grub_config(
                         "\n".join(probe_content), omode='a+')
 
     # if terminal is present in config, but unset, then don't
+<<<<<<< curtin/commands/install_grub.py
     grub_terminal = grubcfg.terminal
+=======
+    grub_terminal = bootcfg.get('terminal', 'console')
+    if not isinstance(grub_terminal, str):
+        raise ValueError("Unexpected value %s for 'terminal'. "
+                         "Value must be a string" % grub_terminal)
+>>>>>>> curtin/commands/install_grub.py
     if not grub_terminal.lower() == "unmodified":
         terminal_content = [
             '# Curtin configured GRUB_TERMINAL value',
@@ -398,6 +417,7 @@ def check_target_arch_machine(target, arch=None, machine=None, uefi=None):
         raise RuntimeError(errmsg)
 
 
+<<<<<<< curtin/commands/install_grub.py
 def install_grub(
         devices: List[str],
         target: str,
@@ -405,13 +425,16 @@ def install_grub(
         grubcfg: config.GrubConfig,
         uefi: Optional[bool] = None,
         ):
+=======
+def install_grub(devices, target, uefi=None, bootcfg=None):
+>>>>>>> curtin/commands/install_grub.py
     """Install grub to devices inside target chroot.
 
     :param: devices: List of block device paths to install grub upon.
     :param: target: A string specifying the path to the chroot mountpoint.
     :param: uefi: A boolean set to True if system is UEFI bootable otherwise
                   False.
-    :param: grubcfg: An config dict with grub config options.
+    :param: bootcfg: An config dict with grub config options.
     """
 
     if not devices:
@@ -421,8 +444,13 @@ def install_grub(
         raise ValueError("Invalid parameter 'target': %s" % target)
 
     LOG.debug("installing grub to target=%s devices=%s [replace_defaults=%s]",
+<<<<<<< curtin/commands/install_grub.py
               target, devices, grubcfg.replace_linux_default)
     update_nvram = grubcfg.update_nvram
+=======
+              target, devices, bootcfg.get('replace_default'))
+    update_nvram = config.value_as_boolean(bootcfg.get('update_nvram', True))
+>>>>>>> curtin/commands/install_grub.py
     distroinfo = distro.get_distroinfo(target=target)
     target_arch = distro.get_architecture(target=target)
     rhel_ver = (distro.rpm_get_dist_id(target)
@@ -435,7 +463,7 @@ def install_grub(
     grub_conf = get_grub_config_file(target, distroinfo.family)
     new_params = get_carryover_params(distroinfo)
     prepare_grub_dir(target, grub_conf)
-    write_grub_config(target, grubcfg, grub_conf, new_params)
+    write_grub_config(target, bootcfg, grub_conf, new_params)
     grub_cmd = get_grub_install_command(uefi, distroinfo, target)
     if uefi:
         install_cmds, post_cmds = gen_uefi_install_commands(
@@ -454,6 +482,7 @@ def install_grub(
             in_chroot.subp(cmd, env=env, capture=True)
 
 
+<<<<<<< curtin/commands/install_grub.py
 def install_grub_main(args):
     state = util.load_command_environment()
 
@@ -484,4 +513,6 @@ def install_grub_main(args):
 def POPULATE_SUBCMD(parser):
     populate_one_subcmd(parser, CMD_ARGUMENTS, install_grub_main)
 
+=======
+>>>>>>> curtin/commands/install_grub.py
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/config.py b/curtin/config.py
index e9f6516..4662dd7 100644
--- a/curtin/config.py
+++ b/curtin/config.py
@@ -130,6 +130,7 @@ def value_as_boolean(value):
     return value not in false_values
 
 
+<<<<<<< curtin/config.py
 def _convert_install_devices(value):
     if isinstance(value, str):
         return [value]
@@ -293,5 +294,20 @@ def fromdict(cls: typing.Type[T], d) -> T:
     deserializer = Deserializer()
     return deserializer.deserialize(cls, d)
 
+=======
+def check_bootcfg(bootcfg):
+    vals = bootcfg.get('bootloaders')
+    if vals is None:
+        raise ValueError("missing required key: 'bootloaders'")
+    if not isinstance(vals, list):
+        raise ValueError('bootloaders must be a list: %s' % vals)
+    if not vals:
+        raise ValueError('Empty bootloaders list: %s' % vals)
+    if len(vals) != len(set(vals)):
+        raise ValueError('bootloaders list contains duplicates: %s' % vals)
+    for val in vals:
+        if val not in ['grub', 'extlinux']:
+            raise ValueError('Unknown bootloader %s: %s' % (val, vals))
+>>>>>>> curtin/config.py
 
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/kernel_crash_dumps.py b/curtin/kernel_crash_dumps.py
index 5012945..3d87760 100644
--- a/curtin/kernel_crash_dumps.py
+++ b/curtin/kernel_crash_dumps.py
@@ -4,7 +4,11 @@ from pathlib import Path
 
 from curtin import distro
 from curtin.log import LOG
+<<<<<<< curtin/kernel_crash_dumps.py
 from curtin.util import ChrootableTarget
+=======
+from curtin.util import ChrootableTarget, ProcessExecutionError
+>>>>>>> curtin/kernel_crash_dumps.py
 
 ENABLEMENT_SCRIPT = "/usr/share/kdump-tools/kdump_set_default"
 
@@ -38,6 +42,7 @@ def detection_script_available(target: Path) -> bool:
 def manual_enable(target: Path) -> None:
     """Manually enable kernel crash dumps with kdump-tools on target."""
     ensure_kdump_installed(target)
+<<<<<<< curtin/kernel_crash_dumps.py
     if detection_script_available(target):
         with ChrootableTarget(str(target)) as in_target:
             in_target.subp([ENABLEMENT_SCRIPT, "true"])
@@ -52,6 +57,17 @@ def manual_enable(target: Path) -> None:
                 "kernel-crash-dumps enablement requested but enablement "
                 "script not found. Not running."
             ),
+=======
+    try:
+        with ChrootableTarget(str(target)) as in_target:
+            in_target.subp([ENABLEMENT_SCRIPT, "true"])
+    except ProcessExecutionError as exc:
+        # Likely the enablement script hasn't been SRU'd
+        # Let's not block the install on this.
+        LOG.warning(
+            "Unable to run kernel-crash-dumps enablement script: %s",
+            exc,
+>>>>>>> curtin/kernel_crash_dumps.py
         )
 
 
diff --git a/curtin/paths.py b/curtin/paths.py
index 064b060..dee4fa3 100644
--- a/curtin/paths.py
+++ b/curtin/paths.py
@@ -1,5 +1,8 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
+import glob
 import os
+from packaging import version
+import re
 
 try:
     string_types = (basestring,)
@@ -31,4 +34,38 @@ def target_path(target, path=None):
 
     return os.path.join(target, path)
 
+
+def kernel_parse(fname):
+    """Function to extract version to use for sorting"""
+    m = re.search(r".*/vmlinu.-([\d.-]+)-.*", fname)
+    if not m:
+        raise ValueError("Cannot extract version from '%s'" % fname)
+    return version.parse(m.group(1))
+
+
+def get_kernel_list(target, full_initrd_path=True):
+    """yields [kernel filename, initrd path, version] for each kernel in target
+
+    For example:
+       ('vmlinuz-6.8.0-48-generic', '/boot/initrd.img-6.8.0-48-generic',
+        '6.8.0-48-generic')
+
+    If full_initrd_path is False, then only the basename of initrd is returned
+    """
+    root_path = target_path(target)
+    boot = target_path(root_path, 'boot')
+
+    for kernel in sorted(glob.glob(boot + '/vmlinu*-*'), key=kernel_parse,
+                         reverse=True):
+        kfile = os.path.basename(kernel)
+
+        # handle vmlinux or vmlinuz
+        kprefix = kfile.split('-')[0]
+        vers = kfile.replace(kprefix + '-', '')
+        initrd = kernel.replace(kprefix, 'initrd.img')
+        if not full_initrd_path:
+            initrd = os.path.basename(initrd)
+        yield kfile, initrd, vers
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/util.py b/curtin/util.py
index a10f1bf..a452ede 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -1421,6 +1421,7 @@ def not_exclusive_retry(fun, *args, **kwargs):
     time.sleep(1)
     return fun(*args, **kwargs)
 
+<<<<<<< curtin/util.py
 
 class FlockEx:
     """Acquire an exclusive lock on device.
@@ -1458,4 +1459,6 @@ class FlockEx:
         with suppress(Exception):
             os.close(self.lock_fd)
 
+=======
+>>>>>>> curtin/util.py
 # vi: ts=4 expandtab syntax=python
diff --git a/debian/control b/debian/control
index b430364..d445b07 100644
--- a/debian/control
+++ b/debian/control
@@ -13,6 +13,10 @@ Build-Depends: debhelper (>= 7),
                python3-jsonschema,
                python3-nose,
                python3-oauthlib,
+<<<<<<< debian/control
+=======
+               python3-packaging,
+>>>>>>> debian/control
                python3-parameterized,
                python3-setuptools,
                python3-yaml
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index 4fbcc5a..ce21673 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -10,10 +10,14 @@ Configuration options
 ---------------------
 Curtin's top level config keys are as follows:
 
+.. contents::
+    :depth: 1
+    :local:
 
 - apt_mirrors (``apt_mirrors``)
 - apt_proxy (``apt_proxy``)
 - block-meta (``block``)
+- boot (``boot``)
 - curthooks (``curthooks``)
 - debconf_selections (``debconf_selections``)
 - disable_overlayroot (``disable_overlayroot``)
@@ -112,81 +116,65 @@ Specify the filesystem label on the boot partition.
           label: my-boot-partition
 
 
-curthooks
-~~~~~~~~~
-Configure how Curtin determines what :ref:`curthooks` to run during the installation
-process.
-
-**mode**: *<['auto', 'builtin', 'target']>*
-
-The default mode is ``auto``.
-
-In ``auto`` mode, curtin will execute curthooks within the image if present.
-For images without curthooks inside, curtin will execute its built-in hooks.
-
-Currently the built-in curthooks support the following OS families:
-
-- Ubuntu
-- Centos
-
-When specifying ``builtin``, curtin will only run the curthooks present in
-Curtin ignoring any curthooks that may be present in the target operating
-system.
-
-When specifying ``target``, curtin will attempt run the curthooks in the target
-operating system.  If the target does NOT contain any curthooks, then the
-built-in curthooks will be run instead.
-
-Any errors during execution of curthooks (built-in or target) will fail the
-installation.
-
-**Example**::
+boot
+~~~~
 
-  # ignore any target curthooks
-  curthooks:
-    mode: builtin
+Configures which bootloader(s) Curtin installs and some associated options.
+This is a list, which can contain up to two options: `grub` and `extlinux`.
 
-  # Only run target curthooks, fall back to built-in
-  curthooks:
-    mode: target
+Two bootloaders are available:
 
+- `GRUB <https://www.gnu.org/software/grub/>`_ (GRand Unified Bootloader)
+  installs itself on one or more block devices and takes care of booting.
+  Typically grub is built as an EFI application. Curtin controls aspects of
+  grub's configuration-file (/boot/grub/grub.cfg) which tells grub which OS
+  options to present to the user.
 
-debconf_selections
-~~~~~~~~~~~~~~~~~~
-Curtin will update the target with debconf set-selection values.  Users will
-need to be familiar with the package debconf options.  Users can probe a
-packages' debconf settings by using ``debconf-get-selections``.
+- `extlinux <https://wiki.syslinux.org/wiki/index.php?title=EXTLINUX>`_
+  is really just a file format, similar to a grub configuration-file but much
+  less flexible. It specifies which OS options to present to the user.
 
-**selection_name**: *<debconf-set-selections input>*
+One or both can be installed.
 
-``debconf-set-selections`` is in the form::
+The following properties are used for both bootloaders:
 
-  <packagename> <packagename/option-name> <type> <value>
+**bootloaders**: *<list of bootloaders>*
 
-**Example**::
+Selects the bootloaders to use. Valid options are "grub" and "extlinux".
 
-  debconf_selections:
-    set1: |
-      cloud-init cloud-init/datasources multiselect MAAS
-      lxd lxd/bridge-name string lxdbr0
-    set2: lxd lxd/setup-bridge boolean true
+**replace_linux_default**: *<boolean: default True>*
 
+Controls whether grub-install will update the Linux Default target
+value during installation.
 
+**terminal**: *<['unmodified', 'console', ...]>*
 
-disable_overlayroot
-~~~~~~~~~~~~~~~~~~~
-Curtin disables overlayroot in the target by default.
+For grub, this configures the target-system grub-option GRUB_TERMINAL
+``terminal`` value which is written to
+/etc/default/grub.d/50-curtin-settings.cfg.  Curtin does not attempt to validate
+this string, grub2 has many values that it accepts and the list is platform
+dependent.
 
-**disable_overlayroot**: *<boolean: default True>*
+For extlinux, this puts the console string in an APPEND line for each OS.
 
-**Example**::
+If ``terminal`` is not provided, Curtin will set the value to 'console'.  If the
+``terminal`` value is 'unmodified' then Curtin will not set any value at all and
+will use Grub defaults.
 
-  disable_overlayroot: False
+extlinux
+""""""""
 
+Curtin can add an ``extlinux.conf`` file to a filesystem. This contains a list
+of possible kernels, etc. similar to grub. This is somewhat more flexible on
+ARM/RISC-V since it can use `FIT <https://fitspec.osfw.foundation/>`_ and deal
+with devicetree, verified boot, etc. automatically. It also avoids specifying
+which bootloader must be used, since extlinux is supported by U-Boot, for
+example.
 
 grub
-~~~~
-Curtin configures grub as the target machine's boot loader.  Users
+""""
+
+Curtin can configure grub as the target machine's grub boot loader.  Users
 can control a few options to tailor how the system will boot after
 installation.
 
@@ -194,11 +182,6 @@ installation.
 
 Specify a list of devices onto which grub will attempt to install.
 
-**replace_linux_default**: *<boolean: default True>*
-
-Controls whether grub-install will update the Linux Default target
-value during installation.
-
 **update_nvram**: *<boolean: default True>*
 
 Certain platforms, like ``uefi`` and ``prep`` systems utilize
@@ -217,16 +200,6 @@ When False, curtin writes "GRUB_DISABLE_OS_PROBER=true" to target system in
 /etc/default/grub.d/50-curtin-settings.cfg.  If True, curtin won't modify the
 grub configuration value in the target system.
 
-**terminal**: *<['unmodified', 'console', ...]>*
-
-Configure target system grub option GRUB_TERMINAL ``terminal`` value
-which is written to /etc/default/grub.d/50-curtin-settings.cfg.  Curtin
-does not attempt to validate this string, grub2 has many values that
-it accepts and the list is platform dependent.  If ``terminal`` is
-not provided, Curtin will set the value to 'console'.  If the ``terminal``
-value is 'unmodified' then Curtin will not set any value at all and will
-use Grub defaults.
-
 **reorder_uefi**: *<boolean: default True>*
 
 Curtin is typically used with MAAS where the systems are configured to boot
@@ -266,7 +239,9 @@ This setting is ignored if *update_nvram* is False.
 
 **Example**::
 
-  grub:
+  boot:
+     bootloaders:
+        - grub
      install_devices:
        - /dev/sda1
      replace_linux_default: False
@@ -276,38 +251,138 @@ This setting is ignored if *update_nvram* is False.
 
 **Default terminal value, GRUB_TERMINAL=console**::
 
-  grub:
+  boot:
+     bootloaders:
+        - grub
      install_devices:
        - /dev/sda1
 
 **Don't set GRUB_TERMINAL in target**::
 
-  grub:
+  boot:
+     bootloaders:
+        - grub
      install_devices:
        - /dev/sda1
      terminal: unmodified
 
 **Allow grub to probe for additional OSes**::
 
-  grub:
-    install_devices:
-      - /dev/sda1
+  boot:
+     bootloaders:
+        - grub
+     install_devices:
+        - /dev/sda1
      probe_additional_os: True
 
 **Avoid writting any settings to etc/default/grub.d/50-curtin-settings.cfg**::
 
-  grub:
-    install_devices:
-      - /dev/sda1
+  boot:
+     bootloaders:
+        - grub
+     install_devices:
+        - /dev/sda1
      probe_additional_os: True
      terminal: unmodified
 
 **Enable Fallback UEFI Reordering**::
 
-  grub:
+  boot:
+     bootloaders:
+        - grub
      reorder_uefi: true
      reorder_uefi_force_fallback: true
 
+extlinux
+""""""""
+
+There are no options specific to extlinux.
+
+**Example**::
+
+  boot:
+     bootloaders:
+        - grub
+        - extlinux
+     install_devices:
+        - /dev/sda1
+
+curthooks
+~~~~~~~~~
+Configure how Curtin determines what :ref:`curthooks` to run during the installation
+process.
+
+**mode**: *<['auto', 'builtin', 'target']>*
+
+The default mode is ``auto``.
+
+In ``auto`` mode, curtin will execute curthooks within the image if present.
+For images without curthooks inside, curtin will execute its built-in hooks.
+
+Currently the built-in curthooks support the following OS families:
+
+- Ubuntu
+- Centos
+
+When specifying ``builtin``, curtin will only run the curthooks present in
+Curtin ignoring any curthooks that may be present in the target operating
+system.
+
+When specifying ``target``, curtin will attempt run the curthooks in the target
+operating system.  If the target does NOT contain any curthooks, then the
+built-in curthooks will be run instead.
+
+Any errors during execution of curthooks (built-in or target) will fail the
+installation.
+
+**Example**::
+
+  # ignore any target curthooks
+  curthooks:
+    mode: builtin
+
+  # Only run target curthooks, fall back to built-in
+  curthooks:
+    mode: target
+
+
+debconf_selections
+~~~~~~~~~~~~~~~~~~
+Curtin will update the target with debconf set-selection values.  Users will
+need to be familiar with the package debconf options.  Users can probe a
+packages' debconf settings by using ``debconf-get-selections``.
+
+**selection_name**: *<debconf-set-selections input>*
+
+``debconf-set-selections`` is in the form::
+
+  <packagename> <packagename/option-name> <type> <value>
+
+**Example**::
+
+  debconf_selections:
+    set1: |
+      cloud-init cloud-init/datasources multiselect MAAS
+      lxd lxd/bridge-name string lxdbr0
+    set2: lxd lxd/setup-bridge boolean true
+
+
+
+disable_overlayroot
+~~~~~~~~~~~~~~~~~~~
+Curtin disables overlayroot in the target by default.
+
+**disable_overlayroot**: *<boolean: default True>*
+
+**Example**::
+
+  disable_overlayroot: False
+
+grub
+~~~~
+
+This is an alias for **boot** with *bootloader* set to "grub". It is provided
+to maintain backwards compatibility
 
 http_proxy
 ~~~~~~~~~~
@@ -470,11 +545,14 @@ directive may be ignored.
     package: linux-image-generic-hwe-24.04
     remove: existing
 
+<<<<<<< doc/topics/config.rst
   # install hwe kernel, remove generic kernel
   kernel:
     package: linux-image-generic-hwe-24.04
     remove: ["linux-generic"]
 
+=======
+>>>>>>> doc/topics/config.rst
 kernel-crash-dumps
 ~~~~~~~~~~~~~~~~~~
 Configure how Curtin will configure kernel crash dumps in the target system
diff --git a/examples/tests/no-grub-file.yaml b/examples/tests/no-grub-file.yaml
index d5ba698..240dbb9 100644
--- a/examples/tests/no-grub-file.yaml
+++ b/examples/tests/no-grub-file.yaml
@@ -4,6 +4,7 @@ placeholder_simple_install: unused
 
 # configure curtin so it does not emit a grub config file
 # in etc/default/grub/grub.d
-grub:
+boot:
+   bootloaders: ['grub']
    probe_additional_os: true
    terminal: unmodified
diff --git a/test-requirements.txt b/test-requirements.txt
index fd23867..f8a7eef 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,7 @@
 coverage
 flake8
 jsonschema
+<<<<<<< test-requirements.txt
 parameterized
 pip
 pyflakes
@@ -8,3 +9,11 @@ pytest
 virtualenv
 wheel
 git+https://salsa.debian.org/apt-team/python-apt@2.4.y
+=======
+mock
+nose
+pip
+pyflakes
+virtualenv
+wheel
+>>>>>>> test-requirements.txt
diff --git a/tests/integration/test_block_meta.py b/tests/integration/test_block_meta.py
index c8113b3..6d64cd4 100644
--- a/tests/integration/test_block_meta.py
+++ b/tests/integration/test_block_meta.py
@@ -1,19 +1,28 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
-import dataclasses
-from dataclasses import dataclass
 import contextlib
 import json
 import os
+<<<<<<< tests/integration/test_block_meta.py
 from parameterized import parameterized
 from pathlib import Path
+=======
+>>>>>>> tests/integration/test_block_meta.py
 import re
 import stat
 import sys
+<<<<<<< tests/integration/test_block_meta.py
 from typing import Optional
 from unittest import skipIf
 import yaml
 
+=======
+from unittest import skipIf
+import yaml
+
+import attr
+
+>>>>>>> tests/integration/test_block_meta.py
 from curtin import block, compat, distro, log, udev, util
 from curtin.commands.block_meta import _get_volume_fstype
 from curtin.commands.block_meta_v2 import ONE_MIB_BYTES
@@ -40,22 +49,25 @@ def loop_dev(image, sector_size=512):
         util.subp(['losetup', '--detach', dev])
 
 
-@dataclass(order=True)
+@attr.s(init=True, cmp=False)
 class PartData:
-    number: Optional[int] = None
-    offset: Optional[int] = None
-    size: Optional[int] = None
-    boot: Optional[bool] = None
-    partition_type: Optional[str] = None
+    number = attr.ib(default=None)
+    offset = attr.ib(default=None)
+    size = attr.ib(default=None)
+    boot = attr.ib(default=None)
+    partition_type = attr.ib(default=None)
 
     # test cases may initialize the values they care about
     # test utilities shall initialize all fields
     def assertFieldsAreNotNone(self):
-        for field in dataclasses.fields(self):
+        for field in attr.fields(self.__class__):
             assert getattr(self, field.name) is not None
 
+    def __lt__(self, other):
+        return self.number < other.number
+
     def __eq__(self, other):
-        for field in dataclasses.fields(self):
+        for field in attr.fields(self.__class__):
             myval = getattr(self, field.name)
             otherval = getattr(other, field.name)
             if myval is not None and otherval is not None \
@@ -66,7 +78,7 @@ class PartData:
 
 def _get_ext_size(dev, part_action):
     num = part_action['number']
-    cmd = ['dumpe2fs', '-h', f'{dev}p{num}']
+    cmd = ['dumpe2fs', '-h', '{}p{}'.format(dev, num)]
     out = util.subp(cmd, capture=True)[0]
     for line in out.splitlines():
         if line.startswith('Block count'):
@@ -81,7 +93,7 @@ def _get_ntfs_size(dev, part_action):
     cmd = ['ntfsresize',
            '--no-action',
            '--force',  # needed post-resize, which otherwise demands a CHKDSK
-           '--info', f'{dev}p{num}']
+           '--info', '{}p{}'.format(dev, num)]
     out = util.subp(cmd, capture=True)[0]
     # Sample input:
     # Current volume size: 41939456 bytes (42 MB)
@@ -103,7 +115,7 @@ _get_fs_sizers = {
 
 def _get_filesystem_size(dev, part_action, fstype='ext4'):
     if fstype not in _get_fs_sizers.keys():
-        raise Exception(f'_get_filesystem_size: no support for {fstype}')
+        raise Exception('_get_filesystem_size: no support for %s' % fstype)
     return _get_fs_sizers[fstype](dev, part_action)
 
 
@@ -126,7 +138,7 @@ def summarize_partitions(dev):
         (unused, s_number, s_offset, s_size) = [
                 entry for entry in sysfs_data
                 if '/dev/' + entry[0] == node][0]
-        assert node.startswith(f'{dev}p')
+        assert node.startswith(dev + 'p')
         number = int(node[len(dev) + 1:])
         ptype = part['type']
         offset = part['start'] * sectorsize
@@ -217,13 +229,13 @@ class TestBlockMeta(IntegrationTestCase):
     def mount(self, dev, partition_cfg):
         mnt_point = self.tmp_dir()
         num = partition_cfg['number']
-        with util.mount(f'{dev}p{num}', mnt_point):
+        with util.mount('{}p{}'.format(dev, num), mnt_point):
             yield mnt_point
 
     @contextlib.contextmanager
     def open_file_on_part(self, dev, part_action, mode):
         with self.mount(dev, part_action) as mnt_point:
-            with open(f'{mnt_point}/data.txt', mode) as fp:
+            with open(mnt_point + '/data.txt', mode) as fp:
                 yield fp
 
     def create_data(self, dev, part_action):
@@ -243,7 +255,7 @@ class TestBlockMeta(IntegrationTestCase):
             tolerance = 512 * 10
         actual_fssize = _get_filesystem_size(dev, part_action, fstype)
         diff = expected - actual_fssize
-        self.assertTrue(0 <= diff <= tolerance, f'difference of {diff}')
+        self.assertTrue(0 <= diff <= tolerance, 'difference of ' + str(diff))
 
     def run_bm(self, config, *args, **kwargs):
         config_path = self.tmp_path('config.yaml')
@@ -615,7 +627,7 @@ class TestBlockMeta(IntegrationTestCase):
             }
             with loop_dev(img) as dev:
                 try:
-                    self.run_bm(curtin_cfg, f'--devices={dev}', env=cmd_env)
+                    self.run_bm(curtin_cfg, '--devices=' + dev, env=cmd_env)
                 finally:
                     util.subp(['umount', mnt_point])
                     udev.udevadm_settle()
@@ -635,7 +647,7 @@ class TestBlockMeta(IntegrationTestCase):
                              fstype=fstype)
         self.run_bm(config.render())
         with loop_dev(img) as dev:
-            self.assertEqual(fstype, _get_volume_fstype(f'{dev}p1'))
+            self.assertEqual(fstype, _get_volume_fstype(dev + 'p1'))
             self.create_data(dev, p1)
             self.assertEqual(
                 summarize_partitions(dev), [
@@ -664,7 +676,11 @@ class TestBlockMeta(IntegrationTestCase):
             p1['size'] = size
             self.run_bm(config.render())
             with loop_dev(img) as dev:
+<<<<<<< tests/integration/test_block_meta.py
                 self.assertEqual('ntfs', _get_volume_fstype(f'{dev}p1'))
+=======
+                self.assertEqual('ntfs', _get_volume_fstype(dev + 'p1'))
+>>>>>>> tests/integration/test_block_meta.py
                 self.create_data(dev, p1)
                 self.assertEqual(
                     summarize_partitions(dev), [
@@ -972,11 +988,11 @@ class TestBlockMeta(IntegrationTestCase):
             with self.mount(dev, p1) as mnt_point:
                 # Attempt to create files across the partition with gaps
                 for i in range(1, 41):
-                    with open(f'{mnt_point}/{str(i)}', 'wb') as fp:
+                    with open('{}/{}'.format(mnt_point, i), 'wb') as fp:
                         fp.write(bytes([i]) * (2 << 20))
                 for i in range(1, 41):
                     if i % 5 != 0:
-                        os.remove(f'{mnt_point}/{str(i)}')
+                        os.remove('{}/{}'.format(mnt_point, i))
 
         config = StorageConfigBuilder(version=2)
         config.add_image(path=img, size='100M', ptable='gpt')
@@ -994,7 +1010,7 @@ class TestBlockMeta(IntegrationTestCase):
                 ])
             with self.mount(dev, p1) as mnt_point:
                 for i in range(5, 41, 5):
-                    with open(f'{mnt_point}/{i}', 'rb') as fp:
+                    with open('{}/{}'.format(mnt_point, i), 'rb') as fp:
                         self.assertEqual(bytes([i]) * (2 << 20), fp.read())
 
     def test_parttype_dos(self):
@@ -1056,8 +1072,7 @@ class TestBlockMeta(IntegrationTestCase):
             PartData(number=4, offset=80 << 20, size=19 << 20,
                      partition_type=winre))
 
-    @parameterized.expand([('msdos',), ('gpt',)])
-    def test_disk_label_id_persistent(self, ptable):
+    def _test_disk_label_id_persistent(self, ptable):
         # when the disk is preserved, the disk label id shall also be preserved
         self.img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=2)
@@ -1076,6 +1091,12 @@ class TestBlockMeta(IntegrationTestCase):
         with loop_dev(self.img) as dev:
             self.assertEqual(orig_label_id, _get_disk_label_id(dev))
 
+    def test_disk_label_id_persistent_msdos(self):
+        self._test_disk_label_id_persistent('msdos')
+
+    def test_disk_label_id_persistent_gpt(self):
+        self._test_disk_label_id_persistent('gpt')
+
     def test_gpt_uuid_persistent(self):
         # A persistent partition with an unspecified uuid shall keep the uuid
         self.img = self.tmp_path('image.img')
@@ -1112,12 +1133,16 @@ class TestBlockMeta(IntegrationTestCase):
             actual_name = sfdisk_info['partitions'][0]['name']
         self.assertEqual(name, actual_name)
 
+<<<<<<< tests/integration/test_block_meta.py
     @parameterized.expand([
         ('random', CiTestCase.random_string(),),
         # "écrasé" means "overwritten"
         ('unicode', "'name' must not be écrasé/덮어쓴!"),
     ])
     def test_gpt_name_persistent(self, title, name):
+=======
+    def _test_gpt_name_persistent(self, title, name):
+>>>>>>> tests/integration/test_block_meta.py
         self.img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=2)
         config.add_image(path=self.img, size='20M', ptable='gpt')
@@ -1140,6 +1165,9 @@ class TestBlockMeta(IntegrationTestCase):
             actual_name = sfdisk_info['partitions'][0]['name']
         self.assertEqual(name, actual_name)
 
+    def test_gpt_name_persistent_random(self):
+        self._test_gpt_name_persistent('random', CiTestCase.random_string())
+
     def test_gpt_set_single_attr(self):
         self.img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=2)
@@ -1284,8 +1312,7 @@ table-length: 256'''.encode()
         self.assertPartitions(
             PartData(number=1, offset=1 << 20, size=1 << 20))
 
-    @parameterized.expand(((1,), (2,)))
-    def test_swap(self, sv):
+    def _test_swap(self, sv):
         self.img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=sv)
         config.add_image(path=self.img, create=True, size='20M',
@@ -1297,6 +1324,7 @@ table-length: 256'''.encode()
             PartData(number=1, offset=1 << 20, size=1 << 20, boot=False,
                      partition_type='82'))
 
+<<<<<<< tests/integration/test_block_meta.py
     @parameterized.expand(((1,), (2,)))
     def test_cryptoswap(self, sv=2):
         self.img = self.tmp_path('image.img')
@@ -1399,3 +1427,10 @@ table-length: 256'''.encode()
         for run in range(5):
             self.run_bm(config.render())
             self.assertPartitions(*parts)
+=======
+    def test_swap_sv1(self):
+        self._test_swap(1)
+
+    def test_swap_sv2(self):
+        self._test_swap(2)
+>>>>>>> tests/integration/test_block_meta.py
diff --git a/tests/integration/webserv.py b/tests/integration/webserv.py
index f4ce4e4..de30e04 100644
--- a/tests/integration/webserv.py
+++ b/tests/integration/webserv.py
@@ -1,8 +1,10 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
-import threading
-import socketserver
 from http.server import SimpleHTTPRequestHandler
+import socketserver
+import threading
+import os
+
 from tests.vmtests.image_sync import IMAGE_DIR
 
 
@@ -12,7 +14,17 @@ class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
 
 class ImageHTTPRequestHandler(SimpleHTTPRequestHandler):
     def __init__(self, *args, **kwargs):
-        super().__init__(*args, directory=IMAGE_DIR, **kwargs)
+        try:
+            super().__init__(*args, directory=IMAGE_DIR, **kwargs)
+        except TypeError:
+            # SimpleHTTPRequestHandler in python < 3.7 doesn't take a directory
+            # arg, fake it.
+            curdir = os.getcwd()
+            os.chdir(IMAGE_DIR)
+            try:
+                super().__init__(*args, **kwargs)
+            finally:
+                os.chdir(curdir)
 
 
 class ImageServer:
@@ -50,4 +62,4 @@ class ImageServer:
         if self.server is not None:
             ip, port = self.server.server_address
 
-        return f"http://{ip}:{port}";
+        return "http://{}:{}".format(ip, port)
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index a5ccac1..c78db59 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -3448,10 +3448,10 @@ label: gpt
         table = block_meta_v2.GPTPartTable(512)
         table.add(dict(number=1, offset=1 << 20, size=9 << 20,
                        flag='boot', partition_type=ptype))
-        expected = f'''\
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={ptype}'''
+1:  start=2048 size=18432 type={}'''.format(ptype)
         self.assertEqual(expected, table.render())
 
     def test_gpt_name(self):
@@ -3460,11 +3460,18 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        partition_name=name))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
+<<<<<<< tests/unittests/test_commands_block_meta.py
         to_hex = block_meta_v2.to_utf8_hex_notation
         expected = f'''\
 label: gpt
 
 1:  start=2048 size=18432 type={type_id} name="{to_hex(name)}"'''
+=======
+        expected = '''\
+label: gpt
+
+1:  start=2048 size=18432 type={} name="{}"'''.format(type_id, name)
+>>>>>>> tests/unittests/test_commands_block_meta.py
         self.assertEqual(expected, table.render())
 
     def test_gpt_name_free_text(self):
@@ -3478,10 +3485,14 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        partition_name=name))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        expected = '''\
 label: gpt
 
+<<<<<<< tests/unittests/test_commands_block_meta.py
 1:  start=2048 size=18432 type={type_id} name="{expected_name}"'''
+=======
+1:  start=2048 size=18432 type={} name="{}"'''.format(type_id, name)
+>>>>>>> tests/unittests/test_commands_block_meta.py
         self.assertEqual(expected, table.render())
 
     def test_gpt_attrs_none(self):
@@ -3489,10 +3500,10 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        attrs=None))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={type_id}'''
+1:  start=2048 size=18432 type={}'''.format(type_id)
         self.assertEqual(expected, table.render())
 
     def test_gpt_attrs_empty(self):
@@ -3500,10 +3511,10 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        attrs=[]))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={type_id}'''
+1:  start=2048 size=18432 type={}'''.format(type_id)
         self.assertEqual(expected, table.render())
 
     def test_gpt_attrs_required(self):
@@ -3511,10 +3522,10 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        attrs=['RequiredPartition']))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={type_id} attrs="RequiredPartition"'''
+1:  start=2048 size=18432 type={} attrs="RequiredPartition"'''.format(type_id)
         self.assertEqual(expected, table.render())
 
     def test_gpt_attrs_bit(self):
@@ -3522,10 +3533,10 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        attrs=['GUID:51']))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={type_id} attrs="GUID:51"'''
+1:  start=2048 size=18432 type={} attrs="GUID:51"'''.format(type_id)
         self.assertEqual(expected, table.render())
 
     def test_gpt_attrs_multi(self):
@@ -3533,10 +3544,11 @@ label: gpt
         table.add(dict(number=1, offset=1 << 20, size=9 << 20, flag='boot',
                        attrs=['RequiredPartition', 'GUID:51']))
         type_id = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
-        expected = f'''\
+        attrs = 'RequiredPartition GUID:51'
+        expected = '''\
 label: gpt
 
-1:  start=2048 size=18432 type={type_id} attrs="RequiredPartition GUID:51"'''
+1:  start=2048 size=18432 type={} attrs="{}"'''.format(type_id, attrs)
         self.assertEqual(expected, table.render())
 
     def test_dos_basic(self):
@@ -3560,10 +3572,10 @@ label: dos
         table = block_meta_v2.DOSPartTable(512)
         table.add(dict(number=1, offset=1 << 20, size=9 << 20,
                        flag='boot', partition_type=ptype))
-        expected = f'''\
+        expected = '''\
 label: dos
 
-1:  start=2048 size=18432 type={ptype} bootable'''
+1:  start=2048 size=18432 type={} bootable'''.format(ptype)
         self.assertEqual(expected, table.render())
 
     def test_preserve_labelid_gpt(self):
@@ -3659,21 +3671,26 @@ label: dos
                 number=1, start=2, size=3, type='04', bootable=True,
                 uuid=uuid, name='name',
                 attrs=['stuff', 'things'])
-        expected = f'1:  start=2 size=3 type=04 uuid={uuid} ' + \
+        expected = '1:  start=2 size=3 type=04 uuid={} '.format(uuid) + \
             'name="name" attrs="stuff things" bootable'
         self.assertEqual(expected, pte.render())
 
     def test_gpt_entry_preserve(self):
         uuid = str(random_uuid())
         name = self.random_string()
-        attrs = f'{self.random_string()} {self.random_string()}'
+        attrs = '{} {}'.format(self.random_string(), self.random_string())
         pte = block_meta_v2.PartTableEntry(
                 number=1, start=2, size=3, type='04', bootable=False,
                 uuid=None, name=None, attrs=None)
         pte.preserve({'uuid': uuid, 'name': name, 'attrs': attrs})
+<<<<<<< tests/unittests/test_commands_block_meta.py
         to_hex = block_meta_v2.to_utf8_hex_notation
         expected = f'1:  start=2 size=3 type=04 uuid={uuid} ' + \
             f'name="{to_hex(name)}" attrs="{attrs}"'
+=======
+        expected = '1:  start=2 size=3 type=04 uuid={} '.format(uuid) + \
+            'name="{}" attrs="{}"'.format(name, attrs)
+>>>>>>> tests/unittests/test_commands_block_meta.py
         self.assertEqual(expected, pte.render())
 
     def test_v2_dos_is_logical(self):
diff --git a/tests/unittests/test_commands_install_extlinux.py b/tests/unittests/test_commands_install_extlinux.py
new file mode 100644
index 0000000..788a301
--- /dev/null
+++ b/tests/unittests/test_commands_install_extlinux.py
@@ -0,0 +1,129 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+import os
+from pathlib import Path
+import tempfile
+
+from .helpers import CiTestCase
+
+from curtin import config
+from curtin import paths
+from curtin.commands import install_extlinux
+
+USE_EXTLINUX = {'bootloaders': ['extlinux']}
+
+EXPECT_HDR = '''\
+## /boot/extlinux/extlinux.conf
+##
+## IMPORTANT WARNING
+##
+## The configuration of this file is generated automatically.
+## Do not edit this file manually, use: u-boot-update
+
+default l0
+menu title U-Boot menu
+prompt 0
+timeout 50
+'''
+
+EXPECT_BODY = '''
+
+label l0
+\tmenu label Ubuntu 22.04.5 LTS 6.8.0-48-generic
+\tlinux /vmlinuz-6.8.0-48-generic
+\tinitrd /initrd.img-6.8.0-48-generic
+\tappend root=/dev/mapper/vgubuntu-root ro quiet
+
+label l0r
+\tmenu label Ubuntu 22.04.5 LTS 6.8.0-48-generic (rescue target)
+\tlinux /vmlinuz-6.8.0-48-generic
+\tinitrd /initrd.img-6.8.0-48-generic
+\tappend root=/dev/mapper/vgubuntu-root ro single
+
+
+label l1
+\tmenu label Ubuntu 22.04.5 LTS 6.8.0-40-generic
+\tlinux /vmlinuz-6.8.0-40-generic
+\tinitrd /initrd.img-6.8.0-40-generic
+\tappend root=/dev/mapper/vgubuntu-root ro quiet
+
+label l1r
+\tmenu label Ubuntu 22.04.5 LTS 6.8.0-40-generic (rescue target)
+\tlinux /vmlinuz-6.8.0-40-generic
+\tinitrd /initrd.img-6.8.0-40-generic
+\tappend root=/dev/mapper/vgubuntu-root ro single
+
+
+label l2
+\tmenu label Ubuntu 22.04.5 LTS 5.15.0-127-generic
+\tlinux /vmlinuz-5.15.0-127-generic
+\tinitrd /initrd.img-5.15.0-127-generic
+\tappend root=/dev/mapper/vgubuntu-root ro quiet
+
+label l2r
+\tmenu label Ubuntu 22.04.5 LTS 5.15.0-127-generic (rescue target)
+\tlinux /vmlinuz-5.15.0-127-generic
+\tinitrd /initrd.img-5.15.0-127-generic
+\tappend root=/dev/mapper/vgubuntu-root ro single
+'''
+
+
+class TestInstallExtlinux(CiTestCase):
+    def setUp(self):
+        self.tmpdir = tempfile.TemporaryDirectory(suffix='-curtin')
+        self.target = self.tmpdir.name
+
+        versions = ['6.8.0-40', '5.15.0-127', '6.8.0-48']
+        boot = os.path.join(self.target, 'boot')
+        Path(boot).mkdir()
+        os.system('ls %s' % boot)
+        for ver in versions:
+            Path('%s/config-%s-generic' % (boot, ver)).touch()
+            Path('%s/initrd.img-%s-generic' % (boot, ver)).touch()
+            Path('%s/vmlinuz-%s-generic' % (boot, ver)).touch()
+
+        Path('%s/empty-dir' % self.target).mkdir()
+        self.maxDiff = None
+
+    def test_get_kernel_list(self):
+        iter = paths.get_kernel_list(self.target, full_initrd_path=False)
+        self.assertEqual(
+            ('vmlinuz-6.8.0-48-generic', 'initrd.img-6.8.0-48-generic',
+             '6.8.0-48-generic'),
+            next(iter))
+        self.assertEqual(
+            ('vmlinuz-6.8.0-40-generic', 'initrd.img-6.8.0-40-generic',
+             '6.8.0-40-generic'),
+            next(iter))
+        self.assertEqual(
+            ('vmlinuz-5.15.0-127-generic', 'initrd.img-5.15.0-127-generic',
+             '5.15.0-127-generic'),
+            next(iter))
+        try:
+            val = next(iter)
+            raise ValueError('Extra value %s' % val)
+        except StopIteration:
+            pass
+
+    def test_empty(self):
+        out = install_extlinux.build_content('%s/empty-dir' % self.target,
+                                             USE_EXTLINUX)
+        self.assertEqual(out, EXPECT_HDR)
+
+    def test_normal(self):
+        out = install_extlinux.build_content(self.target, USE_EXTLINUX)
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY, out)
+
+    def test_no_recovery(self):
+        out = install_extlinux.build_content(self.target, USE_EXTLINUX)
+        self.assertEqual(EXPECT_HDR + EXPECT_BODY, out)
+
+    def test_install(self):
+        install_extlinux.install_extlinux(self.target, USE_EXTLINUX)
+        extlinux_path = self.target + '/boot/extlinux'
+        self.assertTrue(os.path.exists(extlinux_path))
+        extlinux_file = extlinux_path + '/extlinux.conf'
+        self.assertTrue(os.path.exists(extlinux_file))
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
index 7b7b547..a79df8a 100644
--- a/tests/unittests/test_commands_install_grub.py
+++ b/tests/unittests/test_commands_install_grub.py
@@ -10,6 +10,8 @@ from .helpers import CiTestCase
 from unittest import mock
 import os
 
+USE_GRUB = {'bootloaders': ['grub']}
+
 
 class TestGetGrubPackageName(CiTestCase):
 
@@ -457,7 +459,11 @@ class TestWriteGrubConfig(CiTestCase):
                 self.assertEqual(expected, found)
 
     def test_write_grub_config_defaults(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig()
+=======
+        bootcfg = USE_GRUB
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([
              'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
@@ -469,12 +475,17 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_TERMINAL="console"'])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
     def test_write_grub_config_no_replace(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(replace_linux_default=False)
+=======
+        bootcfg = {'replace_linux_default': False}
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([])
         expected_curtin = "\n".join([
@@ -485,12 +496,17 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_TERMINAL="console"'])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
     def test_write_grub_config_disable_probe(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(probe_additional_os=False)
+=======
+        bootcfg = {'probe_additional_os': False}  # DISABLE_OS_PROBER=1
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([
              'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
@@ -502,12 +518,17 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_TERMINAL="console"'])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
     def test_write_grub_config_enable_probe(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(probe_additional_os=True)
+=======
+        bootcfg = {'probe_additional_os': True}  # DISABLE_OS_PROBER=0, default
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([
              'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
@@ -516,23 +537,36 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_TERMINAL="console"'])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
     def test_write_grub_config_no_grub_settings_file(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(
             probe_additional_os=True,
             terminal='unmodified')
+=======
+        bootcfg = {
+            'probe_additional_os': True,
+            'terminal': 'unmodified',
+        }
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = []
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
         self.assertTrue(os.path.exists(self.target_grubdefault))
         self.assertFalse(os.path.exists(self.target_grubconf))
 
     def test_write_grub_config_specify_terminal(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(
             terminal='serial')
+=======
+        bootcfg = {'terminal': 'serial'}
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([
              'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
@@ -544,12 +578,17 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_TERMINAL="serial"'])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
     def test_write_grub_config_terminal_unmodified(self):
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(terminal='unmodified')
+=======
+        bootcfg = {'terminal': 'unmodified'}
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         new_params = ['foo=bar', 'wark=1']
         expected_default = "\n".join([
              'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
@@ -559,10 +598,20 @@ class TestWriteGrubConfig(CiTestCase):
              'GRUB_DISABLE_OS_PROBER="true"', ''])
 
         install_grub.write_grub_config(
-            self.target, grubcfg, self.grubconf, new_params)
+            self.target, bootcfg, self.grubconf, new_params)
 
         self._verify_expected(expected_default, expected_curtin)
 
+<<<<<<< tests/unittests/test_commands_install_grub.py
+=======
+    def test_write_grub_config_invalid_terminal(self):
+        bootcfg = {'terminal': ['color-tv']}
+        new_params = ['foo=bar', 'wark=1']
+        with self.assertRaises(ValueError):
+            install_grub.write_grub_config(
+                self.target, bootcfg, self.grubconf, new_params)
+
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
 class TestFindEfiLoader(CiTestCase):
 
@@ -1637,43 +1686,67 @@ class TestInstallGrub(CiTestCase):
     def test_grub_install_raise_exception_on_no_devices(self):
         devices = []
         with self.assertRaises(ValueError):
+<<<<<<< tests/unittests/test_commands_install_grub.py
             install_grub.install_grub(
                 devices, self.target, uefi=False, grubcfg=config.GrubConfig())
+=======
+            install_grub.install_grub(devices, self.target, False, USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
     def test_grub_install_raise_exception_on_no_target(self):
         devices = ['foobar']
         with self.assertRaises(ValueError):
+<<<<<<< tests/unittests/test_commands_install_grub.py
             install_grub.install_grub(
                 devices, None, uefi=False, grubcfg=config.GrubConfig())
+=======
+            install_grub.install_grub(devices, None, False, USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
     def test_grub_install_raise_exception_on_s390x(self):
         self.m_distro_get_architecture.return_value = 's390x'
         self.m_platform_machine.return_value = 's390x'
         devices = ['foobar']
         with self.assertRaises(RuntimeError):
+<<<<<<< tests/unittests/test_commands_install_grub.py
             install_grub.install_grub(
                 devices, self.target, uefi=False, grubcfg=config.GrubConfig())
+=======
+            install_grub.install_grub(devices, self.target, False, USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
     def test_grub_install_raise_exception_on_armv7(self):
         self.m_distro_get_architecture.return_value = 'armhf'
         self.m_platform_machine.return_value = 'armv7l'
         devices = ['foobar']
         with self.assertRaises(RuntimeError):
+<<<<<<< tests/unittests/test_commands_install_grub.py
             install_grub.install_grub(
                 devices, self.target, uefi=False, grubcfg=config.GrubConfig())
+=======
+            install_grub.install_grub(devices, self.target, False, USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
     def test_grub_install_raise_exception_on_arm64_no_uefi(self):
         self.m_distro_get_architecture.return_value = 'arm64'
         self.m_platform_machine.return_value = 'aarch64'
         devices = ['foobar']
         with self.assertRaises(RuntimeError):
+<<<<<<< tests/unittests/test_commands_install_grub.py
             install_grub.install_grub(
                 devices, self.target, uefi=False, grubcfg=config.GrubConfig())
+=======
+            install_grub.install_grub(devices, self.target, uefi, USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
     def test_grub_install_ubuntu(self):
         devices = ['/dev/disk-a-part1']
         uefi = False
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig()
+=======
+        bootcfg = USE_GRUB
+>>>>>>> tests/unittests/test_commands_install_grub.py
         grub_conf = self.tmp_path('grubconf')
         new_params = []
         self.m_get_grub_package_name.return_value = ('grub-pc', 'i386-pc')
@@ -1683,8 +1756,12 @@ class TestInstallGrub(CiTestCase):
         self.m_gen_install_commands.return_value = (
             [['/bin/true']], [['/bin/false']])
 
+<<<<<<< tests/unittests/test_commands_install_grub.py
         install_grub.install_grub(
             devices, self.target, uefi=uefi, grubcfg=grubcfg)
+=======
+        install_grub.install_grub(devices, self.target, uefi, bootcfg)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
         self.m_distro_get_distroinfo.assert_called_with(target=self.target)
         self.m_distro_get_architecture.assert_called_with(target=self.target)
@@ -1695,7 +1772,7 @@ class TestInstallGrub(CiTestCase):
                                                        self.distroinfo.family)
         self.m_get_carryover_params.assert_called_with(self.distroinfo)
         self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
-        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+        self.m_write_grub_config.assert_called_with(self.target, bootcfg,
                                                     grub_conf, new_params)
         self.m_get_grub_install_command.assert_called_with(
             uefi, self.distroinfo, self.target)
@@ -1712,7 +1789,13 @@ class TestInstallGrub(CiTestCase):
     def test_uefi_grub_install_ubuntu(self):
         devices = ['/dev/disk-a-part1']
         uefi = True
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(update_nvram=True)
+=======
+        update_nvram = True
+        bootcfg = {'update_nvram': update_nvram}
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         grub_conf = self.tmp_path('grubconf')
         new_params = []
         grub_name = 'grub-efi-amd64'
@@ -1725,8 +1808,12 @@ class TestInstallGrub(CiTestCase):
         self.m_gen_uefi_install_commands.return_value = (
             [['/bin/true']], [['/bin/false']])
 
+<<<<<<< tests/unittests/test_commands_install_grub.py
         install_grub.install_grub(
             devices, self.target, uefi=uefi, grubcfg=grubcfg)
+=======
+        install_grub.install_grub(devices, self.target, uefi, bootcfg)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
         self.m_distro_get_distroinfo.assert_called_with(target=self.target)
         self.m_distro_get_architecture.assert_called_with(target=self.target)
@@ -1737,7 +1824,7 @@ class TestInstallGrub(CiTestCase):
                                                        self.distroinfo.family)
         self.m_get_carryover_params.assert_called_with(self.distroinfo)
         self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
-        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+        self.m_write_grub_config.assert_called_with(self.target, bootcfg,
                                                     grub_conf, new_params)
         self.m_get_grub_install_command.assert_called_with(
             uefi, self.distroinfo, self.target)
@@ -1755,7 +1842,13 @@ class TestInstallGrub(CiTestCase):
     def test_uefi_grub_install_ubuntu_multiple_esp(self):
         devices = ['/dev/disk-a-part1']
         uefi = True
+<<<<<<< tests/unittests/test_commands_install_grub.py
         grubcfg = config.GrubConfig(update_nvram=True)
+=======
+        update_nvram = True
+        bootcfg = {'update_nvram': update_nvram}
+        bootcfg.update(USE_GRUB)
+>>>>>>> tests/unittests/test_commands_install_grub.py
         grub_conf = self.tmp_path('grubconf')
         new_params = []
         grub_name = 'grub-efi-amd64'
@@ -1768,8 +1861,12 @@ class TestInstallGrub(CiTestCase):
         self.m_gen_uefi_install_commands.return_value = (
             [['/bin/true']], [['/bin/false']])
 
+<<<<<<< tests/unittests/test_commands_install_grub.py
         install_grub.install_grub(
             devices, self.target, uefi=uefi, grubcfg=grubcfg)
+=======
+        install_grub.install_grub(devices, self.target, uefi, bootcfg)
+>>>>>>> tests/unittests/test_commands_install_grub.py
 
         self.m_distro_get_distroinfo.assert_called_with(target=self.target)
         self.m_distro_get_architecture.assert_called_with(target=self.target)
@@ -1780,7 +1877,7 @@ class TestInstallGrub(CiTestCase):
                                                        self.distroinfo.family)
         self.m_get_carryover_params.assert_called_with(self.distroinfo)
         self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
-        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+        self.m_write_grub_config.assert_called_with(self.target, bootcfg,
                                                     grub_conf, new_params)
         self.m_get_grub_install_command.assert_called_with(
             uefi, self.distroinfo, self.target)
diff --git a/tests/unittests/test_config.py b/tests/unittests/test_config.py
index 1db3191..4055ea5 100644
--- a/tests/unittests/test_config.py
+++ b/tests/unittests/test_config.py
@@ -143,6 +143,7 @@ def _replace_consts(cfgstr):
     return cfgstr
 
 
+<<<<<<< tests/unittests/test_config.py
 class TestDeserializer(CiTestCase):
 
     def test_scalar(self):
@@ -224,6 +225,40 @@ class TestDeserializer(CiTestCase):
         self.assertEqual(
             UnionClass(val=None),
             deserializer.deserialize(UnionClass, {"val": None}))
+=======
+class TestBootCfg(CiTestCase):
+    def test_empty(self):
+        with self.assertRaises(ValueError) as exc:
+            config.check_bootcfg({})
+        self.assertIn("missing required key: 'bootloaders'", str(exc.exception))
+
+    def test_not_list(self):
+        with self.assertRaises(ValueError) as exc:
+            config.check_bootcfg({'bootloaders': 'invalid'})
+        self.assertIn("bootloaders must be a list: invalid", str(exc.exception))
+
+    def test_empty_list(self):
+        with self.assertRaises(ValueError) as exc:
+            config.check_bootcfg({'bootloaders': []})
+        self.assertIn("Empty bootloaders list:", str(exc.exception))
+
+    def test_duplicate(self):
+        with self.assertRaises(ValueError) as exc:
+            config.check_bootcfg({'bootloaders': ['grub', 'grub']})
+        self.assertIn("bootloaders list contains duplicates: ['grub', 'grub']",
+                      str(exc.exception))
+
+    def test_invalid(self):
+        with self.assertRaises(ValueError) as exc:
+            config.check_bootcfg({'bootloaders': ['fred']})
+        self.assertIn("Unknown bootloader fred: ['fred']", str(exc.exception))
+
+    def test_valid(self):
+        config.check_bootcfg({'bootloaders': ['grub']})
+        config.check_bootcfg({'bootloaders': ['extlinux']})
+        config.check_bootcfg({'bootloaders': ['grub', 'extlinux']})
+        config.check_bootcfg({'bootloaders': ['extlinux', 'grub']})
+>>>>>>> tests/unittests/test_config.py
 
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index e76eb54..1075111 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -17,6 +17,8 @@ from curtin import config
 from curtin.reporter import events
 from .helpers import CiTestCase, dir2dict, populate_dir, random
 
+USE_GRUB = {'bootloaders': ['grub']}
+
 
 class TestGetFlashKernelPkgs(CiTestCase):
     def setUp(self):
@@ -489,10 +491,10 @@ class TestUpdateInitramfs(CiTestCase):
             call(['update-initramfs', '-c', '-k', kversion3],
                  target=self.target))
         subp_calls += self._subp_calls(
-            call(['update-initramfs', '-c', '-k', self.kversion],
+            call(['update-initramfs', '-c', '-k', kversion2],
                  target=self.target))
         subp_calls += self._subp_calls(
-            call(['update-initramfs', '-c', '-k', kversion2],
+            call(['update-initramfs', '-c', '-k', self.kversion],
                  target=self.target))
         self.mock_subp.assert_has_calls(subp_calls)
         self.assertEqual(24, self.mock_subp.call_count)
@@ -513,10 +515,10 @@ class TestUpdateInitramfs(CiTestCase):
         subp_calls = self._subp_calls(
             call(['dpkg-divert', '--list'], capture=True, target=self.target))
         subp_calls += self._subp_calls(
-            call(['update-initramfs', '-u', '-k', kversion2],
+            call(['update-initramfs', '-c', '-k', self.kversion],
                  target=self.target))
         subp_calls += self._subp_calls(
-            call(['update-initramfs', '-c', '-k', self.kversion],
+            call(['update-initramfs', '-u', '-k', kversion2],
                  target=self.target))
         self.mock_subp.assert_has_calls(subp_calls)
         self.assertEqual(18, self.mock_subp.call_count)
@@ -845,15 +847,29 @@ class TestSetupGrub(CiTestCase):
         cfg = {
             'grub_install_devices': ['/dev/vdb']
         }
+<<<<<<< tests/unittests/test_curthooks.py
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
                              variant=self.variant)
         self.m_install_grub.assert_called_with(
             ['/dev/vdb'], self.target, uefi=False,
             grubcfg=config.GrubConfig(install_devices=['/dev/vdb']))
+=======
+        updated_cfg = {
+            'install_devices': ['/dev/vdb']
+        }
+        updated_cfg.update(USE_GRUB)
 
-    def test_uses_install_devices_in_grubcfg(self):
+        curthooks.setup_boot(cfg, self.target, machine='amd64',
+                             stack_prefix='stack_prefix',
+                             osfamily=self.distro_family, variant=self.variant)
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False, bootcfg=updated_cfg)
+>>>>>>> tests/unittests/test_curthooks.py
+
+    def test_uses_install_devices_in_bootcfg(self):
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
             },
         }
@@ -861,14 +877,62 @@ class TestSetupGrub(CiTestCase):
             cfg, self.target,
             osfamily=self.distro_family, variant=self.variant)
         self.m_install_grub.assert_called_with(
+<<<<<<< tests/unittests/test_curthooks.py
             ['/dev/vdb'], self.target, uefi=False,
             grubcfg=config.fromdict(config.GrubConfig, cfg.get('grub')))
+=======
+            ['/dev/vdb'], self.target, uefi=False, bootcfg=cfg.get('boot'))
+
+    def test_uses_old_schema_install_devices_in_grubcfg(self):
+        cfg = {
+            'grub': {
+                'install_devices': ['/dev/vdb'],
+            },
+        }
+        updated_cfg = {
+            'install_devices': ['/dev/vdb']
+        }
+        updated_cfg.update(USE_GRUB)
+        curthooks.setup_boot(
+            cfg, self.target, machine='amd64', stack_prefix='stack_prefix',
+            osfamily=self.distro_family, variant=self.variant)
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False, bootcfg=updated_cfg)
+
+    def test_calls_install_grub(self):
+        cfg = {
+            'boot': {
+                'bootloaders': ['grub'],
+                'install_devices': ['/dev/vdb'],
+            },
+        }
+        curthooks.setup_boot(
+            cfg, self.target, 'amd64', '/testing',
+            osfamily=self.distro_family, variant=self.variant)
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False, bootcfg=cfg.get('boot'))
+
+    def test_skips_install_grub(self):
+        cfg = {
+            'boot': {
+                'bootloaders': ['grub'],
+                'install_devices': ['/dev/vdb'],
+            },
+        }
+        curthooks.setup_boot(
+            cfg, self.target, 'aarch64', '/testing',
+            osfamily=self.distro_family, variant=self.variant)
+        self.assertEqual(0, self.m_install_grub.call_count)
+>>>>>>> tests/unittests/test_curthooks.py
 
     @patch('curtin.commands.block_meta.multipath')
     @patch('curtin.commands.curthooks.os.path.exists')
     def test_uses_grub_install_on_storage_config(self, m_exists, m_multipath):
         m_multipath.is_mpath_member.return_value = False
         cfg = {
+            'boot': {
+                'bootloaders': ['grub'],
+            },
             'storage': {
                 'version': 1,
                 'config': [
@@ -881,12 +945,20 @@ class TestSetupGrub(CiTestCase):
                 ]
             },
         }
+        updated_cfg = {
+            'bootloaders': ['grub'],
+            'install_devices': ['/dev/vdb']
+        }
         m_exists.return_value = True
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
                              variant=self.variant)
         self.m_install_grub.assert_called_with(
             ['/dev/vdb'], self.target, uefi=False,
+<<<<<<< tests/unittests/test_curthooks.py
             grubcfg=config.GrubConfig(install_devices=['/dev/vdb']))
+=======
+            bootcfg=updated_cfg)
+>>>>>>> tests/unittests/test_curthooks.py
 
     @patch('curtin.commands.block_meta.multipath')
     @patch('curtin.block.is_valid_device')
@@ -927,7 +999,8 @@ class TestSetupGrub(CiTestCase):
                     },
                 ]
             },
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'update_nvram': False,
             },
         }
@@ -937,13 +1010,20 @@ class TestSetupGrub(CiTestCase):
                              variant='centos')
         self.m_install_grub.assert_called_with(
             ['/dev/vdb1'], self.target, uefi=True,
+<<<<<<< tests/unittests/test_curthooks.py
             grubcfg=config.GrubConfig(
                 update_nvram=False,
                 install_devices=['/dev/vdb1']))
+=======
+            bootcfg={'update_nvram': False, 'bootloaders': ['grub'],
+                     'install_devices': ['/dev/vdb1']}
+        )
+>>>>>>> tests/unittests/test_curthooks.py
 
     def test_grub_install_installs_to_none_if_install_devices_None(self):
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': None,
             },
         }
@@ -951,7 +1031,11 @@ class TestSetupGrub(CiTestCase):
                              variant=self.variant)
         self.m_install_grub.assert_called_with(
             ['none'], self.target, uefi=False,
+<<<<<<< tests/unittests/test_curthooks.py
             grubcfg=config.GrubConfig(install_devices=None),
+=======
+            bootcfg={'bootloaders': ['grub'], 'install_devices': None}
+>>>>>>> tests/unittests/test_curthooks.py
         )
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
@@ -961,7 +1045,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': False,
@@ -981,8 +1066,12 @@ class TestSetupGrub(CiTestCase):
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
                              variant=self.variant)
         self.m_install_grub.assert_called_with(
+<<<<<<< tests/unittests/test_curthooks.py
             ['/dev/vdb'], self.target, uefi=True,
             grubcfg=config.fromdict(config.GrubConfig, cfg.get('grub'))
+=======
+            ['/dev/vdb'], self.target, uefi=True, bootcfg=cfg.get('boot')
+>>>>>>> tests/unittests/test_curthooks.py
         )
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
@@ -992,7 +1081,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': True,
@@ -1034,7 +1124,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': False,
@@ -1070,7 +1161,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': False,
@@ -1101,7 +1193,10 @@ class TestSetupGrub(CiTestCase):
                              variant=self.variant)
         logs = self.logs.getvalue()
         print(logs)
+<<<<<<< tests/unittests/test_curthooks.py
         print(self.mock_subp.call_args_list)
+=======
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([], self.mock_subp.call_args_list)
         self.assertIn("Using fallback UEFI reordering:", logs)
         self.assertIn("missing 'BootCurrent' value", logs)
@@ -1116,7 +1211,8 @@ class TestSetupGrub(CiTestCase):
                        'mock_remove_old_loaders')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': False,
@@ -1168,7 +1264,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': True,
@@ -1209,7 +1306,10 @@ class TestSetupGrub(CiTestCase):
                              variant=self.variant)
         logs = self.logs.getvalue()
         print(logs)
+<<<<<<< tests/unittests/test_curthooks.py
         print(self.mock_subp.call_args_list)
+=======
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([
             call(['efibootmgr', '-o', '0001,0000'], target=self.target)],
             self.mock_subp.call_args_list)
@@ -1223,7 +1323,8 @@ class TestSetupGrub(CiTestCase):
         self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
-            'grub': {
+            'boot': {
+                'bootloaders': ['grub'],
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
                 'remove_old_uefi_loaders': True,
@@ -1263,7 +1364,11 @@ class TestSetupGrub(CiTestCase):
         print(logs)
         print('Number of bootmgr calls: %s' % self.mock_efibootmgr.call_count)
         self.assertEqual([
+<<<<<<< tests/unittests/test_curthooks.py
             call(['efibootmgr', '-o', '%s' % (",".join(final_state.order))],
+=======
+            call(['efibootmgr', '-o', '%s' % (",".join(expected_order))],
+>>>>>>> tests/unittests/test_curthooks.py
                  target=self.target)],
             self.mock_subp.call_args_list)
         self.assertIn("Using fallback UEFI reordering:", logs)
@@ -1311,8 +1416,13 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries(self):
+<<<<<<< tests/unittests/test_curthooks.py
         grubcfg = config.GrubConfig()
         curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+=======
+        bootcfg = USE_GRUB
+        curthooks.uefi_remove_duplicate_entries(bootcfg, self.target)
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([
             call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
                  target=self.target),
@@ -1322,11 +1432,19 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries_no_bootcurrent(self):
+<<<<<<< tests/unittests/test_curthooks.py
         grubcfg = config.GrubConfig()
         efiout = copy_efi_state(self.efibootmgr_output)
         efiout.current = ''
         self.m_efibootmgr.return_value = efiout
         curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+=======
+        bootcfg = USE_GRUB
+        efiout = copy.deepcopy(self.efibootmgr_output)
+        del efiout['current']
+        self.m_efibootmgr.return_value = efiout
+        curthooks.uefi_remove_duplicate_entries(bootcfg, self.target)
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([
             call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
                  target=self.target),
@@ -1336,19 +1454,35 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries_disabled(self):
+<<<<<<< tests/unittests/test_curthooks.py
         grubcfg = config.GrubConfig(
             remove_duplicate_entries=False,
             )
         curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+=======
+        bootcfg = {
+            'remove_duplicate_entries': False,
+        }
+        bootcfg.update(USE_GRUB)
+        curthooks.uefi_remove_duplicate_entries(bootcfg, self.target)
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([], self.m_subp.call_args_list)
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries_skip_bootcurrent(self):
+<<<<<<< tests/unittests/test_curthooks.py
         grubcfg = config.GrubConfig()
         efiout = copy_efi_state(self.efibootmgr_output)
         efiout.current = '0003'
         self.m_efibootmgr.return_value = efiout
         curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+=======
+        bootcfg = USE_GRUB
+        efiout = copy.deepcopy(self.efibootmgr_output)
+        efiout['current'] = '0003'
+        self.m_efibootmgr.return_value = efiout
+        curthooks.uefi_remove_duplicate_entries(bootcfg, self.target)
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([
             call(['efibootmgr', '--bootnum=0000', '--delete-bootnum'],
                  target=self.target),
@@ -1358,6 +1492,7 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries_no_change(self):
+<<<<<<< tests/unittests/test_curthooks.py
         grubcfg = config.GrubConfig()
         self.m_efibootmgr.return_value = util.EFIBootState(
             order=[],
@@ -1378,6 +1513,30 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
                 ),
             })
         curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+=======
+        bootcfg = USE_GRUB
+        self.m_efibootmgr.return_value = {
+            'current': '0000',
+            'entries': {
+                '0000': {
+                    'name': 'ubuntu',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
+                },
+                '0001': {
+                    'name': 'centos',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\centos\\shimx64.efi)'),
+                },
+                '0002': {
+                    'name': 'sles',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\sles\\shimx64.efi)'),
+                },
+            }
+        }
+        curthooks.uefi_remove_duplicate_entries(bootcfg, self.target)
+>>>>>>> tests/unittests/test_curthooks.py
         self.assertEqual([], self.m_subp.call_args_list)
 
 
@@ -2604,19 +2763,21 @@ class TestDoAptConfig(CiTestCase):
     def test_apt_config_dict(self):
         with patch(self.handle_apt_sym) as m_handle_apt:
             curthooks.do_apt_config({"apt": {}}, target="/")
-        m_handle_apt.assert_called()
+        m_handle_apt.assert_any_call({}, '/')
 
     def test_with_apt_config(self):
         with patch(self.handle_apt_sym) as m_handle_apt:
             curthooks.do_apt_config(
                     {"apt": {"proxy": {"http_proxy": "http://proxy:3128"}}},
                     target="/")
-        m_handle_apt.assert_called_once()
+        m_handle_apt.assert_any_call(
+                {'proxy': {'http_proxy': 'http://proxy:3128'}}, '/')
 
     def test_with_debconf_selections(self):
         # debconf_selections are translated to apt config
         with patch(self.handle_apt_sym) as m_handle_apt:
             curthooks.do_apt_config({"debconf_selections": "foo"}, target="/")
-        m_handle_apt.assert_called_once()
+        m_handle_apt.assert_any_call({'debconf_selections': 'foo'}, '/')
+
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_distro.py b/tests/unittests/test_distro.py
index 41c3b15..4a09067 100644
--- a/tests/unittests/test_distro.py
+++ b/tests/unittests/test_distro.py
@@ -311,7 +311,7 @@ class TestAptInstall(CiTestCase):
         ]
 
         distro.run_apt_command('install', ['foobar', 'wark'])
-        m_apt_update.assert_called_once()
+        self.assertEqual(1, m_apt_update.call_count)
         m_apt_install.assert_has_calls(expected_calls)
         m_subp.assert_called_once_with(['apt-get', 'clean'], target='/')
 
@@ -321,7 +321,7 @@ class TestAptInstall(CiTestCase):
 
         # no clean option
         distro.run_apt_command('install', ['foobar', 'wark'], clean=False)
-        m_apt_update.assert_called_once()
+        self.assertEqual(1, m_apt_update.call_count)
         m_subp.assert_has_calls(expected_calls[:-1])
 
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
@@ -334,11 +334,11 @@ class TestAptInstall(CiTestCase):
         ]
 
         expected_calls = [
-            mock.call(cmd_prefix + ['install', '--download-only']
-                                 + ['foobar', 'wark'],
+            mock.call(cmd_prefix +
+                      ['install', '--download-only'] +
+                      ['foobar', 'wark'],
                       env=None, target='/', retries=None),
-            mock.call(cmd_prefix + ['install']
-                                 + ['foobar', 'wark'],
+            mock.call(cmd_prefix + ['install', 'foobar', 'wark'],
                       env=None, target='/'),
         ]
 
diff --git a/tests/unittests/test_kernel_crash_dumps.py b/tests/unittests/test_kernel_crash_dumps.py
index 5747100..a878958 100644
--- a/tests/unittests/test_kernel_crash_dumps.py
+++ b/tests/unittests/test_kernel_crash_dumps.py
@@ -3,14 +3,20 @@
 from pathlib import Path
 from unittest.mock import MagicMock, patch
 
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
 from parameterized import parameterized
 
+=======
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
 from curtin.commands.curthooks import configure_kernel_crash_dumps
 from curtin.kernel_crash_dumps import (ENABLEMENT_SCRIPT, automatic_detect,
                                        detection_script_available,
                                        ensure_kdump_installed, manual_disable,
                                        manual_enable)
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
 from curtin.util import ProcessExecutionError
+=======
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
 from tests.unittests.helpers import CiTestCase
 
 
@@ -19,17 +25,21 @@ from tests.unittests.helpers import CiTestCase
 @patch("curtin.kernel_crash_dumps.automatic_detect")
 class TestKernelCrashDumpsCurthook(CiTestCase):
 
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
     @parameterized.expand(
         (
             ({"kernel-crash-dumps": {}},),
             ({"kernel-crash-dumps": {"enabled": None}},),
         )
     )
+=======
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
     def test_config__automatic(
         self,
         auto_mock,
         enable_mock,
         disable_mock,
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
         config,
     ):
         """Test expected automatic configs."""
@@ -38,6 +48,21 @@ class TestKernelCrashDumpsCurthook(CiTestCase):
         auto_mock.assert_called_once()
         enable_mock.assert_not_called()
         disable_mock.assert_not_called()
+=======
+    ):
+        """Test expected automatic configs."""
+        configs = [
+            {"kernel-crash-dumps": {}},
+            {"kernel-crash-dumps": {"enabled": None}},
+        ]
+        for config in configs:
+            configure_kernel_crash_dumps(config, "/target")
+            auto_mock.assert_called_once_with("/target")
+            enable_mock.assert_not_called()
+            disable_mock.assert_not_called()
+
+            auto_mock.reset_mock()
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
 
     def test_config__manual_enable(
         self,
@@ -49,7 +74,11 @@ class TestKernelCrashDumpsCurthook(CiTestCase):
         config = {"kernel-crash-dumps": {"enabled": True}}
         configure_kernel_crash_dumps(config, "/target")
         auto_mock.assert_not_called()
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
         enable_mock.assert_called_once()
+=======
+        enable_mock.assert_called_once_with("/target")
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
         disable_mock.assert_not_called()
 
     def test_config__manual_disable(
@@ -63,11 +92,16 @@ class TestKernelCrashDumpsCurthook(CiTestCase):
         configure_kernel_crash_dumps(config, "/target")
         auto_mock.assert_not_called()
         enable_mock.assert_not_called()
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
         disable_mock.assert_called_once()
+=======
+        disable_mock.assert_called_once_with("/target")
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
 
 
 class TestKernelCrashDumpsUtilities(CiTestCase):
 
+<<<<<<< tests/unittests/test_kernel_crash_dumps.py
     @parameterized.expand(
         (
             (True, True),
@@ -218,3 +252,106 @@ class TestKernelCrashDumpsUtilities(CiTestCase):
             subp_mock.assert_called_with([ENABLEMENT_SCRIPT])
         else:
             subp_mock.assert_not_called()
+=======
+    def test_detection_script_available(self):
+        """Test detection_script_available checks for script path."""
+
+        cases = [
+            (True, True),
+            (False, False),
+        ]
+
+        for preinstalled, expected in cases:
+            with patch(
+                "curtin.kernel_crash_dumps.Path.exists",
+                return_value=preinstalled,
+            ):
+                self.assertEqual(
+                    detection_script_available(Path("")),
+                    expected,
+                )
+
+    def test_ensure_kdump_installed(self):
+        """Test detection of preinstall and install of kdump-tools."""
+
+        cases = [True, False]
+
+        target = Path("/target")
+        for preinstalled in cases:
+            with (
+                patch(
+                    "curtin.distro.get_installed_packages",
+                    return_value=["kdump-tools" if preinstalled else ""],
+                )
+            ):
+                with patch("curtin.distro.install_packages") as do_install:
+                    ensure_kdump_installed(target)
+
+            if preinstalled:
+                do_install.assert_not_called()
+            else:
+                do_install.assert_called_with(
+                    ["kdump-tools"], target=str(target),
+                )
+
+    def test_manual_enable(self):
+        """Test manual enablement logic."""
+        target = Path("/target")
+        with patch(
+            "curtin.kernel_crash_dumps.ensure_kdump_installed",
+        ) as ensure_mock:
+            with patch(
+                "curtin.kernel_crash_dumps.ChrootableTarget",
+                new=MagicMock(),
+            ) as chroot_mock:
+                manual_enable(target)
+        ensure_mock.assert_called_once_with(Path("/target"))
+        subp_mock = chroot_mock.return_value.__enter__.return_value.subp
+        subp_mock.assert_called_with(
+            [ENABLEMENT_SCRIPT, "true"],
+        )
+
+    def test_manual_disable(self):
+        """Test manual disable logic."""
+        cases = [True, False]
+        target = Path("/target")
+
+        for preinstalled in cases:
+            with patch(
+                "curtin.distro.get_installed_packages",
+                return_value=["kdump-tools" if preinstalled else ""],
+            ):
+                with patch(
+                    "curtin.kernel_crash_dumps.ChrootableTarget",
+                    new=MagicMock(),
+                ) as chroot_mock:
+                    manual_disable(target)
+
+            subp_mock = chroot_mock.return_value.__enter__.return_value.subp
+            if preinstalled:
+                subp_mock.assert_called_with([ENABLEMENT_SCRIPT, "false"])
+            else:
+                subp_mock.assert_not_called()
+
+    def test_automatic_detect(self):
+        """Test automatic enablement logic."""
+        cases = [True, False]
+        target = Path("/target")
+
+        for wants_enablement in cases:
+            with patch(
+                "curtin.kernel_crash_dumps.detection_script_available",
+                return_value=wants_enablement,
+            ):
+                with patch(
+                    "curtin.kernel_crash_dumps.ChrootableTarget",
+                    new=MagicMock(),
+                ) as chroot_mock:
+                    automatic_detect(target)
+
+            subp_mock = chroot_mock.return_value.__enter__.return_value.subp
+            if wants_enablement:
+                subp_mock.assert_called_with([ENABLEMENT_SCRIPT])
+            else:
+                subp_mock.assert_not_called()
+>>>>>>> tests/unittests/test_kernel_crash_dumps.py
diff --git a/tests/unittests/test_storage_config.py b/tests/unittests/test_storage_config.py
index 7b0f68c..d1b89cd 100644
--- a/tests/unittests/test_storage_config.py
+++ b/tests/unittests/test_storage_config.py
@@ -1117,9 +1117,11 @@ class TestExtractStorageConfig(CiTestCase):
         config = extracted['storage']['config']
         disks = [cfg for cfg in config if cfg['type'] == 'disk']
         expected_count = len([
-            1 for bd_name, bd_data in self.probe_data['blockdev'].items()
-            if bd_data.get('DM_UUID', '').startswith('mpath-')
-            or bd_name.startswith('/dev/dasd') and bd_data['DEVTYPE'] == 'disk'
+            1
+            for bd_name, bd_data in self.probe_data['blockdev'].items()
+            if bd_data.get('DM_UUID', '').startswith('mpath-') or
+            bd_name.startswith('/dev/dasd') and
+            bd_data['DEVTYPE'] == 'disk'
             ])
         self.assertEqual(expected_count, len(disks))
 
@@ -1178,7 +1180,10 @@ class TestSelectConfigs(CiTestCase):
         id1 = {'a': 1, 'c': 3}
         sc = {'id0': id0, 'id1': id1}
 
-        self.assertEqual([id0, id1], select_configs(sc, a=1))
+        actual = select_configs(sc, a=1)
+        self.assertEqual(2, len(actual))
+        self.assertIn(id0, actual)
+        self.assertIn(id1, actual)
 
     def test_not_found(self):
         id0 = {'a': 1, 'b': 2}
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 90303e9..40d92b8 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -1077,6 +1077,7 @@ class TestGetEFIBootMGR(CiTestCase):
             Boot0005* UEFI:Network Device	BBS(131,,0x0)
             """), ''))
         observed = util.get_efibootmgr('target')
+<<<<<<< tests/unittests/test_util.py
         expected = util.EFIBootState(
             current='0000',
             timeout='1 seconds',
@@ -1109,6 +1110,39 @@ class TestGetEFIBootMGR(CiTestCase):
                 })
 
         self.assertEqual(expected, observed)
+=======
+        self.assertEqual({
+            'current': '0000',
+            'timeout': '1 seconds',
+            'order': ['0000', '0002', '0001', '0003', '0004', '0005'],
+            'entries': {
+                '0000': {
+                    'name': 'ubuntu',
+                    'path': 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)',
+                },
+                '0001': {
+                    'name': 'CD/DVD Drive',
+                    'path': 'BBS(CDROM,,0x0)',
+                },
+                '0002': {
+                    'name': 'Hard Drive',
+                    'path': 'BBS(HD,,0x0)',
+                },
+                '0003': {
+                    'name': 'UEFI:CD/DVD Drive',
+                    'path': 'BBS(129,,0x0)',
+                },
+                '0004': {
+                    'name': 'UEFI:Removable Device',
+                    'path': 'BBS(130,,0x0)',
+                },
+                '0005': {
+                    'name': 'UEFI:Network Device',
+                    'path': 'BBS(131,,0x0)',
+                },
+            }
+        }, observed)
+>>>>>>> tests/unittests/test_util.py
 
     def test_parses_output_filter_missing(self):
         """ensure parsing ignores items in order that don't have entries"""
@@ -1125,6 +1159,7 @@ class TestGetEFIBootMGR(CiTestCase):
             Boot0005* UEFI:Network Device	BBS(131,,0x0)
             """), ''))
         observed = util.get_efibootmgr('target')
+<<<<<<< tests/unittests/test_util.py
         expected = util.EFIBootState(
             current='0000',
             timeout='1 seconds',
@@ -1156,6 +1191,39 @@ class TestGetEFIBootMGR(CiTestCase):
                     ),
                 })
         self.assertEqual(expected, observed)
+=======
+        self.assertEqual({
+            'current': '0000',
+            'timeout': '1 seconds',
+            'order': ['0000', '0002', '0001', '0003', '0004', '0005'],
+            'entries': {
+                '0000': {
+                    'name': 'ubuntu',
+                    'path': 'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)',
+                },
+                '0001': {
+                    'name': 'CD/DVD Drive',
+                    'path': 'BBS(CDROM,,0x0)',
+                },
+                '0002': {
+                    'name': 'Hard Drive',
+                    'path': 'BBS(HD,,0x0)',
+                },
+                '0003': {
+                    'name': 'UEFI:CD/DVD Drive',
+                    'path': 'BBS(129,,0x0)',
+                },
+                '0004': {
+                    'name': 'UEFI:Removable Device',
+                    'path': 'BBS(130,,0x0)',
+                },
+                '0005': {
+                    'name': 'UEFI:Network Device',
+                    'path': 'BBS(131,,0x0)',
+                },
+            }
+        }, observed)
+>>>>>>> tests/unittests/test_util.py
 
 
 class TestUsesSystemd(CiTestCase):
@@ -1346,7 +1414,11 @@ class TestNotExclusiveRetry(CiTestCase):
         f = mock.Mock(side_effect=[util.NotExclusiveError, 'success'])
 
         self.assertEqual(util.not_exclusive_retry(f, 1, 2, 3), 'success')
+<<<<<<< tests/unittests/test_util.py
         sleep.assert_called_once()
+=======
+        self.assertEqual(1, sleep.call_count)
+>>>>>>> tests/unittests/test_util.py
 
     @mock.patch('curtin.util.time.sleep')
     def test_not_exclusive_retry_not_exclusive_twice(self, sleep):
@@ -1354,7 +1426,11 @@ class TestNotExclusiveRetry(CiTestCase):
 
         with self.assertRaises(util.NotExclusiveError):
             util.not_exclusive_retry(f, 1, 2, 3)
+<<<<<<< tests/unittests/test_util.py
         sleep.assert_called_once()
+=======
+        self.assertEqual(1, sleep.call_count)
+>>>>>>> tests/unittests/test_util.py
 
     @mock.patch('curtin.util.time.sleep')
     def test_not_exclusive_retry_not_exclusive_once_then_error(self, sleep):
@@ -1362,6 +1438,7 @@ class TestNotExclusiveRetry(CiTestCase):
 
         with self.assertRaises(OSError):
             util.not_exclusive_retry(f, 1, 2, 3)
+<<<<<<< tests/unittests/test_util.py
         sleep.assert_called_once()
 
 
@@ -1438,5 +1515,8 @@ class TestFlockEx(CiTestCase):
 
             fcntl.flock(fp, fcntl.LOCK_UN)
 
+=======
+        self.assertEqual(1, sleep.call_count)
+>>>>>>> tests/unittests/test_util.py
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/releases.py b/tests/vmtests/releases.py
index 56e5e8c..3202893 100644
--- a/tests/vmtests/releases.py
+++ b/tests/vmtests/releases.py
@@ -172,6 +172,13 @@ class _JammyBase(_UbuntuBase):
         subarch = "ga-22.04"
 
 
+class _JammyBase(_UbuntuBase):
+    release = "jammy"
+    target_release = "jammy"
+    if _UbuntuBase.arch == "arm64":
+        subarch = "ga-22.04"
+
+
 class _Releases(object):
     trusty = _TrustyBase
     precise = _PreciseBase
@@ -190,6 +197,11 @@ class _Releases(object):
     disco = _DiscoBase
     eoan = _EoanBase
     focal = _FocalBase
+<<<<<<< tests/vmtests/releases.py
+=======
+    hirsute = _HirsuteBase
+    impish = _ImpishBase
+>>>>>>> tests/vmtests/releases.py
     jammy = _JammyBase
 
 
diff --git a/tests/vmtests/test_apt_config_cmd.py b/tests/vmtests/test_apt_config_cmd.py
index 5a1c322..82bdae3 100644
--- a/tests/vmtests/test_apt_config_cmd.py
+++ b/tests/vmtests/test_apt_config_cmd.py
@@ -79,4 +79,8 @@ class JammyTestAptConfigCMDCMD(relbase.jammy, TestAptConfigCMD):
     __test__ = True
 
 
+class JammyTestAptConfigCMDCMD(relbase.jammy, TestAptConfigCMD):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_basic.py b/tests/vmtests/test_basic.py
index f9c3b04..33b0e95 100644
--- a/tests/vmtests/test_basic.py
+++ b/tests/vmtests/test_basic.py
@@ -251,6 +251,10 @@ class JammyTestBasic(relbase.jammy, TestBasicAbs):
     __test__ = True
 
 
+class JammyTestBasic(relbase.jammy, TestBasicAbs):
+    __test__ = True
+
+
 class TestBasicScsiAbs(TestBasicAbs):
     arch_skip = [
         'arm64',  # arm64 is UEFI only
@@ -377,4 +381,8 @@ class JammyTestScsiBasic(relbase.jammy, TestBasicScsiAbs):
     __test__ = True
 
 
+class JammyTestScsiBasic(relbase.jammy, TestBasicScsiAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_basic_dasd.py b/tests/vmtests/test_basic_dasd.py
index 154f26d..78edb3a 100644
--- a/tests/vmtests/test_basic_dasd.py
+++ b/tests/vmtests/test_basic_dasd.py
@@ -60,4 +60,8 @@ class JammyTestBasicDasd(relbase.jammy, TestBasicDasd):
     __test__ = True
 
 
+class JammyTestBasicDasd(relbase.jammy, TestBasicDasd):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_bcache_basic.py b/tests/vmtests/test_bcache_basic.py
index 2e62f0b..a7e4f37 100644
--- a/tests/vmtests/test_bcache_basic.py
+++ b/tests/vmtests/test_bcache_basic.py
@@ -72,4 +72,8 @@ class JammyBcacheBasic(relbase.jammy, TestBcacheBasic):
     __test__ = True
 
 
+class JammyBcacheBasic(relbase.jammy, TestBcacheBasic):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_bcache_bug1718699.py b/tests/vmtests/test_bcache_bug1718699.py
index 82a765a..56ee7b0 100644
--- a/tests/vmtests/test_bcache_bug1718699.py
+++ b/tests/vmtests/test_bcache_bug1718699.py
@@ -27,4 +27,8 @@ class JammyTestBcacheBug1718699(relbase.jammy, TestBcacheBug1718699):
     __test__ = True
 
 
+class JammyTestBcacheBug1718699(relbase.jammy, TestBcacheBug1718699):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_bcache_ceph.py b/tests/vmtests/test_bcache_ceph.py
index 15cce59..53df0ad 100644
--- a/tests/vmtests/test_bcache_ceph.py
+++ b/tests/vmtests/test_bcache_ceph.py
@@ -83,6 +83,10 @@ class JammyTestBcacheCeph(relbase.jammy, TestBcacheCeph):
     __test__ = True
 
 
+class JammyTestBcacheCeph(relbase.jammy, TestBcacheCeph):
+    __test__ = True
+
+
 class TestBcacheCephLvm(TestBcacheCeph):
     test_type = 'storage'
     nr_cpus = 2
@@ -113,4 +117,8 @@ class JammyTestBcacheCephLvm(relbase.jammy, TestBcacheCephLvm):
     __test__ = True
 
 
+class JammyTestBcacheCephLvm(relbase.jammy, TestBcacheCephLvm):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_bcache_partitions.py b/tests/vmtests/test_bcache_partitions.py
index c3c41bc..42f8575 100644
--- a/tests/vmtests/test_bcache_partitions.py
+++ b/tests/vmtests/test_bcache_partitions.py
@@ -33,4 +33,8 @@ class JammyTestBcachePartitions(relbase.jammy, TestBcachePartitions):
     __test__ = True
 
 
+class JammyTestBcachePartitions(relbase.jammy, TestBcachePartitions):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_fs_battery.py b/tests/vmtests/test_fs_battery.py
index bd1e629..be63ba2 100644
--- a/tests/vmtests/test_fs_battery.py
+++ b/tests/vmtests/test_fs_battery.py
@@ -254,4 +254,8 @@ class JammyTestFsBattery(relbase.jammy, TestFsBattery):
     __test__ = True
 
 
+class JammyTestFsBattery(relbase.jammy, TestFsBattery):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_iscsi.py b/tests/vmtests/test_iscsi.py
index b5d9082..6f6951b 100644
--- a/tests/vmtests/test_iscsi.py
+++ b/tests/vmtests/test_iscsi.py
@@ -82,4 +82,8 @@ class JammyTestIscsiBasic(relbase.jammy, TestBasicIscsiAbs):
     __test__ = True
 
 
+class JammyTestIscsiBasic(relbase.jammy, TestBasicIscsiAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_journald_reporter.py b/tests/vmtests/test_journald_reporter.py
index ba39af2..0056b73 100644
--- a/tests/vmtests/test_journald_reporter.py
+++ b/tests/vmtests/test_journald_reporter.py
@@ -40,4 +40,8 @@ class JammyTestJournaldReporter(relbase.jammy, TestJournaldReporter):
     __test__ = True
 
 
+class JammyTestJournaldReporter(relbase.jammy, TestJournaldReporter):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_lvm.py b/tests/vmtests/test_lvm.py
index e53e9f2..2c19e94 100644
--- a/tests/vmtests/test_lvm.py
+++ b/tests/vmtests/test_lvm.py
@@ -89,4 +89,8 @@ class JammyTestLvm(relbase.jammy, TestLvmAbs):
     __test__ = True
 
 
+class JammyTestLvm(relbase.jammy, TestLvmAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_lvm_iscsi.py b/tests/vmtests/test_lvm_iscsi.py
index 94e523b..77c16f3 100644
--- a/tests/vmtests/test_lvm_iscsi.py
+++ b/tests/vmtests/test_lvm_iscsi.py
@@ -103,4 +103,8 @@ class JammyTestIscsiLvm(relbase.jammy, TestLvmIscsiAbs):
     __test__ = True
 
 
+class JammyTestIscsiLvm(relbase.jammy, TestLvmIscsiAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_lvm_root.py b/tests/vmtests/test_lvm_root.py
index 4c9b5f3..04c4b9b 100644
--- a/tests/vmtests/test_lvm_root.py
+++ b/tests/vmtests/test_lvm_root.py
@@ -104,6 +104,13 @@ class JammyTestLvmRootExt4(relbase.jammy, TestLvmRootAbs):
     }
 
 
+class JammyTestLvmRootExt4(relbase.jammy, TestLvmRootAbs):
+    __test__ = True
+    conf_replace = {
+        '__ROOTFS_FORMAT__': 'ext4',
+    }
+
+
 class XenialTestLvmRootXfs(relbase.xenial, TestLvmRootAbs):
     __test__ = True
     conf_replace = {
@@ -158,6 +165,14 @@ class JammyTestUefiLvmRootExt4(relbase.jammy, TestUefiLvmRootAbs):
     }
 
 
+class JammyTestUefiLvmRootExt4(relbase.jammy, TestUefiLvmRootAbs):
+    __test__ = True
+    conf_replace = {
+        '__BOOTFS_FORMAT__': 'ext4',
+        '__ROOTFS_FORMAT__': 'ext4',
+    }
+
+
 class XenialTestUefiLvmRootXfs(relbase.xenial, TestUefiLvmRootAbs):
     __test__ = True
     conf_replace = {
diff --git a/tests/vmtests/test_mdadm_bcache.py b/tests/vmtests/test_mdadm_bcache.py
index 8de7702..a2911c0 100644
--- a/tests/vmtests/test_mdadm_bcache.py
+++ b/tests/vmtests/test_mdadm_bcache.py
@@ -169,6 +169,10 @@ class JammyTestMdadmBcache(relbase.jammy, TestMdadmBcacheAbs):
     __test__ = True
 
 
+class JammyTestMdadmBcache(relbase.jammy, TestMdadmBcacheAbs):
+    __test__ = True
+
+
 class TestMirrorbootAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/mirrorboot.yaml"
@@ -215,6 +219,10 @@ class JammyTestMirrorboot(relbase.jammy, TestMirrorbootAbs):
     __test__ = True
 
 
+class JammyTestMirrorboot(relbase.jammy, TestMirrorbootAbs):
+    __test__ = True
+
+
 class TestMirrorbootPartitionsAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/mirrorboot-msdos-partition.yaml"
@@ -266,6 +274,11 @@ class JammyTestMirrorbootPartitions(relbase.jammy,
     __test__ = True
 
 
+class JammyTestMirrorbootPartitions(relbase.jammy,
+                                    TestMirrorbootPartitionsAbs):
+    __test__ = True
+
+
 class TestMirrorbootPartitionsUEFIAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/mirrorboot-uefi.yaml"
@@ -366,6 +379,11 @@ class JammyTestMirrorbootPartitionsUEFI(relbase.jammy,
     __test__ = True
 
 
+class JammyTestMirrorbootPartitionsUEFI(relbase.jammy,
+                                        TestMirrorbootPartitionsUEFIAbs):
+    __test__ = True
+
+
 class TestRaid5bootAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/raid5boot.yaml"
@@ -414,6 +432,10 @@ class JammyTestRaid5boot(relbase.jammy, TestRaid5bootAbs):
     __test__ = True
 
 
+class JammyTestRaid5boot(relbase.jammy, TestRaid5bootAbs):
+    __test__ = True
+
+
 class TestRaid6bootAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/raid6boot.yaml"
@@ -475,6 +497,10 @@ class JammyTestRaid6boot(relbase.jammy, TestRaid6bootAbs):
     __test__ = True
 
 
+class JammyTestRaid6boot(relbase.jammy, TestRaid6bootAbs):
+    __test__ = True
+
+
 class TestRaid10bootAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/raid10boot.yaml"
@@ -522,6 +548,10 @@ class JammyTestRaid10boot(relbase.jammy, TestRaid10bootAbs):
     __test__ = True
 
 
+class JammyTestRaid10boot(relbase.jammy, TestRaid10bootAbs):
+    __test__ = True
+
+
 class TestAllindataAbs(TestMdadmAbs):
     # more complex, needs more time
     # alternative config for more complex setup
@@ -632,4 +662,8 @@ class FocalTestAllindata(relbase.focal, TestAllindataAbs):
 #     __test__ = True
 
 
+class JammyTestAllindata(relbase.jammy, TestAllindataAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_mdadm_iscsi.py b/tests/vmtests/test_mdadm_iscsi.py
index 8c61efe..456e779 100644
--- a/tests/vmtests/test_mdadm_iscsi.py
+++ b/tests/vmtests/test_mdadm_iscsi.py
@@ -59,4 +59,8 @@ class JammyTestIscsiMdadm(relbase.jammy, TestMdadmIscsiAbs):
     __test__ = True
 
 
+class JammyTestIscsiMdadm(relbase.jammy, TestMdadmIscsiAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_multipath.py b/tests/vmtests/test_multipath.py
index 50db226..96cda7b 100644
--- a/tests/vmtests/test_multipath.py
+++ b/tests/vmtests/test_multipath.py
@@ -170,6 +170,10 @@ class JammyTestMultipathBasic(relbase.jammy, TestMultipathBasicAbs):
     __test__ = True
 
 
+class JammyTestMultipathBasic(relbase.jammy, TestMultipathBasicAbs):
+    __test__ = True
+
+
 class TestMultipathReuseAbs(TestMultipathBasicAbs):
     conf_file = "examples/tests/multipath-reuse.yaml"
 
@@ -182,4 +186,8 @@ class JammyTestMultipathReuse(relbase.jammy, TestMultipathReuseAbs):
     __test__ = True
 
 
+class JammyTestMultipathReuse(relbase.jammy, TestMultipathReuseAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_multipath_lvm.py b/tests/vmtests/test_multipath_lvm.py
index 86cb393..375aa0e 100644
--- a/tests/vmtests/test_multipath_lvm.py
+++ b/tests/vmtests/test_multipath_lvm.py
@@ -68,6 +68,10 @@ class JammyTestMultipathLvm(relbase.jammy, TestMultipathLvmAbs):
     __test__ = True
 
 
+class JammyTestMultipathLvm(relbase.jammy, TestMultipathLvmAbs):
+    __test__ = True
+
+
 class TestMultipathLvmPartWipeAbs(TestMultipathLvmAbs):
     conf_file = "examples/tests/multipath-lvm-part-wipe.yaml"
 
@@ -83,4 +87,9 @@ class JammyTestMultipathLvmPartWipe(relbase.jammy,
     __test__ = True
 
 
+class JammyTestMultipathLvmPartWipe(relbase.jammy,
+                                    TestMultipathLvmPartWipeAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network.py b/tests/vmtests/test_network.py
index ad5a8bd..baebfc9 100644
--- a/tests/vmtests/test_network.py
+++ b/tests/vmtests/test_network.py
@@ -489,6 +489,10 @@ class JammyTestNetworkBasic(relbase.jammy, TestNetworkBasicAbs):
     __test__ = True
 
 
+class JammyTestNetworkBasic(relbase.jammy, TestNetworkBasicAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkBasic(centos_relbase.centos70_xenial,
                                CentosTestNetworkBasicAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_alias.py b/tests/vmtests/test_network_alias.py
index 628a8b7..9925faf 100644
--- a/tests/vmtests/test_network_alias.py
+++ b/tests/vmtests/test_network_alias.py
@@ -55,4 +55,8 @@ class JammyTestNetworkAlias(relbase.jammy, TestNetworkAliasAbs):
     __test__ = True
 
 
+class JammyTestNetworkAlias(relbase.jammy, TestNetworkAliasAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network_bonding.py b/tests/vmtests/test_network_bonding.py
index ea411d8..3a3477e 100644
--- a/tests/vmtests/test_network_bonding.py
+++ b/tests/vmtests/test_network_bonding.py
@@ -65,6 +65,10 @@ class JammyTestBonding(relbase.jammy, TestNetworkBondingAbs):
     __test__ = True
 
 
+class JammyTestBonding(relbase.jammy, TestNetworkBondingAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkBonding(centos_relbase.centos70_xenial,
                                  CentosTestNetworkBondingAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_bridging.py b/tests/vmtests/test_network_bridging.py
index 329a49f..eca0bc0 100644
--- a/tests/vmtests/test_network_bridging.py
+++ b/tests/vmtests/test_network_bridging.py
@@ -237,6 +237,10 @@ class JammyTestBridging(relbase.jammy, TestBridgeNetworkAbs):
     __test__ = True
 
 
+class JammyTestBridging(relbase.jammy, TestBridgeNetworkAbs):
+    __test__ = True
+
+
 class XenialTestBridgingV2(relbase.xenial, TestBridgeNetworkAbs):
     """ This class only needs to verify that when provided a v2 config
         that the Xenial network packages are installed. """
diff --git a/tests/vmtests/test_network_disabled.py b/tests/vmtests/test_network_disabled.py
index f9efaa6..e0c99af 100644
--- a/tests/vmtests/test_network_disabled.py
+++ b/tests/vmtests/test_network_disabled.py
@@ -66,6 +66,21 @@ class FocalCurtinDisableNetworkRendering(relbase.focal, TestKlass1):
     __test__ = True
 
 
+<<<<<<< tests/vmtests/test_network_disabled.py
+=======
+class HirsuteCurtinDisableNetworkRendering(relbase.hirsute, TestKlass1):
+    __test__ = True
+
+
+class ImpishCurtinDisableNetworkRendering(relbase.impish, TestKlass1):
+    __test__ = True
+
+
+class JammyCurtinDisableNetworkRendering(relbase.jammy, TestKlass1):
+    __test__ = True
+
+
+>>>>>>> tests/vmtests/test_network_disabled.py
 class FocalCurtinDisableCloudInitNetworking(relbase.focal, TestKlass2):
     __test__ = True
 
@@ -74,7 +89,15 @@ class FocalCurtinDisableCloudInitNetworkingVersion1(relbase.focal, TestKlass3):
     __test__ = True
 
 
+<<<<<<< tests/vmtests/test_network_disabled.py
 class JammyCurtinDisableNetworkRendering(relbase.jammy, TestKlass1):
+=======
+class JammyCurtinDisableCloudInitNetworking(relbase.jammy, TestKlass2):
+    __test__ = True
+
+
+class FocalCurtinDisableCloudInitNetworkingVersion1(relbase.focal, TestKlass3):
+>>>>>>> tests/vmtests/test_network_disabled.py
     __test__ = True
 
 
@@ -88,4 +111,9 @@ class JammyCurtinDisableCloudInitNetworkingVersion1(relbase.jammy, TestKlass3):
     __test__ = True
 
 
+class JammyCurtinDisableCloudInitNetworkingVersion1(relbase.jammy,
+                                                    TestKlass3):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network_ipv6.py b/tests/vmtests/test_network_ipv6.py
index 5c31b48..64c162f 100644
--- a/tests/vmtests/test_network_ipv6.py
+++ b/tests/vmtests/test_network_ipv6.py
@@ -61,6 +61,10 @@ class JammyTestNetworkIPV6(relbase.jammy, TestNetworkIPV6Abs):
     __test__ = True
 
 
+class JammyTestNetworkIPV6(relbase.jammy, TestNetworkIPV6Abs):
+    __test__ = True
+
+
 class Centos70TestNetworkIPV6(centos_relbase.centos70_xenial,
                               CentosTestNetworkIPV6Abs):
     __test__ = True
diff --git a/tests/vmtests/test_network_ipv6_static.py b/tests/vmtests/test_network_ipv6_static.py
index 5352bfe..d317977 100644
--- a/tests/vmtests/test_network_ipv6_static.py
+++ b/tests/vmtests/test_network_ipv6_static.py
@@ -31,6 +31,10 @@ class JammyTestNetworkIPV6Static(relbase.jammy, TestNetworkIPV6StaticAbs):
     __test__ = True
 
 
+class JammyTestNetworkIPV6Static(relbase.jammy, TestNetworkIPV6StaticAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkIPV6Static(centos_relbase.centos70_xenial,
                                     CentosTestNetworkIPV6StaticAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_ipv6_vlan.py b/tests/vmtests/test_network_ipv6_vlan.py
index 8f3fe64..3516d60 100644
--- a/tests/vmtests/test_network_ipv6_vlan.py
+++ b/tests/vmtests/test_network_ipv6_vlan.py
@@ -30,6 +30,10 @@ class JammyTestNetworkIPV6Vlan(relbase.jammy, TestNetworkIPV6VlanAbs):
     __test__ = True
 
 
+class JammyTestNetworkIPV6Vlan(relbase.jammy, TestNetworkIPV6VlanAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkIPV6Vlan(centos_relbase.centos70_xenial,
                                   CentosTestNetworkIPV6VlanAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_mtu.py b/tests/vmtests/test_network_mtu.py
index 67d5d01..5a80d1b 100644
--- a/tests/vmtests/test_network_mtu.py
+++ b/tests/vmtests/test_network_mtu.py
@@ -197,6 +197,10 @@ class JammyTestNetworkMtu(relbase.jammy, TestNetworkMtuNetworkdAbs):
     __test__ = True
 
 
+class JammyTestNetworkMtu(relbase.jammy, TestNetworkMtuNetworkdAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkMtu(centos_relbase.centos70_xenial,
                              CentosTestNetworkMtuAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_ovs.py b/tests/vmtests/test_network_ovs.py
index c50d115..363f140 100644
--- a/tests/vmtests/test_network_ovs.py
+++ b/tests/vmtests/test_network_ovs.py
@@ -49,4 +49,8 @@ class JammyTestNetworkOvs(relbase.jammy, TestNetworkOvsAbs):
     __test__ = True
 
 
+class JammyTestNetworkOvs(relbase.jammy, TestNetworkOvsAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network_static.py b/tests/vmtests/test_network_static.py
index 620c952..e3f6711 100644
--- a/tests/vmtests/test_network_static.py
+++ b/tests/vmtests/test_network_static.py
@@ -36,6 +36,10 @@ class JammyTestNetworkStatic(relbase.jammy, TestNetworkStaticAbs):
     __test__ = True
 
 
+class JammyTestNetworkStatic(relbase.jammy, TestNetworkStaticAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkStatic(centos_relbase.centos70_xenial,
                                 CentosTestNetworkStaticAbs):
     __test__ = True
diff --git a/tests/vmtests/test_network_static_routes.py b/tests/vmtests/test_network_static_routes.py
index 0258113..e099ddd 100644
--- a/tests/vmtests/test_network_static_routes.py
+++ b/tests/vmtests/test_network_static_routes.py
@@ -38,6 +38,11 @@ class JammyTestNetworkStaticRoutes(relbase.jammy,
     __test__ = True
 
 
+class JammyTestNetworkStaticRoutes(relbase.jammy,
+                                   TestNetworkStaticRoutesAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkStaticRoutes(centos_relbase.centos70_xenial,
                                       CentosTestNetworkStaticRoutesAbs):
     __test__ = False
diff --git a/tests/vmtests/test_network_vlan.py b/tests/vmtests/test_network_vlan.py
index 0ab62bb..0f2473a 100644
--- a/tests/vmtests/test_network_vlan.py
+++ b/tests/vmtests/test_network_vlan.py
@@ -84,6 +84,10 @@ class JammyTestNetworkVlan(relbase.jammy, TestNetworkVlanAbs):
     __test__ = True
 
 
+class JammyTestNetworkVlan(relbase.jammy, TestNetworkVlanAbs):
+    __test__ = True
+
+
 class Centos70TestNetworkVlan(centos_relbase.centos70_xenial,
                               CentosTestNetworkVlanAbs):
     __test__ = True
diff --git a/tests/vmtests/test_nvme.py b/tests/vmtests/test_nvme.py
index dd071e9..158c4e8 100644
--- a/tests/vmtests/test_nvme.py
+++ b/tests/vmtests/test_nvme.py
@@ -85,6 +85,10 @@ class JammyTestNvme(relbase.jammy, TestNvmeAbs):
     # OSError - [Errno 16] Device or resource busy: '/dev/mapper/mpatha'
 
 
+class JammyTestNvme(relbase.jammy, TestNvmeAbs):
+    __test__ = False
+
+
 class TestNvmeBcacheAbs(TestNvmeAbs):
     arch_skip = [
         "s390x",  # nvme is a pci device, no pci on s390x
@@ -155,4 +159,8 @@ class JammyTestNvmeBcache(relbase.jammy, TestNvmeBcacheAbs):
     __test__ = True
 
 
+class JammyTestNvmeBcache(relbase.jammy, TestNvmeBcacheAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_panic.py b/tests/vmtests/test_panic.py
index d8d3f22..39f30e1 100644
--- a/tests/vmtests/test_panic.py
+++ b/tests/vmtests/test_panic.py
@@ -33,4 +33,8 @@ class JammyTestInstallPanic(relbase.jammy, TestInstallPanic):
     __test__ = True
 
 
+class JammyTestInstallPanic(relbase.jammy, TestInstallPanic):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_pollinate_useragent.py b/tests/vmtests/test_pollinate_useragent.py
index 5a4b0fb..cfb75a8 100644
--- a/tests/vmtests/test_pollinate_useragent.py
+++ b/tests/vmtests/test_pollinate_useragent.py
@@ -69,4 +69,8 @@ class JammyTestPollinateUserAgent(relbase.jammy, TestPollinateUserAgent):
     __test__ = True
 
 
+class JammyTestPollinateUserAgent(relbase.jammy, TestPollinateUserAgent):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve.py b/tests/vmtests/test_preserve.py
index d6eb922..ff0ab59 100644
--- a/tests/vmtests/test_preserve.py
+++ b/tests/vmtests/test_preserve.py
@@ -33,4 +33,8 @@ class JammyTestPreserve(relbase.jammy, TestPreserve):
     __test__ = True
 
 
+class JammyTestPreserve(relbase.jammy, TestPreserve):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_bcache.py b/tests/vmtests/test_preserve_bcache.py
index 2384be8..762e146 100644
--- a/tests/vmtests/test_preserve_bcache.py
+++ b/tests/vmtests/test_preserve_bcache.py
@@ -64,4 +64,8 @@ class JammyTestPreserveBcache(relbase.jammy, TestPreserveBcache):
     __test__ = True
 
 
+class JammyTestPreserveBcache(relbase.jammy, TestPreserveBcache):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_lvm.py b/tests/vmtests/test_preserve_lvm.py
index e0bab24..351176b 100644
--- a/tests/vmtests/test_preserve_lvm.py
+++ b/tests/vmtests/test_preserve_lvm.py
@@ -81,4 +81,8 @@ class JammyTestLvmPreserve(relbase.jammy, TestLvmPreserveAbs):
     __test__ = True
 
 
+class JammyTestLvmPreserve(relbase.jammy, TestLvmPreserveAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_partition_wipe_vg.py b/tests/vmtests/test_preserve_partition_wipe_vg.py
index 7120e63..bc1dcf3 100644
--- a/tests/vmtests/test_preserve_partition_wipe_vg.py
+++ b/tests/vmtests/test_preserve_partition_wipe_vg.py
@@ -35,6 +35,10 @@ class JammyTestPreserveWipeLvm(relbase.jammy, TestPreserveWipeLvm):
     __test__ = True
 
 
+class JammyTestPreserveWipeLvm(relbase.jammy, TestPreserveWipeLvm):
+    __test__ = True
+
+
 class TestPreserveWipeLvmSimple(VMBaseClass):
     conf_file = "examples/tests/preserve-partition-wipe-vg-simple.yaml"
     uefi = False
@@ -59,4 +63,9 @@ class JammyTestPreserveWipeLvmSimple(relbase.jammy, TestPreserveWipeLvmSimple):
     __test__ = True
 
 
+class JammyTestPreserveWipeLvmSimple(relbase.jammy,
+                                     TestPreserveWipeLvmSimple):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_raid.py b/tests/vmtests/test_preserve_raid.py
index cf97203..8fd5c82 100644
--- a/tests/vmtests/test_preserve_raid.py
+++ b/tests/vmtests/test_preserve_raid.py
@@ -33,6 +33,10 @@ class JammyTestPreserveRAID(relbase.jammy, TestPreserveRAID):
     __test__ = True
 
 
+class JammyTestPreserveRAID(relbase.jammy, TestPreserveRAID):
+    __test__ = True
+
+
 class TestPartitionExistingRAID(VMBaseClass):
     """ Test that curtin can repartition an existing RAID. """
     conf_file = "examples/tests/partition-existing-raid.yaml"
@@ -66,4 +70,9 @@ class JammyTestPartitionExistingRAID(
     __test__ = True
 
 
+class JammyTestPartitionExistingRAID(
+        relbase.jammy, TestPartitionExistingRAID):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_python_apt.py b/tests/vmtests/test_python_apt.py
index 5243578..1f2d6d9 100644
--- a/tests/vmtests/test_python_apt.py
+++ b/tests/vmtests/test_python_apt.py
@@ -37,4 +37,8 @@ class JammyTestPythonApt(relbase.jammy, TestPythonApt):
     __test__ = True
 
 
+class JammyTestPythonApt(relbase.jammy, TestPythonApt):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_raid5_bcache.py b/tests/vmtests/test_raid5_bcache.py
index e04120d..ed1328c 100644
--- a/tests/vmtests/test_raid5_bcache.py
+++ b/tests/vmtests/test_raid5_bcache.py
@@ -96,4 +96,8 @@ class JammyTestRaid5Bcache(relbase.jammy, TestMdadmBcacheAbs):
     __test__ = True
 
 
+class JammyTestRaid5Bcache(relbase.jammy, TestMdadmBcacheAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_raid_partition_to_disk.py b/tests/vmtests/test_raid_partition_to_disk.py
index fa1939c..1adba86 100644
--- a/tests/vmtests/test_raid_partition_to_disk.py
+++ b/tests/vmtests/test_raid_partition_to_disk.py
@@ -26,4 +26,8 @@ class JammyTestRAIDPartitionToDisk(relbase.jammy, TestRAIDPartitionToDisk):
     __test__ = True
 
 
+class JammyTestRAIDPartitionToDisk(relbase.jammy, TestRAIDPartitionToDisk):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_lvm_member.py b/tests/vmtests/test_reuse_lvm_member.py
index cd7efd6..448adce 100644
--- a/tests/vmtests/test_reuse_lvm_member.py
+++ b/tests/vmtests/test_reuse_lvm_member.py
@@ -31,4 +31,9 @@ class JammyTestReuseLVMMemberPartition(relbase.jammy,
     __test__ = True
 
 
+class JammyTestReuseLVMMemberPartition(relbase.jammy,
+                                       TestReuseLVMMemberPartition):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_msdos_partitions.py b/tests/vmtests/test_reuse_msdos_partitions.py
index 32b4ad3..ad7fb42 100644
--- a/tests/vmtests/test_reuse_msdos_partitions.py
+++ b/tests/vmtests/test_reuse_msdos_partitions.py
@@ -30,4 +30,9 @@ class JammyTestReuseMSDOSPartitions(relbase.jammy,
     __test__ = True
 
 
+class JammyTestReuseMSDOSPartitions(relbase.jammy,
+                                    TestReuseMSDOSPartitions):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_raid_member.py b/tests/vmtests/test_reuse_raid_member.py
index 43c7185..fa62d42 100644
--- a/tests/vmtests/test_reuse_raid_member.py
+++ b/tests/vmtests/test_reuse_raid_member.py
@@ -36,6 +36,10 @@ class JammyTestReuseRAIDMember(relbase.jammy, TestReuseRAIDMember):
     __test__ = True
 
 
+class JammyTestReuseRAIDMember(relbase.jammy, TestReuseRAIDMember):
+    __test__ = True
+
+
 class BionicTestReuseRAIDMemberPartition(relbase.bionic,
                                          TestReuseRAIDMemberPartition):
     __test__ = True
@@ -51,4 +55,9 @@ class JammyTestReuseRAIDMemberPartition(relbase.jammy,
     __test__ = True
 
 
+class JammyTestReuseRAIDMemberPartition(relbase.jammy,
+                                        TestReuseRAIDMemberPartition):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_uefi_esp.py b/tests/vmtests/test_reuse_uefi_esp.py
index 3116505..1d94c98 100644
--- a/tests/vmtests/test_reuse_uefi_esp.py
+++ b/tests/vmtests/test_reuse_uefi_esp.py
@@ -13,7 +13,12 @@ class TestUefiReuseEspAbs(TestBasicAbs):
     def test_efiboot_menu_has_one_distro_entry(self):
         efi_state = util.parse_efibootmgr(
             self.load_collect_file("efibootmgr.out"))
+<<<<<<< tests/vmtests/test_reuse_uefi_esp.py
         duplicates = uefi_find_duplicate_entries(efi_state)
+=======
+        duplicates = uefi_find_duplicate_entries(
+            bootcfg=None, target=None, efi_output=efi_output)
+>>>>>>> tests/vmtests/test_reuse_uefi_esp.py
         print(duplicates)
         self.assertEqual(0, len(duplicates))
 
@@ -48,4 +53,11 @@ class JammyTestUefiReuseEsp(relbase.jammy, TestUefiReuseEspAbs):
         return super().test_efiboot_menu_has_one_distro_entry()
 
 
+class JammyTestUefiReuseEsp(relbase.jammy, TestUefiReuseEspAbs):
+    __test__ = True
+
+    def test_efiboot_menu_has_one_distro_entry(self):
+        return super().test_efiboot_menu_has_one_distro_entry()
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_simple.py b/tests/vmtests/test_simple.py
index 47f4d15..da5e9f1 100644
--- a/tests/vmtests/test_simple.py
+++ b/tests/vmtests/test_simple.py
@@ -54,6 +54,13 @@ class JammyTestSimple(relbase.jammy, TestSimple):
         self.output_files_exist(["netplan.yaml"])
 
 
+class JammyTestSimple(relbase.jammy, TestSimple):
+    __test__ = True
+
+    def test_output_files_exist(self):
+        self.output_files_exist(["netplan.yaml"])
+
+
 class TestSimpleStorage(VMBaseClass):
     """ Test curtin runs clear-holders when mode=simple with storage cfg. """
     conf_file = "examples/tests/simple-storage.yaml"
@@ -113,6 +120,13 @@ class JammyTestSimpleStorage(relbase.jammy, TestSimpleStorage):
         self.output_files_exist(["netplan.yaml"])
 
 
+class JammyTestSimpleStorage(relbase.jammy, TestSimpleStorage):
+    __test__ = True
+
+    def test_output_files_exist(self):
+        self.output_files_exist(["netplan.yaml"])
+
+
 class TestGrubNoDefaults(VMBaseClass):
     """ Test that curtin does not emit any grub configuration files. """
     conf_file = "examples/tests/no-grub-file.yaml"
@@ -146,4 +160,11 @@ class JammyTestGrubNoDefaults(relbase.jammy, TestGrubNoDefaults):
         self.output_files_exist(["netplan.yaml"])
 
 
+class JammyTestGrubNoDefaults(relbase.jammy, TestGrubNoDefaults):
+    __test__ = True
+
+    def test_output_files_exist(self):
+        self.output_files_exist(["netplan.yaml"])
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_uefi_basic.py b/tests/vmtests/test_uefi_basic.py
index e962e09..3821129 100644
--- a/tests/vmtests/test_uefi_basic.py
+++ b/tests/vmtests/test_uefi_basic.py
@@ -104,6 +104,10 @@ class JammyUefiTestBasic(relbase.jammy, TestBasicAbs):
     __test__ = True
 
 
+class JammyUefiTestBasic(relbase.jammy, TestBasicAbs):
+    __test__ = True
+
+
 class Centos70UefiTestBasic4k(centos_relbase.centos70_xenial, TestBasicAbs):
     __test__ = True
     disk_block_size = 4096
@@ -129,4 +133,9 @@ class JammyUefiTestBasic4k(relbase.jammy, TestBasicAbs):
     disk_block_size = 4096
 
 
+class JammyUefiTestBasic4k(relbase.jammy, TestBasicAbs):
+    __test__ = True
+    disk_block_size = 4096
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_zfsroot.py b/tests/vmtests/test_zfsroot.py
index 6b4209d..ad8d02f 100644
--- a/tests/vmtests/test_zfsroot.py
+++ b/tests/vmtests/test_zfsroot.py
@@ -106,6 +106,11 @@ class JammyTestZfsRoot(relbase.jammy, TestZfsRootAbs):
     mem = 4096
 
 
+class JammyTestZfsRoot(relbase.jammy, TestZfsRootAbs):
+    __test__ = True
+    mem = 4096
+
+
 class TestZfsRootFsTypeAbs(TestZfsRootAbs):
     conf_file = "examples/tests/basic-zfsroot.yaml"
 
@@ -135,4 +140,9 @@ class JammyTestZfsRootFsType(relbase.jammy, TestZfsRootFsTypeAbs):
     mem = 4096
 
 
+class JammyTestZfsRootFsType(relbase.jammy, TestZfsRootFsTypeAbs):
+    __test__ = True
+    mem = 4096
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tools/vmtest-system-setup b/tools/vmtest-system-setup
index 7d416b6..07a785b 100755
--- a/tools/vmtest-system-setup
+++ b/tools/vmtest-system-setup
@@ -14,19 +14,30 @@ esac
 DEPS=(
   build-essential
   cloud-image-utils
+<<<<<<< tools/vmtest-system-setup
   cryptsetup
   git
   libapt-pkg-dev
+=======
+  git
+>>>>>>> tools/vmtest-system-setup
   make
   net-tools
   python3
   python3-apt
   python3-attr
   python3-coverage
+<<<<<<< tools/vmtest-system-setup
   python3-debian
   python3-jsonschema
   python3-nose
   python3-oauthlib
+=======
+  python3-jsonschema
+  python3-nose
+  python3-oauthlib
+  python3-packaging
+>>>>>>> tools/vmtest-system-setup
   python3-parameterized
   python3-pep8
   python3-pip
diff --git a/tox.ini b/tox.ini
index 4e3a880..34fa117 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,7 +29,11 @@ sitepackages = false
 [testenv:py3-flake8]
 basepython = python3
 deps = {[testenv]deps}
+<<<<<<< tox.ini
 commands = {envpython} -m flake8 {posargs:--isolated curtin tests/}
+=======
+commands = {envpython} -m flake8 {posargs:curtin tests/}
+>>>>>>> tox.ini
 
 [testenv:py3-pyflakes]
 basepython = python3