← Back to team overview

curtin-dev team mailing list archive

[Merge] ~paride/curtin:release/focal/20.2 into curtin:ubuntu/focal

 

Paride Legovini has proposed merging ~paride/curtin:release/focal/20.2 into curtin:ubuntu/focal.

Commit message:
Release curtin version 20.2-0ubuntu1~20.04.1

Requested reviews:
  curtin developers (curtin-dev)
Related bugs:
  Bug #1888726 in systemd (Ubuntu): "systemd-udevd regression: some renamed network interfaces stuck in "pending" state"
  https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1888726
  Bug #1894910 in linux (Ubuntu): "fallocate swapfile has holes on 5.8 ext4, causes: swapon failed: Invalid argument"
  https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1894910
  Bug #1896947 in curtin: "Release 20.2"
  https://bugs.launchpad.net/curtin/+bug/1896947

For more details, see:
https://code.launchpad.net/~paride/curtin/+git/curtin/+merge/391530
-- 
Your team curtin developers is requested to review the proposed merge of ~paride/curtin:release/focal/20.2 into curtin:ubuntu/focal.
diff --git a/Makefile b/Makefile
index 68a3ad3..187132c 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ ifeq ($(COVERAGE), 1)
 endif
 CURTIN_VMTEST_IMAGE_SYNC ?= False
 export CURTIN_VMTEST_IMAGE_SYNC
-noseopts ?= -vv --nologcapture
+noseopts ?= -vv
 pylintopts ?= --rcfile=pylintrc --errors-only
 target_dirs ?= curtin tests tools
 
