← Back to team overview

curtin-dev team mailing list archive

[Merge] ~raharper/curtin:ubuntu-devel-new-upstream-snapshot-20200507 into curtin:ubuntu/devel

 

Ryan Harper has proposed merging ~raharper/curtin:ubuntu-devel-new-upstream-snapshot-20200507 into curtin:ubuntu/devel.

Requested reviews:
  curtin developers (curtin-dev)
Related bugs:
  Bug #1873909 in curtin: "Recently introduced reuse-lvm-member-partition tests fail with rsync errors"
  https://bugs.launchpad.net/curtin/+bug/1873909
  Bug #1875085 in curtin (Ubuntu): "curtin install hook fails with No closing quotation on new 20.04 install"
  https://bugs.launchpad.net/ubuntu/+source/curtin/+bug/1875085
  Bug #1875903 in curtin: "Curtin fails if a partition with bootable flag is present"
  https://bugs.launchpad.net/curtin/+bug/1875903
  Bug #1876626 in curtin: "'toram' kernel parameter does not work with subiquity"
  https://bugs.launchpad.net/curtin/+bug/1876626
  Bug #1876848 in subiquity (Ubuntu): "Installation of Focal on a linux raid consistently fails"
  https://bugs.launchpad.net/ubuntu/+source/subiquity/+bug/1876848

For more details, see:
https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/383648
-- 
Your team curtin developers is requested to review the proposed merge of ~raharper/curtin:ubuntu-devel-new-upstream-snapshot-20200507 into curtin:ubuntu/devel.
diff --git a/Makefile b/Makefile
index 827102c..68a3ad3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 TOP := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
 CWD := $(shell pwd)
-PYTHON ?= python3
+PYTHON2 ?= python2
+PYTHON3 ?= python3
 COVERAGE ?= 1
 DEFAULT_COVERAGEOPTS = --with-coverage --cover-erase --cover-branches --cover-package=curtin --cover-inclusive 
 ifeq ($(COVERAGE), 1)
@@ -9,6 +10,8 @@ endif
 CURTIN_VMTEST_IMAGE_SYNC ?= False
 export CURTIN_VMTEST_IMAGE_SYNC
 noseopts ?= -vv --nologcapture
+pylintopts ?= --rcfile=pylintrc --errors-only
+target_dirs ?= curtin tests tools
 
 build:
 
@@ -26,16 +29,22 @@ pep8:
 	@$(CWD)/tools/run-pep8
 
 pyflakes:
-	@$(CWD)/tools/run-pyflakes
+	$(PYTHON2) -m pyflakes $(target_dirs)
 
 pyflakes3:
-	@$(CWD)/tools/run-pyflakes3
+	$(PYTHON3) -m pyflakes $(target_dirs)
+
+pylint:
+	$(PYTHON2) -m pylint $(pylintopts) $(target_dirs)
+
+pylint3:
+	$(PYTHON3) -m pylint $(pylintopts) $(target_dirs)
 
 unittest2:
-	nosetests $(coverageopts) $(noseopts) tests/unittests
+	$(PYTHON2) -m nose $(coverageopts) $(noseopts) tests/unittests
 
 unittest3:
-	nosetests3 $(coverageopts) $(noseopts) tests/unittests
+	$(PYTHON3) -m nose $(coverageopts) $(noseopts) tests/unittests
 
 unittest: unittest2 unittest3
 
@@ -53,7 +62,7 @@ check-doc-deps:
 
 # By default don't sync images when running all tests.
 vmtest: schema-validate
-	nosetests3 $(noseopts) tests/vmtests
+	$(PYTHON3) -m nose $(noseopts) tests/vmtests
 
 vmtest-deps:
 	@$(CWD)/tools/vmtest-system-setup
diff --git a/curtin/__init__.py b/curtin/__init__.py
index 142d288..7114b3b 100644
--- a/curtin/__init__.py
+++ b/curtin/__init__.py
@@ -22,6 +22,8 @@ FEATURES = [
     'STORAGE_CONFIG_V1',
     # install supports the 'storage' config version 1 for DD images
     'STORAGE_CONFIG_V1_DD',
+    # has separate 'preserve' and 'wipe' config options
+    'STORAGE_CONFIG_SEPARATE_PRESERVE_AND_WIPE'
     # subcommand 'system-install' is present
     'SUBCOMMAND_SYSTEM_INSTALL',
     # subcommand 'system-upgrade' is present
diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
index f30c5df..35a91c6 100644
--- a/curtin/block/__init__.py
+++ b/curtin/block/__init__.py
@@ -16,6 +16,9 @@ from curtin.udev import udevadm_settle, udevadm_info
 from curtin import storage_config
 
 
+SECTOR_SIZE_BYTES = 512
+
+
 def get_dev_name_entry(devname):
     """
     convert device name to path in /dev
@@ -111,7 +114,9 @@ def partition_kname(disk_kname, partition_number):
         # linux will create a -partX symlink against the disk by-id name.
         devpath = '/dev/' + disk_kname
         disk_link = get_device_mapper_links(devpath, first=True)
-        return '%s-part%s' % (disk_link, partition_number)
+        return path_to_kname(
+                    os.path.realpath('%s-part%s' % (disk_link,
+                                                    partition_number)))
 
     for dev_type in ['bcache', 'nvme', 'mmcblk', 'cciss', 'mpath', 'md']:
         if disk_kname.startswith(dev_type):
@@ -138,6 +143,8 @@ def sys_block_path(devname, add=None, strict=True):
     toks = ['/sys/class/block']
     # insert parent dev if devname is partition
     devname = os.path.normpath(devname)
+    if devname.startswith('/dev/') and not os.path.exists(devname):
+        LOG.warning('block.sys_block_path: devname %s does not exist', devname)
     (parent, partnum) = get_blockdev_for_partition(devname, strict=strict)
     if partnum:
         toks.append(path_to_kname(parent))
@@ -241,6 +248,86 @@ def _lsblock(args=None):
     return _lsblock_pairs_to_dict(out)
 
 
+def sfdisk_info(devpath):
+    ''' returns dict of sfdisk info about disk partitions
+    {
+      "label": "gpt",
+      "id": "877716F7-31D0-4D56-A1ED-4D566EFE418E",
+      "device": "/dev/vda",
+      "unit": "sectors",
+      "firstlba": 34,
+      "lastlba": 41943006,
+      "partitions": [
+         {"node": "/dev/vda1", "start": 227328, "size": 41715679,
+          "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+          "uuid": "60541CAF-E2AC-48CD-BF89-AF16051C833F"},
+      ]
+    }
+    {
+      "label":"dos",
+      "id":"0xb0dbdde1",
+      "device":"/dev/vdb",
+      "unit":"sectors",
+      "partitions": [
+         {"node":"/dev/vdb1", "start":2048, "size":8388608,
+          "type":"83", "bootable":true},
+         {"node":"/dev/vdb2", "start":8390656, "size":8388608, "type":"83"},
+         {"node":"/dev/vdb3", "start":16779264, "size":62914560, "type":"5"},
+         {"node":"/dev/vdb5", "start":16781312, "size":31457280, "type":"83"},
+         {"node":"/dev/vdb6", "start":48240640, "size":10485760, "type":"83"},
+         {"node":"/dev/vdb7", "start":58728448, "size":20965376, "type":"83"}
+      ]
+    }
+    '''
+    (parent, partnum) = get_blockdev_for_partition(devpath)
+    try:
+        (out, _err) = util.subp(['sfdisk', '--json', parent], capture=True)
+    except util.ProcessExecutionError as e:
+        out = None
+        LOG.exception(e)
+    if out is not None:
+        return util.load_json(out).get('partitiontable', {})
+
+    return {}
+
+
+def get_partition_sfdisk_info(devpath, sfdisk_info=None):
+    if not sfdisk_info:
+        sfdisk_info = sfdisk_info(devpath)
+
+    entry = [part for part in sfdisk_info['partitions']
+             if part['node'] == devpath]
+    if len(entry) != 1:
+        raise RuntimeError('Device %s not present in sfdisk dump:\n%s' %
+                           devpath, util.json_dumps(sfdisk_info))
+    return entry.pop()
+
+
+def dmsetup_info(devname):
+    ''' returns dict of info about device mapper dev.
+
+    {'blkdevname': 'dm-0',
+     'blkdevs_used': 'sda5',
+     'name': 'sda5_crypt',
+     'subsystem': 'CRYPT',
+     'uuid': 'CRYPT-LUKS1-2b370697149743b0b2407d11f88311f1-sda5_crypt'
+    }
+    '''
+    _SEP = '='
+    fields = ('name,uuid,blkdevname,blkdevs_used,subsystem'.split(','))
+    try:
+        (out, _err) = util.subp(['dmsetup', 'info', devname, '-C', '-o',
+                                 ','.join(fields), '--noheading',
+                                 '--separator', _SEP], capture=True)
+    except util.ProcessExecutionError as e:
+        LOG.error('Failed to run dmsetup info:', e)
+        return {}
+
+    values = out.strip().split(_SEP)
+    info = dict(zip(fields, values))
+    return info
+
+
 def get_unused_blockdev_info():
     """
     return a list of unused block devices.
@@ -456,7 +543,7 @@ def blkid(devs=None, cache=True):
     return data
 
 
-def detect_multipath(target_mountpoint):
+def _legacy_detect_multipath(target_mountpoint=None):
     """
     Detect if the operating system has been installed to a multipath device.
     """
@@ -478,7 +565,7 @@ def detect_multipath(target_mountpoint):
     # while installing the system.
     rescan_block_devices()
     binfo = blkid(cache=False)
-    LOG.debug("detect_multipath found blkid info: %s", binfo)
+    LOG.debug("legacy_detect_multipath found blkid info: %s", binfo)
     # get_devices_for_mp may return multiple devices by design. It is not yet
     # implemented but it should return multiple devices when installer creates
     # separate disk partitions for / and /boot. We need to do UUID-based
@@ -510,6 +597,71 @@ def detect_multipath(target_mountpoint):
     return False
 
 
+def _device_is_multipathed(devpath):
+    devpath = os.path.realpath(devpath)
+    info = udevadm_info(devpath)
+    if multipath.is_mpath_device(devpath, info=info):
+        return True
+    if multipath.is_mpath_partition(devpath, info=info):
+        return True
+
+    if devpath.startswith('/dev/dm-'):
+        # check members of composed devices (LVM, dm-crypt)
+        if 'DM_LV_NAME' in info:
+            volgroup = info.get('DM_VG_NAME')
+            if volgroup:
+                if any((multipath.is_mpath_member(pv) for pv in
+                        lvm.get_pvols_in_volgroup(volgroup))):
+                    return True
+
+    elif devpath.startswith('/dev/md'):
+        if any((multipath.is_mpath_member(md) for md in
+                md_get_devices_list(devpath) + md_get_spares_list(devpath))):
+            return True
+
+    result = multipath.is_mpath_member(devpath)
+    return result
+
+
+def _md_get_members_list(devpath, state_check):
+    md_dev, _partno = get_blockdev_for_partition(devpath)
+    sysfs_md = sys_block_path(md_dev, "md")
+    return [
+        dev_path(dev[4:]) for dev in os.listdir(sysfs_md)
+        if (dev.startswith('dev-') and
+            state_check(
+                util.load_file(os.path.join(sysfs_md, dev, 'state')).strip()))]
+
+
+def md_get_spares_list(devpath):
+    def state_is_spare(state):
+        return (state == 'spare')
+    return _md_get_members_list(devpath, state_is_spare)
+
+
+def md_get_devices_list(devpath):
+    def state_is_not_spare(state):
+        return (state != 'spare')
+    return _md_get_members_list(devpath, state_is_not_spare)
+
+
+def detect_multipath(target_mountpoint=None):
+    if multipath.multipath_supported():
+        for device in (os.path.realpath(dev)
+                       for (dev, _mp, _vfs, _opts, _freq, _passno)
+                       in get_proc_mounts() if dev.startswith('/dev/')):
+            if not is_block_device(device):
+                # A tmpfs can be mounted with any old junk in the "device"
+                # field and unfortunately casper sometimes puts "/dev/shm"
+                # there, which is usually a directory. Ignore such cases.
+                # (See https://bugs.launchpad.net/bugs/1876626)
+                continue
+            if _device_is_multipathed(device):
+                return device
+
+    return _legacy_detect_multipath(target_mountpoint)
+
+
 def get_scsi_wwid(device, replace_whitespace=False):
     """
     Issue a call to scsi_id utility to get WWID of the device.
@@ -626,6 +778,15 @@ def get_blockdev_sector_size(devpath):
     return (int(logical), int(physical))
 
 
+def read_sys_block_size_bytes(device):
+    """ /sys/class/block/<device>/size and return integer value in bytes"""
+    device_dir = os.path.join('/sys/class/block', os.path.basename(device))
+    blockdev_size = os.path.join(device_dir, 'size')
+    with open(blockdev_size) as d:
+        size = int(d.read().strip()) * SECTOR_SIZE_BYTES
+    return size
+
+
 def get_volume_uuid(path):
     """
     Get uuid of disk with given path. This address uniquely identifies
@@ -754,18 +915,19 @@ def lookup_disk(serial):
     disks.sort(key=lambda x: len(x))
     LOG.debug('lookup_disks found: %s', disks)
     path = os.path.realpath("/dev/disk/by-id/%s" % disks[0])
-    LOG.debug('lookup_disks realpath(%s)=%s', disks[0], path)
+    # /dev/dm-X
     if multipath.is_mpath_device(path):
-        LOG.debug('Detected multipath device, finding a members')
         info = udevadm_info(path)
-        mpath_members = sorted(multipath.find_mpath_members(info['DM_NAME']))
-        LOG.debug('mpath members: %s', mpath_members)
-        if len(mpath_members):
-            path = mpath_members[0]
+        path = os.path.join('/dev/mapper', info['DM_NAME'])
+    # /dev/sdX
+    elif multipath.is_mpath_member(path):
+        mp_name = multipath.find_mpath_id_by_path(path)
+        path = os.path.join('/dev/mapper', mp_name)
 
     if not os.path.exists(path):
         raise ValueError("path '%s' to block device for disk with serial '%s' \
             does not exist" % (path, serial_udev))
+    LOG.debug('block.lookup_disk() returning path %s', path)
     return path
 
 
@@ -843,7 +1005,8 @@ def get_part_table_type(device):
     # signature, because a gpt formatted disk usually has a valid mbr to
     # protect the disk from being modified by older partitioning tools
     return ('gpt' if check_efi_signature(device) else
-            'dos' if check_dos_signature(device) else None)
+            'dos' if check_dos_signature(device) else
+            'vtoc' if check_vtoc_signature(device) else None)
 
 
 def check_dos_signature(device):
@@ -880,6 +1043,17 @@ def check_efi_signature(device):
                             offset=sector_size) == b'EFI PART'))
 
 
+def check_vtoc_signature(device):
+    """ check if the specified device has a vtoc partition table. """
+    devname = dev_path(path_to_kname(device))
+    try:
+        util.subp(['fdasd', '--table', devname])
+    except util.ProcessExecutionError:
+        return False
+
+    return True
+
+
 def is_extended_partition(device):
     """
     check if the specified device path is a dos extended partition
@@ -1145,7 +1319,7 @@ def get_supported_filesystems():
             for l in util.load_file(proc_fs).splitlines()]
 
 
-def discover():
+def _discover_get_probert_data():
     try:
         LOG.debug('Importing probert prober')
         from probert import prober
@@ -1156,7 +1330,11 @@ def discover():
     probe = prober.Prober()
     LOG.debug('Probing system for storage devices')
     probe.probe_storage()
-    probe_data = probe.get_results()
+    return probe.get_results()
+
+
+def discover():
+    probe_data = _discover_get_probert_data()
     if 'storage' not in probe_data:
         raise ValueError('Probing storage failed')
 
diff --git a/curtin/block/bcache.py b/curtin/block/bcache.py
index c31852e..188b4e0 100644
--- a/curtin/block/bcache.py
+++ b/curtin/block/bcache.py
@@ -2,13 +2,16 @@
 
 import errno
 import os
+import time
 
 from curtin import util
 from curtin.log import LOG
-from . import sys_block_path
+from curtin.udev import udevadm_settle
+from . import dev_path, sys_block_path
 
 # Wait up to 20 minutes (150 + 300 + 750 = 1200 seconds)
 BCACHE_RETRIES = [sleep for nap in [1, 2, 5] for sleep in [nap] * 150]
+BCACHE_REGISTRATION_RETRY = [0.2] * 60
 
 
 def superblock_asdict(device=None, data=None):
@@ -163,6 +166,15 @@ def get_cacheset_cachedev(cset_uuid):
     return None
 
 
+def attach_backing_to_cacheset(backing_device, cache_device, cset_uuid):
+    LOG.info("Attaching backing device to cacheset: "
+             "{} -> {} cset.uuid: {}".format(backing_device, cache_device,
+                                             cset_uuid))
+    backing_device_sysfs = sys_block_path(backing_device)
+    attach = os.path.join(backing_device_sysfs, "bcache", "attach")
+    util.write_file(attach, cset_uuid, mode=None)
+
+
 def get_backing_device(bcache_kname):
     """ For a given bcacheN kname, return the backing device
         bcache sysfs dir.
@@ -263,4 +275,233 @@ def _stop_device(device):
         util.wait_for_removal(bcache_stop, retries=BCACHE_RETRIES)
 
 
+def register_bcache(bcache_device):
+    LOG.debug('register_bcache: %s > /sys/fs/bcache/register', bcache_device)
+    util.write_file('/sys/fs/bcache/register', bcache_device, mode=None)
+
+
+def set_cache_mode(bcache_dev, cache_mode):
+    LOG.info("Setting cache_mode on {} to {}".format(bcache_dev, cache_mode))
+    cache_mode_file = '/sys/block/{}/bcache/cache_mode'.format(bcache_dev)
+    util.write_file(cache_mode_file, cache_mode, mode=None)
+
+
+def validate_bcache_ready(bcache_device, bcache_sys_path):
+    """ check if bcache is ready, dump info
+
+    For cache devices, we expect to find a cacheN symlink
+    which will point to the underlying cache device; Find
+    this symlink, read it and compare bcache_device
+    specified in the parameters.
+
+    For backing devices, we expec to find a dev symlink
+    pointing to the bcacheN device to which the backing
+    device is enslaved.  From the dev symlink, we can
+    read the bcacheN holders list, which should contain
+    the backing device kname.
+
+    In either case, if we fail to find the correct
+    symlinks in sysfs, this method will raise
+    an OSError indicating the missing attribute.
+    """
+    # cacheset
+    # /sys/fs/bcache/<uuid>
+
+    # cache device
+    # /sys/class/block/<cdev>/bcache/set -> # .../fs/bcache/uuid
+
+    # backing
+    # /sys/class/block/<bdev>/bcache/cache -> # .../block/bcacheN
+    # /sys/class/block/<bdev>/bcache/dev -> # .../block/bcacheN
+
+    if bcache_sys_path.startswith('/sys/fs/bcache'):
+        LOG.debug("validating bcache caching device '%s' from sys_path"
+                  " '%s'", bcache_device, bcache_sys_path)
+        # we expect a cacheN symlink to point to bcache_device/bcache
+        sys_path_links = [os.path.join(bcache_sys_path, l)
+                          for l in os.listdir(bcache_sys_path)]
+        cache_links = [l for l in sys_path_links
+                       if os.path.islink(l) and (
+                          os.path.basename(l).startswith('cache'))]
+
+        if len(cache_links) == 0:
+            msg = ('Failed to find any cache links in %s:%s' % (
+                   bcache_sys_path, sys_path_links))
+            raise OSError(msg)
+
+        for link in cache_links:
+            target = os.readlink(link)
+            LOG.debug('Resolving symlink %s -> %s', link, target)
+            # cacheN  -> ../../../devices/.../<bcache_device>/bcache
+            # basename(dirname(readlink(link)))
+            target_cache_device = os.path.basename(
+                os.path.dirname(target))
+            if os.path.basename(bcache_device) == target_cache_device:
+                LOG.debug('Found match: bcache_device=%s target_device=%s',
+                          bcache_device, target_cache_device)
+                return
+            else:
+                msg = ('Cache symlink %s ' % target_cache_device +
+                       'points to incorrect device: %s' % bcache_device)
+                raise OSError(msg)
+    elif bcache_sys_path.startswith('/sys/class/block'):
+        LOG.debug("validating bcache backing device '%s' from sys_path"
+                  " '%s'", bcache_device, bcache_sys_path)
+        # we expect a 'dev' symlink to point to the bcacheN device
+        bcache_dev = os.path.join(bcache_sys_path, 'dev')
+        if os.path.islink(bcache_dev):
+            bcache_dev_link = (
+                os.path.basename(os.readlink(bcache_dev)))
+            LOG.debug('bcache device %s using bcache kname: %s',
+                      bcache_sys_path, bcache_dev_link)
+
+            bcache_slaves_path = os.path.join(bcache_dev, 'slaves')
+            slaves = os.listdir(bcache_slaves_path)
+            LOG.debug('bcache device %s has slaves: %s',
+                      bcache_sys_path, slaves)
+            if os.path.basename(bcache_device) in slaves:
+                LOG.debug('bcache device %s found in slaves',
+                          os.path.basename(bcache_device))
+                return
+            else:
+                msg = ('Failed to find bcache device %s' % bcache_device +
+                       'in slaves list %s' % slaves)
+                raise OSError(msg)
+        else:
+            msg = 'didnt find "dev" attribute on: %s', bcache_dev
+            return OSError(msg)
+
+    else:
+        LOG.debug("Failed to validate bcache device '%s' from sys_path"
+                  " '%s'", bcache_device, bcache_sys_path)
+        msg = ('sysfs path %s does not appear to be a bcache device' %
+               bcache_sys_path)
+        return ValueError(msg)
+
+
+def ensure_bcache_is_registered(bcache_device, expected, retry=None):
+    """ Test that bcache_device is found at an expected path and
+        re-register the device if it's not ready.
+
+        Retry the validation and registration as needed.
+    """
+    if not retry:
+        retry = BCACHE_REGISTRATION_RETRY
+
+    for attempt, wait in enumerate(retry):
+        # find the actual bcache device name via sysfs using the
+        # backing device's holders directory.
+        LOG.debug('check just created bcache %s if it is registered,'
+                  ' try=%s', bcache_device, attempt + 1)
+        try:
+            udevadm_settle()
+            if os.path.exists(expected):
+                LOG.debug('Found bcache dev %s at expected path %s',
+                          bcache_device, expected)
+                validate_bcache_ready(bcache_device, expected)
+            else:
+                msg = 'bcache device path not found: %s' % expected
+                LOG.debug(msg)
+                raise ValueError(msg)
+
+            # if bcache path exists and holders are > 0 we can return
+            LOG.debug('bcache dev %s at path %s successfully registered'
+                      ' on attempt %s/%s',  bcache_device, expected,
+                      attempt + 1, len(retry))
+            return
+
+        except (OSError, IndexError, ValueError):
+            # Some versions of bcache-tools will register the bcache device
+            # as soon as we run make-bcache using udev rules, so wait for
+            # udev to settle, then try to locate the dev, on older versions
+            # we need to register it manually though
+            LOG.debug('bcache device was not registered, registering %s '
+                      'at /sys/fs/bcache/register', bcache_device)
+            try:
+                register_bcache(bcache_device)
+            except IOError:
+                # device creation is notoriously racy and this can trigger
+                # "Invalid argument" IOErrors if it got created in "the
+                # meantime" - just restart the function a few times to
+                # check it all again
+                pass
+
+        LOG.debug("bcache dev %s not ready, waiting %ss",
+                  bcache_device, wait)
+        time.sleep(wait)
+
+    # we've exhausted our retries
+    LOG.warning('Repetitive error registering the bcache dev %s',
+                bcache_device)
+    raise RuntimeError("bcache device %s can't be registered" %
+                       bcache_device)
+
+
+def create_cache_device(cache_device):
+    # /sys/class/block/XXX/YYY/
+    cache_device_sysfs = sys_block_path(cache_device)
+
+    if os.path.exists(os.path.join(cache_device_sysfs, "bcache")):
+        LOG.debug('caching device already exists at {}/bcache. Read '
+                  'cset.uuid'.format(cache_device_sysfs))
+        (out, err) = util.subp(["bcache-super-show", cache_device],
+                               capture=True)
+        LOG.debug('bcache-super-show=[{}]'.format(out))
+        [cset_uuid] = [line.split()[-1] for line in out.split("\n")
+                       if line.startswith('cset.uuid')]
+    else:
+        LOG.debug('caching device does not yet exist at {}/bcache. Make '
+                  'cache and get uuid'.format(cache_device_sysfs))
+        # make the cache device, extracting cacheset uuid
+        (out, err) = util.subp(["make-bcache", "-C", cache_device],
+                               capture=True)
+        LOG.debug('out=[{}]'.format(out))
+        [cset_uuid] = [line.split()[-1] for line in out.split("\n")
+                       if line.startswith('Set UUID:')]
+
+    target_sysfs_path = '/sys/fs/bcache/%s' % cset_uuid
+    ensure_bcache_is_registered(cache_device, target_sysfs_path)
+    return cset_uuid
+
+
+def create_backing_device(backing_device, cache_device, cache_mode, cset_uuid):
+    backing_device_sysfs = sys_block_path(backing_device)
+    target_sysfs_path = os.path.join(backing_device_sysfs, "bcache")
+
+    # there should not be any pre-existing bcache device
+    bdir = os.path.join(backing_device_sysfs, "bcache")
+    if os.path.exists(bdir):
+        raise RuntimeError(
+            'Unexpected old bcache device: %s', backing_device)
+
+    LOG.debug('Creating a backing device on %s', backing_device)
+    util.subp(["make-bcache", "-B", backing_device])
+    ensure_bcache_is_registered(backing_device, target_sysfs_path)
+
+    # via the holders we can identify which bcache device we just created
+    # for a given backing device
+    from .clear_holders import get_holders
+    holders = get_holders(backing_device)
+    if len(holders) != 1:
+        err = ('Invalid number {} of holding devices:'
+               ' "{}"'.format(len(holders), holders))
+        LOG.error(err)
+        raise ValueError(err)
+    [bcache_dev] = holders
+    LOG.debug('The just created bcache device is {}'.format(holders))
+
+    if cache_device:
+        # if we specify both then we need to attach backing to cache
+        if cset_uuid:
+            attach_backing_to_cacheset(backing_device, cache_device, cset_uuid)
+        else:
+            msg = "Invalid cset_uuid: {}".format(cset_uuid)
+            LOG.error(msg)
+            raise ValueError(msg)
+
+    if cache_mode:
+        set_cache_mode(bcache_dev, cache_mode)
+    return dev_path(bcache_dev)
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/block/clear_holders.py b/curtin/block/clear_holders.py
index 8e6fc62..116ee81 100644
--- a/curtin/block/clear_holders.py
+++ b/curtin/block/clear_holders.py
@@ -288,39 +288,21 @@ def wipe_superblock(device):
     # the blockdev (e.g. /dev/sda2) may be a multipath partition which can
     # only be wiped via its device mapper device (e.g. /dev/dm-4)
     # check for this and determine the correct device mapper value to use.
-    mp_dev = None
-    mp_support = multipath.multipath_supported()
-    if mp_support:
-        parent, partnum = block.get_blockdev_for_partition(blockdev)
-        parent_mpath_id = multipath.find_mpath_id_by_path(parent)
-        if parent_mpath_id is not None:
-            # construct multipath dmsetup id
-            # <mpathid>-part%d -> /dev/dm-1
-            mp_id, mp_dev = multipath.find_mpath_id_by_parent(parent_mpath_id,
-                                                              partnum=partnum)
-            # if we don't find a mapping then the mp partition has already been
-            # wiped/removed
-            if mp_dev:
-                LOG.debug('Found multipath device over %s, wiping holder %s',
-                          blockdev, mp_dev)
-
-            # check if we can remove the parent mpath_id mapping; this is
-            # is possible after removing all dependent mpath devices (like
-            # mpath partitions.  Once the mpath parts are wiped and unmapped
-            # we can remove the parent mpath mapping which releases the lock
-            # on the underlying disk partitions.
-            dm_map = multipath.dmname_to_blkdev_mapping()
-            LOG.debug('dm map: %s', dm_map)
-            parent_mp_dev = dm_map.get(parent_mpath_id)
-            if parent_mp_dev is not None:
-                parent_mp_holders = get_holders(parent_mp_dev)
-                if len(parent_mp_holders) == 0:
-                    LOG.debug('Parent multipath device (%s, %s) has no '
-                              'holders, removing.', parent_mpath_id,
-                              parent_mp_dev)
-                    multipath.remove_map(parent_mpath_id)
-
-    _wipe_superblock(mp_dev if mp_dev else blockdev)
+    if multipath.multipath_supported():
+        # handle /dev/mapper/mpatha , base mp device
+        if multipath.is_mpath_device(blockdev):
+            # if mpath device has "partitions" those need to be removed.
+            # clear-holders will have already wiped these devices as they
+            # are higher up in the dependency tree.
+            mpath_id = multipath.find_mpath_id(blockdev)
+            for mp_part_id in multipath.find_mpath_partitions(mpath_id):
+                multipath.remove_partition(mp_part_id)
+        # handle /dev/sdX which are held by multipath layer
+        if multipath.is_mpath_member(blockdev):
+            LOG.debug('Skipping multipath partition path member: %s', blockdev)
+            return
+
+    _wipe_superblock(blockdev)
 
     # if we had partitions, make sure they've been removed
     if partitions:
@@ -343,20 +325,6 @@ def wipe_superblock(device):
                       device, attempt + 1, len(retries), wait)
             time.sleep(wait)
 
-    if mp_support:
-        # multipath partitions are separate block devices (disks)
-        if mp_dev or multipath.is_mpath_partition(blockdev):
-            multipath.remove_partition(mp_dev if mp_dev else blockdev)
-        # multipath devices must be hidden to utilize a single member (path)
-        elif multipath.is_mpath_device(blockdev):
-            mp_id = multipath.find_mpath_id(blockdev)
-            multipath.remove_partition(blockdev)
-            if mp_id:
-                multipath.remove_map(mp_id)
-            else:
-                raise RuntimeError(
-                    'Failed to find multipath id for %s' % blockdev)
-
 
 def _wipe_superblock(blockdev, exclusive=True, strict=True):
     """ No checks, just call wipe_volume """
@@ -452,7 +420,8 @@ def get_holders(device):
     # block.sys_block_path works when given a /sys or /dev path
     sysfs_path = block.sys_block_path(device)
     # get holders
-    holders = os.listdir(os.path.join(sysfs_path, 'holders'))
+    hpath = os.path.join(sysfs_path, 'holders')
+    holders = os.listdir(hpath)
     LOG.debug("devname '%s' had holders: %s", device, holders)
     return holders
 
@@ -467,7 +436,8 @@ def gen_holders_tree(device):
     # dir in sysfs and any partitions on the device. this ensures that a
     # storage tree starting from a disk will include all devices holding the
     # disk's partitions
-    holder_paths = ([block.sys_block_path(h) for h in get_holders(device)] +
+    holders = get_holders(device)
+    holder_paths = ([block.sys_block_path(h) for h in holders] +
                     block.get_sysfs_partitions(device))
     # the DEV_TYPE registry contains a function under the key 'ident' for each
     # device type entry that returns true if the device passed to it is of the
@@ -635,6 +605,7 @@ def clear_holders(base_paths, try_preserve=False):
     # handle single path
     if not isinstance(base_paths, (list, tuple)):
         base_paths = [base_paths]
+    LOG.info('Generating device storage trees for path(s): %s', base_paths)
 
     # get current holders and plan how to shut them down
     holder_trees = [gen_holders_tree(path) for path in base_paths]
@@ -709,9 +680,21 @@ def start_clear_holders_deps():
         except util.ProcessExecutionError:
             LOG.debug('Non-fatal error when querying mdadm detail on %s', md)
 
+    mp_support = multipath.multipath_supported()
+    if mp_support:
+        LOG.debug('Detected multipath support, reload maps')
+        multipath.reload()
+        multipath.force_devmapper_symlinks()
+
     # scan and activate for logical volumes
-    lvm.lvm_scan()
-    lvm.activate_volgroups()
+    lvm.lvm_scan(multipath=mp_support)
+    try:
+        lvm.activate_volgroups(multipath=mp_support)
+    except util.ProcessExecutionError:
+        # partial vg may not come up due to missing members, that's OK
+        pass
+    udev.udevadm_settle()
+
     # the bcache module needs to be present to properly detect bcache devs
     # on some systems (precise without hwe kernel) it may not be possible to
     # lad the bcache module bcause it is not present in the kernel. if this
diff --git a/curtin/block/lvm.py b/curtin/block/lvm.py
index b3f8bcb..da29c7b 100644
--- a/curtin/block/lvm.py
+++ b/curtin/block/lvm.py
@@ -13,12 +13,14 @@ import os
 _SEP = '='
 
 
-def _filter_lvm_info(lvtool, match_field, query_field, match_key):
+def _filter_lvm_info(lvtool, match_field, query_field, match_key, args=None):
     """
     filter output of pv/vg/lvdisplay tools
     """
+    if args is None:
+        args = []
     (out, _) = util.subp([lvtool, '-C', '--separator', _SEP, '--noheadings',
-                          '-o', ','.join([match_field, query_field])],
+                          '-o', ','.join([match_field, query_field])] + args,
                          capture=True)
     return [qf for (mf, qf) in
             [l.strip().split(_SEP) for l in out.strip().splitlines()]
@@ -39,6 +41,14 @@ def get_lvols_in_volgroup(vg_name):
     return _filter_lvm_info('lvdisplay', 'vg_name', 'lv_name', vg_name)
 
 
+def get_lv_size_bytes(lv_name):
+    """ get the size in bytes of a logical volume specified by lv_name."""
+    result = _filter_lvm_info('lvdisplay', 'lv_name', 'lv_size', lv_name,
+                              args=['--units=B'])
+    if result:
+        return util.human2bytes(result[0])
+
+
 def split_lvm_name(full):
     """
     split full lvm name into tuple of (volgroup, lv_name)
@@ -58,7 +68,7 @@ def lvmetad_running():
                                          '/run/lvmetad.pid'))
 
 
-def activate_volgroups():
+def activate_volgroups(multipath=False):
     """
     Activate available volgroups and logical volumes within.
 
@@ -69,15 +79,36 @@ def activate_volgroups():
     # none found (no output)
     % vgchange -ay
     """
+    cmd = ['vgchange', '--activate=y']
+    if multipath:
+        # only operate on mp devices
+        mp_filter = generate_multipath_dev_mapper_filter()
+        cmd.extend(['--config', 'devices{ %s }' % mp_filter])
 
     # vgchange handles syncing with udev by default
     # see man 8 vgchange and flag --noudevsync
-    out, _ = util.subp(['vgchange', '--activate=y'], capture=True)
+    out, _ = util.subp(cmd, capture=True)
     if out:
         LOG.info(out)
 
 
-def lvm_scan(activate=True):
+def _generate_multipath_filter(accept=None):
+    if not accept:
+        raise ValueError('Missing list of accept patterns')
+    prefix = ", ".join(['"a|%s|"' % p for p in accept])
+    return 'filter = [ {prefix}, "r|.*|" ]'.format(prefix=prefix)
+
+
+def generate_multipath_dev_mapper_filter():
+    return _generate_multipath_filter(accept=['/dev/mapper/mpath.*'])
+
+
+def generate_multipath_dm_uuid_filter():
+    return _generate_multipath_filter(
+        accept=['/dev/disk/by-id/dm-uuid-.*mpath-.*'])
+
+
+def lvm_scan(activate=True, multipath=False):
     """
     run full scan for volgroups, logical volumes and physical volumes
     """
@@ -94,9 +125,15 @@ def lvm_scan(activate=True):
         LOG.warning('unable to find release number, assuming xenial or later')
         release = 'xenial'
 
-    for cmd in [['pvscan'], ['vgscan', '--mknodes']]:
+    if multipath:
+        # only operate on mp devices
+        mponly = 'devices{ filter = [ "a|/dev/mapper/mpath.*|", "r|.*|" ] }'
+
+    for cmd in [['pvscan'], ['vgscan']]:
         if release != 'precise' and lvmetad_running():
             cmd.append('--cache')
+        if multipath:
+            cmd.extend(['--config', mponly])
         util.subp(cmd, capture=True)
 
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/block/mdadm.py b/curtin/block/mdadm.py
index b7c4d06..32b467c 100644
--- a/curtin/block/mdadm.py
+++ b/curtin/block/mdadm.py
@@ -10,8 +10,16 @@ import re
 import shlex
 import time
 
-from curtin.block import (dev_short, dev_path, is_valid_device, sys_block_path)
-from curtin.block import get_holders, zero_file_at_offsets
+from curtin.block import (
+    dev_path,
+    dev_short,
+    get_holders,
+    is_valid_device,
+    md_get_devices_list,
+    md_get_spares_list,
+    sys_block_path,
+    zero_file_at_offsets,
+)
 from curtin.distro import lsb_release
 from curtin import (util, udev)
 from curtin.log import LOG
@@ -683,29 +691,6 @@ def md_read_run_mdadm_map():
     return mdadm_map
 
 
-def md_get_spares_list(devpath):
-    sysfs_md = sys_block_path(devpath, "md")
-    spares = [dev_path(dev[4:])
-              for dev in os.listdir(sysfs_md)
-              if (dev.startswith('dev-') and
-                  util.load_file(os.path.join(sysfs_md,
-                                              dev,
-                                              'state')).strip() == 'spare')]
-
-    return spares
-
-
-def md_get_devices_list(devpath):
-    sysfs_md = sys_block_path(devpath, "md")
-    devices = [dev_path(dev[4:])
-               for dev in os.listdir(sysfs_md)
-               if (dev.startswith('dev-') and
-                   util.load_file(os.path.join(sysfs_md,
-                                               dev,
-                                               'state')).strip() != 'spare')]
-    return devices
-
-
 def md_check_array_uuid(md_devname, md_uuid):
     valid_mdname(md_devname)
 
diff --git a/curtin/block/mkfs.py b/curtin/block/mkfs.py
index 4a1e1f9..ea5f09d 100644
--- a/curtin/block/mkfs.py
+++ b/curtin/block/mkfs.py
@@ -132,7 +132,8 @@ def get_flag_mapping(flag_name, fs_family, param=None, strict=False):
     return ret
 
 
-def mkfs(path, fstype, strict=False, label=None, uuid=None, force=False):
+def mkfs(path, fstype, strict=False, label=None, uuid=None, force=False,
+         extra_options=None):
     """Make filesystem on block device with given path using given fstype and
        appropriate flags for filesystem family.
 
@@ -146,6 +147,8 @@ def mkfs(path, fstype, strict=False, label=None, uuid=None, force=False):
 
        Force can be specified to force the mkfs command to continue even if it
        finds old data or filesystems on the partition.
+
+       If extra_options are supplied they are appended to mkfs command.
        """
 
     if path is None:
@@ -201,6 +204,9 @@ def mkfs(path, fstype, strict=False, label=None, uuid=None, force=False):
             cmd.extend(get_flag_mapping("fatsize", fs_family, param=fat_size,
                                         strict=strict))
 
+    if extra_options:
+        cmd.extend(extra_options)
+
     cmd.append(path)
     util.subp(cmd, capture=True)
 
@@ -226,6 +232,6 @@ def mkfs_from_config(path, info, strict=False):
     # NOTE: Since old metadata on partitions that have not been wiped can cause
     #       some mkfs commands to refuse to work, it's best to use force=True
     mkfs(path, fstype, strict=strict, force=True, uuid=info.get('uuid'),
-         label=info.get('label'))
+         label=info.get('label'), extra_options=info.get('extra_options'))
 
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/block/multipath.py b/curtin/block/multipath.py
index 8ce0509..9c7f510 100644
--- a/curtin/block/multipath.py
+++ b/curtin/block/multipath.py
@@ -50,32 +50,42 @@ def dmname_to_blkdev_mapping():
     return mapping
 
 
-def is_mpath_device(devpath):
+def is_mpath_device(devpath, info=None):
     """ Check if devpath is a multipath device, returns boolean. """
-    info = udev.udevadm_info(devpath)
+    result = False
+    if not info:
+        info = udev.udevadm_info(devpath)
     if info.get('DM_UUID', '').startswith('mpath-'):
-        return True
+        result = True
 
-    return False
+    LOG.debug('%s is multipath device? %s', devpath, result)
+    return result
 
 
-def is_mpath_member(devpath):
+def is_mpath_member(devpath, info=None):
     """ Check if a device is a multipath member (a path), returns boolean. """
+    result = False
     try:
         util.subp(['multipath', '-c', devpath], capture=True)
-        return True
+        result = True
     except util.ProcessExecutionError:
-        return False
+        pass
+
+    LOG.debug('%s is multipath device member? %s', devpath, result)
+    return result
 
 
-def is_mpath_partition(devpath):
+def is_mpath_partition(devpath, info=None):
     """ Check if a device is a multipath partition, returns boolean. """
+    result = False
     if devpath.startswith('/dev/dm-'):
+        if not info:
+            info = udev.udevadm_info(devpath)
         if 'DM_PART' in udev.udevadm_info(devpath):
-            LOG.debug("%s is multipath device partition", devpath)
-            return True
+            result = True
 
-    return False
+    LOG.debug("%s is multipath device partition? %s", devpath, result)
+    return result
 
 
 def mpath_partition_to_mpath_id(devpath):
@@ -116,6 +126,13 @@ def find_mpath_members(multipath_id, paths=None):
     """ Return a list of device path for each member of aspecified mpath_id."""
     if not paths:
         paths = show_paths()
+        for retry in range(0, 5):
+            orphans = [path for path in paths if 'orphan' in path['multipath']]
+            if len(orphans):
+                udev.udevadm_settle()
+                paths = show_paths()
+            else:
+                break
 
     members = ['/dev/' + path['device']
                for path in paths if path['multipath'] == multipath_id]
@@ -142,6 +159,10 @@ def find_mpath_id_by_path(devpath, paths=None):
     if not paths:
         paths = show_paths()
 
+    if devpath.startswith('/dev/dm-'):
+        raise ValueError('find_mpath_id_by_path does not handle '
+                         'device-mapper devices: %s' % devpath)
+
     for path in paths:
         if devpath == '/dev/' + path['device']:
             return path['multipath']
@@ -160,6 +181,64 @@ def find_mpath_id_by_parent(multipath_id, partnum=None):
     return (dm_name, devmap.get(dm_name))
 
 
+def find_mpath_partitions(mpath_id):
+    """
+    Return a generator of multipath ids which are partitions of 'mpath-id'
+    """
+    # {'mpatha': '/dev/dm-0',
+    #  'mpatha-part1': '/dev/dm-3',
+    #  'mpatha-part2': '/dev/dm-4',
+    #  'mpathb': '/dev/dm-12'}
+    if not mpath_id:
+        raise ValueError('Invalid mpath_id parameter: %s' % mpath_id)
+
+    return (mp_id for (mp_id, _dm_dev) in dmname_to_blkdev_mapping().items()
+            if mp_id.startswith(mpath_id + '-'))
+
+
+def get_mpath_id_from_device(device):
+    # /dev/dm-X
+    if is_mpath_device(device) or is_mpath_partition(device):
+        info = udev.udevadm_info(device)
+        return info.get('DM_NAME')
+    # /dev/sdX
+    if is_mpath_member(device):
+        return find_mpath_id_by_path(device)
+
+    return None
+
+
+def force_devmapper_symlinks():
+    """Check if /dev/mapper/mpath* files are symlinks, if not trigger udev."""
+    LOG.debug('Verifying /dev/mapper/mpath* files are symlinks')
+    needs_trigger = []
+    for mp_id, dm_dev in dmname_to_blkdev_mapping().items():
+        if mp_id.startswith('mpath'):
+            mapper_path = '/dev/mapper/' + mp_id
+            if not os.path.islink(mapper_path):
+                LOG.warning(
+                    'Found invalid device mapper mp path: %s, removing',
+                    mapper_path)
+                util.del_file(mapper_path)
+                needs_trigger.append((mapper_path, dm_dev))
+
+    if len(needs_trigger):
+        for (mapper_path, dm_dev) in needs_trigger:
+            LOG.debug('multipath: regenerating symlink for %s (%s)',
+                      mapper_path, dm_dev)
+            util.subp(['udevadm', 'trigger', '--subsystem-match=block',
+                       '--action=add',
+                       '/sys/class/block/' + os.path.basename(dm_dev)])
+            udev.udevadm_settle(exists=mapper_path)
+            if not os.path.islink(mapper_path):
+                LOG.error('Failed to regenerate udev symlink %s', mapper_path)
+
+
+def reload():
+    """ Request multipath to force reload devmaps. """
+    util.subp(['multipath', '-r'])
+
+
 def multipath_supported():
     """Return a boolean indicating if multipath is supported."""
     try:
diff --git a/curtin/block/schemas.py b/curtin/block/schemas.py
index 34c4400..9e2c41f 100644
--- a/curtin/block/schemas.py
+++ b/curtin/block/schemas.py
@@ -7,8 +7,9 @@ _path_nondev = r'(^/$|^(/[^/]+)+$)'
 _fstypes = ['btrfs', 'ext2', 'ext3', 'ext4', 'fat', 'fat12', 'fat16', 'fat32',
             'iso9660', 'vfat', 'jfs', 'ntfs', 'reiserfs', 'swap', 'xfs',
             'zfsroot']
-_ptables = ['dos', 'gpt', 'msdos', 'vtoc']
 _ptable_unsupported = 'unsupported'
+_ptables = ['dos', 'gpt', 'msdos', 'vtoc']
+_ptables_valid = _ptables + [_ptable_unsupported]
 
 definitions = {
     'id': {'type': 'string'},
@@ -16,7 +17,7 @@ definitions = {
     'devices': {'type': 'array', 'items': {'$ref': '#/definitions/ref_id'}},
     'name': {'type': 'string'},
     'preserve': {'type': 'boolean'},
-    'ptable': {'type': 'string', 'enum': _ptables + [_ptable_unsupported]},
+    'ptable': {'type': 'string', 'enum': _ptables_valid},
     'size': {'type': ['string', 'number'],
              'minimum': 1,
              'pattern': r'^([1-9]\d*(.\d+)?|\d+.\d+)(K|M|G|T)?B?'},
@@ -65,6 +66,7 @@ BCACHE = {
         'backing_device': {'$ref': '#/definitions/ref_id'},
         'cache_device': {'$ref': '#/definitions/ref_id'},
         'name': {'$ref': '#/definitions/name'},
+        'preserve': {'$ref': '#/definitions/preserve'},
         'type': {'const': 'bcache'},
         'cache_mode': {
             'type': ['string'],
@@ -166,6 +168,7 @@ DM_CRYPT = {
         'volume': {'$ref': '#/definitions/ref_id'},
         'key': {'$ref': '#/definitions/id'},
         'keyfile': {'$ref': '#/definitions/id'},
+        'preserve': {'$ref': '#/definitions/preserve'},
         'type': {'const': 'dm_crypt'},
     },
 }
@@ -187,6 +190,7 @@ FORMAT = {
         'fstype': {'$ref': '#/definitions/fstype'},
         'label': {'type': 'string'},
         'volume': {'$ref': '#/definitions/ref_id'},
+        'extra_options': {'type': 'array', 'items': {'type': 'string'}},
     }
 }
 LVM_PARTITION = {
@@ -201,6 +205,7 @@ LVM_PARTITION = {
     'properties': {
         'id': {'$ref': '#/definitions/id'},
         'name': {'$ref': '#/definitions/name'},
+        'preserve': {'type': 'boolean'},
         'size': {'$ref': '#/definitions/size'},  # XXX: This is not used
         'type': {'const': 'lvm_partition'},
         'volgroup': {'$ref': '#/definitions/ref_id'},
@@ -219,6 +224,7 @@ LVM_VOLGROUP = {
         'id': {'$ref': '#/definitions/id'},
         'devices': {'$ref': '#/definitions/devices'},
         'name': {'$ref': '#/definitions/name'},
+        'preserve': {'type': 'boolean'},
         'uuid': {'$ref': '#/definitions/uuid'},    # XXX: This is not used
         'type': {'const': 'lvm_volgroup'},
     },
@@ -285,6 +291,11 @@ PARTITION = {
                  'enum': ['bios_grub', 'boot', 'extended', 'home', 'linux',
                           'logical', 'lvm', 'mbr', 'prep', 'raid', 'swap',
                           '']},
+        'grub_device': {
+            'type': ['boolean', 'integer'],
+            'minimum': 0,
+            'maximum': 1
+        },
     }
 }
 RAID = {
diff --git a/curtin/commands/apt_config.py b/curtin/commands/apt_config.py
index 8bd6e79..f012ae0 100644
--- a/curtin/commands/apt_config.py
+++ b/curtin/commands/apt_config.py
@@ -135,6 +135,7 @@ def apply_debconf_selections(cfg, target=None):
         LOG.debug("debconf_selections was not set in config")
         return
 
+    LOG.debug('Applying debconf selections')
     selections = '\n'.join(
         [selsets[key] for key in sorted(selsets.keys())])
     debconf_set_selections(selections.encode() + b"\n", target=target)
@@ -149,13 +150,8 @@ def apply_debconf_selections(cfg, target=None):
             pkgs_cfgd.add(pkg)
 
     pkgs_installed = distro.get_installed_packages(target)
-
-    LOG.debug("pkgs_cfgd: %s", pkgs_cfgd)
-    LOG.debug("pkgs_installed: %s", pkgs_installed)
     need_reconfig = pkgs_cfgd.intersection(pkgs_installed)
-
     if len(need_reconfig) == 0:
-        LOG.debug("no need for reconfig")
         return
 
     dpkg_reconfigure(need_reconfig, target=target)
diff --git a/curtin/commands/block_discover.py b/curtin/commands/block_discover.py
index f309970..055c4c7 100644
--- a/curtin/commands/block_discover.py
+++ b/curtin/commands/block_discover.py
@@ -8,10 +8,19 @@ from curtin import block
 def block_discover_main(args):
     """probe for existing devices and emit Curtin storage config output."""
 
-    print(json.dumps(block.discover(), indent=2, sort_keys=True))
+    if args.probe_data:
+        probe_data = block._discover_get_probert_data()
+    else:
+        probe_data = block.discover()
 
+    print(json.dumps(probe_data, indent=2, sort_keys=True))
 
-CMD_ARGUMENTS = ()
+
+CMD_ARGUMENTS = (
+    (('-p', '--probe-data'),
+     {'help': 'dump probert probe-data to stdout emitting storage config.',
+      'action': 'store_true', 'default': False}),
+)
 
 
 def POPULATE_SUBCMD(parser):
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index c48ddbf..f2bb8da 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -4,11 +4,12 @@ from collections import OrderedDict, namedtuple
 from curtin import (block, config, paths, util)
 from curtin.block import schemas
 from curtin.block import (bcache, clear_holders, dasd, iscsi, lvm, mdadm, mkfs,
-                          zfs)
+                          multipath, zfs)
 from curtin import distro
 from curtin.log import LOG, logged_time
 from curtin.reporter import events
-from curtin.storage_config import extract_storage_ordered_dict
+from curtin.storage_config import (extract_storage_ordered_dict,
+                                   ptable_uuid_to_flag_entry)
 
 
 from . import populate_one_subcmd
@@ -32,9 +33,29 @@ FstabData.__new__.__defaults__ = (None, None, None, "", "0", "0", None)
 SIMPLE = 'simple'
 SIMPLE_BOOT = 'simple-boot'
 CUSTOM = 'custom'
-BCACHE_REGISTRATION_RETRY = [0.2] * 60
 PTABLE_UNSUPPORTED = schemas._ptable_unsupported
-
+PTABLES_SUPPORTED = schemas._ptables
+PTABLES_VALID = schemas._ptables_valid
+
+SGDISK_FLAGS = {
+    "boot": 'ef00',
+    "lvm": '8e00',
+    "raid": 'fd00',
+    "bios_grub": 'ef02',
+    "prep": '4100',
+    "swap": '8200',
+    "home": '8302',
+    "linux": '8300'
+}
+
+MSDOS_FLAGS = {
+    'boot': 'boot',
+    'extended': 'extended',
+    'logical': 'logical',
+}
+
+DNAME_BYID_KEYS = ['DM_UUID', 'ID_WWN_WITH_EXTENSION', 'ID_WWN', 'ID_SERIAL',
+                   'ID_SERIAL_SHORT']
 CMD_ARGUMENTS = (
     ((('-D', '--devices'),
       {'help': 'which devices to operate on', 'action': 'append',
@@ -71,11 +92,13 @@ def block_meta(args):
         if 'storage' in cfg:
             devices = get_device_paths_from_storage_config(
                 extract_storage_ordered_dict(cfg))
+            LOG.debug('block-meta: extracted devices to clear: %s', devices)
         if len(devices) == 0:
             devices = cfg.get('block-meta', {}).get('devices', [])
         LOG.debug('Declared block devices: %s', devices)
         args.devices = devices
 
+    LOG.debug('clearing devices=%s', devices)
     meta_clear(devices, state.get('report_stack_prefix', ''))
 
     # dd-images requires use of meta_simple
@@ -123,7 +146,14 @@ def write_image_to_disk(source, dev):
                     '--', source['uri'], devnode])
     util.subp(['partprobe', devnode])
     udevadm_settle()
-    paths = ["curtin", "system-data/var/lib/snapd"]
+    # Images from MAAS have well-known/required paths present
+    # on the rootfs partition.  Use these values to select the
+    # root (target) partition to complete installation.
+    #
+    # /curtin -> Most Ubuntu Images
+    # /system-data/var/lib/snapd -> UbuntuCore 16 or 18
+    # /snaps -> UbuntuCore20
+    paths = ["curtin", "system-data/var/lib/snapd", "snaps"]
     return block.get_root_device([devname], paths=paths)
 
 
@@ -171,7 +201,6 @@ def get_partition_format_type(cfg, machine=None, uefi_bootable=None):
 
 
 def devsync(devpath):
-    LOG.debug('devsync for %s', devpath)
     util.subp(['partprobe', devpath], rcs=[0, 1])
     udevadm_settle()
     for x in range(0, 10):
@@ -247,13 +276,11 @@ def make_dname_byid(path, error_msg=None, info=None):
             "Disk tag udev rules are only for disks, %s has devtype=%s" %
             (error_msg, devtype))
 
-    byid_keys = ['ID_WWN_WITH_EXTENSION', 'ID_WWN',
-                 'ID_SERIAL', 'ID_SERIAL_SHORT']
-    present = [k for k in byid_keys if info.get(k)]
+    present = [k for k in DNAME_BYID_KEYS if info.get(k)]
     if not present:
         LOG.warning(
             "Cannot create disk tag udev rule for %s, "
-            "missing 'serial' or 'wwn' value" % error_msg)
+            "missing 'serial' or 'wwn' value", error_msg)
         return []
 
     return [[compose_udev_equality('ENV{%s}' % k, info[k]) for k in present]]
@@ -278,8 +305,8 @@ def make_dname(volume, storage_config):
             byid = make_dname_byid(path, error_msg="id=%s" % vol.get('id'))
     # we may not always be able to find a uniq identifier on devices with names
     if (not ptuuid and not byid) and vol.get('type') in ["disk", "partition"]:
-        LOG.warning("Can't find a uuid for volume: {}. Skipping dname.".format(
-            volume))
+        LOG.warning("Can't find a uuid for volume: %s. Skipping dname.",
+                    volume)
         return
 
     matches = []
@@ -332,23 +359,37 @@ def make_dname(volume, storage_config):
     #       lvm devices may use the name attribute and may permit special chars
     sanitized = sanitize_dname(dname)
     if sanitized != dname:
-        LOG.warning(
-            "dname modified to remove invalid chars. old: '{}' new: '{}'"
-            .format(dname, sanitized))
+        LOG.warning("dname modified to remove invalid chars. old:"
+                    "'%s' new: '%s'", dname, sanitized)
     content = ['# Written by curtin']
     for match in matches:
         rule = (base_rule + match +
                 ["SYMLINK+=\"disk/by-dname/%s\"\n" % sanitized])
-        LOG.debug("Creating dname udev rule '{}'".format(str(rule)))
+        LOG.debug("Creating dname udev rule '%s'", str(rule))
         content.append(', '.join(rule))
 
     if vol.get('type') == 'disk':
         for brule in byid:
-            rule = (base_rule +
+            part_rule = None
+            for env_rule in brule:
+                # multipath partitions prefix partN- to DM_UUID for fun!
+                # and partitions are "disks" yay \o/ /sarcasm
+                if 'ENV{DM_UUID}=="mpath' not in env_rule:
+                    continue
+                dm_uuid = env_rule.split("==")[1].replace('"', '')
+                part_dm_uuid = 'part*-' + dm_uuid
+                part_rule = (
+                    [compose_udev_equality('ENV{DEVTYPE}', 'disk')] +
+                    [compose_udev_equality('ENV{DM_UUID}', part_dm_uuid)])
+
+            # non-multipath partition rule
+            if not part_rule:
+                part_rule = (
                     [compose_udev_equality('ENV{DEVTYPE}', 'partition')] +
-                    brule +
+                    brule)
+            rule = (base_rule + part_rule +
                     ['SYMLINK+="disk/by-dname/%s-part%%n"\n' % sanitized])
-            LOG.debug("Creating dname udev rule '{}'".format(str(rule)))
+            LOG.debug("Creating dname udev rule '%s'", str(rule))
             content.append(', '.join(rule))
 
     util.ensure_dir(rules_dir)
@@ -359,7 +400,7 @@ def make_dname(volume, storage_config):
 def get_poolname(info, storage_config):
     """ Resolve pool name from zfs info """
 
-    LOG.debug('get_poolname for volume {}'.format(info))
+    LOG.debug('get_poolname for volume %s', info)
     if info.get('type') == 'zfs':
         pool_id = info.get('pool')
         poolname = get_poolname(storage_config.get(pool_id), storage_config)
@@ -377,9 +418,9 @@ def get_path_to_storage_volume(volume, storage_config):
     # Get path to block device for volume. Volume param should refer to id of
     # volume in storage config
 
-    LOG.debug('get_path_to_storage_volume for volume {}'.format(volume))
     devsync_vol = None
     vol = storage_config.get(volume)
+    LOG.debug('get_path_to_storage_volume for volume %s(%s)', volume, vol)
     if not vol:
         raise ValueError("volume with id '%s' not found" % volume)
 
@@ -388,9 +429,12 @@ def get_path_to_storage_volume(volume, storage_config):
         partnumber = determine_partition_number(vol.get('id'), storage_config)
         disk_block_path = get_path_to_storage_volume(vol.get('device'),
                                                      storage_config)
-        disk_kname = block.path_to_kname(disk_block_path)
-        partition_kname = block.partition_kname(disk_kname, partnumber)
-        volume_path = block.kname_to_path(partition_kname)
+        if disk_block_path.startswith('/dev/mapper/mpath'):
+            volume_path = disk_block_path + '-part%s' % partnumber
+        else:
+            disk_kname = block.path_to_kname(disk_block_path)
+            partition_kname = block.partition_kname(disk_kname, partnumber)
+            volume_path = block.kname_to_path(partition_kname)
         devsync_vol = os.path.join(disk_block_path)
 
     elif vol.get('type') == "dasd":
@@ -417,13 +461,19 @@ def get_path_to_storage_volume(volume, storage_config):
                         # sys/class/block access is valid.  ie, there are no
                         # udev generated values in sysfs
                         volume_path = os.path.realpath(vol_value)
+                    # convert /dev/sdX to /dev/mapper/mpathX value
+                    if multipath.is_mpath_member(volume_path):
+                        volume_path = '/dev/mapper/' + (
+                            multipath.get_mpath_id_from_device(volume_path))
                 elif disk_key == 'device_id':
                     dasd_device = dasd.DasdDevice(vol_value)
                     volume_path = dasd_device.devname
             except ValueError:
                 continue
             # verify path exists otherwise try the next key
-            if not os.path.exists(volume_path):
+            if os.path.exists(volume_path):
+                break
+            else:
                 volume_path = None
 
         if volume_path is None:
@@ -469,7 +519,7 @@ def get_path_to_storage_volume(volume, storage_config):
             sys_path = os.path.split(sys_path)[0]
         bcache_kname = block.path_to_kname(sys_path)
         volume_path = block.kname_to_path(bcache_kname)
-        LOG.debug('got bcache volume path {}'.format(volume_path))
+        LOG.debug('got bcache volume path %s', volume_path)
 
     else:
         raise NotImplementedError("cannot determine the path to storage \
@@ -480,7 +530,7 @@ def get_path_to_storage_volume(volume, storage_config):
         devsync_vol = volume_path
     devsync(devsync_vol)
 
-    LOG.debug('return volume path {}'.format(volume_path))
+    LOG.debug('return volume path %s', volume_path)
     return volume_path
 
 
@@ -507,9 +557,11 @@ def dasd_handler(info, storage_config):
     disk_layout = info.get('disk_layout')
     label = info.get('label')
     mode = info.get('mode')
+    force_format = config.value_as_boolean(info.get('wipe'))
 
     dasd_device = dasd.DasdDevice(device_id)
-    if dasd_device.needs_formatting(blocksize, disk_layout, label):
+    if (force_format or dasd_device.needs_formatting(blocksize,
+                                                     disk_layout, label)):
         if config.value_as_boolean(info.get('preserve')):
             raise ValueError(
                 "dasd '%s' does not match configured properties and"
@@ -530,14 +582,17 @@ def dasd_handler(info, storage_config):
 def disk_handler(info, storage_config):
     _dos_names = ['dos', 'msdos']
     ptable = info.get('ptable')
-    disk = get_path_to_storage_volume(info.get('id'), storage_config)
+    if ptable and ptable not in PTABLES_VALID:
+        raise ValueError(
+            'Invalid partition table type: %s in %s' % (ptable, info))
 
+    disk = get_path_to_storage_volume(info.get('id'), storage_config)
     if config.value_as_boolean(info.get('preserve')):
         # Handle preserve flag, verifying if ptable specified in config
-        if config.value_as_boolean(ptable) and ptable != PTABLE_UNSUPPORTED:
+        if ptable and ptable != PTABLE_UNSUPPORTED:
             current_ptable = block.get_part_table_type(disk)
-            if not ((ptable in _dos_names and current_ptable in _dos_names) or
-                    (ptable == 'gpt' and current_ptable == 'gpt')):
+            LOG.debug('disk: current ptable type: %s', current_ptable)
+            if current_ptable not in PTABLES_SUPPORTED:
                 raise ValueError(
                     "disk '%s' does not have correct partition table or "
                     "cannot be read, but preserve is set to true. "
@@ -562,8 +617,6 @@ def disk_handler(info, storage_config):
             elif ptable == "vtoc":
                 # ignore dasd partition tables
                 pass
-            else:
-                raise ValueError('invalid partition table type: %s', ptable)
         holders = clear_holders.get_holders(disk)
         if len(holders) > 0:
             LOG.info('Detected block holders on disk %s: %s', disk, holders)
@@ -615,6 +668,120 @@ def find_extended_partition(part_device, storage_config):
             return item_id
 
 
+def calc_dm_partition_info(partition):
+    # dm- partitions are not in the same dir as disk dm device,
+    # dmsetup table <dm_name>
+    # handle linear types only
+    #    mpatha-part1: 0 6291456 linear 253:0, 2048
+    #    <dm_name>: <log. start sec> <num sec> <type> <dest dev>  <start sec>
+    #
+    # Mapping this:
+    #   previous_size_sectors = <num_sec> | /sys/class/block/dm-1/size
+    #   previous_start_sectors = <start_sec> |  No 'start' sysfs file
+    pp_size_sec = pp_start_sec = None
+    mpath_id = multipath.get_mpath_id_from_device(block.dev_path(partition))
+    if mpath_id is None:
+        raise RuntimeError('Failed to find mpath_id for partition')
+    table_cmd = ['dmsetup', 'table', '--target', 'linear', mpath_id]
+    out, _err = util.subp(table_cmd, capture=True)
+    if out:
+        (_logical_start, previous_size_sectors, _table_type,
+         _destination, previous_start_sectors) = out.split()
+        pp_size_sec = int(previous_size_sectors)
+        pp_start_sec = int(previous_start_sectors)
+
+    return (pp_start_sec, pp_size_sec)
+
+
+def calc_partition_info(disk, partition, logical_block_size_bytes):
+    if partition.startswith('dm-'):
+        pp = partition
+        pp_start_sec, pp_size_sec = calc_dm_partition_info(partition)
+    else:
+        pp = os.path.join(disk, partition)
+        # XXX: sys/block/X/{size,start} is *ALWAYS* in 512b value
+        pp_size = int(
+            util.load_file(os.path.join(pp, "size")))
+        pp_size_sec = int(pp_size * 512 / logical_block_size_bytes)
+        pp_start = int(util.load_file(os.path.join(pp, "start")))
+        pp_start_sec = int(pp_start * 512 / logical_block_size_bytes)
+
+    LOG.debug("previous partition: %s size_sectors=%s start_sectors=%s",
+              pp, pp_size_sec, pp_start_sec)
+    if not all([pp_size_sec, pp_start_sec]):
+        raise RuntimeError(
+            'Failed to determine previous partition %s info', partition)
+
+    return (pp_start_sec, pp_size_sec)
+
+
+def verify_exists(devpath):
+    LOG.debug('Verifying %s exists', devpath)
+    if not os.path.exists(devpath):
+        raise RuntimeError("Device %s does not exist" % devpath)
+
+
+def verify_size(devpath, expected_size_bytes, sfdisk_info=None):
+    if not sfdisk_info:
+        sfdisk_info = block.sfdisk_info(devpath)
+
+    part_info = block.get_partition_sfdisk_info(devpath,
+                                                sfdisk_info=sfdisk_info)
+    (found_type, _code) = ptable_uuid_to_flag_entry(part_info.get('type'))
+    if found_type == 'extended':
+        found_size_bytes = int(part_info['size']) * 512
+    else:
+        found_size_bytes = block.read_sys_block_size_bytes(devpath)
+    msg = (
+        'Verifying %s size, expecting %s bytes, found %s bytes' % (
+         devpath, expected_size_bytes, found_size_bytes))
+    LOG.debug(msg)
+    if expected_size_bytes != found_size_bytes:
+        raise RuntimeError(msg)
+
+
+def verify_ptable_flag(devpath, expected_flag, sfdisk_info=None):
+    if (expected_flag not in SGDISK_FLAGS.keys()) and (expected_flag not in
+                                                       MSDOS_FLAGS.keys()):
+        raise RuntimeError(
+            'Cannot verify unknown partition flag: %s' % expected_flag)
+
+    if not sfdisk_info:
+        sfdisk_info = block.sfdisk_info(devpath)
+
+    entry = block.get_partition_sfdisk_info(devpath, sfdisk_info=sfdisk_info)
+    LOG.debug("Device %s ptable entry: %s", devpath, util.json_dumps(entry))
+    found_flag = None
+    if (sfdisk_info['label'] in ('dos', 'msdos')):
+        if expected_flag == 'boot':
+            found_flag = 'boot' if entry.get('bootable') is True else None
+        elif expected_flag == 'extended':
+            (found_flag, _code) = ptable_uuid_to_flag_entry(entry['type'])
+        elif expected_flag == 'logical':
+            (_parent, partnumber) = block.get_blockdev_for_partition(devpath)
+            found_flag = 'logical' if int(partnumber) > 4 else None
+    else:
+        (found_flag, _code) = ptable_uuid_to_flag_entry(entry['type'])
+    msg = (
+        'Verifying %s partition flag, expecting %s, found %s' % (
+         devpath, expected_flag, found_flag))
+    LOG.debug(msg)
+    if expected_flag != found_flag:
+        raise RuntimeError(msg)
+
+
+def partition_verify(devpath, info):
+    verify_exists(devpath)
+    sfdisk_info = block.sfdisk_info(devpath)
+    if not sfdisk_info:
+        raise RuntimeError('Failed to extract sfdisk info from %s' % devpath)
+    verify_size(devpath, int(util.human2bytes(info['size'])),
+                sfdisk_info=sfdisk_info)
+    expected_flag = info.get('flag')
+    if expected_flag:
+        verify_ptable_flag(devpath, info['flag'], sfdisk_info=sfdisk_info)
+
+
 def partition_handler(info, storage_config):
     device = info.get('device')
     size = info.get('size')
@@ -634,9 +801,8 @@ def partition_handler(info, storage_config):
     # consider the disks logical sector size when calculating sectors
     try:
         (logical_block_size_bytes, _) = block.get_blockdev_sector_size(disk)
-        LOG.debug(
-            "{} logical_block_size_bytes: {}".format(disk_kname,
-                                                     logical_block_size_bytes))
+        LOG.debug("%s logical_block_size_bytes: %s",
+                  disk_kname, logical_block_size_bytes)
     except OSError as e:
         LOG.warning("Couldn't read block size, using default size 512: %s", e)
         logical_block_size_bytes = 512
@@ -663,21 +829,10 @@ def partition_handler(info, storage_config):
         LOG.debug("previous partition number for '%s' found to be '%s'",
                   info.get('id'), pnum)
         partition_kname = block.partition_kname(disk_kname, pnum)
-        previous_partition = os.path.join(disk_sysfs_path, partition_kname)
-        LOG.debug("previous partition: {}".format(previous_partition))
-        # XXX: sys/block/X/{size,start} is *ALWAYS* in 512b value
-        previous_size = int(
-            util.load_file(os.path.join(previous_partition, "size")))
-        previous_size_sectors = int(previous_size * 512 /
-                                    logical_block_size_bytes)
-        previous_start = int(
-            util.load_file(os.path.join(previous_partition, "start")))
-        previous_start_sectors = int(previous_start * 512 /
-                                     logical_block_size_bytes)
-        LOG.debug("previous partition.size_sectors: {}".format(
-                  previous_size_sectors))
-        LOG.debug("previous partition.start_sectors: {}".format(
-                  previous_start_sectors))
+        LOG.debug('partition_kname=%s', partition_kname)
+        (previous_start_sectors, previous_size_sectors) = (
+            calc_partition_info(disk_sysfs_path, partition_kname,
+                                logical_block_size_bytes))
 
     # Align to 1M at the beginning of the disk and at logical partitions
     alignment_offset = int((1 << 20) / logical_block_size_bytes)
@@ -716,86 +871,96 @@ def partition_handler(info, storage_config):
         length_sectors = length_sectors + (logdisks * alignment_offset)
 
     # Handle preserve flag
+    create_partition = True
     if config.value_as_boolean(info.get('preserve')):
-        return
-    elif config.value_as_boolean(storage_config.get(device).get('preserve')):
-        raise NotImplementedError("Partition '%s' is not marked to be \
-            preserved, but device '%s' is. At this time, preserving devices \
-            but not also the partitions on the devices is not supported, \
-            because of the possibility of damaging partitions intended to be \
-            preserved." % (info.get('id'), device))
-
-    # Set flag
-    # 'sgdisk --list-types'
-    sgdisk_flags = {"boot": 'ef00',
-                    "lvm": '8e00',
-                    "raid": 'fd00',
-                    "bios_grub": 'ef02',
-                    "prep": '4100',
-                    "swap": '8200',
-                    "home": '8302',
-                    "linux": '8300'}
-
-    LOG.info("adding partition '%s' to disk '%s' (ptable: '%s')",
-             info.get('id'), device, disk_ptable)
-    LOG.debug("partnum: %s offset_sectors: %s length_sectors: %s",
-              partnumber, offset_sectors, length_sectors)
-
-    # Wipe the partition if told to do so, do not wipe dos extended partitions
-    # as this may damage the extended partition table
-    if config.value_as_boolean(info.get('wipe')):
-        LOG.info("Preparing partition location on disk %s", disk)
-        if info.get('flag') == "extended":
-            LOG.warn("extended partitions do not need wiping, so skipping: "
-                     "'%s'" % info.get('id'))
+        part_path = block.dev_path(
+            block.partition_kname(disk_kname, partnumber))
+        partition_verify(part_path, info)
+        LOG.debug('Partition %s already present, skipping create', part_path)
+        create_partition = False
+
+    if create_partition:
+        # Set flag
+        # 'sgdisk --list-types'
+        LOG.info("adding partition '%s' to disk '%s' (ptable: '%s')",
+                 info.get('id'), device, disk_ptable)
+        LOG.debug("partnum: %s offset_sectors: %s length_sectors: %s",
+                  partnumber, offset_sectors, length_sectors)
+
+        # Pre-Wipe the partition if told to do so, do not wipe dos extended
+        # partitions as this may damage the extended partition table
+        if config.value_as_boolean(info.get('wipe')):
+            LOG.info("Preparing partition location on disk %s", disk)
+            if info.get('flag') == "extended":
+                LOG.warn("extended partitions do not need wiping, "
+                         "so skipping: '%s'" % info.get('id'))
+            else:
+                # wipe the start of the new partition first by zeroing 1M at
+                # the length of the previous partition
+                wipe_offset = int(offset_sectors * logical_block_size_bytes)
+                LOG.debug('Wiping 1M on %s at offset %s', disk, wipe_offset)
+                # We don't require exclusive access as we're wiping data at an
+                # offset and the current holder maybe part of the current
+                # storage configuration.
+                block.zero_file_at_offsets(disk, [wipe_offset],
+                                           exclusive=False)
+
+        if disk_ptable == "msdos":
+            if flag and flag == 'prep':
+                raise ValueError(
+                    'PReP partitions require a GPT partition table')
+
+            if flag in ["extended", "logical", "primary"]:
+                partition_type = flag
+            else:
+                partition_type = "primary"
+            cmd = ["parted", disk, "--script", "mkpart", partition_type,
+                   "%ss" % offset_sectors, "%ss" % str(offset_sectors +
+                                                       length_sectors)]
+            if flag == 'boot':
+                cmd.extend(['set', str(partnumber), 'boot', 'on'])
+
+            util.subp(cmd, capture=True)
+        elif disk_ptable == "gpt":
+            if flag and flag in SGDISK_FLAGS:
+                typecode = SGDISK_FLAGS[flag]
+            else:
+                typecode = SGDISK_FLAGS['linux']
+            cmd = ["sgdisk", "--new", "%s:%s:%s" % (partnumber, offset_sectors,
+                   length_sectors + offset_sectors),
+                   "--typecode=%s:%s" % (partnumber, typecode), disk]
+            util.subp(cmd, capture=True)
+        elif disk_ptable == "vtoc":
+            disk_device_id = storage_config.get(device).get('device_id')
+            dasd_device = dasd.DasdDevice(disk_device_id)
+            dasd_device.partition(partnumber, length_bytes)
         else:
-            # wipe the start of the new partition first by zeroing 1M at the
-            # length of the previous partition
-            wipe_offset = int(offset_sectors * logical_block_size_bytes)
-            LOG.debug('Wiping 1M on %s at offset %s', disk, wipe_offset)
-            # We don't require exclusive access as we're wiping data at an
-            # offset and the current holder maybe part of the current storage
-            # configuration.
-            block.zero_file_at_offsets(disk, [wipe_offset], exclusive=False)
-
-    if disk_ptable == "msdos":
-        if flag and flag == 'prep':
-            raise ValueError('PReP partitions require a GPT partition table')
-
-        if flag in ["extended", "logical", "primary"]:
-            partition_type = flag
+            raise ValueError("parent partition has invalid partition table")
+
+        # ensure partition exists
+        if multipath.is_mpath_device(disk):
+            udevadm_settle()  # allow partition creation to happen
+            # update device mapper table mapping to mpathX-partN
+            part_path = disk + "-part%s" % partnumber
+            # sometimes multipath lib creates a block device instead of
+            # a udev symlink, remove this and allow kpartx to create it
+            if os.path.exists(part_path) and not os.path.islink(part_path):
+                util.del_file(part_path)
+            util.subp(['kpartx', '-v', '-a', '-s', '-p', '-part', disk])
         else:
-            partition_type = "primary"
-        cmd = ["parted", disk, "--script", "mkpart", partition_type,
-               "%ss" % offset_sectors, "%ss" % str(offset_sectors +
-                                                   length_sectors)]
-        util.subp(cmd, capture=True)
-    elif disk_ptable == "gpt":
-        if flag and flag in sgdisk_flags:
-            typecode = sgdisk_flags[flag]
+            part_path = block.dev_path(block.partition_kname(disk_kname,
+                                                             partnumber))
+            block.rescan_block_devices([disk])
+        udevadm_settle(exists=part_path)
+
+    wipe_mode = info.get('wipe')
+    if wipe_mode:
+        if wipe_mode == 'superblock' and create_partition:
+            # partition creation pre-wipes partition superblock locations
+            pass
         else:
-            typecode = sgdisk_flags['linux']
-        cmd = ["sgdisk", "--new", "%s:%s:%s" % (partnumber, offset_sectors,
-               length_sectors + offset_sectors),
-               "--typecode=%s:%s" % (partnumber, typecode), disk]
-        util.subp(cmd, capture=True)
-    elif disk_ptable == "vtoc":
-        disk_device_id = storage_config.get(device).get('device_id')
-        dasd_device = dasd.DasdDevice(disk_device_id)
-        dasd_device.partition(partnumber, length_bytes)
-    else:
-        raise ValueError("parent partition has invalid partition table")
-
-    # ensure partition exists
-    part_path = block.dev_path(block.partition_kname(disk_kname, partnumber))
-    block.rescan_block_devices([disk])
-    udevadm_settle(exists=part_path)
-
-    # wipe the created partition if needed, superblocks have already been wiped
-    wipe_mode = info.get('wipe', 'superblock')
-    if wipe_mode != 'superblock':
-        LOG.debug('Wiping partition %s mode=%s', part_path, wipe_mode)
-        block.wipe_volume(part_path, mode=wipe_mode, exclusive=False)
+            LOG.debug('Wiping partition %s mode=%s', part_path, wipe_mode)
+            block.wipe_volume(part_path, mode=wipe_mode, exclusive=False)
 
     # Make the name if needed
     if storage_config.get(device).get('name') and partition_type != 'extended':
@@ -817,7 +982,7 @@ def format_handler(info, storage_config):
         return
 
     # Make filesystem using block library
-    LOG.debug("mkfs {} info: {}".format(volume_path, info))
+    LOG.debug("mkfs %s info: %s", volume_path, info)
     mkfs.mkfs_from_config(volume_path, info)
 
     device_type = storage_config.get(volume).get('type')
@@ -922,6 +1087,10 @@ def get_volume_spec(device_path):
         if platform.machine() == 's390x':
             devlinks = [link for link in info['DEVLINKS']
                         if link.startswith('/dev/disk/by-path')]
+        # use device-mapper uuid if present
+        if 'DM_UUID' in info:
+            devlinks = [link for link in info['DEVLINKS']
+                        if os.path.basename(link).startswith('dm-uuid-')]
         if len(devlinks) == 0:
             # use FS UUID if present
             devlinks = [link for link in info['DEVLINKS']
@@ -1041,10 +1210,28 @@ def mount_handler(info, storage_config):
                 target=state.get('target'), fstab=state.get('fstab'))
 
 
+def verify_volgroup_members(vg_name, pv_paths):
+    # LVM may be offline, so start it
+    lvm.activate_volgroups()
+    # Verify that volgroup exists and contains all specified devices
+    found_pvs = set(lvm.get_pvols_in_volgroup(vg_name))
+    expected_pvs = set(pv_paths)
+    msg = ('Verifying lvm volgroup %s members, expected %s, found %s ' % (
+           vg_name, expected_pvs, found_pvs))
+    LOG.debug(msg)
+    if expected_pvs != found_pvs:
+        raise RuntimeError(msg)
+
+
+def lvm_volgroup_verify(vg_name, device_paths):
+    verify_volgroup_members(vg_name, device_paths)
+
+
 def lvm_volgroup_handler(info, storage_config):
     devices = info.get('devices')
     device_paths = []
     name = info.get('name')
+    preserve = config.value_as_boolean(info.get('preserve'))
     if not devices:
         raise ValueError("devices for volgroup '%s' must be specified" %
                          info.get('id'))
@@ -1059,16 +1246,13 @@ def lvm_volgroup_handler(info, storage_config):
         device_paths.append(get_path_to_storage_volume(device_id,
                             storage_config))
 
-    # Handle preserve flag
-    if config.value_as_boolean(info.get('preserve')):
-        # LVM will probably be offline, so start it
-        util.subp(["vgchange", "-a", "y"])
-        # Verify that volgroup exists and contains all specified devices
-        if set(lvm.get_pvols_in_volgroup(name)) != set(device_paths):
-            raise ValueError("volgroup '%s' marked to be preserved, but does "
-                             "not exist or does not contain the right "
-                             "physical volumes" % info.get('id'))
-    else:
+    create_vg = True
+    if preserve:
+        lvm_volgroup_verify(name, device_paths)
+        LOG.debug('lvm_volgroup %s already present, skipping create', name)
+        create_vg = False
+
+    if create_vg:
         # Create vgrcreate command and run
         # capture output to avoid printing it to log
         # Use zero to clear target devices of any metadata
@@ -1079,9 +1263,34 @@ def lvm_volgroup_handler(info, storage_config):
     lvm.lvm_scan()
 
 
+def verify_lv_in_vg(lv_name, vg_name):
+    found_lvols = lvm.get_lvols_in_volgroup(vg_name)
+    msg = ('Verifying %s logical volume is in %s volume '
+           'group, found %s ' % (lv_name, vg_name, found_lvols))
+    LOG.debug(msg)
+    if lv_name not in found_lvols:
+        raise RuntimeError(msg)
+
+
+def verify_lv_size(lv_name, size):
+    expected_size_bytes = util.human2bytes(size)
+    found_size_bytes = lvm.get_lv_size_bytes(lv_name)
+    msg = ('Verifying %s logical value is size bytes %s, found %s '
+           % (lv_name, expected_size_bytes, found_size_bytes))
+    LOG.debug(msg)
+    if expected_size_bytes != found_size_bytes:
+        raise RuntimeError(msg)
+
+
+def lvm_partition_verify(lv_name, vg_name, info):
+    verify_lv_in_vg(lv_name, vg_name)
+    if 'size' in info:
+        verify_lv_size(lv_name, info['size'])
+
+
 def lvm_partition_handler(info, storage_config):
-    volgroup = storage_config.get(info.get('volgroup')).get('name')
-    name = info.get('name')
+    volgroup = storage_config[info['volgroup']]['name']
+    name = info['name']
     if not volgroup:
         raise ValueError("lvm volgroup for lvm partition must be specified")
     if not name:
@@ -1089,21 +1298,15 @@ def lvm_partition_handler(info, storage_config):
     if info.get('ptable'):
         raise ValueError("Partition tables on top of lvm logical volumes is "
                          "not supported")
+    preserve = config.value_as_boolean(info.get('preserve'))
 
-    # Handle preserve flag
-    if config.value_as_boolean(info.get('preserve')):
-        if name not in lvm.get_lvols_in_volgroup(volgroup):
-            raise ValueError("lvm partition '%s' marked to be preserved, but "
-                             "does not exist or does not mach storage "
-                             "configuration" % info.get('id'))
-    elif storage_config.get(info.get('volgroup')).get('preserve'):
-        raise NotImplementedError(
-            "Lvm Partition '%s' is not marked to be preserved, but volgroup "
-            "'%s' is. At this time, preserving volgroups but not also the lvm "
-            "partitions on the volgroup is not supported, because of the "
-            "possibility of damaging lvm  partitions intended to be "
-            "preserved." % (info.get('id'), volgroup))
-    else:
+    create_lv = True
+    if preserve:
+        lvm_partition_verify(name, volgroup, info)
+        LOG.debug('lvm_partition %s already present, skipping create', name)
+        create_lv = False
+
+    if create_lv:
         # Use 'wipesignatures' (if available) and 'zero' to clear target lv
         # of any fs metadata
         cmd = ["lvcreate", volgroup, "--name", name, "--zero=y"]
@@ -1122,7 +1325,29 @@ def lvm_partition_handler(info, storage_config):
     # refresh lvmetad
     lvm.lvm_scan()
 
-    make_dname(info.get('id'), storage_config)
+    wipe_mode = info.get('wipe', 'superblock')
+    if wipe_mode and create_lv:
+        lv_path = get_path_to_storage_volume(info['id'], storage_config)
+        LOG.debug('Wiping logical volume %s mode=%s', lv_path, wipe_mode)
+        block.wipe_volume(lv_path, mode=wipe_mode, exclusive=False)
+
+    make_dname(info['id'], storage_config)
+
+
+def verify_blkdev_used(dmcrypt_dev, expected_blkdev):
+    dminfo = block.dmsetup_info(dmcrypt_dev)
+    found_blkdev = dminfo['blkdevs_used']
+    msg = (
+        'Verifying %s volume, expecting %s , found %s ' % (
+         dmcrypt_dev, expected_blkdev, found_blkdev))
+    LOG.debug(msg)
+    if expected_blkdev != found_blkdev:
+        raise RuntimeError(msg)
+
+
+def dm_crypt_verify(dmcrypt_dev, volume_path):
+    verify_exists(dmcrypt_dev)
+    verify_blkdev_used(dmcrypt_dev, volume_path)
 
 
 def dm_crypt_handler(info, storage_config):
@@ -1131,11 +1356,13 @@ def dm_crypt_handler(info, storage_config):
     keysize = info.get('keysize')
     cipher = info.get('cipher')
     dm_name = info.get('dm_name')
+    if not dm_name:
+        dm_name = info.get('id')
+    dmcrypt_dev = os.path.join("/dev", "mapper", dm_name)
+    preserve = config.value_as_boolean(info.get('preserve'))
     if not volume:
         raise ValueError("volume for cryptsetup to operate on must be \
             specified")
-    if not dm_name:
-        dm_name = info.get('id')
 
     volume_path = get_path_to_storage_volume(volume, storage_config)
     volume_byid_path = block.disk_to_byid_path(volume_path)
@@ -1154,51 +1381,68 @@ def dm_crypt_handler(info, storage_config):
     else:
         raise ValueError("encryption key or keyfile must be specified")
 
-    # if zkey is available, attempt to generate and use it; if it's not
-    # available or fails to setup properly, fallback to normal cryptsetup
-    # passing strict=False downgrades log messages to warnings
-    zkey_used = None
-    if block.zkey_supported(strict=False):
-        volume_name = "%s:%s" % (volume_byid_path, dm_name)
-        LOG.debug('Attempting to setup zkey for %s', volume_name)
-        luks_type = 'luks2'
-        gen_cmd = ['zkey', 'generate', '--xts', '--volume-type', luks_type,
-                   '--sector-size', '4096', '--name', dm_name,
-                   '--description',
-                   "curtin generated zkey for %s" % volume_name,
-                   '--volumes', volume_name]
-        run_cmd = ['zkey', 'cryptsetup', '--run', '--volumes',
-                   volume_byid_path, '--batch-mode', '--key-file', keyfile]
-        try:
-            util.subp(gen_cmd, capture=True)
-            util.subp(run_cmd, capture=True)
-            zkey_used = os.path.join(os.path.split(state['fstab'])[0],
-                                     "zkey_used")
-            # mark in state that we used zkey
-            util.write_file(zkey_used, "1")
-        except util.ProcessExecutionError as e:
-            LOG.exception(e)
-            msg = 'Setup of zkey on %s failed, fallback to cryptsetup.'
-            LOG.error(msg % volume_path)
-
-    if not zkey_used:
-        LOG.debug('Using cryptsetup on %s', volume_path)
-        luks_type = "luks"
-        cmd = ["cryptsetup"]
-        if cipher:
-            cmd.extend(["--cipher", cipher])
-        if keysize:
-            cmd.extend(["--key-size", keysize])
-        cmd.extend(["luksFormat", volume_path, keyfile])
-        util.subp(cmd)
+    create_dmcrypt = True
+    if preserve:
+        dm_crypt_verify(dmcrypt_dev, volume_path)
+        LOG.debug('dm_crypt %s already present, skipping create', dmcrypt_dev)
+        create_dmcrypt = False
+
+    if create_dmcrypt:
+        # if zkey is available, attempt to generate and use it; if it's not
+        # available or fails to setup properly, fallback to normal cryptsetup
+        # passing strict=False downgrades log messages to warnings
+        zkey_used = None
+        if block.zkey_supported(strict=False):
+            volume_name = "%s:%s" % (volume_byid_path, dm_name)
+            LOG.debug('Attempting to setup zkey for %s', volume_name)
+            luks_type = 'luks2'
+            gen_cmd = ['zkey', 'generate', '--xts', '--volume-type', luks_type,
+                       '--sector-size', '4096', '--name', dm_name,
+                       '--description',
+                       "curtin generated zkey for %s" % volume_name,
+                       '--volumes', volume_name]
+            run_cmd = ['zkey', 'cryptsetup', '--run', '--volumes',
+                       volume_byid_path, '--batch-mode', '--key-file', keyfile]
+            try:
+                util.subp(gen_cmd, capture=True)
+                util.subp(run_cmd, capture=True)
+                zkey_used = os.path.join(os.path.split(state['fstab'])[0],
+                                         "zkey_used")
+                # mark in state that we used zkey
+                util.write_file(zkey_used, "1")
+            except util.ProcessExecutionError as e:
+                LOG.exception(e)
+                msg = 'Setup of zkey on %s failed, fallback to cryptsetup.'
+                LOG.error(msg % volume_path)
+
+        if not zkey_used:
+            LOG.debug('Using cryptsetup on %s', volume_path)
+            luks_type = "luks"
+            cmd = ["cryptsetup"]
+            if cipher:
+                cmd.extend(["--cipher", cipher])
+            if keysize:
+                cmd.extend(["--key-size", keysize])
+            cmd.extend(["luksFormat", volume_path, keyfile])
+            util.subp(cmd)
+
+        cmd = ["cryptsetup", "open", "--type", luks_type, volume_path, dm_name,
+               "--key-file", keyfile]
 
-    cmd = ["cryptsetup", "open", "--type", luks_type, volume_path, dm_name,
-           "--key-file", keyfile]
+        util.subp(cmd)
 
-    util.subp(cmd)
+        if keyfile_is_tmp:
+            os.remove(keyfile)
 
-    if keyfile_is_tmp:
-        os.remove(keyfile)
+    wipe_mode = info.get('wipe')
+    if wipe_mode:
+        if wipe_mode == 'superblock' and create_dmcrypt:
+            # newly created dmcrypt volumes do not need superblock wiping
+            pass
+        else:
+            LOG.debug('Wiping dm_crypt device %s mode=%s',
+                      dmcrypt_dev, wipe_mode)
+            block.wipe_volume(dmcrypt_dev, mode=wipe_mode, exclusive=False)
 
     # A crypttab will be created in the same directory as the fstab in the
     # configuration. This will then be copied onto the system later
@@ -1214,12 +1458,33 @@ def dm_crypt_handler(info, storage_config):
             so not writing crypttab")
 
 
+def verify_md_components(md_devname, raidlevel, device_paths, spare_paths):
+    # check if the array is already up, if not try to assemble
+    check_ok = mdadm.md_check(md_devname, raidlevel, device_paths,
+                              spare_paths)
+    if not check_ok:
+        LOG.info("assembling preserved raid for %s", md_devname)
+        mdadm.mdadm_assemble(md_devname, device_paths, spare_paths)
+        check_ok = mdadm.md_check(md_devname, raidlevel, device_paths,
+                                  spare_paths)
+    msg = ('Verifying %s raid composition, found raid is %s'
+           % (md_devname, 'OK' if check_ok else 'not OK'))
+    LOG.debug(msg)
+    if not check_ok:
+        raise RuntimeError(msg)
+
+
+def raid_verify(md_devname, raidlevel, device_paths, spare_paths):
+    verify_md_components(md_devname, raidlevel, device_paths, spare_paths)
+
+
 def raid_handler(info, storage_config):
     state = util.load_command_environment(strict=True)
     devices = info.get('devices')
     raidlevel = info.get('raidlevel')
     spare_devices = info.get('spare_devices')
     md_devname = block.dev_path(info.get('name'))
+    preserve = config.value_as_boolean(info.get('preserve'))
     if not devices:
         raise ValueError("devices for raid must be specified")
     if raidlevel not in ['linear', 'raid0', 0, 'stripe', 'raid1', 1, 'mirror',
@@ -1229,40 +1494,40 @@ def raid_handler(info, storage_config):
         if spare_devices:
             raise ValueError("spareunsupported in raidlevel '%s'" % raidlevel)
 
-    LOG.debug('raid: cfg: {}'.format(util.json_dumps(info)))
+    LOG.debug('raid: cfg: %s', util.json_dumps(info))
     device_paths = list(get_path_to_storage_volume(dev, storage_config) for
                         dev in devices)
-    LOG.debug('raid: device path mapping: {}'.format(
-              zip(devices, device_paths)))
+    LOG.debug('raid: device path mapping: %s',
+              list(zip(devices, device_paths)))
 
     spare_device_paths = []
     if spare_devices:
         spare_device_paths = list(get_path_to_storage_volume(dev,
                                   storage_config) for dev in spare_devices)
-        LOG.debug('raid: spare device path mapping: {}'.format(
-                  zip(spare_devices, spare_device_paths)))
-
-    # Handle preserve flag
-    if config.value_as_boolean(info.get('preserve')):
-        # check if the array is already up, if not try to assemble
-        if not mdadm.md_check(md_devname, raidlevel,
-                              device_paths, spare_device_paths):
-            LOG.info("assembling preserved raid for "
-                     "{}".format(md_devname))
-
-            mdadm.mdadm_assemble(md_devname, device_paths, spare_device_paths)
-
-            # try again after attempting to assemble
-            if not mdadm.md_check(md_devname, raidlevel,
-                                  devices, spare_device_paths):
-                raise ValueError("Unable to confirm preserved raid array: "
-                                 " {}".format(md_devname))
-        # raid is all OK
-        return
-
-    mdadm.mdadm_create(md_devname, raidlevel,
-                       device_paths, spare_device_paths,
-                       info.get('mdname', ''))
+        LOG.debug('raid: spare device path mapping: %s',
+                  list(zip(spare_devices, spare_device_paths)))
+
+    create_raid = True
+    if preserve:
+        raid_verify(md_devname, raidlevel, device_paths, spare_device_paths)
+        LOG.debug('raid %s already present, skipping create', md_devname)
+        create_raid = False
+
+    if create_raid:
+        mdadm.mdadm_create(md_devname, raidlevel,
+                           device_paths, spare_device_paths,
+                           info.get('mdname', ''))
+
+    wipe_mode = info.get('wipe')
+    if wipe_mode:
+        if wipe_mode == 'superblock' and create_raid:
+            # Newly created raid devices already wipe member superblocks at
+            # their data offset (this is equivalent to wiping the assembled
+            # device, see curtin.block.mdadm.zero_device for more details.
+            pass
+        else:
+            LOG.debug('Wiping raid device %s mode=%s', md_devname, wipe_mode)
+            block.wipe_volume(md_devname, mode=wipe_mode, exclusive=False)
 
     # Make dname rule for this dev
     make_dname(info.get('id'), storage_config)
@@ -1287,264 +1552,122 @@ def raid_handler(info, storage_config):
         disk_handler(info, storage_config)
 
 
+def verify_bcache_cachedev(cachedev):
+    """ verify that the specified cache_device is a bcache cache device."""
+    result = bcache.is_caching(cachedev)
+    msg = ('Verifying %s is bcache cache device, found device is %s'
+           % (cachedev, 'OK' if result else 'not OK'))
+    LOG.debug(msg)
+    if not result:
+        raise RuntimeError(msg)
+
+
+def verify_bcache_backingdev(backingdev):
+    """ verify that the specified backingdev is a bcache backing device."""
+    result = bcache.is_backing(backingdev)
+    msg = ('Verifying %s is bcache backing device, found device is %s'
+           % (backingdev, 'OK' if result else 'not OK'))
+    LOG.debug(msg)
+    if not result:
+        raise RuntimeError(msg)
+
+
+def verify_cache_mode(backing_dev, backing_superblock, expected_mode):
+    """ verify the backing device cache-mode is set as expected. """
+    found = backing_superblock.get('dev.data.cache_mode', '')
+    msg = ('Verifying %s bcache cache-mode, expecting %s, found %s'
+           % (backing_dev, expected_mode, found))
+    LOG.debug(msg)
+    if expected_mode not in found:
+        raise RuntimeError(msg)
+
+
+def verify_bcache_cset_uuid_match(backing_dev, cinfo, binfo):
+    expected_cset_uuid = cinfo.get('cset.uuid')
+    found_cset_uuid = binfo.get('cset.uuid')
+    result = ((expected_cset_uuid == found_cset_uuid)
+              if expected_cset_uuid else False)
+    msg = ('Verifying bcache backing_device %s cset.uuid is %s, found %s'
+           % (backing_dev, expected_cset_uuid, found_cset_uuid))
+    LOG.debug(msg)
+    if not result:
+        raise RuntimeError(msg)
+
+
+def bcache_verify_cachedev(cachedev):
+    verify_bcache_cachedev(cachedev)
+    return True
+
+
+def bcache_verify_backingdev(backingdev):
+    verify_bcache_backingdev(backingdev)
+    return True
+
+
+def bcache_verify(cachedev, backingdev, cache_mode):
+    bcache_verify_cachedev(cachedev)
+    bcache_verify_backingdev(backingdev)
+    cache_info = bcache.superblock_asdict(cachedev)
+    backing_info = bcache.superblock_asdict(backingdev)
+    verify_bcache_cset_uuid_match(backingdev, cache_info, backing_info)
+    if cache_mode:
+        verify_cache_mode(backingdev, backing_info, cache_mode)
+
+    return True
+
+
 def bcache_handler(info, storage_config):
     backing_device = get_path_to_storage_volume(info.get('backing_device'),
                                                 storage_config)
     cache_device = get_path_to_storage_volume(info.get('cache_device'),
                                               storage_config)
     cache_mode = info.get('cache_mode', None)
+    preserve = config.value_as_boolean(info.get('preserve'))
 
     if not backing_device or not cache_device:
         raise ValueError("backing device and cache device for bcache"
                          " must be specified")
 
-    bcache_sysfs = "/sys/fs/bcache"
-    udevadm_settle(exists=bcache_sysfs)
-
-    def register_bcache(bcache_device):
-        LOG.debug('register_bcache: %s > /sys/fs/bcache/register',
-                  bcache_device)
-        with open("/sys/fs/bcache/register", "w") as fp:
-            fp.write(bcache_device)
-
-    def _validate_bcache(bcache_device, bcache_sys_path):
-        """ check if bcache is ready, dump info
-
-        For cache devices, we expect to find a cacheN symlink
-        which will point to the underlying cache device; Find
-        this symlink, read it and compare bcache_device
-        specified in the parameters.
-
-        For backing devices, we expec to find a dev symlink
-        pointing to the bcacheN device to which the backing
-        device is enslaved.  From the dev symlink, we can
-        read the bcacheN holders list, which should contain
-        the backing device kname.
-
-        In either case, if we fail to find the correct
-        symlinks in sysfs, this method will raise
-        an OSError indicating the missing attribute.
-        """
-        # cacheset
-        # /sys/fs/bcache/<uuid>
-
-        # cache device
-        # /sys/class/block/<cdev>/bcache/set -> # .../fs/bcache/uuid
-
-        # backing
-        # /sys/class/block/<bdev>/bcache/cache -> # .../block/bcacheN
-        # /sys/class/block/<bdev>/bcache/dev -> # .../block/bcacheN
-
-        if bcache_sys_path.startswith('/sys/fs/bcache'):
-            LOG.debug("validating bcache caching device '%s' from sys_path"
-                      " '%s'", bcache_device, bcache_sys_path)
-            # we expect a cacheN symlink to point to bcache_device/bcache
-            sys_path_links = [os.path.join(bcache_sys_path, l)
-                              for l in os.listdir(bcache_sys_path)]
-            cache_links = [l for l in sys_path_links
-                           if os.path.islink(l) and (
-                              os.path.basename(l).startswith('cache'))]
-
-            if len(cache_links) == 0:
-                msg = ('Failed to find any cache links in %s:%s' % (
-                       bcache_sys_path, sys_path_links))
-                raise OSError(msg)
-
-            for link in cache_links:
-                target = os.readlink(link)
-                LOG.debug('Resolving symlink %s -> %s', link, target)
-                # cacheN  -> ../../../devices/.../<bcache_device>/bcache
-                # basename(dirname(readlink(link)))
-                target_cache_device = os.path.basename(
-                    os.path.dirname(target))
-                if os.path.basename(bcache_device) == target_cache_device:
-                    LOG.debug('Found match: bcache_device=%s target_device=%s',
-                              bcache_device, target_cache_device)
-                    return
-                else:
-                    msg = ('Cache symlink %s ' % target_cache_device +
-                           'points to incorrect device: %s' % bcache_device)
-                    raise OSError(msg)
-        elif bcache_sys_path.startswith('/sys/class/block'):
-            LOG.debug("validating bcache backing device '%s' from sys_path"
-                      " '%s'", bcache_device, bcache_sys_path)
-            # we expect a 'dev' symlink to point to the bcacheN device
-            bcache_dev = os.path.join(bcache_sys_path, 'dev')
-            if os.path.islink(bcache_dev):
-                bcache_dev_link = (
-                    os.path.basename(os.readlink(bcache_dev)))
-                LOG.debug('bcache device %s using bcache kname: %s',
-                          bcache_sys_path, bcache_dev_link)
-
-                bcache_slaves_path = os.path.join(bcache_dev, 'slaves')
-                slaves = os.listdir(bcache_slaves_path)
-                LOG.debug('bcache device %s has slaves: %s',
-                          bcache_sys_path, slaves)
-                if os.path.basename(bcache_device) in slaves:
-                    LOG.debug('bcache device %s found in slaves',
-                              os.path.basename(bcache_device))
-                    return
-                else:
-                    msg = ('Failed to find bcache device %s' % bcache_device +
-                           'in slaves list %s' % slaves)
-                    raise OSError(msg)
-            else:
-                msg = 'didnt find "dev" attribute on: %s', bcache_dev
-                return OSError(msg)
-
-        else:
-            LOG.debug("Failed to validate bcache device '%s' from sys_path"
-                      " '%s'", bcache_device, bcache_sys_path)
-            msg = ('sysfs path %s does not appear to be a bcache device' %
-                   bcache_sys_path)
-            return ValueError(msg)
-
-    def ensure_bcache_is_registered(bcache_device, expected, retry=None):
-        """ Test that bcache_device is found at an expected path and
-            re-register the device if it's not ready.
-
-            Retry the validation and registration as needed.
-        """
-        if not retry:
-            retry = BCACHE_REGISTRATION_RETRY
-
-        for attempt, wait in enumerate(retry):
-            # find the actual bcache device name via sysfs using the
-            # backing device's holders directory.
-            LOG.debug('check just created bcache %s if it is registered,'
-                      ' try=%s', bcache_device, attempt + 1)
-            try:
-                udevadm_settle()
-                if os.path.exists(expected):
-                    LOG.debug('Found bcache dev %s at expected path %s',
-                              bcache_device, expected)
-                    _validate_bcache(bcache_device, expected)
-                else:
-                    msg = 'bcache device path not found: %s' % expected
-                    LOG.debug(msg)
-                    raise ValueError(msg)
-
-                # if bcache path exists and holders are > 0 we can return
-                LOG.debug('bcache dev %s at path %s successfully registered'
-                          ' on attempt %s/%s',  bcache_device, expected,
-                          attempt + 1, len(retry))
-                return
-
-            except (OSError, IndexError, ValueError):
-                # Some versions of bcache-tools will register the bcache device
-                # as soon as we run make-bcache using udev rules, so wait for
-                # udev to settle, then try to locate the dev, on older versions
-                # we need to register it manually though
-                LOG.debug('bcache device was not registered, registering %s '
-                          'at /sys/fs/bcache/register', bcache_device)
-                try:
-                    register_bcache(bcache_device)
-                except IOError:
-                    # device creation is notoriously racy and this can trigger
-                    # "Invalid argument" IOErrors if it got created in "the
-                    # meantime" - just restart the function a few times to
-                    # check it all again
-                    pass
-
-            LOG.debug("bcache dev %s not ready, waiting %ss",
-                      bcache_device, wait)
-            time.sleep(wait)
-
-        # we've exhausted our retries
-        LOG.warning('Repetitive error registering the bcache dev %s',
-                    bcache_device)
-        raise RuntimeError("bcache device %s can't be registered" %
-                           bcache_device)
-
-    if cache_device:
-        # /sys/class/block/XXX/YYY/
-        cache_device_sysfs = block.sys_block_path(cache_device)
-
-        if os.path.exists(os.path.join(cache_device_sysfs, "bcache")):
-            LOG.debug('caching device already exists at {}/bcache. Read '
-                      'cset.uuid'.format(cache_device_sysfs))
-            (out, err) = util.subp(["bcache-super-show", cache_device],
-                                   capture=True)
-            LOG.debug('bcache-super-show=[{}]'.format(out))
-            [cset_uuid] = [line.split()[-1] for line in out.split("\n")
-                           if line.startswith('cset.uuid')]
-        else:
-            LOG.debug('caching device does not yet exist at {}/bcache. Make '
-                      'cache and get uuid'.format(cache_device_sysfs))
-            # make the cache device, extracting cacheset uuid
-            (out, err) = util.subp(["make-bcache", "-C", cache_device],
-                                   capture=True)
-            LOG.debug('out=[{}]'.format(out))
-            [cset_uuid] = [line.split()[-1] for line in out.split("\n")
-                           if line.startswith('Set UUID:')]
-
-        target_sysfs_path = '/sys/fs/bcache/%s' % cset_uuid
-        ensure_bcache_is_registered(cache_device, target_sysfs_path)
-
-    if backing_device:
-        backing_device_sysfs = block.sys_block_path(backing_device)
-        target_sysfs_path = os.path.join(backing_device_sysfs, "bcache")
-
-        # there should not be any pre-existing bcache device
-        bdir = os.path.join(backing_device_sysfs, "bcache")
-        if os.path.exists(bdir):
-            raise RuntimeError(
-                'Unexpected old bcache device: %s', backing_device)
-
-        LOG.debug('Creating a backing device on %s', backing_device)
-        util.subp(["make-bcache", "-B", backing_device])
-        ensure_bcache_is_registered(backing_device, target_sysfs_path)
-
-        # via the holders we can identify which bcache device we just created
-        # for a given backing device
-        holders = clear_holders.get_holders(backing_device)
-        if len(holders) != 1:
-            err = ('Invalid number {} of holding devices:'
-                   ' "{}"'.format(len(holders), holders))
-            LOG.error(err)
-            raise ValueError(err)
-        [bcache_dev] = holders
-        LOG.debug('The just created bcache device is {}'.format(holders))
-
-        if cache_device:
-            # if we specify both then we need to attach backing to cache
-            if cset_uuid:
-                LOG.info("Attaching backing device to cacheset: "
-                         "{} -> {} cset.uuid: {}".format(backing_device,
-                                                         cache_device,
-                                                         cset_uuid))
-                attach = os.path.join(backing_device_sysfs,
-                                      "bcache",
-                                      "attach")
-                with open(attach, "w") as fp:
-                    fp.write(cset_uuid)
-            else:
-                msg = "Invalid cset_uuid: {}".format(cset_uuid)
-                LOG.error(msg)
-                raise ValueError(msg)
-
-        if cache_mode:
-            LOG.info("Setting cache_mode on {} to {}".format(bcache_dev,
-                                                             cache_mode))
-            cache_mode_file = \
-                '/sys/block/{}/bcache/cache_mode'.format(bcache_dev)
-            with open(cache_mode_file, "w") as fp:
-                fp.write(cache_mode)
-    else:
-        # no backing device
-        if cache_mode:
-            raise ValueError("cache mode specified which can only be set per \
-                              backing devices, but none was specified")
+    create_bcache = True
+    if preserve:
+        if cache_device and backing_device:
+            if bcache_verify(cache_device, backing_device, cache_mode):
+                create_bcache = False
+        elif cache_device:
+            if bcache_verify_cachedev(cache_device):
+                create_bcache = False
+        elif backing_device:
+            if bcache_verify_backingdev(backing_device):
+                create_bcache = False
+        if not create_bcache:
+            LOG.debug('bcache %s already present, skipping create', info['id'])
+
+    cset_uuid = bcache_dev = None
+    if create_bcache and cache_device:
+        cset_uuid = bcache.create_cache_device(cache_device)
+
+    if create_bcache and backing_device:
+        bcache_dev = bcache.create_backing_device(backing_device, cache_device,
+                                                  cache_mode, cset_uuid)
+
+    if cache_mode and not backing_device:
+        raise ValueError("cache mode specified which can only be set on "
+                         "backing devices, but none was specified")
+
+    wipe_mode = info.get('wipe')
+    if wipe_mode and bcache_dev:
+        LOG.debug('Wiping bcache device %s mode=%s', bcache_dev, wipe_mode)
+        block.wipe_volume(bcache_dev, mode=wipe_mode, exclusive=False)
 
     if info.get('name'):
         # Make dname rule for this dev
         make_dname(info.get('id'), storage_config)
 
     if info.get('ptable'):
-        raise ValueError("Partition tables on top of lvm logical volumes is \
-                         not supported")
-    LOG.debug('Finished bcache creation for backing {} or caching {}'
-              .format(backing_device, cache_device))
+        disk_handler(info, storage_config)
+
+    LOG.debug('Finished bcache creation for backing %s or caching %s',
+              backing_device, cache_device)
 
 
 def zpool_handler(info, storage_config):
@@ -1611,8 +1734,8 @@ def zfs_handler(info, storage_config):
 
 
 def get_device_paths_from_storage_config(storage_config):
-    """Returns a list of device paths in a storage config filtering out
-       preserved or devices which do not have wipe configuration.
+    """Returns a list of device paths in a storage config which have wipe
+       config enabled filtering out constructed paths that do not exist.
 
     :param: storage_config: Ordered dict of storage configation
     """
@@ -1620,11 +1743,11 @@ def get_device_paths_from_storage_config(storage_config):
     for (k, v) in storage_config.items():
         if v.get('type') in ['disk', 'partition']:
             if config.value_as_boolean(v.get('wipe')):
-                if config.value_as_boolean(v.get('preserve')):
-                    continue
                 try:
-                    dpaths.append(
-                        get_path_to_storage_volume(k, storage_config))
+                    # skip paths that do not exit, nothing to wipe
+                    dpath = get_path_to_storage_volume(k, storage_config)
+                    if os.path.exists(dpath):
+                        dpaths.append(dpath)
                 except Exception:
                     pass
     return dpaths
diff --git a/curtin/commands/clear_holders.py b/curtin/commands/clear_holders.py
index 06f1398..2b2f881 100644
--- a/curtin/commands/clear_holders.py
+++ b/curtin/commands/clear_holders.py
@@ -1,27 +1,62 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
+from .block_meta import (
+    extract_storage_ordered_dict,
+    get_device_paths_from_storage_config,
+)
 from curtin import block
-from . import populate_one_subcmd
+from curtin.log import LOG
+from .import populate_one_subcmd
 
 
 def clear_holders_main(args):
     """
     wrapper for clear_holders accepting cli args
     """
-    if (not all(block.is_block_device(device) for device in args.devices) or
-            len(args.devices) == 0):
+    cfg = {}
+    if args.config:
+        cfg = args.config
+
+    # run clear holders on potential devices
+    devices = args.devices
+    if not devices:
+        if 'storage' in cfg:
+            devices = get_device_paths_from_storage_config(
+                extract_storage_ordered_dict(cfg))
+        if len(devices) == 0:
+            devices = cfg.get('block-meta', {}).get('devices', [])
+
+    if (not all(block.is_block_device(device) for device in devices) or
+            len(devices) == 0):
         raise ValueError('invalid devices specified')
+
     block.clear_holders.start_clear_holders_deps()
-    block.clear_holders.clear_holders(args.devices, try_preserve=args.preserve)
-    if args.preserve:
-        print('ran clear_holders attempting to preserve data. however, '
-              'hotplug support for some devices may cause holders to restart ')
-    block.clear_holders.assert_clear(args.devices)
+    if args.shutdown_plan:
+        # get current holders and plan how to shut them down
+        holder_trees = [block.clear_holders.gen_holders_tree(path)
+                        for path in devices]
+        LOG.info('Current device storage tree:\n%s',
+                 '\n'.join(block.clear_holders.format_holders_tree(tree)
+                           for tree in holder_trees))
+        ordered_devs = (
+            block.clear_holders.plan_shutdown_holder_trees(holder_trees))
+        LOG.info('Shutdown Plan:\n%s', "\n".join(map(str, ordered_devs)))
+
+    else:
+        block.clear_holders.clear_holders(devices, try_preserve=args.preserve)
+        if args.preserve:
+            print('ran clear_holders attempting to preserve data. however, '
+                  'hotplug support for some devices may cause holders to '
+                  'restart ')
+        block.clear_holders.assert_clear(devices)
 
 
 CMD_ARGUMENTS = (
     (('devices',
-      {'help': 'devices to free', 'default': [], 'nargs': '+'}),
+      {'help': 'devices to free', 'default': [], 'nargs': '*'}),
+     (('-P', '--shutdown-plan'),
+      {'help': 'Print the clear-holder shutdown plan only',
+       'default': False, 'action': 'store_true'}),
      (('-p', '--preserve'),
       {'help': 'try to shut down holders without erasing anything',
        'default': False, 'action': 'store_true'}),
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index fd48a5b..4afe00c 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -13,6 +13,7 @@ from curtin import config
 from curtin import block
 from curtin import distro
 from curtin.block import iscsi
+from curtin.block import lvm
 from curtin import net
 from curtin import futil
 from curtin.log import LOG
@@ -443,7 +444,7 @@ def uefi_reorder_loaders(grubcfg, target):
     front of the BootOrder.
     """
     if grubcfg.get('reorder_uefi', True):
-        efi_output = util.get_efibootmgr(target)
+        efi_output = util.get_efibootmgr(target=target)
         currently_booted = efi_output.get('current', None)
         boot_order = efi_output.get('order', [])
         if currently_booted:
@@ -463,6 +464,133 @@ def uefi_reorder_loaders(grubcfg, target):
         LOG.debug("Currently booted UEFI loader might no longer boot.")
 
 
+def uefi_remove_duplicate_entries(grubcfg, target):
+    seen = set()
+    to_remove = []
+    efi_output = util.get_efibootmgr(target=target)
+    entries = efi_output.get('entries', {})
+    for bootnum in sorted(entries):
+        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'])
+
+
+def _debconf_multiselect(package, variable, choices):
+    return "{package} {variable} multiselect {choices}".format(
+        package=package, variable=variable, choices=", ".join(choices))
+
+
+def configure_grub_debconf(boot_devices, target, uefi):
+    """Configure grub debconf variables in target.
+
+    Non-UEFI:
+    grub-pc grub-pc/install_devices multiselect d1, d2, d3
+
+    UEFI:
+    grub-pc grub-efi/install_devices multiselect d1
+
+    """
+    LOG.debug('Generating grub debconf_selections for devices=%s uefi=%s',
+              boot_devices, uefi)
+
+    byid_links = []
+    for dev in boot_devices:
+        link = block.disk_to_byid_path(dev)
+        byid_links.extend([link] if link else [dev])
+
+    selections = []
+    if uefi:
+        selections.append(_debconf_multiselect(
+            'grub-pc', 'grub-efi/install_devices', byid_links))
+    else:
+        selections.append(_debconf_multiselect(
+            'grub-pc', 'grub-pc/install_devices', byid_links))
+
+    cfg = {'debconf_selections': {'grub': "\n".join(selections)}}
+    LOG.info('Applying grub debconf_selections config:\n%s', cfg)
+    apt_config.apply_debconf_selections(cfg, target)
+    return
+
+
+def uefi_find_grub_device_ids(sconfig):
+    """ Scan the provided storage config for device_ids on which we
+        will install grub.  An order of precendence is required due to
+        legacy configurations which set grub_device on the disk but not
+        on the ESP config itself.  We prefer the latter as this allows
+        a disk to contain more than on ESP and choose to install grub
+        to a subset.  We always look for the 'primary' ESP which is
+        signified by being mounted at /boot/efi (only one can be mounted).
+
+        1. ESPs with grub_device: true are the preferred way to find
+           the specific set of devices on which to install grub
+        2. ESPs whose parent disk has grub_device: true
+
+        The primary ESP is the first element of the result if any
+        devices are found.
+
+        returns a list of storage-config ids on which grub will be installed.
+    """
+    # Only one EFI system partition can be mounted, but backup EFI
+    # partitions may exist.  Find all EFI partitions and determine
+    # the primary.
+    grub_device_ids = []
+    primary_esp = None
+    grub_partitions = []
+    esp_partitions = []
+    for item_id, item in sconfig.items():
+        if item['type'] == 'partition':
+            if item.get('grub_device'):
+                grub_partitions.append(item_id)
+                continue
+            elif item.get('flag') == 'boot':
+                esp_partitions.append(item_id)
+                continue
+
+        if item['type'] == 'mount' and item['path'] == '/boot/efi':
+            if primary_esp:
+                LOG.debug('Ignoring duplicate mounted primary ESP: %s',
+                          item_id)
+                continue
+            primary_esp = sconfig[item['device']]['volume']
+            if sconfig[primary_esp]['type'] == 'partition':
+                LOG.debug("Found primary UEFI ESP: %s", primary_esp)
+            else:
+                LOG.warn('Found primary ESP not on a partition: %s', item)
+
+    if primary_esp is None:
+        raise RuntimeError('Failed to find primary ESP mounted at /boot/efi')
+
+    grub_device_ids = [primary_esp]
+    # prefer grub_device: true partitions
+    if len(grub_partitions):
+        if primary_esp in grub_partitions:
+            grub_partitions.remove(primary_esp)
+        # insert the primary esp as first element
+        grub_device_ids.extend(grub_partitions)
+
+    # look at all esp entries, check if parent disk is grub_device: true
+    elif len(esp_partitions):
+        if primary_esp in esp_partitions:
+            esp_partitions.remove(primary_esp)
+        for esp_id in esp_partitions:
+            esp_disk = sconfig[sconfig[esp_id]['device']]
+            if esp_disk.get('grub_device'):
+                grub_device_ids.append(esp_id)
+
+    LOG.debug('Found UEFI ESP(s) for grub install: %s', grub_device_ids)
+    return grub_device_ids
+
+
 def setup_grub(cfg, target, osfamily=DISTROS.debian):
     # target is the path to the mounted filesystem
 
@@ -485,19 +613,13 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
     except ValueError:
         pass
 
+    uefi_bootable = util.is_uefi_bootable()
     if storage_cfg_odict:
         storage_grub_devices = []
-        if util.is_uefi_bootable():
-            # Curtin only supports creating one EFI system partition. Thus the
-            # grub_device can only be the default system partition mounted at
-            # /boot/efi.
-            for item_id, item in storage_cfg_odict.items():
-                if item.get('path') == '/boot/efi':
-                    efi_dev_id = storage_cfg_odict[item['device']]['volume']
-                    LOG.debug("checking: %s", item)
-                    storage_grub_devices.append(get_path_to_storage_volume(
-                        efi_dev_id, storage_cfg_odict))
-                    break
+        if uefi_bootable:
+            storage_grub_devices.extend([
+                get_path_to_storage_volume(dev_id, storage_cfg_odict)
+                for dev_id in uefi_find_grub_device_ids(storage_cfg_odict)])
         else:
             for item_id, item in storage_cfg_odict.items():
                 if not item.get('grub_device'):
@@ -507,6 +629,10 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
                     get_path_to_storage_volume(item_id, storage_cfg_odict))
 
         if len(storage_grub_devices) > 0:
+            if len(grubcfg.get('install_devices', [])):
+                LOG.warn("Storage Config grub device config takes precedence "
+                         "over grub 'install_devices' value, ignoring: %s",
+                         grubcfg['install_devices'])
             grubcfg['install_devices'] = storage_grub_devices
 
     LOG.debug("install_devices: %s", grubcfg.get('install_devices'))
@@ -580,10 +706,12 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
 
     if instdevs:
         instdevs = [block.get_dev_name_entry(i)[1] for i in instdevs]
+        if osfamily == DISTROS.debian:
+            configure_grub_debconf(instdevs, target, uefi_bootable)
     else:
         instdevs = ["none"]
 
-    if util.is_uefi_bootable() and grubcfg.get('update_nvram', True):
+    if uefi_bootable and grubcfg.get('update_nvram', True):
         uefi_remove_old_loaders(grubcfg, target)
 
     LOG.debug("installing grub to %s [replace_default=%s]",
@@ -591,7 +719,7 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
 
     with util.ChrootableTarget(target):
         args = ['install-grub']
-        if util.is_uefi_bootable():
+        if uefi_bootable:
             args.append("--uefi")
             LOG.debug("grubcfg: %s", grubcfg)
             if grubcfg.get('update_nvram', True):
@@ -600,6 +728,11 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
             else:
                 LOG.debug("NOT enabling UEFI nvram updates")
                 LOG.debug("Target system may not boot")
+            if len(instdevs) > 1:
+                instdevs = [instdevs[0]]
+                LOG.debug("Selecting primary EFI boot device %s for install",
+                          instdevs[0])
+
         args.append('--os-family=%s' % osfamily)
         args.append(target)
 
@@ -609,7 +742,8 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
             join_stdout_err + args + instdevs, env=env, capture=True)
         LOG.debug("%s\n%s\n", args + instdevs, out)
 
-    if util.is_uefi_bootable() and grubcfg.get('update_nvram', True):
+    if uefi_bootable and grubcfg.get('update_nvram', True):
+        uefi_remove_duplicate_entries(grubcfg, target)
         uefi_reorder_loaders(grubcfg, target)
 
 
@@ -843,10 +977,12 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
     if mpmode == 'disabled':
         return
 
-    if mpmode == 'auto' and not block.detect_multipath(target):
+    mp_device = block.detect_multipath(target)
+    LOG.info('Multipath detection found: %s', mp_device)
+    if mpmode == 'auto' and not mp_device:
         return
 
-    LOG.info("Detected multipath devices. Installing support via %s", mppkgs)
+    LOG.info("Detected multipath device. Installing support via %s", mppkgs)
     needed = [pkg for pkg in mppkgs if pkg
               not in distro.get_installed_packages(target)]
     if needed:
@@ -894,6 +1030,13 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
         blockdev, partno = block.get_blockdev_for_partition(target_dev)
 
         mpname = "mpath0"
+        mp_supported = block.multipath.multipath_supported()
+        if mp_supported:
+            mpname = block.multipath.get_mpath_id_from_device(mp_device)
+            if not mpname:
+                LOG.warning('Failed to determine multipath device name, using'
+                            ' fallback name "mpatha".')
+                mpname = 'mpatha'
         grub_dev = "/dev/mapper/" + mpname
         if partno is not None:
             if osfamily == DISTROS.debian:
@@ -904,16 +1047,30 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
                 raise ValueError(
                         'Unknown grub_dev mapping for distro: %s' % osfamily)
 
-        LOG.debug("configuring multipath install for root=%s wwid=%s",
-                  grub_dev, wwid)
-
-        multipath_bind_content = '\n'.join(
-            ['# This file was created by curtin while installing the system.',
-             "%s %s" % (mpname, wwid),
-             '# End of content generated by curtin.',
-             '# Everything below is maintained by multipath subsystem.',
-             ''])
-        util.write_file(multipath_bind_path, content=multipath_bind_content)
+        LOG.debug("configuring multipath for root=%s wwid=%s mpname=%s",
+                  grub_dev, wwid, mpname)
+        # use host bindings in target if it exists
+        if mp_supported and os.path.exists('/etc/multipath/bindings'):
+            if os.path.exists(multipath_bind_path):
+                util.del_file(multipath_bind_path)
+            util.ensure_dir(os.path.dirname(multipath_bind_path))
+            shutil.copy('/etc/multipath/bindings', multipath_bind_path)
+        else:
+            # bindings map the wwid of the disk to an mpath name, if we have
+            # a partition extract just the parent mpath_id, otherwise we'll
+            # get /dev/mapper/mpatha-part1-part1 entries in dm.
+            if '-part' in mpname:
+                mpath_id, mpath_part_num = mpname.split("-part")
+            else:
+                mpath_id = mpname
+            multipath_bind_content = '\n'.join([
+                ('# This file was created by curtin while '
+                 'installing the system.'), "%s %s" % (mpath_id, wwid),
+                '# End of content generated by curtin.',
+                '# Everything below is maintained by multipath subsystem.',
+                ''])
+            util.write_file(multipath_bind_path,
+                            content=multipath_bind_content)
 
         if osfamily == DISTROS.debian:
             grub_cfg = os.path.sep.join(
@@ -926,12 +1083,37 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
             raise ValueError(
                     'Unknown grub_cfg mapping for distro: %s' % osfamily)
 
-        msg = '\n'.join([
-            '# Written by curtin for multipath device %s %s' % (mpname, wwid),
-            'GRUB_DEVICE=%s' % grub_dev,
-            'GRUB_DISABLE_LINUX_UUID=true',
-            ''])
-        util.write_file(grub_cfg, omode=omode, content=msg)
+        if mp_supported:
+            # if root is on lvm, emit a multipath filter to lvm
+            lvmfilter = lvm.generate_multipath_dm_uuid_filter()
+            # lvm.conf device section indents config by 8 spaces
+            indent = ' ' * 8
+            mpfilter = '\n'.join([
+                indent + ('# Modified by curtin for multipath '
+                          'device %s' % (mpname)),
+                indent + lvmfilter])
+            lvmconf = paths.target_path(target, '/etc/lvm/lvm.conf')
+            orig_content = util.load_file(lvmconf)
+            devices_match = re.search(r'devices\ {',
+                                      orig_content, re.MULTILINE)
+            if devices_match:
+                LOG.debug('Adding multipath filter (%s) to lvm.conf', mpfilter)
+                shutil.move(lvmconf, lvmconf + '.orig-curtin')
+                index = devices_match.end()
+                new_content = (
+                    orig_content[:index] + '\n' + mpfilter + '\n' +
+                    orig_content[index + 1:])
+                util.write_file(lvmconf, new_content)
+        else:
+            # TODO: fix up dnames without multipath available on host
+            msg = '\n'.join([
+                '# Written by curtin for multipath device %s %s' % (mpname,
+                                                                    wwid),
+                'GRUB_DEVICE=%s' % grub_dev,
+                'GRUB_DISABLE_LINUX_UUID=true',
+                ''])
+            util.write_file(grub_cfg, omode=omode, content=msg)
+
     else:
         LOG.warn("Not sure how this will boot")
 
@@ -946,7 +1128,7 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
         msg = '\n'.join([
             '# Written by curtin for multipath device wwid "%s"' % wwid,
             'force_drivers+=" dm-multipath "',
-            'add_dracutmodules+="multipath"',
+            'add_dracutmodules+=" multipath"',
             'install_items+="/etc/multipath.conf /etc/multipath/bindings"',
             ''])
         util.write_file(dracut_conf_multipath, content=msg)
@@ -1245,13 +1427,18 @@ def handle_cloudconfig(cfg, base_dir=None):
 
 
 def ubuntu_core_curthooks(cfg, target=None):
-    """ Ubuntu-Core 16 images cannot execute standard curthooks
-        Instead we copy in any cloud-init configuration to
-        the 'LABEL=writable' partition mounted at target.
+    """ Ubuntu-Core images cannot execute standard curthooks.
+        Instead, for core16/18 we copy in any cloud-init configuration to
+        the 'LABEL=writable' partition mounted at target.  For core20, we
+        write a cloud-config.d directory in the 'ubuntu-seed' location.
     """
 
     ubuntu_core_target = os.path.join(target, "system-data")
     cc_target = os.path.join(ubuntu_core_target, 'etc/cloud/cloud.cfg.d')
+    if not os.path.exists(ubuntu_core_target):  # uc20
+        ubuntu_core_target = target
+        cc_target = os.path.join(ubuntu_core_target, 'data', 'etc',
+                                 'cloud', 'cloud.cfg.d')
 
     cloudconfig = cfg.get('cloudconfig', None)
     if cloudconfig:
@@ -1385,7 +1572,7 @@ def redhat_update_dracut_config(target, cfg):
             add_modules.add(initramfs_mapping['lvm']['modules'])
 
     dconfig = ['# Written by curtin for custom storage config']
-    dconfig.append('add_dracutmodules+="%s"' % (" ".join(add_modules)))
+    dconfig.append('add_dracutmodules+=" %s"' % (" ".join(add_modules)))
     for conf in add_conf:
         dconfig.append('%s="yes"' % conf)
 
diff --git a/curtin/distro.py b/curtin/distro.py
index ed178bd..1f62e7a 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -131,10 +131,27 @@ def get_osfamily(target=None):
 
 
 def is_ubuntu_core(target=None):
-    """Check if Ubuntu-Core specific directory is present at target"""
+    """Check if any Ubuntu-Core specific directory is present at target"""
+    return any([is_ubuntu_core_16(target),
+                is_ubuntu_core_18(target),
+                is_ubuntu_core_20(target)])
+
+
+def is_ubuntu_core_16(target=None):
+    """Check if Ubuntu-Core 16 specific directory is present at target"""
     return os.path.exists(target_path(target, 'system-data/var/lib/snapd'))
 
 
+def is_ubuntu_core_18(target=None):
+    """Check if Ubuntu-Core 18 specific directory is present at target"""
+    return is_ubuntu_core_16(target)
+
+
+def is_ubuntu_core_20(target=None):
+    """Check if Ubuntu-Core 20 specific directory is present at target"""
+    return os.path.exists(target_path(target, 'snaps'))
+
+
 def is_centos(target=None):
     """Check if CentOS specific file is present at target"""
     return os.path.exists(target_path(target, 'etc/centos-release'))
diff --git a/curtin/storage_config.py b/curtin/storage_config.py
index 545b50c..e285f98 100644
--- a/curtin/storage_config.py
+++ b/curtin/storage_config.py
@@ -11,6 +11,36 @@ from curtin.block import schemas
 from curtin import config as curtin_config
 from curtin import util
 
+# map
+# https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
+# to
+# curtin/commands/block_meta.py:partition_handler()sgdisk_flags/types
+GPT_GUID_TO_CURTIN_MAP = {
+    'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': ('boot', 'EF00'),
+    '21686148-6449-6E6F-744E-656564454649': ('bios_grub', 'EF02'),
+    '933AC7E1-2EB4-4F13-B844-0E14E2AEF915': ('home', '8302'),
+    '0FC63DAF-8483-4772-8E79-3D69D8477DE4': ('linux', '8300'),
+    'E6D6D379-F507-44C2-A23C-238F2A3DF928': ('lvm', '8e00'),
+    '024DEE41-33E7-11D3-9D69-0008C781F39F': ('mbr', ''),
+    '9E1A2D38-C612-4316-AA26-8B49521E5A8B': ('prep', '4200'),
+    'A19D880F-05FC-4D3B-A006-743F0F84911E': ('raid', 'fd00'),
+    '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F': ('swap', '8200'),
+}
+
+# MBR types
+# https://www.win.tue.nl/~aeb/partitions/partition_types-2.html
+# to
+# curtin/commands/block_meta.py:partition_handler()sgdisk_flags/types
+MBR_TYPE_TO_CURTIN_MAP = {
+    '0XF': ('extended', 'f'),
+    '0X5': ('extended', 'f'),
+    '0X80': ('boot', '80'),
+    '0X83': ('linux', '83'),
+    '0X85': ('extended', 'f'),
+    '0XC5': ('extended', 'f'),
+}
+
+PTABLE_TYPE_MAP = dict(GPT_GUID_TO_CURTIN_MAP, **MBR_TYPE_TO_CURTIN_MAP)
 
 StorageConfig = namedtuple('StorageConfig', ('type', 'schema'))
 STORAGE_CONFIG_TYPES = {
@@ -40,7 +70,7 @@ def get_storage_type_schemas():
 
 
 STORAGE_CONFIG_SCHEMA = {
-    '$schema': 'http://json-schema.org/draft-07/schema#',
+    '$schema': 'http://json-schema.org/draft-04/schema#',
     'name': 'ASTORAGECONFIG',
     'title': 'curtin storage configuration for an installation.',
     'description': (
@@ -437,9 +467,9 @@ class ProbertParser(object):
         if blockdev['DEVTYPE'] == 'partition':
             bd_name = self.partition_parent_devname(blockdev)
         bd_name = os.path.basename(bd_name)
-        for path in mpath_data['paths']:
-            if bd_name == path['device']:
-                rv = path['multipath']
+        for path in mpath_data.get('paths', []):
+            if bd_name == path.get('device'):
+                rv = path.get('multipath')
                 return rv
 
     def find_mpath_member(self, blockdev):
@@ -457,9 +487,12 @@ class ProbertParser(object):
             dm_mpath = blockdev.get('DM_MPATH')
             dm_uuid = blockdev.get('DM_UUID')
             dm_part = blockdev.get('DM_PART')
+            dm_name = blockdev.get('DM_NAME')
 
             if dm_mpath:
                 multipath = dm_mpath
+            elif dm_name:
+                multipath = dm_name
             else:
                 # part1-mpath-30000000000000064
                 # mpath-30000000000000064
@@ -648,7 +681,7 @@ class BlockdevParser(ProbertParser):
 
         for devname, data in self.blockdev_data.items():
             # skip composed devices here, except partitions
-            if data.get('DEVPATH', '').startswith('/devices/virtual'):
+            if data.get('DEVPATH', '').startswith('/devices/virtual/block'):
                 if data.get('DEVTYPE', '') != "partition":
                     continue
             entry = self.asdict(data)
@@ -661,34 +694,12 @@ class BlockdevParser(ProbertParser):
                 configs.append(entry)
         return (configs, errors)
 
-    def ptable_uuid_to_flag_entry(self, guid):
-        # map
-        # https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
-        # to
-        # curtin/commands/block_meta.py:partition_handler()sgdisk_flags/types
-        # MBR types
-        # https://www.win.tue.nl/~aeb/partitions/partition_types-2.html
-        guid_map = {
-            'C12A7328-F81F-11D2-BA4B-00A0C93EC93B': ('boot', 'EF00'),
-            '21686148-6449-6E6F-744E-656564454649': ('bios_grub', 'EF02'),
-            '933AC7E1-2EB4-4F13-B844-0E14E2AEF915': ('home', '8302'),
-            '0FC63DAF-8483-4772-8E79-3D69D8477DE4': ('linux', '8300'),
-            'E6D6D379-F507-44C2-A23C-238F2A3DF928': ('lvm', '8e00'),
-            '024DEE41-33E7-11D3-9D69-0008C781F39F': ('mbr', ''),
-            '9E1A2D38-C612-4316-AA26-8B49521E5A8B': ('prep', '4200'),
-            'A19D880F-05FC-4D3B-A006-743F0F84911E': ('raid', 'fd00'),
-            '0657FD6D-A4AB-43C4-84E5-0933C84B4F4F': ('swap', '8200'),
-            '0X83': ('linux', '83'),
-            '0XF': ('extended', 'f'),
-            '0X5': ('extended', 'f'),
-            '0X85': ('extended', 'f'),
-            '0XC5': ('extended', 'f'),
-        }
-        name = code = None
-        if guid and guid.upper() in guid_map:
-            name, code = guid_map[guid.upper()]
-
-        return (name, code)
+    def valid_id(self, id_value):
+        # reject wwn=0x0+
+        if id_value.lower().startswith('0x'):
+            return int(id_value, 16) > 0
+        # accept non-empty (removing whitspace) strings
+        return len(''.join(id_value.split())) > 0
 
     def get_unique_ids(self, blockdev):
         """ extract preferred ID_* keys for www and serial values.
@@ -705,7 +716,8 @@ class BlockdevParser(ProbertParser):
         for skey, id_keys in source_keys.items():
             for id_key in id_keys:
                 if id_key in blockdev and skey not in uniq:
-                    uniq[skey] = blockdev[id_key]
+                    if self.valid_id(blockdev[id_key]):
+                        uniq[skey] = blockdev[id_key]
 
         return uniq
 
@@ -744,7 +756,8 @@ class BlockdevParser(ProbertParser):
         }
         if blockdev_data.get('DM_MULTIPATH_DEVICE_PATH') == "1":
             mpath_name = self.get_mpath_name(blockdev_data)
-            entry['multipath'] = mpath_name
+            if mpath_name:
+                entry['multipath'] = mpath_name
 
         # default disks to gpt
         if entry['type'] == 'disk':
@@ -807,7 +820,11 @@ class BlockdevParser(ProbertParser):
                 entry['size'] *= 512
 
             ptype = blockdev_data.get('ID_PART_ENTRY_TYPE')
-            flag_name, _flag_code = self.ptable_uuid_to_flag_entry(ptype)
+            # use PART_ENTRY_FLAGS if set, msdos
+            ptype_flag = blockdev_data.get('ID_PART_ENTRY_FLAGS')
+            if ptype_flag:
+                ptype = ptype_flag
+            flag_name, _flag_code = ptable_uuid_to_flag_entry(ptype)
 
             # logical partitions are not tagged in data, however
             # the partition number > 4 (ie, not primary nor extended)
@@ -1242,6 +1259,17 @@ class ZfsParser(ProbertParser):
         return (zpool_configs + zfs_configs, errors)
 
 
+def ptable_uuid_to_flag_entry(guid):
+    name = code = None
+    # prefix non-uuid guid values with 0x
+    if guid and '-' not in guid and not guid.upper().startswith('0X'):
+        guid = '0x' + guid
+    if guid and guid.upper() in PTABLE_TYPE_MAP:
+        name, code = PTABLE_TYPE_MAP[guid.upper()]
+
+    return (name, code)
+
+
 def extract_storage_config(probe_data, strict=False):
     """ Examine a probert storage dictionary and extract a curtin
         storage configuration that would recreate all of the
diff --git a/curtin/udev.py b/curtin/udev.py
index e2e3dd0..f83f216 100644
--- a/curtin/udev.py
+++ b/curtin/udev.py
@@ -4,7 +4,14 @@ import shlex
 import os
 
 from curtin import util
-from curtin.log import logged_call
+from curtin.log import logged_call, LOG
+
+try:
+    shlex_quote = shlex.quote
+except AttributeError:
+    # python2.7 uses pipes.quote
+    import pipes
+    shlex_quote = pipes.quote
 
 
 def compose_udev_equality(key, value):
@@ -90,7 +97,23 @@ def udevadm_info(path=None):
             value = None
         if value:
             # preserve spaces in values to match udev database
-            parsed = shlex.split(value)
+            try:
+                parsed = shlex.split(value)
+            except ValueError:
+                # strip the leading/ending single tick from udev output before
+                # escaping the value to prevent their inclusion in the result.
+                trimmed_value = value[1:-1]
+                try:
+                    quoted = shlex_quote(trimmed_value)
+                    LOG.debug('udevadm_info: quoting shell-escape chars '
+                              'in %s=%s -> %s', key, value, quoted)
+                    parsed = shlex.split(quoted)
+                except ValueError:
+                    escaped_value = (
+                        trimmed_value.replace("'", "_").replace('"', "_"))
+                    LOG.debug('udevadm_info: replacing shell-escape chars '
+                              'in %s=%s -> %s', key, value, escaped_value)
+                    parsed = shlex.split(escaped_value)
             if ' ' not in value:
                 info[key] = parsed[0]
             else:
diff --git a/curtin/util.py b/curtin/util.py
index eb2228f..afef58d 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -574,6 +574,15 @@ def decode_binary(blob, encoding='utf-8', errors='replace'):
     return blob.decode(encoding, errors=errors)
 
 
+def load_json(text, root_types=(dict,)):
+    decoded = json.loads(text)
+    if not isinstance(decoded, tuple(root_types)):
+        expected_types = ", ".join([str(t) for t in root_types])
+        raise TypeError("(%s) root types expected, got %s instead"
+                        % (expected_types, type(decoded)))
+    return decoded
+
+
 def file_size(path):
     """get the size of a file"""
     with open(path, 'rb') as fp:
@@ -633,6 +642,8 @@ class ChrootableTarget(object):
             self.mounts = mounts
         else:
             self.mounts = ["/dev", "/proc", "/run", "/sys"]
+            if is_uefi_bootable():
+                self.mounts.append('/sys/firmware/efi/efivars')
         self.umounts = []
         self.disabled_daemons = False
         self.allow_daemons = allow_daemons
@@ -856,7 +867,7 @@ def parse_efibootmgr(content):
     return output
 
 
-def get_efibootmgr(target):
+def get_efibootmgr(target=None):
     """Return mapping of EFI information.
 
     Calls `efibootmgr` inside the `target`.
@@ -879,7 +890,7 @@ def get_efibootmgr(target):
             }
         }
     """
-    with ChrootableTarget(target) as in_chroot:
+    with ChrootableTarget(target=target) as in_chroot:
         stdout, _ = in_chroot.subp(['efibootmgr', '-v'], capture=True)
         output = parse_efibootmgr(stdout)
         return output
diff --git a/debian/changelog b/debian/changelog
index 924a9c5..d0a4c9e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,61 @@
+curtin (19.3-68-g6cbdc02d-0ubuntu1) groovy; urgency=medium
+
+  * New upstream snapshot.
+    - Makefile: make adjustments to call lint/style tools via python module
+    - block-discover: ignore invalid id_serial/id_wwn values (LP: #1876848)
+    - Fix handing of reusing msdos partitions and flags (LP: #1875903)
+    - block.detect_multipath: ignore fake "devices" from /proc/mounts
+      [Michael Hudson-Doyle] (LP: #1876626)
+    - udev: use shlex.quote when shlex.split errors on shell-escape chars
+      (LP: #1875085)
+    - lvm: don't use vgscan --mknodes
+    - vmtest: rsync don't cross filesystem boundaries when copying
+      (LP: #1873909)
+    - vmtest: basic/basic_scsi adjust collect/tests for unstable device names
+      (LP: #1874100)
+    - Add unittests for partition_handler calc_[dm]_part_info and kpartx paths
+    - multipath: attempt to enforce /dev/mapper/mpath files are symlinks
+    - block-meta: device mapper partitions may be block devices not links
+    - Default to dm_name being id if empty earlier in dm_crypt_handler()
+      [Łukasz 'sil2100' Zemczak] (LP: #1874243)
+    - storage: correct declared schema draft version for storage schema
+    - test_clear_holders: add missing zfs mock
+    - Mock out zfs_supported to prevent attempting to load kernel modules
+    - block-meta: skip wipe device paths if not present (LP: #1869075)
+    - unittest: do not allow util.subp by default (LP: #1873913)
+    - curthooks: support multiple ESP on UEFI bootable systems
+    - block-discover: handle missing multipath 'path' data, use DM_NAME
+      (LP: #1873728)
+    - lvm-over-multipath: handle lookups of multipath members (LP: #1869075)
+    - block-meta: don't filter preserve=true devices, select by wipe
+      (LP: #1837214)
+    - vmtest: basic use dname to lookup disk with multiple partitions
+    - block-meta: Don't check the ptable type of a disk with no ptable
+    - curthooks: always use ChrootableTarget.subp when calling efibootmgr
+    - storage: enable and use multipath during storage configuration
+      (LP: #1869075)
+    - block-discover: detect nvme multipath devices (LP: #1868109)
+    - clear-holders: Tolerate vgchange errors during discovery (LP: #1870037)
+    - block-meta: handle preserve with vtoc ptable (LP: #1871158)
+    - vmtest: use -partition file for TestReuseRAIDMemberPartition class
+    - format: extra_options should be a list type
+    - tox: add pyflakes to the default tox run [Paride Legovini]
+    - storage_config: Add 'extra_options' parameter to allow custom mkfs
+      (LP: #1869069)
+    - Add support for installing Ubuntu Core 20 images
+    - tox.ini: Fix issues with newer tox on focal
+    - vmtest: Fix test_basic.py to run on s390x (LP: #1866663)
+    - vmtest: use util.load_file for loading collect files
+    - block-meta: refactor storage_config preserve and wipe settings
+      (LP: #1837214)
+    - block-discover: skip 'multipath' key in blockdevice if mpath name is None
+    - tox: all py27 environments should use the base py27 deps
+    - uefi: refactor efibootmg handling to support removing duplicate entries
+      (LP: #1864257)
+    - tox: pin setuptools < 45 to allow installing py27 in virtenv
+
+ -- Ryan Harper <ryan.harper@xxxxxxxxxxxxx>  Thu, 07 May 2020 15:43:37 -0500
+
 curtin (19.3-27-g437caaa9-0ubuntu1) focal; urgency=medium
 
   * New upstream snapshot.
diff --git a/doc/topics/storage.rst b/doc/topics/storage.rst
index c85174d..ebcf8a4 100644
--- a/doc/topics/storage.rst
+++ b/doc/topics/storage.rst
@@ -212,7 +212,7 @@ This can specify the manufacturer or model of the disk. It is not currently
 used by curtin, but can be useful for a human reading a config file. Future
 versions of curtin may make use of this information.
 
-**wipe**: *superblock, superblock-recursive, zero, random*
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
 
 If wipe is specified, **the disk contents will be destroyed**.  In the case that
 a disk is a part of virtual block device, like bcache, RAID array, or LVM, then
@@ -233,22 +233,34 @@ The ``wipe: random`` option will write pseudo-random data from /dev/urandom
 Depending on the size and speed of the disk; it may take a long time to
 complete.
 
+The ``wipe: pvremove`` option will execute the ``pvremove`` command to
+wipe the LVM metadata so that the device is no longer part of an LVM.
+
+
 **preserve**: *true, false*
 
 When the preserve key is present and set to ``true`` curtin will attempt
-to use the disk without damaging data present on it. If ``preserve`` is set and
-``ptable`` is also set, then curtin will validate that the partition table
-specified by ``ptable`` exists on the disk and will raise an error if it does
-not. If ``preserve`` is set and ``ptable`` is not, then curtin will be able to
-use the disk in later commands, but will not check if the disk has a valid
-partition table, and will only verify that the disk exists.
-
-It can be dangerous to try to move or re-size filesystems and partitions
-containing data that needs to be preserved. Therefor curtin does not support
-preserving a disk without also preserving the partitions on it. If a disk is
-set to be preserved and curtin is told to move a partition on that disk,
-installation will stop. It is still possible to reformat partitions that do
-not need to be preserved.
+reuse the existing storage device.  Curtin will verify aspects of the device
+against the configuration provided.  For example, when assessing whether
+curtin can use a preserved partition, curtin checks that the device exists,
+size of the partition matches the value in the config and checks if the same
+partition flag is set.  The set of verification checks vary by device type.
+If curtin encounters a mismatch between config and what is found on the
+device a RuntimeError will be raised with the expected and found values and
+halt the installation.  Currently curtin will verify the follow storage types:
+
+- disk
+- partition
+- lvm_volgroup
+- lvm_partition
+- dm_crypt
+- raid
+- bcache
+- format
+
+One specific use-case of ``preserve: true`` is in conjunction with the ``wipe``
+flag.  This allows a device to reused, but have the *content* of the device to
+be removed.
 
 **name**: *<name>*
 
@@ -327,7 +339,7 @@ The ``device`` key refers to the ``id`` of a disk in the storage configuration.
 The disk entry must already be defined in the list of commands to ensure that
 it has already been processed.
 
-**wipe**: *superblock, pvremove, zero, random*
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
 
 After the partition is added to the disk's partition table, curtin can run a
 wipe command on the partition. The wipe command values are the sames as for
@@ -337,9 +349,7 @@ disks.
 
   Curtin will automatically wipe 1MB at the starting location of the partition
   prior to creating the partition to ensure that other block layers or devices
-  do not enable themselves and prevent accessing the partition.  Wipe
-  and other destructive operations only occur if the ``preserve`` value
-  is not set to ``True``.
+  do not enable themselves and prevent accessing the partition.
 
 **flag**: *logical, extended, boot, bios_grub, swap, lvm, raid, home, prep*
 
@@ -370,7 +380,7 @@ filesystem or be mounted anywhere on the system.
 **preserve**: *true, false*
 
 If the preserve flag is set to true, curtin will verify that the partition
-exists and will not modify the partition.
+exists and that  the ``size`` and ``flag`` match the configuration provided.
 
 **name**: *<name>*
 
@@ -462,6 +472,12 @@ curtin will set the uuid of the new filesystem to the specified value.
 
 If the ``preserve`` key is set to true, curtin will not format the partition.
 
+**extra_options**: *<list of strings>*
+
+The ``extra_options`` key is a list of strings that is appended to the mkfs
+command used to create the filesystem.  **Use of this setting is dangerous.
+Some flags may cause an error during creation of a filesystem.**
+
 **Config Example**::
 
  - id: disk0-part1-fs1
@@ -470,6 +486,21 @@ If the ``preserve`` key is set to true, curtin will not format the partition.
    label: cloud-image
    volume: disk0-part1
 
+ - id: disk1-part1-fs1
+   type: format
+   fstype: ext4
+   label: osdata1
+   uuid: ed51882e-8688-4cd8-97ca-1f2b8bbee458
+   extra_options: ['-O', '^metadata_csum,^64bit']
+
+ - id: nvme1-part1-fs1
+   type: format
+   fstype: ext4
+   label: cacheset1
+   extra_options:
+     - -E
+     - offset=1024,nodiscard
+
 Mount Command
 ~~~~~~~~~~~~~
 The mount command mounts the target filesystem and creates an entry for it in
@@ -594,6 +625,14 @@ The ``devices`` key gives a list of devices to use as physical volumes. Each
 device is specified using the ``id`` of existing devices in the storage config.
 Almost anything can be used as a device such as partitions, whole disks, RAID.
 
+**preserve**: *true, false*
+
+If the ``preserve`` option is True, curtin will verify that volume group
+specified by the ``name`` option is present and that the physical volumes
+of the group match the devices specified in ``devices``.  There is no ``wipe``
+option for volume groups.
+
+
 **Config Example**::
 
  - id: volgroup1
@@ -642,6 +681,18 @@ number followed by a SI unit should work, i.e. *B, kB, MB, GB, TB*.
 If the ``size`` key is omitted then all remaining space on the volgroup will be
 used for the logical volume.
 
+**preserve**: *true, false*
+
+If the ``preserve`` option is True, curtin will verify that specified lvm
+partition is part of the specified volume group.  If ``size`` is specified
+curtin will verify the size matches the specified value.
+
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
+
+If ``wipe`` option is set, and ``preserve`` is False, curtin will wipe the
+contents of the lvm partition.  Curtin skips wipe settings if it creates
+the lvm partition.
+
 .. note::
 
   Curtin does not adjust size values.  If you specific a size that exceeds the 
@@ -705,6 +756,19 @@ system will prompt for this password in order to mount the disk.
 
 Exactly one of **key** and **keyfile** must be supplied.
 
+**preserve**: *true, false*
+
+If the ``preserve`` option is True, curtin will verify the dm-crypt device
+specified is composed of the device specified in ``volume``.
+
+
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
+
+If ``wipe`` option is set, and ``preserve`` is False, curtin will wipe the
+contents of the dm-crypt device.  Curtin skips wipe settings if it creates
+the dm-crypt volume.
+
+
 .. note::
 
   Encrypted disks and partitions are tracked in ``/etc/crypttab`` and will  be
@@ -768,6 +832,19 @@ version of mdadm used during the install will control the value here.  Note
 that metadata version 1.2 is the default in mdadm since release version 3.3
 in 2013.
 
+**preserve**: *true, false*
+
+If the ``preserve`` option is True, curtin will verify the composition of
+the raid device.  This includes array state, raid level, device md-uuid,
+composition of the array devices and spares and that all are present.
+
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
+
+If ``wipe`` option is set to values other than 'superblock', curtin will
+wipe contents of the assembled raid device.  Curtin skips 'superblock` wipes
+as it already clears raid data on the members before assembling the array.
+
+
 **Config Example**::
 
  - id: raid_array
@@ -825,6 +902,18 @@ If the ``name`` key is present, curtin will create a link to the device at
    as long as the device metadata does not change.  If users modify the device
    such that device metadata is changed then the udev rule may no longer apply.
 
+**preserve**: *true, false*
+
+If the ``preserve`` option is True, curtin will verify the composition of
+the bcache device.  This includes checking that backing device and cache
+device are enabled and bound correctly (backing device is cached by expected
+cache device).  If ``cache-mode`` is specified, verify that the mode matches.
+
+**wipe**: *superblock, superblock-recursive, pvremove, zero, random*
+
+If ``wipe`` option is set, curtin will wipe the contents of the bcache device.
+If only ``cache`` device is specified, wipe option is ignored.
+
 
 **Config Example**::
 
diff --git a/examples/tests/bcache-ceph-nvme-simple.yaml b/examples/tests/bcache-ceph-nvme-simple.yaml
index ffb4aec..42624e7 100644
--- a/examples/tests/bcache-ceph-nvme-simple.yaml
+++ b/examples/tests/bcache-ceph-nvme-simple.yaml
@@ -1,7 +1,6 @@
 storage:
     config:
-    -   grub_device: true
-        id: sda
+    -   id: sda
         model: MM1000GBKAL
         name: sda
         ptable: gpt
@@ -35,6 +34,7 @@ storage:
         type: partition
         uuid: 1e27e7af-26dc-4af4-9ef5-aea928204997
         wipe: superblock
+        grub_device: true
     -   device: sda
         id: sda-part2
         name: sda-part2
diff --git a/examples/tests/bcache-ceph-nvme.yaml b/examples/tests/bcache-ceph-nvme.yaml
index 507bc0e..e16e0c0 100644
--- a/examples/tests/bcache-ceph-nvme.yaml
+++ b/examples/tests/bcache-ceph-nvme.yaml
@@ -3,8 +3,7 @@ install:
 showtrace: true
 storage:
   config:
-  - grub_device: true
-    id: sda
+  - id: sda
     model: MG04SCA60EA
     name: sda
     ptable: gpt
@@ -80,6 +79,7 @@ storage:
     wipe: superblock
   - device: sdf
     id: sdf-part1
+    grub_device: true
     name: sdf-part1
     number: 1
     offset: 4194304B
diff --git a/examples/tests/filesystem_battery.yaml b/examples/tests/filesystem_battery.yaml
index 4eae5b6..8166360 100644
--- a/examples/tests/filesystem_battery.yaml
+++ b/examples/tests/filesystem_battery.yaml
@@ -67,6 +67,7 @@ storage:
       label: myext4
       volume: d2p04
       uuid: 5da136b6-0c41-11e8-a664-525400123456
+      extra_options: ['-O', '^ext_attr']
     - id: fs05
       type: format
       fstype: fat16
diff --git a/examples/tests/mirrorboot-uefi.yaml b/examples/tests/mirrorboot-uefi.yaml
index ca55be9..95108f4 100644
--- a/examples/tests/mirrorboot-uefi.yaml
+++ b/examples/tests/mirrorboot-uefi.yaml
@@ -1,4 +1,18 @@
 showtrace: true
+install:
+  unmount: disabled
+
+_install_debconf_utils:
+ - &install_debconf_utils |
+   command -v apt-get && {
+       apt-get -qy install debconf-utils &>/dev/null || { echo "No debconf-utils available"; }
+   }
+   exit 0
+
+late_commands:
+  # used to collect debconf_selections
+  01_install_debconf_utils: ['curtin', 'in-target', '--', 'bash', '-c', *install_debconf_utils]
+
 storage:
   config:
   - grub_device: true
@@ -9,7 +23,8 @@ storage:
     wipe: superblock
     serial: disk-a
     name: main_disk
-  - id: sdb
+  - grub_device: true
+    id: sdb
     name: sdb
     ptable: gpt
     type: disk
@@ -123,4 +138,5 @@ storage:
     options: ''
     path: /var
     type: mount
+
   version: 1
diff --git a/examples/tests/multipath-lvm-part-wipe.yaml b/examples/tests/multipath-lvm-part-wipe.yaml
new file mode 100644
index 0000000..0b22aa3
--- /dev/null
+++ b/examples/tests/multipath-lvm-part-wipe.yaml
@@ -0,0 +1,125 @@
+showtrace: true
+install:
+  unmount: disabled
+bucket:
+  - &setup |
+    export DEBIAN_FRONTEND=noninteractive
+    aptopts="--quiet --assume-yes"
+    aptopts="$aptopts --option=Dpkg::options::=--force-unsafe-io"
+    aptopts="$aptopts --option=Dpkg::Options::=--force-confold"
+    if ! command -v multipath; then
+        eatmydata apt-get install -y $aptopts multipath-tools
+        echo -e "defaults {\n    user_friendly_names yes\n}" > /etc/multipath.conf
+        udevadm trigger --subsystem-match=block --action=add
+        udevadm settle
+        multipath -v3 -R3 -r
+    fi
+    multipath -ll
+    dmsetup remove --force --retry /dev/dm-0
+    dmsetup ls
+    multipath -v3 -R3 -f mpatha
+    udevadm settle
+    parted /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-a --script -- \
+        mklabel gpt              \
+        mkpart primary 1MiB 2MiB \
+        set 1 bios_grub on \
+        mkpart primary 3MiB 999MiB \
+        set 2 boot on \
+        mkpart primary 1000MiB 4099MiB
+    udevadm settle
+    ls -al /dev/disk/by-id
+    vgcreate --force --zero=y --yes root_vg /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-a-part3
+    pvscan --verbose
+    vgscan --verbose
+    udevadm settle
+    for x in $(seq 1 10); do
+        if vgs root_vg; then
+            break;
+        fi
+        sleep 1
+    done
+    lvcreate root_vg --name lv1_root --zero=y --wipesignatures=y \
+        --size 2684354560B
+    udevadm settle
+    mkfs.ext4 /dev/root_vg/lv1_root
+    # stop lvm bits
+    for vg in `pvdisplay -C --separator = -o vg_name --noheadings`; do
+        vgchange -an $vg ||:
+    done
+    command -v systemctl && systemctl mask lvm2-pvscan\@.service
+    rm -rf /etc/lvm/archive /etc/lvm/backup  /run/lvm/*
+    multipath -r
+    udevadm settle
+    dmsetup ls
+    multipath -ll
+    ls -al /dev/mapper/
+    sleep 5
+    ls -al /sys/class/block/dm-0/holders/
+    /curtin/bin/curtin \
+        -v --config /curtin/config/multipath-lvm-part-wipe.yaml \
+        clear-holders --shutdown-plan
+
+
+# Create a LVM now to test curtin's reuse of existing LVMs
+early_commands:
+  00-setup-lvm: [bash, -exuc, *setup]
+
+storage:
+    version: 1
+    config:
+      - id: main_disk
+        type: disk
+        ptable: gpt
+        name: root_disk
+        multipath: mpatha
+        serial: disk-a
+        path: /dev/sda
+        grub_device: true
+        wipe: superblock
+      - id: boot_bios
+        type: partition
+        size: 1MB
+        number: 1
+        device: main_disk
+        flag: bios_grub
+        wipe: superblock
+      - id: boot_partition
+        type: partition
+        size: 1GB
+        number: 2
+        device: main_disk
+        wipe: superblock
+      - id: main_disk_p3
+        type: partition
+        number: 3
+        size: 4GB
+        device: main_disk
+        wipe: superblock
+      - id: root_vg
+        type: lvm_volgroup
+        name: root_vg
+        devices:
+            - main_disk_p3
+      - id: root_vg_lv1
+        type: lvm_partition
+        name: lv1_root
+        size: 2.5G
+        volgroup: root_vg
+      - id: lv1_root_fs
+        type: format
+        fstype: ext4
+        volume: root_vg_lv1
+      - id: lvroot_mount
+        path: /
+        type: mount
+        device: lv1_root_fs
+      - fstype: ext4
+        volume: boot_partition
+        preserve: false
+        type: format
+        id: format-0
+      - device: format-0
+        path: /boot
+        type: mount
+        id: mount-0
+
diff --git a/examples/tests/multipath-lvm.yaml b/examples/tests/multipath-lvm.yaml
new file mode 100644
index 0000000..fd5ea07
--- /dev/null
+++ b/examples/tests/multipath-lvm.yaml
@@ -0,0 +1,121 @@
+showtrace: true
+install:
+  unmount: disabled
+bucket:
+  - &setup |
+    export DEBIAN_FRONTEND=noninteractive
+    aptopts="--quiet --assume-yes"
+    aptopts="$aptopts --option=Dpkg::options::=--force-unsafe-io"
+    aptopts="$aptopts --option=Dpkg::Options::=--force-confold"
+    if ! command -v multipath; then
+        eatmydata apt-get install -y $aptopts multipath-tools
+        echo -e "defaults {\n    user_friendly_names yes\n}" > /etc/multipath.conf
+        udevadm trigger --subsystem-match=block --action=add
+        udevadm settle
+        multipath -v3 -R3 -r
+    fi
+    multipath -ll
+    dmsetup remove --force --retry /dev/dm-0
+    dmsetup ls
+    multipath -v3 -R3 -f mpatha
+    udevadm settle
+    parted /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-a --script -- \
+        mklabel gpt              \
+        mkpart primary 1MiB 2MiB \
+        set 1 bios_grub on \
+        mkpart primary 3MiB 4099MiB \
+        set 2 boot on
+    udevadm settle
+    ls -al /dev/disk/by-id
+    vgcreate --force --zero=y --yes root_vg /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-a-part2
+    pvscan --verbose
+    vgscan --verbose
+    udevadm settle
+    for x in $(seq 1 10); do
+        if vgs root_vg; then
+            break;
+        fi
+        sleep 1
+    done
+    lvcreate root_vg --name lv1_root --zero=y --wipesignatures=y \
+        --size 3758096384B
+    udevadm settle
+    mkfs.ext4 /dev/root_vg/lv1_root
+    # stop lvm bits
+    for vg in `pvdisplay -C --separator = -o vg_name --noheadings`; do
+        vgchange -an $vg ||:
+    done
+    command -v systemctl && systemctl mask lvm2-pvscan\@.service
+    rm -rf /etc/lvm/archive /etc/lvm/backup  /run/lvm/*
+    multipath -r
+    udevadm settle
+    dmsetup ls
+    multipath -ll
+    ls -al /dev/mapper/
+    sleep 5
+    ls -al /sys/class/block/dm-0/holders/
+
+
+# Create a LVM now to test curtin's reuse of existing LVMs
+early_commands:
+  00-setup-lvm: [bash, -exuc, *setup]
+
+storage:
+    version: 1
+    config:
+      - id: main_disk
+        type: disk
+        ptable: gpt
+        name: root_disk
+        multipath: mpatha
+        serial: disk-a
+        path: /dev/sda
+        grub_device: true
+        wipe: superblock
+      - id: boot_bios
+        type: partition
+        size: 1MB
+        number: 1
+        device: main_disk
+        flag: bios_grub
+        wipe: superblock
+      - id: boot_partition
+        type: partition
+        size: 1GB
+        number: 2
+        device: main_disk
+        wipe: superblock
+      - id: main_disk_p3
+        type: partition
+        number: 3
+        size: 4GB
+        device: main_disk
+        wipe: superblock
+      - id: root_vg
+        type: lvm_volgroup
+        name: root_vg
+        devices:
+            - main_disk_p3
+      - id: root_vg_lv1
+        type: lvm_partition
+        name: lv1_root
+        size: 3.5G
+        volgroup: root_vg
+      - id: lv1_root_fs
+        type: format
+        fstype: ext4
+        volume: root_vg_lv1
+      - id: lvroot_mount
+        path: /
+        type: mount
+        device: lv1_root_fs
+      - fstype: ext4
+        volume: boot_partition
+        preserve: false
+        type: format
+        id: format-0
+      - device: format-0
+        path: /boot
+        type: mount
+        id: mount-0
+
diff --git a/examples/tests/multipath.yaml b/examples/tests/multipath.yaml
index 8447d55..11838d1 100644
--- a/examples/tests/multipath.yaml
+++ b/examples/tests/multipath.yaml
@@ -1,3 +1,5 @@
+install:
+  unmount: disabled
 showtrace: true
 storage:
     version: 1
@@ -9,17 +11,21 @@ storage:
         name: mpath_a
         wipe: superblock
         grub_device: true
+        multipath: mpatha
+        path: /dev/sda
       - id: sda1
         type: partition
         number: 1
         size: 3GB
         device: sda
         flag: boot
+        wipe: superblock
       - id: sda2
         type: partition
         number: 2
         size: 1GB
         device: sda
+        wipe: superblock
       - id: sda1_root
         type: format
         fstype: ext4
diff --git a/examples/tests/preserve-bcache.yaml b/examples/tests/preserve-bcache.yaml
new file mode 100644
index 0000000..f614f37
--- /dev/null
+++ b/examples/tests/preserve-bcache.yaml
@@ -0,0 +1,82 @@
+showtrace: true
+
+bucket:
+  - &setup |
+    parted /dev/disk/by-id/virtio-disk-a --script -- \
+        mklabel msdos \
+        mkpart primary 1MiB 1025Mib \
+        set 1 boot on \
+        mkpart primary 1026MiB 9218MiB
+    udevadm settle
+    make-bcache -C /dev/disk/by-id/virtio-disk-b \
+                -B /dev/disk/by-id/virtio-disk-a-part2 --writeback
+    udevadm settle
+    mkfs.ext4 /dev/bcache0
+    mount /dev/bcache0 /mnt
+    touch /mnt/existing
+    umount /mnt
+    echo 1 > /sys/class/block/bcache0/bcache/stop
+    udevadm settle
+
+# Create a bcache now to test curtin's reuse of existing bcache.
+early_commands:
+  00-setup-raid: [sh, -exuc, *setup]
+
+
+storage:
+  config:
+  - id: id_rotary0
+    type: disk
+    name: rotary0
+    serial: disk-a
+    ptable: msdos
+    preserve: true
+    grub_device: true
+  - id: id_ssd0
+    type: disk
+    name: ssd0
+    serial: disk-b
+    preserve: true
+  - id: id_rotary0_part1
+    type: partition
+    name: rotary0-part1
+    device: id_rotary0
+    number: 1
+    offset: 1M
+    size: 1024M
+    preserve: true
+    wipe: superblock
+  - id: id_rotary0_part2
+    type: partition
+    name: rotary0-part2
+    device: id_rotary0
+    number: 2
+    size: 8G
+    preserve: true
+  - id: id_bcache0
+    type: bcache
+    name: bcache0
+    backing_device: id_rotary0_part2
+    cache_device: id_ssd0
+    cache_mode: writeback
+    preserve: true
+  - id: bootfs
+    type: format
+    label: boot-fs
+    volume: id_rotary0_part1
+    fstype: ext4
+  - id: rootfs
+    type: format
+    label: root-fs
+    volume: id_bcache0
+    fstype: ext4
+    preserve: true
+  - id: rootfs_mount
+    type: mount
+    path: /
+    device: rootfs
+  - id: bootfs_mount
+    type: mount
+    path: /boot
+    device: bootfs
+  version: 1
diff --git a/examples/tests/preserve-lvm.yaml b/examples/tests/preserve-lvm.yaml
new file mode 100644
index 0000000..a939759
--- /dev/null
+++ b/examples/tests/preserve-lvm.yaml
@@ -0,0 +1,77 @@
+showtrace: true
+bucket:
+  - &setup |
+    parted /dev/disk/by-id/virtio-disk-a --script -- \
+        mklabel gpt              \
+        mkpart primary 1MiB 2MiB \
+        set 1 bios_grub on \
+        mkpart primary 3MiB 4099MiB \
+        set 2 boot on
+    udevadm settle
+    ls -al /dev/disk/by-id
+    vgcreate --force --zero=y --yes root_vg /dev/disk/by-id/virtio-disk-a-part2
+    pvscan --verbose
+    vgscan --verbose
+    lvcreate root_vg --name lv1_root --zero=y --wipesignatures=y \
+        --size 3758096384B
+    udevadm settle
+    mkfs.ext4 /dev/root_vg/lv1_root
+    mount /dev/root_vg/lv1_root /mnt
+    touch /mnt/existing
+    umount /mnt
+    # disable vg/lv
+    for vg in `pvdisplay -C --separator = -o vg_name --noheadings`; do
+        vgchange -an $vg ||:
+    done
+    command -v systemctl && systemctl mask lvm2-pvscan\@.service
+    rm -rf /etc/lvm/archive /etc/lvm/backup
+
+# Create a LVM now to test curtin's reuse of existing LVMs
+early_commands:
+  00-setup-lvm: [sh, -exuc, *setup]
+
+storage:
+    version: 1
+    config:
+      - id: main_disk
+        type: disk
+        ptable: gpt
+        name: root_disk
+        serial: disk-a
+        grub_device: true
+        preserve: true
+      - id: bios_boot
+        type: partition
+        size: 1MB
+        number: 1
+        device: main_disk
+        flag: bios_grub
+        preserve: true
+      - id: main_disk_p2
+        type: partition
+        number: 2
+        size: 4GB
+        device: main_disk
+        flag: boot
+        preserve: true
+      - id: root_vg
+        type: lvm_volgroup
+        name: root_vg
+        devices:
+            - main_disk_p2
+        preserve: true
+      - id: root_vg_lv1
+        type: lvm_partition
+        name: lv1_root
+        size: 3.5G
+        volgroup: root_vg
+        preserve: true
+      - id: lv1_root_fs
+        type: format
+        fstype: ext4
+        volume: root_vg_lv1
+        preserve: true
+      - id: lvroot_mount
+        path: /
+        type: mount
+        device: lv1_root_fs
diff --git a/examples/tests/preserve-partition-wipe-vg-simple.yaml b/examples/tests/preserve-partition-wipe-vg-simple.yaml
new file mode 100644
index 0000000..e1f0b9e
--- /dev/null
+++ b/examples/tests/preserve-partition-wipe-vg-simple.yaml
@@ -0,0 +1,62 @@
+showtrace: true
+
+bucket:
+  - &setup |
+    parted /dev/disk/by-id/virtio-disk-a --script -- \
+        mklabel gpt                   \
+        mkpart primary ext4 2MiB 4MiB \
+        set 1 bios_grub on            \
+        mkpart primary ext4 4GiB 7GiB
+    udevadm settle
+    ls -al /dev/disk/by-id
+    vgcreate --force --zero=y --yes root_vg /dev/disk/by-id/virtio-disk-a-part2
+    pvscan --verbose
+    vgscan --verbose
+    vgs
+    lvcreate root_vg --name lv1_root --zero=y --wipesignatures=y \
+        --size 2G
+    udevadm settle
+    lvs
+    ls -al /dev/disk/by-id
+
+# Partition the disk now to test curtin's reuse of partitions.
+early_commands:
+  00-setup-disk: [sh, -exuc, *setup]
+
+storage:
+  config:
+  - ptable: gpt
+    serial: disk-a
+    preserve: true
+    name: disk-a
+    grub_device: true
+    type: disk
+    id: disk-sda
+  - device: disk-sda
+    size: 2097152
+    flag: bios_grub
+    preserve: true
+    number: 1
+    type: partition
+    id: disk-sda-part-1
+  - device: disk-sda
+    size: 3G
+    flag: linux
+    preserve: true
+    number: 2
+    wipe: zero
+    type: partition
+    id: disk-sda-part-2
+  - fstype: ext4
+    volume: disk-sda-part-2
+    preserve: false
+    type: format
+    id: format-0
+  - device: format-0
+    path: /
+    type: mount
+    id: mount-0
+  version: 1
+
+verbosity: 3
+
diff --git a/examples/tests/preserve-partition-wipe-vg.yaml b/examples/tests/preserve-partition-wipe-vg.yaml
new file mode 100644
index 0000000..97686e1
--- /dev/null
+++ b/examples/tests/preserve-partition-wipe-vg.yaml
@@ -0,0 +1,116 @@
+showtrace: true
+
+bucket:
+  - &setup |
+    parted /dev/disk/by-id/virtio-disk-a --script -- \
+        mklabel gpt                   \
+        mkpart primary ext4 2MiB 4MiB \
+        set 1 bios_grub on            \
+        mkpart primary ext4 1GiB 4GiB \
+        mkpart primary ext4 4GiB 7GiB
+    parted /dev/disk/by-id/virtio-disk-b --script -- \
+        mklabel gpt                   \
+        mkpart primary ext4 1GiB 4GiB  \
+        mkpart primary ext4 4GiB 7GiB
+    udevadm settle
+    ls -al /dev/disk/by-id
+    vgcreate --force --zero=y --yes vg8 /dev/disk/by-id/virtio-disk-b-part1
+    pvscan --verbose
+    vgscan --verbose
+    udevadm settle
+    ls -al /dev/disk/by-id
+    mkfs.ext4 /dev/disk/by-id/virtio-disk-a-part3
+    mkfs.ext4 /dev/disk/by-id/virtio-disk-b-part2
+    mount /dev/disk/by-id/virtio-disk-b-part2 /mnt
+    touch /mnt/existing-virtio-disk-b-part2
+    umount /mnt
+
+# Partition the disk now to test curtin's reuse of partitions.
+early_commands:
+  00-setup-disk: [sh, -exuc, *setup]
+
+storage:
+  config:
+  - ptable: gpt
+    serial: disk-a
+    preserve: true
+    name: disk-a
+    grub_device: true
+    type: disk
+    id: disk-sda
+    wipe: superblock
+  - serial: disk-b
+    name: disk-b
+    grub_device: false
+    type: disk
+    id: disk-sdb
+    preserve: true
+  - device: disk-sda
+    size: 2097152
+    flag: bios_grub
+    preserve: true
+    wipe: zero
+    type: partition
+    id: disk-sda-part-1
+  - device: disk-sda
+    size: 3G
+    flag: linux
+    preserve: true
+    wipe: zero
+    type: partition
+    id: disk-sda-part-2
+  - device: disk-sdb
+    flag: linux
+    size: 3G
+    preserve: true
+    wipe: zero
+    type: partition
+    id: disk-sdb-part-1
+  - device: disk-sdb
+    flag: linux
+    size: 3G
+    preserve: true
+    type: partition
+    id: disk-sdb-part-2
+  - fstype: ext4
+    volume: disk-sda-part-2
+    preserve: false
+    type: format
+    id: format-0
+  - fstype: ext4
+    volume: disk-sdb-part-2
+    preserve: true
+    type: format
+    id: format-disk-sdb-part-2
+  - device: format-0
+    path: /
+    type: mount
+    id: mount-0
+  - name: vg1
+    devices:
+    - disk-sdb-part-1
+    preserve: false
+    type: lvm_volgroup
+    id: lvm_volgroup-0
+  - name: lv-0
+    volgroup: lvm_volgroup-0
+    size: 2G
+    preserve: false
+    type: lvm_partition
+    id: lvm_partition-0
+  - fstype: ext4
+    volume: lvm_partition-0
+    preserve: false
+    type: format
+    id: format-1
+  - device: format-1
+    path: /home
+    type: mount
+    id: mount-1
+  - device: format-disk-sdb-part-2
+    path: /opt
+    type: mount
+    id: mount-2
+
+  version: 1
+verbosity: 3
diff --git a/examples/tests/preserve-raid.yaml b/examples/tests/preserve-raid.yaml
index 3a6cc18..9e0489f 100644
--- a/examples/tests/preserve-raid.yaml
+++ b/examples/tests/preserve-raid.yaml
@@ -4,10 +4,12 @@ bucket:
   - &setup |
     parted /dev/disk/by-id/virtio-disk-b --script -- \
         mklabel gpt              \
-        mkpart primary 1GiB 9GiB
+        mkpart primary 1GiB 9GiB \
+        set 1 boot on
     parted /dev/disk/by-id/virtio-disk-c --script -- \
         mklabel gpt              \
-        mkpart primary 1GiB 9GiB
+        mkpart primary 1GiB 9GiB \
+        set 1 boot on
     udevadm settle
     mdadm --create --metadata 1.2 --level 1 -n 2 /dev/md1 --assume-clean \
         /dev/disk/by-id/virtio-disk-b-part1 /dev/disk/by-id/virtio-disk-c-part1
diff --git a/examples/tests/reuse-lvm-member-partition.yaml b/examples/tests/reuse-lvm-member-partition.yaml
new file mode 100644
index 0000000..fd8f602
--- /dev/null
+++ b/examples/tests/reuse-lvm-member-partition.yaml
@@ -0,0 +1,94 @@
+showtrace: true
+
+# The point of this test is to test installing to a partition that used to
+# be a LVM member where the other disks that used to be part of the
+# RAID are not present (the scenario that the disk was just grabbed
+# out of a pile of previously used disks and shoved into a server).
+
+# So what it does is to create a lvm out of two partition from two
+# disks, stop the lvm, wipe one of the disks and then install to the
+# other, reusing the partition that was part of the lvm.
+
+bucket:
+  - &setup |
+    SDA=/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-a
+    SDB=/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_disk-b
+    VG="vg8"
+    LV="lv1_root"
+    parted ${SDA} --script -- \
+        mklabel gpt              \
+        mkpart primary 1GiB 2GiB \
+        set 1 esp on             \
+        mkpart primary 2GiB 9GiB
+    parted ${SDB} --script -- \
+        mklabel gpt              \
+        mkpart primary 2GiB 9GiB
+    udevadm settle
+    vgcreate --verbose --force --zero=y --yes ${VG} ${SDA}-part2 ${SDB}-part1
+    pvscan --verbose
+    vgscan --verbose
+    # create a striped lv
+    lvcreate ${VG} --extents 100%FREE --stripes 2 --stripesize 256 \
+        --name ${LV}
+    udevadm settle
+    # populate the lv with data to ensure we allocate extents on both pv devs
+    mkfs.ext4 /dev/${VG}/${LV}
+    mount /dev/${VG}/${LV} /mnt
+    rsync -aqpP --one-file-system /var /mnt
+    umount /mnt
+    # simulate the first boot environment by turning off the vg, and wiping
+    # one physical volume
+    for vg in `pvdisplay -C --separator = -o vg_name --noheadings`; do
+       vgchange --verbose -an $vg ||:
+    done
+    rm -rf /etc/lvm/archive /etc/lvm/backup /run/lvm
+    wipefs -a ${SDB}-part1
+    mkfs.ext4 ${SDB}-part1
+    udevadm settle
+    udevadm trigger --subsystem-match=block
+    udevadm settle
+    exit 0
+
+early_commands:
+  00-setup-raid: [sh, -exuc, *setup]
+
+storage:
+  config:
+  - type: disk
+    id: id_disk0
+    serial: disk-a
+    ptable: gpt
+    preserve: true
+  - type: disk
+    id: id_disk1
+    serial: disk-b
+  - type: partition
+    id: id_disk0_part1
+    preserve: true
+    device: id_disk0
+    flag: boot
+    number: 1
+    size: 1G
+  - type: partition
+    id: id_disk0_part2
+    preserve: true
+    device: id_disk0
+    number: 2
+    size: 7G
+  - type: format
+    id: id_efi_format
+    volume: id_disk0_part1
+    fstype: fat32
+  - type: format
+    id: id_root_format
+    volume: id_disk0_part2
+    fstype: ext4
+  - type: mount
+    device: id_root_format
+    id: id_root_mount
+    path: /
+  - type: mount
+    id: id_efi_mount
+    device: id_efi_format
+    path: /boot/efi
+  version: 1
diff --git a/examples/tests/reuse-msdos-partitions.yaml b/examples/tests/reuse-msdos-partitions.yaml
new file mode 100644
index 0000000..d444517
--- /dev/null
+++ b/examples/tests/reuse-msdos-partitions.yaml
@@ -0,0 +1,77 @@
+showtrace: true
+install:
+   unmount: disabled
+
+# The point of this test is to test installing to a disk that contains
+# a typical MSDOS partition table with extended and logical parititions,
+# including a 'bootable' flag set and then reuse the existing partition
+# table triggering the partition_verify path to ensure we validate MSDOS
+# partition layouts.
+
+bucket:
+  - &setup |
+    parted /dev/disk/by-id/virtio-disk-a --script -- \
+        mklabel msdos              \
+        mkpart primary 1MiB 3073MiB \
+        mkpart extended 3074MiB 8193MiB \
+        mkpart logical 3075MiB 5122MiB \
+        mkpart logical 5123MiB 8192MiB \
+        set 1 boot on
+    udevadm settle
+
+early_commands:
+  00-setup-msdos-ptable: [sh, -exuc, *setup]
+
+
+showtrace: true
+storage:
+    version: 1
+    config:
+      - id: sda
+        type: disk
+        ptable: msdos
+        model: QEMU HARDDISK
+        serial: disk-a
+        name: main_disk
+        preserve: true
+        grub_device: true
+      - id: sda1
+        type: partition
+        number: 1
+        size: 3072M
+        device: sda
+        flag: boot
+        preserve: true
+        wipe: superblock
+      - id: sda2
+        type: partition
+        number: 2
+        size: 5119M
+        flag: extended
+        device: sda
+        preserve: true
+      - id: sda5
+        type: partition
+        number: 5
+        size: 2047M
+        flag: logical
+        device: sda
+        preserve: true
+        wipe: superblock
+      - id: sda6
+        type: partition
+        number: 6
+        size: 3069M
+        flag: logical
+        device: sda
+        preserve: true
+        wipe: superblock
+      - id: sda1_root
+        type: format
+        fstype: ext4
+        volume: sda1
+      - id: sda1_mount
+        type: mount
+        path: /
+        device: sda1_root
+
diff --git a/examples/tests/reuse-raid-member-partition.yaml b/examples/tests/reuse-raid-member-wipe-partition.yaml
similarity index 98%
rename from examples/tests/reuse-raid-member-partition.yaml
rename to examples/tests/reuse-raid-member-wipe-partition.yaml
index 3fe2d83..d20b79c 100644
--- a/examples/tests/reuse-raid-member-partition.yaml
+++ b/examples/tests/reuse-raid-member-wipe-partition.yaml
@@ -14,6 +14,7 @@ bucket:
     parted /dev/disk/by-id/virtio-disk-a --script -- \
         mklabel gpt              \
         mkpart primary 1GiB 2GiB \
+        set 1 boot on \
         mkpart primary 2GiB 9GiB
     parted /dev/disk/by-id/virtio-disk-b --script -- \
         mklabel gpt              \
@@ -54,6 +55,7 @@ storage:
     device: id_disk0
     number: 2
     size: 7G
+    wipe: superblock
   - type: format
     id: id_efi_format
     volume: id_disk0_part1
diff --git a/examples/tests/reuse-raid-member-wipe.yaml b/examples/tests/reuse-raid-member-wipe.yaml
index 84a2686..9abd5d4 100644
--- a/examples/tests/reuse-raid-member-wipe.yaml
+++ b/examples/tests/reuse-raid-member-wipe.yaml
@@ -14,7 +14,8 @@ bucket:
   - &setup |
     parted /dev/disk/by-id/virtio-disk-a --script -- \
         mklabel gpt              \
-        mkpart primary 1GiB 9GiB
+        mkpart primary 1GiB 9GiB \
+        set 1 boot on
     parted /dev/disk/by-id/virtio-disk-b --script -- \
         mklabel gpt              \
         mkpart primary 1GiB 9GiB
diff --git a/examples/tests/uefi_reuse_esp.yaml b/examples/tests/uefi_reuse_esp.yaml
index c53064c..7ad7fdf 100644
--- a/examples/tests/uefi_reuse_esp.yaml
+++ b/examples/tests/uefi_reuse_esp.yaml
@@ -1,12 +1,14 @@
 showtrace: true
+install:
+  unmount: disabled
 
 bucket:
   - &setup |
     parted /dev/disk/by-id/virtio-disk-a --script -- \
         mklabel gpt \
-        mkpart primary fat32 1MiB 512MiB \
+        mkpart primary fat32 1MiB 513MiB \
         set 1 esp on \
-        mkpart primary ext4 512MiB 3512Mib
+        mkpart primary ext4 513MiB 3585MiB
 
     udevadm settle
     mkfs.vfat -I -n EFI -F 32 /dev/disk/by-id/virtio-disk-a-part1
diff --git a/examples/tests/vmtest_defaults.yaml b/examples/tests/vmtest_defaults.yaml
index 797fe6c..b440472 100644
--- a/examples/tests/vmtest_defaults.yaml
+++ b/examples/tests/vmtest_defaults.yaml
@@ -19,6 +19,17 @@ _persist_journal:
     }
     exit 0
 
+_install_probert:
+ - &install_probert |
+   command -v apt && {
+       # bionic probert doesn't work with block-discover
+       if [ "`lsb_release -sc`" = "bionic" ]; then
+           exit 0;
+       fi
+       apt-get -qy install probert &>/dev/null || { echo "No probert available"; }
+   }
+   exit 0
+
 # this runs curtin block-discover and stores the result
 # in the target system root dir
 _block_discover:
@@ -39,8 +50,11 @@ _block_discover:
        # xenial and lower don't have jsonschema
        if has_jschema; then
            outfile="${TARGET_MOUNT_POINT}/root/curtin-block-discover.json"
+           probefile="${TARGET_MOUNT_POINT}/root/probe-data.json"
+           echo "dumping probert probe data"
+           curtin -vv block-discover -p > $probefile 2>/dev/null
            echo "discovering block devices"
-           curtin -vv block-discover > $outfile
+           curtin -vv block-discover > $outfile 2>/dev/null
            which curtin > $TARGET_MOUNT_POINT/root/which-curtin
            cdir=$(realpath -m "$(dirname `which curtin`)/../")
            echo $cdir >> $TARGET_MOUNT_POINT/root/which-curtin
@@ -51,6 +65,10 @@ _block_discover:
    exit 0
 
 
+early_commands:
+  # use bash to allow capturing all apt errors when probert isn't available
+  01_install_probert: ['bash', '-c', *install_probert]
+
 late_commands:
   01_vmtest_pollinate: ['curtin', 'in-target', '--', 'sh', '-c', *pvmtest]
   02_persist_journal: ['curtin', 'in-target', '--', 'sh', '-c', *persist_journal]
diff --git a/helpers/common b/helpers/common
index 1def1bc..5638d39 100644
--- a/helpers/common
+++ b/helpers/common
@@ -901,9 +901,13 @@ install_grub() {
             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
                     ;;
@@ -953,7 +957,7 @@ install_grub() {
             echo "Dumping /boot/efi contents"
             find /boot/efi
             echo "Checking for existing EFI grub entry on ESP"
-            if [ -f /boot/efi/EFI/$bootid/grubx64.efi ]; then
+            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.
@@ -970,15 +974,17 @@ install_grub() {
                         --disk $efi_disk --part $efi_part_num --loader $loader
                     rc=$?
                     [ "$rc" != "0" ] && { exit $rc; }
-                    # check and remove duplicates
-                    efibootmgr --verbose --remove-dups
                 else
                     echo "skip EFI entry creation due to \"$no_nvram\" flag"
                 fi
             else
                 echo "No previous EFI grub entry found on ESP, use $grubcmd"
-                $grubcmd $target $efi_dir \
-                    --bootloader-id=$bootid --recheck $no_nvram
+                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 ||
diff --git a/tests/data/probert_storage_bogus_wwn.json b/tests/data/probert_storage_bogus_wwn.json
new file mode 100644
index 0000000..b3211fd
--- /dev/null
+++ b/tests/data/probert_storage_bogus_wwn.json
@@ -0,0 +1,1258 @@
+{
+    "filesystem": {
+        "/dev/sdc": {
+            "BOOT_SYSTEM_ID": "EL\\x20TORITO\\x20SPECIFICATION",
+            "LABEL": "Ubuntu-Server_20.04_LTS_amd64",
+            "LABEL_ENC": "Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "TYPE": "iso9660",
+            "USAGE": "filesystem",
+            "UUID": "2020-04-23-08-02-07-00",
+            "UUID_ENC": "2020-04-23-08-02-07-00",
+            "VERSION": "Joliet Extension"
+        },
+        "/dev/sdc1": {
+            "BOOT_SYSTEM_ID": "EL\\x20TORITO\\x20SPECIFICATION",
+            "LABEL": "Ubuntu-Server_20.04_LTS_amd64",
+            "LABEL_ENC": "Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "TYPE": "iso9660",
+            "USAGE": "filesystem",
+            "UUID": "2020-04-23-08-02-07-00",
+            "UUID_ENC": "2020-04-23-08-02-07-00",
+            "VERSION": "Joliet Extension"
+        },
+        "/dev/sdc2": {
+            "TYPE": "vfat",
+            "USAGE": "filesystem",
+            "UUID": "1AC3-20ED",
+            "UUID_ENC": "1AC3-20ED",
+            "VERSION": "FAT12"
+        },
+        "/dev/sdc3": {
+            "LABEL": "writable",
+            "LABEL_ENC": "writable",
+            "TYPE": "ext4",
+            "USAGE": "filesystem",
+            "UUID": "8bcd1ba9-a05d-4f66-b64a-2d9042753ee7",
+            "UUID_ENC": "8bcd1ba9-a05d-4f66-b64a-2d9042753ee7",
+            "VERSION": "1.0"
+        }
+    },
+    "dmcrypt": {},
+    "mount": [
+        {
+            "target": "/",
+            "source": "/cow",
+            "fstype": "overlay",
+            "options": "rw,relatime,lowerdir=/installer.squashfs:/filesystem.squashfs,upperdir=/cow/upper,workdir=/cow/work",
+            "children": [
+                {
+                    "target": "/sys",
+                    "source": "sysfs",
+                    "fstype": "sysfs",
+                    "options": "rw,nosuid,nodev,noexec,relatime",
+                    "children": [
+                        {
+                            "target": "/sys/kernel/security",
+                            "source": "securityfs",
+                            "fstype": "securityfs",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/fs/cgroup",
+                            "source": "tmpfs",
+                            "fstype": "tmpfs",
+                            "options": "ro,nosuid,nodev,noexec,mode=755",
+                            "children": [
+                                {
+                                    "target": "/sys/fs/cgroup/unified",
+                                    "source": "cgroup2",
+                                    "fstype": "cgroup2",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,nsdelegate"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/systemd",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,xattr,name=systemd"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/rdma",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,rdma"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/cpu,cpuacct",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,cpu,cpuacct"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/pids",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,pids"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/memory",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,memory"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/devices",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,devices"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/freezer",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,freezer"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/net_cls,net_prio",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,net_cls,net_prio"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/perf_event",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,perf_event"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/blkio",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,blkio"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/hugetlb",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,hugetlb"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/cpuset",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,cpuset"
+                                }
+                            ]
+                        },
+                        {
+                            "target": "/sys/fs/pstore",
+                            "source": "pstore",
+                            "fstype": "pstore",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/fs/bpf",
+                            "source": "none",
+                            "fstype": "bpf",
+                            "options": "rw,nosuid,nodev,noexec,relatime,mode=700"
+                        },
+                        {
+                            "target": "/sys/kernel/debug",
+                            "source": "debugfs",
+                            "fstype": "debugfs",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/kernel/tracing",
+                            "source": "tracefs",
+                            "fstype": "tracefs",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/kernel/config",
+                            "source": "configfs",
+                            "fstype": "configfs",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/fs/fuse/connections",
+                            "source": "fusectl",
+                            "fstype": "fusectl",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        }
+                    ]
+                },
+                {
+                    "target": "/proc",
+                    "source": "proc",
+                    "fstype": "proc",
+                    "options": "rw,nosuid,nodev,noexec,relatime",
+                    "children": [
+                        {
+                            "target": "/proc/sys/fs/binfmt_misc",
+                            "source": "systemd-1",
+                            "fstype": "autofs",
+                            "options": "rw,relatime,fd=28,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=17022"
+                        }
+                    ]
+                },
+                {
+                    "target": "/dev",
+                    "source": "udev",
+                    "fstype": "devtmpfs",
+                    "options": "rw,nosuid,noexec,relatime,size=16353256k,nr_inodes=4088314,mode=755",
+                    "children": [
+                        {
+                            "target": "/dev/pts",
+                            "source": "devpts",
+                            "fstype": "devpts",
+                            "options": "rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000"
+                        },
+                        {
+                            "target": "/dev/shm",
+                            "source": "tmpfs",
+                            "fstype": "tmpfs",
+                            "options": "rw,nosuid,nodev"
+                        },
+                        {
+                            "target": "/dev/hugepages",
+                            "source": "hugetlbfs",
+                            "fstype": "hugetlbfs",
+                            "options": "rw,relatime,pagesize=2M"
+                        },
+                        {
+                            "target": "/dev/mqueue",
+                            "source": "mqueue",
+                            "fstype": "mqueue",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        }
+                    ]
+                },
+                {
+                    "target": "/run",
+                    "source": "tmpfs",
+                    "fstype": "tmpfs",
+                    "options": "rw,nosuid,nodev,noexec,relatime,size=3279500k,mode=755",
+                    "children": [
+                        {
+                            "target": "/run/lock",
+                            "source": "tmpfs",
+                            "fstype": "tmpfs",
+                            "options": "rw,nosuid,nodev,noexec,relatime,size=5120k"
+                        }
+                    ]
+                },
+                {
+                    "target": "/cdrom",
+                    "source": "/dev/sdc1",
+                    "fstype": "iso9660",
+                    "options": "ro,noatime,nojoliet,check=s,map=n,blocksize=2048"
+                },
+                {
+                    "target": "/rofs",
+                    "source": "/dev/loop0",
+                    "fstype": "squashfs",
+                    "options": "ro,noatime"
+                },
+                {
+                    "target": "/var/log",
+                    "source": "/dev/disk/by-label/writable[/install-logs-2020-05-04.0/log]",
+                    "fstype": "ext4",
+                    "options": "rw,relatime"
+                },
+                {
+                    "target": "/var/crash",
+                    "source": "/dev/disk/by-label/writable[/install-logs-2020-05-04.0/crash]",
+                    "fstype": "ext4",
+                    "options": "rw,relatime"
+                },
+                {
+                    "target": "/usr/lib/modules",
+                    "source": "/dev/loop2",
+                    "fstype": "squashfs",
+                    "options": "ro,relatime"
+                },
+                {
+                    "target": "/media/filesystem",
+                    "source": "/dev/loop0",
+                    "fstype": "squashfs",
+                    "options": "ro,relatime"
+                },
+                {
+                    "target": "/tmp",
+                    "source": "tmpfs",
+                    "fstype": "tmpfs",
+                    "options": "rw,nosuid,nodev,relatime"
+                },
+                {
+                    "target": "/snap/snapd/7264",
+                    "source": "/dev/loop3",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                },
+                {
+                    "target": "/snap/core18/1705",
+                    "source": "/dev/loop4",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                },
+                {
+                    "target": "/snap/subiquity/1772",
+                    "source": "/dev/loop5",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                }
+            ]
+        }
+    ],
+    "multipath": {
+        "paths": [
+            {
+                "device": "nvme0n1",
+                "serial": "S4EVNJ0N203359W     ",
+                "multipath": "[orphan]",
+                "host_wwnn": "[undef]",
+                "target_wwnn": "[undef]",
+                "host_wwpn": "[undef]",
+                "target_wwpn": "[undef]",
+                "host_adapter": "[undef]"
+            },
+            {
+                "device": "sda",
+                "serial": "13207907000097410026",
+                "multipath": "[orphan]",
+                "host_wwnn": "[undef]",
+                "target_wwnn": "ata-1.00",
+                "host_wwpn": "[undef]",
+                "target_wwpn": "[undef]",
+                "host_adapter": "[undef]"
+            },
+            {
+                "device": "sdb",
+                "serial": "1320790700009741003D",
+                "multipath": "[orphan]",
+                "host_wwnn": "[undef]",
+                "target_wwnn": "ata-2.00",
+                "host_wwpn": "[undef]",
+                "target_wwpn": "[undef]",
+                "host_adapter": "[undef]"
+            }
+        ]
+    },
+    "lvm": {},
+    "dasd": {},
+    "raid": {
+        "/dev/md127": {
+            "DEVLINKS": "/dev/md/ubuntu-server:0 /dev/disk/by-id/md-name-ubuntu-server:0 /dev/disk/by-id/md-uuid-079fa971:c86dcd37:05c54ec1:b343dc99",
+            "DEVNAME": "/dev/md127",
+            "DEVPATH": "/devices/virtual/block/md127",
+            "DEVTYPE": "disk",
+            "MAJOR": "9",
+            "MD_DEVICES": "2",
+            "MD_DEVICE_ev_sda2_DEV": "/dev/sda2",
+            "MD_DEVICE_ev_sda2_ROLE": "0",
+            "MD_DEVICE_ev_sdb2_DEV": "/dev/sdb2",
+            "MD_DEVICE_ev_sdb2_ROLE": "1",
+            "MD_DEVNAME": "ubuntu-server:0",
+            "MD_LEVEL": "raid1",
+            "MD_METADATA": "1.2",
+            "MD_NAME": "ubuntu-server:0",
+            "MD_UUID": "079fa971:c86dcd37:05c54ec1:b343dc99",
+            "MINOR": "127",
+            "SUBSYSTEM": "block",
+            "SYSTEMD_WANTS": "mdmonitor.service",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1083467",
+            "raidlevel": "raid1",
+            "devices": [
+                "/dev/sda2",
+                "/dev/sdb2"
+            ],
+            "spare_devices": []
+        }
+    },
+    "zfs": {
+        "zpools": {}
+    },
+    "blockdev": {
+        "/dev/sdc": {
+            "DEVLINKS": "/dev/disk/by-uuid/2020-04-23-08-02-07-00 /dev/disk/by-path/pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0 /dev/disk/by-id/usb-Generic_Flash_Disk_72FFA457-0:0 /dev/disk/by-label/Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "DEVNAME": "/dev/sdc",
+            "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb3/3-11/3-11:1.0/host6/target6:0:0/6:0:0:0/block/sdc",
+            "DEVTYPE": "disk",
+            "ID_BUS": "usb",
+            "ID_FS_BOOT_SYSTEM_ID": "EL\\x20TORITO\\x20SPECIFICATION",
+            "ID_FS_LABEL": "Ubuntu-Server_20.04_LTS_amd64",
+            "ID_FS_LABEL_ENC": "Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "ID_FS_TYPE": "iso9660",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "2020-04-23-08-02-07-00",
+            "ID_FS_UUID_ENC": "2020-04-23-08-02-07-00",
+            "ID_FS_VERSION": "Joliet Extension",
+            "ID_INSTANCE": "0:0",
+            "ID_MODEL": "Flash_Disk",
+            "ID_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "ID_MODEL_ID": "6387",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "36b64baf",
+            "ID_PATH": "pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0",
+            "ID_PATH_TAG": "pci-0000_00_14_0-usb-0_11_1_0-scsi-0_0_0_0",
+            "ID_REVISION": "8.07",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Generic_Flash_Disk_72FFA457-0:0",
+            "ID_SERIAL_SHORT": "72FFA457",
+            "ID_TYPE": "disk",
+            "ID_USB_DRIVER": "usb-storage",
+            "ID_USB_INTERFACES": ":080650:",
+            "ID_USB_INTERFACE_NUM": "00",
+            "ID_VENDOR": "Generic",
+            "ID_VENDOR_ENC": "Generic\\x20",
+            "ID_VENDOR_ID": "058f",
+            "MAJOR": "8",
+            "MINOR": "32",
+            "MPATH_SBIN_PATH": "/sbin",
+            "SCSI_MODEL": "Flash_Disk",
+            "SCSI_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "SCSI_REVISION": "8.07",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "Generic",
+            "SCSI_VENDOR_ENC": "Generic\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "2376183",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "capability": "51",
+                "dev": "8:32",
+                "device": null,
+                "discard_alignment": "0",
+                "events": "media_change",
+                "events_async": "",
+                "events_poll_msecs": "-1",
+                "ext_range": "256",
+                "hidden": "0",
+                "inflight": "       0        0",
+                "range": "16",
+                "removable": "1",
+                "ro": "0",
+                "size": "33409990656",
+                "stat": "   15770       95  1885214    86569     1333     1088   284936   138030        0    40216   204400        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=32\nDEVNAME=sdc\nDEVTYPE=disk"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0x36b64baf",
+                "device": "/dev/sdc",
+                "unit": "sectors",
+                "partitions": [
+                    {
+                        "node": "/dev/sdc1",
+                        "start": 0,
+                        "size": 1859584,
+                        "type": "0",
+                        "bootable": true
+                    },
+                    {
+                        "node": "/dev/sdc2",
+                        "start": 21052,
+                        "size": 7936,
+                        "type": "ef"
+                    },
+                    {
+                        "node": "/dev/sdc3",
+                        "start": 1859584,
+                        "size": 63394304,
+                        "type": "83"
+                    }
+                ]
+            }
+        },
+        "/dev/sdc1": {
+            "DEVLINKS": "/dev/disk/by-id/usb-Generic_Flash_Disk_72FFA457-0:0-part1 /dev/disk/by-partuuid/36b64baf-01 /dev/disk/by-uuid/2020-04-23-08-02-07-00 /dev/disk/by-path/pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0-part1 /dev/disk/by-label/Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "DEVNAME": "/dev/sdc1",
+            "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb3/3-11/3-11:1.0/host6/target6:0:0/6:0:0:0/block/sdc/sdc1",
+            "DEVTYPE": "partition",
+            "ID_BUS": "usb",
+            "ID_FS_BOOT_SYSTEM_ID": "EL\\x20TORITO\\x20SPECIFICATION",
+            "ID_FS_LABEL": "Ubuntu-Server_20.04_LTS_amd64",
+            "ID_FS_LABEL_ENC": "Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+            "ID_FS_TYPE": "iso9660",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "2020-04-23-08-02-07-00",
+            "ID_FS_UUID_ENC": "2020-04-23-08-02-07-00",
+            "ID_FS_VERSION": "Joliet Extension",
+            "ID_INSTANCE": "0:0",
+            "ID_MODEL": "Flash_Disk",
+            "ID_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "ID_MODEL_ID": "6387",
+            "ID_PART_ENTRY_DISK": "8:32",
+            "ID_PART_ENTRY_FLAGS": "0x80",
+            "ID_PART_ENTRY_NUMBER": "1",
+            "ID_PART_ENTRY_OFFSET": "0",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "1859584",
+            "ID_PART_ENTRY_TYPE": "0x0",
+            "ID_PART_ENTRY_UUID": "36b64baf-01",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "36b64baf",
+            "ID_PATH": "pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0",
+            "ID_PATH_TAG": "pci-0000_00_14_0-usb-0_11_1_0-scsi-0_0_0_0",
+            "ID_REVISION": "8.07",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Generic_Flash_Disk_72FFA457-0:0",
+            "ID_SERIAL_SHORT": "72FFA457",
+            "ID_TYPE": "disk",
+            "ID_USB_DRIVER": "usb-storage",
+            "ID_USB_INTERFACES": ":080650:",
+            "ID_USB_INTERFACE_NUM": "00",
+            "ID_VENDOR": "Generic",
+            "ID_VENDOR_ENC": "Generic\\x20",
+            "ID_VENDOR_ID": "058f",
+            "MAJOR": "8",
+            "MINOR": "33",
+            "PARTN": "1",
+            "SCSI_MODEL": "Flash_Disk",
+            "SCSI_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "SCSI_REVISION": "8.07",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "Generic",
+            "SCSI_VENDOR_ENC": "Generic\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "13873530",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:33",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "1",
+                "ro": "0",
+                "size": "952107008",
+                "start": "0",
+                "stat": "   15342       92  1866238    81581        0        0        0        0        0    34464    65564        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=33\nDEVNAME=sdc1\nDEVTYPE=partition\nPARTN=1"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0x36b64baf",
+                "device": "/dev/sdc1",
+                "unit": "sectors",
+                "partitions": [
+                    {
+                        "node": "/dev/sdc1p1",
+                        "start": 0,
+                        "size": 1859584,
+                        "type": "0",
+                        "bootable": true
+                    },
+                    {
+                        "node": "/dev/sdc1p2",
+                        "start": 21052,
+                        "size": 7936,
+                        "type": "ef"
+                    },
+                    {
+                        "node": "/dev/sdc1p3",
+                        "start": 1859584,
+                        "size": 63394304,
+                        "type": "83"
+                    }
+                ]
+            }
+        },
+        "/dev/sdc2": {
+            "DEVLINKS": "/dev/disk/by-partuuid/36b64baf-02 /dev/disk/by-id/usb-Generic_Flash_Disk_72FFA457-0:0-part2 /dev/disk/by-path/pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0-part2 /dev/disk/by-uuid/1AC3-20ED",
+            "DEVNAME": "/dev/sdc2",
+            "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb3/3-11/3-11:1.0/host6/target6:0:0/6:0:0:0/block/sdc/sdc2",
+            "DEVTYPE": "partition",
+            "ID_BUS": "usb",
+            "ID_FS_TYPE": "vfat",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "1AC3-20ED",
+            "ID_FS_UUID_ENC": "1AC3-20ED",
+            "ID_FS_VERSION": "FAT12",
+            "ID_INSTANCE": "0:0",
+            "ID_MODEL": "Flash_Disk",
+            "ID_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "ID_MODEL_ID": "6387",
+            "ID_PART_ENTRY_DISK": "8:32",
+            "ID_PART_ENTRY_NUMBER": "2",
+            "ID_PART_ENTRY_OFFSET": "21052",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "7936",
+            "ID_PART_ENTRY_TYPE": "0xef",
+            "ID_PART_ENTRY_UUID": "36b64baf-02",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "36b64baf",
+            "ID_PATH": "pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0",
+            "ID_PATH_TAG": "pci-0000_00_14_0-usb-0_11_1_0-scsi-0_0_0_0",
+            "ID_REVISION": "8.07",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Generic_Flash_Disk_72FFA457-0:0",
+            "ID_SERIAL_SHORT": "72FFA457",
+            "ID_TYPE": "disk",
+            "ID_USB_DRIVER": "usb-storage",
+            "ID_USB_INTERFACES": ":080650:",
+            "ID_USB_INTERFACE_NUM": "00",
+            "ID_VENDOR": "Generic",
+            "ID_VENDOR_ENC": "Generic\\x20",
+            "ID_VENDOR_ID": "058f",
+            "MAJOR": "8",
+            "MINOR": "34",
+            "PARTN": "2",
+            "SCSI_MODEL": "Flash_Disk",
+            "SCSI_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "SCSI_REVISION": "8.07",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "Generic",
+            "SCSI_VENDOR_ENC": "Generic\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "13544949",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:34",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "2",
+                "ro": "0",
+                "size": "4063232",
+                "start": "21052",
+                "stat": "       0        0        0        0        0        0        0        0        0        0        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=34\nDEVNAME=sdc2\nDEVTYPE=partition\nPARTN=2"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0x00000000",
+                "device": "/dev/sdc2",
+                "unit": "sectors",
+                "grain": "512",
+                "partitions": []
+            }
+        },
+        "/dev/sdc3": {
+            "DEVLINKS": "/dev/disk/by-partuuid/36b64baf-03 /dev/disk/by-id/usb-Generic_Flash_Disk_72FFA457-0:0-part3 /dev/disk/by-uuid/8bcd1ba9-a05d-4f66-b64a-2d9042753ee7 /dev/disk/by-label/writable /dev/disk/by-path/pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0-part3",
+            "DEVNAME": "/dev/sdc3",
+            "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb3/3-11/3-11:1.0/host6/target6:0:0/6:0:0:0/block/sdc/sdc3",
+            "DEVTYPE": "partition",
+            "ID_BUS": "usb",
+            "ID_FS_LABEL": "writable",
+            "ID_FS_LABEL_ENC": "writable",
+            "ID_FS_TYPE": "ext4",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "8bcd1ba9-a05d-4f66-b64a-2d9042753ee7",
+            "ID_FS_UUID_ENC": "8bcd1ba9-a05d-4f66-b64a-2d9042753ee7",
+            "ID_FS_VERSION": "1.0",
+            "ID_INSTANCE": "0:0",
+            "ID_MODEL": "Flash_Disk",
+            "ID_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "ID_MODEL_ID": "6387",
+            "ID_PART_ENTRY_DISK": "8:32",
+            "ID_PART_ENTRY_NUMBER": "3",
+            "ID_PART_ENTRY_OFFSET": "1859584",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "63394304",
+            "ID_PART_ENTRY_TYPE": "0x83",
+            "ID_PART_ENTRY_UUID": "36b64baf-03",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "36b64baf",
+            "ID_PATH": "pci-0000:00:14.0-usb-0:11:1.0-scsi-0:0:0:0",
+            "ID_PATH_TAG": "pci-0000_00_14_0-usb-0_11_1_0-scsi-0_0_0_0",
+            "ID_REVISION": "8.07",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Generic_Flash_Disk_72FFA457-0:0",
+            "ID_SERIAL_SHORT": "72FFA457",
+            "ID_TYPE": "disk",
+            "ID_USB_DRIVER": "usb-storage",
+            "ID_USB_INTERFACES": ":080650:",
+            "ID_USB_INTERFACE_NUM": "00",
+            "ID_VENDOR": "Generic",
+            "ID_VENDOR_ENC": "Generic\\x20",
+            "ID_VENDOR_ID": "058f",
+            "MAJOR": "8",
+            "MINOR": "35",
+            "PARTN": "3",
+            "SCSI_MODEL": "Flash_Disk",
+            "SCSI_MODEL_ENC": "Flash\\x20Disk\\x20\\x20\\x20\\x20\\x20\\x20",
+            "SCSI_REVISION": "8.07",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "Generic",
+            "SCSI_VENDOR_ENC": "Generic\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "12691034",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:35",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "3",
+                "ro": "0",
+                "size": "32457883648",
+                "start": "1859584",
+                "stat": "     211        3    10310     4443     1332     1088   284928   137320        0     5916   137836        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=35\nDEVNAME=sdc3\nDEVTYPE=partition\nPARTN=3"
+            }
+        },
+        "/dev/nvme0n1": {
+            "DEVLINKS": "/dev/disk/by-id/nvme-Samsung_SSD_970_EVO_Plus_500GB_S4EVNJ0N203359W /dev/disk/by-id/nvme-eui.0025385201404936 /dev/disk/by-path/pci-0000:04:00.0-nvme-1",
+            "DEVNAME": "/dev/nvme0n1",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1c.4/0000:04:00.0/nvme/nvme0/nvme0n1",
+            "DEVTYPE": "disk",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_MODEL": "Samsung SSD 970 EVO Plus 500GB",
+            "ID_PATH": "pci-0000:04:00.0-nvme-1",
+            "ID_PATH_TAG": "pci-0000_04_00_0-nvme-1",
+            "ID_REVISION": "2B2QEXM7",
+            "ID_SERIAL": "Samsung SSD 970 EVO Plus 500GB_S4EVNJ0N203359W",
+            "ID_SERIAL_SHORT": "S4EVNJ0N203359W",
+            "ID_WWN": "eui.0025385201404936",
+            "MAJOR": "259",
+            "MINOR": "0",
+            "MPATH_SBIN_PATH": "/sbin",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1075625",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "capability": "50",
+                "dev": "259:0",
+                "device": null,
+                "discard_alignment": "0",
+                "eui": "00 25 38 52 01 40 49 36",
+                "events": "",
+                "events_async": "",
+                "events_poll_msecs": "-1",
+                "ext_range": "256",
+                "hidden": "0",
+                "inflight": "       0        0",
+                "nsid": "1",
+                "range": "0",
+                "removable": "0",
+                "ro": "0",
+                "size": "500107862016",
+                "stat": "     646        0    29360       93        0        0        0        0        0      172       28        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=259\nMINOR=0\nDEVNAME=nvme0n1\nDEVTYPE=disk",
+                "wwid": "eui.0025385201404936"
+            }
+        },
+        "/dev/sda": {
+            "DEVLINKS": "/dev/disk/by-id/scsi-30000000000000000 /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_13207907000097410026 /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-path/pci-0000:00:1f.2-ata-1 /dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_13207907000097410026 /dev/disk/by-id/ata-Corsair_Force_GS_13207907000097410026 /dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_13207907000097410026",
+            "DEVNAME": "/dev/sda",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda",
+            "DEVTYPE": "disk",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-1",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-1",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_13207907000097410026",
+            "ID_SERIAL_SHORT": "13207907000097410026",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "0",
+            "MPATH_SBIN_PATH": "/sbin",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_VENDOR": "13207907000097410026",
+            "SCSI_IDENT_SERIAL": "13207907000097410026",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1078655",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "capability": "50",
+                "dev": "8:0",
+                "device": null,
+                "discard_alignment": "0",
+                "events": "",
+                "events_async": "",
+                "events_poll_msecs": "-1",
+                "ext_range": "256",
+                "hidden": "0",
+                "inflight": "       0        0",
+                "range": "16",
+                "removable": "0",
+                "ro": "0",
+                "size": "128035676160",
+                "stat": "     711        0    27630      117        1        0        1        0        0      248        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=0\nDEVNAME=sda\nDEVTYPE=disk"
+            },
+            "partitiontable": {
+                "label": "gpt",
+                "id": "F3254C46-A7DC-4A2B-8ED1-ACB53A05CE0A",
+                "device": "/dev/sda",
+                "unit": "sectors",
+                "firstlba": 34,
+                "lastlba": 250069646,
+                "partitions": [
+                    {
+                        "node": "/dev/sda1",
+                        "start": 2048,
+                        "size": 1,
+                        "type": "21686148-6449-6E6F-744E-656564454649",
+                        "uuid": "91F8E67C-5675-4B6B-B0C4-54B990FBC684",
+                        "name": "BIOS boot partition"
+                    },
+                    {
+                        "node": "/dev/sda2",
+                        "start": 4096,
+                        "size": 247463936,
+                        "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+                        "uuid": "2942FC77-138E-4269-9850-9E80D0D1E13B",
+                        "name": "Linux filesystem"
+                    }
+                ]
+            }
+        },
+        "/dev/sda1": {
+            "DEVLINKS": "/dev/disk/by-id/ata-Corsair_Force_GS_13207907000097410026-part1 /dev/disk/by-id/wwn-0x0000000000000000-part1 /dev/disk/by-path/pci-0000:00:1f.2-ata-1-part1 /dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_13207907000097410026-part1 /dev/disk/by-partuuid/91f8e67c-5675-4b6b-b0c4-54b990fbc684 /dev/disk/by-partlabel/BIOS\\x20boot\\x20partition /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_13207907000097410026-part1 /dev/disk/by-id/scsi-30000000000000000-part1 /dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_13207907000097410026-part1",
+            "DEVNAME": "/dev/sda1",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda/sda1",
+            "DEVTYPE": "partition",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_ENTRY_DISK": "8:0",
+            "ID_PART_ENTRY_NAME": "BIOS\\x20boot\\x20partition",
+            "ID_PART_ENTRY_NUMBER": "1",
+            "ID_PART_ENTRY_OFFSET": "2048",
+            "ID_PART_ENTRY_SCHEME": "gpt",
+            "ID_PART_ENTRY_SIZE": "1",
+            "ID_PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649",
+            "ID_PART_ENTRY_UUID": "91f8e67c-5675-4b6b-b0c4-54b990fbc684",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-1",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-1",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_13207907000097410026",
+            "ID_SERIAL_SHORT": "13207907000097410026",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "1",
+            "PARTN": "1",
+            "PARTNAME": "BIOS boot partition",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_VENDOR": "13207907000097410026",
+            "SCSI_IDENT_SERIAL": "13207907000097410026",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1081180",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:1",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "1",
+                "ro": "0",
+                "size": "512",
+                "start": "2048",
+                "stat": "      12        0       26        7        0        0        0        0        0       44        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=1\nDEVNAME=sda1\nDEVTYPE=partition\nPARTN=1\nPARTNAME=BIOS boot partition"
+            }
+        },
+        "/dev/sda2": {
+            "DEVLINKS": "/dev/disk/by-id/scsi-30000000000000000-part2 /dev/disk/by-partlabel/Linux\\x20filesystem /dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_13207907000097410026-part2 /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_13207907000097410026-part2 /dev/disk/by-id/ata-Corsair_Force_GS_13207907000097410026-part2 /dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_13207907000097410026-part2 /dev/disk/by-path/pci-0000:00:1f.2-ata-1-part2 /dev/disk/by-id/wwn-0x0000000000000000-part2 /dev/disk/by-partuuid/2942fc77-138e-4269-9850-9e80d0d1e13b",
+            "DEVNAME": "/dev/sda2",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda/sda2",
+            "DEVTYPE": "partition",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_FS_LABEL": "ubuntu-server:0",
+            "ID_FS_LABEL_ENC": "ubuntu-server:0",
+            "ID_FS_TYPE": "linux_raid_member",
+            "ID_FS_USAGE": "raid",
+            "ID_FS_UUID": "079fa971-c86d-cd37-05c5-4ec1b343dc99",
+            "ID_FS_UUID_ENC": "079fa971-c86d-cd37-05c5-4ec1b343dc99",
+            "ID_FS_UUID_SUB": "383e86fc-6472-2d62-e62b-bec2529b2c2d",
+            "ID_FS_UUID_SUB_ENC": "383e86fc-6472-2d62-e62b-bec2529b2c2d",
+            "ID_FS_VERSION": "1.2",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_ENTRY_DISK": "8:0",
+            "ID_PART_ENTRY_NAME": "Linux\\x20filesystem",
+            "ID_PART_ENTRY_NUMBER": "2",
+            "ID_PART_ENTRY_OFFSET": "4096",
+            "ID_PART_ENTRY_SCHEME": "gpt",
+            "ID_PART_ENTRY_SIZE": "247463936",
+            "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4",
+            "ID_PART_ENTRY_UUID": "2942fc77-138e-4269-9850-9e80d0d1e13b",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-1",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-1",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_13207907000097410026",
+            "ID_SERIAL_SHORT": "13207907000097410026",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "2",
+            "PARTN": "2",
+            "PARTNAME": "Linux filesystem",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_13207907000097410026",
+            "SCSI_IDENT_LUN_VENDOR": "13207907000097410026",
+            "SCSI_IDENT_SERIAL": "13207907000097410026",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1083076",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:2",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "2",
+                "ro": "0",
+                "size": "126701535232",
+                "start": "4096",
+                "stat": "     600        0    21796       80        1        0        1        0        0      188        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=2\nDEVNAME=sda2\nDEVTYPE=partition\nPARTN=2\nPARTNAME=Linux filesystem"
+            }
+        },
+        "/dev/sdb": {
+            "DEVLINKS": "/dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_1320790700009741003D /dev/disk/by-id/ata-Corsair_Force_GS_1320790700009741003D /dev/disk/by-id/wwn-0x0000000000000000 /dev/disk/by-id/scsi-30000000000000000 /dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_1320790700009741003D /dev/disk/by-path/pci-0000:00:1f.2-ata-2 /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_1320790700009741003D",
+            "DEVNAME": "/dev/sdb",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata2/host1/target1:0:0/1:0:0:0/block/sdb",
+            "DEVTYPE": "disk",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-2",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-2",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_1320790700009741003D",
+            "ID_SERIAL_SHORT": "1320790700009741003D",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "16",
+            "MPATH_SBIN_PATH": "/sbin",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_VENDOR": "1320790700009741003D",
+            "SCSI_IDENT_SERIAL": "1320790700009741003D",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1079071",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "capability": "50",
+                "dev": "8:16",
+                "device": null,
+                "discard_alignment": "0",
+                "events": "",
+                "events_async": "",
+                "events_poll_msecs": "-1",
+                "ext_range": "256",
+                "hidden": "0",
+                "inflight": "       0        0",
+                "range": "16",
+                "removable": "0",
+                "ro": "0",
+                "size": "128035676160",
+                "stat": "     298        0    12529       72        1        0        1        0        0      188        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=16\nDEVNAME=sdb\nDEVTYPE=disk"
+            },
+            "partitiontable": {
+                "label": "gpt",
+                "id": "F3254C46-A7DC-4A2B-8ED1-ACB53A05CE0A",
+                "device": "/dev/sdb",
+                "unit": "sectors",
+                "firstlba": 34,
+                "lastlba": 250069646,
+                "partitions": [
+                    {
+                        "node": "/dev/sdb1",
+                        "start": 2048,
+                        "size": 1,
+                        "type": "21686148-6449-6E6F-744E-656564454649",
+                        "uuid": "D997F579-F242-4C88-B51A-87BF5111545D",
+                        "name": "BIOS boot partition"
+                    },
+                    {
+                        "node": "/dev/sdb2",
+                        "start": 4096,
+                        "size": 247463936,
+                        "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+                        "uuid": "8B3C809D-54AC-4515-B719-CDE1F8A361B7",
+                        "name": "Linux filesystem"
+                    }
+                ]
+            }
+        },
+        "/dev/sdb1": {
+            "DEVLINKS": "/dev/disk/by-path/pci-0000:00:1f.2-ata-2-part1 /dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_1320790700009741003D-part1 /dev/disk/by-id/scsi-30000000000000000-part1 /dev/disk/by-id/ata-Corsair_Force_GS_1320790700009741003D-part1 /dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_1320790700009741003D-part1 /dev/disk/by-partlabel/BIOS\\x20boot\\x20partition /dev/disk/by-partuuid/d997f579-f242-4c88-b51a-87bf5111545d /dev/disk/by-id/wwn-0x0000000000000000-part1 /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_1320790700009741003D-part1",
+            "DEVNAME": "/dev/sdb1",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata2/host1/target1:0:0/1:0:0:0/block/sdb/sdb1",
+            "DEVTYPE": "partition",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_ENTRY_DISK": "8:16",
+            "ID_PART_ENTRY_NAME": "BIOS\\x20boot\\x20partition",
+            "ID_PART_ENTRY_NUMBER": "1",
+            "ID_PART_ENTRY_OFFSET": "2048",
+            "ID_PART_ENTRY_SCHEME": "gpt",
+            "ID_PART_ENTRY_SIZE": "1",
+            "ID_PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649",
+            "ID_PART_ENTRY_UUID": "d997f579-f242-4c88-b51a-87bf5111545d",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-2",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-2",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_1320790700009741003D",
+            "ID_SERIAL_SHORT": "1320790700009741003D",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "17",
+            "PARTN": "1",
+            "PARTNAME": "BIOS boot partition",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_VENDOR": "1320790700009741003D",
+            "SCSI_IDENT_SERIAL": "1320790700009741003D",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1082533",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:17",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "1",
+                "ro": "0",
+                "size": "512",
+                "start": "2048",
+                "stat": "      12        0       26       11        0        0        0        0        0       48        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=17\nDEVNAME=sdb1\nDEVTYPE=partition\nPARTN=1\nPARTNAME=BIOS boot partition"
+            }
+        },
+        "/dev/sdb2": {
+            "DEVLINKS": "/dev/disk/by-id/scsi-0ATA_Corsair_Force_GS_1320790700009741003D-part2 /dev/disk/by-path/pci-0000:00:1f.2-ata-2-part2 /dev/disk/by-partuuid/8b3c809d-54ac-4515-b719-cde1f8a361b7 /dev/disk/by-id/scsi-1ATA_Corsair_Force_GS_1320790700009741003D-part2 /dev/disk/by-id/scsi-SATA_Corsair_Force_GS_1320790700009741003D-part2 /dev/disk/by-id/scsi-30000000000000000-part2 /dev/disk/by-id/ata-Corsair_Force_GS_1320790700009741003D-part2 /dev/disk/by-id/wwn-0x0000000000000000-part2 /dev/disk/by-partlabel/Linux\\x20filesystem",
+            "DEVNAME": "/dev/sdb2",
+            "DEVPATH": "/devices/pci0000:00/0000:00:1f.2/ata2/host1/target1:0:0/1:0:0:0/block/sdb/sdb2",
+            "DEVTYPE": "partition",
+            "DM_MULTIPATH_DEVICE_PATH": "0",
+            "ID_ATA": "1",
+            "ID_BUS": "ata",
+            "ID_FS_LABEL": "ubuntu-server:0",
+            "ID_FS_LABEL_ENC": "ubuntu-server:0",
+            "ID_FS_TYPE": "linux_raid_member",
+            "ID_FS_USAGE": "raid",
+            "ID_FS_UUID": "079fa971-c86d-cd37-05c5-4ec1b343dc99",
+            "ID_FS_UUID_ENC": "079fa971-c86d-cd37-05c5-4ec1b343dc99",
+            "ID_FS_UUID_SUB": "df50ba68-38bd-acc2-3ad1-057980954294",
+            "ID_FS_UUID_SUB_ENC": "df50ba68-38bd-acc2-3ad1-057980954294",
+            "ID_FS_VERSION": "1.2",
+            "ID_MODEL": "Corsair_Force_GS",
+            "ID_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "ID_PART_ENTRY_DISK": "8:16",
+            "ID_PART_ENTRY_NAME": "Linux\\x20filesystem",
+            "ID_PART_ENTRY_NUMBER": "2",
+            "ID_PART_ENTRY_OFFSET": "4096",
+            "ID_PART_ENTRY_SCHEME": "gpt",
+            "ID_PART_ENTRY_SIZE": "247463936",
+            "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4",
+            "ID_PART_ENTRY_UUID": "8b3c809d-54ac-4515-b719-cde1f8a361b7",
+            "ID_PART_TABLE_TYPE": "gpt",
+            "ID_PART_TABLE_UUID": "f3254c46-a7dc-4a2b-8ed1-acb53a05ce0a",
+            "ID_PATH": "pci-0000:00:1f.2-ata-2",
+            "ID_PATH_TAG": "pci-0000_00_1f_2-ata-2",
+            "ID_REVISION": "A",
+            "ID_SCSI": "1",
+            "ID_SCSI_INQUIRY": "1",
+            "ID_SERIAL": "Corsair_Force_GS_1320790700009741003D",
+            "ID_SERIAL_SHORT": "1320790700009741003D",
+            "ID_TYPE": "disk",
+            "ID_VENDOR": "ATA",
+            "ID_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "ID_WWN": "0x0000000000000000",
+            "ID_WWN_WITH_EXTENSION": "0x0000000000000000",
+            "MAJOR": "8",
+            "MINOR": "18",
+            "PARTN": "2",
+            "PARTNAME": "Linux filesystem",
+            "SCSI_IDENT_LUN_ATA": "Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_NAA_LOCAL": "0000000000000000",
+            "SCSI_IDENT_LUN_T10": "ATA_Corsair_Force_GS_1320790700009741003D",
+            "SCSI_IDENT_LUN_VENDOR": "1320790700009741003D",
+            "SCSI_IDENT_SERIAL": "1320790700009741003D",
+            "SCSI_MODEL": "Corsair_Force_GS",
+            "SCSI_MODEL_ENC": "Corsair\\x20Force\\x20GS",
+            "SCSI_REVISION": "A",
+            "SCSI_TPGS": "0",
+            "SCSI_TYPE": "disk",
+            "SCSI_VENDOR": "ATA",
+            "SCSI_VENDOR_ENC": "ATA\\x20\\x20\\x20\\x20\\x20",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1103395",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "8:18",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "2",
+                "ro": "0",
+                "size": "126701535232",
+                "start": "4096",
+                "stat": "     187        0     6695       31        1        0        1        0        0      140        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=8\nMINOR=18\nDEVNAME=sdb2\nDEVTYPE=partition\nPARTN=2\nPARTNAME=Linux filesystem"
+            }
+        },
+        "/dev/md127": {
+            "DEVLINKS": "/dev/md/ubuntu-server:0 /dev/disk/by-id/md-name-ubuntu-server:0 /dev/disk/by-id/md-uuid-079fa971:c86dcd37:05c54ec1:b343dc99",
+            "DEVNAME": "/dev/md127",
+            "DEVPATH": "/devices/virtual/block/md127",
+            "DEVTYPE": "disk",
+            "MAJOR": "9",
+            "MD_DEVICES": "2",
+            "MD_DEVICE_ev_sda2_DEV": "/dev/sda2",
+            "MD_DEVICE_ev_sda2_ROLE": "0",
+            "MD_DEVICE_ev_sdb2_DEV": "/dev/sdb2",
+            "MD_DEVICE_ev_sdb2_ROLE": "1",
+            "MD_DEVNAME": "ubuntu-server:0",
+            "MD_LEVEL": "raid1",
+            "MD_METADATA": "1.2",
+            "MD_NAME": "ubuntu-server:0",
+            "MD_UUID": "079fa971:c86dcd37:05c54ec1:b343dc99",
+            "MINOR": "127",
+            "SUBSYSTEM": "block",
+            "SYSTEMD_WANTS": "mdmonitor.service",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1083467",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "capability": "50",
+                "dev": "9:127",
+                "discard_alignment": "0",
+                "events": "",
+                "events_async": "",
+                "events_poll_msecs": "-1",
+                "ext_range": "256",
+                "hidden": "0",
+                "inflight": "       0        0",
+                "range": "1",
+                "removable": "0",
+                "ro": "0",
+                "size": "126633377792",
+                "stat": "     619        0    26848        0        0        0        0        0        0        0        0        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=9\nMINOR=127\nDEVNAME=md127\nDEVTYPE=disk"
+            }
+        }
+    },
+    "bcache": {
+        "backing": {},
+        "caching": {}
+    }
+}
diff --git a/tests/data/probert_storage_nvme_multipath.json b/tests/data/probert_storage_nvme_multipath.json
new file mode 100644
index 0000000..56a761d
--- /dev/null
+++ b/tests/data/probert_storage_nvme_multipath.json
@@ -0,0 +1,310 @@
+{
+  "blockdev": {
+    "/dev/sr0": {
+      "DEVLINKS": "/dev/disk/by-id/usb-0ea0_1111 /dev/dvd /dev/disk/by-path/pci-0000:00:14.0-usb-0:7.2:1.0-scsi-0:0:0:0 /dev/cdrom /dev/disk/by-uuid/2020-04-05-09-44-04-00 /dev/disk/by-label/Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+      "DEVNAME": "/dev/sr0",
+      "DEVPATH": "/devices/pci0000:00/0000:00:14.0/usb1/1-7/1-7.2/1-7.2:1.0/host14/target14:0:0/14:0:0:0/block/sr0",
+      "DEVTYPE": "disk",
+      "ID_BUS": "usb",
+      "ID_CDROM": "1",
+      "ID_CDROM_CD": "1",
+      "ID_CDROM_DVD": "1",
+      "ID_CDROM_MEDIA": "1",
+      "ID_CDROM_MEDIA_CD": "1",
+      "ID_CDROM_MEDIA_SESSION_COUNT": "1",
+      "ID_CDROM_MEDIA_TRACK_COUNT": "1",
+      "ID_CDROM_MEDIA_TRACK_COUNT_DATA": "1",
+      "ID_CDROM_MRW": "1",
+      "ID_CDROM_MRW_W": "1",
+      "ID_FOR_SEAT": "block-pci-0000_00_14_0-usb-0_7_2_1_0-scsi-0_0_0_0",
+      "ID_FS_BOOT_SYSTEM_ID": "EL\\x20TORITO\\x20SPECIFICATION",
+      "ID_FS_LABEL": "Ubuntu-Server_20.04_LTS_amd64",
+      "ID_FS_LABEL_ENC": "Ubuntu-Server\\x2020.04\\x20LTS\\x20amd64",
+      "ID_FS_TYPE": "iso9660",
+      "ID_FS_USAGE": "filesystem",
+      "ID_FS_UUID": "2020-04-05-09-44-04-00",
+      "ID_FS_UUID_ENC": "2020-04-05-09-44-04-00",
+      "ID_FS_VERSION": "Joliet Extension",
+      "ID_MODEL": "1111",
+      "ID_MODEL_ENC": "1111",
+      "ID_MODEL_ID": "1111",
+      "ID_PART_TABLE_TYPE": "dos",
+      "ID_PART_TABLE_UUID": "15e92274",
+      "ID_PATH": "pci-0000:00:14.0-usb-0:7.2:1.0-scsi-0:0:0:0",
+      "ID_PATH_TAG": "pci-0000_00_14_0-usb-0_7_2_1_0-scsi-0_0_0_0",
+      "ID_REVISION": "0200",
+      "ID_SCSI": "1",
+      "ID_SCSI_INQUIRY": "1",
+      "ID_SERIAL": "0ea0_1111",
+      "ID_TYPE": "generic",
+      "ID_USB_DRIVER": "usb-storage",
+      "ID_USB_INTERFACES": ":080550:",
+      "ID_USB_INTERFACE_NUM": "00",
+      "ID_VENDOR": "0ea0",
+      "ID_VENDOR_ENC": "0ea0",
+      "ID_VENDOR_ID": "0ea0",
+      "MAJOR": "11",
+      "MINOR": "0",
+      "SCSI_MODEL": "Virtual_CDROM",
+      "SCSI_MODEL_ENC": "Virtual\\x20CDROM\\x20\\x20\\x20",
+      "SCSI_REVISION": "3000",
+      "SCSI_TPGS": "0",
+      "SCSI_TYPE": "cd/dvd",
+      "SCSI_VENDOR": "IPMI",
+      "SCSI_VENDOR_ENC": "IPMI\\x20\\x20\\x20\\x20",
+      "SUBSYSTEM": "block",
+      "SYSTEMD_MOUNT_DEVICE_BOUND": "1",
+      "TAGS": ":uaccess:systemd:seat:",
+      "USEC_INITIALIZED": "8087244",
+      "attrs": {
+        "alignment_offset": "0",
+        "bdi": null,
+        "capability": "119",
+        "dev": "11:0",
+        "device": null,
+        "discard_alignment": "0",
+        "events": "media_change eject_request",
+        "events_async": "",
+        "events_poll_msecs": "-1",
+        "ext_range": "1",
+        "hidden": "0",
+        "inflight": "       0        0",
+        "range": "1",
+        "removable": "1",
+        "ro": "0",
+        "size": "963641344",
+        "stat": "   15299       95  1879276  1488515        0        0        0        0        0    62500  1457848        0        0        0        0",
+        "subsystem": "block",
+        "uevent": "MAJOR=11\nMINOR=0\nDEVNAME=sr0\nDEVTYPE=disk"
+      },
+      "partitiontable": {
+        "label": "dos",
+        "id": "0x15e92274",
+        "device": "/dev/sr0",
+        "unit": "sectors",
+        "partitions": [
+          {
+            "node": "/dev/sr0p1",
+            "start": 0,
+            "size": 1882112,
+            "type": "0",
+            "bootable": true
+          },
+          {
+            "node": "/dev/sr0p2",
+            "start": 20464,
+            "size": 8000,
+            "type": "ef"
+          }
+        ]
+      }
+    },
+    "/dev/nvme0n1": {
+      "DEVLINKS": "/dev/disk/by-id/nvme-SAMSUNG_MZPLL3T2HAJQ-00005_S4CCNE0M300015 /dev/disk/by-id/nvme-eui.344343304d3000150025384500000004",
+      "DEVNAME": "/dev/nvme0n1",
+      "DEVPATH": "/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1",
+      "DEVTYPE": "disk",
+      "DM_MULTIPATH_DEVICE_PATH": "0",
+      "ID_MODEL": "SAMSUNG MZPLL3T2HAJQ-00005",
+      "ID_PART_TABLE_TYPE": "gpt",
+      "ID_PART_TABLE_UUID": "4bac57b7-307b-4b0e-a853-e0232c6fb955",
+      "ID_REVISION": "GPJA0B3Q",
+      "ID_SERIAL": "SAMSUNG MZPLL3T2HAJQ-00005_S4CCNE0M300015",
+      "ID_SERIAL_SHORT": "S4CCNE0M300015",
+      "ID_WWN": "eui.344343304d3000150025384500000004",
+      "MAJOR": "259",
+      "MINOR": "1",
+      "MPATH_SBIN_PATH": "/sbin",
+      "SUBSYSTEM": "block",
+      "TAGS": ":systemd:",
+      "USEC_INITIALIZED": "5210525",
+      "attrs": {
+        "alignment_offset": "0",
+        "bdi": null,
+        "capability": "50",
+        "dev": "259:1",
+        "device": null,
+        "discard_alignment": "0",
+        "events": "",
+        "events_async": "",
+        "events_poll_msecs": "-1",
+        "ext_range": "256",
+        "hidden": "0",
+        "inflight": "       0        0",
+        "nguid": "34434330-4d30-0015-0025-384500000004",
+        "nsid": "1",
+        "range": "0",
+        "removable": "0",
+        "ro": "0",
+        "size": "3200631791616",
+        "stat": "       0        0        0        0        0        0        0        0        0        0        0        0        0        0        0",
+        "subsystem": "block",
+        "uevent": "MAJOR=259\nMINOR=1\nDEVNAME=nvme0n1\nDEVTYPE=disk",
+        "uuid": "34434330-4d30-0015-0025-384500000004",
+        "wwid": "eui.344343304d3000150025384500000004"
+      },
+      "partitiontable": {
+        "label": "gpt",
+        "id": "4BAC57B7-307B-4B0E-A853-E0232C6FB955",
+        "device": "/dev/nvme0n1",
+        "unit": "sectors",
+        "firstlba": 34,
+        "lastlba": 6251233934,
+        "partitions": [
+          {
+            "node": "/dev/nvme0n1p1",
+            "start": 2048,
+            "size": 2048,
+            "type": "21686148-6449-6E6F-744E-656564454649",
+            "uuid": "B6D4F123-1AD8-4893-9B39-0E48074EE38B"
+          },
+          {
+            "node": "/dev/nvme0n1p2",
+            "start": 4096,
+            "size": 48234496,
+            "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+            "uuid": "76A1E7BC-4C68-4BC2-A0F5-7D1D07445FDB"
+          },
+          {
+            "node": "/dev/nvme0n1p3",
+            "start": 48238592,
+            "size": 6202995343,
+            "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+            "uuid": "763B085C-8010-F646-A3D4-E25DE4B83C87"
+          }
+        ]
+      }
+    },
+    "/dev/nvme0n1p1": {
+      "DEVLINKS": "/dev/disk/by-id/nvme-SAMSUNG_MZPLL3T2HAJQ-00005_S4CCNE0M300015-part1 /dev/disk/by-partuuid/b6d4f123-1ad8-4893-9b39-0e48074ee38b /dev/disk/by-id/nvme-eui.344343304d3000150025384500000004-part1",
+      "DEVNAME": "/dev/nvme0n1p1",
+      "DEVPATH": "/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1/nvme0n1p1",
+      "DEVTYPE": "partition",
+      "DM_MULTIPATH_DEVICE_PATH": "0",
+      "ID_MODEL": "SAMSUNG MZPLL3T2HAJQ-00005",
+      "ID_PART_ENTRY_DISK": "259:1",
+      "ID_PART_ENTRY_NUMBER": "1",
+      "ID_PART_ENTRY_OFFSET": "2048",
+      "ID_PART_ENTRY_SCHEME": "gpt",
+      "ID_PART_ENTRY_SIZE": "2048",
+      "ID_PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649",
+      "ID_PART_ENTRY_UUID": "b6d4f123-1ad8-4893-9b39-0e48074ee38b",
+      "ID_PART_TABLE_TYPE": "gpt",
+      "ID_PART_TABLE_UUID": "4bac57b7-307b-4b0e-a853-e0232c6fb955",
+      "ID_REVISION": "GPJA0B3Q",
+      "ID_SCSI": "1",
+      "ID_SERIAL": "SAMSUNG MZPLL3T2HAJQ-00005_S4CCNE0M300015",
+      "ID_SERIAL_SHORT": "S4CCNE0M300015",
+      "ID_WWN": "eui.344343304d3000150025384500000004",
+      "MAJOR": "259",
+      "MINOR": "2",
+      "PARTN": "1",
+      "SUBSYSTEM": "block",
+      "TAGS": ":systemd:",
+      "USEC_INITIALIZED": "5214260",
+      "attrs": {
+        "alignment_offset": "0",
+        "dev": "259:2",
+        "discard_alignment": "0",
+        "inflight": "       0        0",
+        "partition": "1",
+        "ro": "0",
+        "size": "1048576",
+        "start": "2048",
+        "stat": "       0        0        0        0        0        0        0        0        0        0        0        0        0        0        0",
+        "subsystem": "block",
+        "uevent": "MAJOR=259\nMINOR=2\nDEVNAME=nvme0n1p1\nDEVTYPE=partition\nPARTN=1"
+      }
+    },
+    "/dev/nvme0n1p2": {
+      "DEVLINKS": "/dev/disk/by-id/nvme-SAMSUNG_MZPLL3T2HAJQ-00005_S4CCNE0M300015-part2 /dev/disk/by-uuid/25fa500f-e450-4a13-b51d-74eba6f1c915 /dev/disk/by-id/nvme-eui.344343304d3000150025384500000004-part2 /dev/disk/by-partuuid/76a1e7bc-4c68-4bc2-a0f5-7d1d07445fdb",
+      "DEVNAME": "/dev/nvme0n1p2",
+      "DEVPATH": "/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1/nvme0n1p2",
+      "DEVTYPE": "partition",
+      "DM_MULTIPATH_DEVICE_PATH": "0",
+      "ID_FS_TYPE": "xfs",
+      "ID_FS_USAGE": "filesystem",
+      "ID_FS_UUID": "25fa500f-e450-4a13-b51d-74eba6f1c915",
+      "ID_FS_UUID_ENC": "25fa500f-e450-4a13-b51d-74eba6f1c915",
+      "ID_MODEL": "SAMSUNG MZPLL3T2HAJQ-00005",
+      "ID_PART_ENTRY_DISK": "259:1",
+      "ID_PART_ENTRY_NUMBER": "2",
+      "ID_PART_ENTRY_OFFSET": "4096",
+      "ID_PART_ENTRY_SCHEME": "gpt",
+      "ID_PART_ENTRY_SIZE": "48234496",
+      "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4",
+      "ID_PART_ENTRY_UUID": "76a1e7bc-4c68-4bc2-a0f5-7d1d07445fdb",
+      "ID_PART_TABLE_TYPE": "gpt",
+      "ID_PART_TABLE_UUID": "4bac57b7-307b-4b0e-a853-e0232c6fb955",
+      "ID_REVISION": "GPJA0B3Q",
+      "ID_SCSI": "1",
+      "ID_SERIAL": "SAMSUNG MZPLL3T2HAJQ-00005_S4CCNE0M300015",
+      "ID_SERIAL_SHORT": "S4CCNE0M300015",
+      "ID_WWN": "eui.344343304d3000150025384500000004",
+      "MAJOR": "259",
+      "MINOR": "3",
+      "PARTN": "2",
+      "SUBSYSTEM": "block",
+      "TAGS": ":systemd:",
+      "USEC_INITIALIZED": "5215731",
+      "attrs": {
+        "alignment_offset": "0",
+        "dev": "259:3",
+        "discard_alignment": "0",
+        "inflight": "       0        0",
+        "partition": "2",
+        "ro": "0",
+        "size": "24696061952",
+        "start": "4096",
+        "stat": "       0        0        0        0        0        0        0        0        0        0        0        0        0        0        0",
+        "subsystem": "block",
+        "uevent": "MAJOR=259\nMINOR=3\nDEVNAME=nvme0n1p2\nDEVTYPE=partition\nPARTN=2"
+      }
+    },
+    "/dev/nvme0n1p3": {
+      "DEVLINKS": "/dev/disk/by-partuuid/763b085c-8010-f646-a3d4-e25de4b83c87 /dev/disk/by-id/nvme-eui.344343304d3000150025384500000004-part3 /dev/disk/by-id/nvme-SAMSUNG_MZPLL3T2HAJQ-00005_S4CCNE0M300015-part3 /dev/disk/by-uuid/38d3b1d3-05db-441e-b71b-70cd058f7313",
+      "DEVNAME": "/dev/nvme0n1p3",
+      "DEVPATH": "/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1/nvme0n1p3",
+      "DEVTYPE": "partition",
+      "DM_MULTIPATH_DEVICE_PATH": "0",
+      "ID_FS_TYPE": "xfs",
+      "ID_FS_USAGE": "filesystem",
+      "ID_FS_UUID": "38d3b1d3-05db-441e-b71b-70cd058f7313",
+      "ID_FS_UUID_ENC": "38d3b1d3-05db-441e-b71b-70cd058f7313",
+      "ID_MODEL": "SAMSUNG MZPLL3T2HAJQ-00005",
+      "ID_PART_ENTRY_DISK": "259:1",
+      "ID_PART_ENTRY_NUMBER": "3",
+      "ID_PART_ENTRY_OFFSET": "48238592",
+      "ID_PART_ENTRY_SCHEME": "gpt",
+      "ID_PART_ENTRY_SIZE": "6202995343",
+      "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4",
+      "ID_PART_ENTRY_UUID": "763b085c-8010-f646-a3d4-e25de4b83c87",
+      "ID_PART_TABLE_TYPE": "gpt",
+      "ID_PART_TABLE_UUID": "4bac57b7-307b-4b0e-a853-e0232c6fb955",
+      "ID_REVISION": "GPJA0B3Q",
+      "ID_SCSI": "1",
+      "ID_SERIAL": "SAMSUNG MZPLL3T2HAJQ-00005_S4CCNE0M300015",
+      "ID_SERIAL_SHORT": "S4CCNE0M300015",
+      "ID_WWN": "eui.344343304d3000150025384500000004",
+      "MAJOR": "259",
+      "MINOR": "4",
+      "PARTN": "3",
+      "SUBSYSTEM": "block",
+      "TAGS": ":systemd:",
+      "USEC_INITIALIZED": "5215490",
+      "attrs": {
+        "alignment_offset": "0",
+        "dev": "259:4",
+        "discard_alignment": "0",
+        "inflight": "       0        0",
+        "partition": "3",
+        "ro": "0",
+        "size": "3175933615616",
+        "start": "48238592",
+        "stat": "       0        0        0        0        0        0        0        0        0        0        0        0        0        0        0",
+        "subsystem": "block",
+        "uevent": "MAJOR=259\nMINOR=4\nDEVNAME=nvme0n1p3\nDEVTYPE=partition\nPARTN=3"
+      }
+    }
+  }
+}
diff --git a/tests/data/udevadm_info_sandisk_cruzer.txt b/tests/data/udevadm_info_sandisk_cruzer.txt
new file mode 100644
index 0000000..a605afe
--- /dev/null
+++ b/tests/data/udevadm_info_sandisk_cruzer.txt
@@ -0,0 +1,54 @@
+DEVPATH='/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.5/2-1.5.1/2-1.5.1:1.0/host6/target6:0:0/6:0:0:0/block/sdc/sdc1'
+DEVNAME='/dev/sdc1'
+DEVTYPE='partition'
+PARTN='1'
+MAJOR='8'
+MINOR='33'
+SUBSYSTEM='block'
+USEC_INITIALIZED='5265867'
+SCSI_TPGS='0'
+SCSI_TYPE='disk'
+SCSI_VENDOR='SanDisk''
+SCSI_VENDOR_ENC='SanDisk''
+SCSI_MODEL='Cruzer_Fit'
+SCSI_MODEL_ENC='Cruzer\x20Fit\x20\x20\x20\x20\x20\x20'
+SCSI_REVISION='1.00'
+ID_SCSI='1'
+ID_VENDOR='SanDisk_'
+ID_VENDOR_ENC='SanDisk\x27'
+ID_MODEL='Cruzer_Fit'
+ID_MODEL_ENC='Cruzer\x20Fit\x20\x20\x20\x20\x20\x20'
+ID_REVISION='1.00'
+ID_TYPE='disk'
+ID_SCSI_INQUIRY='1'
+ID_VENDOR_ID='0781'
+ID_MODEL_ID='5571'
+ID_SERIAL='SanDisk__Cruzer_Fit_4C530000140118216265-0:0'
+ID_SERIAL_SHORT='4C530000140118216265'
+ID_INSTANCE='0:0'
+ID_BUS='usb'
+ID_USB_INTERFACES=':080650:'
+ID_USB_INTERFACE_NUM='00'
+ID_USB_DRIVER='usb-storage'
+ID_PATH='pci-0000:00:1d.0-usb-0:1.5.1:1.0-scsi-0:0:0:0'
+ID_PATH_TAG='pci-0000_00_1d_0-usb-0_1_5_1_1_0-scsi-0_0_0_0'
+ID_PART_TABLE_UUID='36b64baf'
+ID_PART_TABLE_TYPE='dos'
+ID_FS_UUID='2020-04-23-08-02-07-00'
+ID_FS_UUID_ENC='2020-04-23-08-02-07-00'
+ID_FS_BOOT_SYSTEM_ID='EL\x20TORITO\x20SPECIFICATION'
+ID_FS_VERSION='Joliet Extension'
+ID_FS_LABEL='Ubuntu-Server_20.04_LTS_amd64'
+ID_FS_LABEL_ENC='Ubuntu-Server\x2020.04\x20LTS\x20amd64'
+ID_FS_TYPE='iso9660'
+ID_FS_USAGE='filesystem'
+ID_PART_ENTRY_SCHEME='dos'
+ID_PART_ENTRY_UUID='36b64baf-01'
+ID_PART_ENTRY_TYPE='0x0'
+ID_PART_ENTRY_FLAGS='0x80'
+ID_PART_ENTRY_NUMBER='1'
+ID_PART_ENTRY_OFFSET='0'
+ID_PART_ENTRY_SIZE='1859584'
+ID_PART_ENTRY_DISK='8:32'
+DEVLINKS='/dev/disk/by-path/pci-0000:00:1d.0-usb-0:1.5.1:1.0-scsi-0:0:0:0-part1 /dev/disk/by-id/usb-SanDisk__Cruzer_Fit_4C530000140118216265-0:0-part1 /dev/disk/by-label/Ubuntu-Server\x2020.04\x20LTS\x20amd64 /dev/disk/by-partuuid/36b64baf-01 /dev/disk/by-uuid/2020-04-23-08-02-07-00'
+TAGS=':systemd:'
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index 9514745..2f5e51a 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -1,6 +1,5 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
-import contextlib
 import imp
 import importlib
 import mock
@@ -10,9 +9,11 @@ import shutil
 import string
 import tempfile
 from unittest import TestCase, skipIf
-
+from contextlib import contextmanager
 from curtin import util
 
+_real_subp = util.subp
+
 
 def builtin_module_name():
     options = ('builtins', '__builtin__')
@@ -27,7 +28,7 @@ def builtin_module_name():
             return name
 
 
-@contextlib.contextmanager
+@contextmanager
 def simple_mocked_open(content=None):
     if not content:
         content = ''
@@ -54,6 +55,54 @@ def skipUnlessJsonSchema():
 class CiTestCase(TestCase):
     """Common testing class which all curtin unit tests subclass."""
 
+    allowed_subp = False
+    SUBP_SHELL_TRUE = "shell=True"
+
+    @contextmanager
+    def allow_subp(self, allowed_subp):
+        orig = self.allowed_subp
+        try:
+            self.allowed_subp = allowed_subp
+            yield
+        finally:
+            self.allowed_subp = orig
+
+    def setUp(self):
+        super(CiTestCase, self).setUp()
+        if self.allowed_subp is True:
+            util.subp = _real_subp
+        else:
+            util.subp = self._fake_subp
+
+    def _fake_subp(self, *args, **kwargs):
+        if 'args' in kwargs:
+            cmd = kwargs['args']
+        else:
+            cmd = args[0]
+
+        if not isinstance(cmd, str):
+            cmd = cmd[0]
+        pass_through = False
+        if not isinstance(self.allowed_subp, (list, bool)):
+            raise TypeError("self.allowed_subp supports list or bool.")
+        if isinstance(self.allowed_subp, bool):
+            pass_through = self.allowed_subp
+        else:
+            pass_through = (
+                (cmd in self.allowed_subp) or
+                (self.SUBP_SHELL_TRUE in self.allowed_subp and
+                 kwargs.get('shell')))
+        if pass_through:
+            return _real_subp(*args, **kwargs)
+        raise Exception(
+            "called subp. set self.allowed_subp=True to allow\n subp(%s)" %
+            ', '.join([str(repr(a)) for a in args] +
+                      ["%s=%s" % (k, repr(v)) for k, v in kwargs.items()]))
+
+    def tearDown(self):
+        util.subp = _real_subp
+        super(CiTestCase, self).tearDown()
+
     def add_patch(self, target, attr, **kwargs):
         """Patches specified target object and sets it as attr on test
         instance also schedules cleanup"""
diff --git a/tests/unittests/test_apt_custom_sources_list.py b/tests/unittests/test_apt_custom_sources_list.py
index fb6eb0c..bf004b1 100644
--- a/tests/unittests/test_apt_custom_sources_list.py
+++ b/tests/unittests/test_apt_custom_sources_list.py
@@ -93,7 +93,9 @@ class TestAptSourceConfigSourceList(CiTestCase):
     def setUp(self):
         super(TestAptSourceConfigSourceList, self).setUp()
         self.new_root = self.tmp_dir()
+        self.add_patch('curtin.util.subp', 'm_subp')
         # self.patchUtils(self.new_root)
+        self.m_subp.return_value = ("amd64", "")
 
     def _apt_source_list(self, cfg, expected):
         "_apt_source_list - Test rendering from template (generic)"
diff --git a/tests/unittests/test_apt_source.py b/tests/unittests/test_apt_source.py
index 353cdf8..6ae5579 100644
--- a/tests/unittests/test_apt_source.py
+++ b/tests/unittests/test_apt_source.py
@@ -75,6 +75,8 @@ class TestAptSourceConfig(CiTestCase):
         self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
         self.join = os.path.join
         self.matcher = re.compile(ADD_APT_REPO_MATCH).search
+        self.add_patch('curtin.util.subp', 'm_subp')
+        self.m_subp.return_value = ('s390x', '')
 
     @staticmethod
     def _add_apt_sources(*args, **kwargs):
diff --git a/tests/unittests/test_block.py b/tests/unittests/test_block.py
index e70503d..c62c153 100644
--- a/tests/unittests/test_block.py
+++ b/tests/unittests/test_block.py
@@ -1,9 +1,11 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 import functools
+import json
 import os
 import mock
 import sys
+import textwrap
 
 from collections import OrderedDict
 
@@ -85,6 +87,7 @@ class TestBlock(CiTestCase):
         mock_os_path_exists.return_value = True
         mock_os_path_realpath.return_value = "/dev/sda"
         mock_mpath.is_mpath_device.return_value = False
+        mock_mpath.is_mpath_member.return_value = False
 
         path = block.lookup_disk(serial)
 
@@ -120,6 +123,7 @@ class TestBlock(CiTestCase):
         mock_os_path_exists.return_value = True
         mock_os_path_realpath.return_value = device
         mock_mpath.is_mpath_device.return_value = False
+        mock_mpath.is_mpath_member.return_value = False
 
         path = block.lookup_disk(wwn)
 
@@ -440,21 +444,29 @@ class TestWipeVolume(CiTestCase):
 class TestBlockKnames(CiTestCase):
     """Tests for some of the kname functions in block"""
 
+    @mock.patch('curtin.block.os.path.realpath')
     @mock.patch('curtin.block.get_device_mapper_links')
-    def test_determine_partition_kname(self, m_mlink):
+    def test_determine_partition_kname(self, m_mlink, m_realp):
         dm0_link = '/dev/disk/by-id/dm-name-XXXX2406'
         m_mlink.return_value = dm0_link
+
+        # we need to convert the -part path to the real dm value
+        def _my_realp(pp):
+            if pp.startswith(dm0_link):
+                return 'dm-1'
+            return pp
+        m_realp.side_effect = _my_realp
         part_knames = [(('sda', 1), 'sda1'),
                        (('vda', 1), 'vda1'),
                        (('nvme0n1', 1), 'nvme0n1p1'),
                        (('mmcblk0', 1), 'mmcblk0p1'),
                        (('cciss!c0d0', 1), 'cciss!c0d0p1'),
-                       (('dm-0', 1), dm0_link + '-part1'),
+                       (('dm-0', 1),  'dm-1'),
                        (('md0', 1), 'md0p1'),
                        (('mpath1', 2), 'mpath1p2')]
         for ((disk_kname, part_number), part_kname) in part_knames:
-            self.assertEqual(block.partition_kname(disk_kname, part_number),
-                             part_kname)
+            self.assertEqual(part_kname,
+                             block.partition_kname(disk_kname, part_number))
 
     @mock.patch('curtin.block.os.path.realpath')
     def test_path_to_kname(self, mock_os_realpath):
@@ -512,6 +524,12 @@ class TestPartTableSignature(CiTestCase):
     gpt_content_4k = b'\x00' * 0x800 + b'EFI PART' + b'\x00' * (0x800 - 8)
     null_content = b'\x00' * 0xf00
 
+    def setUp(self):
+        super(TestPartTableSignature, self).setUp()
+        self.add_patch('curtin.util.subp', 'm_subp')
+        self.m_subp.side_effect = iter([
+            util.ProcessExecutionError(stdout="", stderr="", exit_code=1)])
+
     def _test_util_load_file(self, content, device, read_len, offset, decode):
         return (bytes if not decode else str)(content[offset:offset+read_len])
 
@@ -567,6 +585,15 @@ class TestPartTableSignature(CiTestCase):
                 (self.assertTrue if expected else self.assertFalse)(
                     block.check_efi_signature(self.blockdev))
 
+    def test_check_vtoc_signature_finds_vtoc_returns_true(self):
+        self.m_subp.side_effect = iter([("vtoc.....ok", "")])
+        self.assertTrue(block.check_vtoc_signature(self.blockdev))
+
+    def test_check_vtoc_signature_returns_false_with_no_sig(self):
+        self.m_subp.side_effect = iter([
+            util.ProcessExecutionError(stdout="", stderr="", exit_code=1)])
+        self.assertFalse(block.check_vtoc_signature(self.blockdev))
+
 
 class TestNonAscii(CiTestCase):
     @mock.patch('curtin.block.util.subp')
@@ -633,7 +660,7 @@ class TestSlaveKnames(CiTestCase):
         # construct side effects to os.path.exists
         # and os.listdir based on mapping.
         dirs = []
-        exists = []
+        exists = [True] if device.startswith('/dev') else []
         for (dev, slvs) in cfg.items():
             # sys_block_dev checks if dev exists
             exists.append(True)
@@ -771,4 +798,76 @@ class TestZkeySupported(CiTestCase):
         m_util.subp.assert_called_with(['zkey', 'generate', testname],
                                        capture=True)
 
+
+class TestSfdiskInfo(CiTestCase):
+
+    VALID_SFDISK_OUTPUT = textwrap.dedent("""\
+    {
+       "partitiontable": {
+          "label":"dos",
+          "id":"0xb0dbdde1",
+          "device":"/dev/vdb",
+          "unit":"sectors",
+          "partitions": [
+             {"node":"/dev/vdb1", "start":2048, "size":8388608,
+              "type":"83", "bootable":true},
+             {"node":"/dev/vdb2", "start":8390656, "size":8388608,
+              "type":"83"},
+             {"node":"/dev/vdb3", "start":16779264, "size":62914560,
+              "type":"85"},
+             {"node":"/dev/vdb5", "start":16781312, "size":31457280,
+              "type":"83"},
+             {"node":"/dev/vdb6", "start":48240640, "size":10485760,
+              "type":"83"},
+             {"node":"/dev/vdb7", "start":58728448, "size":20965376,
+              "type":"83"}
+          ]
+       }
+    }""")
+
+    def setUp(self):
+        super(TestSfdiskInfo, self).setUp()
+        self.add_patch('curtin.block.get_blockdev_for_partition',
+                       'm_get_blockdev_for_partition')
+        self.add_patch('curtin.block.util.subp', 'm_subp')
+        self.add_patch('curtin.block.util.load_json', 'm_load_json')
+        self.device = '/dev/vdb3'
+        self.disk = '/dev/vdb'
+        self.part = '3'
+        self.m_get_blockdev_for_partition.return_value = (self.disk, self.part)
+        self.m_subp.return_value = (self.VALID_SFDISK_OUTPUT, "")
+        self.loaded_json = json.loads(self.VALID_SFDISK_OUTPUT)
+        self.m_load_json.return_value = self.loaded_json
+        self.expected = self.loaded_json.get('partitiontable', {})
+
+    def test_sfdisk_info(self):
+        """verify sfdisk_info returns correct info dictionary for device."""
+        self.assertEqual(self.expected, block.sfdisk_info(self.device))
+        self.assertEqual(
+            [mock.call(self.device)],
+            self.m_get_blockdev_for_partition.call_args_list)
+        self.assertEqual(
+            [mock.call(['sfdisk', '--json', self.disk], capture=True)],
+            self.m_subp.call_args_list)
+        self.assertEqual(
+            [mock.call(self.m_subp.return_value[0])],
+            self.m_load_json.call_args_list)
+
+    def test_sfdisk_info_returns_empty_on_subp_error(self):
+        """verify sfdisk_info returns empty dict on subp errors."""
+        self.m_subp.side_effect = (
+            util.ProcessExecutionError(
+                stdout="",
+                stderr="sfdisk: cannot open /dev/vdb: Permission denied",
+                exit_code=1))
+        self.assertEqual({}, block.sfdisk_info(self.device))
+        self.assertEqual(
+            [mock.call(self.device)],
+            self.m_get_blockdev_for_partition.call_args_list)
+        self.assertEqual(
+            [mock.call(['sfdisk', '--json', self.disk], capture=True)],
+            self.m_subp.call_args_list)
+        self.assertEqual([], self.m_load_json.call_args_list)
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_block_lvm.py b/tests/unittests/test_block_lvm.py
index c92c1ec..ff58b30 100644
--- a/tests/unittests/test_block_lvm.py
+++ b/tests/unittests/test_block_lvm.py
@@ -76,7 +76,7 @@ class TestBlockLvm(CiTestCase):
     @mock.patch('curtin.block.lvm.distro')
     def test_lvm_scan(self, mock_distro, mock_util, mock_lvmetad):
         """check that lvm_scan formats commands correctly for each release"""
-        cmds = [['pvscan'], ['vgscan', '--mknodes']]
+        cmds = [['pvscan'], ['vgscan']]
         for (count, (codename, lvmetad_status, use_cache)) in enumerate(
                 [('precise', False, False),
                  ('trusty', False, False),
@@ -95,5 +95,35 @@ class TestBlockLvm(CiTestCase):
             mock_util.subp.has_calls(calls)
             mock_util.subp.reset_mock()
 
+    @mock.patch('curtin.block.lvm.lvmetad_running')
+    @mock.patch('curtin.block.lvm.util')
+    @mock.patch('curtin.block.lvm.distro')
+    def test_lvm_scan_multipath(self, mock_distro, mock_util, mock_lvmetad):
+        """check that lvm_scan formats commands correctly for multipath."""
+        cmds = [['pvscan'], ['vgscan']]
+        mock_distro.lsb_release.return_value = {'codename': 'focal'}
+        mock_lvmetad.return_value = False
+        lvm.lvm_scan(multipath=True)
+        cmd_filter = [
+            '--config',
+            'devices{ filter = [ "a|/dev/mapper/mpath.*|", "r|.*|" ] }'
+        ]
+        expected = [cmd + cmd_filter for cmd in cmds]
+        calls = [mock.call(cmd, capture=True) for cmd in expected]
+        self.assertEqual(len(expected), len(mock_util.subp.call_args_list))
+        mock_util.subp.has_calls(calls)
+
+
+class TestBlockLvmMultipathFilter(CiTestCase):
+
+    def test_generate_multipath_dev_mapper_filter(self):
+        expected = 'filter = [ "a|/dev/mapper/mpath.*|", "r|.*|" ]'
+        self.assertEqual(expected, lvm.generate_multipath_dev_mapper_filter())
+
+    def test_generate_multipath_dm_uuid_filter(self):
+        expected = (
+            'filter = [ "a|/dev/disk/by-id/dm-uuid-.*mpath-.*|", "r|.*|" ]')
+        self.assertEqual(expected, lvm.generate_multipath_dm_uuid_filter())
+
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_block_mdadm.py b/tests/unittests/test_block_mdadm.py
index e778871..dba0f74 100644
--- a/tests/unittests/test_block_mdadm.py
+++ b/tests/unittests/test_block_mdadm.py
@@ -758,18 +758,19 @@ class TestBlockMdadmMdHelpers(CiTestCase):
         with self.assertRaises(ValueError):
             mdadm.md_device_key_dev(devname)
 
+    @patch('curtin.block.util.load_file')
     @patch('curtin.block.get_blockdev_for_partition')
     @patch('curtin.block.mdadm.os.path.exists')
     @patch('curtin.block.mdadm.os.listdir')
     def tests_md_get_spares_list(self, mock_listdir, mock_exists,
-                                 mock_getbdev):
+                                 mock_getbdev, mock_load_file):
         mdname = '/dev/md0'
         devices = ['dev-vda', 'dev-vdb', 'dev-vdc']
         states = ['in-sync', 'in-sync', 'spare']
 
         mock_exists.return_value = True
         mock_listdir.return_value = devices
-        self.mock_util.load_file.side_effect = states
+        mock_load_file.side_effect = iter(states)
         mock_getbdev.return_value = ('md0', None)
 
         sysfs_path = '/sys/class/block/md0/md/'
@@ -779,7 +780,7 @@ class TestBlockMdadmMdHelpers(CiTestCase):
             expected_calls.append(call(os.path.join(sysfs_path, d, 'state')))
 
         spares = mdadm.md_get_spares_list(mdname)
-        self.mock_util.load_file.assert_has_calls(expected_calls)
+        mock_load_file.assert_has_calls(expected_calls)
         self.assertEqual(['/dev/vdc'], spares)
 
     @patch('curtin.block.get_blockdev_for_partition')
@@ -791,18 +792,19 @@ class TestBlockMdadmMdHelpers(CiTestCase):
         with self.assertRaises(OSError):
             mdadm.md_get_spares_list(mdname)
 
+    @patch('curtin.block.util.load_file')
     @patch('curtin.block.get_blockdev_for_partition')
     @patch('curtin.block.mdadm.os.path.exists')
     @patch('curtin.block.mdadm.os.listdir')
     def tests_md_get_devices_list(self, mock_listdir, mock_exists,
-                                  mock_getbdev):
+                                  mock_getbdev, mock_load_file):
         mdname = '/dev/md0'
         devices = ['dev-vda', 'dev-vdb', 'dev-vdc']
         states = ['in-sync', 'in-sync', 'spare']
 
         mock_exists.return_value = True
         mock_listdir.return_value = devices
-        self.mock_util.load_file.side_effect = states
+        mock_load_file.side_effect = states
         mock_getbdev.return_value = ('md0', None)
 
         sysfs_path = '/sys/class/block/md0/md/'
@@ -812,7 +814,7 @@ class TestBlockMdadmMdHelpers(CiTestCase):
             expected_calls.append(call(os.path.join(sysfs_path, d, 'state')))
 
         devs = mdadm.md_get_devices_list(mdname)
-        self.mock_util.load_file.assert_has_calls(expected_calls)
+        mock_load_file.assert_has_calls(expected_calls)
         self.assertEqual(sorted(['/dev/vda', '/dev/vdb']), sorted(devs))
 
     @patch('curtin.block.get_blockdev_for_partition')
diff --git a/tests/unittests/test_block_mkfs.py b/tests/unittests/test_block_mkfs.py
index 679f85b..f7acbd7 100644
--- a/tests/unittests/test_block_mkfs.py
+++ b/tests/unittests/test_block_mkfs.py
@@ -63,6 +63,15 @@ class TestBlockMkfs(CiTestCase):
                           ["-U", self.test_uuid]]
         self._run_mkfs_with_config(conf, "mkfs.ext4", expected_flags)
 
+    def test_mkfs_ext_with_extra_options(self):
+        conf = self._get_config("ext4")
+        extra_options = ["-D", "-e", "continue"
+                         "-E", "offset=1024,nodiscard,resize=10"]
+        conf['extra_options'] = extra_options
+        expected_flags = [["-L", "format1"], "-F",
+                          ["-U", self.test_uuid]] + extra_options
+        self._run_mkfs_with_config(conf, "mkfs.ext4", expected_flags)
+
     def test_mkfs_btrfs(self):
         conf = self._get_config("btrfs")
         expected_flags = [["--label", "format1"], "--force",
diff --git a/tests/unittests/test_block_multipath.py b/tests/unittests/test_block_multipath.py
index b0a8e32..2101eae 100644
--- a/tests/unittests/test_block_multipath.py
+++ b/tests/unittests/test_block_multipath.py
@@ -151,6 +151,7 @@ class TestMultipath(CiTestCase):
         paths = ['device=bar multipath=mpatha',
                  'device=wark multipath=mpatha']
         self.m_subp.return_value = ("\n".join(paths), "")
+
         self.assertEqual([], multipath.find_mpath_members(mp_id))
 
     def test_find_mpath_id(self):
@@ -226,5 +227,49 @@ class TestMultipath(CiTestCase):
         self.m_subp.return_value = ("\n".join(paths), "")
         self.assertIsNone(multipath.find_mpath_id_by_path('/dev/xxx'))
 
+    @mock.patch('curtin.block.multipath.util.del_file')
+    @mock.patch('curtin.block.multipath.os.path.islink')
+    @mock.patch('curtin.block.multipath.dmname_to_blkdev_mapping')
+    def test_force_devmapper_symlinks(self, m_blkmap, m_islink, m_del_file):
+        """ensure non-symlink for /dev/mapper/mpath* files are regenerated."""
+        m_blkmap.return_value = {
+            'mpatha': '/dev/dm-0',
+            'mpatha-part1': '/dev/dm-1',
+            '1gb zero': '/dev/dm-2',
+        }
+
+        m_islink.side_effect = iter([
+            False, False,  # mpatha, mpath-part1 are not links
+            True, True,    # mpatha, mpath-part1 are symlinks
+        ])
+
+        multipath.force_devmapper_symlinks()
+
+        udev = ['udevadm', 'trigger', '--subsystem-match=block',
+                '--action=add']
+        subp_expected_calls = [
+            mock.call(udev + ['/sys/class/block/dm-0']),
+            mock.call(udev + ['/sys/class/block/dm-1']),
+        ]
+        # sorted for py27, whee!
+        self.assertEqual(sorted(subp_expected_calls),
+                         sorted(self.m_subp.call_args_list))
+
+        islink_expected_calls = [
+            mock.call('/dev/mapper/mpatha'),
+            mock.call('/dev/mapper/mpatha-part1'),
+            mock.call('/dev/mapper/mpatha'),
+            mock.call('/dev/mapper/mpatha-part1'),
+        ]
+        self.assertEqual(sorted(islink_expected_calls),
+                         sorted(m_islink.call_args_list))
+
+        del_file_expected_calls = [
+            mock.call('/dev/mapper/mpatha'),
+            mock.call('/dev/mapper/mpatha-part1'),
+        ]
+        self.assertEqual(sorted(del_file_expected_calls),
+                         sorted(m_del_file.call_args_list))
+
 
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_block_zfs.py b/tests/unittests/test_block_zfs.py
index 3508d4b..e392000 100644
--- a/tests/unittests/test_block_zfs.py
+++ b/tests/unittests/test_block_zfs.py
@@ -468,6 +468,30 @@ class TestAssertZfsSupported(CiTestCase):
             with self.assertRaises(RuntimeError):
                 zfs.zfs_assert_supported()
 
+    @mock.patch('curtin.block.zfs.get_supported_filesystems')
+    @mock.patch('curtin.block.zfs.util.lsb_release')
+    @mock.patch('curtin.block.zfs.util.get_platform_arch')
+    @mock.patch('curtin.block.zfs.util')
+    def test_zfs_assert_supported_raises_exc_on_missing_binaries(self,
+                                                                 mock_util,
+                                                                 m_arch,
+                                                                 m_release,
+                                                                 m_supfs):
+        """zfs_assert_supported raises RuntimeError if no zpool or zfs tools"""
+        mock_util.get_platform_arch.return_value = 'amd64'
+        mock_util.lsb_release.return_value = {'codename': 'bionic'}
+        mock_util.subp.return_value = ("", "")
+        m_supfs.return_value = ['zfs']
+        mock_util.which.return_value = None
+
+        with self.assertRaises(RuntimeError):
+            zfs.zfs_assert_supported()
+
+
+class TestAssertZfsSupportedSubp(TestAssertZfsSupported):
+
+    allowed_subp = True
+
     @mock.patch('curtin.block.zfs.util.subprocess.Popen')
     @mock.patch('curtin.block.zfs.util.is_kmod_loaded')
     @mock.patch('curtin.block.zfs.get_supported_filesystems')
@@ -481,7 +505,6 @@ class TestAssertZfsSupported(CiTestCase):
                                                                m_popen,
                                                                ):
         """zfs_assert_supported raises RuntimeError modprobe zfs error"""
-
         m_arch.return_value = 'amd64'
         m_release.return_value = {'codename': 'bionic'}
         m_supfs.return_value = ['ext4']
@@ -497,25 +520,6 @@ class TestAssertZfsSupported(CiTestCase):
         with self.assertRaises(RuntimeError):
             zfs.zfs_assert_supported()
 
-    @mock.patch('curtin.block.zfs.get_supported_filesystems')
-    @mock.patch('curtin.block.zfs.util.lsb_release')
-    @mock.patch('curtin.block.zfs.util.get_platform_arch')
-    @mock.patch('curtin.block.zfs.util')
-    def test_zfs_assert_supported_raises_exc_on_missing_binaries(self,
-                                                                 mock_util,
-                                                                 m_arch,
-                                                                 m_release,
-                                                                 m_supfs):
-        """zfs_assert_supported raises RuntimeError if no zpool or zfs tools"""
-        mock_util.get_platform_arch.return_value = 'amd64'
-        mock_util.lsb_release.return_value = {'codename': 'bionic'}
-        mock_util.subp.return_value = ("", "")
-        m_supfs.return_value = ['zfs']
-        mock_util.which.return_value = None
-
-        with self.assertRaises(RuntimeError):
-            zfs.zfs_assert_supported()
-
 
 class TestZfsSupported(CiTestCase):
 
diff --git a/tests/unittests/test_clear_holders.py b/tests/unittests/test_clear_holders.py
index a91e003..25e9e79 100644
--- a/tests/unittests/test_clear_holders.py
+++ b/tests/unittests/test_clear_holders.py
@@ -305,14 +305,14 @@ class TestClearHolders(CiTestCase):
         self.assertTrue(mock_log.debug.called)
         self.assertTrue(mock_log.critical.called)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.os.path.exists')
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_wipe_superblock(self, mock_block, mock_log,
                                            mock_os_path, mock_swap,
-                                           mock_mp_bp):
+                                           mock_mp):
         """test clear_holders.wipe_superblock handles errors right"""
         mock_swap.return_value = False
         mock_os_path.return_value = False
@@ -320,7 +320,7 @@ class TestClearHolders(CiTestCase):
         mock_block.is_extended_partition.return_value = True
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
         clear_holders.wipe_superblock(self.test_syspath)
         self.assertFalse(mock_block.wipe_volume.called)
         mock_block.is_extended_partition.return_value = False
@@ -330,21 +330,21 @@ class TestClearHolders(CiTestCase):
         mock_block.wipe_volume.assert_called_with(
             self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_wipe_superblock_zfs(self, mock_block, mock_log,
                                                mock_zfs, mock_swap,
-                                               mock_mp_bp):
+                                               mock_mp):
         """test clear_holders.wipe_superblock handles zfs member"""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
         mock_block.is_extended_partition.return_value = True
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
         clear_holders.wipe_superblock(self.test_syspath)
         self.assertFalse(mock_block.wipe_volume.called)
         mock_block.is_extended_partition.return_value = False
@@ -358,21 +358,21 @@ class TestClearHolders(CiTestCase):
         mock_block.wipe_volume.assert_called_with(
             self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_wipe_superblock_no_zfs(self, mock_block, mock_log,
                                                   mock_zfs, mock_swap,
-                                                  mock_mp_bp):
+                                                  mock_mp):
         """test clear_holders.wipe_superblock checks zfs supported"""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
         mock_block.is_extended_partition.return_value = True
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
         clear_holders.wipe_superblock(self.test_syspath)
         self.assertFalse(mock_block.wipe_volume.called)
         mock_block.is_extended_partition.return_value = False
@@ -387,14 +387,16 @@ class TestClearHolders(CiTestCase):
         mock_block.wipe_volume.assert_called_with(
             self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.udev')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_wipe_superblock_zfs_no_utils(self, mock_block,
                                                         mock_log, mock_zfs,
-                                                        mock_swap, mock_mp_bp):
+                                                        mock_swap, mock_mp,
+                                                        mock_udev):
         """test clear_holders.wipe_superblock handles missing zpool cmd"""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
@@ -404,7 +406,7 @@ class TestClearHolders(CiTestCase):
         mock_block.is_extended_partition.return_value = False
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
         mock_block.is_zfs_member.return_value = True
         mock_zfs.zfs_supported.return_value = True
         mock_zfs.device_to_poolname.return_value = 'fake_pool'
@@ -419,27 +421,29 @@ class TestClearHolders(CiTestCase):
         mock_block.wipe_volume.assert_called_with(
             self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.time')
+    @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_wipe_superblock_rereads_pt(self, mock_block,
-                                                      mock_log, m_time,
-                                                      mock_swap, mock_mp_bp):
+                                                      mock_log, m_zfs, m_time,
+                                                      mock_swap, mock_mp):
         """test clear_holders.wipe_superblock re-reads partition table"""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
         mock_block.is_extended_partition.return_value = False
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
         mock_block.is_zfs_member.return_value = False
         mock_block.get_sysfs_partitions.side_effect = iter([
             ['p1', 'p2'],  # has partitions before wipe
             ['p1', 'p2'],  # still has partitions after wipe
             [],  # partitions are now gone
         ])
+        m_zfs.zfs_supported.return_value = True
         clear_holders.wipe_superblock(self.test_syspath)
         mock_block.sysfs_to_devpath.assert_called_with(self.test_syspath)
         mock_block.wipe_volume.assert_called_with(
@@ -449,7 +453,8 @@ class TestClearHolders(CiTestCase):
         mock_block.rescan_block_devices.assert_has_calls(
             [mock.call(devices=[self.test_blockdev])] * 2)
 
-    @mock.patch('curtin.block.multipath.find_mpath_id_by_path')
+    @mock.patch('curtin.block.clear_holders.zfs')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.time')
     @mock.patch('curtin.block.clear_holders.LOG')
@@ -457,14 +462,15 @@ class TestClearHolders(CiTestCase):
     def test_clear_holders_wipe_superblock_rereads_pt_oserr(self, mock_block,
                                                             mock_log, m_time,
                                                             mock_swap,
-                                                            mock_mp_bp):
+                                                            mock_mp, m_zfs):
         """test clear_holders.wipe_superblock re-reads ptable handles oserr"""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
         mock_block.is_extended_partition.return_value = False
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, None)
-        mock_mp_bp.return_value = None
+        mock_mp.multipath_supported.return_value = False
+        m_zfs.zfs_supported.return_value = False
         mock_block.is_zfs_member.return_value = False
         mock_block.get_sysfs_partitions.side_effect = iter([
             ['p1', 'p2'],  # has partitions before wipe
@@ -481,6 +487,7 @@ class TestClearHolders(CiTestCase):
             [mock.call(devices=[self.test_blockdev])] * 2)
         self.assertEqual(1, m_time.sleep.call_count)
 
+    @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.time')
@@ -489,8 +496,8 @@ class TestClearHolders(CiTestCase):
     def test_clear_holders_mp_enabled_not_active_wipes_dev(self, mock_block,
                                                            mock_log, m_time,
                                                            mock_swap,
-                                                           mock_mpath):
-        """wipe_superblock wipes dev with multipath enabled but inactive."""
+                                                           mock_mpath, m_zfs):
+        """wipe_superblock skips wiping multipath member path."""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
         mock_block.is_extended_partition.return_value = False
@@ -498,6 +505,7 @@ class TestClearHolders(CiTestCase):
         mock_mpath.find_mpath_id_by_path.return_value = None
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, 1)
+        m_zfs.zfs_supported.return_value = False
         mock_block.is_zfs_member.return_value = False
         mock_block.get_sysfs_partitions.side_effect = iter([
             ['p1', 'p2'],  # has partitions before wipe
@@ -506,9 +514,9 @@ class TestClearHolders(CiTestCase):
         ])
         clear_holders.wipe_superblock(self.test_syspath)
         mock_block.sysfs_to_devpath.assert_called_with(self.test_syspath)
-        mock_block.wipe_volume.assert_called_with(
-            self.test_blockdev, exclusive=True, mode='superblock', strict=True)
+        self.assertEqual(0, mock_block.wipe_volume.call_count)
 
+    @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
     @mock.patch('curtin.block.clear_holders.time')
@@ -516,7 +524,7 @@ class TestClearHolders(CiTestCase):
     @mock.patch('curtin.block.clear_holders.block')
     def test_clear_holders_mp_disabled_wipes_dev(self, mock_block, mock_log,
                                                  m_time, mock_swap,
-                                                 mock_mpath):
+                                                 mock_mpath, m_zfs):
         """wipe_superblock wipes blockdev with multipath disabled."""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
@@ -524,6 +532,7 @@ class TestClearHolders(CiTestCase):
         mock_mpath.multipath_supported.return_value = False
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, 1)
+        m_zfs.zfs_supported.return_value = False
         mock_block.is_zfs_member.return_value = False
         mock_block.get_sysfs_partitions.side_effect = iter([
             ['p1', 'p2'],  # has partitions before wipe
@@ -535,6 +544,7 @@ class TestClearHolders(CiTestCase):
         mock_block.wipe_volume.assert_called_with(
             self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
+    @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.get_holders')
     @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.is_swap_device')
@@ -545,32 +555,31 @@ class TestClearHolders(CiTestCase):
                                                               mock_log, m_time,
                                                               mock_swap,
                                                               mock_mpath,
-                                                              m_get_holders):
-        """wipe_superblock wipes parent mp_dev and removes from dev mapper."""
+                                                              m_get_holders,
+                                                              m_zfs):
+        """wipe_superblock wipes removes mp parts first and wipes dev."""
         mock_swap.return_value = False
         mock_block.sysfs_to_devpath.return_value = self.test_blockdev
+        m_zfs.zfs_supported.return_value = False
         mock_block.is_zfs_member.return_value = False
         mock_block.is_extended_partition.return_value = False
         mock_block.get_blockdev_for_partition.return_value = (
             self.test_blockdev, 1)
 
         mock_mpath.multipath_supported.return_value = True
-        mock_mpath.find_mpath_id_by_path.return_value = 'mpath-wark'
-        mp_dev = '/wark/dm-1'
-        parent_mpath_id = 'mpath-wark'
-        mock_mpath.find_mpath_id_by_parent.return_value = (
-            parent_mpath_id, mp_dev)
-        mock_mpath.is_mpath_partition.return_value = False
+        mock_mpath.is_mpath_device.return_value = True
+        mock_mpath.find_mpath_id_by_path.return_value = 'mpatha'
+        mock_mpath.find_mpath_partitions.return_value = ['mpatha-part1']
         mock_block.get_sysfs_partitions.side_effect = iter([
             [],  # partitions are now gone
         ])
+        mock_mpath.is_mpath_member.return_value = False
         m_get_holders.return_value = []
         clear_holders.wipe_superblock(self.test_syspath)
         mock_block.sysfs_to_devpath.assert_called_with(self.test_syspath)
+        mock_mpath.remove_partition.assert_called_with('mpatha-part1')
         mock_block.wipe_volume.assert_called_with(
-            mp_dev, exclusive=True, mode='superblock', strict=True)
-        mock_mpath.remove_map.assert_called_with(parent_mpath_id)
-        mock_mpath.remove_partition.assert_called_with(mp_dev)
+            self.test_blockdev, exclusive=True, mode='superblock', strict=True)
 
     @mock.patch('curtin.block.clear_holders.LOG')
     @mock.patch('curtin.block.clear_holders.block')
@@ -703,12 +712,14 @@ class TestClearHolders(CiTestCase):
         mock_gen_holders_tree.return_value = self.example_holders_trees[1][1]
         clear_holders.assert_clear(device)
 
+    @mock.patch('curtin.block.clear_holders.udev')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.lvm')
     @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.mdadm')
     @mock.patch('curtin.block.clear_holders.util')
     def test_start_clear_holders_deps(self, mock_util, mock_mdadm, mock_zfs,
-                                      mock_lvm):
+                                      mock_lvm, mock_mp, mock_udev):
         mock_zfs.zfs_supported.return_value = True
         clear_holders.start_clear_holders_deps()
         mock_mdadm.mdadm_assemble.assert_called_with(
@@ -716,12 +727,15 @@ class TestClearHolders(CiTestCase):
         mock_util.load_kernel_module.assert_has_calls([
                 mock.call('bcache')])
 
+    @mock.patch('curtin.block.clear_holders.udev')
+    @mock.patch('curtin.block.clear_holders.multipath')
     @mock.patch('curtin.block.clear_holders.lvm')
     @mock.patch('curtin.block.clear_holders.zfs')
     @mock.patch('curtin.block.clear_holders.mdadm')
     @mock.patch('curtin.block.clear_holders.util')
     def test_start_clear_holders_deps_nozfs(self, mock_util, mock_mdadm,
-                                            mock_zfs, mock_lvm):
+                                            mock_zfs, mock_lvm, mock_mp,
+                                            mock_udev):
         """test that we skip zfs modprobe on unsupported platforms"""
         mock_zfs.zfs_supported.return_value = False
         clear_holders.start_clear_holders_deps()
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index bc4f1cc..4cc9299 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -19,6 +19,8 @@ class TestGetPathToStorageVolume(CiTestCase):
         self.add_patch(basepath + 'os.path.exists', 'm_exists')
         self.add_patch(basepath + 'block.lookup_disk', 'm_lookup')
         self.add_patch(basepath + 'devsync', 'm_devsync')
+        self.add_patch(basepath + 'util.subp', 'm_subp')
+        self.add_patch(basepath + 'multipath.is_mpath_member', 'm_mp')
 
     def test_block_lookup_called_with_disk_wwn(self):
         volume = 'mydisk'
@@ -93,6 +95,8 @@ class TestGetPathToStorageVolume(CiTestCase):
             ValueError('Error'), ValueError('Error')])
         # no path
         self.m_exists.return_value = False
+        # not multipath
+        self.m_mp.return_value = False
 
         with self.assertRaises(ValueError):
             block_meta.get_path_to_storage_volume(volume, s_cfg)
@@ -148,7 +152,7 @@ class TestBlockMetaSimple(CiTestCase):
         self.mock_subp.assert_has_calls([call(args=wget),
                                          call(['partprobe', devnode]),
                                          call(['udevadm', 'settle'])])
-        paths = ["curtin", "system-data/var/lib/snapd"]
+        paths = ["curtin", "system-data/var/lib/snapd", "snaps"]
         self.mock_block_get_root_device.assert_called_with([devname],
                                                            paths=paths)
 
@@ -171,7 +175,7 @@ class TestBlockMetaSimple(CiTestCase):
         self.mock_subp.assert_has_calls([call(args=wget),
                                          call(['partprobe', devnode]),
                                          call(['udevadm', 'settle'])])
-        paths = ["curtin", "system-data/var/lib/snapd"]
+        paths = ["curtin", "system-data/var/lib/snapd", "snaps"]
         self.mock_block_get_root_device.assert_called_with([devname],
                                                            paths=paths)
 
@@ -212,6 +216,7 @@ class TestBlockMeta(CiTestCase):
         basepath = 'curtin.commands.block_meta.'
         self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath')
         self.add_patch(basepath + 'make_dname', 'mock_make_dname')
+        self.add_patch(basepath + 'multipath', 'm_mp')
         self.add_patch('curtin.util.load_command_environment',
                        'mock_load_env')
         self.add_patch('curtin.util.subp', 'mock_subp')
@@ -286,6 +291,10 @@ class TestBlockMeta(CiTestCase):
         self.storage_config = (
             block_meta.extract_storage_ordered_dict(self.config))
 
+        # mp off by default
+        self.m_mp.is_mpath_device.return_value = False
+        self.m_mp.is_mpath_member.return_value = False
+
     def test_disk_handler_calls_clear_holder(self):
         info = self.storage_config.get('sda')
         disk = info.get('path')
@@ -326,7 +335,8 @@ class TestBlockMeta(CiTestCase):
                                                      exclusive=False)
         self.mock_subp.assert_has_calls(
             [call(['parted', disk_kname, '--script',
-                   'mkpart', 'primary', '2048s', '1001471s'], capture=True)])
+                   'mkpart', 'primary', '2048s', '1001471s',
+                   'set', '1', 'boot', 'on'], capture=True)])
 
     @patch('curtin.util.write_file')
     def test_mount_handler_defaults(self, mock_write_file):
@@ -1212,15 +1222,189 @@ class TestDasdHandler(CiTestCase):
         self.assertEqual(0, m_dasd_format.call_count)
 
 
+class TestDiskHandler(CiTestCase):
+
+    with_logs = True
+
+    @patch('curtin.commands.block_meta.block')
+    @patch('curtin.commands.block_meta.util')
+    @patch('curtin.commands.block_meta.get_path_to_storage_volume')
+    def test_disk_handler_preserves_known_ptable(self, m_getpath, m_util,
+                                                 m_block):
+        storage_config = OrderedDict()
+        info = {'ptable': 'vtoc', 'serial': 'LX260B',
+                'preserve': True, 'name': '', 'grub_device': False,
+                'device_id': '0.0.260b', 'type': 'disk', 'id': 'disk-dasda'}
+
+        disk_path = "/wark/dasda"
+        m_getpath.return_value = disk_path
+        m_block.get_part_table_type.return_value = 'vtoc'
+        m_getpath.return_value = disk_path
+        block_meta.disk_handler(info, storage_config)
+        m_getpath.assert_called_with(info['id'], storage_config)
+        m_block.get_part_table_type.assert_called_with(disk_path)
+
+    @patch('curtin.commands.block_meta.block')
+    @patch('curtin.commands.block_meta.util')
+    @patch('curtin.commands.block_meta.get_path_to_storage_volume')
+    def test_disk_handler_allows_unsupported(self, m_getpath, m_util, m_block):
+        storage_config = OrderedDict()
+        info = {'ptable': 'unsupported', 'type': 'disk', 'id': 'disk-foobar',
+                'preserve': True, 'name': '', 'grub_device': False}
+
+        disk_path = "/wark/foobar"
+        m_getpath.return_value = disk_path
+        m_block.get_part_table_type.return_value = self.random_string()
+        m_getpath.return_value = disk_path
+        block_meta.disk_handler(info, storage_config)
+        m_getpath.assert_called_with(info['id'], storage_config)
+        self.assertEqual(0, m_block.get_part_table_type.call_count)
+
+    @patch('curtin.commands.block_meta.block')
+    @patch('curtin.commands.block_meta.util')
+    @patch('curtin.commands.block_meta.get_path_to_storage_volume')
+    def test_disk_handler_allows_no_ptable(self, m_getpath, m_util, m_block):
+        storage_config = OrderedDict()
+        info = {'type': 'disk', 'id': 'disk-foobar',
+                'preserve': True, 'name': '', 'grub_device': False}
+        self.assertNotIn('ptable', info)
+        disk_path = "/wark/foobar"
+        m_getpath.return_value = disk_path
+        m_block.get_part_table_type.return_value = 'gpt'
+        m_getpath.return_value = disk_path
+        block_meta.disk_handler(info, storage_config)
+        m_getpath.assert_called_with(info['id'], storage_config)
+        self.assertEqual(0, m_block.get_part_table_type.call_count)
+
+    @patch('curtin.commands.block_meta.block')
+    @patch('curtin.commands.block_meta.util')
+    @patch('curtin.commands.block_meta.get_path_to_storage_volume')
+    def test_disk_handler_errors_when_reading_current_ptable(self, m_getpath,
+                                                             m_util, m_block):
+        storage_config = OrderedDict()
+        info = {'ptable': 'gpt', 'type': 'disk', 'id': 'disk-foobar',
+                'preserve': True, 'name': '', 'grub_device': False}
+
+        disk_path = "/wark/foobar"
+        m_getpath.return_value = disk_path
+        m_block.get_part_table_type.return_value = None
+        m_getpath.return_value = disk_path
+        with self.assertRaises(ValueError):
+            block_meta.disk_handler(info, storage_config)
+        m_getpath.assert_called_with(info['id'], storage_config)
+        m_block.get_part_table_type.assert_called_with(disk_path)
+
+
+class TestLvmVolgroupHandler(CiTestCase):
+
+    def setUp(self):
+        super(TestLvmVolgroupHandler, self).setUp()
+
+        basepath = 'curtin.commands.block_meta.'
+        self.add_patch(basepath + 'lvm', 'm_lvm')
+        self.add_patch(basepath + 'util.subp', 'm_subp')
+        self.add_patch(basepath + 'make_dname', 'm_dname')
+        self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
+        self.add_patch(basepath + 'block.wipe_volume', 'm_wipe')
+
+        self.target = "my_target"
+        self.config = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {'id': 'wda2',
+                     'type': 'partition'},
+                    {'id': 'wdb2',
+                     'type': 'partition'},
+                    {'id': 'lvm-volgroup1',
+                     'type': 'lvm_volgroup',
+                     'name': 'vg1',
+                     'devices': ['wda2', 'wdb2']},
+                    {'id': 'lvm-part1',
+                     'type': 'lvm_partition',
+                     'name': 'lv1',
+                     'size': 1073741824,
+                     'volgroup': 'lvm-volgroup1'},
+                ],
+            }
+        }
+        self.storage_config = (
+            block_meta.extract_storage_ordered_dict(self.config))
+
+    def test_lvmvolgroup_creates_volume_group(self):
+        """ lvm_volgroup handler creates volume group. """
+
+        devices = [self.random_string(), self.random_string()]
+        self.m_getpath.side_effect = iter(devices)
+
+        block_meta.lvm_volgroup_handler(self.storage_config['lvm-volgroup1'],
+                                        self.storage_config)
+
+        self.assertEqual([call(['vgcreate', '--force', '--zero=y', '--yes',
+                                'vg1'] + devices,  capture=True)],
+                         self.m_subp.call_args_list)
+        self.assertEqual(1, self.m_lvm.lvm_scan.call_count)
+
+    @patch('curtin.commands.block_meta.lvm_volgroup_verify')
+    def test_lvmvolgroup_preserve_existing_volume_group(self, m_verify):
+        """ lvm_volgroup handler preserves existing volume group. """
+        m_verify.return_value = True
+        devices = [self.random_string(), self.random_string()]
+        self.m_getpath.side_effect = iter(devices)
+
+        self.storage_config['lvm-volgroup1']['preserve'] = True
+        block_meta.lvm_volgroup_handler(self.storage_config['lvm-volgroup1'],
+                                        self.storage_config)
+
+        self.assertEqual(0, self.m_subp.call_count)
+        self.assertEqual(1, self.m_lvm.lvm_scan.call_count)
+
+    def test_lvmvolgroup_preserve_verifies_volgroup_members(self):
+        """ lvm_volgroup handler preserves existing volume group. """
+        devices = [self.random_string(), self.random_string()]
+        self.m_getpath.side_effect = iter(devices)
+        self.m_lvm.get_pvols_in_volgroup.return_value = devices
+        self.storage_config['lvm-volgroup1']['preserve'] = True
+
+        block_meta.lvm_volgroup_handler(self.storage_config['lvm-volgroup1'],
+                                        self.storage_config)
+
+        self.assertEqual(1, self.m_lvm.activate_volgroups.call_count)
+        self.assertEqual([call('vg1')],
+                         self.m_lvm.get_pvols_in_volgroup.call_args_list)
+        self.assertEqual(0, self.m_subp.call_count)
+        self.assertEqual(1, self.m_lvm.lvm_scan.call_count)
+
+    def test_lvmvolgroup_preserve_raises_exception_wrong_pvs(self):
+        """ lvm_volgroup handler preserve raises execption on wrong pv devs."""
+        devices = [self.random_string(), self.random_string()]
+        self.m_getpath.side_effect = iter(devices)
+        self.m_lvm.get_pvols_in_volgroup.return_value = [self.random_string()]
+        self.storage_config['lvm-volgroup1']['preserve'] = True
+
+        with self.assertRaises(RuntimeError):
+            block_meta.lvm_volgroup_handler(
+                self.storage_config['lvm-volgroup1'], self.storage_config)
+
+        self.assertEqual(1, self.m_lvm.activate_volgroups.call_count)
+        self.assertEqual([call('vg1')],
+                         self.m_lvm.get_pvols_in_volgroup.call_args_list)
+        self.assertEqual(0, self.m_subp.call_count)
+        self.assertEqual(0, self.m_lvm.lvm_scan.call_count)
+
+
 class TestLvmPartitionHandler(CiTestCase):
 
     def setUp(self):
         super(TestLvmPartitionHandler, self).setUp()
 
-        self.add_patch('curtin.commands.block_meta.lvm', 'm_lvm')
-        self.add_patch('curtin.commands.block_meta.distro', 'm_distro')
-        self.add_patch('curtin.commands.block_meta.util.subp', 'm_subp')
-        self.add_patch('curtin.commands.block_meta.make_dname', 'm_dname')
+        basepath = 'curtin.commands.block_meta.'
+        self.add_patch(basepath + 'lvm', 'm_lvm')
+        self.add_patch(basepath + 'distro', 'm_distro')
+        self.add_patch(basepath + 'util.subp', 'm_subp')
+        self.add_patch(basepath + 'make_dname', 'm_dname')
+        self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
+        self.add_patch(basepath + 'block.wipe_volume', 'm_wipe')
 
         self.target = "my_target"
         self.config = {
@@ -1257,6 +1441,84 @@ class TestLvmPartitionHandler(CiTestCase):
         # call_args is an n-tuple of arg list
         self.assertIn(expected_size_str, call_args[0])
 
+    def test_lvmpart_wipes_volume_by_default(self):
+        """ lvm_partition_handler wipes superblock by default. """
+
+        self.m_distro.lsb_release.return_value = {'codename': 'bionic'}
+        devpath = self.random_string()
+        self.m_getpath.return_value = devpath
+
+        block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                         self.storage_config)
+        self.m_wipe.assert_called_with(devpath, mode='superblock',
+                                       exclusive=False)
+
+    def test_lvmpart_handles_wipe_setting(self):
+        """ lvm_partition_handler handles wipe settings. """
+
+        self.m_distro.lsb_release.return_value = {'codename': 'bionic'}
+        devpath = self.random_string()
+        self.m_getpath.return_value = devpath
+
+        wipe_mode = 'zero'
+        self.storage_config['lvm-part1']['wipe'] = wipe_mode
+        block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                         self.storage_config)
+        self.m_wipe.assert_called_with(devpath, mode=wipe_mode,
+                                       exclusive=False)
+
+    @patch('curtin.commands.block_meta.lvm_partition_verify')
+    def test_lvmpart_preserve_existing_lvmpart(self, m_verify):
+        m_verify.return_value = True
+        self.storage_config['lvm-part1']['preserve'] = True
+        block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                         self.storage_config)
+        self.assertEqual(0, self.m_distro.lsb_release.call_count)
+        self.assertEqual(0, self.m_subp.call_count)
+
+    def test_lvmpart_preserve_verifies_lv_in_vg_and_lv_size(self):
+        self.storage_config['lvm-part1']['preserve'] = True
+        self.m_lvm.get_lvols_in_volgroup.return_value = ['lv1']
+        self.m_lvm.get_lv_size_bytes.return_value = 1073741824.0
+
+        block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                         self.storage_config)
+        self.assertEqual([call('vg1')],
+                         self.m_lvm.get_lvols_in_volgroup.call_args_list)
+        self.assertEqual([call('lv1')],
+                         self.m_lvm.get_lv_size_bytes.call_args_list)
+        self.assertEqual(0, self.m_distro.lsb_release.call_count)
+        self.assertEqual(0, self.m_subp.call_count)
+
+    def test_lvmpart_preserve_fails_if_lv_not_in_vg(self):
+        self.storage_config['lvm-part1']['preserve'] = True
+        self.m_lvm.get_lvols_in_volgroup.return_value = []
+
+        with self.assertRaises(RuntimeError):
+            block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                             self.storage_config)
+
+            self.assertEqual([call('vg1')],
+                             self.m_lvm.get_lvols_in_volgroup.call_args_list)
+        self.assertEqual(0, self.m_lvm.get_lv_size_bytes.call_count)
+        self.assertEqual(0, self.m_distro.lsb_release.call_count)
+        self.assertEqual(0, self.m_subp.call_count)
+
+    def test_lvmpart_preserve_verifies_lv_size_matches(self):
+        self.storage_config['lvm-part1']['preserve'] = True
+        self.m_lvm.get_lvols_in_volgroup.return_value = ['lv1']
+        self.m_lvm.get_lv_size_bytes.return_value = 0.0
+
+        with self.assertRaises(RuntimeError):
+            block_meta.lvm_partition_handler(self.storage_config['lvm-part1'],
+                                             self.storage_config)
+            self.assertEqual([call('vg1')],
+                             self.m_lvm.get_lvols_in_volgroup.call_args_list)
+            self.assertEqual([call('lv1')],
+                             self.m_lvm.get_lv_size_bytes.call_args_list)
+        self.assertEqual(0, self.m_distro.lsb_release.call_count)
+        self.assertEqual(0, self.m_subp.call_count)
+
 
 class TestDmCryptHandler(CiTestCase):
 
@@ -1328,6 +1590,24 @@ class TestDmCryptHandler(CiTestCase):
         self.m_subp.assert_has_calls(expected_calls)
         self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
 
+    def test_dm_crypt_defaults_dm_name_to_id(self):
+        """ verify dm_crypt_handler falls back to id with no dm_name. """
+        volume_path = self.random_string()
+        self.m_getpath.return_value = volume_path
+        info = self.storage_config['dmcrypt0']
+        del info['dm_name']
+
+        block_meta.dm_crypt_handler(info, self.storage_config)
+        expected_calls = [
+            call(['cryptsetup', '--cipher', self.cipher,
+                  '--key-size', self.keysize,
+                  'luksFormat', volume_path, self.keyfile]),
+            call(['cryptsetup', 'open', '--type', 'luks', volume_path,
+                  info['id'], '--key-file', self.keyfile])
+        ]
+        self.m_subp.assert_has_calls(expected_calls)
+        self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
+
     def test_dm_crypt_zkey_cryptsetup(self):
         """ verify dm_crypt zkey calls generates and run before crypt open."""
 
@@ -1431,6 +1711,317 @@ class TestDmCryptHandler(CiTestCase):
         self.m_subp.assert_has_calls(expected_calls)
         self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
 
+    @patch('curtin.commands.block_meta.dm_crypt_verify')
+    def test_dm_crypt_preserves_existing(self, m_verify):
+        """ verify dm_crypt preserves existing device. """
+        m_verify.return_value = True
+        volume_path = self.random_string()
+        self.m_getpath.return_value = volume_path
+
+        info = self.storage_config['dmcrypt0']
+        info['preserve'] = True
+        block_meta.dm_crypt_handler(info, self.storage_config)
+
+        self.assertEqual(0, self.m_subp.call_count)
+        self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
+
+    @patch('curtin.commands.block_meta.os.path.exists')
+    def test_dm_crypt_preserve_verifies_correct_device_is_present(self, m_ex):
+        """ verify dm_crypt preserve verifies correct dev is used. """
+        volume_path = self.random_string()
+        self.m_getpath.return_value = volume_path
+        self.m_block.dmsetup_info.return_value = {
+            'blkdevname': 'dm-0',
+            'blkdevs_used': volume_path,
+            'name': 'cryptroot',
+            'uuid': self.random_string(),
+            'subsystem': 'crypt'
+        }
+        m_ex.return_value = True
+
+        info = self.storage_config['dmcrypt0']
+        info['preserve'] = True
+        block_meta.dm_crypt_handler(info, self.storage_config)
+        self.assertEqual(len(util.load_file(self.crypttab).splitlines()), 1)
+
+    @patch('curtin.commands.block_meta.os.path.exists')
+    def test_dm_crypt_preserve_raises_exception_if_not_present(self, m_ex):
+        """ verify dm_crypt raises exception if dm device not present. """
+        volume_path = self.random_string()
+        self.m_getpath.return_value = volume_path
+        m_ex.return_value = False
+        info = self.storage_config['dmcrypt0']
+        info['preserve'] = True
+        with self.assertRaises(RuntimeError):
+            block_meta.dm_crypt_handler(info, self.storage_config)
+
+    @patch('curtin.commands.block_meta.os.path.exists')
+    def test_dm_crypt_preserve_raises_exception_if_wrong_dev_used(self, m_ex):
+        """ verify dm_crypt preserve raises exception on wrong dev used. """
+        volume_path = self.random_string()
+        self.m_getpath.return_value = volume_path
+        self.m_block.dmsetup_info.return_value = {
+            'blkdevname': 'dm-0',
+            'blkdevs_used': self.random_string(),
+            'name': 'cryptroot',
+            'uuid': self.random_string(),
+            'subsystem': 'crypt'
+        }
+        m_ex.return_value = True
+        info = self.storage_config['dmcrypt0']
+        info['preserve'] = True
+        with self.assertRaises(RuntimeError):
+            block_meta.dm_crypt_handler(info, self.storage_config)
+
+
+class TestRaidHandler(CiTestCase):
+
+    def setUp(self):
+        super(TestRaidHandler, self).setUp()
+
+        basepath = 'curtin.commands.block_meta.'
+        self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
+        self.add_patch(basepath + 'util', 'm_util')
+        self.add_patch(basepath + 'make_dname', 'm_dname')
+        self.add_patch(basepath + 'mdadm', 'm_mdadm')
+        self.add_patch(basepath + 'block', 'm_block')
+        self.add_patch(basepath + 'udevadm_settle', 'm_uset')
+
+        self.target = "my_target"
+        self.config = {
+            'storage': {
+                 'version': 1,
+                 'config': [
+                        {'grub_device': 1,
+                         'id': 'sda',
+                         'model': 'QEMU HARDDISK',
+                         'name': 'main_disk',
+                         'ptable': 'gpt',
+                         'serial': 'disk-a',
+                         'type': 'disk',
+                         'wipe': 'superblock'},
+                        {'device': 'sda',
+                         'flag': 'bios_grub',
+                         'id': 'bios_boot_partition',
+                         'size': '1MB',
+                         'type': 'partition'},
+                        {'device': 'sda',
+                         'id': 'sda1',
+                         'size': '3GB',
+                         'type': 'partition'},
+                        {'id': 'sdb',
+                         'model': 'QEMU HARDDISK',
+                         'name': 'second_disk',
+                         'ptable': 'gpt',
+                         'serial': 'disk-b',
+                         'type': 'disk',
+                         'wipe': 'superblock'},
+                        {'device': 'sdb',
+                         'id': 'sdb1',
+                         'size': '3GB',
+                         'type': 'partition'},
+                        {'id': 'sdc',
+                         'model': 'QEMU HARDDISK',
+                         'name': 'third_disk',
+                         'ptable': 'gpt',
+                         'serial': 'disk-c',
+                         'type': 'disk',
+                         'wipe': 'superblock'},
+                        {'device': 'sdc',
+                         'id': 'sdc1',
+                         'size': '3GB',
+                         'type': 'partition'},
+                        {'devices': ['sda1', 'sdb1', 'sdc1'],
+                         'id': 'mddevice',
+                         'name': 'md0',
+                         'raidlevel': 5,
+                         'type': 'raid'},
+                        {'fstype': 'ext4',
+                         'id': 'md_root',
+                         'type': 'format',
+                         'volume': 'mddevice'},
+                        {'device': 'md_root',
+                         'id': 'md_mount',
+                         'path': '/',
+                         'type': 'mount'}],
+            },
+        }
+        self.storage_config = (
+            block_meta.extract_storage_ordered_dict(self.config))
+        self.m_util.load_command_environment.return_value = {'fstab': None}
+
+    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)
+        self.assertEqual([call(md_devname, 5, devices, [], '')],
+                         self.m_mdadm.mdadm_create.call_args_list)
+
+    @patch('curtin.commands.block_meta.raid_verify')
+    def test_raid_handler_preserves_existing_device(self, m_verify):
+        """ raid_handler preserves existing device. """
+
+        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
+        block_meta.raid_handler(self.storage_config['mddevice'],
+                                self.storage_config)
+        self.assertEqual(0, self.m_mdadm.mdadm_create.call_count)
+
+    def test_raid_handler_preserve_verifies_md_device(self):
+        """ raid_handler preserve verifies existing 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)
+        self.m_mdadm.md_check.return_value = True
+        self.storage_config['mddevice']['preserve'] = True
+        block_meta.raid_handler(self.storage_config['mddevice'],
+                                self.storage_config)
+        self.assertEqual(0, self.m_mdadm.mdadm_create.call_count)
+        self.assertEqual([call(md_devname, 5, devices, [])],
+                         self.m_mdadm.md_check.call_args_list)
+
+    def test_raid_handler_preserve_verifies_md_device_after_assemble(self):
+        """ raid_handler preserve assembles array if first check fails. """
+
+        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
+        block_meta.raid_handler(self.storage_config['mddevice'],
+                                self.storage_config)
+        self.assertEqual(0, self.m_mdadm.mdadm_create.call_count)
+        self.assertEqual([call(md_devname, 5, devices, [])] * 2,
+                         self.m_mdadm.md_check.call_args_list)
+        self.assertEqual([call(md_devname, devices, [])],
+                         self.m_mdadm.mdadm_assemble.call_args_list)
+
+    def test_raid_handler_preserve_raises_exception_if_verify_fails(self):
+        """ raid_handler preserve raises exception on failed verification."""
+
+        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
+        with self.assertRaises(RuntimeError):
+            block_meta.raid_handler(self.storage_config['mddevice'],
+                                    self.storage_config)
+        self.assertEqual(0, self.m_mdadm.mdadm_create.call_count)
+        self.assertEqual([call(md_devname, 5, devices, [])] * 2,
+                         self.m_mdadm.md_check.call_args_list)
+        self.assertEqual([call(md_devname, devices, [])],
+                         self.m_mdadm.mdadm_assemble.call_args_list)
+
+
+class TestBcacheHandler(CiTestCase):
+
+    def setUp(self):
+        super(TestBcacheHandler, self).setUp()
+
+        basepath = 'curtin.commands.block_meta.'
+        self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
+        self.add_patch(basepath + 'util', 'm_util')
+        self.add_patch(basepath + 'make_dname', 'm_dname')
+        self.add_patch(basepath + 'bcache', 'm_bcache')
+        self.add_patch(basepath + 'block', 'm_block')
+        self.add_patch(basepath + 'disk_handler', 'm_disk_handler')
+
+        self.target = "my_target"
+        self.config = {
+            'storage': {
+                 'version': 1,
+                 'config': [
+                    {'grub_device': True,
+                     'id': 'id_rotary0',
+                     'name': 'rotary0',
+                     'ptable': 'msdos',
+                     'serial': 'disk-a',
+                     'type': 'disk',
+                     'wipe': 'superblock'},
+                    {'id': 'id_ssd0',
+                     'name': 'ssd0',
+                     'serial': 'disk-b',
+                     'type': 'disk',
+                     'wipe': 'superblock'},
+                    {'device': 'id_rotary0',
+                     'id': 'id_rotary0_part1',
+                     'name': 'rotary0-part1',
+                     'number': 1,
+                     'offset': '1M',
+                     'size': '999M',
+                     'type': 'partition',
+                     'wipe': 'superblock'},
+                    {'device': 'id_rotary0',
+                     'id': 'id_rotary0_part2',
+                     'name': 'rotary0-part2',
+                     'number': 2,
+                     'size': '9G',
+                     'type': 'partition',
+                     'wipe': 'superblock'},
+                    {'backing_device': 'id_rotary0_part2',
+                     'cache_device': 'id_ssd0',
+                     'cache_mode': 'writeback',
+                     'id': 'id_bcache0',
+                     'name': 'bcache0',
+                     'type': 'bcache'},
+                    {'fstype': 'ext4',
+                     'id': 'bootfs',
+                     'label': 'boot-fs',
+                     'type': 'format',
+                     'volume': 'id_rotary0_part1'},
+                    {'fstype': 'ext4',
+                     'id': 'rootfs',
+                     'label': 'root-fs',
+                     'type': 'format',
+                     'volume': 'id_bcache0'},
+                    {'device': 'rootfs',
+                     'id': 'rootfs_mount',
+                     'path': '/',
+                     'type': 'mount'},
+                    {'device': 'bootfs',
+                     'id': 'bootfs_mount',
+                     'path': '/boot',
+                     'type': 'mount'}
+                 ],
+            },
+        }
+        self.storage_config = (
+            block_meta.extract_storage_ordered_dict(self.config))
+
+    def test_bcache_handler(self):
+        """ bcache_handler creates bcache device. """
+        backing_device = self.random_string()
+        caching_device = self.random_string()
+        cset_uuid = self.random_string()
+        cache_mode = self.storage_config['id_bcache0']['cache_mode']
+        self.m_getpath.side_effect = iter([backing_device, caching_device])
+        self.m_bcache.create_cache_device.return_value = cset_uuid
+
+        block_meta.bcache_handler(self.storage_config['id_bcache0'],
+                                  self.storage_config)
+        self.assertEqual([call(caching_device)],
+                         self.m_bcache.create_cache_device.call_args_list)
+        self.assertEqual([
+            call(backing_device, caching_device, cache_mode, cset_uuid)],
+                         self.m_bcache.create_backing_device.call_args_list)
+
 
 class TestPartitionHandler(CiTestCase):
 
@@ -1442,7 +2033,9 @@ class TestPartitionHandler(CiTestCase):
         self.add_patch(basepath + 'util', 'm_util')
         self.add_patch(basepath + 'make_dname', 'm_dname')
         self.add_patch(basepath + 'block', 'm_block')
+        self.add_patch(basepath + 'multipath', 'm_mp')
         self.add_patch(basepath + 'udevadm_settle', 'm_uset')
+        self.add_patch(basepath + 'udevadm_info', 'm_uinfo')
 
         self.target = "my_target"
         self.config = {
@@ -1568,4 +2161,289 @@ class TestPartitionHandler(CiTestCase):
             block_meta.partition_handler(logical_part, self.storage_config)
 
 
+class TestMultipathPartitionHandler(CiTestCase):
+
+    def setUp(self):
+        super(TestMultipathPartitionHandler, self).setUp()
+
+        basepath = 'curtin.commands.block_meta.'
+        self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
+        self.add_patch(basepath + 'util', 'm_util')
+        self.add_patch(basepath + 'make_dname', 'm_dname')
+        self.add_patch(basepath + 'block', 'm_block')
+        self.add_patch(basepath + 'multipath', 'm_mp')
+        self.add_patch(basepath + 'udevadm_settle', 'm_uset')
+        self.add_patch(basepath + 'udevadm_info', 'm_uinfo')
+
+        self.target = self.tmp_dir()
+        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.storage_config = (
+            block_meta.extract_storage_ordered_dict(self.config))
+
+    @patch('curtin.commands.block_meta.calc_partition_info')
+    def test_part_handler_uses_kpartx_on_multipath_parts(self, m_part_info):
+
+        # dm-0 is mpatha, dm-1 is mpatha-part1, dm-2 is mpatha-part2
+        disk_path = '/wark/mapper/mpatha'
+        self.m_getpath.return_value = disk_path
+        self.m_block.path_to_kname.return_value = 'dm-0'
+        self.m_block.sys_block_path.return_value = 'sys/class/block/dm-0'
+        self.m_block.get_blockdev_sector_size.return_value = (512, 512)
+        self.m_block.partition_kname.return_value = 'dm-2'
+        self.m_mp.is_mpath_device.return_value = True
+
+        # prev_start_sec, prev_size_sec
+        m_part_info.return_value = (2048, 2048)
+
+        part2 = self.storage_config['disk-sda-part-2']
+        block_meta.partition_handler(part2, self.storage_config)
+
+        expected_calls = [
+            call(['sgdisk', '--new', '2:4096:4096', '--typecode=2:8300',
+                  disk_path], capture=True),
+            call(['kpartx', '-v', '-a', '-s', '-p', '-part', disk_path]),
+        ]
+        self.assertEqual(expected_calls, self.m_util.subp.call_args_list)
+
+    @patch('curtin.commands.block_meta.os.path')
+    @patch('curtin.commands.block_meta.calc_partition_info')
+    def test_part_handler_deleted__non_symlink_before_kpartx(self,
+                                                             m_part_info,
+                                                             m_os_path):
+        # dm-0 is mpatha, dm-1 is mpatha-part1, dm-2 is mpatha-part2
+        disk_path = '/wark/mapper/mpatha'
+        self.m_getpath.return_value = disk_path
+        self.m_block.path_to_kname.return_value = 'dm-0'
+        self.m_block.sys_block_path.return_value = 'sys/class/block/dm-0'
+        self.m_block.get_blockdev_sector_size.return_value = (512, 512)
+        self.m_block.partition_kname.return_value = 'dm-2'
+        self.m_mp.is_mpath_device.return_value = True
+        m_os_path.exists.return_value = True
+        m_os_path.islink.return_value = False
+
+        # prev_start_sec, prev_size_sec
+        m_part_info.return_value = (2048, 2048)
+
+        part2 = self.storage_config['disk-sda-part-2']
+        block_meta.partition_handler(part2, self.storage_config)
+
+        expected_calls = [
+            call(['sgdisk', '--new', '2:4096:4096', '--typecode=2:8300',
+                  disk_path], capture=True),
+            call(['kpartx', '-v', '-a', '-s', '-p', '-part', disk_path]),
+        ]
+        self.assertEqual(expected_calls, self.m_util.subp.call_args_list)
+        self.assertEqual([call(disk_path + '-part2')],
+                         self.m_util.del_file.call_args_list)
+
+
+class TestCalcPartitionInfo(CiTestCase):
+
+    def setUp(self):
+        super(TestCalcPartitionInfo, self).setUp()
+        self.add_patch('curtin.commands.block_meta.util.load_file',
+                       'm_load_file')
+
+    def _prepare_load_file_mocks(self, start, size, logsize):
+        partition_size = str(int(size / logsize))
+        partition_start = str(int(start / logsize))
+        self.m_load_file.side_effect = iter([partition_size, partition_start])
+
+    def test_calc_partition_info(self):
+        disk = self.random_string()
+        partition = self.random_string()
+        part_path = os.path.join(disk, partition)
+        part_size = 10 * 1024 * 1024
+        part_start = 1 * 1024 * 1024
+        blk_size = 512
+        self._prepare_load_file_mocks(part_start, part_size, blk_size)
+
+        (start, size) = block_meta.calc_partition_info(
+            disk, partition, blk_size)
+
+        self.assertEqual(part_start / blk_size, start)
+        self.assertEqual(part_size / blk_size, size)
+        self.assertEqual(
+            [call(part_path + '/size'), call(part_path + '/start')],
+            self.m_load_file.call_args_list)
+
+    @patch('curtin.commands.block_meta.calc_dm_partition_info')
+    def test_calc_partition_info_dm_part(self, m_calc_dm):
+        disk = self.random_string()
+        partition = 'dm-237'
+        part_size = 10 * 1024 * 1024
+        part_start = 1 * 1024 * 1024
+        blk_size = 512
+        m_calc_dm.return_value = (part_start / blk_size, part_size / blk_size)
+
+        (start, size) = block_meta.calc_partition_info(
+            disk, partition, blk_size)
+
+        self.assertEqual(part_start / blk_size, start)
+        self.assertEqual(part_size / blk_size, size)
+        self.assertEqual([call(partition)], m_calc_dm.call_args_list)
+        self.assertEqual([], self.m_load_file.call_args_list)
+
+    @patch('curtin.commands.block_meta.calc_dm_partition_info')
+    def test_calc_partition_info_none_start_sec_raise_exc(self, m_calc_dm):
+        disk = self.random_string()
+        partition = 'dm-237'
+        blk_size = 512
+        m_calc_dm.return_value = (None, None)
+
+        with self.assertRaises(RuntimeError):
+            block_meta.calc_partition_info(disk, partition, blk_size)
+
+        self.assertEqual([call(partition)], m_calc_dm.call_args_list)
+        self.assertEqual([], self.m_load_file.call_args_list)
+
+
+class TestCalcDMPartitionInfo(CiTestCase):
+
+    def setUp(self):
+        super(TestCalcDMPartitionInfo, self).setUp()
+        self.add_patch('curtin.commands.block_meta.multipath', 'm_mp')
+        self.add_patch('curtin.commands.block_meta.util.subp', 'm_subp')
+
+        self.mpath_id = 'mpath%s-part1' % self.random_string(length=1)
+        self.m_mp.get_mpath_id_from_device.return_value = self.mpath_id
+
+    def test_calc_dm_partition_info_raises_exc_no_mpath_id(self):
+        self.m_mp.get_mpath_id_from_device.return_value = None
+        with self.assertRaises(RuntimeError):
+            block_meta.calc_dm_partition_info(self.random_string())
+
+    def test_calc_dm_partition_info_return_none_with_no_dmsetup_output(self):
+        self.m_subp.return_value = ("", "")
+        self.assertEqual(
+            (None, None),
+            block_meta.calc_dm_partition_info(self.random_string()))
+
+    def test_calc_dm_partition_info_calls_dmsetup_table(self):
+        partition = 'dm-245'
+        dm_part = '/dev/' + partition
+        self.m_subp.return_value = ("0 20480 linear 253:0 2048", "")
+        (start, size) = block_meta.calc_dm_partition_info(partition)
+        self.assertEqual(2048, start)
+        self.assertEqual(20480, size)
+        self.assertEqual(
+            [call(dm_part)],
+            self.m_mp.get_mpath_id_from_device.call_args_list)
+        self.assertEqual([
+            call(['dmsetup', 'table', '--target', 'linear', self.mpath_id],
+                 capture=True)],
+            self.m_subp.call_args_list)
+
+
+class TestPartitionVerify(CiTestCase):
+
+    def setUp(self):
+        super(TestPartitionVerify, self).setUp()
+        base = 'curtin.commands.block_meta.'
+        self.add_patch(base + 'verify_exists', 'm_verify_exists')
+        self.add_patch(base + 'block.sfdisk_info', 'm_block_sfdisk_info')
+        self.add_patch(base + 'verify_size', 'm_verify_size')
+        self.add_patch(base + 'verify_ptable_flag', 'm_verify_ptable_flag')
+        self.info = {
+            'id': 'disk-sda-part-2',
+            'type': 'partition',
+            'device': 'sda',
+            'number': 2,
+            'size': '5GB',
+            'flag': 'boot',
+        }
+        self.part_size = int(util.human2bytes(self.info['size']))
+        self.devpath = self.random_string()
+
+    def test_partition_verify(self):
+        block_meta.partition_verify(self.devpath, self.info)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_verify_exists.call_args_list)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_block_sfdisk_info.call_args_list)
+        self.assertEqual(
+            [call(self.devpath, self.part_size,
+                  sfdisk_info=self.m_block_sfdisk_info.return_value)],
+            self.m_verify_size.call_args_list)
+        self.assertEqual(
+            [call(self.devpath, self.info['flag'],
+                  sfdisk_info=self.m_block_sfdisk_info.return_value)],
+            self.m_verify_ptable_flag.call_args_list)
+
+    def test_partition_verify_skips_ptable_no_flag(self):
+        del self.info['flag']
+        block_meta.partition_verify(self.devpath, self.info)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_verify_exists.call_args_list)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_block_sfdisk_info.call_args_list)
+        self.assertEqual(
+            [call(self.devpath, self.part_size,
+                  sfdisk_info=self.m_block_sfdisk_info.return_value)],
+            self.m_verify_size.call_args_list)
+        self.assertEqual([], self.m_verify_ptable_flag.call_args_list)
+
+
+class TestVerifyExists(CiTestCase):
+
+    def setUp(self):
+        super(TestVerifyExists, self).setUp()
+        base = 'curtin.commands.block_meta.'
+        self.add_patch(base + 'os.path.exists', 'm_exists')
+        self.devpath = self.random_string()
+        self.m_exists.return_value = True
+
+    def test_verify_exists(self):
+        block_meta.verify_exists(self.devpath)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_exists.call_args_list)
+
+    def test_verify_exists_raise_runtime_exc_if_path_not_exist(self):
+        self.m_exists.return_value = False
+        with self.assertRaises(RuntimeError):
+            block_meta.verify_exists(self.devpath)
+        self.assertEqual(
+            [call(self.devpath)],
+            self.m_exists.call_args_list)
+
+
+class TestVerifySize(CiTestCase):
+
+    def setUp(self):
+        super(TestVerifySize, self).setUp()
+        base = 'curtin.commands.block_meta.'
+        self.add_patch(base + 'block.sfdisk_info', 'm_block_sfdisk_info')
+        self.add_patch(base + 'block.get_partition_sfdisk_info',
+                       'm_block_get_partition_sfdisk_info')
+        self.devpath = self.random_string()
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_commands_clear_holders.py b/tests/unittests/test_commands_clear_holders.py
new file mode 100644
index 0000000..28fab35
--- /dev/null
+++ b/tests/unittests/test_commands_clear_holders.py
@@ -0,0 +1,24 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from curtin.commands import clear_holders
+from .helpers import CiTestCase
+
+import argparse
+
+
+class TestClearHolders(CiTestCase):
+
+    def setUp(self):
+        super(TestClearHolders, self).setUp()
+        self.add_patch('curtin.block.clear_holders', 'm_clear_holders')
+        self.add_patch('curtin.block', 'm_block')
+
+    def test_argument_parsing_devices_is_dict(self):
+        argv = ['/dev/disk/vda1']
+        parser = argparse.ArgumentParser()
+        clear_holders.POPULATE_SUBCMD(parser)
+        args = parser.parse_args(argv)
+        self.assertEqual(list, type(args.devices))
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_commands_collect_logs.py b/tests/unittests/test_commands_collect_logs.py
index 1feba18..b5df902 100644
--- a/tests/unittests/test_commands_collect_logs.py
+++ b/tests/unittests/test_commands_collect_logs.py
@@ -288,6 +288,7 @@ class TestCreateTar(CiTestCase):
 
         Configured log_file or post_files which don't exist are ignored.
         """
+        self.add_patch('curtin.util.subp', 'mock_subp')
         tarfile = self.tmp_path('my.tar', _dir=self.new_root)
         log1 = self.tmp_path('some.log', _dir=self.new_root)
         write_file(log1, 'log content')
@@ -314,6 +315,7 @@ class TestCreateTar(CiTestCase):
 
     def test_create_log_tarfile_redacts_maas_credentials(self):
         """create_log_tarfile redacts sensitive maas credentials configured."""
+        self.add_patch('curtin.util.subp', 'mock_subp')
         tarfile = self.tmp_path('my.tar', _dir=self.new_root)
         self.add_patch(
             'curtin.commands.collect_logs._redact_sensitive_information',
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index ff38240..c126f3a 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -5,6 +5,7 @@ from mock import call, patch, MagicMock
 import textwrap
 
 from curtin.commands import curthooks
+from curtin.commands.block_meta import extract_storage_ordered_dict
 from curtin import distro
 from curtin import util
 from curtin import config
@@ -195,7 +196,9 @@ class TestUpdateInitramfs(CiTestCase):
         super(TestUpdateInitramfs, self).setUp()
         self.add_patch('curtin.util.subp', 'mock_subp')
         self.add_patch('curtin.util.which', 'mock_which')
+        self.add_patch('curtin.util.is_uefi_bootable', 'mock_uefi')
         self.mock_which.return_value = self.random_string()
+        self.mock_uefi.return_value = False
         self.target = self.tmp_dir()
         self.boot = os.path.join(self.target, 'boot')
         os.makedirs(self.boot)
@@ -553,6 +556,8 @@ class TestSetupGrub(CiTestCase):
         self.mock_in_chroot_subp = self.mock_in_chroot.subp
         self.mock_in_chroot_subp.side_effect = iter(self.in_chroot_subp_output)
         self.mock_chroot.return_value = self.mock_in_chroot
+        self.add_patch('curtin.commands.curthooks.configure_grub_debconf',
+                       'm_grub_debconf')
 
     def test_uses_old_grub_install_devices_in_cfg(self):
         cfg = {
@@ -582,8 +587,10 @@ class TestSetupGrub(CiTestCase):
                 self.target, '/dev/vdb'],),
             self.mock_subp.call_args_list[0][0])
 
+    @patch('curtin.commands.block_meta.multipath')
     @patch('curtin.commands.curthooks.os.path.exists')
-    def test_uses_grub_install_on_storage_config(self, m_exists):
+    def test_uses_grub_install_on_storage_config(self, m_exists, m_multipath):
+        m_multipath.is_mpath_member.return_value = False
         cfg = {
             'storage': {
                 'version': 1,
@@ -598,6 +605,7 @@ class TestSetupGrub(CiTestCase):
             },
         }
         self.subp_output.append(('', ''))
+        self.subp_output.append(('', ''))
         m_exists.return_value = True
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
         self.assertEquals(
@@ -607,10 +615,12 @@ class TestSetupGrub(CiTestCase):
                 self.target, '/dev/vdb'],),
             self.mock_subp.call_args_list[0][0])
 
+    @patch('curtin.commands.block_meta.multipath')
     @patch('curtin.block.is_valid_device')
     @patch('curtin.commands.curthooks.os.path.exists')
     def test_uses_grub_install_on_storage_config_uefi(
-            self, m_exists, m_is_valid_device):
+            self, m_exists, m_is_valid_device, m_multipath):
+        m_multipath.is_mpath_member.return_value = False
         self.mock_is_uefi_bootable.return_value = True
         cfg = {
             'storage': {
@@ -627,6 +637,7 @@ class TestSetupGrub(CiTestCase):
                         'id': 'vdb-part1',
                         'type': 'partition',
                         'device': 'vdb',
+                        'flag': 'boot',
                         'number': 1,
                     },
                     {
@@ -649,7 +660,7 @@ class TestSetupGrub(CiTestCase):
         }
         self.subp_output.append(('', ''))
         m_exists.return_value = True
-        m_is_valid_device.side_effect = (False, True)
+        m_is_valid_device.side_effect = (False, True, False, True)
         curthooks.setup_grub(cfg, self.target, osfamily=distro.DISTROS.redhat)
         self.assertEquals(
             ([
@@ -795,15 +806,115 @@ class TestSetupGrub(CiTestCase):
             self.mock_in_chroot_subp.call_args_list[0][0])
 
 
+class TestUefiRemoveDuplicateEntries(CiTestCase):
+
+    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')
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_uefi_remove_duplicate_entries(self):
+        cfg = {
+            'grub': {
+                'install_devices': ['/dev/vdb'],
+                'update_nvram': 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)'),
+                },
+            }
+        }
+
+        curthooks.uefi_remove_duplicate_entries(cfg, 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_change(self):
+        cfg = {
+            'grub': {
+                'install_devices': ['/dev/vdb'],
+                'update_nvram': True,
+            },
+        }
+        self.m_efibootmgr.return_value = {
+            'current': '0000',
+            'entries': {
+                '0000': {
+                    'name': 'ubuntu',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\ubuntu\\shimx64.efi)'),
+                },
+                '0001': {
+                    'name': 'centos',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\centos\\shimx64.efi)'),
+                },
+                '0002': {
+                    'name': 'sles',
+                    'path': (
+                        'HD(1,GPT)/File(\\EFI\\sles\\shimx64.efi)'),
+                },
+            }
+        }
+
+        curthooks.uefi_remove_duplicate_entries(cfg, self.target)
+        self.assertEquals([], self.m_subp.call_args_list)
+
+
 class TestUbuntuCoreHooks(CiTestCase):
+
+    def _make_uc16(self, target):
+        ucpath = os.path.join(target, 'system-data', 'var/lib/snapd')
+        util.ensure_dir(ucpath)
+        return ucpath
+
+    def _make_uc20(self, target):
+        ucpath = os.path.join(target, 'snaps')
+        util.ensure_dir(ucpath)
+        return ucpath
+
     def setUp(self):
         super(TestUbuntuCoreHooks, self).setUp()
         self.target = None
 
-    def test_target_is_ubuntu_core(self):
+    def test_target_is_ubuntu_core_16(self):
+        self.target = self.tmp_dir()
+        ubuntu_core_path = self._make_uc16(self.target)
+        self.assertTrue(os.path.isdir(ubuntu_core_path))
+        is_core = distro.is_ubuntu_core(self.target)
+        self.assertTrue(is_core)
+
+    def test_target_is_ubuntu_core_20(self):
         self.target = self.tmp_dir()
-        ubuntu_core_path = os.path.join(self.target, 'system-data',
-                                        'var/lib/snapd')
+        ubuntu_core_path = self._make_uc20(self.target)
         util.ensure_dir(ubuntu_core_path)
         self.assertTrue(os.path.isdir(ubuntu_core_path))
         is_core = distro.is_ubuntu_core(self.target)
@@ -869,6 +980,8 @@ class TestUbuntuCoreHooks(CiTestCase):
                 }
             }
         }
+        uc_cloud = os.path.join(self.target, 'system-data')
+        util.ensure_dir(uc_cloud)
         curthooks.ubuntu_core_curthooks(cfg, target=self.target)
 
         self.assertEqual(len(mock_del_file.call_args_list), 0)
@@ -881,9 +994,32 @@ class TestUbuntuCoreHooks(CiTestCase):
     @patch('curtin.util.write_file')
     @patch('curtin.util.del_file')
     @patch('curtin.commands.curthooks.handle_cloudconfig')
+    def test_curthooks_uc20_cloud_config(self, mock_handle_cc, mock_del_file,
+                                         mock_write_file):
+        self.target = self.tmp_dir()
+        self._make_uc20(self.target)
+        cfg = {
+            'cloudconfig': {
+                'file1': {
+                    'content': "Hello World!\n",
+                }
+            }
+        }
+        curthooks.ubuntu_core_curthooks(cfg, target=self.target)
+        self.assertEqual(len(mock_del_file.call_args_list), 0)
+        cc_path = os.path.join(self.target,
+                               'data', 'etc', 'cloud', 'cloud.cfg.d')
+        mock_handle_cc.assert_called_with(cfg.get('cloudconfig'),
+                                          base_dir=cc_path)
+        self.assertEqual(len(mock_write_file.call_args_list), 0)
+
+    @patch('curtin.util.write_file')
+    @patch('curtin.util.del_file')
+    @patch('curtin.commands.curthooks.handle_cloudconfig')
     def test_curthooks_net_config(self, mock_handle_cc, mock_del_file,
                                   mock_write_file):
         self.target = self.tmp_dir()
+        self._make_uc16(self.target)
         cfg = {
             'network': {
                 'version': '1',
@@ -891,12 +1027,12 @@ class TestUbuntuCoreHooks(CiTestCase):
                             'name': 'eth0', 'subnets': [{'type': 'dhcp4'}]}]
             }
         }
+        uc_cloud = os.path.join(self.target, 'system-data')
         curthooks.ubuntu_core_curthooks(cfg, target=self.target)
 
         self.assertEqual(len(mock_del_file.call_args_list), 0)
         self.assertEqual(len(mock_handle_cc.call_args_list), 0)
-        netcfg_path = os.path.join(self.target,
-                                   'system-data',
+        netcfg_path = os.path.join(uc_cloud,
                                    'etc/cloud/cloud.cfg.d',
                                    '50-curtin-networking.cfg')
         netcfg = config.dump_config({'network': cfg.get('network')})
@@ -904,6 +1040,33 @@ class TestUbuntuCoreHooks(CiTestCase):
                                            content=netcfg)
         self.assertEqual(len(mock_del_file.call_args_list), 0)
 
+    @patch('curtin.util.write_file')
+    @patch('curtin.util.del_file')
+    @patch('curtin.commands.curthooks.handle_cloudconfig')
+    def test_curthooks_uc20_net_config(self, mock_handle_cc, mock_del_file,
+                                       mock_write_file):
+        self.target = self.tmp_dir()
+        self._make_uc20(self.target)
+        cfg = {
+            'network': {
+                'version': '1',
+                'config': [{'type': 'physical',
+                            'name': 'eth0', 'subnets': [{'type': 'dhcp4'}]}]
+            }
+        }
+        uc_cloud = os.path.join(self.target,
+                                'data', 'etc', 'cloud', 'cloud.cfg.d')
+        curthooks.ubuntu_core_curthooks(cfg, target=self.target)
+
+        self.assertEqual(len(mock_del_file.call_args_list), 0)
+        self.assertEqual(len(mock_handle_cc.call_args_list), 0)
+        netcfg_path = os.path.join(uc_cloud,
+                                   '50-curtin-networking.cfg')
+        netcfg = config.dump_config({'network': cfg.get('network')})
+        mock_write_file.assert_called_with(netcfg_path,
+                                           content=netcfg)
+        self.assertEqual(len(mock_del_file.call_args_list), 0)
+
     @patch('curtin.commands.curthooks.futil.write_files')
     def test_handle_cloudconfig(self, mock_write_files):
         cc_target = "tmpXXXX/systemd-data/etc/cloud/cloud.cfg.d"
@@ -1487,4 +1650,316 @@ class TestCurthooksCopyZkey(CiTestCase):
         self.assertEqual(self.zkey_content, found_files)
 
 
+class TestCurthooksGrubDebconf(CiTestCase):
+    def setUp(self):
+        super(TestCurthooksGrubDebconf, self).setUp()
+        base = 'curtin.commands.curthooks.'
+        self.add_patch(
+            base + 'apt_config.apply_debconf_selections', 'm_debconf')
+        self.add_patch(base + 'block.disk_to_byid_path', 'm_byid')
+
+    def test_debconf_multiselect(self):
+        package = self.random_string()
+        variable = "%s/%s" % (self.random_string(), self.random_string())
+        choices = [c for c in self.random_string()]
+        expected = "%s %s multiselect %s" % (package, variable,
+                                             ", ".join(choices))
+        self.assertEqual(expected,
+                         curthooks._debconf_multiselect(package, variable,
+                                                        choices))
+
+    def test_configure_grub_debconf(self):
+        target = self.random_string()
+        boot_devs = [self.random_string()]
+        byid_boot_devs = ["/dev/disk/by-id/" + dev for dev in boot_devs]
+        uefi = False
+        self.m_byid.side_effect = (lambda x: '/dev/disk/by-id/' + x)
+        curthooks.configure_grub_debconf(boot_devs, target, uefi)
+        expected_selection = [
+            ('grub-pc grub-pc/install_devices '
+             'multiselect %s' % ", ".join(byid_boot_devs))
+        ]
+        expectedcfg = {
+            'debconf_selections': {'grub': "\n".join(expected_selection)}}
+        self.m_debconf.assert_called_with(expectedcfg, target)
+
+    def test_configure_grub_debconf_uefi_enabled(self):
+        target = self.random_string()
+        boot_devs = [self.random_string()]
+        byid_boot_devs = ["/dev/disk/by-id/" + dev for dev in boot_devs]
+        uefi = True
+        self.m_byid.side_effect = (lambda x: '/dev/disk/by-id/' + x)
+        curthooks.configure_grub_debconf(boot_devs, target, uefi)
+        expected_selection = [
+            ('grub-pc grub-efi/install_devices '
+             'multiselect %s' % ", ".join(byid_boot_devs))
+        ]
+        expectedcfg = {
+            'debconf_selections': {'grub': "\n".join(expected_selection)}}
+        self.m_debconf.assert_called_with(expectedcfg, target)
+
+    def test_configure_grub_debconf_handle_no_byid_result(self):
+        target = self.random_string()
+        boot_devs = ['aaaaa', 'bbbbb']
+        uefi = True
+        self.m_byid.side_effect = (
+                lambda x: ('/dev/disk/by-id/' + x if 'a' in x else None))
+        curthooks.configure_grub_debconf(boot_devs, target, uefi)
+        expected_selection = [
+            ('grub-pc grub-efi/install_devices '
+             'multiselect /dev/disk/by-id/aaaaa, bbbbb')
+        ]
+        expectedcfg = {
+            'debconf_selections': {'grub': "\n".join(expected_selection)}}
+        self.m_debconf.assert_called_with(expectedcfg, target)
+
+
+class TestUefiFindGrubDeviceIds(CiTestCase):
+
+    def _sconfig(self, cfg):
+        return extract_storage_ordered_dict(cfg)
+
+    def test_missing_primary_esp_raises_exception(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                ]
+            },
+        }
+        with self.assertRaises(RuntimeError):
+            curthooks.uefi_find_grub_device_ids(self._sconfig(cfg))
+
+    def test_single_esp_grub_device_true(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part1_format',
+                        'path': '/boot/efi',
+                    },
+                ]
+            },
+        }
+        self.assertEqual(['vdb-part1'],
+                         curthooks.uefi_find_grub_device_ids(
+                             self._sconfig(cfg)))
+
+    def test_single_esp_grub_device_true_on_disk(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb',
+                        'type': 'disk',
+                        'name': 'vdb',
+                        'path': '/dev/vdb',
+                        'ptable': 'gpt',
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part1_format',
+                        'path': '/boot/efi',
+                    },
+                ]
+            },
+        }
+        self.assertEqual(['vdb-part1'],
+                         curthooks.uefi_find_grub_device_ids(
+                             self._sconfig(cfg)))
+
+    def test_single_esp_no_grub_device(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vdb',
+                        'type': 'disk',
+                        'name': 'vdb',
+                        'path': '/dev/vdb',
+                        'ptable': 'gpt',
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part1_format',
+                        'path': '/boot/efi',
+                    },
+                ]
+            },
+        }
+        self.assertEqual(['vdb-part1'],
+                         curthooks.uefi_find_grub_device_ids(
+                             self._sconfig(cfg)))
+
+    def test_multiple_esp_grub_device_true(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vda-part1',
+                        'type': 'partition',
+                        'device': 'vda',
+                        'flag': 'boot',
+                        'number': 1,
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vda-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part1_format',
+                        'path': '/boot/efi',
+                    },
+
+                ]
+            },
+        }
+        self.assertEqual(['vdb-part1', 'vda-part1'],
+                         curthooks.uefi_find_grub_device_ids(
+                             self._sconfig(cfg)))
+
+    def test_multiple_esp_grub_device_true_on_disk(self):
+        cfg = {
+            'storage': {
+                'version': 1,
+                'config': [
+                    {
+                        'id': 'vda',
+                        'type': 'disk',
+                        'name': 'vda',
+                        'path': '/dev/vda',
+                        'ptable': 'gpt',
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vdb',
+                        'type': 'disk',
+                        'name': 'vdb',
+                        'path': '/dev/vdb',
+                        'ptable': 'gpt',
+                        'grub_device': True,
+                    },
+                    {
+                        'id': 'vda-part1',
+                        'type': 'partition',
+                        'device': 'vda',
+                        'flag': 'boot',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vdb-part1',
+                        'type': 'partition',
+                        'device': 'vdb',
+                        'flag': 'boot',
+                        'number': 1,
+                    },
+                    {
+                        'id': 'vda-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_format',
+                        'type': 'format',
+                        'volume': 'vdb-part1',
+                        'fstype': 'fat32',
+                    },
+                    {
+                        'id': 'vdb-part1_mount',
+                        'type': 'mount',
+                        'device': 'vdb-part1_format',
+                        'path': '/boot/efi',
+                    },
+
+                ]
+            },
+        }
+        self.assertEqual(['vdb-part1', 'vda-part1'],
+                         curthooks.uefi_find_grub_device_ids(
+                             self._sconfig(cfg)))
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_distro.py b/tests/unittests/test_distro.py
index dc1038c..c994963 100644
--- a/tests/unittests/test_distro.py
+++ b/tests/unittests/test_distro.py
@@ -193,16 +193,65 @@ class TestDistroInfo(CiTestCase):
 
 class TestDistroIdentity(CiTestCase):
 
+    ubuntu_core_os_path_side_effects = [
+        [True, True, True],
+        [True, True, False],
+        [True, False, True],
+        [True, False, False],
+        [False, True, True],
+        [False, True, False],
+        [False, False, True],
+    ]
+
     def setUp(self):
         super(TestDistroIdentity, self).setUp()
         self.add_patch('curtin.distro.os.path.exists', 'mock_os_path')
 
-    def test_is_ubuntu_core(self):
+    def test_is_ubuntu_core_16(self):
         for exists in [True, False]:
             self.mock_os_path.return_value = exists
-            self.assertEqual(exists, distro.is_ubuntu_core())
+            self.assertEqual(exists, distro.is_ubuntu_core_16())
             self.mock_os_path.assert_called_with('/system-data/var/lib/snapd')
 
+    def test_is_ubuntu_core_18(self):
+        for exists in [True, False]:
+            self.mock_os_path.return_value = exists
+            self.assertEqual(exists, distro.is_ubuntu_core_18())
+            self.mock_os_path.assert_called_with('/system-data/var/lib/snapd')
+
+    def test_is_ubuntu_core_is_core20(self):
+        for exists in [True, False]:
+            self.mock_os_path.return_value = exists
+            self.assertEqual(exists, distro.is_ubuntu_core_20())
+            self.mock_os_path.assert_called_with('/snaps')
+
+    def test_is_ubuntu_core_true(self):
+        side_effects = self.ubuntu_core_os_path_side_effects
+        for true_effect in side_effects:
+            self.mock_os_path.side_effect = iter(true_effect)
+            self.assertTrue(distro.is_ubuntu_core())
+
+        expected_calls = [
+            mock.call('/system-data/var/lib/snapd'),
+            mock.call('/system-data/var/lib/snapd'),
+            mock.call('/snaps')]
+        expected_nr_calls = len(side_effects) * len(expected_calls)
+        self.assertEqual(expected_nr_calls, self.mock_os_path.call_count)
+        self.mock_os_path.assert_has_calls(
+            expected_calls * len(side_effects))
+
+    def test_is_ubuntu_core_false(self):
+        self.mock_os_path.return_value = False
+        self.assertFalse(distro.is_ubuntu_core())
+
+        expected_calls = [
+            mock.call('/system-data/var/lib/snapd'),
+            mock.call('/system-data/var/lib/snapd'),
+            mock.call('/snaps')]
+        expected_nr_calls = 3
+        self.assertEqual(expected_nr_calls, self.mock_os_path.call_count)
+        self.mock_os_path.assert_has_calls(expected_calls)
+
     def test_is_centos(self):
         for exists in [True, False]:
             self.mock_os_path.return_value = exists
diff --git a/tests/unittests/test_gpg.py b/tests/unittests/test_gpg.py
index 2c83ae3..42ff8c4 100644
--- a/tests/unittests/test_gpg.py
+++ b/tests/unittests/test_gpg.py
@@ -48,50 +48,6 @@ class TestCurtinGpg(CiTestCase):
                                       "--recv", key], capture=True,
                                      retries=None)
 
-    @patch('time.sleep')
-    @patch('curtin.util._subp')
-    def test_recv_key_retry_raises(self, mock_under_subp, mock_sleep):
-        key = 'DEADBEEF'
-        keyserver = 'keyserver.ubuntu.com'
-        retries = (1, 2, 5, 10)
-        nr_calls = 5
-        mock_under_subp.side_effect = iter([
-            util.ProcessExecutionError()] * nr_calls)
-
-        with self.assertRaises(ValueError):
-            gpg.recv_key(key, keyserver, retries=retries)
-
-        print("_subp calls: %s" % mock_under_subp.call_args_list)
-        print("sleep calls: %s" % mock_sleep.call_args_list)
-        expected_calls = nr_calls * [
-            call(["gpg", "--keyserver", keyserver, "--recv", key],
-                 capture=True)]
-        mock_under_subp.assert_has_calls(expected_calls)
-
-        expected_calls = [call(1), call(2), call(5), call(10)]
-        mock_sleep.assert_has_calls(expected_calls)
-
-    @patch('time.sleep')
-    @patch('curtin.util._subp')
-    def test_recv_key_retry_works(self, mock_under_subp, mock_sleep):
-        key = 'DEADBEEF'
-        keyserver = 'keyserver.ubuntu.com'
-        nr_calls = 2
-        mock_under_subp.side_effect = iter([
-            util.ProcessExecutionError(),  # 1
-            ("", ""),
-        ])
-
-        gpg.recv_key(key, keyserver, retries=[1])
-
-        print("_subp calls: %s" % mock_under_subp.call_args_list)
-        print("sleep calls: %s" % mock_sleep.call_args_list)
-        expected_calls = nr_calls * [
-            call(["gpg", "--keyserver", keyserver, "--recv", key],
-                 capture=True)]
-        mock_under_subp.assert_has_calls(expected_calls)
-        mock_sleep.assert_has_calls([call(1)])
-
     @patch('curtin.util.subp')
     def test_delete_key(self, mock_subp):
         key = 'DEADBEEF'
@@ -160,4 +116,54 @@ class TestCurtinGpg(CiTestCase):
             call(key, keyserver=keyserver, retries=None)])
         mock_del.assert_has_calls([call(key)])
 
+
+class TestCurtinGpgSubp(TestCurtinGpg):
+
+    allowed_subp = True
+
+    @patch('time.sleep')
+    @patch('curtin.util._subp')
+    def test_recv_key_retry_raises(self, mock_under_subp, mock_sleep):
+        key = 'DEADBEEF'
+        keyserver = 'keyserver.ubuntu.com'
+        retries = (1, 2, 5, 10)
+        nr_calls = 5
+        mock_under_subp.side_effect = iter([
+            util.ProcessExecutionError()] * nr_calls)
+
+        with self.assertRaises(ValueError):
+            gpg.recv_key(key, keyserver, retries=retries)
+
+        print("_subp calls: %s" % mock_under_subp.call_args_list)
+        print("sleep calls: %s" % mock_sleep.call_args_list)
+        expected_calls = nr_calls * [
+            call(["gpg", "--keyserver", keyserver, "--recv", key],
+                 capture=True)]
+        mock_under_subp.assert_has_calls(expected_calls)
+
+        expected_calls = [call(1), call(2), call(5), call(10)]
+        mock_sleep.assert_has_calls(expected_calls)
+
+    @patch('time.sleep')
+    @patch('curtin.util._subp')
+    def test_recv_key_retry_works(self, mock_under_subp, mock_sleep):
+        key = 'DEADBEEF'
+        keyserver = 'keyserver.ubuntu.com'
+        nr_calls = 2
+        mock_under_subp.side_effect = iter([
+            util.ProcessExecutionError(),  # 1
+            ("", ""),
+        ])
+
+        gpg.recv_key(key, keyserver, retries=[1])
+
+        print("_subp calls: %s" % mock_under_subp.call_args_list)
+        print("sleep calls: %s" % mock_sleep.call_args_list)
+        expected_calls = nr_calls * [
+            call(["gpg", "--keyserver", keyserver, "--recv", key],
+                 capture=True)]
+        mock_under_subp.assert_has_calls(expected_calls)
+        mock_sleep.assert_has_calls([call(1)])
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_make_dname.py b/tests/unittests/test_make_dname.py
index eb58cfd..ec48339 100644
--- a/tests/unittests/test_make_dname.py
+++ b/tests/unittests/test_make_dname.py
@@ -153,19 +153,19 @@ class TestMakeDname(CiTestCase):
             '', self.trusty_blkid)
         mock_util.load_command_environment.return_value = self.state
 
-        warning_msg = "Can't find a uuid for volume: {}. Skipping dname."
+        warning_msg = "Can't find a uuid for volume: %s. Skipping dname."
 
         # disk with no PT_UUID
         disk = 'disk_noid'
         block_meta.make_dname(disk, self.storage_config)
-        mock_log.warning.assert_called_with(warning_msg.format(disk))
+        mock_log.warning.assert_called_with(warning_msg, disk)
         self.assertFalse(mock_util.write_file.called)
 
         mock_util.subp.side_effect = self._make_mock_subp_blkid(
             '', self.trusty_blkid)
         # partition with no PART_UUID
         block_meta.make_dname('disk1p1', self.storage_config)
-        mock_log.warning.assert_called_with(warning_msg.format('disk1p1'))
+        mock_log.warning.assert_called_with(warning_msg, 'disk1p1')
         self.assertFalse(mock_util.write_file.called)
 
     @mock.patch('curtin.commands.block_meta.LOG')
diff --git a/tests/unittests/test_storage_config.py b/tests/unittests/test_storage_config.py
index da83fec..ecdc565 100644
--- a/tests/unittests/test_storage_config.py
+++ b/tests/unittests/test_storage_config.py
@@ -7,6 +7,7 @@ from curtin.storage_config import ProbertParser as baseparser
 from curtin.storage_config import (BcacheParser, BlockdevParser, DasdParser,
                                    DmcryptParser, FilesystemParser, LvmParser,
                                    RaidParser, MountParser, ZfsParser)
+from curtin.storage_config import ptable_uuid_to_flag_entry
 from curtin import util
 
 
@@ -207,21 +208,21 @@ class TestBlockdevParser(CiTestCase):
         expected_tuple = ('boot', 'EF00')
         for guid in boot_guids:
             self.assertEqual(expected_tuple,
-                             self.bdevp.ptable_uuid_to_flag_entry(guid))
+                             ptable_uuid_to_flag_entry(guid))
 
     # XXX: Parameterize me
     def test_blockdev_ptable_uuid_flag_invalid(self):
         """ BlockdevParser returns (None, None) for invalid uuids. """
         for invalid in [None, '', {}, []]:
             self.assertEqual((None, None),
-                             self.bdevp.ptable_uuid_to_flag_entry(invalid))
+                             ptable_uuid_to_flag_entry(invalid))
 
     # XXX: Parameterize me
     def test_blockdev_ptable_uuid_flag_unknown_uuid(self):
         """ BlockdevParser returns (None, None) for unknown uuids. """
         for unknown in [self.random_string(), self.random_string()]:
             self.assertEqual((None, None),
-                             self.bdevp.ptable_uuid_to_flag_entry(unknown))
+                             ptable_uuid_to_flag_entry(unknown))
 
     def test_get_unique_ids(self):
         """ BlockdevParser extracts uniq udev ID_ values. """
@@ -231,6 +232,34 @@ class TestBlockdevParser(CiTestCase):
         self.assertDictEqual(expected_ids,
                              self.bdevp.get_unique_ids(blockdev))
 
+    def test_get_unique_ids_ignores_empty_wwn_values(self):
+        """ BlockdevParser skips invalid ID_WWN_* values. """
+        self.bdevp.blockdev_data['/dev/sda'] = {
+            'DEVTYPE': 'disk',
+            'DEVNAME': 'sda',
+            'ID_SERIAL': 'Corsair_Force_GS_1785234921906',
+            'ID_SERIAL_SHORT': '1785234921906',
+            'ID_WWN': '0x0000000000000000',
+            'ID_WWN_WITH_EXTENSION': '0x0000000000000000',
+        }
+        blockdev = self.bdevp.blockdev_data['/dev/sda']
+        expected_ids = {'serial': 'Corsair_Force_GS_1785234921906'}
+        self.assertEqual(expected_ids,
+                         self.bdevp.get_unique_ids(blockdev))
+
+    def test_get_unique_ids_ignores_empty_serial_values(self):
+        """ BlockdevParser skips invalid ID_SERIAL_* values. """
+        self.bdevp.blockdev_data['/dev/sda'] = {
+            'DEVTYPE': 'disk',
+            'DEVNAME': 'sda',
+            'ID_SERIAL': '                      ',
+            'ID_SERIAL_SHORT': 'My Serial is My PassPort',
+        }
+        blockdev = self.bdevp.blockdev_data['/dev/sda']
+        expected_ids = {'serial': 'My Serial is My PassPort'}
+        self.assertEqual(expected_ids,
+                         self.bdevp.get_unique_ids(blockdev))
+
     def test_partition_parent_devname(self):
         """ BlockdevParser calculate partition parent name. """
         expected_parent = '/dev/sda'
@@ -419,6 +448,52 @@ class TestBlockdevParser(CiTestCase):
             'number': 2}
         self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
 
+    def test_blockdev_skips_multipath_entry_if_no_multipath_data(self):
+        self.probe_data = _get_data('probert_storage_multipath.json')
+        del self.probe_data['multipath']
+        self.bdevp = BlockdevParser(self.probe_data)
+        blockdev = self.bdevp.blockdev_data['/dev/sda2']
+        expected_dict = {
+            'flag': 'linux',
+            'id': 'partition-sda2',
+            'offset': 2097152,
+            'size': 10734272512,
+            'type': 'partition',
+            'device': 'disk-sda',
+            'number': 2}
+        self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
+
+    def test_blockdev_skips_multipath_entry_if_bad_multipath_data(self):
+        self.probe_data = _get_data('probert_storage_multipath.json')
+        for path in self.probe_data['multipath']['paths']:
+            path['multipath'] = ''
+        self.bdevp = BlockdevParser(self.probe_data)
+        blockdev = self.bdevp.blockdev_data['/dev/sda2']
+        expected_dict = {
+            'flag': 'linux',
+            'id': 'partition-sda2',
+            'offset': 2097152,
+            'size': 10734272512,
+            'type': 'partition',
+            'device': 'disk-sda',
+            'number': 2}
+        self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
+
+    def test_blockdev_skips_multipath_entry_if_no_mp_paths(self):
+        self.probe_data = _get_data('probert_storage_multipath.json')
+        del self.probe_data['multipath']['paths']
+        self.bdevp = BlockdevParser(self.probe_data)
+        blockdev = self.bdevp.blockdev_data['/dev/sda2']
+        expected_dict = {
+            'flag': 'linux',
+            'id': 'partition-sda2',
+            'offset': 2097152,
+            'size': 10734272512,
+            'type': 'partition',
+            'device': 'disk-sda',
+            'number': 2}
+        self.assertDictEqual(expected_dict, self.bdevp.asdict(blockdev))
+
     def test_blockdev_finds_multipath_id_from_dm_uuid(self):
         self.probe_data = _get_data('probert_storage_zlp6.json')
         self.bdevp = BlockdevParser(self.probe_data)
@@ -426,6 +501,82 @@ class TestBlockdevParser(CiTestCase):
         result = self.bdevp.blockdev_to_id(blockdev)
         self.assertEqual('disk-sda', result)
 
+    def test_blockdev_find_mpath_members_checks_dm_name(self):
+        """ BlockdevParser find_mpath_members uses dm_name if present."""
+        dm14 = {
+            "DEVTYPE": "disk",
+            "DEVLINKS": "/dev/disk/by-id/dm-name-mpathb",
+            "DEVNAME": "/dev/dm-14",
+            "DEVTYPE": "disk",
+            "DM_NAME": "mpathb",
+            "DM_UUID": "mpath-360050768028211d8b000000000000062",
+            "DM_WWN": "0x60050768028211d8b000000000000062",
+            "MPATH_DEVICE_READY": "1",
+            "MPATH_SBIN_PATH": "/sbin",
+        }
+        multipath = {
+            "maps": [
+                {
+                    "multipath": "360050768028211d8b000000000000061",
+                    "sysfs": "dm-11",
+                    "paths": "4"
+                },
+                {
+                    "multipath": "360050768028211d8b000000000000062",
+                    "sysfs": "dm-14",
+                    "paths": "4"
+                },
+                {
+                    "multipath": "360050768028211d8b000000000000063",
+                    "sysfs": "dm-15",
+                    "paths": "4"
+                }],
+            "paths": [
+                {
+                    "device": "sdej",
+                    "serial": "0200a084762cXX00",
+                    "multipath": "mpatha",
+                    "host_wwnn": "0x20000024ff9127de",
+                    "target_wwnn": "0x5005076802065e38",
+                    "host_wwpn": "0x21000024ff9127de",
+                    "target_wwpn": "0x5005076802165e38",
+                    "host_adapter": "[undef]"
+                },
+                {
+                    "device": "sdel",
+                    "serial": "0200a084762cXX00",
+                    "multipath": "mpathb",
+                    "host_wwnn": "0x20000024ff9127de",
+                    "target_wwnn": "0x5005076802065e38",
+                    "host_wwpn": "0x21000024ff9127de",
+                    "target_wwpn": "0x5005076802165e38",
+                    "host_adapter": "[undef]"
+                },
+                {
+                    "device": "sdet",
+                    "serial": "0200a084762cXX00",
+                    "multipath": "mpatha",
+                    "host_wwnn": "0x20000024ff9127de",
+                    "target_wwnn": "0x5005076802065e37",
+                    "host_wwpn": "0x21000024ff9127de",
+                    "target_wwpn": "0x5005076802165e37",
+                    "host_adapter": "[undef]"
+                },
+                {
+                    "device": "sdev",
+                    "serial": "0200a084762cXX00",
+                    "multipath": "mpathb",
+                    "host_wwnn": "0x20000024ff9127de",
+                    "target_wwnn": "0x5005076802065e37",
+                    "host_wwpn": "0x21000024ff9127de",
+                    "target_wwpn": "0x5005076802165e37",
+                    "host_adapter": "[undef]"
+                }],
+        }
+        self.bdevp.blockdev_data['/dev/dm-14'] = dm14
+        self.probe_data['multipath'] = multipath
+        self.assertEqual('disk-sdel', self.bdevp.blockdev_to_id(dm14))
+
     def test_blockdev_detects_dasd_device_id_and_vtoc_ptable(self):
         self.probe_data = _get_data('probert_storage_dasd.json')
         self.bdevp = BlockdevParser(self.probe_data)
@@ -844,4 +995,39 @@ class TestExtractStorageConfig(CiTestCase):
         extended = [part for part in partitions if part['flag'] == 'extended']
         self.assertEqual(1, len(extended))
 
+    @skipUnlessJsonSchema()
+    def test_blockdev_detects_nvme_multipath_devices(self):
+        self.probe_data = _get_data('probert_storage_nvme_multipath.json')
+        extracted = storage_config.extract_storage_config(self.probe_data)
+        config = extracted['storage']['config']
+        disks = [cfg for cfg in config if cfg['type'] == 'disk']
+        expected_dict = {
+            'id': 'disk-nvme0n1',
+            'path': '/dev/nvme0n1',
+            'ptable': 'gpt',
+            'serial': 'SAMSUNG MZPLL3T2HAJQ-00005_S4CCNE0M300015',
+            'type': 'disk',
+            'wwn': 'eui.344343304d3000150025384500000004',
+        }
+        self.assertEqual(1, len(disks))
+        self.assertEqual(expected_dict, disks[0])
+
+    @skipUnlessJsonSchema()
+    def test_blockdev_skips_invalid_wwn(self):
+        self.probe_data = _get_data('probert_storage_bogus_wwn.json')
+        extracted = storage_config.extract_storage_config(self.probe_data)
+        config = extracted['storage']['config']
+        disks = [cfg for cfg in config
+                 if cfg['type'] == 'disk' and cfg['path'] == '/dev/sda']
+        expected_dict = {
+            'id': 'disk-sda',
+            'path': '/dev/sda',
+            'ptable': 'gpt',
+            'serial': 'Corsair_Force_GS_13207907000097410026',
+            'type': 'disk',
+        }
+        self.assertEqual(1, len(disks))
+        self.assertEqual(expected_dict, disks[0])
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_udev.py b/tests/unittests/test_udev.py
index 33d5f44..919c7c0 100644
--- a/tests/unittests/test_udev.py
+++ b/tests/unittests/test_udev.py
@@ -1,14 +1,18 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 import mock
+import shlex
 
-from curtin import udev
+from curtin.udev import (
+        udevadm_info,
+        shlex_quote,
+        )
 from curtin import util
 from .helpers import CiTestCase
 
 
 UDEVADM_INFO_QUERY = """\
-DEVLINKS='/dev/disk/by-id/nvme-eui.0025388b710116a1'
+DEVLINKS='/dev/disk/by-id/nvme-eui.0025388b710116a1 /dev/disk/by-id/nvme-n1'
 DEVNAME='/dev/nvme0n1'
 DEVPATH='/devices/pci0000:00/0000:00:1c.4/0000:05:00.0/nvme/nvme0/nvme0n1'
 DEVTYPE='disk'
@@ -24,7 +28,8 @@ USEC_INITIALIZED='2026691'
 """
 
 INFO_DICT = {
-    'DEVLINKS': ['/dev/disk/by-id/nvme-eui.0025388b710116a1'],
+    'DEVLINKS': ['/dev/disk/by-id/nvme-eui.0025388b710116a1',
+                 '/dev/disk/by-id/nvme-n1'],
     'DEVNAME': '/dev/nvme0n1',
     'DEVPATH':
         '/devices/pci0000:00/0000:00:1c.4/0000:05:00.0/nvme/nvme0/nvme0n1',
@@ -48,17 +53,49 @@ class TestUdevInfo(CiTestCase):
         """ udevadm_info returns dictionary for specified device """
         mypath = '/dev/nvme0n1'
         m_subp.return_value = (UDEVADM_INFO_QUERY, "")
-        info = udev.udevadm_info(mypath)
+        info = udevadm_info(mypath)
         m_subp.assert_called_with(
             ['udevadm', 'info', '--query=property', '--export', mypath],
             capture=True)
         self.assertEqual(sorted(INFO_DICT), sorted(info))
 
+    @mock.patch('curtin.util.subp')
+    def test_udevadm_info_escape_quotes(self, m_subp):
+        """verify we escape quotes when we fail to split. """
+        mypath = '/dev/sdz'
+        datafile = 'tests/data/udevadm_info_sandisk_cruzer.txt'
+        m_subp.return_value = (util.load_file(datafile), "")
+        info = udevadm_info(mypath)
+        m_subp.assert_called_with(
+            ['udevadm', 'info', '--query=property', '--export', mypath],
+            capture=True)
+
+        """
+        Replicate what udevadm_info parsing does and use pdb to examine what's
+        happening.
+
+        (Pdb) original_value
+        "SanDisk'"
+        (Pdb) quoted_value
+        '\'SanDisk\'"\'"\'\''
+        (Pdb) split_value
+        ["SanDisk'"]
+        (Pdb) expected_value
+        "SanDisk'"
+        """
+        original_value = "SanDisk'"
+        quoted_value = shlex_quote(original_value)
+        split_value = shlex.split(quoted_value)
+        expected_value = split_value if ' ' in split_value else split_value[0]
+
+        self.assertEqual(expected_value, info['SCSI_VENDOR'])
+        self.assertEqual(expected_value, info['SCSI_VENDOR_ENC'])
+
     def test_udevadm_info_no_path(self):
         """ udevadm_info raises ValueError for invalid path value"""
         mypath = None
         with self.assertRaises(ValueError):
-            udev.udevadm_info(mypath)
+            udevadm_info(mypath)
 
     @mock.patch('curtin.util.subp')
     def test_udevadm_info_path_not_exists(self, m_subp):
@@ -66,4 +103,4 @@ class TestUdevInfo(CiTestCase):
         mypath = self.random_string()
         m_subp.side_effect = util.ProcessExecutionError()
         with self.assertRaises(util.ProcessExecutionError):
-            udev.udevadm_info(mypath)
+            udevadm_info(mypath)
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 80bb85f..0377357 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -113,6 +113,7 @@ class TestSubp(CiTestCase):
     utf8_invalid = b'ab\xaadef'
     utf8_valid = b'start \xc3\xa9 end'
     utf8_valid_2 = b'd\xc3\xa9j\xc8\xa7'
+    allowed_subp = True
 
     try:
         decode_type = unicode
@@ -552,7 +553,7 @@ class TestTargetPath(CiTestCase):
                          paths.target_path("/target/", "///my/path/"))
 
 
-class TestRunInChroot(CiTestCase):
+class TestRunInChrootTestSubp(CiTestCase):
     """Test the legacy 'RunInChroot'.
 
     The test works by mocking ChrootableTarget's __enter__ to do nothing.
@@ -560,6 +561,7 @@ class TestRunInChroot(CiTestCase):
       a.) RunInChroot is a subclass of ChrootableTarget
       b.) ChrootableTarget's __exit__ only un-does work that its __enter__
           did.  Meaning for our mocked case, it does nothing."""
+    allowed_subp = True
 
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_run_in_chroot_with_target_slash(self):
@@ -567,6 +569,16 @@ class TestRunInChroot(CiTestCase):
             out, err = i(['echo', 'HI MOM'], capture=True)
         self.assertEqual('HI MOM\n', out)
 
+
+class TestRunInChrootTest(CiTestCase):
+    """Test the legacy 'RunInChroot'.
+
+    The test works by mocking ChrootableTarget's __enter__ to do nothing.
+    The assumptions made are:
+      a.) RunInChroot is a subclass of ChrootableTarget
+      b.) ChrootableTarget's __exit__ only un-does work that its __enter__
+          did.  Meaning for our mocked case, it does nothing."""
+
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     @mock.patch("curtin.util.subp")
     def test_run_in_chroot_with_target(self, m_subp):
@@ -585,12 +597,23 @@ class TestRunInChroot(CiTestCase):
 class TestChrootableTargetMounts(CiTestCase):
     """Test ChrootableTargets mounts dirs"""
 
+    @mock.patch('curtin.util.is_uefi_bootable')
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
-    def test_chrootable_target_default_mounts(self):
+    def test_chrootable_target_default_mounts(self, m_uefi):
+        m_uefi.return_value = False
         in_chroot = util.ChrootableTarget("mytarget")
         default_mounts = ['/dev', '/proc', '/run', '/sys']
         self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
 
+    @mock.patch('curtin.util.is_uefi_bootable')
+    @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    def test_chrootable_target_default_mounts_uefi(self, m_uefi):
+        m_uefi.return_value = True
+        in_chroot = util.ChrootableTarget("mytarget")
+        default_mounts = ['/dev', '/proc', '/run', '/sys',
+                          '/sys/firmware/efi/efivars']
+        self.assertEqual(sorted(default_mounts), sorted(in_chroot.mounts))
+
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_chrootable_target_custom_mounts(self):
         my_mounts = ['/foo', '/bar', '/wark']
diff --git a/tests/vmtests/__init__.py b/tests/vmtests/__init__.py
index 39dfb40..222adcc 100644
--- a/tests/vmtests/__init__.py
+++ b/tests/vmtests/__init__.py
@@ -66,6 +66,8 @@ _TOPDIR = None
 
 UC16_IMAGE = os.path.join(IMAGE_DIR,
                           'ubuntu-core-16/amd64/20170217/root-image.xz')
+UC20_IMAGE = os.path.join(IMAGE_DIR, ('ubuntu-core-20/amd64/20200304/'
+                                      'ubuntu-core-20-amd64.img.xz'))
 
 
 def remove_empty_dir(dirpath):
@@ -543,8 +545,9 @@ DEFAULT_COLLECT_SCRIPTS = {
         ls /dev/disk/by-dname/ | cat >ls_dname
         ls -al /dev/disk/by-dname/ | cat >ls_al_bydname
         ls -al /dev/disk/by-id/ | cat >ls_al_byid
-        ls -al /dev/disk/by-uuid/ | cat >ls_al_byuuid
         ls -al /dev/disk/by-partuuid/ | cat >ls_al_bypartuuid
+        ls -al /dev/disk/by-path/ | cat >ls_al_bypath
+        ls -al /dev/disk/by-uuid/ | cat >ls_al_byuuid
         blkid -o export | cat >blkid.out
         find /boot | cat > find_boot.out
         if [ -e /sys/firmware/efi ]; then
@@ -639,6 +642,7 @@ class VMBaseClass(TestCase):
     target_distro = None
     target_release = None
     target_krel = None
+    target_kflavor = None
     target_ftype = "squashfs"
     target_kernel_package = None
 
@@ -669,9 +673,16 @@ class VMBaseClass(TestCase):
 
         tftype = cls.target_ftype
         if tftype in ["root-image.xz"]:
-            logger.info('get-testfiles UC16 hack!')
-            target_ftypes = {'root-image.xz': UC16_IMAGE}
-            target_img_verstr = "UbuntuCore 16"
+            logger.info('get-testfiles UC hack!')
+            if cls.target_release == 'ubuntu-core-16':
+                target_ftypes = {'root-image.xz': UC16_IMAGE}
+                target_img_verstr = "UbuntuCore 16"
+            elif cls.target_release == 'ubuntu-core-20':
+                target_ftypes = {'root-image.xz': UC20_IMAGE}
+                target_img_verstr = "UbuntuCore 20"
+            else:
+                raise ValueError(
+                    "Unknown target_release=%s" % cls.target_release)
         elif cls.target_release == cls.release:
             target_ftypes = ftypes.copy()
             target_img_verstr = eph_img_verstr
@@ -681,7 +692,7 @@ class VMBaseClass(TestCase):
                 cls.target_distro,
                 cls.target_release,
                 cls.target_arch, subarch=cls.subarch if cls.subarch else None,
-                kflavor=cls.kflavor if cls.kflavor else None,
+                kflavor=cls.target_kflavor if cls.target_kflavor else None,
                 krel=cls.target_krel, sync=CURTIN_VMTEST_IMAGE_SYNC,
                 ftypes=(tftype,))
 
@@ -880,11 +891,17 @@ class VMBaseClass(TestCase):
                                                              cls.arch)
             raise SkipTest(reason)
 
+        # there are only centos images for amd64
+        if cls.target_distro == 'centos' and cls.arch != "amd64":
+            reason = "{} is not supported on arch {}".format(cls.__name__,
+                                                             cls.arch)
+            raise SkipTest(reason)
+
         # assign default collect scripts
         if not cls.collect_scripts:
             cls.collect_scripts = (
                 DEFAULT_COLLECT_SCRIPTS['common'] +
-                DEFAULT_COLLECT_SCRIPTS[cls.target_distro])
+                DEFAULT_COLLECT_SCRIPTS.get(cls.target_distro, []))
         else:
             raise RuntimeError('cls collect scripts not empty: %s' %
                                cls.collect_scripts)
@@ -1070,7 +1087,10 @@ class VMBaseClass(TestCase):
         disks.extend(cls.build_iscsi_disks())
 
         # class config file and vmtest defaults
-        configs = [cls.conf_file, 'examples/tests/vmtest_defaults.yaml']
+        configs = [cls.conf_file]
+        if cls.target_distro not in ['ubuntu-core']:
+            configs.append('examples/tests/vmtest_defaults.yaml')
+
         # proxy config
         cls.proxy = get_apt_proxy()
         if cls.proxy is not None and not cls.td.restored:
@@ -1544,17 +1564,14 @@ class VMBaseClass(TestCase):
         self.assertTrue(True not in results.values(),
                         msg="Collected files exist that should not.")
 
-    def load_collect_file(self, filename, mode="r"):
-        with open(self.collect_path(filename), mode) as fp:
-            return fp.read()
+    def load_collect_file(self, filename):
+        return util.load_file(self.collect_path(filename))
 
-    def load_collect_file_shell_content(self, filename, mode="r"):
-        with open(self.collect_path(filename), mode) as fp:
-            return util.load_shell_content(content=fp.read())
+    def load_collect_file_shell_content(self, filename):
+        return util.load_shell_content(self.load_collect_file(filename))
 
     def load_log_file(self, filename):
-        with open(filename, 'rb') as fp:
-            return fp.read().decode('utf-8', errors='replace')
+        return util.load_file(filename)
 
     def get_install_log_curtin_version(self):
         # curtin: Installation started. (%s)
@@ -1680,6 +1697,8 @@ class VMBaseClass(TestCase):
             'wwn': 'ID_WWN_WITH_EXTENSION',
         }
         for disk in disks:
+            if not disk.get('name'):
+                continue
             dname_file = "%s.rules" % sanitize_dname(disk.get('name'))
             contents = self.load_collect_file("udev_rules.d/%s" % dname_file)
             for key, key_name in key_to_udev.items():
@@ -1713,12 +1732,26 @@ class VMBaseClass(TestCase):
         kname = [os.path.basename(line.split()[10])
                  for line in ls_byid.split('\n')
                  if ("virtio-" + serial) in line.split() or
-                    ("scsi-" + serial) in line.split()]
+                    ("scsi-" + serial) in line.split() or
+                    ("wwn-" + serial) in line.split()]
         self.assertEqual(len(kname), 1)
         kname = kname.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
+        # lrwxrwxrwx 1 root root  10 Mar 10 21:28
+        #  ccw-0.0.0000-scsi-0:0:0:0-part1 -> ../../sda1
+        ls_bypath = self.load_collect_file("ls_al_bypath")
+        bypath = [line.split()[8] for line in ls_bypath.split('\n')
+                  if ("../../" + kname) in line.split()]
+        self.assertEqual(len(bypath), 1)
+        bypath = bypath.pop()
+        self.assertIsNotNone(bypath)
+        return bypath
+
     def _kname_to_uuid(self, kname):
         # extract uuid from /dev/disk/by-uuid on /dev/<kname>
         # parsing ls -al output on /dev/disk/by-uuid:
@@ -1908,7 +1941,10 @@ class VMBaseClass(TestCase):
 
     def has_storage_config(self):
         '''check if test used storage config'''
-        return len(self.get_storage_config()) > 0
+        try:
+            return len(self.get_storage_config()) > 0
+        except util.FileMissingError:
+            return False
 
     @skip_if_flag('expected_failure')
     def test_swaps_used(self):
diff --git a/tests/vmtests/releases.py b/tests/vmtests/releases.py
index bf8be2f..3dcb415 100644
--- a/tests/vmtests/releases.py
+++ b/tests/vmtests/releases.py
@@ -21,12 +21,13 @@ class _CentosFromUbuntuBase(_UbuntuBase):
     # base for installing centos tarballs from ubuntu base
     target_distro = "centos"
     target_ftype = "root-tgz"
-    kflavor = None
+    target_kflavor = None
+    kflavor = "generic"
 
 
 class _UbuntuCoreUbuntuBase(_UbuntuBase):
     # base for installing UbuntuCore root-image.xz from ubuntu base
-    target_distro = "ubuntu-core-16"
+    target_distro = "ubuntu-core"
     target_ftype = "root-image.xz"
     kflavor = None
 
@@ -57,7 +58,21 @@ class _UbuntuCore16FromXenialBase(_UbuntuCoreUbuntuBase):
     release = "xenial"
     # release for target
     target_release = "ubuntu-core-16"
-    target_distro = "ubuntu-core"
+
+
+class _UbuntuCore18FromBionicBase(_UbuntuCoreUbuntuBase):
+    # release for boot
+    release = "bionic"
+    # release for target
+    target_release = "ubuntu-core-18"
+
+
+class _UbuntuCore20FromFocalBase(_UbuntuCoreUbuntuBase):
+    # release for boot
+    release = "focal"
+    # release for target
+    target_release = "ubuntu-core-20"
+    mem = "2048"
 
 
 class _Centos66FromXenialBase(_CentosFromUbuntuBase):
@@ -201,6 +216,8 @@ class _CentosReleases(object):
 
 class _UbuntuCoreReleases(object):
     uc16fromxenial = _UbuntuCore16FromXenialBase
+    uc18frombionic = _UbuntuCore18FromBionicBase
+    uc20fromfocal = _UbuntuCore20FromFocalBase
 
 
 base_vm_classes = _Releases
diff --git a/tests/vmtests/test_basic.py b/tests/vmtests/test_basic.py
index 91c05db..e50318d 100644
--- a/tests/vmtests/test_basic.py
+++ b/tests/vmtests/test_basic.py
@@ -14,6 +14,7 @@ from unittest import SkipTest
 class TestBasicAbs(VMBaseClass):
     arch_skip = [
         'arm64',  # arm64 is UEFI only
+        's390x',  # LP: #1806823
     ]
     test_type = 'storage'
     interactive = False
@@ -130,8 +131,12 @@ class TestBasicAbs(VMBaseClass):
         self._test_ptable("blkid_output_diska", expected_ptable)
 
     def test_partition_numbers(self):
-        # disk-d should have partitions 1 2, and 10
-        disk = self._serial_to_kname('disk-d')
+        # pnum_disk should have partitions 1 2, and 10
+        if self.target_release != 'centos66':
+            disk = self._dname_to_kname('pnum_disk')
+        else:
+            disk = self._serial_to_kname('disk-d')
+
         expected = [disk + s for s in ["", "1", "2", "10"]]
         self._test_partition_numbers(disk, expected)
 
@@ -254,15 +259,20 @@ class FocalTestBasic(relbase.focal, TestBasicAbs):
 
 
 class TestBasicScsiAbs(TestBasicAbs):
+    arch_skip = [
+        'arm64',  # arm64 is UEFI only
+    ]
     conf_file = "examples/tests/basic_scsi.yaml"
     disk_driver = 'scsi-hd'
     extra_disks = ['15G', '20G', '25G']
     extra_collect_scripts = [textwrap.dedent("""
         cd OUTPUT_COLLECT_D
-        blkid -o export /dev/sda | cat >blkid_output_sda
-        blkid -o export /dev/sda1 | cat >blkid_output_sda1
-        blkid -o export /dev/sda2 | cat >blkid_output_sda2
-        dev="/dev/disk/by-dname/btrfs_volume";
+        main_disk_id="/dev/disk/by-id/wwn-0x39cc071e72c64cc4"
+        main_disk=$(readlink -f ${main_disk_id})
+        blkid -o export ${main_disk} | cat >blkid_output_main_disk
+        blkid -o export ${main_disk}1 | cat >blkid_output_main_disk-part1
+        blkid -o export ${main_disk}2 | cat >blkid_output_main_disk_part2
+        dev="/dev/disk/by-id/wwn-0x22dc58dc023c7008"
         if command -v btrfs-debug-tree >/dev/null; then
            btrfs-debug-tree -r $dev | awk '/^uuid/ {print $2}' | grep "-"
         else
@@ -271,7 +281,7 @@ class TestBasicScsiAbs(TestBasicAbs):
         fi | cat >btrfs_uuid
 
         # compare via /dev/zero 8MB
-        dev="/dev/disk/by-dname/prep"
+        dev="/dev/disk/by-id/wwn-0x550a270c3a5811c5-part2"
         cmp --bytes=8388608 /dev/zero $dev; echo "$?" > cmp_prep.out
         # extract partition info
         udevadm info --export --query=property $dev | cat >udev_info.out
@@ -283,28 +293,32 @@ class TestBasicScsiAbs(TestBasicAbs):
         expected_ptable = "dos"
         if self.target_arch == "ppc64el":
             expected_ptable = "gpt"
-        self._test_ptable("blkid_output_sda", expected_ptable)
+        self._test_ptable("blkid_output_main_disk", expected_ptable)
 
     def test_partition_numbers(self):
-        # sdd should have partitions 1, 2, and 10
-        disk = "sdd"
+        # pnum_disk should have partitions 1, 2, and 10
+        disk = self._serial_to_kname('0x550a270c3a5811c5')
         expected = [disk + s for s in ["", "1", "2", "10"]]
         self._test_partition_numbers(disk, expected)
 
     def get_fstab_expected(self):
-
         root_kname = (
-            self._dname_to_kname('main_disk_with_in---valid--dname-part1'))
+            self._serial_to_kname('0x39cc071e72c64cc4-part1'))
         home_kname = (
-            self._dname_to_kname('main_disk_with_in---valid--dname-part2'))
-        btrfs_kname = self._dname_to_kname('btrfs_volume')
-        return [(self._kname_to_byuuid(root_kname), '/', 'defaults'),
-                (self._kname_to_byuuid(home_kname), '/home', 'defaults'),
-                (self._kname_to_byuuid(btrfs_kname),
-                 '/btrfs', 'defaults,noatime')]
+            self._serial_to_kname('0x39cc071e72c64cc4-part2'))
+        btrfs_kname = self._serial_to_kname('0x22dc58dc023c7008')
+
+        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')]
 
+    @skip_if_arch('s390x')
     def test_whole_disk_uuid(self):
-        kname = self._dname_to_kname('btrfs_volume')
+        kname = self._serial_to_kname('0x22dc58dc023c7008')
         self._test_whole_disk_uuid(kname, "btrfs_uuid")
 
     def test_partition_is_prep(self):
diff --git a/tests/vmtests/test_fs_battery.py b/tests/vmtests/test_fs_battery.py
index 6c8cfc2..ecd1729 100644
--- a/tests/vmtests/test_fs_battery.py
+++ b/tests/vmtests/test_fs_battery.py
@@ -101,6 +101,9 @@ class TestFsBattery(VMBaseClass):
                 echo "$part umount: FAIL: $out"
         done >> battery-mount-umount
 
+        # collect ext4 features on myext4 partition
+        dumpe2fs /dev/disk/by-label/myext4 > myext4.dump
+
         exit 0
         """)]
 
@@ -196,6 +199,10 @@ class TestFsBattery(VMBaseClass):
             {'/my/bind-over-var-cache/man': 'present',
              '/my/bind-ro-etc/passwd': 'present'}, paths)
 
+    def test_ext4_extra_parameters_used_with_mkfs(self):
+        data = self.load_collect_file("myext4.dump")
+        self.assertNotIn("ext_attr", data)
+
 
 class Centos70XenialTestFsBattery(centos_relbase.centos70_xenial,
                                   TestFsBattery):
diff --git a/tests/vmtests/test_mdadm_bcache.py b/tests/vmtests/test_mdadm_bcache.py
index f8d3e6a..53637ae 100644
--- a/tests/vmtests/test_mdadm_bcache.py
+++ b/tests/vmtests/test_mdadm_bcache.py
@@ -3,7 +3,9 @@
 from . import VMBaseClass
 from .releases import base_vm_classes as relbase
 from .releases import centos_base_vm_classes as centos_relbase
+import re
 import textwrap
+from unittest import SkipTest
 
 
 class TestMdadmAbs(VMBaseClass):
@@ -271,6 +273,19 @@ class TestMirrorbootPartitionsUEFIAbs(TestMdadmAbs):
     uefi = True
     nr_cpus = 2
     dirty_disks = True
+    GRUB_RE = r'(?P<pkg>grub-pc)\s(?P<var>\S+)\smultiselect\s(?P<cfg>.*$)'
+
+    extra_collect_scripts = TestMdadmAbs.extra_collect_scripts + [
+        textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        debconf-get-selections > debconf_selections.txt
+        ls -al /usr/lib/grub/* > usr_lib_grub.txt
+        (cd /boot/efi && find .) | sort >  diska-part1-efi.out
+        mount /dev/disk/by-id/virtio-disk-b-part1 /mnt
+        (cd /mnt && find .) | sort > diskb-part1-efi.out
+        umount /mnt
+        exit 0
+        """)]
 
     def get_fstab_expected(self):
         return [
@@ -280,6 +295,20 @@ class TestMirrorbootPartitionsUEFIAbs(TestMdadmAbs):
              '/var', 'defaults'),
         ]
 
+    def test_grub_debconf_selections(self):
+        """Verify we have grub2/efi_install_devices set correctly."""
+        if self.target_distro not in ["ubuntu", "debian"]:
+            raise SkipTest("debconf-selections not present in distro "
+                           "%s" % self.target_release)
+
+        selections = self.load_collect_file("debconf_selections.txt")
+        found_selections = re.findall(self.GRUB_RE, selections, re.MULTILINE)
+        disks_byid = ['/dev/disk/by-id/virtio-disk-a-part1',
+                      '/dev/disk/by-id/virtio-disk-b-part1']
+        choice = ", ".join(disks_byid)
+        self.assertIn(
+            ('grub-pc', 'grub-efi/install_devices', choice), found_selections)
+
 
 class Centos70TestMirrorbootPartitionsUEFI(centos_relbase.centos70_xenial,
                                            TestMirrorbootPartitionsUEFIAbs):
@@ -315,6 +344,11 @@ 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 TestRaid5bootAbs(TestMdadmAbs):
     # alternative config for more complex setup
diff --git a/tests/vmtests/test_multipath.py b/tests/vmtests/test_multipath.py
index b00303e..7c7e621 100644
--- a/tests/vmtests/test_multipath.py
+++ b/tests/vmtests/test_multipath.py
@@ -1,8 +1,10 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
-from . import VMBaseClass
+from . import VMBaseClass, load_config, sanitize_dname
 from .releases import base_vm_classes as relbase
 from .releases import centos_base_vm_classes as centos_relbase
+from curtin import util
+from curtin.commands.block_meta import DNAME_BYID_KEYS
 
 from unittest import SkipTest
 import os
@@ -29,15 +31,56 @@ class TestMultipathBasicAbs(VMBaseClass):
             systemctl show -- home.mount > systemctl_show_home.mount;
             systemctl status --full home.mount > systemctl_status_home.mount
         }
+        for dev in $(ls /dev/dm-* /dev/sd?); do
+            [ -b $dev ] && {
+                udevadm info --query=property \
+                    --export $dev > udevadm_info_$(basename $dev)
+            }
+        done
+        cat /proc/cmdline > proc_cmdline
         exit 0
         """)]
 
+    def test_dname_rules(self, disk_to_check=None):
+        if self.target_distro != "ubuntu":
+            raise SkipTest("dname not present in non-ubuntu releases")
+
+        print('test_dname_rules: checking disks: %s', disk_to_check)
+        self.output_files_exist(["udev_rules.d"])
+
+        cfg = load_config(self.collect_path("root/curtin-install-cfg.yaml"))
+        stgcfg = cfg.get("storage", {}).get("config", [])
+        disks = [ent for ent in stgcfg if (ent.get('type') == 'disk' and
+                                           'name' in ent)]
+        for disk in disks:
+            if not disk.get('name'):
+                continue
+            dname = sanitize_dname(disk.get('name'))
+            dname_file = "%s.rules" % dname
+            dm_dev = self._dname_to_kname(dname)
+            info = util.load_shell_content(
+                self.load_collect_file("udevadm_info_%s" % dm_dev))
+            contents = self.load_collect_file("udev_rules.d/%s" % dname_file)
+
+            present = [k for k in DNAME_BYID_KEYS if info.get(k)]
+            # xenial and bionic do not have multipath in ephemeral environment
+            # so dnames cannot use DM_UUID in rule files.
+            if self.target_release in ['xenial', 'bionic', 'centos70']:
+                present.remove('DM_UUID')
+            if present:
+                for id_key in present:
+                    value = info[id_key]
+                    if value:
+                        self.assertIn(id_key, contents)
+                        self.assertIn(value, contents)
+
     def test_multipath_disks_match(self):
         sda_data = self.load_collect_file("holders_sda")
         print('sda holders:\n%s' % sda_data)
         sdb_data = self.load_collect_file("holders_sdb")
         print('sdb holders:\n%s' % sdb_data)
-        self.assertEqual(sda_data, sdb_data)
+        self.assertEqual(os.path.basename(sda_data),
+                         os.path.basename(sdb_data))
 
     def test_home_mount_unit(self):
         unit_file = 'systemctl_show_home.mount'
@@ -57,7 +100,41 @@ class TestMultipathBasicAbs(VMBaseClass):
                 [line.split('=') for line in content.splitlines()
                  if line.split('=')[0] in expected_results.keys()]}
 
-        self.assertEqual(sorted(expected_results), sorted(show))
+        self.assertEqual(expected_results, show)
+
+    def get_fstab_expected(self):
+        # xenial and bionic do not have multipath in ephemeral environment
+        # so fstab entries are not DM_UUID based.
+        if self.target_release in ['xenial', 'bionic', 'centos70']:
+            return [
+                (self._kname_to_byuuid('dm-1'), '/', 'defaults'),
+                (self._kname_to_byuuid('dm-2'), '/home', 'defaults,nofail')]
+
+        root = self._dname_to_kname('mpath_a-part1')
+        home = self._dname_to_kname('mpath_a-part2')
+        return [
+            (self._kname_to_uuid_devpath('dm-uuid-part1-mpath', root),
+             '/', 'defaults'),
+            (self._kname_to_uuid_devpath('dm-uuid-part2-mpath', home),
+             '/home', 'defaults,nofail')]
+
+    def test_proc_command_line_has_mp_device(self):
+        cmdline = self.load_collect_file('proc_cmdline')
+        root = [tok for tok in cmdline.split() if tok.startswith('root=')]
+        self.assertEqual(len(root), 1)
+
+        root = root.pop()
+        root = root.split('root=')[1]
+        if self.target_release in ['xenial', 'bionic']:
+            self.assertEqual('/dev/mapper/mpath0-part1', root)
+        elif self.target_release in ['centos70']:
+            self.assertEqual('/dev/mapper/mpath0p1', root)
+        else:
+            dm_dev = self._dname_to_kname('mpath_a-part1')
+            info = util.load_shell_content(
+                self.load_collect_file("udevadm_info_%s" % dm_dev))
+            dev_mapper = '/dev/mapper/' + info['DM_NAME']
+            self.assertEqual(dev_mapper, root)
 
 
 class Centos70TestMultipathBasic(centos_relbase.centos70_xenial,
diff --git a/tests/vmtests/test_multipath_lvm.py b/tests/vmtests/test_multipath_lvm.py
new file mode 100644
index 0000000..39b8587
--- /dev/null
+++ b/tests/vmtests/test_multipath_lvm.py
@@ -0,0 +1,76 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from .releases import base_vm_classes as relbase
+from .releases import centos_base_vm_classes as centos_relbase
+from .test_multipath import TestMultipathBasicAbs
+
+from unittest import SkipTest
+import textwrap
+
+
+class TestMultipathLvmAbs(TestMultipathBasicAbs):
+    conf_file = "examples/tests/multipath-lvm.yaml"
+    dirty_disks = False
+    test_type = 'storage'
+    multipath = True
+    multipath_num_paths = 4
+    disk_driver = 'scsi-hd'
+    extra_disks = []
+    nvme_disks = []
+    extra_collect_scripts = TestMultipathBasicAbs.extra_collect_scripts + [
+        textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        pvs > pvs.out
+        vgs > vgs.out
+        lvs > lvs.out
+        exit 0
+        """)]
+
+    def test_home_mount_unit(self):
+        raise SkipTest('Test case does not have separate home mount')
+
+    def get_fstab_expected(self):
+        root = self._dname_to_kname('root_vg-lv1_root')
+        boot = self._dname_to_kname('root_disk-part2')
+        return [
+            (self._kname_to_uuid_devpath('dm-uuid-LVM', root),
+             '/', 'defaults'),
+            (self._kname_to_uuid_devpath('dm-uuid-part2-mpath', boot),
+             '/boot', 'defaults')]
+
+    def test_proc_command_line_has_mp_device(self):
+        cmdline = self.load_collect_file('proc_cmdline')
+        root = [tok for tok in cmdline.split() if tok.startswith('root=')]
+        self.assertEqual(len(root), 1)
+        root = root.pop()
+        root = root.split('root=')[1]
+        self.assertEqual('/dev/mapper/root_vg-lv1_root', root)
+
+
+class Centos70TestMultipathLvm(centos_relbase.centos70_bionic,
+                               TestMultipathLvmAbs):
+    __test__ = True
+
+
+class BionicTestMultipathLvm(relbase.bionic, TestMultipathLvmAbs):
+    __test__ = True
+
+
+class EoanTestMultipathLvm(relbase.eoan, TestMultipathLvmAbs):
+    __test__ = True
+
+
+class FocalTestMultipathLvm(relbase.focal, TestMultipathLvmAbs):
+    __test__ = True
+
+
+class TestMultipathLvmPartWipeAbs(TestMultipathLvmAbs):
+    conf_file = "examples/tests/multipath-lvm-part-wipe.yaml"
+
+
+class FocalTestMultipathLvmPartWipe(relbase.focal,
+                                    TestMultipathLvmPartWipeAbs):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_bcache.py b/tests/vmtests/test_preserve_bcache.py
new file mode 100644
index 0000000..e2d2a34
--- /dev/null
+++ b/tests/vmtests/test_preserve_bcache.py
@@ -0,0 +1,67 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass, skip_if_flag
+from .releases import base_vm_classes as relbase
+
+import textwrap
+
+
+class TestPreserveBcache(VMBaseClass):
+    arch_skip = [
+        "s390x",  # lp:1565029
+    ]
+    test_type = 'storage'
+    conf_file = 'examples/tests/preserve-bcache.yaml'
+    nr_cpus = 2
+    dirty_disks = False
+    extra_disks = ['2G']
+    extra_collect_scripts = [textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        ls / > ls-root
+        bcache-super-show /dev/vda2 > bcache_super_vda2
+        ls /sys/fs/bcache > bcache_ls
+        cat /sys/block/bcache0/bcache/cache_mode > bcache_cache_mode
+
+        exit 0
+        """)]
+
+    @skip_if_flag('expected_failure')
+    def test_bcache_output_files_exist(self):
+        self.output_files_exist(["bcache_super_vda2", "bcache_ls",
+                                 "bcache_cache_mode"])
+
+    @skip_if_flag('expected_failure')
+    def test_bcache_status(self):
+        bcache_cset_uuid = None
+        for line in self.load_collect_file("bcache_super_vda2").splitlines():
+            if line != "" and line.split()[0] == "cset.uuid":
+                bcache_cset_uuid = line.split()[-1].rstrip()
+        self.assertIsNotNone(bcache_cset_uuid)
+        self.assertTrue(bcache_cset_uuid in
+                        self.load_collect_file("bcache_ls").splitlines())
+
+    @skip_if_flag('expected_failure')
+    def test_bcache_cachemode(self):
+        self.check_file_regex("bcache_cache_mode", r"\[writeback\]")
+
+    @skip_if_flag('expected_failure')
+    def test_proc_cmdline_root_by_uuid(self):
+        self.check_file_regex("proc_cmdline", r"root=UUID=")
+
+    def test_preserved_data_exists(self):
+        self.assertIn('existing', self.load_collect_file('ls-root'))
+
+
+class BionicTestPreserveBcache(relbase.bionic, TestPreserveBcache):
+    __test__ = True
+
+
+class EoanTestPreserveBcache(relbase.eoan, TestPreserveBcache):
+    __test__ = True
+
+
+class FocalTestPreserveBcache(relbase.focal, TestPreserveBcache):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_lvm.py b/tests/vmtests/test_preserve_lvm.py
new file mode 100644
index 0000000..90f15cb
--- /dev/null
+++ b/tests/vmtests/test_preserve_lvm.py
@@ -0,0 +1,80 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass
+from .releases import base_vm_classes as relbase
+
+import json
+import os
+import textwrap
+
+
+class TestLvmPreserveAbs(VMBaseClass):
+    conf_file = "examples/tests/preserve-lvm.yaml"
+    test_type = 'storage'
+    interactive = False
+    extra_disks = ['10G']
+    dirty_disks = False
+    extra_collect_scripts = [textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        lsblk --json --fs -o KNAME,MOUNTPOINT,UUID,FSTYPE > lsblk.json
+        lsblk --fs -P -o KNAME,MOUNTPOINT,UUID,FSTYPE > lsblk.out
+        pvdisplay -C --separator = -o vg_name,pv_name --noheadings > pvs
+        lvdisplay -C --separator = -o lv_name,vg_name --noheadings > lvs
+        pvdisplay > pvdisplay
+        vgdisplay > vgdisplay
+        lvdisplay > lvdisplay
+        ls -al /dev/root_vg/ > dev_root_vg
+        ls / > ls-root
+
+        exit 0
+        """)]
+    conf_replace = {}
+
+    def get_fstab_output(self):
+        rootvg = self._dname_to_kname('root_vg-lv1_root')
+        return [
+            (self._kname_to_uuid_devpath('dm-uuid', rootvg), '/', 'defaults')
+        ]
+
+    def test_output_files_exist(self):
+        self.output_files_exist(["fstab"])
+
+    def test_rootfs_format(self):
+        self.output_files_exist(["lsblk.json"])
+        if os.path.getsize(self.collect_path('lsblk.json')) > 0:
+            lsblk_data = json.load(open(self.collect_path('lsblk.json')))
+            print(json.dumps(lsblk_data, indent=4))
+            [entry] = [entry for entry in lsblk_data.get('blockdevices')
+                       if entry['mountpoint'] == '/']
+            print(entry)
+            self.assertEqual('ext4', entry['fstype'])
+        else:
+            # no json output on older releases
+            self.output_files_exist(["lsblk.out"])
+            lsblk_data = open(self.collect_path('lsblk.out')).readlines()
+            print(lsblk_data)
+            [root] = [line.strip() for line in lsblk_data
+                      if 'MOUNTPOINT="/"' in line]
+            print(root)
+            [fstype] = [val.replace('"', '').split("=")[1]
+                        for val in root.split() if 'FSTYPE' in val]
+            print(fstype)
+            self.assertEqual('ext4', fstype)
+
+    def test_preserved_data_exists(self):
+        self.assertIn('existing', self.load_collect_file('ls-root'))
+
+
+class BionicTestLvmPreserve(relbase.bionic, TestLvmPreserveAbs):
+    __test__ = True
+
+
+class EoanTestLvmPreserve(relbase.eoan, TestLvmPreserveAbs):
+    __test__ = True
+
+
+class FocalTestLvmPreserve(relbase.focal, TestLvmPreserveAbs):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_preserve_partition_wipe_vg.py b/tests/vmtests/test_preserve_partition_wipe_vg.py
new file mode 100644
index 0000000..96346ff
--- /dev/null
+++ b/tests/vmtests/test_preserve_partition_wipe_vg.py
@@ -0,0 +1,59 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass
+from .releases import base_vm_classes as relbase
+
+import textwrap
+
+
+class TestPreserveWipeLvm(VMBaseClass):
+    """ Test that curtin can reuse a partition that was previously in lvm. """
+    conf_file = "examples/tests/preserve-partition-wipe-vg.yaml"
+    extra_disks = ['20G']
+    uefi = False
+    extra_collect_scripts = [textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        ls /opt > ls-opt
+        exit 0
+        """)]
+
+    def test_existing_exists(self):
+        self.assertIn('existing', self.load_collect_file('ls-opt'))
+
+
+class BionicTestPreserveWipeLvm(relbase.bionic, TestPreserveWipeLvm):
+    __test__ = True
+
+
+class EoanTestPreserveWipeLvm(relbase.eoan, TestPreserveWipeLvm):
+    __test__ = True
+
+
+class FocalTestPreserveWipeLvm(relbase.focal, TestPreserveWipeLvm):
+    __test__ = True
+
+
+class TestPreserveWipeLvmSimple(VMBaseClass):
+    conf_file = "examples/tests/preserve-partition-wipe-vg-simple.yaml"
+    uefi = False
+    extra_collect_scripts = [textwrap.dedent("""
+        cd OUTPUT_COLLECT_D
+        ls /opt > ls-opt
+        exit 0
+        """)]
+
+
+class BionicTestPreserveWipeLvmSimple(relbase.bionic,
+                                      TestPreserveWipeLvmSimple):
+    __test__ = True
+
+
+class EoanTestPreserveWipeLvmSimple(relbase.eoan, TestPreserveWipeLvmSimple):
+    __test__ = True
+
+
+class FocalTestPreserveWipeLvmSimple(relbase.focal, TestPreserveWipeLvmSimple):
+    __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
new file mode 100644
index 0000000..749ea24
--- /dev/null
+++ b/tests/vmtests/test_reuse_lvm_member.py
@@ -0,0 +1,34 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass
+from .releases import base_vm_classes as relbase
+
+
+class TestReuseLVMMemberPartition(VMBaseClass):
+    """ Curtin can install to a LVM member if other members are missing. """
+    conf_file = "examples/tests/reuse-lvm-member-partition.yaml"
+    extra_disks = ['10G', '10G']
+    disk_driver = 'scsi-hd'
+    test_stype = 'storage'
+    uefi = True
+
+    def test_simple(self):
+        pass
+
+
+class BionicTestReuseLVMMemberPartition(relbase.bionic,
+                                        TestReuseLVMMemberPartition):
+    __test__ = True
+
+
+class EoanTestReuseLVMMemberPartition(relbase.eoan,
+                                      TestReuseLVMMemberPartition):
+    __test__ = True
+
+
+class FocalTestReuseLVMMemberPartition(relbase.focal,
+                                       TestReuseLVMMemberPartition):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_msdos_partitions.py b/tests/vmtests/test_reuse_msdos_partitions.py
new file mode 100644
index 0000000..f8e20d9
--- /dev/null
+++ b/tests/vmtests/test_reuse_msdos_partitions.py
@@ -0,0 +1,31 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass
+from .releases import base_vm_classes as relbase
+
+
+class TestReuseMSDOSPartitions(VMBaseClass):
+    """ Curtin can reuse MSDOS partitions with flags. """
+    conf_file = "examples/tests/reuse-msdos-partitions.yaml"
+    test_stype = 'storage'
+
+    def test_simple(self):
+        pass
+
+
+class BionicTestReuseMSDOSPartitions(relbase.bionic,
+                                     TestReuseMSDOSPartitions):
+    __test__ = True
+
+
+class EoanTestReuseMSDOSPartitions(relbase.eoan,
+                                   TestReuseMSDOSPartitions):
+    __test__ = True
+
+
+class FocalTestReuseMSDOSPartitions(relbase.focal,
+                                    TestReuseMSDOSPartitions):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_reuse_raid_member.py b/tests/vmtests/test_reuse_raid_member.py
index 0725966..425105f 100644
--- a/tests/vmtests/test_reuse_raid_member.py
+++ b/tests/vmtests/test_reuse_raid_member.py
@@ -16,7 +16,7 @@ class TestReuseRAIDMember(VMBaseClass):
 
 class TestReuseRAIDMemberPartition(VMBaseClass):
     """ Curtin can install to a RAID member if other members are missing. """
-    conf_file = "examples/tests/reuse-raid-member-wipe.yaml"
+    conf_file = "examples/tests/reuse-raid-member-wipe-partition.yaml"
     extra_disks = ['10G', '10G']
     uefi = True
 
diff --git a/tests/vmtests/test_ubuntu_core.py b/tests/vmtests/test_ubuntu_core.py
index a282940..bd62175 100644
--- a/tests/vmtests/test_ubuntu_core.py
+++ b/tests/vmtests/test_ubuntu_core.py
@@ -18,6 +18,7 @@ class TestUbuntuCoreAbs(VMBaseClass):
         cp -a /etc/cloud ./etc_cloud |:
         cp -a /home . |:
         cp -a /var/lib/extrausers . |:
+        find /boot > ./boot.files
 
         exit 0
         """)]
@@ -44,4 +45,12 @@ class TestUbuntuCoreAbs(VMBaseClass):
 class UbuntuCore16TestUbuntuCore(relbase.uc16fromxenial, TestUbuntuCoreAbs):
     __test__ = False
 
+
+class UbuntuCore20TestUbuntuCore(relbase.uc20fromfocal, TestUbuntuCoreAbs):
+    uefi = True
+    __test__ = False
+    mem = 2048
+    nr_cpus = 2
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tools/block-discover-to-config b/tools/block-discover-to-config
index e77eb07..03c7ba7 100755
--- a/tools/block-discover-to-config
+++ b/tools/block-discover-to-config
@@ -28,7 +28,7 @@ def main():
 if __name__ == "__main__":
     try:
         ret = main()
-    except:
+    except Exception:
         traceback.print_exc()
         pdb.post_mortem()
         ret = 1
diff --git a/tools/run-pyflakes b/tools/run-pyflakes
deleted file mode 100755
index 86eb3cc..0000000
--- a/tools/run-pyflakes
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/bin/bash
-# This file is part of curtin. See LICENSE file for copyright and license info.
-
-PYTHON_VERSION=${PYTHON_VERSION:-2}
-CR="
-"
-vmtests=""
-if [ "$PYTHON_VERSION" = "3" ]; then
-    vmtests="tests/vmtests/"
-fi
-pycheck_dirs=(
-    "curtin/"
-    "tests/unittests/"
-    $vmtests
-    "tools/curtin-log-print"
-    "tools/report_webhook_logger"
-    "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-sync-images"
-    "tools/webserv"
-    "tools/write-curtin"
-)
-
-set -f
-if [ $# -eq 0 ]; then
-   files=( "${pycheck_dirs[@]}" )
-else
-   files=( "$@" )
-fi
-
-cmd=( "python${PYTHON_VERSION}" -m "pyflakes" "${files[@]}" )
-
-echo "Running: " "${cmd[@]}" 1>&2
-exec "${cmd[@]}"
-
-# vi: ts=4 expandtab syntax=sh
diff --git a/tools/run-pyflakes3 b/tools/run-pyflakes3
deleted file mode 100755
index 2b12e07..0000000
--- a/tools/run-pyflakes3
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-# This file is part of curtin. See LICENSE file for copyright and license info.
-
-PYTHON_VERSION=3 exec "${0%/*}/run-pyflakes" "$@"
-
-# vi: ts=4 expandtab syntax=sh
diff --git a/tools/schema-validate-storage b/tools/schema-validate-storage
index 771ee5b..e07f860 100755
--- a/tools/schema-validate-storage
+++ b/tools/schema-validate-storage
@@ -20,6 +20,7 @@ def get_configs(conf_input):
                        for f in os.listdir(conf_input)])
     return [conf_input]
 
+
 errors = []
 for conf in get_configs(conf_input):
     # validate entire config
diff --git a/tools/vmtest-filter b/tools/vmtest-filter
index ae427c3..2d7f0a9 100755
--- a/tools/vmtest-filter
+++ b/tools/vmtest-filter
@@ -56,10 +56,11 @@ def main():
     for tc in find_testcases_by_attr(**kwargs):
         print(tc)
 
+
 if __name__ == '__main__':
     try:
         ret = main()
-    except:
+    except Exception:
         traceback.print_exc()
         pdb.post_mortem()
         ret = 1
diff --git a/tools/vmtest-sync-images b/tools/vmtest-sync-images
index 5ee7f9c..f6b8e1d 100755
--- a/tools/vmtest-sync-images
+++ b/tools/vmtest-sync-images
@@ -22,6 +22,7 @@ from tests.vmtests.helpers import (
 def _fmt_list_filter(filter_name, matches):
     return '~'.join((filter_name, '|'.join(matches)))
 
+
 if __name__ == '__main__':
     # Acquire an exclusive lock on IMAGE_DIR. This will prevent jenkins-runner
     # from running while vmtest-sync-images is working.
diff --git a/tox.ini b/tox.ini
index 3a8e40e..6efc3f9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,9 +5,11 @@ envlist =
    py3-flake8,
    py27,
    py3,
+   py3-pyflakes,
    py3-pylint,
    py27-pylint,
    trusty-py27,
+   block-schema,
    xenial-py3
 
 [tox:jenkins]
@@ -29,6 +31,9 @@ basepython = python3
 
 [testenv:py27]
 basepython = python2.7
+# https://github.com/pypa/setuptools/issues/1963
+deps = {[testenv]deps}
+    setuptools<45
 
 # tox uses '--pre' by default to pip install.  We don't want that, and
 # 'pip_pre=False' isn't available until tox version 1.9.
@@ -46,6 +51,11 @@ deps = {[testenv]deps}
     flake8
 commands = {envpython} -m flake8 {posargs:curtin tests/}
 
+[testenv:py3-pyflakes]
+basepython = python3
+deps = pyflakes==2.1.1
+commands = {envpython} -m pyflakes {posargs:curtin/ tests/ tools/}
+
 [testenv:py3-pylint]
 # set basepython because tox 1.6 (trusty) does not support generated environments
 basepython = python3
@@ -58,6 +68,7 @@ commands = {envpython} -m pylint --errors-only {posargs:curtin tests/vmtests}
 # set basepython because tox 1.6 (trusty) does not support generated environments
 basepython = python2.7
 deps = {[testenv]deps}
+   {[testenv:py27]deps}
     pylint==1.8.1
 commands = {envpython} -m pylint --errors-only {posargs:curtin}
 
@@ -68,6 +79,11 @@ deps = {[testenv]deps}
 commands =
     sphinx-build -b html -d doc/_build/doctrees doc/ doc/_build/html
 
+[testenv:block-schema]
+basepython = python3
+commands =
+   {toxinidir}/tools/schema-validate-storage
+
 [testenv:trusty]
 # this environment provides roughly a trusty build environment where
 # where 'make check' is run during package build.  This protects against
@@ -88,6 +104,8 @@ commands =
 
 [testenv:trusty-py27]
 deps = {[testenv:trusty]deps}
+    setuptools<45
+
 basepython = python2.7
 commands = {envpython} {toxinidir}/tools/noproxy nosetests \
     {posargs:tests/unittests}
@@ -108,6 +126,7 @@ deps =
 [testenv:xenial-py27]
 basepython = python27
 deps = {[testenv:xenial]deps}
+   {[testenv:py27]deps}
 commands = {envpython} {toxinidir}/tools/noproxy nosetests \
     {posargs:tests/unittests}
 

Follow ups