diff --git a/curtin/__init__.py b/curtin/__init__.py
index 2e1a0ed..82b6f26 100644
--- a/curtin/__init__.py
+++ b/curtin/__init__.py
@@ -8,6 +8,8 @@ KERNEL_CMDLINE_COPY_TO_INSTALL_SEP = "---"
 # can determine which features are supported.  Each entry should have
 # a consistent meaning.
 FEATURES = [
+    # curtin supports creating swapfiles on btrfs, if possible
+    'BTRFS_SWAPFILE',
     # curtin can apply centos networking via centos_apply_network_config
     'CENTOS_APPLY_NETWORK_CONFIG',
     # curtin can configure centos storage devices and boot devices
@@ -32,8 +34,10 @@ FEATURES = [
     'APT_CONFIG_V1',
     # has version module
     'HAS_VERSION_MODULE',
+    # uefi_reoder has fallback support if BootCurrent is missing
+    'UEFI_REORDER_FALLBACK_SUPPORT',
 ]
 
-__version__ = "20.1"
+__version__ = "20.2"
 
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
index 35e3a64..0cf0866 100644
--- a/curtin/block/__init__.py
+++ b/curtin/block/__init__.py
@@ -1,5 +1,5 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
-
+import re
 from contextlib import contextmanager
 import errno
 import itertools
@@ -67,6 +67,19 @@ def dev_path(devname):
         return '/dev/' + devname
 
 
+def md_path(mdname):
+    """ Convert device name to path in /dev/md """
+    full_mdname = dev_path(mdname)
+    if full_mdname.startswith('/dev/md/'):
+        return full_mdname
+    elif re.match(r'/dev/md\d+$', full_mdname):
+        return full_mdname
+    elif '/' in mdname:
+        raise ValueError("Invalid RAID device name: {}".format(mdname))
+    else:
+        return '/dev/md/{}'.format(mdname)
+
+
 def path_to_kname(path):
     """
     converts a path in /dev or a path in /sys/block to the device kname,
@@ -320,7 +333,7 @@ def dmsetup_info(devname):
                                  ','.join(fields), '--noheading',
                                  '--separator', _SEP], capture=True)
     except util.ProcessExecutionError as e:
-        LOG.error('Failed to run dmsetup info:', e)
+        LOG.error('Failed to run dmsetup info: %s', e)
         return {}
 
     values = out.strip().split(_SEP)
@@ -840,6 +853,8 @@ def _get_dev_disk_by_prefix(prefix):
      '/dev/sda1': '/dev/disk/<prefix>/virtio-aaaa-part1',
     }
     """
+    if not os.path.exists(prefix):
+        return {}
     return {
         os.path.realpath(bypfx): bypfx
         for bypfx in [os.path.join(prefix, path)
diff --git a/curtin/block/dasd.py b/curtin/block/dasd.py
index 682f9d3..b7008f6 100644
--- a/curtin/block/dasd.py
+++ b/curtin/block/dasd.py
@@ -269,9 +269,9 @@ def _valid_device_id(device_id):
     if not (0 <= int(dsn, 16) < 256):
         raise ValueError("device_id invalid: dsn not in 0-255: '%s'" % dsn)
 
-    if not (0 <= int(dev.lower(), 16) < 65535):
+    if not (0 <= int(dev.lower(), 16) <= 65535):
         raise ValueError(
-            "device_id invalid: devno not in 0-0x10000: '%s'" % dev)
+            "device_id invalid: devno not in 0-0xffff: '%s'" % dev)
 
     return True
 
diff --git a/curtin/block/multipath.py b/curtin/block/multipath.py
index 9c7f510..7ad1791 100644
--- a/curtin/block/multipath.py
+++ b/curtin/block/multipath.py
@@ -7,7 +7,7 @@ from curtin import udev
 SHOW_PATHS_FMT = ("device='%d' serial='%z' multipath='%m' host_wwpn='%N' "
                   "target_wwnn='%n' host_wwpn='%R' target_wwpn='%r' "
                   "host_adapter='%a'")
-SHOW_MAPS_FMT = "name=%n multipath='%w' sysfs='%d' paths='%N'"
+SHOW_MAPS_FMT = "name='%n' multipath='%w' sysfs='%d' paths='%N'"
 
 
 def _extract_mpath_data(cmd, show_verb):
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index ff0f2e9..dee73b1 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -502,7 +502,7 @@ def get_path_to_storage_volume(volume, storage_config):
     elif vol.get('type') == "raid":
         # For raid partitions, block device is at /dev/mdX
         name = vol.get('name')
-        volume_path = os.path.join("/dev", name)
+        volume_path = block.md_path(name)
 
     elif vol.get('type') == "bcache":
         # For bcache setups, the only reliable way to determine the name of the
@@ -1485,7 +1485,7 @@ def raid_handler(info, storage_config):
     devices = info.get('devices')
     raidlevel = info.get('raidlevel')
     spare_devices = info.get('spare_devices')
-    md_devname = block.dev_path(info.get('name'))
+    md_devname = block.md_path(info.get('name'))
     preserve = config.value_as_boolean(info.get('preserve'))
     if not devices:
         raise ValueError("devices for raid must be specified")
@@ -1744,7 +1744,12 @@ def get_device_paths_from_storage_config(storage_config):
     dpaths = []
     for (k, v) in storage_config.items():
         if v.get('type') in ['disk', 'partition']:
-            if config.value_as_boolean(v.get('wipe')):
+            wipe = config.value_as_boolean(v.get('wipe'))
+            preserve = config.value_as_boolean(v.get('preserve'))
+            if v.get('type') == 'disk' and all([wipe, preserve]):
+                msg = 'type:disk id=%s has both wipe and preserve' % v['id']
+                raise RuntimeError(msg)
+            if wipe:
                 try:
                     # skip paths that do not exit, nothing to wipe
                     dpath = get_path_to_storage_volume(k, storage_config)
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index d66afa7..4cf7301 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -85,6 +85,8 @@ do_initrd = yes
 link_in_boot = {inboot}
 """
 
+UEFI_BOOT_ENTRY_IS_NETWORK = r'.*(Network|PXE|NIC|Ethernet|LAN|IP4|IP6)+.*'
+
 
 def do_apt_config(cfg, target):
     cfg = apt_config.translate_old_apt_features(cfg)
@@ -411,6 +413,7 @@ def install_kernel(cfg, target):
 def uefi_remove_old_loaders(grubcfg, target):
     """Removes the old UEFI loaders from efibootmgr."""
     efi_output = util.get_efibootmgr(target)
+    LOG.debug('UEFI remove old olders efi output:\n%s', efi_output)
     current_uefi_boot = efi_output.get('current', None)
     old_efi_entries = {
         entry: info
@@ -437,18 +440,90 @@ def uefi_remove_old_loaders(grubcfg, target):
                     "should be removed.", info['name'])
 
 
-def uefi_reorder_loaders(grubcfg, target):
+def uefi_boot_entry_is_network(boot_entry_name):
+    """
+    Return boolean if boot entry name looks like a known network entry.
+    """
+    return re.match(UEFI_BOOT_ENTRY_IS_NETWORK,
+                    boot_entry_name, re.IGNORECASE) is not None
+
+
+def _reorder_new_entry(boot_order, efi_output, efi_orig=None, variant=None):
+    """
+    Reorder the EFI boot menu as follows
+
+    1. All PXE/Network boot entries
+    2. The newly installed entry variant (ubuntu/centos)
+    3. The other items in the boot order that are not in [1, 2]
+
+    returns a list of bootnum strings
+    """
+
+    if not boot_order:
+        raise RuntimeError('boot_order is not a list')
+
+    if efi_orig is None:
+        raise RuntimeError('Missing efi_orig boot dictionary')
+
+    if variant is None:
+        variant = ""
+
+    net_boot = []
+    other = []
+    target = []
+
+    LOG.debug("UEFI previous boot order: %s", efi_orig['order'])
+    LOG.debug("UEFI current  boot order: %s", boot_order)
+    new_entries = list(set(boot_order).difference(set(efi_orig['order'])))
+    if new_entries:
+        LOG.debug("UEFI Found new boot entries: %s", new_entries)
+    LOG.debug('UEFI Looking for installed entry variant=%s', variant.lower())
+    for bootnum in boot_order:
+        entry = efi_output['entries'][bootnum]
+        if uefi_boot_entry_is_network(entry['name']):
+            net_boot.append(bootnum)
+        else:
+            if entry['name'].lower() == variant.lower():
+                target.append(bootnum)
+            else:
+                other.append(bootnum)
+
+    if net_boot:
+        LOG.debug("UEFI found netboot entries: %s", net_boot)
+    if other:
+        LOG.debug("UEFI found other entries: %s", other)
+    if target:
+        LOG.debug("UEFI found target entry: %s", target)
+    else:
+        LOG.debug("UEFI Did not find an entry with variant=%s",
+                  variant.lower())
+    new_order = net_boot + target + other
+    if boot_order == new_order:
+        LOG.debug("UEFI Current and Previous bootorders match")
+    return new_order
+
+
+def uefi_reorder_loaders(grubcfg, target, efi_orig=None, variant=None):
     """Reorders the UEFI BootOrder to place BootCurrent first.
 
     The specifically doesn't try to do to much. The order in which grub places
     a new EFI loader is up to grub. This only moves the BootCurrent to the
     front of the BootOrder.
+
+    In some systems, BootCurrent may not be set/present.  In this case
+    curtin will attempt to place the new boot entry created when grub
+    is installed after the the previous first entry (before we installed grub).
+
     """
     if grubcfg.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', [])
-        if currently_booted:
+        new_boot_order = None
+        force_fallback_reorder = config.value_as_boolean(
+            grubcfg.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
@@ -456,6 +531,23 @@ def uefi_reorder_loaders(grubcfg, target):
             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:
@@ -465,25 +557,44 @@ def uefi_reorder_loaders(grubcfg, target):
         LOG.debug("Currently booted UEFI loader might no longer boot.")
 
 
-def uefi_remove_duplicate_entries(grubcfg, target):
+def uefi_remove_duplicate_entries(grubcfg, target, to_remove=None):
+    if not grubcfg.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(grubcfg, target)
+
+    # check so we don't run ChrootableTarget code unless we have things to do
+    if to_remove:
+        with util.ChrootableTarget(target) as in_chroot:
+            for bootnum, entry in to_remove:
+                LOG.debug('Removing duplicate EFI entry (%s, %s)',
+                          bootnum, entry)
+                in_chroot.subp(['efibootmgr', '--bootnum=%s' % bootnum,
+                                '--delete-bootnum'])
+
+
+def uefi_find_duplicate_entries(grubcfg, target, efi_output=None):
     seen = set()
     to_remove = []
-    efi_output = util.get_efibootmgr(target=target)
+    if efi_output is None:
+        efi_output = util.get_efibootmgr(target=target)
     entries = efi_output.get('entries', {})
+    current_bootnum = efi_output.get('current', None)
+    # adding BootCurrent to seen first allows us to remove any other duplicate
+    # entry of BootCurrent.
+    if current_bootnum:
+        seen.add(tuple(entries[current_bootnum].items()))
     for bootnum in sorted(entries):
+        if bootnum == current_bootnum:
+            continue
         entry = entries[bootnum]
         t = tuple(entry.items())
         if t not in seen:
             seen.add(t)
         else:
             to_remove.append((bootnum, entry))
-    if to_remove:
-        with util.ChrootableTarget(target) as in_chroot:
-            for bootnum, entry in to_remove:
-                LOG.debug('Removing duplicate EFI entry (%s, %s)',
-                          bootnum, entry)
-                in_chroot.subp(['efibootmgr', '--bootnum=%s' % bootnum,
-                                '--delete-bootnum'])
+    return to_remove
 
 
 def _debconf_multiselect(package, variable, choices):
@@ -557,7 +668,7 @@ def uefi_find_grub_device_ids(sconfig):
                 esp_partitions.append(item_id)
                 continue
 
-        if item['type'] == 'mount' and item['path'] == '/boot/efi':
+        if item['type'] == 'mount' and item.get('path') == '/boot/efi':
             if primary_esp:
                 LOG.debug('Ignoring duplicate mounted primary ESP: %s',
                           item_id)
@@ -592,7 +703,7 @@ def uefi_find_grub_device_ids(sconfig):
     return grub_device_ids
 
 
-def setup_grub(cfg, target, osfamily=DISTROS.debian):
+def setup_grub(cfg, target, osfamily=DISTROS.debian, variant=None):
     # target is the path to the mounted filesystem
 
     # FIXME: these methods need moving to curtin.block
@@ -692,13 +803,14 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
 
     update_nvram = grubcfg.get('update_nvram', True)
     if uefi_bootable and update_nvram:
+        efi_orig_output = util.get_efibootmgr(target)
         uefi_remove_old_loaders(grubcfg, target)
 
     install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg)
 
     if uefi_bootable and update_nvram:
+        uefi_reorder_loaders(grubcfg, target, efi_orig_output, variant)
         uefi_remove_duplicate_entries(grubcfg, target)
-        uefi_reorder_loaders(grubcfg, target)
 
 
 def update_initramfs(target=None, all_kernels=False):
@@ -900,6 +1012,7 @@ def add_swap(cfg, target, fstab):
     fname = swapcfg.get('filename', None)
     size = swapcfg.get('size', None)
     maxsize = swapcfg.get('maxsize', None)
+    force = swapcfg.get('force', False)
 
     if size:
         size = util.human2bytes(str(size))
@@ -907,7 +1020,7 @@ def add_swap(cfg, target, fstab):
         maxsize = util.human2bytes(str(maxsize))
 
     swap.setup_swapfile(target=target, fstab=fstab, swapfile=fname, size=size,
-                        maxsize=maxsize)
+                        maxsize=maxsize, force=force)
 
 
 def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
@@ -1733,7 +1846,8 @@ def builtin_curthooks(cfg, target, state):
                 name=stack_prefix + '/install-grub',
                 reporting_enabled=True, level="INFO",
                 description="installing grub to target devices"):
-            setup_grub(cfg, target, osfamily=osfamily)
+            setup_grub(cfg, target, osfamily=osfamily,
+                       variant=distro_info.variant)
 
 
 def curthooks(args):
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
index 777aa35..5f8311f 100644
--- a/curtin/commands/install_grub.py
+++ b/curtin/commands/install_grub.py
@@ -346,7 +346,7 @@ def install_grub(devices, target, uefi=None, grubcfg=None):
 
     LOG.debug("installing grub to target=%s devices=%s [replace_defaults=%s]",
               target, devices, grubcfg.get('replace_default'))
-    update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', False))
+    update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', True))
     distroinfo = distro.get_distroinfo(target=target)
     target_arch = distro.get_architecture(target=target)
     rhel_ver = (distro.rpm_get_dist_id(target)
diff --git a/curtin/commands/swap.py b/curtin/commands/swap.py
index f2381e6..089cd73 100644
--- a/curtin/commands/swap.py
+++ b/curtin/commands/swap.py
@@ -40,7 +40,7 @@ def swap_main(args):
 
     swap.setup_swapfile(target=state['target'], fstab=state['fstab'],
                         swapfile=args.swapfile, size=size,
-                        maxsize=args.maxsize)
+                        maxsize=args.maxsize, force=args.force)
     sys.exit(2)
 
 
@@ -54,6 +54,9 @@ CMD_ARGUMENTS = (
                 'default is env[TARGET_MOUNT_POINT]'),
        'action': 'store', 'metavar': 'TARGET',
        'default': os.environ.get('TARGET_MOUNT_POINT')}),
+     (('-F', '--force'),
+      {'help': 'force creating of swapfile even if it may fail (btrfs,xfs)',
+               'default': False, 'action': 'store_true'}),
      (('-s', '--size'),
       {'help': 'size of swap file (eg: 1G, 1500M, 1024K, 100000. def: "auto")',
                'default': None, 'action': 'store'}),
diff --git a/curtin/distro.py b/curtin/distro.py
index 43b0c19..82a4dd5 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -264,7 +264,7 @@ def apt_update(target=None, env=None, force=False, comment=None,
 
 
 def run_apt_command(mode, args=None, opts=None, env=None, target=None,
-                    execute=True, allow_daemons=False):
+                    execute=True, allow_daemons=False, clean=True):
     defopts = ['--quiet', '--assume-yes',
                '--option=Dpkg::options::=--force-unsafe-io',
                '--option=Dpkg::Options::=--force-confold']
@@ -289,7 +289,11 @@ def run_apt_command(mode, args=None, opts=None, env=None, target=None,
 
     apt_update(target, env=env, comment=' '.join(cmd))
     with ChrootableTarget(target, allow_daemons=allow_daemons) as inchroot:
-        return inchroot.subp(cmd, env=env)
+        cmd_rv = inchroot.subp(cmd, env=env)
+        if clean and mode in ['dist-upgrade', 'install', 'upgrade']:
+            inchroot.subp(['apt-get', 'clean'])
+
+    return cmd_rv
 
 
 def run_yum_command(mode, args=None, opts=None, env=None, target=None,
@@ -472,6 +476,7 @@ def parse_dpkg_version(raw, name=None, semx=None):
        as the upstream version.
 
        returns a dictionary with fields:
+          'epoch'
           'major' (int), 'minor' (int), 'micro' (int),
           'semantic_version' (int),
           'extra' (string), 'raw' (string), 'upstream' (string),
@@ -484,12 +489,20 @@ def parse_dpkg_version(raw, name=None, semx=None):
     if semx is None:
         semx = (10000, 100, 1)
 
-    if "-" in raw:
-        upstream = raw.rsplit('-', 1)[0]
+    raw_offset = 0
+    if ':' in raw:
+        epoch, _, upstream = raw.partition(':')
+        raw_offset = len(epoch) + 1
     else:
-        # this is a native package, package version treated as upstream.
+        epoch = 0
         upstream = raw
 
+    if "-" in raw[raw_offset:]:
+        upstream = raw[raw_offset:].rsplit('-', 1)[0]
+    else:
+        # this is a native package, package version treated as upstream.
+        upstream = raw[raw_offset:]
+
     match = re.search(r'[^0-9.]', upstream)
     if match:
         extra = upstream[match.start():]
@@ -498,8 +511,10 @@ def parse_dpkg_version(raw, name=None, semx=None):
         upstream_base = upstream
         extra = None
 
-    toks = upstream_base.split(".", 2)
-    if len(toks) == 3:
+    toks = upstream_base.split(".", 3)
+    if len(toks) == 4:
+        major, minor, micro, extra = toks
+    elif len(toks) == 3:
         major, minor, micro = toks
     elif len(toks) == 2:
         major, minor, micro = (toks[0], toks[1], 0)
@@ -507,6 +522,7 @@ def parse_dpkg_version(raw, name=None, semx=None):
         major, minor, micro = (toks[0], 0, 0)
 
     version = {
+        'epoch': int(epoch),
         'major': int(major),
         'minor': int(minor),
         'micro': int(micro),
diff --git a/curtin/net/deps.py b/curtin/net/deps.py
index f912d1d..b78654d 100644
--- a/curtin/net/deps.py
+++ b/curtin/net/deps.py
@@ -34,10 +34,13 @@ def network_config_required_packages(network_config, mapping=None):
             if cfgtype == 'version':
                 continue
             dev_configs.add(cfgtype)
-            # the renderer type may trigger package adds
+            # subkeys under the type may trigger package adds
             for entry, entry_cfg in cfg.items():
                 if entry_cfg.get('renderer'):
                     dev_configs.add(entry_cfg.get('renderer'))
+                else:
+                    for sub_entry, sub_cfg in entry_cfg.items():
+                        dev_configs.add(sub_entry)
 
     needed_packages = []
     for dev_type in dev_configs:
diff --git a/curtin/swap.py b/curtin/swap.py
index d3f29dc..11e95c4 100644
--- a/curtin/swap.py
+++ b/curtin/swap.py
@@ -5,6 +5,8 @@ import resource
 
 from .log import LOG
 from . import util
+from curtin import paths
+from curtin import distro
 
 
 def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
@@ -51,7 +53,62 @@ def suggested_swapsize(memsize=None, maxsize=None, fsys=None):
     return maxsize
 
 
-def setup_swapfile(target, fstab=None, swapfile=None, size=None, maxsize=None):
+def get_fstype(target, source):
+    target_source = paths.target_path(target, source)
+    try:
+        out, _ = util.subp(['findmnt', '--noheading', '--target',
+                            target_source, '-o', 'FSTYPE'], capture=True)
+    except util.ProcessExecutionError as exc:
+        LOG.warning('Failed to query %s fstype, findmnt returned error: %s',
+                    target_source, exc)
+        return None
+
+    if out:
+        """
+        $ findmnt --noheading --target /btrfs  -o FSTYPE
+        btrfs
+        """
+        return out.splitlines()[-1]
+
+    return None
+
+
+def get_target_kernel_version(target):
+    pkg_ver = None
+
+    distro_info = distro.get_distroinfo(target=target)
+    if not distro_info:
+        raise RuntimeError('Failed to determine target distro')
+    osfamily = distro_info.family
+    if osfamily == distro.DISTROS.debian:
+        try:
+            # check in-target version
+            pkg_ver = distro.get_package_version('linux-image-generic',
+                                                 target=target)
+        except Exception as e:
+            LOG.warn(
+                "failed reading linux-image-generic package version, %s", e)
+    return pkg_ver
+
+
+def can_use_swapfile(target, fstype):
+    if fstype is None:
+        raise RuntimeError(
+            'Unknown target filesystem type, may not support swapfiles')
+    if fstype in ['btrfs', 'xfs']:
+        # check kernel version
+        pkg_ver = get_target_kernel_version(target)
+        if not pkg_ver:
+            raise RuntimeError('Failed to read target kernel version')
+        if fstype == 'btrfs' and pkg_ver['major'] < 5:
+            raise RuntimeError(
+                'btrfs requiers kernel version 5.0+ to use swapfiles')
+    elif fstype in ['zfs']:
+        raise RuntimeError('ZFS cannot use swapfiles')
+
+
+def setup_swapfile(target, fstab=None, swapfile=None, size=None, maxsize=None,
+                   force=False):
     if size is None:
         size = suggested_swapsize(fsys=target, maxsize=maxsize)
 
@@ -65,6 +122,24 @@ def setup_swapfile(target, fstab=None, swapfile=None, size=None, maxsize=None):
     if not swapfile.startswith("/"):
         swapfile = "/" + swapfile
 
+    # query the directory in which swapfile will reside
+    fstype = get_fstype(target, os.path.dirname(swapfile))
+    try:
+        can_use_swapfile(target, fstype)
+    except RuntimeError as err:
+        if force:
+            LOG.warning('swapfile may not work: %s', err)
+        else:
+            LOG.debug('Not creating swap: %s', err)
+            return
+
+    allocate_cmd = 'fallocate -l "${2}M" "$1"'
+    # fallocate uses IOCTLs to allocate space in a filesystem, however it's not
+    # clear (from curtin's POV) that it creates non-sparse files as required by
+    # mkswap so we'll skip fallocate for now and use dd.
+    if fstype in ['btrfs', 'xfs']:
+        allocate_cmd = 'dd if=/dev/zero "of=$1" bs=1M "count=$2"'
+
     mbsize = str(int(size / (2 ** 20)))
     msg = "creating swap file '%s' of %sMB" % (swapfile, mbsize)
     fpath = os.path.sep.join([target, swapfile])
@@ -73,10 +148,9 @@ def setup_swapfile(target, fstab=None, swapfile=None, size=None, maxsize=None):
         with util.LogTimer(LOG.debug, msg):
             util.subp(
                 ['sh', '-c',
-                 ('rm -f "$1" && umask 0066 && '
-                  '{ fallocate -l "${2}M" "$1" || '
-                  '  dd if=/dev/zero "of=$1" bs=1M "count=$2"; } && '
-                  'mkswap "$1" || { r=$?; rm -f "$1"; exit $r; }'),
+                 ('rm -f "$1" && umask 0066 && truncate -s 0 "$1" && '
+                  '{ chattr +C "$1" || true; } && ') + allocate_cmd +
+                 (' && mkswap "$1" || { r=$?; rm -f "$1"; exit $r; }'),
                  'setup_swap', fpath, mbsize])
     except Exception:
         LOG.warn("failed %s" % msg)
diff --git a/debian/changelog b/debian/changelog
index 67c16c4..246ea94 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,43 @@
+curtin (20.2-0ubuntu1~20.04.1) focal; urgency=medium
+
+  * New upstream release. (LP: #1896947)
+    - Release 20.2 [Paride Legovini]
+    - Get debian/rules in sync with 0.1.0~bzr470-0ubuntu1 [Paride Legovini]
+    - Fix the py3 pylint ci run [Paride Legovini]
+    - vmtest: Fix multiple issues with vmtest on master
+    - Refactor uefi_remove_duplicates into find/remove functions for reuse
+    - distro: run apt-get clean after dist-upgrade, install, upgrade
+    - curthooks: UEFI remove dupes: don't remove BootCurrent, config option
+    - Pin the dependency on pyrsistent [Paride Legovini]
+    - restore default of grub.update_nvram to True in install_grub
+      [Michael Hudson-Doyle]
+    - block: disk_to_byid_path handle missing /dev/disk/by-id directory
+    - UEFI: Handle missing BootCurrent entry when reordering UEFI entries
+    - dasd: fix off-by-one device_id devno range check [Paride Legovini]
+    - curthooks: uefi_find_grub_device_ids handle type:mount without path
+    - netplan openvswitch yaml changed
+    - tools/curtainer: do not wait for snapd.seeded.service
+    - tools/curtainer: enable using ubuntu-minimal images
+    - vmtests: add Groovy [Paride Legovini]
+    - Drop the Eoan vmtests (EOL) [Paride Legovini]
+    - tools: rename remove-vmtest-release to vmtest-remove-release
+    - Snooze the tests failing because of LP: #1861941 for two more months
+      [Paride Legovini]
+    - LP: #1671951 is Fix Released => Drop the PPA [Paride Legovini]
+    - swaps: handle swapfiles on btrfs
+    - curtainer: fail is masking of zfs-mount or zfs-share fails
+      [Paride Legovini]
+    - multipath: handle multipath nvme name fields correctly
+    - curtainer: mask the zfs-mount and zfs-share services [Paride Legovini]
+    - tools/jenkins-runner: shuffle test-cases to randomize load
+      [Paride Legovini]
+    - Add Trusty/UEFI/HWE-X vmtest, drop realpath add, drop shell code
+    - LP: #1881977 - Install realpath on Trusty UEFI. [Lee Trager]
+    - vmtests: fix PreservePartitionWipeVg storage config
+    - Fix mdraid name creates broken configuration [James Falcon]
+
+ -- Paride Legovini <paride.legovini@xxxxxxxxxxxxx>  Tue, 29 Sep 2020 14:22:54 +0200
+
 curtin (20.1-2-g42a9667f-0ubuntu1~20.04.1) focal; urgency=medium
 
   * New upstream snapshot. (LP: #1881003)
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index 72cd683..ec8a109 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -226,6 +226,42 @@ 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
+from the network leaving MAAS in control.  On UEFI systems, after installing
+a bootloader the systems BootOrder may be updated to boot from the new entry.
+This breaks MAAS control over the system as all subsequent reboots of the node
+will no longer boot over the network.  Therefore, if ``reorder_uefi`` is True
+curtin will modify the UEFI BootOrder settings to place the currently booted
+entry (BootCurrent) to the first option after installing the new target OS into
+the UEFI boot menu.  The result is that the system will boot from the same
+device that it booted to run curtin; for MAAS this will be a network device.
+
+On some UEFI systems the BootCurrent entry may not be present.  This can
+cause a system to not boot to the same device that it was previously booting.
+If BootCurrent is not present, curtin will update the BootOrder such that
+all Network related entries are placed before the newly installed boot entry and
+all other entries are placed at the end.  This enables the system to network
+boot first and on failure will boot the most recently installed entry.
+
+This setting is ignored if *update_nvram* is False.
+
+**reorder_uefi_force_fallback**: *<boolean: default False>*
+
+The fallback reodering mechanism is only active if BootCurrent is not present
+in the efibootmgr output.  The fallback reordering method may be enabled
+even if BootCurrent is present if *reorder_uefi_force_fallback* is True.
+
+This setting is ignored if *update_nvram* or *reorder_uefi* are False.
+
+**remove_duplicate_entries**: <*boolean: default True>*
+
+When curtin updates UEFI NVRAM it will remove duplicate entries that are
+present in the UEFI menu.  If you do not wish for curtin to remove duplicate
+entries setting *remove_duplicate_entries* to False.
+
+This setting is ignored if *update_nvram* is False.
 
 **Example**::
 
@@ -235,6 +271,7 @@ use Grub defaults.
      replace_linux_default: False
      update_nvram: True
      terminal: serial
+     remove_duplicate_entries: True
 
 **Default terminal value, GRUB_TERMINAL=console**::
 
@@ -264,6 +301,12 @@ use Grub defaults.
      probe_additional_os: True
      terminal: unmodified
 
+**Enable Fallback UEFI Reordering**::
+
+  grub:
+     reorder_uefi: true
+     reorder_uefi_force_fallback: true
+
 
 http_proxy
 ~~~~~~~~~~
@@ -752,13 +795,27 @@ Configure the max size of the swapfile, defaults to 8GB
 Configure the exact size of the swapfile.  Setting ``size`` to 0 will
 disable swap.
 
+**force**: *<boolean>*
+
+Force the creation of swapfile even if curtin detects it may not work.
+In some target filesystems, e.g. btrfs, xfs, zfs, the use of a swap file has
+restrictions.  If curtin detects that there may be issues it will refuse
+to create the swapfile.  Users can force creation of a swapfile by passing
+``force: true``.  A forced swapfile may not be used by the target OS and could
+log cause an error.
+
 **Example**::
 
   swap:
     filename: swap.img
-    size: None
+    size: 1GB
     maxsize: 4GB
 
+  swap:
+    filename: btrfs_swapfile.img
+    size: 1GB
+    force: true
+
 
 system_upgrade
 ~~~~~~~~~~~~~~
diff --git a/examples/tests/basic.yaml b/examples/tests/basic.yaml
index 71730c0..82f5ad1 100644
--- a/examples/tests/basic.yaml
+++ b/examples/tests/basic.yaml
@@ -1,4 +1,8 @@
 showtrace: true
+swap:
+    filename: /btrfs/btrfsswap.img
+    size: 1GB
+    maxsize: 1GB
 storage:
     version: 1
     config:
diff --git a/examples/tests/basic_scsi.yaml b/examples/tests/basic_scsi.yaml
index 51f5236..fd28bbe 100644
--- a/examples/tests/basic_scsi.yaml
+++ b/examples/tests/basic_scsi.yaml
@@ -1,4 +1,8 @@
 showtrace: true
+swap:
+    filename: /btrfs/btrfsswap.img
+    size: 1GB
+    maxsize: 1GB
 storage:
     version: 1
     config:
diff --git a/examples/tests/network_v2_ovs.yaml b/examples/tests/network_v2_ovs.yaml
index 6d1dc3d..0d063ce 100644
--- a/examples/tests/network_v2_ovs.yaml
+++ b/examples/tests/network_v2_ovs.yaml
@@ -8,35 +8,6 @@ network:
             match:
                 macaddress: '52:54:00:12:34:00'
             set-name: eth0
-        eth1:
-            match:
-                macaddress: '52:54:00:12:34:02'
-            set-name: eth1
-        eth2:
-            match:
-                macaddress: '52:54:00:12:34:04'
-            set-name: eth2
-    openvswitch:
-      bridges:
-        br-int:
-          fail-mode: secure
-          datapath_type: system
-          stp: false
-          rstp: false
-          mcast-snooping: false
-          controller:
-            addresses:
-              - tcp:127.0.0.1:6653
-          protocols:
-            - OpenFlow10
-            - OpenFlow12
-            - OpenFlow13
-          ports:
-            patch-tun:
-              type: patch
-              options:
-                peer: patch-int
-            eth1:
-              tag: 2
-            eth2:
-              tag: 2
+    bridges:
+      br-empty:
+        openvswitch: {}
diff --git a/examples/tests/nvme_bcache.yaml b/examples/tests/nvme_bcache.yaml
index 4fefd94..ed705c4 100644
--- a/examples/tests/nvme_bcache.yaml
+++ b/examples/tests/nvme_bcache.yaml
@@ -1,8 +1,7 @@
 showtrace: true
 storage:
   config:
-  - grub_device: true
-    id: sda
+  - id: sda
     model: LOGICAL VOLUME
     name: sda
     ptable: gpt
@@ -23,7 +22,7 @@ storage:
     type: disk
     wipe: superblock
   - device: sda
-    flag: boot
+    grub_device: true
     id: sda-part1
     name: sda-part1
     number: 1
@@ -33,7 +32,6 @@ storage:
     uuid: 9f0fda16-c096-4cee-82ac-4f5f294253a2
     wipe: superblock
   - device: sda
-    flag: boot
     id: sda-part2
     name: sda-part2
     number: 2
diff --git a/examples/tests/preserve-partition-wipe-vg.yaml b/examples/tests/preserve-partition-wipe-vg.yaml
index 97686e1..27a4235 100644
--- a/examples/tests/preserve-partition-wipe-vg.yaml
+++ b/examples/tests/preserve-partition-wipe-vg.yaml
@@ -38,7 +38,6 @@ storage:
     grub_device: true
     type: disk
     id: disk-sda
-    wipe: superblock
   - serial: disk-b
     name: disk-b
     grub_device: false
diff --git a/examples/tests/raid5boot.yaml b/examples/tests/raid5boot.yaml
index b1df21d..d8afe7f 100644
--- a/examples/tests/raid5boot.yaml
+++ b/examples/tests/raid5boot.yaml
@@ -42,7 +42,7 @@ storage:
        size: 3GB
        device: sdc
      - id: mddevice
-       name: md0
+       name: os-raid1
        type: raid
        raidlevel: 5
        devices:
diff --git a/helpers/common b/helpers/common
index 5638d39..4afb6a1 100644
--- a/helpers/common
+++ b/helpers/common
@@ -29,19 +29,6 @@ EOF
     [ $# -eq 0 ] || echo "$@"
 }
 
-grub_install_usage() {
-    cat <<EOF
-Usage: ${0##*/} [ options ] mount-point target-dev
-
-   perform grub-install with mount-point onto target-dev.
-
-   options:
-          --uefi           install grub-efi instead of grub-pc
-          --update-nvram   request grub to update nvram
-EOF
-    [ $# -eq 0 ] || echo "$@"
-}
-
 cleanup() {
     if [ -d "$TEMP_D" ]; then
         rm -Rf "$TEMP_D"
@@ -480,569 +467,4 @@ getsize() {
     fi
 }
 
-is_md() {
-    case "${1##*/}" in
-        md[0-9]) return 0;;
-    esac
-    return 1
-}
-
-get_carryover_params() {
-    local cmdline=" $1 " extra="" lead="" carry_extra="" carry_lead=""
-    # return a string to append to installed systems boot parameters
-    # it may include a '--' after a '---'
-    # see LP: 1402042 for some history here.
-    # this is similar to 'user-params' from d-i
-    local preferred_sep="---"  # KERNEL_CMDLINE_COPY_TO_INSTALL_SEP
-    local legacy_sep="--"
-    case "$cmdline" in
-        *\ ${preferred_sep}\ *)
-            extra=${cmdline#* ${preferred_sep} }
-            lead=${cmdline%% ${preferred_sep} *}
-            ;;
-        *\ ${legacy_sep}\ *)
-            extra="${cmdline#* ${legacy_sep} }"
-            lead=${cmdline%% ${legacy_sep} *}
-            ;;
-        *)
-            extra=""
-            lead="$cmdline"
-            ;;
-    esac
-
-    if [ -n "$extra" ]; then
-        carry_extra=$(set -f;
-            c="";
-            for p in $extra; do
-                case "$p" in
-                    (BOOTIF=*|initrd=*|BOOT_IMAGE=*) continue;;
-                esac
-                c="$c $p";
-            done
-            echo "${c# }"
-        )
-    fi
-
-    # these get copied even if they werent after the separator
-    local padded=" $carry_extra "
-    carry_lead=$(set -f;
-        padded=" ${carry_extra} "
-        c=""
-        for p in $lead; do
-            # skip any that are already in carry_extra
-            [ "${padded#* $p }" != "$padded" ] && continue
-            case "$p" in
-                (console=*) c="$c $p";;
-            esac
-        done
-        echo "${c# }"
-    )
-    [ -n "${carry_lead}" -a -n "${carry_extra}" ] &&
-        carry_lead="${carry_lead} "
-    _RET="${carry_lead}${carry_extra}"
-}
-
-shell_config_update() {
-    # shell_config_update(file, name, value)
-    # update variable 'name' setting value to 'val' in shell syntax 'file'.
-    # if 'name' is not present, then append declaration.
-    local file="$1" name="$2" val="$3"
-    if ! [ -f "$file" ] || ! grep -q "^$name=" "$file"; then
-        debug 2 "appending to $file shell $name=\"$val\""
-        echo "$name=\"$val\"" >> "$file"
-        return
-    fi
-    local cand="" del=""
-    for cand in "|" "," "/"; do
-        [ "${val#*${del}}" = "${val}" ] && del="$cand" && break
-    done
-    [ -n "$del" ] || {
-        error "Couldn't find a sed delimiter for '$val'";
-        return 1;
-    }
-
-    sed -i -e "s${del}^$name=.*${del}$name=\"$val\"${del}" "$file" ||
-        { error "Failed editing '$file' to set $name=$val"; return 1; }
-    debug 2 "updated $file to set $name=\"$val\""
-    return 0
-}
-
-apply_grub_cmdline_linux_default() {
-    local mp="$1" newargs="$2" edg="${3:-etc/default/grub}"
-    local gcld="GRUB_CMDLINE_LINUX_DEFAULT"
-    debug 1 "setting $gcld to '$newargs' in $edg"
-    shell_config_update "$mp/$edg" "$gcld" "$newargs" || {
-        error "Failed to set '$gcld=$newargs' in $edg"
-        return 1
-    }
-}
-
-get_parent_disk() {
-    # Look up the parent /dev path via sysfs.  Using the partition
-    # kname (nvme0n1p1), construct a /sys/class/block path, use
-    # realpath to resolve this to an absolute path which includes
-    # the parent:
-    #   /sys/devices/pci0000:00/*/*/nvme/nvme0/nvme0n1/nvme0n1p1
-    # dirname to extract the parent, then read the 'dev' entry
-    #   /sys/devices/pci0000:00/*/*/nvme/nvme0/nvme0n1/dev
-    # which contains the MAJOR:MINOR value and construct a /dev/block
-    # path which is a symbolic link that udev constructs that points
-    # to the real device name and use realpath to return the absolute path.
-    #   /dev/block/259:0 -> ../nvme0n1
-    local devpath="${1}"
-    local kname=$(basename "$devpath")
-    local syspath=$(realpath "/sys/class/block/$kname")
-    local disksyspath=$(dirname "$syspath")
-    local diskmajmin=$(cat "${disksyspath}/dev")
-    local diskdevpath=$(realpath "/dev/block/${diskmajmin}")
-    echo $diskdevpath
-}
-
-install_grub() {
-    local long_opts="uefi,update-nvram,os-family:"
-    local getopt_out="" mp_efi=""
-    getopt_out=$(getopt --name "${0##*/}" \
-        --options "" --long "${long_opts}" -- "$@") &&
-        eval set -- "${getopt_out}"
-
-    local uefi=0 update_nvram=0 os_family=""
-
-    while [ $# -ne 0 ]; do
-        cur="$1"; next="$2";
-        case "$cur" in
-            --os-family) os_family=${next};;
-            --uefi) uefi=$((${uefi}+1));;
-            --update-nvram) update_nvram=$((${update_nvram}+1));;
-            --) shift; break;;
-        esac
-        shift;
-    done
-
-    [ $# -lt 2 ] && { grub_install_usage "must provide mount-point and target-dev" 1>&2; return 1; }
-
-    local mp="$1"
-    local cmdline tmp r=""
-    shift
-    local grubdevs
-    grubdevs=( "$@" )
-    if [ "${#grubdevs[@]}" = "1" -a "${grubdevs[0]}" = "none" ]; then
-        grubdevs=( )
-    fi
-    debug 1 "grubdevs: [${grubdevs[@]}]"
-
-    # find the mp device
-    local mp_dev="" fstype=""
-    mp_dev=$(awk -v "MP=$mp" '$2 == MP { print $1 }' /proc/mounts) || {
-        error "unable to determine device for mount $mp";
-        return 1;
-    }
-    debug 1 "/proc/mounts shows $mp_dev is mounted at $mp"
-
-    fstype=$(awk -v MP=$mp '$2 == MP { print $3 }' /proc/mounts) || {
-        error "unable to fstype for mount $mp";
-        return 1;
-    }
-
-    [ -z "$mp_dev" ] && {
-        error "did not find '$mp' in /proc/mounts"
-        cat /proc/mounts 1>&2
-        return 1
-    }
-    # check if parsed mount point is a block device
-    # error unless fstype is zfs, where entry will not point to block device.
-    if ! [ -b "$mp_dev" ] && [ "$fstype" != "zfs" ]; then
-        # error unless mp is zfs, entry doesn't point to block devs
-        error "$mp_dev ($fstype) is not a block device!"; return 1;
-    fi
-
-    local os_variant=""
-    if [ -e "${mp}/etc/os-release" ]; then
-        os_variant=$(chroot "$mp" \
-                     /bin/sh -c 'echo $(. /etc/os-release; echo $ID)')
-    else
-        # Centos6 doesn't have os-release, so check for centos/redhat release
-        # looks like: CentOS release 6.9 (Final)
-        for rel in $(ls ${mp}/etc/*-release); do
-            os_variant=$(awk '{print tolower($1)}' $rel)
-            [ -n "$os_variant" ] && break
-        done
-    fi
-    [ $? != 0 ] &&
-        { error "Failed to read ID from $mp/etc/os-release"; return 1; }
-
-    local rhel_ver=""
-    case $os_variant in
-        debian|ubuntu) os_family="debian";;
-        centos|rhel)
-            os_family="redhat"
-            rhel_ver=$(chroot "$mp" rpm -E '%rhel')
-        ;;
-    esac
-
-    # ensure we have both settings, family and variant are needed
-    [ -n "${os_variant}" -a -n "${os_family}" ] ||
-        { error "Failed to determine os variant and family"; return 1; }
-
-    # get target arch
-    local target_arch="" r="1"
-    case $os_family in
-        debian)
-            target_arch=$(chroot "$mp" dpkg --print-architecture)
-            r=$?
-            ;;
-        redhat)
-            target_arch=$(chroot "$mp" rpm -E '%_arch')
-            r=$?
-            ;;
-    esac
-    [ $r -eq 0 ] || {
-        error "failed to get target architecture [$r]"
-        return 1;
-    }
-
-    # grub is not the bootloader you are looking for
-    if [ "${target_arch}" = "s390x" ]; then
-        return 0;
-    fi
-
-    # set correct grub package
-    local grub_name=""
-    local grub_target=""
-    case "$target_arch" in
-        i386|amd64)
-            # debian
-            grub_name="grub-pc"
-            grub_target="i386-pc"
-            ;;
-        x86_64)
-            case $rhel_ver in
-               6) grub_name="grub";;
-               7|8) grub_name="grub2-pc";;
-               *)
-                   error "Unknown rhel_ver [$rhel_ver]";
-                   return 1;
-               ;;
-            esac
-            grub_target="i386-pc"
-            ;;
-    esac
-    if [ "${target_arch#ppc64}" != "${target_arch}" ]; then
-        grub_name="grub-ieee1275"
-        grub_target="powerpc-ieee1275"
-    elif [ "$uefi" -ge 1 ]; then
-        grub_name="grub-efi-$target_arch"
-        case "$target_arch" in
-            x86_64)
-                # centos 7+, no centos6 support
-                # grub2-efi-x64 installs a signed grub bootloader while
-                # curtin uses grub2-efi-x64-modules to generate grubx64.efi.
-                # Either works just check that one of them is installed.
-                grub_name="grub2-efi-x64 grub2-efi-x64-modules"
-                grub_target="x86_64-efi"
-                ;;
-            amd64)
-                grub_target="x86_64-efi";;
-            arm64)
-                grub_target="arm64-efi";;
-        esac
-    fi
-
-    # check that the grub package is installed
-    local r=$?
-    case $os_family in
-        debian)
-            tmp=$(chroot "$mp" dpkg-query --show \
-                --showformat='${Status}\n' $grub_name)
-            r=$?
-            ;;
-        redhat)
-            tmp=$(chroot "$mp" rpm -q \
-                --queryformat='install ok installed\n' $grub_name)
-            r=$?
-            ;;
-    esac
-    if [ $r -ne 0 -a $r -ne 1 ]; then
-        error "failed to check if $grub_name installed";
-        return 1;
-    fi
-    # Check that any of the packages in $grub_name are installed. If
-    # grub_name contains multiple packages, as it does for CentOS 7+,
-    # only one package has to be installed for this to pass.
-    if ! echo $tmp | grep -q 'install ok installed'; then
-        debug 1 "$grub_name not installed, not doing anything"
-        return 1
-    fi
-
-    local grub_d="etc/default/grub.d"
-    # ubuntu writes to /etc/default/grub.d/50-curtin-settings.cfg
-    # to avoid tripping prompts on upgrade LP: #564853
-    local mygrub_cfg="$grub_d/50-curtin-settings.cfg"
-    case $os_family in
-        redhat)
-            grub_d="etc/default"
-            mygrub_cfg="etc/default/grub";;
-    esac
-    [ -d "$mp/$grub_d" ] || mkdir -p "$mp/$grub_d" ||
-        { error "Failed to create $grub_d"; return 1; }
-
-    # LP: #1179940 . The 50-cloudig-settings.cfg file is written by the cloud
-    # images build and defines/override some settings. Disable it.
-    local cicfg="$grub_d/50-cloudimg-settings.cfg"
-    if [ -f "$mp/$cicfg" ]; then
-       debug 1 "moved $cicfg out of the way"
-       mv "$mp/$cicfg" "$mp/$cicfg.disabled"
-    fi
-
-    # get the user provided / carry-over kernel arguments
-    local newargs=""
-    read cmdline < /proc/cmdline &&
-        get_carryover_params "$cmdline" && newargs="$_RET" || {
-        error "Failed to get carryover parrameters from cmdline"; 
-        return 1;
-    }
-    # always append rd.auto=1 for centos
-    case $os_family in
-        redhat)
-            newargs="${newargs:+${newargs} }rd.auto=1";;
-    esac
-    debug 1 "carryover command line params '$newargs'"
-
-    if [ "${REPLACE_GRUB_LINUX_DEFAULT:-1}" != "0" ]; then
-        apply_grub_cmdline_linux_default "$mp" "$newargs" || {
-            error "Failed to apply grub cmdline."
-            return 1
-        }
-    fi
-
-    if [ "${DISABLE_OS_PROBER:-1}" == "1" ]; then
-        {
-            echo "# Curtin disable grub os prober that might find other OS installs."
-            echo "GRUB_DISABLE_OS_PROBER=true"
-        } >> "$mp/$mygrub_cfg"
-    fi
-
-    if [ -n "${GRUB_TERMINAL}" ]; then
-        {
-            echo "# Curtin configured GRUB_TERMINAL value"
-            echo "GRUB_TERMINAL=${GRUB_TERMINAL}"
-        } >> "$mp/$mygrub_cfg"
-    fi
-
-    debug 1 "processing grubdevs values for expansion if needed"
-    local short="" bd="" grubdev grubdevs_new=""
-    grubdevs_new=()
-    for grubdev in "${grubdevs[@]}"; do
-        if is_md "$grubdev"; then
-            debug 1 "$grubdev is raid, find members"
-            short=${grubdev##*/}
-            for bd in "/sys/block/$short/slaves/"/*; do
-                [ -d "$bd" ] || continue
-                bd=${bd##*/}
-                bd="/dev/${bd%[0-9]}" # FIXME: part2bd
-                debug 1 "Add dev $bd to grubdevs_new"
-                grubdevs_new[${#grubdevs_new[@]}]="$bd"
-            done
-        else
-            debug 1 "Found dev [$grubdev] add to grubdevs_new"
-            grubdevs_new[${#grubdevs_new[@]}]="$grubdev"
-        fi
-    done
-    grubdevs=( "${grubdevs_new[@]}" )
-    debug 1 "updated grubdevs: [${grubdevs[@]}]"
-
-    if [ "$uefi" -ge 1 ]; then
-        nvram="--no-nvram"
-        if [ "$update_nvram" -ge 1 ]; then
-            nvram=""
-        fi
-        debug 1 "number of entries in grubdevs_new: ${#grubdevs[@]}"
-        if [ "${#grubdevs_new[@]}" -eq 1 ] && [ -b "${grubdevs_new[0]}" ]; then
-            debug 1 "Found a single entry in grubdevs, ${grubdevs_new[0]}"
-            # Currently UEFI can only be pointed to one system partition. If
-            # for some reason multiple install locations are given only use the
-            # first.
-            efi_dev="${grubdevs_new[0]}"
-            debug 1 "efi_dev=[${efi_dev}]"
-        elif [ "${#grubdevs_new[@]}" -gt 1 ]; then
-            error "Only one grub device supported on UEFI!"
-            exit 1
-        else
-            debug 1 "no storage config, parsing /proc/mounts with awk"
-            # If no storage configuration was given try to determine the system
-            # partition.
-            efi_dev=$(awk -v "MP=${mp}/boot/efi" '$2 == MP { print $1 }' /proc/mounts)
-            debug 1 "efi_dev=[${efi_dev}]"
-            [ -n "$efi_dev" ] || {
-                error "Failed to find efi device from parsing /proc/mounts"
-                return 1
-            }
-
-        fi
-        # The partition number of block device name need to be determined here
-        # so both getting the UEFI device from Curtin config and discovering it
-        # work.
-        efi_part_num=$(cat /sys/class/block/$(basename $efi_dev)/partition)
-        debug 1 "efi_part_num: $efi_part_num"
-        [ -n "${efi_part_num}" ] || {
-            error "Failed to determine $efi_dev partition number"
-            return 1
-        }
-        efi_disk=$(get_parent_disk "$efi_dev")
-        debug 1 "efi_disk: [$efi_disk]"
-        [ -b "${efi_disk}" ] || {
-            error "${efi_disk} is not a valid block device"
-            return 1
-        }
-        debug 1 "curtin uefi: installing ${grub_name} to: /boot/efi"
-        chroot "$mp" env DEBIAN_FRONTEND=noninteractive sh -exc '
-            echo "before grub-install efiboot settings"
-            efibootmgr -v || echo "WARN: efibootmgr exited $?"
-            bootid="$4"
-            efi_disk="$5"
-            efi_part_num="$6"
-            grubpost=""
-            grubmulti="/usr/lib/grub/grub-multi-install"
-            case $bootid in
-                debian|ubuntu)
-                    grubcmd="grub-install"
-                    if [ -e "${grubmulti}" ]; then
-                        grubcmd="${grubmulti}"
-                    fi
-                    dpkg-reconfigure "$1"
-                    update-grub
-                    ;;
-                centos|redhat|rhel)
-                    grubcmd="grub2-install"
-                    # RHEL uses redhat instead of the os_variant rhel for the bootid.
-                    if [ "$bootid" = "rhel" ]; then
-                        bootid="redhat"
-                    fi
-                    if [ -f /boot/efi/EFI/$bootid/grubx64.efi ]; then
-                        grubpost="grub2-mkconfig -o /boot/efi/EFI/$bootid/grub.cfg"
-                    else
-                        grubpost="grub2-mkconfig -o /boot/grub2/grub.cfg"
-                    fi
-                    ;;
-                *)
-                    echo "Unsupported OS: $bootid" 1>&2
-                    exit 1
-                    ;;
-            esac
-            # grub-install in 12.04 does not contain --no-nvram, --target,
-            # or --efi-directory
-            target="--target=$2"
-            no_nvram="$3"
-            efi_dir="--efi-directory=/boot/efi"
-            gi_out=$($grubcmd --help 2>&1)
-            echo "$gi_out" | grep -q -- "$no_nvram" || no_nvram=""
-            echo "$gi_out" | grep -q -- "--target" || target=""
-            echo "$gi_out" | grep -q -- "--efi-directory" || efi_dir=""
-
-            # Do not overwrite grubx64.efi if it already exists. grub-install
-            # generates grubx64.efi and overwrites any existing binary in
-            # /boot/efi/EFI/$bootid. This binary is not signed and will cause
-            # secure boot to fail.
-            #
-            # CentOS, RHEL, Fedora ship the signed boot loader in the package
-            # grub2-efi-x64 which installs the signed boot loader to
-            # /boot/efi/EFI/$bootid/grubx64.efi. All Curtin has to do is
-            # configure the firmware. This mirrors what Anaconda does.
-            #
-            # Debian and Ubuntu come with a patched version of grub which
-            # add the install flag --uefi-secure-boot which is enabled by
-            # default. When enabled if a signed version of grub exists on
-            # the filesystem it will be copied into /boot/efi/EFI/$bootid.
-            # Stock Ubuntu images do not ship with anything in /boot. Those
-            # files are generated by installing a kernel and grub.
-            echo "Dumping /boot/efi contents"
-            find /boot/efi
-            echo "Checking for existing EFI grub entry on ESP"
-            if [ "$grubcmd" = "grub2-install" -a -f /boot/efi/EFI/$bootid/grubx64.efi ]; then
-                if [ -z "$no_nvram" ]; then
-                    # UEFI firmware should be pointed to the shim if available to
-                    # enable secure boot.
-                    for boot_uefi in \
-                            /boot/efi/EFI/$bootid/shimx64.efi \
-                            /boot/efi/EFI/BOOT/BOOTX64.EFI \
-                            /boot/efi/EFI/$bootid/grubx64.efi; do
-                        if [ -f $boot_uefi ]; then
-                            break
-                        fi
-                    done
-                    loader=$(echo ${boot_uefi##/boot/efi} | sed "s|/|\\\|g")
-                    efibootmgr --create --write-signature --label $bootid \
-                        --disk $efi_disk --part $efi_part_num --loader $loader
-                    rc=$?
-                    [ "$rc" != "0" ] && { exit $rc; }
-                else
-                    echo "skip EFI entry creation due to \"$no_nvram\" flag"
-                fi
-            else
-                echo "No previous EFI grub entry found on ESP, use $grubcmd"
-                if [ "${grubcmd}" = "${grubmulti}" ]; then
-                    $grubcmd
-                else
-                    $grubcmd $target $efi_dir \
-                        --bootloader-id=$bootid --recheck $no_nvram
-                fi
-            fi
-            [ -z "$grubpost" ] || $grubpost;' \
-            -- "$grub_name" "$grub_target" "$nvram" "$os_variant" "$efi_disk" "$efi_part_num" </dev/null ||
-            { error "failed to install grub!"; return 1; }
-
-        chroot "$mp" sh -exc '
-            echo "after grub-install efiboot settings"
-            efibootmgr -v || echo "WARN: efibootmgr exited $?"
-            ' -- </dev/null ||
-            { error "failed to list efi boot entries!"; return 1; }
-    else
-        # Note: dpkg-reconfigure calls grub-install on ppc64
-        # this means that using '--no-nvram' below ends up
-        # failing very oddly.  This is because grub's post-inst
-        # runs grub-install with no target.  That ends up
-        # updating nvram badly, and then the grub-install would
-        # not fix it because of the no-nvram there.
-        debug 1 "curtin non-uefi: installing ${grub_name} to: ${grubdevs[*]}"
-        chroot "$mp" env DEBIAN_FRONTEND=noninteractive sh -exc '
-            pkg=$1; shift;
-            bootid=$1; shift;
-            bootver=$1; shift;
-            grubpost=""
-            case $bootid in
-                debian|ubuntu)
-                    grubcmd="grub-install"
-                    dpkg-reconfigure "$pkg"
-                    update-grub
-                    ;;
-                centos|redhat|rhel)
-                    case $bootver in
-                        6) grubcmd="grub-install";;
-                        7|8) grubcmd="grub2-install"
-                           grubpost="grub2-mkconfig -o /boot/grub2/grub.cfg";;
-                        *)
-                           echo "Unknown rhel_ver [$bootver]"
-                           exit 1
-                    esac
-                    ;;
-                *)
-                    echo "Unsupported OS: $bootid"; 1>&2
-                    exit 1
-                    ;;
-            esac
-            for d in "$@"; do
-                echo $grubcmd "$d";
-                $grubcmd "$d" || exit; done
-            [ -z "$grubpost" ] || $grubpost;' \
-            -- "${grub_name}" "${os_variant}" "${rhel_ver}" "${grubdevs[@]}" </dev/null ||
-            { error "failed to install grub!"; return 1; }
-    fi
-
-    if [ -n "${mp_efi}" ]; then
-        umount "$mp_efi" ||
-            { error "failed to unmount $mp_efi"; return 1; }
-    fi
-
-    return
-}
-
 # vi: ts=4 expandtab syntax=sh
diff --git a/helpers/install-grub b/helpers/install-grub
deleted file mode 100755
index e1bfb23..0000000
--- a/helpers/install-grub
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-# This file is part of curtin. See LICENSE file for copyright and license info.
-
-[ "${0%/*}" = "$0" ] && . ./common || . "${0%/*}/common"
-install_grub "$@"
-
-# vi: ts=4 expandtab syntax=sh
diff --git a/pylintrc b/pylintrc
index 167cff0..67a4e01 100644
--- a/pylintrc
+++ b/pylintrc
@@ -7,7 +7,7 @@ jobs=0
 # List of members which are set dynamically and missed by pylint inference
 # system, and so shouldn't trigger E1101 when accessed. Python regular
 # expressions are accepted.
-generated-members=DISTROS.*,parse_*,*_data
+generated-members=DISTROS\.
 
 # List of module names for which member attributes should not be checked
 # (useful for modules/projects where namespaces are manipulated during runtime
diff --git a/requirements.txt b/requirements.txt
index 9066728..6afa3b8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,6 @@ pyyaml
 oauthlib
 # For validation of storate configuration format
 jsonschema
+# Dependency of jsonschema.
+# Version 0.16.0 is the latest version supporting Python < 3.5.
+pyrsistent==0.16.0
diff --git a/tests/data/multipath-nvme.txt b/tests/data/multipath-nvme.txt
new file mode 100644
index 0000000..30b59e4
--- /dev/null
+++ b/tests/data/multipath-nvme.txt
@@ -0,0 +1 @@
+name='nqn.1994-11.com.samsung:nvme:PM1725a:HHHL:S3RVNA0J300208      :nsid.1' multipath='eui.335256304a3002080025384100000001' sysfs='nvme0n1' paths='1'
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index 2f5e51a..64a79ca 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -2,11 +2,13 @@
 
 import imp
 import importlib
+import logging
 import mock
 import os
 import random
 import shutil
 import string
+import sys
 import tempfile
 from unittest import TestCase, skipIf
 from contextlib import contextmanager
@@ -55,6 +57,7 @@ def skipUnlessJsonSchema():
 class CiTestCase(TestCase):
     """Common testing class which all curtin unit tests subclass."""
 
+    with_logs = False
     allowed_subp = False
     SUBP_SHELL_TRUE = "shell=True"
 
@@ -69,6 +72,21 @@ class CiTestCase(TestCase):
 
     def setUp(self):
         super(CiTestCase, self).setUp()
+        if self.with_logs:
+            # Create a log handler so unit tests can search expected logs.
+            self.logger = logging.getLogger()
+            if sys.version_info[0] == 2:
+                import StringIO
+                self.logs = StringIO.StringIO()
+            else:
+                import io
+                self.logs = io.StringIO()
+            formatter = logging.Formatter('%(levelname)s: %(message)s')
+            handler = logging.StreamHandler(self.logs)
+            handler.setFormatter(formatter)
+            self.old_handlers = self.logger.handlers
+            self.logger.handlers = [handler]
+
         if self.allowed_subp is True:
             util.subp = _real_subp
         else:
diff --git a/tests/unittests/test_block.py b/tests/unittests/test_block.py
index c62c153..78e331d 100644
--- a/tests/unittests/test_block.py
+++ b/tests/unittests/test_block.py
@@ -179,6 +179,18 @@ class TestBlock(CiTestCase):
         byid_path = block.disk_to_byid_path('/dev/sdb')
         self.assertEqual(mapping.get('/dev/sdb'), byid_path)
 
+    @mock.patch("curtin.block.os.path.exists")
+    def test__get_dev_disk_by_prefix_returns_empty_dict(self, m_exists):
+        """ _get_disk_by_prefix returns empty dict prefix dir does not exit """
+        m_exists.return_value = False
+        self.assertEqual({}, block._get_dev_disk_by_prefix("/dev/disk/by-id"))
+
+    @mock.patch("curtin.block.os.path.exists")
+    def test_disk_to_byid_returns_none_if_disk_byid_missing(self, m_exists):
+        """ disk_to_byid path returns None if /dev/disk/by-id is missing """
+        m_exists.return_value = False
+        self.assertEqual(None, block.disk_to_byid_path('/dev/sdb'))
+
 
 class TestSysBlockPath(CiTestCase):
     @mock.patch("curtin.block.get_blockdev_for_partition")
diff --git a/tests/unittests/test_block_multipath.py b/tests/unittests/test_block_multipath.py
index 2101eae..96cbcba 100644
--- a/tests/unittests/test_block_multipath.py
+++ b/tests/unittests/test_block_multipath.py
@@ -39,6 +39,20 @@ class TestMultipath(CiTestCase):
                          multipath.show_maps())
         self.m_subp.assert_called_with(expected, capture=True)
 
+    def test_show_maps_nvme(self):
+        """verify show_maps extracts mulitpath map data correctly."""
+        NVME_MP = multipath.util.load_file('tests/data/multipath-nvme.txt')
+        self.m_subp.return_value = (NVME_MP, "")
+        expected = ['multipathd', 'show', 'maps', 'raw', 'format',
+                    multipath.SHOW_MAPS_FMT]
+        self.assertEqual([
+            {'name':
+             ('nqn.1994-11.com.samsung:nvme:PM1725a:HHHL:S3RVNA0J300208      '
+              ':nsid.1'),
+             'multipath': 'eui.335256304a3002080025384100000001',
+             'sysfs': 'nvme0n1', 'paths': '1'}], multipath.show_maps())
+        self.m_subp.assert_called_with(expected, capture=True)
+
     def test_is_mpath_device_true(self):
         """is_mpath_device returns true if dev DM_UUID starts with mpath-"""
         self.m_udev.udevadm_info.return_value = {'DM_UUID': 'mpath-mpatha-foo'}
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index b768cdc..d954296 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -1779,6 +1779,7 @@ class TestRaidHandler(CiTestCase):
     def setUp(self):
         super(TestRaidHandler, self).setUp()
 
+        orig_md_path = block_meta.block.md_path
         basepath = 'curtin.commands.block_meta.'
         self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
         self.add_patch(basepath + 'util', 'm_util')
@@ -1787,6 +1788,10 @@ class TestRaidHandler(CiTestCase):
         self.add_patch(basepath + 'block', 'm_block')
         self.add_patch(basepath + 'udevadm_settle', 'm_uset')
 
+        # The behavior of this function is being directly tested in
+        # these tests, so we can't mock it
+        self.m_block.md_path = orig_md_path
+
         self.target = "my_target"
         self.config = {
             'storage': {
@@ -1850,12 +1855,40 @@ class TestRaidHandler(CiTestCase):
             block_meta.extract_storage_ordered_dict(self.config))
         self.m_util.load_command_environment.return_value = {'fstab': None}
 
+    def test_md_name(self):
+        input_to_result = [
+            ('md1', '/dev/md1'),
+            ('os-raid1', '/dev/md/os-raid1'),
+            ('md/os-raid1', '/dev/md/os-raid1'),
+            ('/dev/md1', '/dev/md1'),
+            ('/dev/md/os-raid1', '/dev/md/os-raid1'),
+            ('bad/path', ValueError)
+        ]
+        for index, test in enumerate(input_to_result):
+            param, expected = test
+            self.storage_config['mddevice']['name'] = param
+            try:
+                block_meta.raid_handler(self.storage_config['mddevice'],
+                                        self.storage_config)
+            except ValueError:
+                if param in ['bad/path']:
+                    continue
+                else:
+                    raise
+
+            actual = self.m_mdadm.mdadm_create.call_args_list[index][0][0]
+            self.assertEqual(
+                expected,
+                actual,
+                "Expected {} to result in mdadm being called with {}. "
+                "mdadm instead called with {}".format(param, expected, actual)
+            )
+
     def test_raid_handler(self):
         """ raid_handler creates raid device. """
         devices = [self.random_string(), self.random_string(),
                    self.random_string()]
         md_devname = '/dev/' + self.storage_config['mddevice']['name']
-        self.m_block.dev_path.return_value = '/dev/md0'
         self.m_getpath.side_effect = iter(devices)
         block_meta.raid_handler(self.storage_config['mddevice'],
                                 self.storage_config)
@@ -1868,7 +1901,6 @@ class TestRaidHandler(CiTestCase):
 
         devices = [self.random_string(), self.random_string(),
                    self.random_string()]
-        self.m_block.dev_path.return_value = '/dev/md0'
         self.m_getpath.side_effect = iter(devices)
         m_verify.return_value = True
         self.storage_config['mddevice']['preserve'] = True
@@ -1882,7 +1914,6 @@ class TestRaidHandler(CiTestCase):
         devices = [self.random_string(), self.random_string(),
                    self.random_string()]
         md_devname = '/dev/' + self.storage_config['mddevice']['name']
-        self.m_block.dev_path.return_value = '/dev/md0'
         self.m_getpath.side_effect = iter(devices)
         self.m_mdadm.md_check.return_value = True
         self.storage_config['mddevice']['preserve'] = True
@@ -1898,7 +1929,6 @@ class TestRaidHandler(CiTestCase):
         devices = [self.random_string(), self.random_string(),
                    self.random_string()]
         md_devname = '/dev/' + self.storage_config['mddevice']['name']
-        self.m_block.dev_path.return_value = '/dev/md0'
         self.m_getpath.side_effect = iter(devices)
         self.m_mdadm.md_check.side_effect = iter([False, True])
         self.storage_config['mddevice']['preserve'] = True
@@ -1916,7 +1946,6 @@ class TestRaidHandler(CiTestCase):
         devices = [self.random_string(), self.random_string(),
                    self.random_string()]
         md_devname = '/dev/' + self.storage_config['mddevice']['name']
-        self.m_block.dev_path.return_value = '/dev/md0'
         self.m_getpath.side_effect = iter(devices)
         self.m_mdadm.md_check.side_effect = iter([False, False])
         self.storage_config['mddevice']['preserve'] = True
@@ -2557,4 +2586,115 @@ class TestVerifyPtableFlag(CiTestCase):
                                       sfdisk_info=self.sfdisk_info_dos)
 
 
+class TestGetDevicePathsFromStorageConfig(CiTestCase):
+
+    def setUp(self):
+        super(TestGetDevicePathsFromStorageConfig, self).setUp()
+        base = 'curtin.commands.block_meta.'
+        self.add_patch(base + 'get_path_to_storage_volume', 'mock_getpath')
+        self.add_patch(base + 'os.path.exists', 'm_exists')
+        self.m_exists.return_value = True
+        self.mock_getpath.side_effect = self._getpath
+        self.prefix = '/test/dev/'
+        self.config = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {'id': 'sda',
+                     'type': 'disk',
+                     'name': 'main_disk',
+                     'ptable': 'gpt',
+                     'serial': 'disk-a'},
+                    {'id': 'disk-sda-part-1',
+                     'type': 'partition',
+                     'device': 'sda',
+                     'name': 'bios_boot',
+                     'number': 1,
+                     'size': '1M',
+                     'flag': 'bios_grub'},
+                    {'id': 'disk-sda-part-2',
+                     'type': 'partition',
+                     'device': 'sda',
+                     'number': 2,
+                     'size': '5GB'},
+                ],
+            }
+        }
+        self.disk1 = self.config['storage']['config'][0]
+        self.part1 = self.config['storage']['config'][1]
+        self.part2 = self.config['storage']['config'][2]
+        self.sconfig = self._sconfig(self.config)
+
+    def _sconfig(self, config):
+        return block_meta.extract_storage_ordered_dict(config)
+
+    def _getpath(self, item_id, _sconfig):
+        return self.prefix + item_id
+
+    def test_devpath_selects_disks_partitions_with_wipe_setting(self):
+        self.disk1['wipe'] = 'superblock'
+        self.part1['wipe'] = 'superblock'
+        self.sconfig = self._sconfig(self.config)
+
+        expected_devpaths = [
+            self.prefix + self.disk1['id'], self.prefix + self.part1['id']]
+        result = block_meta.get_device_paths_from_storage_config(self.sconfig)
+        self.assertEqual(sorted(expected_devpaths), sorted(result))
+        self.assertEqual([
+            call(self.disk1['id'], self.sconfig),
+            call(self.part1['id'], self.sconfig)],
+            self.mock_getpath.call_args_list)
+        self.assertEqual(
+            sorted([call(devpath) for devpath in expected_devpaths]),
+            sorted(self.m_exists.call_args_list))
+
+    def test_devpath_raises_exception_if_wipe_and_preserve_set(self):
+        self.disk1['wipe'] = 'superblock'
+        self.disk1['preserve'] = True
+        self.sconfig = self._sconfig(self.config)
+
+        with self.assertRaises(RuntimeError):
+            block_meta.get_device_paths_from_storage_config(self.sconfig)
+        self.assertEqual([], self.mock_getpath.call_args_list)
+        self.assertEqual([], self.m_exists.call_args_list)
+
+    def test_devpath_check_boolean_value_if_wipe_and_preserve_set(self):
+        self.disk1['wipe'] = 'superblock'
+        self.disk1['preserve'] = False
+        self.sconfig = self._sconfig(self.config)
+
+        expected_devpaths = [self.prefix + self.disk1['id']]
+        result = block_meta.get_device_paths_from_storage_config(self.sconfig)
+        self.assertEqual(expected_devpaths, result)
+        self.assertEqual(
+            [call(self.disk1['id'], self.sconfig)],
+            self.mock_getpath.call_args_list)
+        self.assertEqual(
+            sorted([call(devpath) for devpath in expected_devpaths]),
+            sorted(self.m_exists.call_args_list))
+
+    def test_devpath_check_preserved_devices_skipped(self):
+        self.disk1['preserve'] = True
+        self.sconfig = self._sconfig(self.config)
+
+        result = block_meta.get_device_paths_from_storage_config(self.sconfig)
+        self.assertEqual([], result)
+        self.assertEqual([], self.mock_getpath.call_args_list)
+        self.assertEqual([], self.m_exists.call_args_list)
+
+    def test_devpath_check_missing_path_devices_skipped(self):
+        self.disk1['wipe'] = 'superblock'
+        self.sconfig = self._sconfig(self.config)
+
+        self.m_exists.return_value = False
+        result = block_meta.get_device_paths_from_storage_config(self.sconfig)
+        self.assertEqual([], result)
+        self.assertEqual(
+            [call(self.disk1['id'], self.sconfig)],
+            self.mock_getpath.call_args_list)
+        self.assertEqual(
+            [call(self.prefix + self.disk1['id'])],
+            self.m_exists.call_args_list)
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index 2349456..e5fead3 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -1,5 +1,6 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
+import copy
 import os
 from mock import call, patch
 import textwrap
@@ -10,7 +11,7 @@ from curtin import distro
 from curtin import util
 from curtin import config
 from curtin.reporter import events
-from .helpers import CiTestCase, dir2dict, populate_dir
+from .helpers import CiTestCase, dir2dict, populate_dir, random
 
 
 class TestGetFlashKernelPkgs(CiTestCase):
@@ -531,12 +532,55 @@ class TestSetupZipl(CiTestCase):
             content)
 
 
+class EfiOutput(object):
+
+    def __init__(self, current=None, order=None, entries=None):
+        self.entries = {}
+        if entries:
+            for entry in entries:
+                self.entries.update(entry)
+        self.current = current
+        self.order = order
+        if not order and self.entries:
+            self.order = sorted(self.entries.keys())
+
+    def add_entry(self, bootnum=None, name=None, path=None, current=False):
+        if not bootnum:
+            bootnum = "%04x" % random.randint(0, 1000)
+        if not name:
+            name = CiTestCase.random_string()
+        if not path:
+            path = ''
+        if bootnum not in self.entries:
+            self.entries[bootnum] = {'name': name, 'path': path}
+            if not self.order:
+                self.order = []
+            self.order.append(bootnum)
+        if current:
+            self.current = bootnum
+
+    def set_order(self, new_order):
+        self.order = new_order
+
+    def as_dict(self):
+        output = {}
+        if self.current:
+            output['current'] = self.current
+        if self.order:
+            output['order'] = self.order
+        output['entries'] = self.entries
+        return output
+
+
 class TestSetupGrub(CiTestCase):
 
+    with_logs = True
+
     def setUp(self):
         super(TestSetupGrub, self).setUp()
         self.target = self.tmp_dir()
         self.distro_family = distro.DISTROS.debian
+        self.variant = 'ubuntu'
         self.add_patch('curtin.distro.lsb_release', 'mock_lsb_release')
         self.mock_lsb_release.return_value = {'codename': 'xenial'}
         self.add_patch('curtin.util.is_uefi_bootable',
@@ -556,7 +600,8 @@ class TestSetupGrub(CiTestCase):
         updated_cfg = {
             'install_devices': ['/dev/vdb']
         }
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        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=updated_cfg)
 
@@ -588,7 +633,8 @@ class TestSetupGrub(CiTestCase):
             },
         }
         m_exists.return_value = True
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        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={'install_devices': ['/dev/vdb']})
@@ -638,7 +684,8 @@ class TestSetupGrub(CiTestCase):
         }
         m_exists.return_value = True
         m_is_valid_device.side_effect = (False, True, False, True)
-        curthooks.setup_grub(cfg, self.target, osfamily=distro.DISTROS.redhat)
+        curthooks.setup_grub(cfg, self.target, osfamily=distro.DISTROS.redhat,
+                             variant='centos')
         self.m_install_grub.assert_called_with(
             ['/dev/vdb1'], self.target, uefi=True,
             grubcfg={'update_nvram': False, 'install_devices': ['/dev/vdb1']}
@@ -650,7 +697,8 @@ class TestSetupGrub(CiTestCase):
                 'install_devices': None,
             },
         }
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
         self.m_install_grub.assert_called_with(
             ['none'], self.target, uefi=False,
             grubcfg={'install_devices': None}
@@ -681,7 +729,8 @@ class TestSetupGrub(CiTestCase):
                 }
             }
         }
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        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=True, grubcfg=cfg.get('grub')
         )
@@ -721,7 +770,8 @@ class TestSetupGrub(CiTestCase):
             }
         }
         self.mock_haspkg.return_value = False
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
 
         expected_calls = [
             call(['efibootmgr', '-B', '-b', '0001'],
@@ -762,70 +812,304 @@ class TestSetupGrub(CiTestCase):
             }
         }
         self.mock_haspkg.return_value = False
-        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
         self.assertEquals([
             call(['efibootmgr', '-o', '0001,0000'], target=self.target)],
             self.mock_subp.call_args_list)
 
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_grub_install_uefi_reorders_no_current_new_entry(self):
+        self.add_patch('curtin.distro.install_packages', 'mock_install')
+        self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
+        self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
+        self.mock_is_uefi_bootable.return_value = True
+        cfg = {
+            'grub': {
+                'install_devices': ['/dev/vdb'],
+                'update_nvram': True,
+                'remove_old_uefi_loaders': False,
+                'reorder_uefi': True,
+            },
+        }
+
+        # Single existing entry 0001
+        efi_orig = EfiOutput()
+        efi_orig.add_entry(bootnum='0001', name='centos')
 
-class TestUefiRemoveDuplicateEntries(CiTestCase):
+        # After install add a second entry, 0000 to the front of order
+        efi_post = copy.deepcopy(efi_orig)
+        efi_post.add_entry(bootnum='0000', name='ubuntu')
+        efi_post.set_order(['0000', '0001'])
 
-    def setUp(self):
-        super(TestUefiRemoveDuplicateEntries, self).setUp()
-        self.target = self.tmp_dir()
-        self.add_patch('curtin.util.get_efibootmgr', 'm_efibootmgr')
-        self.add_patch('curtin.util.subp', 'm_subp')
+        # After reorder we should have the target install first
+        efi_final = copy.deepcopy(efi_post)
+
+        self.mock_efibootmgr.side_effect = iter([
+            efi_orig.as_dict(),   # collect original order before install
+            efi_orig.as_dict(),   # remove_old_loaders query (no change)
+            efi_post.as_dict(),   # efi table after grub install, (changed)
+            efi_final.as_dict(),  # remove duplicates checks and finds reorder
+                                  # has changed
+        ])
+        self.mock_haspkg.return_value = False
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
+        logs = self.logs.getvalue()
+        print(logs)
+        self.assertEquals([], self.mock_subp.call_args_list)
+        self.assertIn("Using fallback UEFI reordering:", logs)
+        self.assertIn("missing 'BootCurrent' value", logs)
+        self.assertIn("Found new boot entries: ['0000']", logs)
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
-    def test_uefi_remove_duplicate_entries(self):
+    def test_grub_install_uefi_reorders_no_curr_same_size_order_no_match(self):
+        self.add_patch('curtin.distro.install_packages', 'mock_install')
+        self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
+        self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
+        self.add_patch('curtin.commands.curthooks.uefi_remove_old_loaders',
+                       'mock_remove_old_loaders')
+        self.mock_is_uefi_bootable.return_value = True
         cfg = {
             'grub': {
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
+                'remove_old_uefi_loaders': False,
+                'reorder_uefi': True,
             },
         }
-        self.m_efibootmgr.return_value = {
-            'current': '0000',
-            'entries': {
-                '0000': {
-                    'name': 'ubuntu',
-                    'path': (
-                        'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
-                },
-                '0001': {
-                    'name': 'ubuntu',
-                    'path': (
-                        'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
-                },
-                '0002': {  # Is not a duplicate because of unique path
-                    'name': 'ubuntu',
-                    'path': (
-                        'HD(2,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
-                },
-                '0003': {  # Is duplicate of 0000
-                    'name': 'ubuntu',
-                    'path': (
-                        'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
-                },
-            }
+
+        # Existing Custom Ubuntu, usb and cd/dvd entry, booting Ubuntu
+        efi_orig = EfiOutput()
+        efi_orig.add_entry(bootnum='0001', name='Ubuntu Deluxe Edition')
+        efi_orig.add_entry(bootnum='0002', name='USB Device')
+        efi_orig.add_entry(bootnum='0000', name='CD/DVD')
+        efi_orig.set_order(['0001', '0002', '0000'])
+
+        # after install existing ubuntu entry is reused, no change in order
+        efi_post = efi_orig
+
+        # after reorder, no change is made due to the installed distro variant
+        # string 'ubuntu' is not found in the boot entries so we retain the
+        # original efi order.
+        efi_final = efi_post
+
+        self.mock_efibootmgr.side_effect = iter([
+            efi_orig.as_dict(),   # collect original order before install
+            efi_orig.as_dict(),   # remove_old_loaders query
+            efi_post.as_dict(),   # reorder entries queries post install
+            efi_final.as_dict(),  # remove duplicates checks and finds reorder
+        ])
+
+        self.mock_haspkg.return_value = False
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
+
+        logs = self.logs.getvalue()
+        print(logs)
+        self.assertEquals([], self.mock_subp.call_args_list)
+        self.assertIn("Using fallback UEFI reordering:", logs)
+        self.assertIn("missing 'BootCurrent' value", logs)
+        self.assertIn("Current and Previous bootorders match", logs)
+        self.assertIn("Looking for installed entry variant=", logs)
+        self.assertIn("Did not find an entry with variant=", logs)
+        self.assertIn("No changes to boot order.", logs)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_grub_install_uefi_reorders_force_fallback(self):
+        self.add_patch('curtin.distro.install_packages', 'mock_install')
+        self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
+        self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
+        self.mock_is_uefi_bootable.return_value = True
+        cfg = {
+            'grub': {
+                'install_devices': ['/dev/vdb'],
+                'update_nvram': True,
+                'remove_old_uefi_loaders': True,
+                'reorder_uefi': True,
+                'reorder_uefi_force_fallback': True,
+            },
         }
+        # Single existing entry 0001 and set as current, which should avoid
+        # any fallback logic, but we're forcing fallback pack via config
+        efi_orig = EfiOutput()
+        efi_orig.add_entry(bootnum='0001', name='PXE', current=True)
+        print(efi_orig.as_dict())
+
+        # After install add a second entry, 0000 to the front of order
+        efi_post = copy.deepcopy(efi_orig)
+        efi_post.add_entry(bootnum='0000', name='ubuntu')
+        efi_post.set_order(['0000', '0001'])
+        print(efi_orig.as_dict())
+
+        # After reorder we should have the original boot entry 0001 as first
+        efi_final = copy.deepcopy(efi_post)
+        efi_final.set_order(['0001', '0000'])
+
+        self.mock_efibootmgr.side_effect = iter([
+            efi_orig.as_dict(),   # collect original order before install
+            efi_orig.as_dict(),   # remove_old_loaders query (no change)
+            efi_post.as_dict(),   # efi table after grub install, (changed)
+            efi_final.as_dict(),  # remove duplicates checks and finds reorder
+                                  # has changed
+        ])
 
-        curthooks.uefi_remove_duplicate_entries(cfg, self.target)
+        self.mock_haspkg.return_value = False
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
+        logs = self.logs.getvalue()
+        print(logs)
         self.assertEquals([
-            call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
-                 target=self.target),
-            call(['efibootmgr', '--bootnum=0003', '--delete-bootnum'],
-                 target=self.target)
-            ], self.m_subp.call_args_list)
+            call(['efibootmgr', '-o', '0001,0000'], target=self.target)],
+            self.mock_subp.call_args_list)
+        self.assertIn("Using fallback UEFI reordering:", logs)
+        self.assertIn("config 'reorder_uefi_force_fallback' is True", logs)
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
-    def test_uefi_remove_duplicate_entries_no_change(self):
+    def test_grub_install_uefi_reorders_network_first(self):
+        self.add_patch('curtin.distro.install_packages', 'mock_install')
+        self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
+        self.add_patch('curtin.util.get_efibootmgr', 'mock_efibootmgr')
+        self.mock_is_uefi_bootable.return_value = True
         cfg = {
             'grub': {
                 'install_devices': ['/dev/vdb'],
                 'update_nvram': True,
+                'remove_old_uefi_loaders': True,
+                'reorder_uefi': True,
+            },
+        }
+
+        # Existing ubuntu, usb and cd/dvd entry, booting ubuntu
+        efi_orig = EfiOutput()
+        efi_orig.add_entry(bootnum='0001', name='centos')
+        efi_orig.add_entry(bootnum='0002', name='Network')
+        efi_orig.add_entry(bootnum='0003', name='PXE')
+        efi_orig.add_entry(bootnum='0004', name='LAN')
+        efi_orig.add_entry(bootnum='0000', name='CD/DVD')
+        efi_orig.set_order(['0001', '0002', '0003', '0004', '0000'])
+        print(efi_orig.as_dict())
+
+        # after install we add an ubuntu entry, and grub puts it first
+        efi_post = copy.deepcopy(efi_orig)
+        efi_post.add_entry(bootnum='0007', name='ubuntu')
+        efi_post.set_order(['0007'] + efi_orig.order)
+        print(efi_post.as_dict())
+
+        # reorder must place all network devices first, then ubuntu, and others
+        efi_final = copy.deepcopy(efi_post)
+        expected_order = ['0002', '0003', '0004', '0007', '0001', '0000']
+        efi_final.set_order(expected_order)
+
+        self.mock_efibootmgr.side_effect = iter([
+            efi_orig.as_dict(),   # collect original order before install
+            efi_orig.as_dict(),   # remove_old_loaders query
+            efi_post.as_dict(),   # reorder entries queries post install
+            efi_final.as_dict(),  # remove duplicates checks and finds reorder
+        ])
+        self.mock_haspkg.return_value = False
+        curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family,
+                             variant=self.variant)
+        logs = self.logs.getvalue()
+        print(logs)
+        print('Number of bootmgr calls: %s' % self.mock_efibootmgr.call_count)
+        self.assertEquals([
+            call(['efibootmgr', '-o', '%s' % (",".join(expected_order))],
+                 target=self.target)],
+            self.mock_subp.call_args_list)
+        self.assertIn("Using fallback UEFI reordering:", logs)
+        self.assertIn("missing 'BootCurrent' value", logs)
+        self.assertIn("Looking for installed entry variant=", logs)
+        self.assertIn("found netboot entries: ['0002', '0003', '0004']", logs)
+        self.assertIn("found other entries: ['0001', '0000']", logs)
+        self.assertIn("found target entry: ['0007']", logs)
+
+
+class TestUefiRemoveDuplicateEntries(CiTestCase):
+
+    efibootmgr_output = {
+        'current': '0000',
+        'entries': {
+            '0000': {
+                'name': 'ubuntu',
+                'path': (
+                    'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
+            },
+            '0001': {  # Is duplicate of 0000
+                'name': 'ubuntu',
+                'path': (
+                    'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
+            },
+            '0002': {  # Is not a duplicate because of unique path
+                'name': 'ubuntu',
+                'path': (
+                    'HD(2,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
+            },
+            '0003': {  # Is duplicate of 0000
+                'name': 'ubuntu',
+                'path': (
+                    'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
             },
         }
+    }
+
+    def setUp(self):
+        super(TestUefiRemoveDuplicateEntries, self).setUp()
+        self.target = self.tmp_dir()
+        self.add_patch('curtin.util.get_efibootmgr', 'm_efibootmgr')
+        self.add_patch('curtin.util.subp', 'm_subp')
+        self.m_efibootmgr.return_value = copy.deepcopy(self.efibootmgr_output)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries(self):
+        grubcfg = {}
+        curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+        self.assertEquals([
+            call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
+                 target=self.target),
+            call(['efibootmgr', '--bootnum=0003', '--delete-bootnum'],
+                 target=self.target)
+            ], self.m_subp.call_args_list)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries_no_bootcurrent(self):
+        grubcfg = {}
+        efiout = copy.deepcopy(self.efibootmgr_output)
+        del efiout['current']
+        self.m_efibootmgr.return_value = efiout
+        curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+        self.assertEquals([
+            call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
+                 target=self.target),
+            call(['efibootmgr', '--bootnum=0003', '--delete-bootnum'],
+                 target=self.target)
+            ], self.m_subp.call_args_list)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries_disabled(self):
+        grubcfg = {
+            'remove_duplicate_entries': False,
+        }
+        curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+        self.assertEquals([], self.m_subp.call_args_list)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries_skip_bootcurrent(self):
+        grubcfg = {}
+        efiout = copy.deepcopy(self.efibootmgr_output)
+        efiout['current'] = '0003'
+        self.m_efibootmgr.return_value = efiout
+        curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
+        self.assertEquals([
+            call(['efibootmgr', '--bootnum=0000', '--delete-bootnum'],
+                 target=self.target),
+            call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
+                 target=self.target),
+            ], self.m_subp.call_args_list)
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries_no_change(self):
+        grubcfg = {}
         self.m_efibootmgr.return_value = {
             'current': '0000',
             'entries': {
@@ -846,8 +1130,7 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
                 },
             }
         }
-
-        curthooks.uefi_remove_duplicate_entries(cfg, self.target)
+        curthooks.uefi_remove_duplicate_entries(grubcfg, self.target)
         self.assertEquals([], self.m_subp.call_args_list)
 
 
@@ -1110,9 +1393,8 @@ class TestDetectRequiredPackages(CiTestCase):
                         {'type': 'static', 'address': '2001:1::1/64'}]}},
             2: {
                 'openvswitch': {
-                    'openvswitch': {
-                        'bridges': {
-                            'br-int': {'ports': {'eth15': {'tag': 2}}}}}},
+                    'bridges': {
+                        'br-int': {'openvswitch': {}}}},
                 'vlans': {
                     'vlans': {
                         'en-intra': {'id': 1, 'link': 'eno1', 'dhcp4': 'yes'},
@@ -1245,7 +1527,7 @@ class TestDetectRequiredPackages(CiTestCase):
             ({'network': {
                 'version': 2,
                 'items': ('openvswitch',)}},
-             ('openvswitch-switch', )),
+             ('bridge-utils', 'openvswitch-switch', )),
         ))
 
     def test_network_v2_detect_renderers(self):
@@ -1726,6 +2008,12 @@ class TestUefiFindGrubDeviceIds(CiTestCase):
                         'fstype': 'fat32',
                     },
                     {
+                        'id': 'vdb-part2-swap_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part2-swap_format',
+                        'options': '',
+                    },
+                    {
                         'id': 'vdb-part1_mount',
                         'type': 'mount',
                         'device': 'vdb-part1_format',
diff --git a/tests/unittests/test_distro.py b/tests/unittests/test_distro.py
index eb62dd8..380680c 100644
--- a/tests/unittests/test_distro.py
+++ b/tests/unittests/test_distro.py
@@ -65,7 +65,7 @@ class TestParseDpkgVersion(CiTestCase):
     def test_simple_native_package_version(self):
         """dpkg versions must have a -. If not present expect value error."""
         self.assertEqual(
-            {'major': 2, 'minor': 28, 'micro': 0, 'extra': None,
+            {'epoch': 0, 'major': 2, 'minor': 28, 'micro': 0, 'extra': None,
              'raw': '2.28', 'upstream': '2.28', 'name': 'germinate',
              'semantic_version': 22800},
             distro.parse_dpkg_version('2.28', name='germinate'))
@@ -73,7 +73,7 @@ class TestParseDpkgVersion(CiTestCase):
     def test_complex_native_package_version(self):
         dver = '1.0.106ubuntu2+really1.0.97ubuntu1'
         self.assertEqual(
-            {'major': 1, 'minor': 0, 'micro': 106,
+            {'epoch': 0, 'major': 1, 'minor': 0, 'micro': 106,
              'extra': 'ubuntu2+really1.0.97ubuntu1',
              'raw': dver, 'upstream': dver, 'name': 'debootstrap',
              'semantic_version': 100106},
@@ -82,14 +82,14 @@ class TestParseDpkgVersion(CiTestCase):
 
     def test_simple_valid(self):
         self.assertEqual(
-            {'major': 1, 'minor': 2, 'micro': 3, 'extra': None,
+            {'epoch': 0, 'major': 1, 'minor': 2, 'micro': 3, 'extra': None,
              'raw': '1.2.3-0', 'upstream': '1.2.3', 'name': 'foo',
              'semantic_version': 10203},
             distro.parse_dpkg_version('1.2.3-0', name='foo'))
 
     def test_simple_valid_with_semx(self):
         self.assertEqual(
-            {'major': 1, 'minor': 2, 'micro': 3, 'extra': None,
+            {'epoch': 0, 'major': 1, 'minor': 2, 'micro': 3, 'extra': None,
              'raw': '1.2.3-0', 'upstream': '1.2.3',
              'semantic_version': 123},
             distro.parse_dpkg_version('1.2.3-0', semx=(100, 10, 1)))
@@ -98,7 +98,8 @@ class TestParseDpkgVersion(CiTestCase):
         """upstream versions may have a hyphen."""
         cver = '18.2-14-g6d48d265-0ubuntu1'
         self.assertEqual(
-            {'major': 18, 'minor': 2, 'micro': 0, 'extra': '-14-g6d48d265',
+            {'epoch': 0, 'major': 18, 'minor': 2, 'micro': 0,
+             'extra': '-14-g6d48d265',
              'raw': cver, 'upstream': '18.2-14-g6d48d265',
              'name': 'cloud-init', 'semantic_version': 180200},
             distro.parse_dpkg_version(cver, name='cloud-init'))
@@ -107,11 +108,30 @@ class TestParseDpkgVersion(CiTestCase):
         """multipath tools has a + in it."""
         mver = '0.5.0+git1.656f8865-5ubuntu2.5'
         self.assertEqual(
-            {'major': 0, 'minor': 5, 'micro': 0, 'extra': '+git1.656f8865',
+            {'epoch': 0, 'major': 0, 'minor': 5, 'micro': 0,
+             'extra': '+git1.656f8865',
              'raw': mver, 'upstream': '0.5.0+git1.656f8865',
              'semantic_version': 500},
             distro.parse_dpkg_version(mver))
 
+    def test_package_with_epoch(self):
+        """xxd has epoch"""
+        mver = '2:8.1.2269-1ubuntu5'
+        self.assertEqual(
+            {'epoch': 2, 'major': 8, 'minor': 1, 'micro': 2269,
+             'extra': None, 'raw': mver, 'upstream': '8.1.2269',
+             'semantic_version': 82369},
+            distro.parse_dpkg_version(mver))
+
+    def test_package_with_dot_in_extra(self):
+        """linux-image-generic has multiple dots in extra"""
+        mver = '5.4.0.37.40'
+        self.assertEqual(
+            {'epoch': 0, 'major': 5, 'minor': 4, 'micro': 0,
+             'extra': '37.40', 'raw': mver, 'upstream': '5.4.0.37.40',
+             'semantic_version': 50400},
+            distro.parse_dpkg_version(mver))
+
 
 class TestDistros(CiTestCase):
 
@@ -429,6 +449,7 @@ class TestSystemUpgrade(CiTestCase):
         auto_remove = apt_base + ['autoremove']
         expected_calls = [
             mock.call(apt_cmd, env=env, target=paths.target_path(target)),
+            mock.call(['apt-get', 'clean'], target=paths.target_path(target)),
             mock.call(auto_remove, env=env, target=paths.target_path(target)),
         ]
         which_calls = [mock.call('eatmydata', target=target)]
diff --git a/tests/unittests/test_feature.py b/tests/unittests/test_feature.py
index 7c55882..8690ad8 100644
--- a/tests/unittests/test_feature.py
+++ b/tests/unittests/test_feature.py
@@ -24,4 +24,10 @@ class TestExportsFeatures(CiTestCase):
     def test_has_centos_curthook_support(self):
         self.assertIn('CENTOS_CURTHOOK_SUPPORT', curtin.FEATURES)
 
+    def test_has_btrfs_swapfile_support(self):
+        self.assertIn('BTRFS_SWAPFILE', curtin.FEATURES)
+
+    def test_has_uefi_reorder_fallback_support(self):
+        self.assertIn('UEFI_REORDER_FALLBACK_SUPPORT', curtin.FEATURES)
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/__init__.py b/tests/vmtests/__init__.py
index adfcd24..0b19d8f 100644
--- a/tests/vmtests/__init__.py
+++ b/tests/vmtests/__init__.py
@@ -633,6 +633,7 @@ class VMBaseClass(TestCase):
 
     # these get set from base_vm_classes
     release = None
+    supported_releases = []
     arch = None
     target_arch = None
     kflavor = None
@@ -855,6 +856,13 @@ class VMBaseClass(TestCase):
         return {'kernel': {'fallback-package': package}}
 
     @classmethod
+    def is_unsupported_release(cls):
+        # allow unsupported releases opt-in to avoid the skiptest
+        if cls.release in cls.supported_releases:
+            return False
+        return is_unsupported_ubuntu(cls.release)
+
+    @classmethod
     def skip_by_date(cls, *args, **kwargs):
         """skip_by_date wrapper. this way other modules do not have
         to add an import of skip_by_date to start skipping."""
@@ -883,7 +891,7 @@ class VMBaseClass(TestCase):
                 "Class %s does not have required attrs set: %s" %
                 (cls.__name__, missing))
 
-        if is_unsupported_ubuntu(cls.release):
+        if cls.is_unsupported_release():
             raise SkipTest('"%s" is unsupported release.' % cls.release)
 
         # check if we should skip due to host arch
@@ -1668,8 +1676,8 @@ class VMBaseClass(TestCase):
                 if spec in line:
                     fstab_entry = line
                     self.assertIsNotNone(fstab_entry)
-                    self.assertEqual(mp, fstab_entry.split(' ')[1])
-                    self.assertEqual(fsopts, fstab_entry.split(' ')[3])
+                    self.assertEqual(mp, fstab_entry.split()[1])
+                    self.assertEqual(fsopts, fstab_entry.split()[3])
                     found.append((spec, mp, fsopts))
 
         self.assertEqual(sorted(expected), sorted(found))
@@ -1755,12 +1763,28 @@ class VMBaseClass(TestCase):
                  for line in ls_byid.split('\n')
                  if ("virtio-" + serial) in line.split() or
                     ("scsi-" + serial) in line.split() or
-                    ("wwn-" + serial) in line.split()]
+                    ("wwn-" + serial) in line.split() or
+                    (serial) in line.split()]
+        print("Looking for serial %s in 'ls_al_byid' content\n%s" % (serial,
+                                                                     ls_byid))
         self.assertEqual(len(kname), 1)
         kname = kname.pop()
         self.assertIsNotNone(kname)
         return kname
 
+    def _mdname_to_kname(self, mdname):
+        # extract kname from /dev/md/ on /dev/<kname>
+        # parsing ls -al output on /dev/md/*:
+        # lrwxrwxrwx 1 root root 8 May 28 16:26 /dev/md/os-raid1 -> ../md127
+        ls_dev_md = self.load_collect_file("ls_al_dev_md")
+        knames = [os.path.basename(line.split()[-1])
+                  for line in ls_dev_md.split('\n')
+                  if mdname in line]
+        self.assertEqual(len(knames), 1)
+        kname = knames.pop()
+        self.assertIsNotNone(kname)
+        return kname
+
     def _kname_to_bypath(self, kname):
         # extract path from /dev/disk/by-path on /dev/<kname>
         # parsing ls -al output on /dev/disk/by-path
@@ -1789,6 +1813,36 @@ class VMBaseClass(TestCase):
         self.assertEqual(len(uuid), 36)
         return uuid
 
+    def _byuuid_to_kname(self, devpath):
+        # lookup kname via /dev/disk/by-uuid symlink
+        # parsing ls -al output on /dev/disk/by-uuid:
+        # lrwxrwxrwx 1 root root   9 Dec  4 20:02
+        #  d591e9e9-825a-4f0a-b280-3bfaf470b83c -> ../../vdg
+        uuid = os.path.basename(devpath)
+        self.assertIsNotNone(uuid)
+        print(uuid)
+        ls_uuid = self.load_collect_file("ls_al_byuuid")
+        kname = [line.split()[-1] for line in ls_uuid.split('\n')
+                 if uuid in line.split()]
+        self.assertEqual(len(kname), 1)
+        kname = os.path.basename(kname.pop())
+        return kname
+
+    def _bypath_to_kname(self, devpath):
+        # lookup kname via /dev/disk/by-path symlink
+        # parsing ls -al output on /dev/disk/by-path:
+        # lrwxrwxrwx 1 root root   9 Dec  4 20:02
+        #  pci-0000:00:03.0-scsi-0:0:0:0-part3 -> ../../sda3
+        dpath = os.path.basename(devpath)
+        self.assertIsNotNone(dpath)
+        print(dpath)
+        ls_bypath = self.load_collect_file("ls_al_bypath")
+        kname = [line.split()[-1] for line in ls_bypath.split('\n')
+                 if dpath in line.split()]
+        self.assertEqual(len(kname), 1)
+        kname = os.path.basename(kname.pop())
+        return kname
+
     def _bcache_to_byuuid(self, kname):
         # extract bcache uuid from /dev/bcache/by-uuid on /dev/<kname>
         # parsing ls -al output on /dev/bcache/by-uuid
@@ -1970,25 +2024,44 @@ class VMBaseClass(TestCase):
 
     @skip_if_flag('expected_failure')
     def test_swaps_used(self):
-        if not self.has_storage_config():
-            raise SkipTest("This test does not use storage config.")
 
-        stgcfg = self.get_storage_config()
-        swap_ids = [d["id"] for d in stgcfg if d.get("fstype") == "swap"]
-        swap_mounts = [d for d in stgcfg if d.get("device") in swap_ids]
-        self.assertEqual(len(swap_ids), len(swap_mounts),
-                         "number config swap fstypes != number swap mounts")
+        def find_fstab_swaps():
+            swaps = []
+            path = self.collect_path("fstab")
+            if not os.path.exists(path):
+                return swaps
+            for line in util.load_file(path).splitlines():
+                if line.startswith("#"):
+                    continue
+                (fs, mp, fstype, opts, dump, passno) = line.split()
+                if fstype == 'swap':
+                    if fs.startswith('/dev/disk/by-uuid'):
+                        swaps.append('/dev/' + self._byuuid_to_kname(fs))
+                    elif fs.startswith('/dev/disk/by-id'):
+                        kname = self._serial_to_kname(os.path.basename(fs))
+                        swaps.append('/dev/' + kname)
+                    elif fs.startswith('/dev/disk/by-path'):
+                        swaps.append('/dev/' + self._bypath_to_kname(fs))
+                    else:
+                        swaps.append(fs)
+
+            return swaps
+
+        # we don't yet have a skip_by_date on specific releases
+        if is_devel_release(self.target_release):
+            name = "test_swaps_used"
+            bug = "1894910"
+            fixby = "2020-10-15"
+            removeby = "2020-11-01"
+            raise SkipTest(
+                "skip_by_date({name}) LP: #{bug} "
+                "fixby={fixby} removeby={removeby}: ".format(
+                    name=name, bug=bug, fixby=fixby, removeby=removeby))
 
-        swaps_found = []
-        for line in self.load_collect_file("proc-swaps").splitlines():
-            fname, ttype, size, used, priority = line.split()
-            if ttype == "partition":
-                swaps_found.append(
-                    {"fname": fname, ttype: "ttype", "size": int(size),
-                     "used": int(used), "priority": int(priority)})
-        self.assertEqual(
-            len(swap_mounts), len(swaps_found),
-            "Number swaps configured != number used")
+        expected_swaps = find_fstab_swaps()
+        proc_swaps = self.load_collect_file("proc-swaps")
+        for swap in expected_swaps:
+            self.assertIn(swap, proc_swaps)
 
 
 class PsuedoVMBaseClass(VMBaseClass):
@@ -2087,6 +2160,9 @@ class PsuedoVMBaseClass(VMBaseClass):
     def test_kernel_img_conf(self):
         pass
 
+    def test_swaps_used(self):
+        pass
+
     def _maybe_raise(self, exc):
         if self.allow_test_fails:
             raise exc
diff --git a/tests/vmtests/releases.py b/tests/vmtests/releases.py
index 3dcb415..11abcb8 100644
--- a/tests/vmtests/releases.py
+++ b/tests/vmtests/releases.py
@@ -185,6 +185,14 @@ class _FocalBase(_UbuntuBase):
         subarch = "ga-20.04"
 
 
+class _GroovyBase(_UbuntuBase):
+    release = "groovy"
+    target_release = "groovy"
+    mem = "2048"
+    if _UbuntuBase.arch == "arm64":
+        subarch = "ga-20.04"
+
+
 class _Releases(object):
     trusty = _TrustyBase
     precise = _PreciseBase
@@ -203,6 +211,7 @@ class _Releases(object):
     disco = _DiscoBase
     eoan = _EoanBase
     focal = _FocalBase
+    groovy = _GroovyBase
 
 
 class _CentosReleases(object):
diff --git a/tests/vmtests/test_apt_config_cmd.py b/tests/vmtests/test_apt_config_cmd.py
index 4e43882..874efad 100644
--- a/tests/vmtests/test_apt_config_cmd.py
+++ b/tests/vmtests/test_apt_config_cmd.py
@@ -41,7 +41,7 @@ class TestAptConfigCMD(VMBaseClass):
         self.check_file_regex("curtin-dev-ubuntu-test-archive-%s.list" %
                               self.release,
                               (r"http://ppa.launchpad.net/";
-                               r"curtin-dev/test-archive/ubuntu"
+                               r"curtin-dev/test-archive/ubuntu(/*)"
                                r" %s main" % self.release))
 
     def test_cmd_preserve_source(self):
@@ -68,4 +68,8 @@ class FocalTestAptConfigCMDCMD(relbase.focal, TestAptConfigCMD):
     __test__ = True
 
 
+class GroovyTestAptConfigCMDCMD(relbase.groovy, TestAptConfigCMD):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_basic.py b/tests/vmtests/test_basic.py
index 88b9897..5723bc6 100644
--- a/tests/vmtests/test_basic.py
+++ b/tests/vmtests/test_basic.py
@@ -143,11 +143,16 @@ class TestBasicAbs(VMBaseClass):
     def get_fstab_expected(self):
         rootdev = self._serial_to_kname('disk-a')
         btrfsdev = self._serial_to_kname('disk-c')
-        return [
+        expected = [
             (self._kname_to_byuuid(rootdev + '1'), '/', 'defaults'),
             (self._kname_to_byuuid(rootdev + '2'), '/home', 'defaults'),
-            (self._kname_to_byuuid(btrfsdev), '/btrfs', 'defaults,noatime')
+            (self._kname_to_byuuid(btrfsdev), '/btrfs', 'defaults,noatime'),
+            (self._kname_to_byuuid(rootdev + '3'), 'none', 'sw'),
         ]
+        if self.target_release in ['focal']:
+            expected.append(('/btrfs/btrfsswap.img', 'none', 'sw'))
+
+        return expected
 
     def test_whole_disk_uuid(self):
         self._test_whole_disk_uuid(
@@ -250,11 +255,11 @@ class BionicTestBasic(relbase.bionic, TestBasicAbs):
     __test__ = True
 
 
-class EoanTestBasic(relbase.eoan, TestBasicAbs):
+class FocalTestBasic(relbase.focal, TestBasicAbs):
     __test__ = True
 
 
-class FocalTestBasic(relbase.focal, TestBasicAbs):
+class GroovyTestBasic(relbase.groovy, TestBasicAbs):
     __test__ = True
 
 
@@ -307,14 +312,23 @@ class TestBasicScsiAbs(TestBasicAbs):
         home_kname = (
             self._serial_to_kname('0x39cc071e72c64cc4-part2'))
         btrfs_kname = self._serial_to_kname('0x22dc58dc023c7008')
+        swap_kname = (
+            self._serial_to_kname('0x39cc071e72c64cc4-part3'))
 
         map_func = self._kname_to_byuuid
         if self.arch == 's390x':
             map_func = self._kname_to_bypath
 
-        return [(map_func(root_kname), '/', 'defaults'),
-                (map_func(home_kname), '/home', 'defaults'),
-                (map_func(btrfs_kname), '/btrfs', 'defaults,noatime')]
+        expected = [
+            (map_func(root_kname), '/', 'defaults'),
+            (map_func(home_kname), '/home', 'defaults'),
+            (map_func(btrfs_kname), '/btrfs', 'defaults,noatime'),
+            (map_func(swap_kname), 'none', 'sw')]
+
+        if self.target_release in ['focal']:
+            expected.append(('/btrfs/btrfsswap.img', 'none', 'sw'))
+
+        return expected
 
     @skip_if_arch('s390x')
     def test_whole_disk_uuid(self):
@@ -361,11 +375,11 @@ class BionicTestScsiBasic(relbase.bionic, TestBasicScsiAbs):
     __test__ = True
 
 
-class EoanTestScsiBasic(relbase.eoan, TestBasicScsiAbs):
+class FocalTestScsiBasic(relbase.focal, TestBasicScsiAbs):
     __test__ = True
 
 
-class FocalTestScsiBasic(relbase.focal, TestBasicScsiAbs):
+class GroovyTestScsiBasic(relbase.groovy, TestBasicScsiAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_basic_dasd.py b/tests/vmtests/test_basic_dasd.py
index 391bafc..d61e1b9 100644
--- a/tests/vmtests/test_basic_dasd.py
+++ b/tests/vmtests/test_basic_dasd.py
@@ -52,11 +52,11 @@ class BionicTestBasicDasd(relbase.bionic, TestBasicDasd):
     __test__ = True
 
 
-class EoanTestBasicDasd(relbase.eoan, TestBasicDasd):
+class FocalTestBasicDasd(relbase.focal, TestBasicDasd):
     __test__ = True
 
 
-class FocalTestBasicDasd(relbase.focal, TestBasicDasd):
+class GroovyTestBasicDasd(relbase.groovy, TestBasicDasd):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_bcache_basic.py b/tests/vmtests/test_bcache_basic.py
index 54bac81..053225f 100644
--- a/tests/vmtests/test_bcache_basic.py
+++ b/tests/vmtests/test_bcache_basic.py
@@ -64,11 +64,11 @@ class BionicBcacheBasic(relbase.bionic, TestBcacheBasic):
     __test__ = True
 
 
-class EoanBcacheBasic(relbase.eoan, TestBcacheBasic):
+class FocalBcacheBasic(relbase.focal, TestBcacheBasic):
     __test__ = True
 
 
-class FocalBcacheBasic(relbase.focal, TestBcacheBasic):
+class GroovyBcacheBasic(relbase.groovy, TestBcacheBasic):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_bcache_bug1718699.py b/tests/vmtests/test_bcache_bug1718699.py
index 8c29046..ebb99ab 100644
--- a/tests/vmtests/test_bcache_bug1718699.py
+++ b/tests/vmtests/test_bcache_bug1718699.py
@@ -19,11 +19,11 @@ class BionicTestBcacheBug1718699(relbase.bionic, TestBcacheBug1718699):
     __test__ = True
 
 
-class EoanTestBcacheBug1718699(relbase.eoan, TestBcacheBug1718699):
+class FocalTestBcacheBug1718699(relbase.focal, TestBcacheBug1718699):
     __test__ = True
 
 
-class FocalTestBcacheBug1718699(relbase.focal, TestBcacheBug1718699):
+class GroovyTestBcacheBug1718699(relbase.groovy, TestBcacheBug1718699):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_bcache_ceph.py b/tests/vmtests/test_bcache_ceph.py
index d24994a..bff4dd4 100644
--- a/tests/vmtests/test_bcache_ceph.py
+++ b/tests/vmtests/test_bcache_ceph.py
@@ -75,11 +75,11 @@ class BionicTestBcacheCeph(relbase.bionic, TestBcacheCeph):
     __test__ = True
 
 
-class EoanTestBcacheCeph(relbase.eoan, TestBcacheCeph):
+class FocalTestBcacheCeph(relbase.focal, TestBcacheCeph):
     __test__ = True
 
 
-class FocalTestBcacheCeph(relbase.focal, TestBcacheCeph):
+class GroovyTestBcacheCeph(relbase.groovy, TestBcacheCeph):
     __test__ = True
 
 
@@ -109,4 +109,8 @@ class FocalTestBcacheCephLvm(relbase.focal, TestBcacheCephLvm):
     __test__ = True
 
 
+class GroovyTestBcacheCephLvm(relbase.groovy, 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 f41e645..1ffea12 100644
--- a/tests/vmtests/test_bcache_partitions.py
+++ b/tests/vmtests/test_bcache_partitions.py
@@ -25,11 +25,11 @@ class BionicTestBcachePartitions(relbase.bionic, TestBcachePartitions):
     __test__ = True
 
 
-class EoanTestBcachePartitions(relbase.eoan, TestBcachePartitions):
+class FocalTestBcachePartitions(relbase.focal, TestBcachePartitions):
     __test__ = True
 
 
-class FocalTestBcachePartitions(relbase.focal, TestBcachePartitions):
+class GroovyTestBcachePartitions(relbase.groovy, TestBcachePartitions):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_fs_battery.py b/tests/vmtests/test_fs_battery.py
index bd44905..7177fea 100644
--- a/tests/vmtests/test_fs_battery.py
+++ b/tests/vmtests/test_fs_battery.py
@@ -239,11 +239,11 @@ class BionicTestFsBattery(relbase.bionic, TestFsBattery):
     __test__ = True
 
 
-class EoanTestFsBattery(relbase.eoan, TestFsBattery):
+class FocalTestFsBattery(relbase.focal, TestFsBattery):
     __test__ = True
 
 
-class FocalTestFsBattery(relbase.focal, TestFsBattery):
+class GroovyTestFsBattery(relbase.groovy, TestFsBattery):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_iscsi.py b/tests/vmtests/test_iscsi.py
index c99264c..f3406cd 100644
--- a/tests/vmtests/test_iscsi.py
+++ b/tests/vmtests/test_iscsi.py
@@ -72,11 +72,11 @@ class BionicTestIscsiBasic(relbase.bionic, TestBasicIscsiAbs):
     __test__ = True
 
 
-class EoanTestIscsiBasic(relbase.eoan, TestBasicIscsiAbs):
+class FocalTestIscsiBasic(relbase.focal, TestBasicIscsiAbs):
     __test__ = True
 
 
-class FocalTestIscsiBasic(relbase.focal, TestBasicIscsiAbs):
+class GroovyTestIscsiBasic(relbase.groovy, TestBasicIscsiAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_journald_reporter.py b/tests/vmtests/test_journald_reporter.py
index d29b4d4..ff003a5 100644
--- a/tests/vmtests/test_journald_reporter.py
+++ b/tests/vmtests/test_journald_reporter.py
@@ -32,11 +32,11 @@ class BionicTestJournaldReporter(relbase.bionic, TestJournaldReporter):
     __test__ = True
 
 
-class EoanTestJournaldReporter(relbase.eoan, TestJournaldReporter):
+class FocalTestJournaldReporter(relbase.focal, TestJournaldReporter):
     __test__ = True
 
 
-class FocalTestJournaldReporter(relbase.focal, TestJournaldReporter):
+class GroovyTestJournaldReporter(relbase.groovy, TestJournaldReporter):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_lvm.py b/tests/vmtests/test_lvm.py
index a79a705..eb65c32 100644
--- a/tests/vmtests/test_lvm.py
+++ b/tests/vmtests/test_lvm.py
@@ -77,11 +77,11 @@ class BionicTestLvm(relbase.bionic, TestLvmAbs):
     __test__ = True
 
 
-class EoanTestLvm(relbase.eoan, TestLvmAbs):
+class FocalTestLvm(relbase.focal, TestLvmAbs):
     __test__ = True
 
 
-class FocalTestLvm(relbase.focal, TestLvmAbs):
+class GroovyTestLvm(relbase.groovy, TestLvmAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_lvm_iscsi.py b/tests/vmtests/test_lvm_iscsi.py
index 077b31a..e0b9606 100644
--- a/tests/vmtests/test_lvm_iscsi.py
+++ b/tests/vmtests/test_lvm_iscsi.py
@@ -95,11 +95,11 @@ class BionicTestIscsiLvm(relbase.bionic, TestLvmIscsiAbs):
     __test__ = True
 
 
-class EoanTestIscsiLvm(relbase.eoan, TestLvmIscsiAbs):
+class FocalTestIscsiLvm(relbase.focal, TestLvmIscsiAbs):
     __test__ = True
 
 
-class FocalTestIscsiLvm(relbase.focal, TestLvmIscsiAbs):
+class GroovyTestIscsiLvm(relbase.groovy, TestLvmIscsiAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_lvm_raid.py b/tests/vmtests/test_lvm_raid.py
index 8d42a1a..5fe7993 100644
--- a/tests/vmtests/test_lvm_raid.py
+++ b/tests/vmtests/test_lvm_raid.py
@@ -47,17 +47,17 @@ class TestLvmOverRaidAbs(TestMdadmAbs, TestLvmAbs):
         return self._test_pvs(dname_to_vg)
 
 
-class FocalTestLvmOverRaid(relbase.focal, TestLvmOverRaidAbs):
+class XenialGATestLvmOverRaid(relbase.xenial_ga, TestLvmOverRaidAbs):
     __test__ = True
 
 
-class EoanTestLvmOverRaid(relbase.eoan, TestLvmOverRaidAbs):
+class BionicTestLvmOverRaid(relbase.bionic, TestLvmOverRaidAbs):
     __test__ = True
 
 
-class BionicTestLvmOverRaid(relbase.bionic, TestLvmOverRaidAbs):
+class FocalTestLvmOverRaid(relbase.focal, TestLvmOverRaidAbs):
     __test__ = True
 
 
-class XenialGATestLvmOverRaid(relbase.xenial_ga, TestLvmOverRaidAbs):
+class GroovyTestLvmOverRaid(relbase.groovy, TestLvmOverRaidAbs):
     __test__ = True
diff --git a/tests/vmtests/test_lvm_root.py b/tests/vmtests/test_lvm_root.py
index 117406e..12b8ea8 100644
--- a/tests/vmtests/test_lvm_root.py
+++ b/tests/vmtests/test_lvm_root.py
@@ -94,6 +94,13 @@ class FocalTestLvmRootExt4(relbase.focal, TestLvmRootAbs):
     }
 
 
+class GroovyTestLvmRootExt4(relbase.groovy, TestLvmRootAbs):
+    __test__ = True
+    conf_replace = {
+        '__ROOTFS_FORMAT__': 'ext4',
+    }
+
+
 class XenialTestLvmRootXfs(relbase.xenial, TestLvmRootAbs):
     __test__ = True
     conf_replace = {
@@ -140,6 +147,14 @@ class FocalTestUefiLvmRootExt4(relbase.focal, TestUefiLvmRootAbs):
     }
 
 
+class GroovyTestUefiLvmRootExt4(relbase.groovy, TestUefiLvmRootAbs):
+    __test__ = True
+    conf_replace = {
+        '__BOOTFS_FORMAT__': 'ext4',
+        '__ROOTFS_FORMAT__': 'ext4',
+    }
+
+
 class XenialTestUefiLvmRootXfs(relbase.xenial, TestUefiLvmRootAbs):
     __test__ = True
     conf_replace = {
@@ -148,13 +163,11 @@ class XenialTestUefiLvmRootXfs(relbase.xenial, TestUefiLvmRootAbs):
     }
 
 
-@VMBaseClass.skip_by_date("1652822", fixby="2020-06-01", install=False)
 class XenialTestUefiLvmRootXfsBootXfs(relbase.xenial, TestUefiLvmRootAbs):
     """This tests xfs root and xfs boot with uefi.
 
-    It is known broken (LP: #1652822) and unlikely to be fixed without pushing,
-    so we skip-by for a long time."""
-    __test__ = True
+    It is known broken (LP: #1652822) and unlikely to be fixed."""
+    __test__ = False
     conf_replace = {
         '__BOOTFS_FORMAT__': 'xfs',
         '__ROOTFS_FORMAT__': 'xfs',
diff --git a/tests/vmtests/test_mdadm_bcache.py b/tests/vmtests/test_mdadm_bcache.py
index 8e250cc..5425221 100644
--- a/tests/vmtests/test_mdadm_bcache.py
+++ b/tests/vmtests/test_mdadm_bcache.py
@@ -26,6 +26,7 @@ class TestMdadmAbs(VMBaseClass):
         ls -al /dev/bcache* > lsal_dev_bcache_star
         ls -al /dev/bcache/by-uuid/ | cat >ls_al_bcache_byuuid
         ls -al /dev/bcache/by-label/ | cat >ls_al_bcache_bylabel
+        ls -al /dev/md/* | cat >ls_al_dev_md
 
         exit 0
         """)]
@@ -153,18 +154,18 @@ class BionicTestMdadmBcache(relbase.bionic, TestMdadmBcacheAbs):
     __test__ = True
 
 
-class EoanTestMdadmBcache(relbase.eoan, TestMdadmBcacheAbs):
-    __test__ = True
-
-
 class FocalTestMdadmBcache(relbase.focal, TestMdadmBcacheAbs):
     __test__ = True
 
-    @TestMdadmBcacheAbs.skip_by_date("1861941", fixby="2020-07-15")
+    @TestMdadmBcacheAbs.skip_by_date("1861941", fixby="2020-09-15")
     def test_fstab(self):
         return super().test_fstab()
 
 
+class GroovyTestMdadmBcache(relbase.groovy, TestMdadmBcacheAbs):
+    __test__ = True
+
+
 class TestMirrorbootAbs(TestMdadmAbs):
     # alternative config for more complex setup
     conf_file = "examples/tests/mirrorboot.yaml"
@@ -202,11 +203,11 @@ class BionicTestMirrorboot(relbase.bionic, TestMirrorbootAbs):
     __test__ = True
 
 
-class EoanTestMirrorboot(relbase.eoan, TestMirrorbootAbs):
+class FocalTestMirrorboot(relbase.focal, TestMirrorbootAbs):
     __test__ = True
 
 
-class FocalTestMirrorboot(relbase.focal, TestMirrorbootAbs):
+class GroovyTestMirrorboot(relbase.groovy, TestMirrorbootAbs):
     __test__ = True
 
 
@@ -250,13 +251,13 @@ class BionicTestMirrorbootPartitions(relbase.bionic,
     __test__ = True
 
 
-class EoanTestMirrorbootPartitions(relbase.eoan,
-                                   TestMirrorbootPartitionsAbs):
+class FocalTestMirrorbootPartitions(relbase.focal,
+                                    TestMirrorbootPartitionsAbs):
     __test__ = True
 
 
-class FocalTestMirrorbootPartitions(relbase.focal,
-                                    TestMirrorbootPartitionsAbs):
+class GroovyTestMirrorbootPartitions(relbase.groovy,
+                                     TestMirrorbootPartitionsAbs):
     __test__ = True
 
 
@@ -309,6 +310,16 @@ class TestMirrorbootPartitionsUEFIAbs(TestMdadmAbs):
         self.assertIn(
             ('grub-pc', 'grub-efi/install_devices', choice), found_selections)
 
+    def test_backup_esp_matches_primary(self):
+        if self.target_distro != "ubuntu":
+            raise SkipTest("backup ESP supported only on Ubuntu")
+        if self.target_release in [
+                "trusty", "xenial", "bionic", "cosmic", "disco", "eoan"]:
+            raise SkipTest("backup ESP supported only on >= Focal")
+        primary_esp = self.load_collect_file("diska-part1-efi.out")
+        backup_esp = self.load_collect_file("diskb-part1-efi.out")
+        self.assertEqual(primary_esp, backup_esp)
+
 
 class Centos70TestMirrorbootPartitionsUEFI(centos_relbase.centos70_xenial,
                                            TestMirrorbootPartitionsUEFIAbs):
@@ -335,19 +346,14 @@ class BionicTestMirrorbootPartitionsUEFI(relbase.bionic,
     __test__ = True
 
 
-class EoanTestMirrorbootPartitionsUEFI(relbase.eoan,
-                                       TestMirrorbootPartitionsUEFIAbs):
-    __test__ = True
-
-
 class FocalTestMirrorbootPartitionsUEFI(relbase.focal,
                                         TestMirrorbootPartitionsUEFIAbs):
     __test__ = True
 
-    def test_backup_esp_matches_primary(self):
-        primary_esp = self.load_collect_file("diska-part1-efi.out")
-        backup_esp = self.load_collect_file("diskb-part1-efi.out")
-        self.assertEqual(primary_esp, backup_esp)
+
+class GroovyTestMirrorbootPartitionsUEFI(relbase.groovy,
+                                         TestMirrorbootPartitionsUEFIAbs):
+    __test__ = True
 
 
 class TestRaid5bootAbs(TestMdadmAbs):
@@ -359,11 +365,14 @@ class TestRaid5bootAbs(TestMdadmAbs):
                      ('main_disk', 2),
                      ('second_disk', 1),
                      ('third_disk', 1),
-                     ('md0', 0)]
+                     ('os-raid1', 0)]
 
     def get_fstab_expected(self):
+        kname = self._mdname_to_kname('os-raid1')
         return [
-            (self._kname_to_uuid_devpath('md-uuid', 'md0'), '/', 'defaults'),
+            (self._kname_to_uuid_devpath('md-uuid', kname),
+             '/',
+             'defaults'),
         ]
 
 
@@ -387,11 +396,11 @@ class BionicTestRaid5boot(relbase.bionic, TestRaid5bootAbs):
     __test__ = True
 
 
-class EoanTestRaid5boot(relbase.eoan, TestRaid5bootAbs):
+class FocalTestRaid5boot(relbase.focal, TestRaid5bootAbs):
     __test__ = True
 
 
-class FocalTestRaid5boot(relbase.focal, TestRaid5bootAbs):
+class GroovyTestRaid5boot(relbase.groovy, TestRaid5bootAbs):
     __test__ = True
 
 
@@ -448,11 +457,11 @@ class BionicTestRaid6boot(relbase.bionic, TestRaid6bootAbs):
     __test__ = True
 
 
-class EoanTestRaid6boot(relbase.eoan, TestRaid6bootAbs):
+class FocalTestRaid6boot(relbase.focal, TestRaid6bootAbs):
     __test__ = True
 
 
-class FocalTestRaid6boot(relbase.focal, TestRaid6bootAbs):
+class GroovyTestRaid6boot(relbase.groovy, TestRaid6bootAbs):
     __test__ = True
 
 
@@ -495,11 +504,11 @@ class BionicTestRaid10boot(relbase.bionic, TestRaid10bootAbs):
     __test__ = True
 
 
-class EoanTestRaid10boot(relbase.eoan, TestRaid10bootAbs):
+class FocalTestRaid10boot(relbase.focal, TestRaid10bootAbs):
     __test__ = True
 
 
-class FocalTestRaid10boot(relbase.focal, TestRaid10bootAbs):
+class GroovyTestRaid10boot(relbase.groovy, TestRaid10bootAbs):
     __test__ = True
 
 
@@ -599,11 +608,11 @@ class BionicTestAllindata(relbase.bionic, TestAllindataAbs):
     __test__ = True
 
 
-class EoanTestAllindata(relbase.eoan, TestAllindataAbs):
+class FocalTestAllindata(relbase.focal, TestAllindataAbs):
     __test__ = True
 
 
-class FocalTestAllindata(relbase.focal, TestAllindataAbs):
+class GroovyTestAllindata(relbase.groovy, TestAllindataAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_mdadm_iscsi.py b/tests/vmtests/test_mdadm_iscsi.py
index 26b1f71..7e6fbf6 100644
--- a/tests/vmtests/test_mdadm_iscsi.py
+++ b/tests/vmtests/test_mdadm_iscsi.py
@@ -50,11 +50,11 @@ class BionicTestIscsiMdadm(relbase.bionic, TestMdadmIscsiAbs):
     __test__ = True
 
 
-class EoanTestIscsiMdadm(relbase.eoan, TestMdadmIscsiAbs):
+class FocalTestIscsiMdadm(relbase.focal, TestMdadmIscsiAbs):
     __test__ = True
 
 
-class FocalTestIscsiMdadm(relbase.focal, TestMdadmIscsiAbs):
+class GroovyTestIscsiMdadm(relbase.groovy, TestMdadmIscsiAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_multipath.py b/tests/vmtests/test_multipath.py
index 7c7e621..6d9c5df 100644
--- a/tests/vmtests/test_multipath.py
+++ b/tests/vmtests/test_multipath.py
@@ -158,11 +158,11 @@ class BionicTestMultipathBasic(relbase.bionic, TestMultipathBasicAbs):
     __test__ = True
 
 
-class EoanTestMultipathBasic(relbase.eoan, TestMultipathBasicAbs):
+class FocalTestMultipathBasic(relbase.focal, TestMultipathBasicAbs):
     __test__ = True
 
 
-class FocalTestMultipathBasic(relbase.focal, TestMultipathBasicAbs):
+class GroovyTestMultipathBasic(relbase.groovy, TestMultipathBasicAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_multipath_lvm.py b/tests/vmtests/test_multipath_lvm.py
index 39b8587..c5a1e42 100644
--- a/tests/vmtests/test_multipath_lvm.py
+++ b/tests/vmtests/test_multipath_lvm.py
@@ -56,11 +56,11 @@ class BionicTestMultipathLvm(relbase.bionic, TestMultipathLvmAbs):
     __test__ = True
 
 
-class EoanTestMultipathLvm(relbase.eoan, TestMultipathLvmAbs):
+class FocalTestMultipathLvm(relbase.focal, TestMultipathLvmAbs):
     __test__ = True
 
 
-class FocalTestMultipathLvm(relbase.focal, TestMultipathLvmAbs):
+class GroovyTestMultipathLvm(relbase.groovy, TestMultipathLvmAbs):
     __test__ = True
 
 
@@ -73,4 +73,9 @@ class FocalTestMultipathLvmPartWipe(relbase.focal,
     __test__ = True
 
 
+class GroovyTestMultipathLvmPartWipe(relbase.groovy,
+                                     TestMultipathLvmPartWipeAbs):
+    __test__ = True
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network.py b/tests/vmtests/test_network.py
index e6ea6e2..43a7c6b 100644
--- a/tests/vmtests/test_network.py
+++ b/tests/vmtests/test_network.py
@@ -474,11 +474,11 @@ class BionicTestNetworkBasic(relbase.bionic, TestNetworkBasicAbs):
     __test__ = True
 
 
-class EoanTestNetworkBasic(relbase.eoan, TestNetworkBasicAbs):
+class FocalTestNetworkBasic(relbase.focal, TestNetworkBasicAbs):
     __test__ = True
 
 
-class FocalTestNetworkBasic(relbase.focal, TestNetworkBasicAbs):
+class GroovyTestNetworkBasic(relbase.groovy, TestNetworkBasicAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_alias.py b/tests/vmtests/test_network_alias.py
index 68e7de4..bc1fb22 100644
--- a/tests/vmtests/test_network_alias.py
+++ b/tests/vmtests/test_network_alias.py
@@ -52,11 +52,11 @@ class BionicTestNetworkAlias(relbase.bionic, TestNetworkAliasAbs):
     __test__ = True
 
 
-class EoanTestNetworkAlias(relbase.eoan, TestNetworkAliasAbs):
+class FocalTestNetworkAlias(relbase.focal, TestNetworkAliasAbs):
     __test__ = True
 
 
-class FocalTestNetworkAlias(relbase.focal, TestNetworkAliasAbs):
+class GroovyTestNetworkAlias(relbase.groovy, TestNetworkAliasAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_bonding.py b/tests/vmtests/test_network_bonding.py
index 913c7ff..6c6dd6d 100644
--- a/tests/vmtests/test_network_bonding.py
+++ b/tests/vmtests/test_network_bonding.py
@@ -57,11 +57,11 @@ class BionicTestBonding(relbase.bionic, TestNetworkBondingAbs):
     __test__ = True
 
 
-class EoanTestBonding(relbase.eoan, TestNetworkBondingAbs):
+class FocalTestBonding(relbase.focal, TestNetworkBondingAbs):
     __test__ = True
 
 
-class FocalTestBonding(relbase.focal, TestNetworkBondingAbs):
+class GroovyTestBonding(relbase.groovy, TestNetworkBondingAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_bridging.py b/tests/vmtests/test_network_bridging.py
index daaade5..9ecd2f6 100644
--- a/tests/vmtests/test_network_bridging.py
+++ b/tests/vmtests/test_network_bridging.py
@@ -236,11 +236,11 @@ class BionicTestBridging(relbase.bionic, TestBridgeNetworkAbs):
     __test__ = True
 
 
-class EoanTestBridging(relbase.eoan, TestBridgeNetworkAbs):
+class FocalTestBridging(relbase.focal, TestBridgeNetworkAbs):
     __test__ = True
 
 
-class FocalTestBridging(relbase.focal, TestBridgeNetworkAbs):
+class GroovyTestBridging(relbase.groovy, TestBridgeNetworkAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_disabled.py b/tests/vmtests/test_network_disabled.py
index b19ca64..ea8dae2 100644
--- a/tests/vmtests/test_network_disabled.py
+++ b/tests/vmtests/test_network_disabled.py
@@ -57,6 +57,11 @@ class FocalCurtinDisableNetworkRendering(relbase.focal,
     __test__ = True
 
 
+class GroovyCurtinDisableNetworkRendering(relbase.groovy,
+                                          CurtinDisableNetworkRendering):
+    __test__ = True
+
+
 class FocalCurtinDisableCloudInitNetworkingVersion1(
     relbase.focal,
     CurtinDisableCloudInitNetworkingVersion1
@@ -64,9 +69,21 @@ class FocalCurtinDisableCloudInitNetworkingVersion1(
     __test__ = True
 
 
+class GroovyCurtinDisableCloudInitNetworkingVersion1(
+    relbase.groovy,
+    CurtinDisableCloudInitNetworkingVersion1
+):
+    __test__ = True
+
+
 class FocalCurtinDisableCloudInitNetworking(relbase.focal,
                                             CurtinDisableCloudInitNetworking):
     __test__ = True
 
 
+class GroovyCurtinDisableCloudInitNetworking(relbase.groovy,
+                                             CurtinDisableCloudInitNetworking):
+    __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 8f0dd54..50a139c 100644
--- a/tests/vmtests/test_network_ipv6.py
+++ b/tests/vmtests/test_network_ipv6.py
@@ -53,9 +53,13 @@ class BionicTestNetworkIPV6(relbase.bionic, TestNetworkIPV6Abs):
     __test__ = True
 
 
-class EoanTestNetworkIPV6(relbase.eoan, TestNetworkIPV6Abs):
+class GroovyTestNetworkIPV6(relbase.groovy, TestNetworkIPV6Abs):
     __test__ = True
 
+    @TestNetworkIPV6Abs.skip_by_date("1888726", "2020-10-15")
+    def test_ip_output(self):
+        return super().test_ip_output()
+
 
 class Centos66TestNetworkIPV6(centos_relbase.centos66_xenial,
                               CentosTestNetworkIPV6Abs):
diff --git a/tests/vmtests/test_network_ipv6_static.py b/tests/vmtests/test_network_ipv6_static.py
index 8a1ba2f..28ff697 100644
--- a/tests/vmtests/test_network_ipv6_static.py
+++ b/tests/vmtests/test_network_ipv6_static.py
@@ -23,11 +23,11 @@ class BionicTestNetworkIPV6Static(relbase.bionic, TestNetworkIPV6StaticAbs):
     __test__ = True
 
 
-class EoanTestNetworkIPV6Static(relbase.eoan, TestNetworkIPV6StaticAbs):
+class FocalTestNetworkIPV6Static(relbase.focal, TestNetworkIPV6StaticAbs):
     __test__ = True
 
 
-class FocalTestNetworkIPV6Static(relbase.focal, TestNetworkIPV6StaticAbs):
+class GroovyTestNetworkIPV6Static(relbase.groovy, TestNetworkIPV6StaticAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_ipv6_vlan.py b/tests/vmtests/test_network_ipv6_vlan.py
index d8e4e16..a0bf267 100644
--- a/tests/vmtests/test_network_ipv6_vlan.py
+++ b/tests/vmtests/test_network_ipv6_vlan.py
@@ -22,13 +22,17 @@ class BionicTestNetworkIPV6Vlan(relbase.bionic, TestNetworkIPV6VlanAbs):
     __test__ = True
 
 
-class EoanTestNetworkIPV6Vlan(relbase.eoan, TestNetworkIPV6VlanAbs):
+class FocalTestNetworkIPV6Vlan(relbase.focal, TestNetworkIPV6VlanAbs):
     __test__ = True
 
 
-class FocalTestNetworkIPV6Vlan(relbase.focal, TestNetworkIPV6VlanAbs):
+class GroovyTestNetworkIPV6Vlan(relbase.groovy, TestNetworkIPV6VlanAbs):
     __test__ = True
 
+    @TestNetworkVlanAbs.skip_by_date("1888726", "2020-10-15")
+    def test_ip_output(self):
+        return super().test_ip_output()
+
 
 class Centos66TestNetworkIPV6Vlan(centos_relbase.centos66_xenial,
                                   CentosTestNetworkIPV6VlanAbs):
diff --git a/tests/vmtests/test_network_mtu.py b/tests/vmtests/test_network_mtu.py
index bf13459..c70b9e0 100644
--- a/tests/vmtests/test_network_mtu.py
+++ b/tests/vmtests/test_network_mtu.py
@@ -137,6 +137,10 @@ class TestNetworkMtuAbs(TestNetworkIPV6Abs):
         self._check_iface_subnets('interface7')
 
 
+class TestNetworkMtuNetworkdAbs(TestNetworkMtuAbs):
+    conf_file = "examples/tests/network_mtu_networkd.yaml"
+
+
 class CentosTestNetworkMtuAbs(TestNetworkMtuAbs):
     conf_file = "examples/tests/network_mtu.yaml"
     extra_collect_scripts = TestNetworkMtuAbs.extra_collect_scripts + [
@@ -181,28 +185,16 @@ class TestNetworkMtu(relbase.xenial, TestNetworkMtuAbs):
     __test__ = True
 
 
-class BionicTestNetworkMtu(relbase.bionic, TestNetworkMtuAbs):
-    conf_file = "examples/tests/network_mtu_networkd.yaml"
+class BionicTestNetworkMtu(relbase.bionic, TestNetworkMtuNetworkdAbs):
     __test__ = True
-    # Until systemd is released with the fix for LP:#1671951
-    add_repos = "ppa:ddstreet/systemd"
-    upgrade_packages = "cloud-init,systemd"
 
 
-class EoanTestNetworkMtu(relbase.eoan, TestNetworkMtuAbs):
-    conf_file = "examples/tests/network_mtu_networkd.yaml"
+class FocalTestNetworkMtu(relbase.focal, TestNetworkMtuNetworkdAbs):
     __test__ = True
-    # Until systemd is released with the fix for LP:#1671951
-    add_repos = "ppa:ddstreet/systemd"
-    upgrade_packages = "cloud-init,systemd"
 
 
-class FocalTestNetworkMtu(relbase.focal, TestNetworkMtuAbs):
-    conf_file = "examples/tests/network_mtu_networkd.yaml"
+class GroovyTestNetworkMtu(relbase.groovy, TestNetworkMtuNetworkdAbs):
     __test__ = True
-    # Until systemd is released with the fix for LP:#1671951
-    add_repos = "ppa:ddstreet/systemd"
-    upgrade_packages = "cloud-init,systemd"
 
 
 class Centos66TestNetworkMtu(centos_relbase.centos66_xenial,
diff --git a/tests/vmtests/test_network_ovs.py b/tests/vmtests/test_network_ovs.py
index 3e23bd0..0cee17e 100644
--- a/tests/vmtests/test_network_ovs.py
+++ b/tests/vmtests/test_network_ovs.py
@@ -34,12 +34,11 @@ class BionicTestNetworkOvs(relbase.bionic, TestNetworkOvsAbs):
     __test__ = True
 
 
-class EoanTestNetworkOvs(relbase.eoan, TestNetworkOvsAbs):
+class FocalTestNetworkOvs(relbase.focal, TestNetworkOvsAbs):
     __test__ = True
 
 
-class FocalTestNetworkOvs(relbase.focal, TestNetworkOvsAbs):
+class GroovyTestNetworkOvs(relbase.groovy, 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 80ff2cd..e0abd54 100644
--- a/tests/vmtests/test_network_static.py
+++ b/tests/vmtests/test_network_static.py
@@ -28,11 +28,11 @@ class BionicTestNetworkStatic(relbase.bionic, TestNetworkStaticAbs):
     __test__ = True
 
 
-class EoanTestNetworkStatic(relbase.eoan, TestNetworkStaticAbs):
+class FocalTestNetworkStatic(relbase.focal, TestNetworkStaticAbs):
     __test__ = True
 
 
-class FocalTestNetworkStatic(relbase.focal, TestNetworkStaticAbs):
+class GroovyTestNetworkStatic(relbase.groovy, TestNetworkStaticAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_static_routes.py b/tests/vmtests/test_network_static_routes.py
index dfcbffe..f99d9d5 100644
--- a/tests/vmtests/test_network_static_routes.py
+++ b/tests/vmtests/test_network_static_routes.py
@@ -28,13 +28,13 @@ class BionicTestNetworkStaticRoutes(relbase.bionic,
     __test__ = True
 
 
-class EoanTestNetworkStaticRoutes(relbase.eoan,
-                                  TestNetworkStaticRoutesAbs):
+class FocalTestNetworkStaticRoutes(relbase.focal,
+                                   TestNetworkStaticRoutesAbs):
     __test__ = True
 
 
-class FocalTestNetworkStaticRoutes(relbase.focal,
-                                   TestNetworkStaticRoutesAbs):
+class GroovyTestNetworkStaticRoutes(relbase.groovy,
+                                    TestNetworkStaticRoutesAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_network_vlan.py b/tests/vmtests/test_network_vlan.py
index 4a8d776..9f1094b 100644
--- a/tests/vmtests/test_network_vlan.py
+++ b/tests/vmtests/test_network_vlan.py
@@ -76,16 +76,17 @@ class BionicTestNetworkVlan(relbase.bionic, TestNetworkVlanAbs):
     __test__ = True
 
 
-class EoanTestNetworkVlan(relbase.eoan, TestNetworkVlanAbs):
+class FocalTestNetworkVlan(relbase.focal, TestNetworkVlanAbs):
     __test__ = True
 
     def test_ip_output(self):
         return super().test_ip_output()
 
 
-class FocalTestNetworkVlan(relbase.focal, TestNetworkVlanAbs):
+class GroovyTestNetworkVlan(relbase.groovy, TestNetworkVlanAbs):
     __test__ = True
 
+    @TestNetworkVlanAbs.skip_by_date("1888726", "2020-10-15")
     def test_ip_output(self):
         return super().test_ip_output()
 
diff --git a/tests/vmtests/test_nvme.py b/tests/vmtests/test_nvme.py
index c1576fa..39f9f3c 100644
--- a/tests/vmtests/test_nvme.py
+++ b/tests/vmtests/test_nvme.py
@@ -73,7 +73,7 @@ class BionicTestNvme(relbase.bionic, TestNvmeAbs):
     __test__ = True
 
 
-class EoanTestNvme(relbase.eoan, TestNvmeAbs):
+class GroovyTestNvme(relbase.groovy, TestNvmeAbs):
     __test__ = True
 
 
@@ -139,12 +139,11 @@ class BionicTestNvmeBcache(relbase.bionic, TestNvmeBcacheAbs):
     __test__ = True
 
 
-class EoanTestNvmeBcache(relbase.eoan, TestNvmeBcacheAbs):
+class FocalTestNvmeBcache(relbase.focal, TestNvmeBcacheAbs):
     __test__ = True
 
 
-@TestNvmeBcacheAbs.skip_by_date("1861941", fixby="2020-07-15")
-class FocalTestNvmeBcache(relbase.focal, TestNvmeBcacheAbs):
+class GroovyTestNvmeBcache(relbase.groovy, TestNvmeBcacheAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_panic.py b/tests/vmtests/test_panic.py
index fe4005e..7b1fdbe 100644
--- a/tests/vmtests/test_panic.py
+++ b/tests/vmtests/test_panic.py
@@ -28,4 +28,9 @@ class TestInstallPanic(VMBaseClass):
 class FocalTestInstallPanic(relbase.focal, TestInstallPanic):
     __test__ = True
 
+
+class GroovyTestInstallPanic(relbase.groovy, 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 ff21f20..ed14719 100644
--- a/tests/vmtests/test_pollinate_useragent.py
+++ b/tests/vmtests/test_pollinate_useragent.py
@@ -61,11 +61,11 @@ class BionicTestPollinateUserAgent(relbase.bionic, TestPollinateUserAgent):
     __test__ = True
 
 
-class EoanTestPollinateUserAgent(relbase.eoan, TestPollinateUserAgent):
+class FocalTestPollinateUserAgent(relbase.focal, TestPollinateUserAgent):
     __test__ = True
 
 
-class FocalTestPollinateUserAgent(relbase.focal, TestPollinateUserAgent):
+class GroovyTestPollinateUserAgent(relbase.groovy, TestPollinateUserAgent):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_preserve.py b/tests/vmtests/test_preserve.py
index f02ba6c..998218c 100644
--- a/tests/vmtests/test_preserve.py
+++ b/tests/vmtests/test_preserve.py
@@ -25,11 +25,11 @@ class BionicTestPreserve(relbase.bionic, TestPreserve):
     __test__ = True
 
 
-class EoanTestPreserve(relbase.eoan, TestPreserve):
+class FocalTestPreserve(relbase.focal, TestPreserve):
     __test__ = True
 
 
-class FocalTestPreserve(relbase.focal, TestPreserve):
+class GroovyTestPreserve(relbase.groovy, TestPreserve):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_preserve_bcache.py b/tests/vmtests/test_preserve_bcache.py
index e2d2a34..bd91c5a 100644
--- a/tests/vmtests/test_preserve_bcache.py
+++ b/tests/vmtests/test_preserve_bcache.py
@@ -56,11 +56,11 @@ class BionicTestPreserveBcache(relbase.bionic, TestPreserveBcache):
     __test__ = True
 
 
-class EoanTestPreserveBcache(relbase.eoan, TestPreserveBcache):
+class FocalTestPreserveBcache(relbase.focal, TestPreserveBcache):
     __test__ = True
 
 
-class FocalTestPreserveBcache(relbase.focal, TestPreserveBcache):
+class GroovyTestPreserveBcache(relbase.groovy, TestPreserveBcache):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_preserve_lvm.py b/tests/vmtests/test_preserve_lvm.py
index 90f15cb..0ed7ad4 100644
--- a/tests/vmtests/test_preserve_lvm.py
+++ b/tests/vmtests/test_preserve_lvm.py
@@ -69,11 +69,11 @@ class BionicTestLvmPreserve(relbase.bionic, TestLvmPreserveAbs):
     __test__ = True
 
 
-class EoanTestLvmPreserve(relbase.eoan, TestLvmPreserveAbs):
+class FocalTestLvmPreserve(relbase.focal, TestLvmPreserveAbs):
     __test__ = True
 
 
-class FocalTestLvmPreserve(relbase.focal, TestLvmPreserveAbs):
+class GroovyTestLvmPreserve(relbase.groovy, TestLvmPreserveAbs):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_preserve_partition_wipe_vg.py b/tests/vmtests/test_preserve_partition_wipe_vg.py
index 96346ff..58b1f65 100644
--- a/tests/vmtests/test_preserve_partition_wipe_vg.py
+++ b/tests/vmtests/test_preserve_partition_wipe_vg.py
@@ -25,11 +25,11 @@ class BionicTestPreserveWipeLvm(relbase.bionic, TestPreserveWipeLvm):
     __test__ = True
 
 
-class EoanTestPreserveWipeLvm(relbase.eoan, TestPreserveWipeLvm):
+class FocalTestPreserveWipeLvm(relbase.focal, TestPreserveWipeLvm):
     __test__ = True
 
 
-class FocalTestPreserveWipeLvm(relbase.focal, TestPreserveWipeLvm):
+class GroovyTestPreserveWipeLvm(relbase.groovy, TestPreserveWipeLvm):
     __test__ = True
 
 
@@ -48,11 +48,12 @@ class BionicTestPreserveWipeLvmSimple(relbase.bionic,
     __test__ = True
 
 
-class EoanTestPreserveWipeLvmSimple(relbase.eoan, TestPreserveWipeLvmSimple):
+class FocalTestPreserveWipeLvmSimple(relbase.focal, TestPreserveWipeLvmSimple):
     __test__ = True
 
 
-class FocalTestPreserveWipeLvmSimple(relbase.focal, TestPreserveWipeLvmSimple):
+class GroovyTestPreserveWipeLvmSimple(relbase.groovy,
+                                      TestPreserveWipeLvmSimple):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_preserve_raid.py b/tests/vmtests/test_preserve_raid.py
index cf3a6bb..15f2f50 100644
--- a/tests/vmtests/test_preserve_raid.py
+++ b/tests/vmtests/test_preserve_raid.py
@@ -25,11 +25,11 @@ class BionicTestPreserveRAID(relbase.bionic, TestPreserveRAID):
     __test__ = True
 
 
-class EoanTestPreserveRAID(relbase.eoan, TestPreserveRAID):
+class FocalTestPreserveRAID(relbase.focal, TestPreserveRAID):
     __test__ = True
 
 
-class FocalTestPreserveRAID(relbase.focal, TestPreserveRAID):
+class GroovyTestPreserveRAID(relbase.groovy, TestPreserveRAID):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_raid5_bcache.py b/tests/vmtests/test_raid5_bcache.py
index 0f0b87b..3fdb217 100644
--- a/tests/vmtests/test_raid5_bcache.py
+++ b/tests/vmtests/test_raid5_bcache.py
@@ -88,16 +88,16 @@ class BionicTestRaid5Bcache(relbase.bionic, TestMdadmBcacheAbs):
     __test__ = True
 
 
-class EoanTestRaid5Bcache(relbase.eoan, TestMdadmBcacheAbs):
-    __test__ = True
-
-
 class FocalTestRaid5Bcache(relbase.focal, TestMdadmBcacheAbs):
     __test__ = True
 
-    @TestMdadmBcacheAbs.skip_by_date("1861941", fixby="2020-07-15")
+    @TestMdadmBcacheAbs.skip_by_date("1861941", fixby="2020-09-15")
     def test_fstab(self):
         return super().test_fstab()
 
 
+class GroovyTestRaid5Bcache(relbase.groovy, TestMdadmBcacheAbs):
+    __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 749ea24..87afcfb 100644
--- a/tests/vmtests/test_reuse_lvm_member.py
+++ b/tests/vmtests/test_reuse_lvm_member.py
@@ -21,13 +21,13 @@ class BionicTestReuseLVMMemberPartition(relbase.bionic,
     __test__ = True
 
 
-class EoanTestReuseLVMMemberPartition(relbase.eoan,
-                                      TestReuseLVMMemberPartition):
+class FocalTestReuseLVMMemberPartition(relbase.focal,
+                                       TestReuseLVMMemberPartition):
     __test__ = True
 
 
-class FocalTestReuseLVMMemberPartition(relbase.focal,
-                                       TestReuseLVMMemberPartition):
+class GroovyTestReuseLVMMemberPartition(relbase.groovy,
+                                        TestReuseLVMMemberPartition):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_reuse_msdos_partitions.py b/tests/vmtests/test_reuse_msdos_partitions.py
index f8e20d9..9f18d3c 100644
--- a/tests/vmtests/test_reuse_msdos_partitions.py
+++ b/tests/vmtests/test_reuse_msdos_partitions.py
@@ -18,13 +18,13 @@ class BionicTestReuseMSDOSPartitions(relbase.bionic,
     __test__ = True
 
 
-class EoanTestReuseMSDOSPartitions(relbase.eoan,
-                                   TestReuseMSDOSPartitions):
+class FocalTestReuseMSDOSPartitions(relbase.focal,
+                                    TestReuseMSDOSPartitions):
     __test__ = True
 
 
-class FocalTestReuseMSDOSPartitions(relbase.focal,
-                                    TestReuseMSDOSPartitions):
+class GroovyTestReuseMSDOSPartitions(relbase.groovy,
+                                     TestReuseMSDOSPartitions):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_reuse_raid_member.py b/tests/vmtests/test_reuse_raid_member.py
index 425105f..7be98f3 100644
--- a/tests/vmtests/test_reuse_raid_member.py
+++ b/tests/vmtests/test_reuse_raid_member.py
@@ -28,11 +28,11 @@ class BionicTestReuseRAIDMember(relbase.bionic, TestReuseRAIDMember):
     __test__ = True
 
 
-class EoanTestReuseRAIDMember(relbase.eoan, TestReuseRAIDMember):
+class FocalTestReuseRAIDMember(relbase.focal, TestReuseRAIDMember):
     __test__ = True
 
 
-class FocalTestReuseRAIDMember(relbase.focal, TestReuseRAIDMember):
+class GroovyTestReuseRAIDMember(relbase.groovy, TestReuseRAIDMember):
     __test__ = True
 
 
@@ -41,13 +41,13 @@ class BionicTestReuseRAIDMemberPartition(relbase.bionic,
     __test__ = True
 
 
-class EoanTestReuseRAIDMemberPartition(relbase.eoan,
-                                       TestReuseRAIDMemberPartition):
+class FocalTestReuseRAIDMemberPartition(relbase.focal,
+                                        TestReuseRAIDMemberPartition):
     __test__ = True
 
 
-class FocalTestReuseRAIDMemberPartition(relbase.focal,
-                                        TestReuseRAIDMemberPartition):
+class GroovyTestReuseRAIDMemberPartition(relbase.groovy,
+                                         TestReuseRAIDMemberPartition):
     __test__ = True
 
 
diff --git a/tests/vmtests/test_reuse_uefi_esp.py b/tests/vmtests/test_reuse_uefi_esp.py
index 31c5e7d..46e7ac7 100644
--- a/tests/vmtests/test_reuse_uefi_esp.py
+++ b/tests/vmtests/test_reuse_uefi_esp.py
@@ -3,20 +3,22 @@
 from .test_uefi_basic import TestBasicAbs
 from .releases import base_vm_classes as relbase
 from .releases import centos_base_vm_classes as cent_rbase
+from curtin.commands.curthooks import uefi_find_duplicate_entries
+from curtin import util
 
 
 class TestUefiReuseEspAbs(TestBasicAbs):
     conf_file = "examples/tests/uefi_reuse_esp.yaml"
 
     def test_efiboot_menu_has_one_distro_entry(self):
-        efiboot_mgr_content = self.load_collect_file("efibootmgr.out")
-        distro_lines = [line for line in efiboot_mgr_content.splitlines()
-                        if self.target_distro in line]
-        print(distro_lines)
-        self.assertEqual(1, len(distro_lines))
+        efi_output = util.parse_efibootmgr(
+            self.load_collect_file("efibootmgr.out"))
+        duplicates = uefi_find_duplicate_entries(
+            grubcfg=None, target=None, efi_output=efi_output)
+        print(duplicates)
+        self.assertEqual(0, len(duplicates))
 
 
-@TestUefiReuseEspAbs.skip_by_date("1881030", fixby="2020-07-15")
 class Cent70TestUefiReuseEsp(cent_rbase.centos70_bionic, TestUefiReuseEspAbs):
     __test__ = True
 
@@ -33,14 +35,14 @@ class BionicTestUefiReuseEsp(relbase.bionic, TestUefiReuseEspAbs):
         return super().test_efiboot_menu_has_one_distro_entry()
 
 
-class EoanTestUefiReuseEsp(relbase.eoan, TestUefiReuseEspAbs):
+class FocalTestUefiReuseEsp(relbase.focal, TestUefiReuseEspAbs):
     __test__ = True
 
     def test_efiboot_menu_has_one_distro_entry(self):
         return super().test_efiboot_menu_has_one_distro_entry()
 
 
-class FocalTestUefiReuseEsp(relbase.focal, TestUefiReuseEspAbs):
+class GroovyTestUefiReuseEsp(relbase.groovy, TestUefiReuseEspAbs):
     __test__ = True
 
     def test_efiboot_menu_has_one_distro_entry(self):
diff --git a/tests/vmtests/test_simple.py b/tests/vmtests/test_simple.py
index b34a6fc..9e71047 100644
--- a/tests/vmtests/test_simple.py
+++ b/tests/vmtests/test_simple.py
@@ -49,14 +49,14 @@ class BionicTestSimple(relbase.bionic, TestSimple):
         self.output_files_exist(["netplan.yaml"])
 
 
-class EoanTestSimple(relbase.eoan, TestSimple):
+class FocalTestSimple(relbase.focal, TestSimple):
     __test__ = True
 
     def test_output_files_exist(self):
         self.output_files_exist(["netplan.yaml"])
 
 
-class FocalTestSimple(relbase.focal, TestSimple):
+class GroovyTestSimple(relbase.groovy, TestSimple):
     __test__ = True
 
     def test_output_files_exist(self):
@@ -105,14 +105,14 @@ class BionicTestSimpleStorage(relbase.bionic, TestSimpleStorage):
         self.output_files_exist(["netplan.yaml"])
 
 
-class EoanTestSimpleStorage(relbase.eoan, TestSimpleStorage):
+class FocalTestSimpleStorage(relbase.focal, TestSimpleStorage):
     __test__ = True
 
     def test_output_files_exist(self):
         self.output_files_exist(["netplan.yaml"])
 
 
-class FocalTestSimpleStorage(relbase.focal, TestSimpleStorage):
+class GroovyTestSimpleStorage(relbase.groovy, TestSimpleStorage):
     __test__ = True
 
     def test_output_files_exist(self):
@@ -145,4 +145,11 @@ class FocalTestGrubNoDefaults(relbase.focal, TestGrubNoDefaults):
         self.output_files_exist(["netplan.yaml"])
 
 
+class GroovyTestGrubNoDefaults(relbase.groovy, 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 90940dd..932c1c8 100644
--- a/tests/vmtests/test_uefi_basic.py
+++ b/tests/vmtests/test_uefi_basic.py
@@ -89,6 +89,11 @@ class PreciseHWETUefiTestBasic(relbase.precise_hwe_t, PreciseUefiTestBasic):
     __test__ = False
 
 
+class TrustyHWEXUefiTestBasic(relbase.trusty_hwe_x, TestBasicAbs):
+    supported_releases = ['trusty']  # avoid unsupported release skiptest
+    __test__ = False
+
+
 class XenialGAUefiTestBasic(relbase.xenial_ga, TestBasicAbs):
     __test__ = True
 
@@ -105,11 +110,11 @@ class BionicUefiTestBasic(relbase.bionic, TestBasicAbs):
     __test__ = True
 
 
-class EoanUefiTestBasic(relbase.eoan, TestBasicAbs):
+class FocalUefiTestBasic(relbase.focal, TestBasicAbs):
     __test__ = True
 
 
-class FocalUefiTestBasic(relbase.focal, TestBasicAbs):
+class GroovyUefiTestBasic(relbase.groovy, TestBasicAbs):
     __test__ = True
 
 
@@ -128,12 +133,12 @@ class BionicUefiTestBasic4k(relbase.bionic, TestBasicAbs):
     disk_block_size = 4096
 
 
-class EoanUefiTestBasic4k(relbase.eoan, TestBasicAbs):
+class FocalUefiTestBasic4k(relbase.focal, TestBasicAbs):
     __test__ = True
     disk_block_size = 4096
 
 
-class FocalUefiTestBasic4k(relbase.focal, TestBasicAbs):
+class GroovyUefiTestBasic4k(relbase.groovy, TestBasicAbs):
     __test__ = True
     disk_block_size = 4096
 
diff --git a/tests/vmtests/test_zfsroot.py b/tests/vmtests/test_zfsroot.py
index c9c73a3..952bf7b 100644
--- a/tests/vmtests/test_zfsroot.py
+++ b/tests/vmtests/test_zfsroot.py
@@ -96,12 +96,12 @@ class BionicTestZfsRoot(relbase.bionic, TestZfsRootAbs):
     __test__ = True
 
 
-class EoanTestZfsRoot(relbase.eoan, TestZfsRootAbs):
+class FocalTestZfsRoot(relbase.focal, TestZfsRootAbs):
     __test__ = True
     mem = 4096
 
 
-class FocalTestZfsRoot(relbase.focal, TestZfsRootAbs):
+class GroovyTestZfsRoot(relbase.groovy, TestZfsRootAbs):
     __test__ = True
     mem = 4096
 
@@ -125,13 +125,12 @@ class BionicTestZfsRootFsType(relbase.bionic, TestZfsRootFsTypeAbs):
     __test__ = True
 
 
-class EoanTestZfsRootFsType(relbase.eoan, TestZfsRootFsTypeAbs):
+class FocalTestZfsRootFsType(relbase.focal, TestZfsRootFsTypeAbs):
     __test__ = True
     mem = 4096
 
 
-class FocalTestZfsRootFsType(relbase.focal, TestZfsRootFsTypeAbs):
+class GroovyTestZfsRootFsType(relbase.groovy, TestZfsRootFsTypeAbs):
     __test__ = True
-    mem = 4096
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tools/curtainer b/tools/curtainer
index 466d719..b24884b 100755
--- a/tools/curtainer
+++ b/tools/curtainer
@@ -153,20 +153,35 @@ main() {
     fi
     getsource="${getsource%/}"
 
-    lxc launch "$src" "$name" || fail "failed lxc launch $src $name"
+    # launch container; mask snapd.seeded.service; not needed
+    {
+        lxc init "$src" "$name" &&
+        lxc file push \
+            /dev/null ${name}/etc/systemd/system/snapd.seeded.service &&
+        lxc start ${name}
+    } || fail "failed lxc launch $src $name"
     CONTAINER=$name
 
     wait_for_ready "$name" $maxwait $VERBOSITY ||
         fail "$name did not become ready after $maxwait"
 
     inside "$name" which eatmydata >/dev/null || eatmydata=""
+    release=$(inside $name lsb_release -sc) ||
+        fail "$name did not have a lsb release codename"
+
+    # curtin depends on zfsutils-linux via probert-storage, but zfsutils-linux
+    # can't be installed in an unprivileged container as it fails to start
+    # the zfs-mount and zfs-share services as /dev/zfs is missing. We do
+    # not actually need ZFS to work in the container, so the problem can be
+    # worked around by masking the services before the package is installed.
+    inside "$name" systemctl mask zfs-mount || fail "failed to mask zfs-mount"
+    inside "$name" systemctl mask zfs-share || fail "failed to mask zfs-share"
 
     if $proposed; then
         mirror=$(inside $name awk '$1 == "deb" { print $2; exit(0); }' \
-            /etc/apt/sources.list) &&
-            rel=$(inside $name lsb_release -sc) ||
+            /etc/apt/sources.list) ||
             fail "failed to get mirror in $name"
-        line="$mirror $rel-proposed main universe"
+        line="$mirror $release-proposed main universe"
         local fname="/etc/apt/sources.list.d/proposed.list"
         debug 1 "enabling proposed in $fname: deb $line"
         inside "$name" sh -c "echo deb $line > $fname" ||
@@ -179,9 +194,30 @@ main() {
     if $daily; then
         local daily_ppa="ppa:curtin-dev/daily"
         debug 1 "enabling daily: $daily_ppa"
-        inside "$name" add-apt-repository --enable-source --yes \
-            "${daily_ppa}" ||
-            fail "failed add-apt-repository for daily."
+        local addaptrepo="add-apt-repository"
+        inside "$name" which $addaptrepo >/dev/null || addaptrepo=""
+        if [ -n "${addaptrepo}" ]; then
+            inside "$name" ${addaptrepo} --enable-source --yes --no-update \
+                "${daily_ppa}" ||
+                fail "failed add-apt-repository for daily."
+        else
+            # https://launchpad.net/~curtin-dev/+archive/ubuntu/daily
+            local url="http://ppa.launchpad.net/curtin-dev/daily/ubuntu";
+            local lfile="/etc/apt/sources.list.d/curtin-daily-ppa.list"
+            local kfile="/etc/apt/trusted.gpg.d/curtin-daily-ppa.asc"
+            local key="0x1bc30f715a3b861247a81a5e55fe7c8c0165013e"
+            local keyserver="keyserver.ubuntu.com"
+            local keyurl="https://${keyserver}/pks/lookup?op=get&search=${key}";
+            inside "$name" sh -c "
+                echo deb $url $release main > $lfile &&
+                wget -q \"$keyurl\" -O $kfile" ||
+                fail "failed to add $daily_ppa repository manually"
+            if [ "$getsource" != "none" ]; then
+                inside "$name" sh -c "
+                    echo deb-src $url $release main >> $lfile" ||
+                    fail "failed adding daily ppa deb-src to $lfile"
+            fi
+        fi
     fi
 
     line="Acquire::Languages \"none\";"
diff --git a/tools/jenkins-runner b/tools/jenkins-runner
index 253b722..375dc3c 100755
--- a/tools/jenkins-runner
+++ b/tools/jenkins-runner
@@ -5,6 +5,7 @@ topdir="${CURTIN_VMTEST_TOPDIR:-${WORKSPACE:-$PWD}/output}"
 pkeep=${CURTIN_VMTEST_KEEP_DATA_PASS:-logs,collect}
 fkeep=${CURTIN_VMTEST_KEEP_DATA_FAIL:-logs,collect}
 reuse=${CURTIN_VMTEST_REUSE_TOPDIR:-0}
+shuffle=${CURTIN_VMTEST_SHUFFLE_TESTS:-1}
 declare -i ltimeout=${CURTIN_VMTEST_IMAGE_LOCK_TIMEOUT:-"-1"}
 export CURTIN_VMTEST_TAR_DISKS=${CURTIN_VMTEST_TAR_DISKS:-0}
 export CURTIN_VMTEST_REUSE_TOPDIR=$reuse
@@ -14,6 +15,7 @@ export CURTIN_VMTEST_KEEP_DATA_FAIL=$fkeep
 export CURTIN_VMTEST_TOPDIR="$topdir"
 export CURTIN_VMTEST_LOG="${CURTIN_VMTEST_LOG:-$topdir/debug.log}"
 export CURTIN_VMTEST_PARALLEL=${CURTIN_VMTEST_PARALLEL:-0}
+export CURTIN_VMTEST_SHUFFLE_TESTS=$shuffle
 export IMAGE_DIR=${IMAGE_DIR:-/srv/images}
 
 # empty TGT_* variables in current env to avoid killing a pid we didn't start.
@@ -50,6 +52,15 @@ if [ "$reuse" != "1" ]; then
     mkdir -p "$topdir" || fail "failed mkdir $topdir"
 fi
 
+# Use 'shuf' to randomize test case execution order
+if [ "$shuffle" == "1" ]; then
+    SHUFFLE="shuf"
+else
+    # when disabled just repeat the input to output
+    SHUFFLE="tee"
+fi
+
+
 start_s=$(date +%s)
 parallel=${CURTIN_VMTEST_PARALLEL}
 ntfilters=( )
@@ -83,9 +94,10 @@ if [ "${#tests[@]}" -ne 0 -a "${#ntfilters[@]}" -ne 0 ]; then
     error "test arguments provided were: ${#tests[*]}"
     fail
 elif [ "${#tests[@]}" -eq 0 -a "${#ntfilters[@]}" -eq 0 ]; then
-    tests=( tests/vmtests )
+    # run filter without args to enumerate all tests and maybe shuffle
+    tests=( $(./tools/vmtest-filter | ${SHUFFLE}) )
 elif [ "${#ntfilters[@]}" -ne 0 ]; then
-    tests=( $(./tools/vmtest-filter "${ntfilters[@]}") )
+    tests=( $(./tools/vmtest-filter "${ntfilters[@]}" | ${SHUFFLE}) )
     if [ "${#tests[@]}" -eq 0 ]; then
         error "Failed to find any tests with filter(s): \"${ntfilters[*]}\""
         fail "Try testing filters with: ./tools/vmtest-filter ${ntfilters[*]}"
diff --git a/tools/run-pep8 b/tools/run-pep8
index c27a96c..227129c 100755
--- a/tools/run-pep8
+++ b/tools/run-pep8
@@ -9,10 +9,10 @@ pycheck_dirs=(
     "tools/block-discover-to-config"
     "tools/curtin-log-print"
     "tools/noproxy"
-    "tools/remove-vmtest-release"
     "tools/schema-validate-storage"
     "tools/ssh-keys-list"
     "tools/vmtest-filter"
+    "tools/vmtest-remove-release"
     "tools/vmtest-sync-images"
     "tools/webserv"
     "tools/write-curtin"
diff --git a/tools/remove-vmtest-release b/tools/vmtest-remove-release
similarity index 97%
rename from tools/remove-vmtest-release
rename to tools/vmtest-remove-release
old mode 100644
new mode 100755
index 8ab9b2e..d2c5f83
--- a/tools/remove-vmtest-release
+++ b/tools/vmtest-remove-release
@@ -36,7 +36,7 @@ def clean_file(fname, distro):
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(
-        prog="remove-vmtest-release",
+        prog="vmtest-remove-release",
         description="Tool to remove vmtest classes by distro release")
     parser.add_argument('--distro-release', '-d',
                         action='store', required=True)
diff --git a/tox.ini b/tox.ini
index 72d56d4..04b43b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -60,7 +60,7 @@ commands = {envpython} -m pyflakes {posargs:curtin/ tests/ tools/}
 # set basepython because tox 1.6 (trusty) does not support generated environments
 basepython = python3
 deps = {[testenv]deps}
-    pylint==2.3.1
+    pylint==2.6.0
     git+https://git.launchpad.net/simplestreams
 commands = {envpython} -m pylint --errors-only {posargs:curtin tests/vmtests}
 

Follow ups