← Back to team overview

curtin-dev team mailing list archive

[Merge] ~raharper/curtin:ubuntu-bionic-sru-20200527 into curtin:ubuntu/bionic

 

Ryan Harper has proposed merging ~raharper/curtin:ubuntu-bionic-sru-20200527 into curtin:ubuntu/bionic.

Commit message:
git checkout -b ubuntu/xenial
git reset --hard upstream/ubuntu/xenial
new-upstream-snapshot --no-bugs --sru-bug 1881003

Requested reviews:
  curtin developers (curtin-dev)
Related bugs:
  Bug #1878890 in subiquity: "[Ubuntu Server 20.04 LTS]: Failed Install (subiquity...install_fail/add_info) during partitioning"
  https://bugs.launchpad.net/subiquity/+bug/1878890
  Bug #1880741 in curtin: "Release 20.1"
  https://bugs.launchpad.net/curtin/+bug/1880741
  Bug #1881011 in curtin (Ubuntu): "vmtest: no-reboot prevents installed centos images from reporting success"
  https://bugs.launchpad.net/ubuntu/+source/curtin/+bug/1881011

For more details, see:
https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/384777
-- 
The attached diff has been truncated due to its size.
Your team curtin developers is requested to review the proposed merge of ~raharper/curtin:ubuntu-bionic-sru-20200527 into curtin:ubuntu/bionic.
diff --git a/HACKING.rst b/HACKING.rst
index 58adf76..f2b618d 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -15,11 +15,11 @@ Do these things once
   be listed in the `contributor-agreement-canonical`_ group.  Unfortunately
   there is no easy way to check if an organization or company you are doing
   work for has signed.  If you are unsure or have questions, email
-  `Josh Powers <mailto:josh.powers@xxxxxxxxxxxxx>` or ping powersj in
+  `Rick Harding <mailto:rick.harding@xxxxxxxxxxxxx>` or ping rick_h in
   ``#curtin`` channel via Freenode IRC.
 
   When prompted for 'Project contact' or 'Canonical Project Manager' enter
-  'Josh Powers'.
+  'Rick Harding'.
 
 * Configure git with your email and name for commit messages.
 
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..2e1a0ed 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
@@ -32,6 +34,6 @@ FEATURES = [
     'HAS_VERSION_MODULE',
 ]
 
-__version__ = "19.3"
+__version__ = "20.1"
 
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/block/__init__.py b/curtin/block/__init__.py
index f30c5df..35e3a64 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
@@ -1141,11 +1315,11 @@ def get_supported_filesystems():
     if not os.path.exists(proc_fs):
         raise RuntimeError("Unable to read 'filesystems' from %s" % proc_fs)
 
-    return [l.split('\t')[1].strip()
-            for l in util.load_file(proc_fs).splitlines()]
+    return [line.split('\t')[1].strip()
+            for line 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..c1a8d26 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, file_name)
+                          for file_name in os.listdir(bcache_sys_path)]
+        cache_links = [file_path for file_path in sys_path_links
+                       if os.path.islink(file_path) and (
+                          os.path.basename(file_path).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..bd0f1aa 100644
--- a/curtin/block/lvm.py
+++ b/curtin/block/lvm.py
@@ -13,15 +13,17 @@ 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()]
+            [line.strip().split(_SEP) for line in out.strip().splitlines()]
             if mf == match_key]
 
 
@@ -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 fc7e522..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'],
@@ -100,7 +102,7 @@ DASD = {
         },
         'disk_layout': {
             'type': ['string'],
-            'enum': ['cdl', 'ldl'],
+            'enum': ['cdl', 'ldl', 'not-formatted'],
         },
     },
 }
@@ -122,6 +124,7 @@ DISK = {
         'id': {'$ref': '#/definitions/id'},
         'name': {'$ref': '#/definitions/name'},
         'multipath': {'type': 'string'},
+        'device_id': {'type': 'string'},
         'preserve': {'$ref': '#/definitions/preserve'},
         'wipe': {'$ref': '#/definitions/wipe'},
         'type': {'const': 'disk'},
@@ -165,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'},
     },
 }
@@ -186,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 = {
@@ -200,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'},
@@ -218,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'},
     },
@@ -284,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/apply_net.py b/curtin/commands/apply_net.py
index ddc5056..68cffc2 100644
--- a/curtin/commands/apply_net.py
+++ b/curtin/commands/apply_net.py
@@ -99,6 +99,9 @@ def apply_net(target, network_state=None, network_config=None):
         else:
             ns = net.parse_net_config_data(netcfg.get('network', {}))
 
+            if ns is None:
+                return
+
     if not passthrough:
         LOG.info('Rendering network configuration in target')
         net.render_network_state(target=target, network_state=ns)
diff --git a/curtin/commands/apt_config.py b/curtin/commands/apt_config.py
index 8bd6e79..e7d84c0 100644
--- a/curtin/commands/apt_config.py
+++ b/curtin/commands/apt_config.py
@@ -46,7 +46,7 @@ def get_default_mirrors(arch=None):
        architecture, for more see:
        https://wiki.ubuntu.com/UbuntuDevelopment/PackageArchive#Ports""";
     if arch is None:
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
     if arch in PRIMARY_ARCHES:
         return PRIMARY_ARCH_MIRRORS.copy()
     if arch in PORTS_ARCHES:
@@ -61,7 +61,7 @@ def handle_apt(cfg, target=None):
         standalone command.
     """
     release = distro.lsb_release(target=target)['codename']
-    arch = util.get_architecture(target)
+    arch = distro.get_architecture(target)
     mirrors = find_apt_mirror_info(cfg, arch)
     LOG.debug("Apt Mirror info: %s", mirrors)
 
@@ -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)
@@ -192,7 +188,7 @@ def mirrorurl_to_apt_fileprefix(mirror):
 
 def rename_apt_lists(new_mirrors, target=None):
     """rename_apt_lists - rename apt lists to preserve old cache data"""
-    default_mirrors = get_default_mirrors(util.get_architecture(target))
+    default_mirrors = get_default_mirrors(distro.get_architecture(target))
 
     pre = paths.target_path(target, APT_LISTS)
     for (name, omirror) in default_mirrors.items():
@@ -289,7 +285,7 @@ def generate_sources_list(cfg, release, mirrors, target=None):
         create a source.list file based on a custom or default template
         by replacing mirrors and release in the template
     """
-    default_mirrors = get_default_mirrors(util.get_architecture(target))
+    default_mirrors = get_default_mirrors(distro.get_architecture(target))
     aptsrc = "/etc/apt/sources.list"
     params = {'RELEASE': release}
     for k in mirrors:
@@ -516,7 +512,7 @@ def find_apt_mirror_info(cfg, arch=None):
     """
 
     if arch is None:
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         LOG.debug("got arch for mirror selection: %s", arch)
     pmirror = get_mirror(cfg, "primary", arch)
     LOG.debug("got primary mirror: %s", pmirror)
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..ff0f2e9 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,122 @@ 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
+
+    # gpt and msdos primary partitions look up flag by entry['type']
+    if found_flag is None:
+        (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 +803,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 +831,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 +873,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 +984,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 +1089,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 +1212,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 +1248,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 +1265,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 +1300,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 +1327,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 +1358,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 +1383,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 +1460,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 +1496,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 +1554,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 +1736,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 +1745,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..d66afa7 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
@@ -25,6 +26,7 @@ from curtin.distro import DISTROS
 from curtin.net import deps as ndeps
 from curtin.reporter import events
 from curtin.commands import apply_net, apt_config
+from curtin.commands.install_grub import install_grub
 from curtin.url_helper import get_maas_version
 
 from . import populate_one_subcmd
@@ -306,7 +308,7 @@ def chzdev_prepare_for_import(chzdev_conf):
 
 def get_flash_kernel_pkgs(arch=None, uefi=None):
     if arch is None:
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
     if uefi is None:
         uefi = util.is_uefi_bootable()
     if uefi:
@@ -443,7 +445,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 +465,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 +614,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 +630,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'))
@@ -556,60 +683,21 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
         else:
             instdevs = list(blockdevs)
 
-    env = os.environ.copy()
-
-    replace_default = grubcfg.get('replace_linux_default', True)
-    if str(replace_default).lower() in ("0", "false"):
-        env['REPLACE_GRUB_LINUX_DEFAULT'] = "0"
-    else:
-        env['REPLACE_GRUB_LINUX_DEFAULT'] = "1"
-
-    probe_os = grubcfg.get('probe_additional_os', False)
-    if probe_os not in (False, True):
-        raise ValueError("Unexpected value %s for 'probe_additional_os'. "
-                         "Value must be boolean" % probe_os)
-    env['DISABLE_OS_PROBER'] = "0" if probe_os else "1"
-
-    # if terminal is present in config, but unset, then don't
-    grub_terminal = grubcfg.get('terminal', 'console')
-    if not isinstance(grub_terminal, str):
-        raise ValueError("Unexpected value %s for 'terminal'. "
-                         "Value must be a string" % grub_terminal)
-    if not grub_terminal.lower() == "unmodified":
-        env['GRUB_TERMINAL'] = grub_terminal
-
     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):
+    update_nvram = grubcfg.get('update_nvram', True)
+    if uefi_bootable and update_nvram:
         uefi_remove_old_loaders(grubcfg, target)
 
-    LOG.debug("installing grub to %s [replace_default=%s]",
-              instdevs, replace_default)
-
-    with util.ChrootableTarget(target):
-        args = ['install-grub']
-        if util.is_uefi_bootable():
-            args.append("--uefi")
-            LOG.debug("grubcfg: %s", grubcfg)
-            if grubcfg.get('update_nvram', True):
-                LOG.debug("GRUB UEFI enabling NVRAM updates")
-                args.append("--update-nvram")
-            else:
-                LOG.debug("NOT enabling UEFI nvram updates")
-                LOG.debug("Target system may not boot")
-        args.append('--os-family=%s' % osfamily)
-        args.append(target)
-
-        # capture stdout and stderr joined.
-        join_stdout_err = ['sh', '-c', 'exec "$0" "$@" 2>&1']
-        out, _err = util.subp(
-            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):
+    install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg)
+
+    if uefi_bootable and update_nvram:
+        uefi_remove_duplicate_entries(grubcfg, target)
         uefi_reorder_loaders(grubcfg, target)
 
 
@@ -843,10 +931,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 +984,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 +1001,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 +1037,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 +1082,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)
@@ -970,7 +1106,9 @@ def detect_required_packages(cfg, osfamily=DISTROS.debian):
 
         # skip missing or invalid config items, configs may
         # only have network or storage, not always both
-        if not isinstance(cfg.get(cfg_type), dict):
+        cfg_type_value = cfg.get(cfg_type)
+        if (not isinstance(cfg_type_value, dict) or
+                cfg_type_value.get('config') == 'disabled'):
             continue
 
         cfg_version = cfg[cfg_type].get('version')
@@ -1025,7 +1163,7 @@ def install_missing_packages(cfg, target, osfamily=DISTROS.debian):
                 # signed version.
                 uefi_pkgs.extend(['grub2-efi-x64', 'shim-x64'])
         elif osfamily == DISTROS.debian:
-            arch = util.get_architecture()
+            arch = distro.get_architecture()
             if arch == 'i386':
                 arch = 'ia32'
             uefi_pkgs.append('grub-efi-%s' % arch)
@@ -1245,13 +1383,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 +1528,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)
 
@@ -1572,17 +1715,25 @@ def builtin_curthooks(cfg, target, state):
         elif osfamily == DISTROS.redhat:
             redhat_update_initramfs(target, cfg)
 
-    # As a rule, ARMv7 systems don't use grub. This may change some
-    # day, but for now, assume no. They do require the initramfs
-    # to be updated, and this also triggers boot loader setup via
-    # flash-kernel.
-    if (machine.startswith('armv7') or
-            machine.startswith('s390x') or
-            machine.startswith('aarch64') and not util.is_uefi_bootable()):
-        return
+    with events.ReportEventStack(
+            name=stack_prefix + '/configuring-bootloader',
+            reporting_enabled=True, level="INFO",
+            description="configuring target system bootloader"):
 
-    # all other paths lead to grub
-    setup_grub(cfg, target, osfamily=osfamily)
+        # As a rule, ARMv7 systems don't use grub. This may change some
+        # day, but for now, assume no. They do require the initramfs
+        # to be updated, and this also triggers boot loader setup via
+        # flash-kernel.
+        if (machine.startswith('armv7') or
+                machine.startswith('s390x') or
+                machine.startswith('aarch64') and not util.is_uefi_bootable()):
+            return
+
+        with events.ReportEventStack(
+                name=stack_prefix + '/install-grub',
+                reporting_enabled=True, level="INFO",
+                description="installing grub to target devices"):
+            setup_grub(cfg, target, osfamily=osfamily)
 
 
 def curthooks(args):
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
new file mode 100644
index 0000000..777aa35
--- /dev/null
+++ b/curtin/commands/install_grub.py
@@ -0,0 +1,406 @@
+import os
+import re
+import platform
+import shutil
+import sys
+
+from curtin import block
+from curtin import config
+from curtin import distro
+from curtin import util
+from curtin.log import LOG
+from curtin.paths import target_path
+from curtin.reporter import events
+from . import populate_one_subcmd
+
+CMD_ARGUMENTS = (
+    ((('-t', '--target'),
+      {'help': 'operate on target. default is env[TARGET_MOUNT_POINT]',
+       'action': 'store', 'metavar': 'TARGET', 'default': None}),
+     (('-c', '--config'),
+      {'help': 'operate on config. default is env[CONFIG]',
+       'action': 'store', 'metavar': 'CONFIG', 'default': None}),
+     )
+)
+
+GRUB_MULTI_INSTALL = '/usr/lib/grub/grub-multi-install'
+
+
+def get_grub_package_name(target_arch, uefi, rhel_ver=None):
+    """Determine the correct grub distro package name.
+
+    :param: target_arch: string specifying the target system architecture
+    :param: uefi: boolean indicating if system is booted via UEFI or not
+    :param: rhel_ver: string specifying the major Redhat version in use.
+    :returns: tuple of strings, grub package name and grub target name
+    """
+    if target_arch is None:
+        raise ValueError('Missing target_arch parameter')
+
+    if uefi is None:
+        raise ValueError('Missing uefi parameter')
+
+    if 'ppc64' in target_arch:
+        return ('grub-ieee1275', 'powerpc-ieee1275')
+    if uefi:
+        if target_arch == 'amd64':
+            grub_name = 'grub-efi-%s' % target_arch
+            grub_target = "x86_64-efi"
+        elif target_arch == 'x86_64':
+            # centos 7+, no centos6 support
+            # grub2-efi-x64 installs a signed grub bootloader
+            grub_name = "grub2-efi-x64"
+            grub_target = "x86_64-efi"
+        elif target_arch == 'arm64':
+            grub_name = 'grub-efi-%s' % target_arch
+            grub_target = "arm64-efi"
+        elif target_arch == 'i386':
+            grub_name = 'grub-efi-ia32'
+            grub_target = 'i386-efi'
+        else:
+            raise ValueError('Unsupported UEFI arch: %s' % target_arch)
+    else:
+        grub_target = 'i386-pc'
+        if target_arch in ['i386', 'amd64']:
+            grub_name = 'grub-pc'
+        elif target_arch == 'x86_64':
+            if rhel_ver == '6':
+                grub_name = 'grub'
+            elif rhel_ver in ['7', '8']:
+                grub_name = 'grub2-pc'
+            else:
+                raise ValueError('Unsupported RHEL version: %s', rhel_ver)
+        else:
+            raise ValueError('Unsupported arch: %s' % target_arch)
+
+    return (grub_name, grub_target)
+
+
+def get_grub_config_file(target=None, osfamily=None):
+    """Return the filename used to configure grub.
+
+    :param: osfamily: string specifying the target os family being configured
+    :returns: string, path to the osfamily grub config file
+    """
+    if not osfamily:
+        osfamily = distro.get_osfamily(target=target)
+
+    if osfamily == distro.DISTROS.debian:
+        # to avoid tripping prompts on upgrade LP: #564853
+        return '/etc/default/grub.d/50-curtin-settings.cfg'
+
+    return '/etc/default/grub'
+
+
+def prepare_grub_dir(target, grub_cfg):
+    util.ensure_dir(os.path.dirname(target_path(target, grub_cfg)))
+
+    # LP: #1179940 . The 50-cloudig-settings.cfg file is written by the cloud
+    # images build and defines/override some settings. Disable it.
+    ci_cfg = target_path(target,
+                         os.path.join(
+                             os.path.dirname(grub_cfg),
+                             "50-cloudimg-settings.cfg"))
+
+    if os.path.exists(ci_cfg):
+        LOG.debug('grub: moved %s out of the way', ci_cfg)
+        shutil.move(ci_cfg, ci_cfg + '.disabled')
+
+
+def get_carryover_params(distroinfo):
+    # return a string to append to installed systems boot parameters
+    # it may include a '--' after a '---'
+    # see LP: 1402042 for some history here.
+    # this is similar to 'user-params' from d-i
+    cmdline = util.load_file('/proc/cmdline')
+    preferred_sep = '---'  # KERNEL_CMDLINE_COPY_TO_INSTALL_SEP
+    legacy_sep = '--'
+
+    def wrap(sep):
+        return ' ' + sep + ' '
+
+    sections = []
+    if wrap(preferred_sep) in cmdline:
+        sections = cmdline.split(wrap(preferred_sep))
+    elif wrap(legacy_sep) in cmdline:
+        sections = cmdline.split(wrap(legacy_sep))
+    else:
+        extra = ""
+        lead = cmdline
+
+    if sections:
+        lead = sections[0]
+        extra = " ".join(sections[1:])
+
+    carry_extra = []
+    if extra:
+        for tok in extra.split():
+            if re.match(r'(BOOTIF=.*|initrd=.*|BOOT_IMAGE=.*)', tok):
+                continue
+            carry_extra.append(tok)
+
+    carry_lead = []
+    for tok in lead.split():
+        if tok in carry_extra:
+            continue
+        if tok.startswith('console='):
+            carry_lead.append(tok)
+
+    # always append rd.auto=1 for redhat family
+    if distroinfo.family == distro.DISTROS.redhat:
+        carry_extra.append('rd.auto=1')
+
+    return carry_lead + carry_extra
+
+
+def replace_grub_cmdline_linux_default(target, new_args):
+    # we always update /etc/default/grub to avoid "hiding" the override in
+    # a grub.d directory.
+    newcontent = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+    target_grubconf = target_path(target, '/etc/default/grub')
+    content = ""
+    if os.path.exists(target_grubconf):
+        content = util.load_file(target_grubconf)
+    existing = re.search(
+        r'GRUB_CMDLINE_LINUX_DEFAULT=.*', content, re.MULTILINE)
+    if existing:
+        omode = 'w+'
+        updated_content = content[:existing.start()]
+        updated_content += newcontent
+        updated_content += content[existing.end():]
+    else:
+        omode = 'a+'
+        updated_content = newcontent + '\n'
+
+    util.write_file(target_grubconf, updated_content, omode=omode)
+    LOG.debug('updated %s to set: %s', target_grubconf, newcontent)
+
+
+def write_grub_config(target, grubcfg, grub_conf, new_params):
+    replace_default = config.value_as_boolean(
+        grubcfg.get('replace_linux_default', True))
+    if replace_default:
+        replace_grub_cmdline_linux_default(target, new_params)
+
+    probe_os = config.value_as_boolean(
+        grubcfg.get('probe_additional_os', False))
+    if not probe_os:
+        probe_content = [
+            ('# Curtin disable grub os prober that might find other '
+             'OS installs.'),
+            'GRUB_DISABLE_OS_PROBER="true"',
+            '']
+        util.write_file(target_path(target, grub_conf),
+                        "\n".join(probe_content), omode='a+')
+
+    # if terminal is present in config, but unset, then don't
+    grub_terminal = grubcfg.get('terminal', 'console')
+    if not isinstance(grub_terminal, str):
+        raise ValueError("Unexpected value %s for 'terminal'. "
+                         "Value must be a string" % grub_terminal)
+    if not grub_terminal.lower() == "unmodified":
+        terminal_content = [
+            '# Curtin configured GRUB_TERMINAL value',
+            'GRUB_TERMINAL="%s"' % grub_terminal]
+        util.write_file(target_path(target, grub_conf),
+                        "\n".join(terminal_content), omode='a+')
+
+
+def find_efi_loader(target, bootid):
+    efi_path = '/boot/efi/EFI'
+    possible_loaders = [
+        os.path.join(efi_path, bootid, 'shimx64.efi'),
+        os.path.join(efi_path, 'BOOT', 'BOOTX64.EFI'),
+        os.path.join(efi_path, bootid, 'grubx64.efi'),
+    ]
+    for loader in possible_loaders:
+        tloader = target_path(target, path=loader)
+        if os.path.exists(tloader):
+            LOG.debug('find_efi_loader: found %s', loader)
+            return loader
+    return None
+
+
+def get_efi_disk_part(devices):
+    for disk in devices:
+        (parent, partnum) = block.get_blockdev_for_partition(disk)
+        if partnum:
+            return (parent, partnum)
+
+    return (None, None)
+
+
+def get_grub_install_command(uefi, distroinfo, target):
+    grub_install_cmd = 'grub-install'
+    if distroinfo.family == distro.DISTROS.debian:
+        # prefer grub-multi-install if present
+        if uefi and os.path.exists(target_path(target, GRUB_MULTI_INSTALL)):
+            grub_install_cmd = GRUB_MULTI_INSTALL
+    elif distroinfo.family == distro.DISTROS.redhat:
+        grub_install_cmd = 'grub2-install'
+
+    LOG.debug('Using grub install command: %s', grub_install_cmd)
+    return grub_install_cmd
+
+
+def gen_uefi_install_commands(grub_name, grub_target, grub_cmd, update_nvram,
+                              distroinfo, devices, target):
+    install_cmds = [['efibootmgr', '-v']]
+    post_cmds = []
+    bootid = distroinfo.variant
+    efidir = '/boot/efi'
+    if distroinfo.family == distro.DISTROS.debian:
+        install_cmds.append(['dpkg-reconfigure', grub_name])
+        install_cmds.append(['update-grub'])
+    elif distroinfo.family == distro.DISTROS.redhat:
+        loader = find_efi_loader(target, bootid)
+        if loader and update_nvram:
+            grub_cmd = None  # don't install just add entry
+            efi_disk, efi_part_num = get_efi_disk_part(devices)
+            install_cmds.append(['efibootmgr', '--create', '--write-signature',
+                                 '--label', bootid, '--disk', efi_disk,
+                                 '--part', efi_part_num, '--loader', loader])
+            post_cmds.append(['grub2-mkconfig', '-o',
+                              '/boot/efi/EFI/%s/grub.cfg' % bootid])
+        else:
+            post_cmds.append(['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'])
+    else:
+        raise ValueError("Unsupported os family for grub "
+                         "install: %s" % distroinfo.family)
+
+    if grub_cmd == GRUB_MULTI_INSTALL:
+        # grub-multi-install is called with no arguments
+        install_cmds.append([grub_cmd])
+    elif grub_cmd:
+        install_cmds.append(
+            [grub_cmd, '--target=%s' % grub_target,
+             '--efi-directory=%s' % efidir, '--bootloader-id=%s' % bootid,
+             '--recheck'] + ([] if update_nvram else ['--no-nvram']))
+
+    # check efi boot menu before and after
+    post_cmds.append(['efibootmgr', '-v'])
+
+    return (install_cmds, post_cmds)
+
+
+def gen_install_commands(grub_name, grub_cmd, distroinfo, devices,
+                         rhel_ver=None):
+    install_cmds = []
+    post_cmds = []
+    if distroinfo.family == distro.DISTROS.debian:
+        install_cmds.append(['dpkg-reconfigure', grub_name])
+        install_cmds.append(['update-grub'])
+    elif distroinfo.family == distro.DISTROS.redhat:
+        if rhel_ver in ["7", "8"]:
+            post_cmds.append(
+                ['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'])
+        else:
+            raise ValueError('Unsupported "rhel_ver" value: %s' % rhel_ver)
+    else:
+        raise ValueError("Unsupported os family for grub "
+                         "install: %s" % distroinfo.family)
+    for dev in devices:
+        install_cmds.append([grub_cmd, dev])
+
+    return (install_cmds, post_cmds)
+
+
+def check_target_arch_machine(target, arch=None, machine=None, uefi=None):
+    """ Check target arch and machine type are grub supported. """
+    if not arch:
+        arch = distro.get_architecture(target=target)
+
+    if not machine:
+        machine = platform.machine()
+
+    errmsg = "Grub is not supported on arch=%s machine=%s" % (arch, machine)
+    # s390x uses zipl
+    if arch == "s390x":
+        raise RuntimeError(errmsg)
+
+    # As a rule, ARMv7 systems don't use grub. This may change some
+    # day, but for now, assume no. They do require the initramfs
+    # to be updated, and this also triggers boot loader setup via
+    # flash-kernel.
+    if (machine.startswith('armv7') or
+            machine.startswith('s390x') or
+            machine.startswith('aarch64') and not uefi):
+        raise RuntimeError(errmsg)
+
+
+def install_grub(devices, target, uefi=None, grubcfg=None):
+    """Install grub to devices inside target chroot.
+
+    :param: devices: List of block device paths to install grub upon.
+    :param: target: A string specifying the path to the chroot mountpoint.
+    :param: uefi: A boolean set to True if system is UEFI bootable otherwise
+                  False.
+    :param: grubcfg: An config dict with grub config options.
+    """
+
+    if not devices:
+        raise ValueError("Invalid parameter 'devices': %s" % devices)
+
+    if not target:
+        raise ValueError("Invalid parameter 'target': %s" % target)
+
+    LOG.debug("installing grub to target=%s devices=%s [replace_defaults=%s]",
+              target, devices, grubcfg.get('replace_default'))
+    update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', False))
+    distroinfo = distro.get_distroinfo(target=target)
+    target_arch = distro.get_architecture(target=target)
+    rhel_ver = (distro.rpm_get_dist_id(target)
+                if distroinfo.family == distro.DISTROS.redhat else None)
+
+    check_target_arch_machine(target, arch=target_arch, uefi=uefi)
+    grub_name, grub_target = get_grub_package_name(target_arch, uefi, rhel_ver)
+    grub_conf = get_grub_config_file(target, distroinfo.family)
+    new_params = get_carryover_params(distroinfo)
+    prepare_grub_dir(target, grub_conf)
+    write_grub_config(target, grubcfg, grub_conf, new_params)
+    grub_cmd = get_grub_install_command(uefi, distroinfo, target)
+    if uefi:
+        install_cmds, post_cmds = gen_uefi_install_commands(
+            grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+            devices, target)
+    else:
+        install_cmds, post_cmds = gen_install_commands(
+            grub_name, grub_cmd, distroinfo, devices, rhel_ver)
+
+    env = os.environ.copy()
+    env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+    LOG.debug('Grub install cmds:\n%s', str(install_cmds + post_cmds))
+    with util.ChrootableTarget(target) as in_chroot:
+        for cmd in install_cmds + post_cmds:
+            in_chroot.subp(cmd, env=env, capture=True)
+
+
+def install_grub_main(args):
+    state = util.load_command_environment()
+
+    if args.target is not None:
+        target = args.target
+    else:
+        target = state['target']
+
+    if target is None:
+        sys.stderr.write("Unable to find target.  "
+                         "Use --target or set TARGET_MOUNT_POINT\n")
+        sys.exit(2)
+
+    cfg = config.load_command_config(args, state)
+    stack_prefix = state.get('report_stack_prefix', '')
+    uefi = util.is_uefi_bootable()
+    grubcfg = cfg.get('grub')
+    with events.ReportEventStack(
+            name=stack_prefix, reporting_enabled=True, level="INFO",
+            description="Installing grub to target devices"):
+        install_grub(args.devices, target, uefi=uefi, grubcfg=grubcfg)
+    sys.exit(0)
+
+
+def POPULATE_SUBCMD(parser):
+    populate_one_subcmd(parser, CMD_ARGUMENTS, install_grub_main)
+
+# vi: ts=4 expandtab syntax=python
diff --git a/curtin/commands/net_meta.py b/curtin/commands/net_meta.py
index fdb909e..5af9391 100644
--- a/curtin/commands/net_meta.py
+++ b/curtin/commands/net_meta.py
@@ -78,6 +78,9 @@ def net_meta(args):
     if util.run_hook_if_exists(args.target, 'network-config'):
         sys.exit(0)
 
+    if args.mode == "disabled":
+        sys.exit(0)
+
     state = util.load_command_environment()
     cfg = config.load_command_config(args, state)
     if cfg.get("network") is not None:
@@ -134,7 +137,7 @@ def net_meta(args):
 
     if not target:
         raise Exception(
-            "No target given for mode = '%s'.  No where to write content: %s" %
+            "No target given for mode = '%s'. Nowhere to write content: %s" %
             (args.mode, content))
 
     LOG.debug("writing to file %s with network config: %s", target, content)
@@ -160,7 +163,7 @@ CMD_ARGUMENTS = (
        'action': 'store', 'metavar': 'TARGET',
        'default': os.environ.get('TARGET_MOUNT_POINT')}),
      ('mode', {'help': 'meta-mode to use',
-               'choices': ['dhcp', 'copy', 'auto', 'custom']})
+               'choices': ['dhcp', 'copy', 'auto', 'custom', 'disabled']})
      )
 )
 
diff --git a/curtin/deps/__init__.py b/curtin/deps/__init__.py
index 714ef18..a9f38d1 100644
--- a/curtin/deps/__init__.py
+++ b/curtin/deps/__init__.py
@@ -5,13 +5,16 @@ import sys
 
 from curtin.util import (
     ProcessExecutionError,
-    get_architecture,
     is_uefi_bootable,
     subp,
     which,
 )
 
-from curtin.distro import install_packages, lsb_release
+from curtin.distro import (
+    get_architecture,
+    install_packages,
+    lsb_release,
+    )
 
 REQUIRED_IMPORTS = [
     # import string to execute, python2 package, python3 package
diff --git a/curtin/distro.py b/curtin/distro.py
index ed178bd..43b0c19 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'))
@@ -340,6 +357,7 @@ def rpm_get_dist_id(target=None):
     """Use rpm command to extract the '%rhel' distro macro which returns
        the major os version id (6, 7, 8).  This works for centos or rhel
     """
+    # rpm requires /dev /sys and /proc be mounted, use ChrootableTarget
     with ChrootableTarget(target) as in_chroot:
         dist, _ = in_chroot.subp(['rpm', '-E', '%rhel'], capture=True)
     return dist.rstrip()
@@ -412,6 +430,7 @@ def has_pkg_available(pkg, target=None, osfamily=None):
 
 
 def get_installed_packages(target=None):
+    out = None
     if which('dpkg-query', target=target):
         (out, _) = subp(['dpkg-query', '--list'], target=target, capture=True)
     elif which('rpm', target=target):
@@ -532,4 +551,30 @@ def fstab_header():
 #
 # <file system> <mount point>   <type>  <options>       <dump>  <pass>""")
 
+
+def dpkg_get_architecture(target=None):
+    out, _ = subp(['dpkg', '--print-architecture'], capture=True,
+                  target=target)
+    return out.strip()
+
+
+def rpm_get_architecture(target=None):
+    # rpm requires /dev /sys and /proc be mounted, use ChrootableTarget
+    with ChrootableTarget(target) as in_chroot:
+        out, _ = in_chroot.subp(['rpm', '-E', '%_arch'], capture=True)
+    return out.strip()
+
+
+def get_architecture(target=None, osfamily=None):
+    if not osfamily:
+        osfamily = get_osfamily(target=target)
+
+    if osfamily == DISTROS.debian:
+        return dpkg_get_architecture(target=target)
+
+    if osfamily == DISTROS.redhat:
+        return rpm_get_architecture(target=target)
+
+    raise ValueError("Unhandled osfamily=%s" % osfamily)
+
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/net/__init__.py b/curtin/net/__init__.py
index ef2ba26..3b02f9d 100644
--- a/curtin/net/__init__.py
+++ b/curtin/net/__init__.py
@@ -252,10 +252,12 @@ def parse_net_config_data(net_config):
     """
     state = None
     if 'version' in net_config and 'config' in net_config:
-        ns = network_state.NetworkState(version=net_config.get('version'),
-                                        config=net_config.get('config'))
-        ns.parse_config()
-        state = ns.network_state
+        # For disabled config, we will not return any network state
+        if net_config["config"] != "disabled":
+            ns = network_state.NetworkState(version=net_config.get('version'),
+                                            config=net_config.get('config'))
+            ns.parse_config()
+            state = ns.network_state
 
     return state
 
diff --git a/curtin/net/deps.py b/curtin/net/deps.py
index fd9e3c0..f912d1d 100644
--- a/curtin/net/deps.py
+++ b/curtin/net/deps.py
@@ -23,8 +23,10 @@ def network_config_required_packages(network_config, mapping=None):
 
     # v1 has 'config' key and uses type: devtype elements
     if 'config' in network_config:
-        dev_configs = set(device['type']
-                          for device in network_config['config'])
+        netconf = network_config['config']
+        dev_configs = set() if netconf == 'disabled' else set(
+            device['type'] for device in netconf)
+
     else:
         # v2 has no config key
         dev_configs = set()
diff --git a/curtin/net/network_state.py b/curtin/net/network_state.py
index ab0f277..d8a9e7d 100644
--- a/curtin/net/network_state.py
+++ b/curtin/net/network_state.py
@@ -21,7 +21,9 @@ def from_state_file(state_file):
 class NetworkState:
     def __init__(self, version=NETWORK_STATE_VERSION, config=None):
         self.version = version
-        self.config = config
+
+        self.config = [] if config in [None, 'disabled'] else config
+
         self.network_state = {
             'interfaces': {},
             'routes': [],
diff --git a/curtin/storage_config.py b/curtin/storage_config.py
index cdfdb70..494b142 100644
--- a/curtin/storage_config.py
+++ b/curtin/storage_config.py
@@ -11,6 +11,37 @@ 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'),
+    '0X83': ('linux', '83'),
+    '0X85': ('extended', 'f'),
+    '0XC5': ('extended', 'f'),
+}
+
+MBR_BOOT_FLAG = '0x80'
+
+PTABLE_TYPE_MAP = dict(GPT_GUID_TO_CURTIN_MAP, **MBR_TYPE_TO_CURTIN_MAP)
 
 StorageConfig = namedtuple('StorageConfig', ('type', 'schema'))
 STORAGE_CONFIG_TYPES = {
@@ -40,7 +71,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': (
@@ -126,7 +157,7 @@ def _stype_to_deps(stype):
     depends_keys = {
         'bcache': {'backing_device', 'cache_device'},
         'dasd': set(),
-        'disk': {'device_id'},
+        'disk': set(),
         'dm_crypt': {'volume'},
         'format': {'volume'},
         'lvm_partition': {'volgroup'},
@@ -144,6 +175,7 @@ def _stype_to_order_key(stype):
     default_sort = {'id'}
     order_key = {
         'bcache': {'name'},
+        'dasd': default_sort,
         'disk': default_sort,
         'dm_crypt': default_sort,
         'format': default_sort,
@@ -170,7 +202,7 @@ def _validate_dep_type(source_id, dep_key, dep_id, sconfig):
         'bcache': {'bcache', 'disk', 'dm_crypt', 'lvm_partition',
                    'partition', 'raid'},
         'dasd': {},
-        'disk': {'device_id'},
+        'disk': {'dasd'},
         'dm_crypt': {'bcache', 'disk', 'dm_crypt', 'lvm_partition',
                      'partition', 'raid'},
         'format': {'bcache', 'disk', 'dm_crypt', 'lvm_partition',
@@ -436,9 +468,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):
@@ -456,9 +488,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
@@ -647,7 +682,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)
@@ -660,34 +695,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.
@@ -704,7 +717,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
 
@@ -743,7 +757,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':
@@ -753,6 +768,19 @@ class BlockdevParser(ProbertParser):
             # set wwn, serial, and path
             entry.update(uniq_ids)
 
+            # disk entry for dasds needs device_id and check for vtoc ptable
+            if devname.startswith('/dev/dasd'):
+                device_id = (
+                    blockdev_data.get('ID_PATH', '').replace('ccw-', ''))
+                if device_id:
+                    entry['device_id'] = device_id
+
+                # if dasd has been formatted, attrs.size is non-zero
+                # formatted dasds have ptable type of 'vtoc'
+                dasd_size = blockdev_data.get('attrs', {}).get('size')
+                if dasd_size and dasd_size != "0":
+                    entry['ptable'] = 'vtoc'
+
             if 'ID_PART_TABLE_TYPE' in blockdev_data:
                 ptype = blockdev_data['ID_PART_TABLE_TYPE']
                 if ptype in schemas._ptables:
@@ -793,12 +821,20 @@ 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)
-
-            # logical partitions are not tagged in data, however
-            # the partition number > 4 (ie, not primary nor extended)
-            if ptable and ptable.get('label') == 'dos' and entry['number'] > 4:
-                flag_name = 'logical'
+            flag_name, _flag_code = ptable_uuid_to_flag_entry(ptype)
+
+            if ptable and ptable.get('label') == 'dos':
+                # if the boot flag is set, use this as the flag, logical
+                # flag is not required as we can determine logical via
+                # partition number
+                ptype_flag = blockdev_data.get('ID_PART_ENTRY_FLAGS')
+                if ptype_flag in [MBR_BOOT_FLAG]:
+                    flag_name = 'boot'
+                else:
+                    # logical partitions are not tagged in data, however
+                    # the partition number > 4 (ie, not primary nor extended)
+                    if entry['number'] > 4:
+                        flag_name = 'logical'
 
             if flag_name:
                 entry['flag'] = flag_name
@@ -843,7 +879,7 @@ class FilesystemParser(ProbertParser):
                 continue
 
             # ignore types that we cannot create
-            if data['TYPE'] not in schemas._fstypes:
+            if data.get('TYPE') not in schemas._fstypes:
                 continue
 
             entry = self.asdict(volume_id, data)
@@ -940,6 +976,43 @@ class LvmParser(ProbertParser):
         return (configs, errors)
 
 
+class DasdParser(ProbertParser):
+
+    probe_data_key = 'dasd'
+
+    def asdict(self, dasd_config):
+        dasd_name = os.path.basename(dasd_config['name'])
+        device_id = dasd_config['device_id']
+        blocksize = dasd_config['blocksize']
+        disk_layout = dasd_config['disk_layout']
+
+        return {'type': 'dasd',
+                'id': 'dasd-%s' % dasd_name,
+                'device_id': device_id,
+                'blocksize': blocksize,
+                'mode': 'full' if disk_layout == 'not-formatted' else 'quick',
+                'disk_layout': disk_layout}
+
+    def parse(self):
+        """parse probert 'dasd' data format.
+
+            returns tuple of lists: (configs, errors)
+            contain configs of type:dasd and any errors.
+        """
+        configs = []
+        errors = []
+        for dasd_name, dasd_config in self.class_data.items():
+            entry = self.asdict(dasd_config)
+            if entry:
+                try:
+                    validate_config(entry)
+                except ValueError as e:
+                    errors.append(e)
+                    continue
+                configs.append(entry)
+        return (configs, errors)
+
+
 class DmcryptParser(ProbertParser):
 
     probe_data_key = 'dmcrypt'
@@ -1191,6 +1264,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
@@ -1201,6 +1285,7 @@ def extract_storage_config(probe_data, strict=False):
     convert_map = {
         'bcache': BcacheParser,
         'blockdev': BlockdevParser,
+        'dasd': DasdParser,
         'dmcrypt': DmcryptParser,
         'filesystem': FilesystemParser,
         'lvm': LvmParser,
@@ -1218,6 +1303,7 @@ def extract_storage_config(probe_data, strict=False):
         errors.extend(found_errs)
 
     LOG.debug('Sorting extracted configurations')
+    dasd = [cfg for cfg in configs if cfg.get('type') == 'dasd']
     disk = [cfg for cfg in configs if cfg.get('type') == 'disk']
     part = [cfg for cfg in configs if cfg.get('type') == 'partition']
     format = [cfg for cfg in configs if cfg.get('type') == 'format']
@@ -1230,8 +1316,8 @@ def extract_storage_config(probe_data, strict=False):
     zpool = [cfg for cfg in configs if cfg.get('type') == 'zpool']
     zfs = [cfg for cfg in configs if cfg.get('type') == 'zfs']
 
-    ordered = (disk + part + format + lvols + lparts + raids + dmcrypts +
-               mounts + bcache + zpool + zfs)
+    ordered = (dasd + disk + part + format + lvols + lparts + raids +
+               dmcrypts + mounts + bcache + zpool + zfs)
 
     final_config = {'storage': {'version': 1, 'config': ordered}}
     try:
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..be063d7 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
@@ -788,12 +799,6 @@ def get_paths(curtin_exe=None, lib=None, helpers=None):
     return({'curtin_exe': curtin_exe, 'lib': mydir, 'helpers': helpers})
 
 
-def get_architecture(target=None):
-    out, _ = subp(['dpkg', '--print-architecture'], capture=True,
-                  target=target)
-    return out.strip()
-
-
 def find_newer(src, files):
     mtime = os.stat(src).st_mtime
     return [f for f in files if
@@ -856,7 +861,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 +884,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 caa2f0b..c0f4bbf 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,68 @@
+curtin (20.1-2-g42a9667f-0ubuntu1~18.04.1) bionic; urgency=medium
+
+  * New upstream snapshot. (LP: #1881003)
+    - vmtests: update skiptests
+    - vmtest: allow installed centos images to reboot
+    - Release 20.1
+    - Handle multiple separators which were found in TestAllindata vmtest
+    - verify_ptable_flag: dos primary partitions use ptable_uuid map for flag
+    - net_meta: add disabled mode to skip writing any network config
+      [Lucas Moura]
+    - vmtest: trigger guest panic to fail fast
+    - Replace grub-shell-helper with install_grub command
+    - vmtest-sync-images: update the URL of the maas streams [Paride Legovini]
+    - Replace references to old team manager with new team manager
+      [James Falcon]
+    - tox: pin flake8 to version and add a tip-flake8 environment
+      [Paride Legovini]
+    - Fix flake8 E741 warning [Lucas Moura]
+    - Makefile: make adjustments to call lint/style tools via python module
+    - block-discover: ignore invalid id_serial/id_wwn values
+    - Fix handing of reusing msdos partitions and flags
+    - block.detect_multipath: ignore fake "devices" from /proc/mounts
+      [Michael Hudson-Doyle]
+    - udev: use shlex.quote when shlex.split errors on shell-escape chars
+    - lvm: don't use vgscan --mknodes
+    - vmtest: rsync don't cross filesystem boundaries when copying
+    - vmtest: basic/basic_scsi adjust collect/tests for unstable device names
+    - 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]
+    - 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
+    - unittest: do not allow util.subp by default
+    - curthooks: support multiple ESP on UEFI bootable systems
+    - block-discover: handle missing multipath 'path' data, use DM_NAME
+    - lvm-over-multipath: handle lookups of multipath members
+    - block-meta: don't filter preserve=true devices, select by wipe
+    - 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
+    - block-discover: detect nvme multipath devices
+    - clear-holders: Tolerate vgchange errors during discovery
+    - block-meta: handle preserve with vtoc ptable
+    - 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
+    - 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
+    - vmtest: use util.load_file for loading collect files
+    - block-meta: refactor storage_config preserve and wipe settings
+    - 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
+    - tox: pin setuptools < 45 to allow installing py27 in virtenv
+    - block-discover: add dasd parser and emit dasd storage config
+
+ -- Ryan Harper <ryan.harper@xxxxxxxxxxxxx>  Thu, 28 May 2020 15:23:57 -0500
+
 curtin (19.3-26-g82f23e3d-0ubuntu1~18.04.1) bionic; urgency=medium
 
   * New upstream snapshot. (LP: #1861452)
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index 59e71f3..72cd683 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -198,14 +198,13 @@ Specify a list of devices onto which grub will attempt to install.
 Controls whether grub-install will update the Linux Default target
 value during installation.
 
-**update_nvram**: *<boolean: default False>*
+**update_nvram**: *<boolean: default True>*
 
 Certain platforms, like ``uefi`` and ``prep`` systems utilize
 NVRAM to hold boot configuration settings which control the order in
-which devices are booted.  Curtin by default will not attempt to
-update the NVRAM settings to preserve the system configuration.
-Users may want to force NVRAM to be updated such that the next boot
-of the system will boot from the installed device.
+which devices are booted.  Curtin by default will enable NVRAM updates
+to boot configuration settings.  Users may disable NVRAM updates by setting
+the ``update_nvram`` value to ``False``.
 
 **probe_additional_os**: *<boolean: default False>*
 
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/crashdump.cfg b/examples/tests/crashdump.cfg
new file mode 100644
index 0000000..fb162b6
--- /dev/null
+++ b/examples/tests/crashdump.cfg
@@ -0,0 +1,33 @@
+_install_crashdump:
+ - &install_crashdump |
+   # On Ubuntu/Debian systems we can install the linux-crashdump package
+   # However crashdump currently does not handle vmtest's ephemeral
+   # environment, namely we boot the VM via -kernel/-initrd and rootfs is
+   # obtained via http download, using overlayroot.  As such, crashdump trips
+   # up over looking for the root disk, and trying to check which kernel modules
+   # are needed to mount it in the initramfs after a crash.
+   command -v apt &>/dev/null && {
+       # Crash dump needs a kernel/initrd to be installed in the rootfs, and the
+       # ephemeral environment rootfs does not contain a kernel (by design)
+       # Note: we may not install the exact same kernel version we booted from
+       # as we obtain the kernel/initrd from images.maas.io and are not stricly
+       # in-sync with the archive.  In the case this happens, the crashdump
+       # output may not be valid due to differing symbol tables.  Since this
+       # is only enabled when required we don't attempt to check/test this.
+       DEBIAN_FRONTEND=noninteractive apt-get -qy install linux-image-generic
+       debconf-set-selections <<< "kexec-tools  kexec-tools/load_kexec  boolean true"
+       debconf-set-selections <<< "kdump-tools  kdump-tools/use_kdname  boolean true"
+       DEBIAN_FRONTEND=noninteractive apt-get -qy install linux-crashdump;
+       mkdir -p /var/lib/kdump
+       # crashdump fails if we cannot find a root block device to check for
+       # kernel module deps to mount the device so we just install most modules.
+       sed -i -e 's,MODULES=dep,MODULES=most,' /etc/kernel/postinst.d/kdump-tools
+       kdump-config load
+       kdump-config show
+    }
+    exit 0
+
+
+early_commands:
+  # run before other install commands
+  0000_aaaa_install_crashdump: ['bash', '-c', *install_crashdump]
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/network_config_disabled.yaml b/examples/tests/network_config_disabled.yaml
new file mode 100644
index 0000000..d9ac464
--- /dev/null
+++ b/examples/tests/network_config_disabled.yaml
@@ -0,0 +1,4 @@
+# example with network config disabled
+# showtrace: true
+network:
+    config: disabled
diff --git a/examples/tests/network_config_disabled_with_version.yaml b/examples/tests/network_config_disabled_with_version.yaml
new file mode 100644
index 0000000..c9edceb
--- /dev/null
+++ b/examples/tests/network_config_disabled_with_version.yaml
@@ -0,0 +1,5 @@
+# example with network config disabled with version
+# showtrace: true
+network:
+    version: 1
+    config: disabled
diff --git a/examples/tests/network_disabled.yaml b/examples/tests/network_disabled.yaml
new file mode 100644
index 0000000..4501966
--- /dev/null
+++ b/examples/tests/network_disabled.yaml
@@ -0,0 +1,8 @@
+# example with net meta command using the disabled mode
+# showtrace: true
+network_commands:
+    builtin: null
+    disabled:
+       - curtin
+       - net-meta
+       - disabled
diff --git a/examples/tests/panic.yaml b/examples/tests/panic.yaml
new file mode 100644
index 0000000..91cb216
--- /dev/null
+++ b/examples/tests/panic.yaml
@@ -0,0 +1,2 @@
+early_commands:
+    00_panic_at_the_disco: ['sh', '-c', 'echo c > /proc/sysrq-trigger']
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_dasd.json b/tests/data/probert_storage_dasd.json
new file mode 100644
index 0000000..052d285
--- /dev/null
+++ b/tests/data/probert_storage_dasd.json
@@ -0,0 +1,809 @@
+{
+    "storage": {
+        "bcache": {
+            "backing": {},
+            "caching": {}
+        },
+        "blockdev": {
+            "/dev/dasda": {
+                "DEVLINKS": "/dev/disk/by-path/ccw-0.0.1522 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.22 /dev/disk/by-id/ccw-0X1522",
+                "DEVNAME": "/dev/dasda",
+                "DEVPATH": "/devices/css0/0.0.0182/0.0.1522/block/dasda",
+                "DEVTYPE": "disk",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1522",
+                "ID_PATH_TAG": "ccw-0_0_1522",
+                "ID_SERIAL": "0X1522",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.22",
+                "ID_XUID": "IBM.750000000DXP71.1500.22",
+                "MAJOR": "94",
+                "MINOR": "0",
+                "MPATH_SBIN_PATH": "/sbin",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9673909",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "bdi": null,
+                    "capability": "10",
+                    "dev": "94:0",
+                    "device": null,
+                    "discard_alignment": "0",
+                    "ext_range": "4",
+                    "hidden": "0",
+                    "inflight": "       0        0",
+                    "range": "4",
+                    "removable": "0",
+                    "ro": "0",
+                    "size": "7385333760",
+                    "stat": "    4147        0   362632    30170        0        0        0        0        0 65899730 65946590",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=0\nDEVNAME=dasda\nDEVTYPE=disk"
+                }
+            },
+            "/dev/dasda1": {
+                "DEVLINKS": "/dev/disk/by-path/ccw-0.0.1522-part1 /dev/disk/by-id/ccw-0X1522-part1 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.22-part1",
+                "DEVNAME": "/dev/dasda1",
+                "DEVPATH": "/devices/css0/0.0.0182/0.0.1522/block/dasda/dasda1",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1522",
+                "ID_PATH_TAG": "ccw-0_0_1522",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X1522",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.22",
+                "ID_XUID": "IBM.750000000DXP71.1500.22",
+                "MAJOR": "94",
+                "MINOR": "1",
+                "PARTN": "1",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "2787550366261",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:1",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "1",
+                    "ro": "0",
+                    "size": "2147598336",
+                    "start": "192",
+                    "stat": "     261        0    27104      650        0        0        0        0        0      890      940",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=1\nDEVNAME=dasda1\nDEVTYPE=partition\nPARTN=1"
+                }
+            },
+            "/dev/dasda2": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-0X1522-part2 /dev/disk/by-path/ccw-0.0.1522-part2 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.22-part2",
+                "DEVNAME": "/dev/dasda2",
+                "DEVPATH": "/devices/css0/0.0.0182/0.0.1522/block/dasda/dasda2",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1522",
+                "ID_PATH_TAG": "ccw-0_0_1522",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X1522",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.22",
+                "ID_XUID": "IBM.750000000DXP71.1500.22",
+                "MAJOR": "94",
+                "MINOR": "2",
+                "PARTN": "2",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "2787550369490",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:2",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "2",
+                    "ro": "0",
+                    "size": "2147598336",
+                    "start": "4194720",
+                    "stat": "     253        0    26304      580        0        0        0        0        0      840      940",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=2\nDEVNAME=dasda2\nDEVTYPE=partition\nPARTN=2"
+                }
+            },
+            "/dev/dasda3": {
+                "DEVLINKS": "/dev/disk/by-path/ccw-0.0.1522-part3 /dev/disk/by-id/ccw-0X1522-part3 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.22-part3",
+                "DEVNAME": "/dev/dasda3",
+                "DEVPATH": "/devices/css0/0.0.0182/0.0.1522/block/dasda/dasda3",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1522",
+                "ID_PATH_TAG": "ccw-0_0_1522",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X1522",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.22",
+                "ID_XUID": "IBM.750000000DXP71.1500.22",
+                "MAJOR": "94",
+                "MINOR": "3",
+                "PARTN": "3",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "2787550372233",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:3",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "3",
+                    "ro": "0",
+                    "size": "2147598336",
+                    "start": "8389248",
+                    "stat": "     253        0    26304      730        0        0        0        0        0      900     1150",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=3\nDEVNAME=dasda3\nDEVTYPE=partition\nPARTN=3"
+                }
+            },
+            "/dev/dasdb": {
+                "DEVLINKS": "/dev/disk/by-path/ccw-0.0.1520 /dev/disk/by-id/ccw-0X1520 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.20",
+                "DEVNAME": "/dev/dasdb",
+                "DEVPATH": "/devices/css0/0.0.0180/0.0.1520/block/dasdb",
+                "DEVTYPE": "disk",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1520",
+                "ID_PATH_TAG": "ccw-0_0_1520",
+                "ID_SERIAL": "0X1520",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.20",
+                "ID_XUID": "IBM.750000000DXP71.1500.20",
+                "MAJOR": "94",
+                "MINOR": "4",
+                "MPATH_SBIN_PATH": "/sbin",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9682643",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "bdi": null,
+                    "capability": "10",
+                    "dev": "94:4",
+                    "device": null,
+                    "discard_alignment": "0",
+                    "ext_range": "4",
+                    "hidden": "0",
+                    "inflight": "       0        0",
+                    "range": "4",
+                    "removable": "0",
+                    "ro": "0",
+                    "size": "7385333760",
+                    "stat": "    1337        0   109632     7030        0        0        0        0        0 65890790 65891590",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=4\nDEVNAME=dasdb\nDEVTYPE=disk"
+                }
+            },
+            "/dev/dasdb1": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-IBM.750000000DXP71.1500.20-part1 /dev/disk/by-label/dasd_root /dev/disk/by-path/ccw-0.0.1520-part1 /dev/disk/by-id/ccw-0X1520-part1 /dev/disk/by-uuid/7ac5930d-1798-460e-ba58-ccf536acc6be",
+                "DEVNAME": "/dev/dasdb1",
+                "DEVPATH": "/devices/css0/0.0.0180/0.0.1520/block/dasdb/dasdb1",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_FS_LABEL": "dasd_root",
+                "ID_FS_LABEL_ENC": "dasd_root",
+                "ID_FS_TYPE": "ext4",
+                "ID_FS_USAGE": "filesystem",
+                "ID_FS_UUID": "7ac5930d-1798-460e-ba58-ccf536acc6be",
+                "ID_FS_UUID_ENC": "7ac5930d-1798-460e-ba58-ccf536acc6be",
+                "ID_FS_VERSION": "1.0",
+                "ID_PATH": "ccw-0.0.1520",
+                "ID_PATH_TAG": "ccw-0_0_1520",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X1520",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.20",
+                "ID_XUID": "IBM.750000000DXP71.1500.20",
+                "MAJOR": "94",
+                "MINOR": "5",
+                "PARTN": "1",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "2787546035084",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:5",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "1",
+                    "ro": "0",
+                    "size": "3221323776",
+                    "start": "192",
+                    "stat": "     210        0    15880      160        0        0        0        0        0      380      410",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=5\nDEVNAME=dasdb1\nDEVTYPE=partition\nPARTN=1"
+                }
+            },
+            "/dev/dasdc": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-IBM.750000000DXP71.1500.1f /dev/disk/by-id/ccw-0X151F /dev/disk/by-path/ccw-0.0.151f",
+                "DEVNAME": "/dev/dasdc",
+                "DEVPATH": "/devices/css0/0.0.017f/0.0.151f/block/dasdc",
+                "DEVTYPE": "disk",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.151f",
+                "ID_PATH_TAG": "ccw-0_0_151f",
+                "ID_SERIAL": "0X151F",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.1f",
+                "ID_XUID": "IBM.750000000DXP71.1500.1f",
+                "MAJOR": "94",
+                "MINOR": "8",
+                "MPATH_SBIN_PATH": "/sbin",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9670665",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "bdi": null,
+                    "capability": "10",
+                    "dev": "94:8",
+                    "device": null,
+                    "discard_alignment": "0",
+                    "ext_range": "4",
+                    "hidden": "0",
+                    "inflight": "       0        0",
+                    "range": "4",
+                    "removable": "0",
+                    "ro": "0",
+                    "size": "7385333760",
+                    "stat": "    2797        0   239184    23300        0        0        0        0        0 65889970 65918210",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=8\nDEVNAME=dasdc\nDEVTYPE=disk"
+                }
+            },
+            "/dev/dasdc1": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-0X151F-part1 /dev/disk/by-uuid/304972bd-27be-471b-9695-429851622e2d /dev/disk/by-path/ccw-0.0.151f-part1 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.1f-part1",
+                "DEVNAME": "/dev/dasdc1",
+                "DEVPATH": "/devices/css0/0.0.017f/0.0.151f/block/dasdc/dasdc1",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_FS_TYPE": "crypto_LUKS",
+                "ID_FS_USAGE": "crypto",
+                "ID_FS_UUID": "304972bd-27be-471b-9695-429851622e2d",
+                "ID_FS_UUID_ENC": "304972bd-27be-471b-9695-429851622e2d",
+                "ID_FS_VERSION": "2",
+                "ID_PATH": "ccw-0.0.151f",
+                "ID_PATH_TAG": "ccw-0_0_151f",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X151F",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.1f",
+                "ID_XUID": "IBM.750000000DXP71.1500.1f",
+                "MAJOR": "94",
+                "MINOR": "9",
+                "PARTN": "1",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "343420823392",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:9",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "1",
+                    "ro": "0",
+                    "size": "2147598336",
+                    "start": "192",
+                    "stat": "     111        0    10800       70        0        0        0        0        0      250      250",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=9\nDEVNAME=dasdc1\nDEVTYPE=partition\nPARTN=1"
+                }
+            },
+            "/dev/dasdc2": {
+                "DEVLINKS": "/dev/disk/by-label/dasd_boot /dev/disk/by-path/ccw-0.0.151f-part2 /dev/disk/by-id/ccw-0X151F-part2 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.1f-part2 /dev/disk/by-uuid/6077f46f-106e-4714-9e3d-ca4f4c588144",
+                "DEVNAME": "/dev/dasdc2",
+                "DEVPATH": "/devices/css0/0.0.017f/0.0.151f/block/dasdc/dasdc2",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_FS_LABEL": "dasd_boot",
+                "ID_FS_LABEL_ENC": "dasd_boot",
+                "ID_FS_TYPE": "ext4",
+                "ID_FS_USAGE": "filesystem",
+                "ID_FS_UUID": "6077f46f-106e-4714-9e3d-ca4f4c588144",
+                "ID_FS_UUID_ENC": "6077f46f-106e-4714-9e3d-ca4f4c588144",
+                "ID_FS_VERSION": "1.0",
+                "ID_PATH": "ccw-0.0.151f",
+                "ID_PATH_TAG": "ccw-0_0_151f",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X151F",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.1f",
+                "ID_XUID": "IBM.750000000DXP71.1500.1f",
+                "MAJOR": "94",
+                "MINOR": "10",
+                "PARTN": "2",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "343420828721",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:10",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "2",
+                    "ro": "0",
+                    "size": "536985600",
+                    "start": "4194720",
+                    "stat": "     243        0    17296      380        0        0        0        0        0      660      720",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=10\nDEVNAME=dasdc2\nDEVTYPE=partition\nPARTN=2"
+                }
+            },
+            "/dev/dasdc3": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-0X151F-part3 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.1f-part3 /dev/disk/by-path/ccw-0.0.151f-part3",
+                "DEVNAME": "/dev/dasdc3",
+                "DEVPATH": "/devices/css0/0.0.017f/0.0.151f/block/dasdc/dasdc3",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.151f",
+                "ID_PATH_TAG": "ccw-0_0_151f",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X151F",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.1f",
+                "ID_XUID": "IBM.750000000DXP71.1500.1f",
+                "MAJOR": "94",
+                "MINOR": "11",
+                "PARTN": "3",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "343420856969",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:11",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "3",
+                    "ro": "0",
+                    "size": "2147598336",
+                    "start": "5243520",
+                    "stat": "     294        0    28520      260        0        0        0        0        0      570      630",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=11\nDEVNAME=dasdc3\nDEVTYPE=partition\nPARTN=3"
+                }
+            },
+            "/dev/dasdd": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-0X1544 /dev/disk/by-id/ccw-IBM.750000000DXP71.1500.44 /dev/disk/by-path/ccw-0.0.1544",
+                "DEVNAME": "/dev/dasdd",
+                "DEVPATH": "/devices/css0/0.0.01a4/0.0.1544/block/dasdd",
+                "DEVTYPE": "disk",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_PATH": "ccw-0.0.1544",
+                "ID_PATH_TAG": "ccw-0_0_1544",
+                "ID_SERIAL": "0X1544",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.44",
+                "ID_XUID": "IBM.750000000DXP71.1500.44",
+                "MAJOR": "94",
+                "MINOR": "12",
+                "MPATH_SBIN_PATH": "/sbin",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9115416",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "bdi": null,
+                    "capability": "10",
+                    "dev": "94:12",
+                    "device": null,
+                    "discard_alignment": "0",
+                    "ext_range": "4",
+                    "hidden": "0",
+                    "inflight": "       0        0",
+                    "range": "4",
+                    "removable": "0",
+                    "ro": "0",
+                    "size": "22156001280",
+                    "stat": "   30819     8613  1477104   291110   442189   346221 19369344   633810        0 2792925220 2801319660",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=12\nDEVNAME=dasdd\nDEVTYPE=disk"
+                }
+            },
+            "/dev/dasdd1": {
+                "DEVLINKS": "/dev/disk/by-id/ccw-IBM.750000000DXP71.1500.44-part1 /dev/disk/by-id/ccw-0X1544-part1 /dev/disk/by-uuid/0ef8a516-dcab-43d4-84d5-b3fd9d355ca8 /dev/disk/by-path/ccw-0.0.1544-part1",
+                "DEVNAME": "/dev/dasdd1",
+                "DEVPATH": "/devices/css0/0.0.01a4/0.0.1544/block/dasdd/dasdd1",
+                "DEVTYPE": "partition",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_BUS": "ccw",
+                "ID_FS_TYPE": "ext4",
+                "ID_FS_USAGE": "filesystem",
+                "ID_FS_UUID": "0ef8a516-dcab-43d4-84d5-b3fd9d355ca8",
+                "ID_FS_UUID_ENC": "0ef8a516-dcab-43d4-84d5-b3fd9d355ca8",
+                "ID_FS_VERSION": "1.0",
+                "ID_PATH": "ccw-0.0.1544",
+                "ID_PATH_TAG": "ccw-0_0_1544",
+                "ID_SCSI": "1",
+                "ID_SERIAL": "0X1544",
+                "ID_TYPE": "disk",
+                "ID_UID": "IBM.750000000DXP71.1500.44",
+                "ID_XUID": "IBM.750000000DXP71.1500.44",
+                "MAJOR": "94",
+                "MINOR": "13",
+                "PARTN": "1",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9184782",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "dev": "94:13",
+                    "discard_alignment": "0",
+                    "inflight": "       0        0",
+                    "partition": "1",
+                    "ro": "0",
+                    "size": "22155804672",
+                    "start": "192",
+                    "stat": "   30639     8613  1456680   290220   442189   346221 19369344   633810        0   319010  1173570",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=13\nDEVNAME=dasdd1\nDEVTYPE=partition\nPARTN=1"
+                }
+            },
+            "/dev/dasde": {
+                "DEVLINKS": "/dev/disk/by-path/ccw-0.0.2520",
+                "DEVNAME": "/dev/dasde",
+                "DEVPATH": "/devices/css0/0.0.05e0/0.0.2520/block/dasde",
+                "DEVTYPE": "disk",
+                "DM_MULTIPATH_DEVICE_PATH": "0",
+                "ID_PATH": "ccw-0.0.2520",
+                "ID_PATH_TAG": "ccw-0_0_2520",
+                "MAJOR": "94",
+                "MINOR": "16",
+                "MPATH_SBIN_PATH": "/sbin",
+                "SUBSYSTEM": "block",
+                "TAGS": ":systemd:",
+                "USEC_INITIALIZED": "9132745",
+                "attrs": {
+                    "alignment_offset": "0",
+                    "bdi": null,
+                    "capability": "10",
+                    "dev": "94:16",
+                    "device": null,
+                    "discard_alignment": "0",
+                    "ext_range": "4",
+                    "hidden": "0",
+                    "inflight": "       0        0",
+                    "range": "4",
+                    "removable": "0",
+                    "ro": "0",
+                    "size": "0",
+                    "stat": "       0        0        0        0        0        0        0        0        0        0        0",
+                    "subsystem": "block",
+                    "uevent": "MAJOR=94\nMINOR=16\nDEVNAME=dasde\nDEVTYPE=disk"
+                }
+            }
+        },
+        "dasd": {
+            "/dev/dasda": {
+                "blocksize": 4096,
+                "device_id": "0.0.1522",
+                "disk_layout": "cdl",
+                "name": "/dev/dasda"
+            },
+            "/dev/dasdb": {
+                "blocksize": 4096,
+                "device_id": "0.0.1520",
+                "disk_layout": "cdl",
+                "name": "/dev/dasdb"
+            },
+            "/dev/dasdc": {
+                "blocksize": 4096,
+                "device_id": "0.0.151f",
+                "disk_layout": "cdl",
+                "name": "/dev/dasdc"
+            },
+            "/dev/dasdd": {
+                "blocksize": 4096,
+                "device_id": "0.0.1544",
+                "disk_layout": "cdl",
+                "name": "/dev/dasdd"
+            },
+            "/dev/dasde": {
+                "blocksize": 512,
+                "device_id": "0.0.2520",
+                "disk_layout": "not-formatted",
+                "name": "/dev/dasde"
+            }
+        },
+        "dmcrypt": {},
+        "filesystem": {
+            "/dev/dasdb1": {
+                "LABEL": "dasd_root",
+                "LABEL_ENC": "dasd_root",
+                "TYPE": "ext4",
+                "USAGE": "filesystem",
+                "UUID": "7ac5930d-1798-460e-ba58-ccf536acc6be",
+                "UUID_ENC": "7ac5930d-1798-460e-ba58-ccf536acc6be",
+                "VERSION": "1.0"
+            },
+            "/dev/dasdc2": {
+                "LABEL": "dasd_boot",
+                "LABEL_ENC": "dasd_boot",
+                "TYPE": "ext4",
+                "USAGE": "filesystem",
+                "UUID": "6077f46f-106e-4714-9e3d-ca4f4c588144",
+                "UUID_ENC": "6077f46f-106e-4714-9e3d-ca4f4c588144",
+                "VERSION": "1.0"
+            },
+            "/dev/dasdd1": {
+                "TYPE": "ext4",
+                "USAGE": "filesystem",
+                "UUID": "0ef8a516-dcab-43d4-84d5-b3fd9d355ca8",
+                "UUID_ENC": "0ef8a516-dcab-43d4-84d5-b3fd9d355ca8",
+                "VERSION": "1.0"
+            }
+        },
+        "lvm": {},
+        "mount": [
+            {
+                "children": [
+                    {
+                        "children": [
+                            {
+                                "fstype": "securityfs",
+                                "options": "rw,nosuid,nodev,noexec,relatime",
+                                "source": "securityfs",
+                                "target": "/sys/kernel/security"
+                            },
+                            {
+                                "children": [
+                                    {
+                                        "fstype": "cgroup2",
+                                        "options": "rw,nosuid,nodev,noexec,relatime",
+                                        "source": "cgroup2",
+                                        "target": "/sys/fs/cgroup/unified"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,xattr,name=systemd",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/systemd"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,perf_event",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/perf_event"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,blkio",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/blkio"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,hugetlb",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/hugetlb"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,freezer",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/freezer"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,rdma",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/rdma"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,net_cls,net_prio",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/net_cls,net_prio"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,pids",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/pids"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,cpu,cpuacct",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/cpu,cpuacct"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,cpuset",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/cpuset"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,devices",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/devices"
+                                    },
+                                    {
+                                        "fstype": "cgroup",
+                                        "options": "rw,nosuid,nodev,noexec,relatime,memory",
+                                        "source": "cgroup",
+                                        "target": "/sys/fs/cgroup/memory"
+                                    }
+                                ],
+                                "fstype": "tmpfs",
+                                "options": "ro,nosuid,nodev,noexec,mode=755",
+                                "source": "tmpfs",
+                                "target": "/sys/fs/cgroup"
+                            },
+                            {
+                                "fstype": "bpf",
+                                "options": "rw,nosuid,nodev,noexec,relatime,mode=700",
+                                "source": "bpf",
+                                "target": "/sys/fs/bpf"
+                            },
+                            {
+                                "children": [
+                                    {
+                                        "fstype": "tracefs",
+                                        "options": "rw,nosuid,nodev,noexec,relatime",
+                                        "source": "tracefs",
+                                        "target": "/sys/kernel/debug/tracing"
+                                    }
+                                ],
+                                "fstype": "debugfs",
+                                "options": "rw,nosuid,nodev,noexec,relatime",
+                                "source": "debugfs",
+                                "target": "/sys/kernel/debug"
+                            },
+                            {
+                                "fstype": "fusectl",
+                                "options": "rw,nosuid,nodev,noexec,relatime",
+                                "source": "fusectl",
+                                "target": "/sys/fs/fuse/connections"
+                            },
+                            {
+                                "fstype": "configfs",
+                                "options": "rw,nosuid,nodev,noexec,relatime",
+                                "source": "configfs",
+                                "target": "/sys/kernel/config"
+                            }
+                        ],
+                        "fstype": "sysfs",
+                        "options": "rw,nosuid,nodev,noexec,relatime",
+                        "source": "sysfs",
+                        "target": "/sys"
+                    },
+                    {
+                        "children": [
+                            {
+                                "fstype": "autofs",
+                                "options": "rw,relatime,fd=27,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=33414",
+                                "source": "systemd-1",
+                                "target": "/proc/sys/fs/binfmt_misc"
+                            }
+                        ],
+                        "fstype": "proc",
+                        "options": "rw,nosuid,nodev,noexec,relatime",
+                        "source": "proc",
+                        "target": "/proc"
+                    },
+                    {
+                        "children": [
+                            {
+                                "fstype": "devpts",
+                                "options": "rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000",
+                                "source": "devpts",
+                                "target": "/dev/pts"
+                            },
+                            {
+                                "fstype": "tmpfs",
+                                "options": "rw,nosuid,nodev",
+                                "source": "tmpfs",
+                                "target": "/dev/shm"
+                            },
+                            {
+                                "fstype": "mqueue",
+                                "options": "rw,nosuid,nodev,noexec,relatime",
+                                "source": "mqueue",
+                                "target": "/dev/mqueue"
+                            },
+                            {
+                                "fstype": "hugetlbfs",
+                                "options": "rw,relatime,pagesize=1M",
+                                "source": "hugetlbfs",
+                                "target": "/dev/hugepages"
+                            }
+                        ],
+                        "fstype": "devtmpfs",
+                        "options": "rw,nosuid,relatime,size=1932980k,nr_inodes=483245,mode=755",
+                        "source": "udev",
+                        "target": "/dev"
+                    },
+                    {
+                        "children": [
+                            {
+                                "fstype": "tmpfs",
+                                "options": "rw,nosuid,nodev,noexec,relatime,size=5120k",
+                                "source": "tmpfs",
+                                "target": "/run/lock"
+                            },
+                            {
+                                "fstype": "tmpfs",
+                                "options": "rw,nosuid,nodev,relatime,size=388824k,mode=700,uid=1001,gid=1002",
+                                "source": "tmpfs",
+                                "target": "/run/user/1001"
+                            },
+                            {
+                                "children": [
+                                    {
+                                        "fstype": "nsfs",
+                                        "options": "rw",
+                                        "source": "nsfs[mnt:[4026532054]]",
+                                        "target": "/run/snapd/ns/lxd.mnt"
+                                    }
+                                ],
+                                "fstype": "tmpfs",
+                                "options": "rw,nosuid,noexec,relatime,size=388828k,mode=755",
+                                "source": "tmpfs[/snapd/ns]",
+                                "target": "/run/snapd/ns"
+                            }
+                        ],
+                        "fstype": "tmpfs",
+                        "options": "rw,nosuid,noexec,relatime,size=388828k,mode=755",
+                        "source": "tmpfs",
+                        "target": "/run"
+                    },
+                    {
+                        "fstype": "squashfs",
+                        "options": "ro,nodev,relatime",
+                        "source": "/dev/loop2",
+                        "target": "/snap/core/8687"
+                    },
+                    {
+                        "fstype": "squashfs",
+                        "options": "ro,nodev,relatime",
+                        "source": "/dev/loop3",
+                        "target": "/snap/lxd/13473"
+                    },
+                    {
+                        "fstype": "squashfs",
+                        "options": "ro,nodev,relatime",
+                        "source": "/dev/loop0",
+                        "target": "/snap/lxd/13460"
+                    },
+                    {
+                        "fstype": "squashfs",
+                        "options": "ro,nodev,relatime",
+                        "source": "/dev/loop4",
+                        "target": "/snap/core/8591"
+                    },
+                    {
+                        "fstype": "fuse.lxcfs",
+                        "options": "rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other",
+                        "source": "lxcfs",
+                        "target": "/var/lib/lxcfs"
+                    }
+                ],
+                "fstype": "ext4",
+                "options": "rw,relatime,errors=remount-ro",
+                "source": "/dev/dasdd1",
+                "target": "/"
+            }
+        ],
+        "multipath": {},
+        "raid": {},
+        "zfs": {
+            "zpools": {}
+        }
+    }
+}
diff --git a/tests/data/probert_storage_msdos_mbr_extended_v2.json b/tests/data/probert_storage_msdos_mbr_extended_v2.json
new file mode 100644
index 0000000..4719f44
--- /dev/null
+++ b/tests/data/probert_storage_msdos_mbr_extended_v2.json
@@ -0,0 +1,537 @@
+{
+    "dasd": {},
+    "raid": {},
+    "zfs": {
+        "zpools": {}
+    },
+    "bcache": {
+        "backing": {},
+        "caching": {}
+    },
+    "filesystem": {
+        "/dev/vdb1": {
+            "TYPE": "vfat",
+            "USAGE": "filesystem",
+            "UUID": "5EB4-6065",
+            "UUID_ENC": "5EB4-6065",
+            "VERSION": "FAT32"
+        },
+        "/dev/vdb5": {
+            "TYPE": "ext4",
+            "USAGE": "filesystem",
+            "UUID": "a55d4dc5-dacb-48af-b589-828ee55f5208",
+            "UUID_ENC": "a55d4dc5-dacb-48af-b589-828ee55f5208",
+            "VERSION": "1.0"
+        }
+    },
+    "dmcrypt": {},
+    "multipath": {},
+    "blockdev": {
+        "/dev/vda": {
+            "DEVLINKS": "/dev/disk/by-path/virtio-pci-0000:00:08.0 /dev/disk/by-path/pci-0000:00:08.0",
+            "DEVNAME": "/dev/vda",
+            "DEVPATH": "/devices/pci0000:00/0000:00:08.0/virtio2/block/vda",
+            "DEVTYPE": "disk",
+            "ID_PATH": "pci-0000:00:08.0",
+            "ID_PATH_TAG": "pci-0000_00_08_0",
+            "MAJOR": "252",
+            "MINOR": "0",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1159634",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "cache_type": "write back",
+                "capability": "50",
+                "dev": "252: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",
+                "serial": "",
+                "size": "21474836480",
+                "stat": "     490        0    22696      179        0        0        0        0        0      176       64        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=252\nMINOR=0\nDEVNAME=vda\nDEVTYPE=disk"
+            }
+        },
+        "/dev/vdb": {
+            "DEVLINKS": "/dev/disk/by-path/pci-0000:00:09.0 /dev/disk/by-path/virtio-pci-0000:00:09.0",
+            "DEVNAME": "/dev/vdb",
+            "DEVPATH": "/devices/pci0000:00/0000:00:09.0/virtio3/block/vdb",
+            "DEVTYPE": "disk",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "c72f0a19",
+            "ID_PATH": "pci-0000:00:09.0",
+            "ID_PATH_TAG": "pci-0000_00_09_0",
+            "MAJOR": "252",
+            "MINOR": "16",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1133535",
+            "attrs": {
+                "alignment_offset": "0",
+                "bdi": null,
+                "cache_type": "write back",
+                "capability": "50",
+                "dev": "252: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",
+                "serial": "",
+                "size": "10737418240",
+                "stat": "     609        0    39218      164        0        0        0        0        0      212       68        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=252\nMINOR=16\nDEVNAME=vdb\nDEVTYPE=disk"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0xc72f0a19",
+                "device": "/dev/vdb",
+                "unit": "sectors",
+                "partitions": [
+                    {
+                        "node": "/dev/vdb1",
+                        "start": 2048,
+                        "size": 1048576,
+                        "type": "b",
+                        "bootable": true
+                    },
+                    {
+                        "node": "/dev/vdb2",
+                        "start": 1052670,
+                        "size": 19916802,
+                        "type": "5"
+                    },
+                    {
+                        "node": "/dev/vdb5",
+                        "start": 1052672,
+                        "size": 19916800,
+                        "type": "83"
+                    }
+                ]
+            }
+        },
+        "/dev/vdb1": {
+            "DEVLINKS": "/dev/disk/by-partuuid/c72f0a19-01 /dev/disk/by-uuid/5EB4-6065 /dev/disk/by-path/virtio-pci-0000:00:09.0-part1 /dev/disk/by-path/pci-0000:00:09.0-part1",
+            "DEVNAME": "/dev/vdb1",
+            "DEVPATH": "/devices/pci0000:00/0000:00:09.0/virtio3/block/vdb/vdb1",
+            "DEVTYPE": "partition",
+            "ID_FS_TYPE": "vfat",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "5EB4-6065",
+            "ID_FS_UUID_ENC": "5EB4-6065",
+            "ID_FS_VERSION": "FAT32",
+            "ID_PART_ENTRY_DISK": "252:16",
+            "ID_PART_ENTRY_FLAGS": "0x80",
+            "ID_PART_ENTRY_NUMBER": "1",
+            "ID_PART_ENTRY_OFFSET": "2048",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "1048576",
+            "ID_PART_ENTRY_TYPE": "0xb",
+            "ID_PART_ENTRY_UUID": "c72f0a19-01",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "c72f0a19",
+            "ID_PATH": "pci-0000:00:09.0",
+            "ID_PATH_TAG": "pci-0000_00_09_0",
+            "ID_SCSI": "1",
+            "MAJOR": "252",
+            "MINOR": "17",
+            "PARTN": "1",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1161634",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "252:17",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "1",
+                "ro": "0",
+                "size": "536870912",
+                "start": "2048",
+                "stat": "     200        0    14424       72        0        0        0        0        0      104       44        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=252\nMINOR=17\nDEVNAME=vdb1\nDEVTYPE=partition\nPARTN=1"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0x00000000",
+                "device": "/dev/vdb1",
+                "unit": "sectors",
+                "partitions": []
+            }
+        },
+        "/dev/vdb2": {
+            "DEVLINKS": "/dev/disk/by-path/pci-0000:00:09.0-part2 /dev/disk/by-path/virtio-pci-0000:00:09.0-part2 /dev/disk/by-partuuid/c72f0a19-02",
+            "DEVNAME": "/dev/vdb2",
+            "DEVPATH": "/devices/pci0000:00/0000:00:09.0/virtio3/block/vdb/vdb2",
+            "DEVTYPE": "partition",
+            "ID_PART_ENTRY_DISK": "252:16",
+            "ID_PART_ENTRY_NUMBER": "2",
+            "ID_PART_ENTRY_OFFSET": "1052670",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "19916802",
+            "ID_PART_ENTRY_TYPE": "0x5",
+            "ID_PART_ENTRY_UUID": "c72f0a19-02",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "e7ad4c09",
+            "ID_PATH": "pci-0000:00:09.0",
+            "ID_PATH_TAG": "pci-0000_00_09_0",
+            "ID_SCSI": "1",
+            "MAJOR": "252",
+            "MINOR": "18",
+            "PARTN": "2",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1149403",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "252:18",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "2",
+                "ro": "0",
+                "size": "1024",
+                "start": "1052670",
+                "stat": "       9        0       18       10        0        0        0        0        0       44        8        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=252\nMINOR=18\nDEVNAME=vdb2\nDEVTYPE=partition\nPARTN=2"
+            },
+            "partitiontable": {
+                "label": "dos",
+                "id": "0xe7ad4c09",
+                "device": "/dev/vdb2",
+                "unit": "sectors",
+                "grain": "512",
+                "partitions": [
+                    {
+                        "node": "/dev/vdb2p1",
+                        "start": 2,
+                        "size": 19916800,
+                        "type": "83"
+                    }
+                ]
+            }
+        },
+        "/dev/vdb5": {
+            "DEVLINKS": "/dev/disk/by-uuid/a55d4dc5-dacb-48af-b589-828ee55f5208 /dev/disk/by-path/pci-0000:00:09.0-part5 /dev/disk/by-partuuid/c72f0a19-05 /dev/disk/by-path/virtio-pci-0000:00:09.0-part5",
+            "DEVNAME": "/dev/vdb5",
+            "DEVPATH": "/devices/pci0000:00/0000:00:09.0/virtio3/block/vdb/vdb5",
+            "DEVTYPE": "partition",
+            "ID_FS_TYPE": "ext4",
+            "ID_FS_USAGE": "filesystem",
+            "ID_FS_UUID": "a55d4dc5-dacb-48af-b589-828ee55f5208",
+            "ID_FS_UUID_ENC": "a55d4dc5-dacb-48af-b589-828ee55f5208",
+            "ID_FS_VERSION": "1.0",
+            "ID_PART_ENTRY_DISK": "252:16",
+            "ID_PART_ENTRY_NUMBER": "5",
+            "ID_PART_ENTRY_OFFSET": "1052672",
+            "ID_PART_ENTRY_SCHEME": "dos",
+            "ID_PART_ENTRY_SIZE": "19916800",
+            "ID_PART_ENTRY_TYPE": "0x83",
+            "ID_PART_ENTRY_UUID": "c72f0a19-05",
+            "ID_PART_TABLE_TYPE": "dos",
+            "ID_PART_TABLE_UUID": "c72f0a19",
+            "ID_PATH": "pci-0000:00:09.0",
+            "ID_PATH_TAG": "pci-0000_00_09_0",
+            "ID_SCSI": "1",
+            "MAJOR": "252",
+            "MINOR": "21",
+            "PARTN": "5",
+            "SUBSYSTEM": "block",
+            "TAGS": ":systemd:",
+            "USEC_INITIALIZED": "1155916",
+            "attrs": {
+                "alignment_offset": "0",
+                "dev": "252:21",
+                "discard_alignment": "0",
+                "inflight": "       0        0",
+                "partition": "5",
+                "ro": "0",
+                "size": "10197401600",
+                "start": "1052672",
+                "stat": "     202        0    14888       36        0        0        0        0        0      108        8        0        0        0        0",
+                "subsystem": "block",
+                "uevent": "MAJOR=252\nMINOR=21\nDEVNAME=vdb5\nDEVTYPE=partition\nPARTN=5"
+            }
+        }
+    },
+    "lvm": {},
+    "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/net_cls,net_prio",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,net_cls,net_prio"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/hugetlb",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,hugetlb"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/pids",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,pids"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/blkio",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,blkio"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/memory",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,memory"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/cpuset",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,cpuset"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/freezer",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,freezer"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/devices",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,devices"
+                                },
+                                {
+                                    "target": "/sys/fs/cgroup/perf_event",
+                                    "source": "cgroup",
+                                    "fstype": "cgroup",
+                                    "options": "rw,nosuid,nodev,noexec,relatime,perf_event"
+                                }
+                            ]
+                        },
+                        {
+                            "target": "/sys/fs/pstore",
+                            "source": "pstore",
+                            "fstype": "pstore",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/firmware/efi/efivars",
+                            "source": "efivarfs",
+                            "fstype": "efivarfs",
+                            "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/fs/fuse/connections",
+                            "source": "fusectl",
+                            "fstype": "fusectl",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/sys/kernel/config",
+                            "source": "configfs",
+                            "fstype": "configfs",
+                            "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=18206"
+                        }
+                    ]
+                },
+                {
+                    "target": "/dev",
+                    "source": "udev",
+                    "fstype": "devtmpfs",
+                    "options": "rw,nosuid,noexec,relatime,size=1969872k,nr_inodes=492468,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/mqueue",
+                            "source": "mqueue",
+                            "fstype": "mqueue",
+                            "options": "rw,nosuid,nodev,noexec,relatime"
+                        },
+                        {
+                            "target": "/dev/hugepages",
+                            "source": "hugetlbfs",
+                            "fstype": "hugetlbfs",
+                            "options": "rw,relatime,pagesize=2M"
+                        }
+                    ]
+                },
+                {
+                    "target": "/run",
+                    "source": "tmpfs",
+                    "fstype": "tmpfs",
+                    "options": "rw,nosuid,nodev,noexec,relatime,size=402820k,mode=755",
+                    "children": [
+                        {
+                            "target": "/run/lock",
+                            "source": "tmpfs",
+                            "fstype": "tmpfs",
+                            "options": "rw,nosuid,nodev,noexec,relatime,size=5120k"
+                        }
+                    ]
+                },
+                {
+                    "target": "/cdrom",
+                    "source": "/dev/loop0",
+                    "fstype": "iso9660",
+                    "options": "ro,relatime,nojoliet,check=s,map=n,blocksize=2048"
+                },
+                {
+                    "target": "/rofs",
+                    "source": "/dev/loop1",
+                    "fstype": "squashfs",
+                    "options": "ro,noatime"
+                },
+                {
+                    "target": "/usr/lib/modules",
+                    "source": "/dev/loop3",
+                    "fstype": "squashfs",
+                    "options": "ro,relatime"
+                },
+                {
+                    "target": "/media/filesystem",
+                    "source": "/dev/loop1",
+                    "fstype": "squashfs",
+                    "options": "ro,relatime"
+                },
+                {
+                    "target": "/tmp",
+                    "source": "tmpfs",
+                    "fstype": "tmpfs",
+                    "options": "rw,nosuid,nodev,relatime"
+                },
+                {
+                    "target": "/snap/core/8935",
+                    "source": "/dev/loop4",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                },
+                {
+                    "target": "/snap/subiquity/1626",
+                    "source": "/dev/loop5",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                },
+                {
+                    "target": "/snap/subiquity/1632",
+                    "source": "/dev/loop6",
+                    "fstype": "squashfs",
+                    "options": "ro,nodev,relatime"
+                }
+            ]
+        }
+    ]
+}
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..dafc478 100644
--- a/tests/unittests/test_apt_custom_sources_list.py
+++ b/tests/unittests/test_apt_custom_sources_list.py
@@ -93,16 +93,19 @@ 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)"
 
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         # would fail inside the unittest context
         bpath = "curtin.commands.apt_config."
         upath = bpath + "util."
-        self.add_patch(upath + "get_architecture", "mockga", return_value=arch)
+        dpath = bpath + 'distro.'
+        self.add_patch(dpath + "get_architecture", "mockga", return_value=arch)
         self.add_patch(upath + "write_file", "mockwrite")
         self.add_patch(bpath + "os.rename", "mockrename")
         self.add_patch(upath + "load_file", "mockload_file",
@@ -141,9 +144,9 @@ class TestAptSourceConfigSourceList(CiTestCase):
         cfg = yaml.safe_load(YAML_TEXT_CUSTOM_SL)
         target = self.new_root
 
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         # would fail inside the unittest context
-        with mock.patch.object(util, 'get_architecture', return_value=arch):
+        with mock.patch.object(distro, 'get_architecture', return_value=arch):
             with mock.patch.object(distro, 'lsb_release',
                                    return_value={'codename': 'fakerel'}):
                 apt_config.handle_apt(cfg, target)
@@ -153,7 +156,7 @@ class TestAptSourceConfigSourceList(CiTestCase):
             util.load_file(paths.target_path(target, "/etc/apt/sources.list")))
 
     @mock.patch("curtin.distro.lsb_release")
-    @mock.patch("curtin.util.get_architecture", return_value="amd64")
+    @mock.patch("curtin.distro.get_architecture", return_value="amd64")
     def test_trusty_source_lists(self, m_get_arch, m_lsb_release):
         """Support mirror equivalency with and without trailing /.
 
diff --git a/tests/unittests/test_apt_source.py b/tests/unittests/test_apt_source.py
index 353cdf8..6556399 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):
@@ -88,7 +90,7 @@ class TestAptSourceConfig(CiTestCase):
         """
         params = {}
         params['RELEASE'] = distro.lsb_release()['codename']
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         params['MIRROR'] = apt_config.get_default_mirrors(arch)["PRIMARY"]
         return params
 
@@ -455,7 +457,7 @@ class TestAptSourceConfig(CiTestCase):
         self.assertFalse(os.path.isfile(self.aptlistfile2))
         self.assertFalse(os.path.isfile(self.aptlistfile3))
 
-    @mock.patch("curtin.commands.apt_config.util.get_architecture")
+    @mock.patch("curtin.commands.apt_config.distro.get_architecture")
     def test_mir_apt_list_rename(self, m_get_architecture):
         """test_mir_apt_list_rename - Test find mirror and apt list renaming"""
         pre = "/var/lib/apt/lists"
@@ -493,7 +495,7 @@ class TestAptSourceConfig(CiTestCase):
 
         mockren.assert_any_call(fromfn, tofn)
 
-    @mock.patch("curtin.commands.apt_config.util.get_architecture")
+    @mock.patch("curtin.commands.apt_config.distro.get_architecture")
     def test_mir_apt_list_rename_non_slash(self, m_get_architecture):
         target = os.path.join(self.tmp, "rename_non_slash")
         apt_lists_d = os.path.join(target, "./" + apt_config.APT_LISTS)
@@ -575,7 +577,7 @@ class TestAptSourceConfig(CiTestCase):
 
     def test_mirror_default(self):
         """test_mirror_default - Test without defining a mirror"""
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         default_mirrors = apt_config.get_default_mirrors(arch)
         pmir = default_mirrors["PRIMARY"]
         smir = default_mirrors["SECURITY"]
@@ -626,7 +628,7 @@ class TestAptSourceConfig(CiTestCase):
         self.assertEqual(mirrors['SECURITY'],
                          smir)
 
-    @mock.patch("curtin.commands.apt_config.util.get_architecture")
+    @mock.patch("curtin.commands.apt_config.distro.get_architecture")
     def test_get_default_mirrors_non_intel_no_arch(self, m_get_architecture):
         arch = 'ppc64el'
         m_get_architecture.return_value = arch
@@ -643,7 +645,7 @@ class TestAptSourceConfig(CiTestCase):
 
     def test_mirror_arches_sysdefault(self):
         """test_mirror_arches - Test arches falling back to sys default"""
-        arch = util.get_architecture()
+        arch = distro.get_architecture()
         default_mirrors = apt_config.get_default_mirrors(arch)
         pmir = default_mirrors["PRIMARY"]
         smir = default_mirrors["SECURITY"]
@@ -956,7 +958,8 @@ class TestDebconfSelections(CiTestCase):
         # assumes called with *args value.
         selections = m_set_sel.call_args_list[0][0][0].decode()
 
-        missing = [l for l in lines if l not in selections.splitlines()]
+        missing = [line for line in lines
+                   if line not in selections.splitlines()]
         self.assertEqual([], missing)
 
     @mock.patch("curtin.commands.apt_config.dpkg_reconfigure")
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_dasd.py b/tests/unittests/test_block_dasd.py
index 95788b0..b5e2215 100644
--- a/tests/unittests/test_block_dasd.py
+++ b/tests/unittests/test_block_dasd.py
@@ -17,8 +17,8 @@ def random_device_id():
 
 class TestDasdValidDeviceId(CiTestCase):
 
-    nonhex = [l for l in string.ascii_lowercase if l not in
-              ['a', 'b', 'c', 'd', 'e', 'f']]
+    nonhex = [letter for letter in string.ascii_lowercase
+              if letter not in ['a', 'b', 'c', 'd', 'e', 'f']]
 
     invalids = [None, '', {}, ('', ), 12, '..', CiTestCase.random_string(),
                 'qz.zq.ffff', '.ff.1420', 'ff..1518', '0.0.xyyz',
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..b768cdc 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,400 @@ 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()
+
+
+class TestVerifyPtableFlag(CiTestCase):
+
+    def setUp(self):
+        super(TestVerifyPtableFlag, self).setUp()
+        base = 'curtin.commands.block_meta.'
+        self.add_patch(base + 'block.sfdisk_info', 'm_block_sfdisk_info')
+        self.add_patch(base + 'block.get_blockdev_for_partition',
+                       'm_block_get_blockdev_for_partition')
+        self.sfdisk_info_dos = {
+            "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"}]}
+        self.sfdisk_info_gpt = {
+            "label": "gpt",
+            "id": "AEA37E20-8E52-4B37-BDFD-9946A352A37B",
+            "device": "/dev/vda",
+            "unit": "sectors",
+            "firstlba": 34,
+            "lastlba": 41943006,
+            "partitions": [
+               {"node": "/dev/vda1", "start": 227328, "size": 41715679,
+                "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
+                "uuid": "42C72DE9-FF5E-4CD6-A4C8-283685DEB1D5"},
+               {"node": "/dev/vda14", "start": 2048, "size": 8192,
+                "type": "21686148-6449-6E6F-744E-656564454649",
+                "uuid": "762F070A-122A-4EB8-90BF-2CA6E9171B01"},
+               {"node": "/dev/vda15", "start": 10240, "size": 217088,
+                "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+                "uuid": "789133C6-8579-4792-9D61-FC9A7BEC2A15"}]}
+
+    def test_verify_ptable_flag_finds_boot_on_gpt(self):
+        devpath = '/dev/vda15'
+        expected_flag = 'boot'
+        block_meta.verify_ptable_flag(devpath, expected_flag,
+                                      sfdisk_info=self.sfdisk_info_gpt)
+
+    def test_verify_ptable_flag_raises_exception_missing_flag(self):
+        devpath = '/dev/vda1'
+        expected_flag = 'boot'
+        with self.assertRaises(RuntimeError):
+            block_meta.verify_ptable_flag(devpath, expected_flag,
+                                          sfdisk_info=self.sfdisk_info_gpt)
+
+    def test_verify_ptable_flag_raises_exception_invalid_flag(self):
+        devpath = '/dev/vda1'
+        expected_flag = self.random_string()
+        self.assertNotIn(expected_flag, block_meta.SGDISK_FLAGS.keys())
+        self.assertNotIn(expected_flag, block_meta.MSDOS_FLAGS.keys())
+        with self.assertRaises(RuntimeError):
+            block_meta.verify_ptable_flag(devpath, expected_flag,
+                                          sfdisk_info=self.sfdisk_info_gpt)
+
+    def test_verify_ptable_flag_checks_bootable_not_table_type(self):
+        devpath = '/dev/vdb1'
+        expected_flag = 'boot'
+        del self.sfdisk_info_dos['partitions'][0]['bootable']
+        self.sfdisk_info_dos['partitions'][0]['type'] = '0x80'
+        with self.assertRaises(RuntimeError):
+            block_meta.verify_ptable_flag(devpath, expected_flag,
+                                          sfdisk_info=self.sfdisk_info_dos)
+
+    def test_verify_ptable_flag_calls_block_sfdisk_if_info_none(self):
+        devpath = '/dev/vda15'
+        expected_flag = 'boot'
+        self.m_block_sfdisk_info.return_value = self.sfdisk_info_gpt
+        block_meta.verify_ptable_flag(devpath, expected_flag, sfdisk_info=None)
+        self.assertEqual(
+            [call(devpath)],
+            self.m_block_sfdisk_info.call_args_list)
+
+    def test_verify_ptable_flag_finds_boot_on_msdos(self):
+        devpath = '/dev/vdb1'
+        expected_flag = 'boot'
+        block_meta.verify_ptable_flag(devpath, expected_flag,
+                                      sfdisk_info=self.sfdisk_info_dos)
+
+    def test_verify_ptable_flag_finds_linux_on_dos_primary_partition(self):
+        devpath = '/dev/vdb2'
+        expected_flag = 'linux'
+        block_meta.verify_ptable_flag(devpath, expected_flag,
+                                      sfdisk_info=self.sfdisk_info_dos)
+
+    def test_verify_ptable_flag_finds_dos_extended_partition(self):
+        devpath = '/dev/vdb3'
+        expected_flag = 'extended'
+        block_meta.verify_ptable_flag(devpath, expected_flag,
+                                      sfdisk_info=self.sfdisk_info_dos)
+
+    def test_verify_ptable_flag_finds_dos_logical_partition(self):
+        devpath = '/dev/vdb5'
+        expected_flag = 'logical'
+        self.m_block_get_blockdev_for_partition.return_value = (
+            ('/dev/vdb', '5'))
+        block_meta.verify_ptable_flag(devpath, expected_flag,
+                                      sfdisk_info=self.sfdisk_info_dos)
+
+
 # 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_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
new file mode 100644
index 0000000..8808159
--- /dev/null
+++ b/tests/unittests/test_commands_install_grub.py
@@ -0,0 +1,1031 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from curtin import distro
+from curtin import util
+from curtin import paths
+from curtin.commands import install_grub
+from .helpers import CiTestCase
+
+import mock
+import os
+
+
+class TestGetGrubPackageName(CiTestCase):
+
+    def test_ppc64_arch(self):
+        target_arch = 'ppc64le'
+        uefi = False
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-ieee1275', 'powerpc-ieee1275'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_uefi_debian_amd64(self):
+        target_arch = 'amd64'
+        uefi = True
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-efi-amd64', 'x86_64-efi'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_uefi_rhel7_amd64(self):
+        target_arch = 'x86_64'
+        uefi = True
+        rhel_ver = '7'
+        self.assertEqual(
+            ('grub2-efi-x64', 'x86_64-efi'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_uefi_rhel8_amd64(self):
+        target_arch = 'x86_64'
+        uefi = True
+        rhel_ver = '8'
+        self.assertEqual(
+            ('grub2-efi-x64', 'x86_64-efi'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_uefi_debian_arm64(self):
+        target_arch = 'arm64'
+        uefi = True
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-efi-arm64', 'arm64-efi'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_uefi_debian_i386(self):
+        target_arch = 'i386'
+        uefi = True
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-efi-ia32', 'i386-efi'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_debian_amd64(self):
+        target_arch = 'amd64'
+        uefi = False
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-pc', 'i386-pc'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_rhel6_amd64(self):
+        target_arch = 'x86_64'
+        uefi = False
+        rhel_ver = '6'
+        self.assertEqual(
+            ('grub', 'i386-pc'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_rhel7_amd64(self):
+        target_arch = 'x86_64'
+        uefi = False
+        rhel_ver = '7'
+        self.assertEqual(
+            ('grub2-pc', 'i386-pc'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_rhel8_amd64(self):
+        target_arch = 'x86_64'
+        uefi = False
+        rhel_ver = '8'
+        self.assertEqual(
+            ('grub2-pc', 'i386-pc'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_debian_i386(self):
+        target_arch = 'i386'
+        uefi = False
+        rhel_ver = None
+        self.assertEqual(
+            ('grub-pc', 'i386-pc'),
+            install_grub.get_grub_package_name(target_arch, uefi, rhel_ver))
+
+    def test_invalid_rhel_version(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package_name('x86_64', uefi=False,
+                                               rhel_ver='5')
+
+    def test_invalid_arch(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package_name(self.random_string(),
+                                               uefi=False, rhel_ver=None)
+
+    def test_invalid_arch_uefi(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package_name(self.random_string(),
+                                               uefi=True, rhel_ver=None)
+
+
+class TestGetGrubConfigFile(CiTestCase):
+
+    @mock.patch('curtin.commands.install_grub.distro.os_release')
+    def test_grub_config_redhat(self, mock_os_release):
+        mock_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            '/etc/default/grub',
+            install_grub.get_grub_config_file(distroinfo.family))
+
+    @mock.patch('curtin.commands.install_grub.distro.os_release')
+    def test_grub_config_debian(self, mock_os_release):
+        mock_os_release.return_value = {'ID': 'ubuntu'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            '/etc/default/grub.d/50-curtin-settings.cfg',
+            install_grub.get_grub_config_file(distroinfo.family))
+
+
+class TestPrepareGrubDir(CiTestCase):
+
+    def setUp(self):
+        super(TestPrepareGrubDir, self).setUp()
+        self.target = self.tmp_dir()
+        self.add_patch('curtin.commands.install_grub.util.ensure_dir',
+                       'm_ensure_dir')
+        self.add_patch('curtin.commands.install_grub.shutil.move', 'm_move')
+        self.add_patch('curtin.commands.install_grub.os.path.exists', 'm_path')
+
+    def test_prepare_grub_dir(self):
+        grub_conf = 'etc/default/grub.d/%s' % self.random_string()
+        target_grub_conf = os.path.join(self.target, grub_conf)
+        ci_conf = os.path.join(
+            os.path.dirname(target_grub_conf), '50-cloudimg-settings.cfg')
+        self.m_path.return_value = True
+        install_grub.prepare_grub_dir(self.target, grub_conf)
+        self.m_ensure_dir.assert_called_with(os.path.dirname(target_grub_conf))
+        self.m_move.assert_called_with(ci_conf, ci_conf + '.disabled')
+
+    def test_prepare_grub_dir_no_ci_cfg(self):
+        grub_conf = 'etc/default/grub.d/%s' % self.random_string()
+        target_grub_conf = os.path.join(self.target, grub_conf)
+        self.m_path.return_value = False
+        install_grub.prepare_grub_dir(self.target, grub_conf)
+        self.m_ensure_dir.assert_called_with(
+            os.path.dirname(target_grub_conf))
+        self.assertEqual(0, self.m_move.call_count)
+
+
+class TestGetCarryoverParams(CiTestCase):
+
+    def setUp(self):
+        super(TestGetCarryoverParams, self).setUp()
+        self.add_patch('curtin.commands.install_grub.util.load_file',
+                       'm_load_file')
+        self.add_patch('curtin.commands.install_grub.distro.os_release',
+                       'm_os_release')
+        self.m_os_release.return_value = {'ID': 'ubuntu'}
+
+    def test_no_carry_params(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        cmdline = "root=ZFS=rpool/ROOT/ubuntu_bo2om9 ro quiet splash"
+        self.m_load_file.return_value = cmdline
+        self.assertEqual([], install_grub.get_carryover_params(distroinfo))
+
+    def test_legacy_separator(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '--'
+        expected_carry_params = ['foo=bar', 'debug=1']
+        cmdline = "root=/dev/xvda1 ro quiet splash %s %s" % (
+            sep, " ".join(expected_carry_params))
+        self.m_load_file.return_value = cmdline
+        self.assertEqual(expected_carry_params,
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_preferred_separator(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '---'
+        expected_carry_params = ['foo=bar', 'debug=1']
+        cmdline = "root=/dev/xvda1 ro quiet splash %s %s" % (
+            sep, " ".join(expected_carry_params))
+        self.m_load_file.return_value = cmdline
+        self.assertEqual(expected_carry_params,
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_multiple_preferred_separator(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '---'
+        expected_carry_params = ['extra', 'additional']
+        cmdline = "lead=args %s extra %s additional" % (sep, sep)
+        self.m_load_file.return_value = cmdline
+        self.assertEqual(expected_carry_params,
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_drop_bootif_initrd_boot_image_from_extra(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '---'
+        expected_carry_params = ['foo=bar', 'debug=1']
+        filtered = ["BOOTIF=eth0", "initrd=initrd-2.3", "BOOT_IMAGE=/xv1"]
+        cmdline = "root=/dev/xvda1 ro quiet splash %s %s" % (
+            sep, " ".join(filtered + expected_carry_params))
+        self.m_load_file.return_value = cmdline
+        self.assertEqual(expected_carry_params,
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_keep_console_always(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '---'
+        console = "console=ttyS1,115200"
+        cmdline = "root=/dev/xvda1 ro quiet splash %s %s" % (console, sep)
+        self.m_load_file.return_value = cmdline
+        self.assertEqual([console],
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_keep_console_only_once(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        sep = '---'
+        console = "console=ttyS1,115200"
+        cmdline = "root=/dev/xvda1 ro quiet splash %s %s %s" % (
+            console, sep, console)
+        self.m_load_file.return_value = cmdline
+        self.assertEqual([console],
+                         install_grub.get_carryover_params(distroinfo))
+
+    def test_always_set_rh_params(self):
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        cmdline = "root=ZFS=rpool/ROOT/ubuntu_bo2om9 ro quiet splash"
+        self.m_load_file.return_value = cmdline
+        self.assertEqual(['rd.auto=1'],
+                         install_grub.get_carryover_params(distroinfo))
+
+
+class TestReplaceGrubCmdlineLinuxDefault(CiTestCase):
+
+    def setUp(self):
+        super(TestReplaceGrubCmdlineLinuxDefault, self).setUp()
+        self.target = self.tmp_dir()
+        self.grubconf = "/etc/default/grub"
+        self.target_grubconf = paths.target_path(self.target, self.grubconf)
+        util.ensure_dir(os.path.dirname(self.target_grubconf))
+
+    @mock.patch('curtin.commands.install_grub.util.write_file')
+    @mock.patch('curtin.commands.install_grub.util.load_file')
+    def test_append_line_if_not_found(self, m_load_file, m_write_file):
+        existing = [
+            "# If you change this file, run 'update-grub' after to update",
+            "# /boot/grub/grub.cfg",
+        ]
+        m_load_file.return_value = "\n".join(existing)
+        new_args = ["foo=bar", "wark=1"]
+        newline = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+        expected = newline + "\n"
+
+        install_grub.replace_grub_cmdline_linux_default(
+            self.target, new_args)
+
+        m_write_file.assert_called_with(
+            self.target_grubconf, expected, omode="a+")
+
+    def test_append_line_if_not_found_verify_content(self):
+        existing = [
+            "# If you change this file, run 'update-grub' after to update",
+            "# /boot/grub/grub.cfg",
+        ]
+        with open(self.target_grubconf, "w") as fh:
+            fh.write("\n".join(existing))
+
+        new_args = ["foo=bar", "wark=1"]
+        newline = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+        expected = "\n".join(existing) + newline + "\n"
+
+        install_grub.replace_grub_cmdline_linux_default(
+            self.target, new_args)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    @mock.patch('curtin.commands.install_grub.os.path.exists')
+    @mock.patch('curtin.commands.install_grub.util.write_file')
+    @mock.patch('curtin.commands.install_grub.util.load_file')
+    def test_replace_line_when_found(self, m_load_file, m_write_file,
+                                     m_exists):
+        existing = [
+            "# Line1",
+            "# Line2",
+            'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"',
+            "# Line4",
+            "# Line5",
+        ]
+        m_exists.return_value = True
+        m_load_file.return_value = "\n".join(existing)
+        new_args = ["foo=bar", "wark=1"]
+        newline = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+        expected = ("\n".join(existing[0:2]) + "\n" +
+                    newline + "\n" +
+                    "\n".join(existing[3:]))
+
+        install_grub.replace_grub_cmdline_linux_default(
+            self.target, new_args)
+
+        m_write_file.assert_called_with(
+            self.target_grubconf, expected, omode="w+")
+
+    def test_replace_line_when_found_verify_content(self):
+        existing = [
+            "# Line1",
+            "# Line2",
+            'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"',
+            "# Line4",
+            "# Line5",
+        ]
+        with open(self.target_grubconf, "w") as fh:
+            fh.write("\n".join(existing))
+
+        new_args = ["foo=bar", "wark=1"]
+        newline = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+        expected = ("\n".join(existing[0:2]) + "\n" +
+                    newline + "\n" +
+                    "\n".join(existing[3:]))
+
+        install_grub.replace_grub_cmdline_linux_default(
+            self.target, new_args)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+            print(found)
+        self.assertEqual(expected, found)
+
+
+class TestWriteGrubConfig(CiTestCase):
+
+    def setUp(self):
+        super(TestWriteGrubConfig, self).setUp()
+        self.target = self.tmp_dir()
+        self.grubdefault = "/etc/default/grub"
+        self.grubconf = "/etc/default/grub.d/50-curtin.cfg"
+        self.target_grubdefault = paths.target_path(self.target,
+                                                    self.grubdefault)
+        self.target_grubconf = paths.target_path(self.target, self.grubconf)
+
+    def _verify_expected(self, expected_default, expected_curtin):
+
+        for expected, conffile in zip([expected_default, expected_curtin],
+                                      [self.target_grubdefault,
+                                       self.target_grubconf]):
+            if expected:
+                with open(conffile) as fh:
+                    found = fh.read()
+                self.assertEqual(expected, found)
+
+    def test_write_grub_config_defaults(self):
+        grubcfg = {}
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
+        expected_curtin = "\n".join([
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="true"',
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="console"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_no_replace(self):
+        grubcfg = {'replace_linux_default': False}
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([])
+        expected_curtin = "\n".join([
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="true"',
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="console"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_disable_probe(self):
+        grubcfg = {'probe_additional_os': False}  # DISABLE_OS_PROBER=1
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
+        expected_curtin = "\n".join([
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="true"',
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="console"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_enable_probe(self):
+        grubcfg = {'probe_additional_os': True}  # DISABLE_OS_PROBER=0, default
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
+        expected_curtin = "\n".join([
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="console"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_no_grub_settings_file(self):
+        grubcfg = {
+            'probe_additional_os': True,
+            'terminal': 'unmodified',
+        }
+        new_params = []
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+        self.assertTrue(os.path.exists(self.target_grubdefault))
+        self.assertFalse(os.path.exists(self.target_grubconf))
+
+    def test_write_grub_config_specify_terminal(self):
+        grubcfg = {'terminal': 'serial'}
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
+        expected_curtin = "\n".join([
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="true"',
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="serial"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_terminal_unmodified(self):
+        grubcfg = {'terminal': 'unmodified'}
+        new_params = ['foo=bar', 'wark=1']
+        expected_default = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"', ''])
+        expected_curtin = "\n".join([
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="true"', ''])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        self._verify_expected(expected_default, expected_curtin)
+
+    def test_write_grub_config_invalid_terminal(self):
+        grubcfg = {'terminal': ['color-tv']}
+        new_params = ['foo=bar', 'wark=1']
+        with self.assertRaises(ValueError):
+            install_grub.write_grub_config(
+                self.target, grubcfg, self.grubconf, new_params)
+
+
+class TestFindEfiLoader(CiTestCase):
+
+    def setUp(self):
+        super(TestFindEfiLoader, self).setUp()
+        self.target = self.tmp_dir()
+        self.efi_path = 'boot/efi/EFI'
+        self.target_efi_path = os.path.join(self.target, self.efi_path)
+        self.bootid = self.random_string()
+
+    def _possible_loaders(self):
+        return [
+            os.path.join(self.efi_path, self.bootid, 'shimx64.efi'),
+            os.path.join(self.efi_path, 'BOOT', 'BOOTX64.EFI'),
+            os.path.join(self.efi_path, self.bootid, 'grubx64.efi'),
+        ]
+
+    def test_return_none_with_no_loaders(self):
+        self.assertIsNone(
+            install_grub.find_efi_loader(self.target, self.bootid))
+
+    def test_prefer_shim_loader(self):
+        # touch loaders in target filesystem
+        loaders = self._possible_loaders()
+        for loader in loaders:
+            tloader = os.path.join(self.target, loader)
+            util.ensure_dir(os.path.dirname(tloader))
+            with open(tloader, 'w+') as fh:
+                fh.write('\n')
+
+        found = install_grub.find_efi_loader(self.target, self.bootid)
+        self.assertTrue(found.endswith(
+            os.path.join(self.efi_path, self.bootid, 'shimx64.efi')))
+
+    def test_prefer_existing_bootx_loader_with_no_shim(self):
+        # touch all loaders in target filesystem
+        loaders = self._possible_loaders()[1:]
+        for loader in loaders:
+            tloader = os.path.join(self.target, loader)
+            util.ensure_dir(os.path.dirname(tloader))
+            with open(tloader, 'w+') as fh:
+                fh.write('\n')
+
+        found = install_grub.find_efi_loader(self.target, self.bootid)
+        self.assertTrue(found.endswith(
+            os.path.join(self.efi_path, 'BOOT', 'BOOTX64.EFI')))
+
+    def test_prefer_existing_grub_loader_with_no_other_loader(self):
+        # touch all loaders in target filesystem
+        loaders = self._possible_loaders()[2:]
+        for loader in loaders:
+            tloader = os.path.join(self.target, loader)
+            util.ensure_dir(os.path.dirname(tloader))
+            with open(tloader, 'w+') as fh:
+                fh.write('\n')
+
+        found = install_grub.find_efi_loader(self.target, self.bootid)
+        print(found)
+        self.assertTrue(found.endswith(
+            os.path.join(self.efi_path, self.bootid, 'grubx64.efi')))
+
+
+class TestGetGrubInstallCommand(CiTestCase):
+
+    def setUp(self):
+        super(TestGetGrubInstallCommand, self).setUp()
+        self.add_patch('curtin.commands.install_grub.distro.os_release',
+                       'm_os_release')
+        self.add_patch('curtin.commands.install_grub.os.path.exists',
+                       'm_exists')
+        self.m_os_release.return_value = {'ID': 'ubuntu'}
+        self.m_exists.return_value = False
+        self.target = self.tmp_dir()
+
+    def test_grub_install_command_ubuntu_no_uefi(self):
+        uefi = False
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            'grub-install',
+            install_grub.get_grub_install_command(
+                uefi, distroinfo, self.target))
+
+    def test_grub_install_command_ubuntu_with_uefi(self):
+        self.m_exists.return_value = True
+        uefi = True
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            install_grub.GRUB_MULTI_INSTALL,
+            install_grub.get_grub_install_command(
+                uefi, distroinfo, self.target))
+
+    def test_grub_install_command_ubuntu_with_uefi_no_multi(self):
+        uefi = True
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            'grub-install',
+            install_grub.get_grub_install_command(
+                uefi, distroinfo, self.target))
+
+    def test_grub_install_command_redhat_no_uefi(self):
+        uefi = False
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        self.assertEqual(
+            'grub2-install',
+            install_grub.get_grub_install_command(
+                uefi, distroinfo, self.target))
+
+
+class TestGetEfiDiskPart(CiTestCase):
+
+    def setUp(self):
+        super(TestGetEfiDiskPart, self).setUp()
+        self.add_patch(
+            'curtin.commands.install_grub.block.get_blockdev_for_partition',
+            'm_blkpart')
+
+    def test_returns_first_result_with_partition(self):
+        self.m_blkpart.side_effect = iter([
+            ('/dev/disk-a', None),
+            ('/dev/disk-b', '1'),
+            ('/dev/disk-c', None),
+        ])
+        devices = ['/dev/disk-a', '/dev/disk-b', '/dev/disc-c']
+        self.assertEqual(('/dev/disk-b', '1'),
+                         install_grub.get_efi_disk_part(devices))
+
+    def test_returns_none_tuple_if_no_partitions(self):
+        self.m_blkpart.side_effect = iter([
+            ('/dev/disk-a', None),
+            ('/dev/disk-b', None),
+            ('/dev/disk-c', None),
+        ])
+        devices = ['/dev/disk-a', '/dev/disk-b', '/dev/disc-c']
+        self.assertEqual((None, None),
+                         install_grub.get_efi_disk_part(devices))
+
+
+class TestGenUefiInstallCommands(CiTestCase):
+
+    def setUp(self):
+        super(TestGenUefiInstallCommands, self).setUp()
+        self.add_patch(
+            'curtin.commands.install_grub.get_efi_disk_part',
+            'm_get_disk_part')
+        self.add_patch('curtin.commands.install_grub.distro.os_release',
+                       'm_os_release')
+        self.m_os_release.return_value = {'ID': 'ubuntu'}
+        self.target = self.tmp_dir()
+
+    def test_unsupported_distro_family_raises_valueerror(self):
+        self.m_os_release.return_value = {'ID': 'arch'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        grub_name = 'grub-efi-amd64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        with self.assertRaises(ValueError):
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target)
+
+    def test_ubuntu_install(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        grub_name = 'grub-efi-amd64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_install = [
+            ['efibootmgr', '-v'],
+            ['dpkg-reconfigure', grub_name],
+            ['update-grub'],
+            [grub_cmd, '--target=%s' % grub_target,
+             '--efi-directory=/boot/efi',
+             '--bootloader-id=%s' % distroinfo.variant, '--recheck'],
+        ]
+        expected_post = [['efibootmgr', '-v']]
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram,
+                distroinfo, devices, self.target))
+
+    def test_ubuntu_install_multiple_esp(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        grub_name = 'grub-efi-amd64'
+        grub_cmd = install_grub.GRUB_MULTI_INSTALL
+        grub_target = 'x86_64-efi'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_install = [
+            ['efibootmgr', '-v'],
+            ['dpkg-reconfigure', grub_name],
+            ['update-grub'],
+            [install_grub.GRUB_MULTI_INSTALL],
+        ]
+        expected_post = [['efibootmgr', '-v']]
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
+    def test_redhat_install(self):
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        grub_name = 'grub2-efi-x64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub2-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_install = [
+            ['efibootmgr', '-v'],
+            [grub_cmd, '--target=%s' % grub_target,
+             '--efi-directory=/boot/efi',
+             '--bootloader-id=%s' % distroinfo.variant, '--recheck'],
+        ]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'],
+            ['efibootmgr', '-v']
+        ]
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
+    def test_redhat_install_existing(self):
+        # simulate existing bootloaders already installed in target system
+        # by touching the files grub would have installed, including shim
+        def _enable_loaders(bootid):
+            efi_path = 'boot/efi/EFI'
+            target_efi_path = os.path.join(self.target, efi_path)
+            loaders = [
+                os.path.join(target_efi_path, bootid, 'shimx64.efi'),
+                os.path.join(target_efi_path, 'BOOT', 'BOOTX64.EFI'),
+                os.path.join(target_efi_path, bootid, 'grubx64.efi'),
+            ]
+            for loader in loaders:
+                util.ensure_dir(os.path.dirname(loader))
+                with open(loader, 'w+') as fh:
+                    fh.write('\n')
+
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        bootid = distroinfo.variant
+        _enable_loaders(bootid)
+        grub_name = 'grub2-efi-x64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub2-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_loader = '/boot/efi/EFI/%s/shimx64.efi' % bootid
+        expected_install = [
+            ['efibootmgr', '-v'],
+            ['efibootmgr', '--create', '--write-signature',
+             '--label', bootid, '--disk', disk, '--part', part,
+             '--loader', expected_loader],
+        ]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/efi/EFI/%s/grub.cfg' % bootid],
+            ['efibootmgr', '-v']
+        ]
+
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
+
+class TestGenInstallCommands(CiTestCase):
+
+    def setUp(self):
+        super(TestGenInstallCommands, self).setUp()
+        self.add_patch('curtin.commands.install_grub.distro.os_release',
+                       'm_os_release')
+        self.m_os_release.return_value = {'ID': 'ubuntu'}
+
+    def test_unsupported_install(self):
+        self.m_os_release.return_value = {'ID': 'gentoo'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        devices = ['/dev/disk-a-part1', '/dev/disk-b-part1']
+        rhel_ver = None
+        grub_name = 'grub-pc'
+        grub_cmd = 'grub-install'
+        with self.assertRaises(ValueError):
+            install_grub.gen_install_commands(
+                grub_name, grub_cmd, distroinfo, devices, rhel_ver)
+
+    def test_ubuntu_install(self):
+        distroinfo = install_grub.distro.get_distroinfo()
+        devices = ['/dev/disk-a-part1', '/dev/disk-b-part1']
+        rhel_ver = None
+        grub_name = 'grub-pc'
+        grub_cmd = 'grub-install'
+        expected_install = [
+            ['dpkg-reconfigure', grub_name],
+            ['update-grub']
+        ] + [[grub_cmd, dev] for dev in devices]
+        expected_post = []
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_install_commands(
+                grub_name, grub_cmd, distroinfo, devices, rhel_ver))
+
+    def test_redhat_6_install_unsupported(self):
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        devices = ['/dev/disk-a-part1', '/dev/disk-b-part1']
+        rhel_ver = '6'
+        grub_name = 'grub-pc'
+        grub_cmd = 'grub-install'
+        with self.assertRaises(ValueError):
+            install_grub.gen_install_commands(
+                grub_name, grub_cmd, distroinfo, devices, rhel_ver)
+
+    def test_redhatp_7_or_8_install(self):
+        self.m_os_release.return_value = {'ID': 'redhat'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        devices = ['/dev/disk-a-part1', '/dev/disk-b-part1']
+        rhel_ver = '7'
+        grub_name = 'grub-pc'
+        grub_cmd = 'grub2-install'
+        expected_install = [[grub_cmd, dev] for dev in devices]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg']
+        ]
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_install_commands(
+                grub_name, grub_cmd, distroinfo, devices, rhel_ver))
+
+
+@mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+class TestInstallGrub(CiTestCase):
+
+    def setUp(self):
+        super(TestInstallGrub, self).setUp()
+        base = 'curtin.commands.install_grub.'
+        self.add_patch(base + 'distro.get_distroinfo',
+                       'm_distro_get_distroinfo')
+        self.add_patch(base + 'distro.get_architecture',
+                       'm_distro_get_architecture')
+        self.add_patch(base + 'distro.rpm_get_dist_id',
+                       'm_distro_rpm_get_dist_id')
+        self.add_patch(base + 'get_grub_package_name',
+                       'm_get_grub_package_name')
+        self.add_patch(base + 'platform.machine', 'm_platform_machine')
+        self.add_patch(base + 'get_grub_config_file', 'm_get_grub_config_file')
+        self.add_patch(base + 'get_carryover_params', 'm_get_carryover_params')
+        self.add_patch(base + 'prepare_grub_dir', 'm_prepare_grub_dir')
+        self.add_patch(base + 'write_grub_config', 'm_write_grub_config')
+        self.add_patch(base + 'get_grub_install_command',
+                       'm_get_grub_install_command')
+        self.add_patch(base + 'gen_uefi_install_commands',
+                       'm_gen_uefi_install_commands')
+        self.add_patch(base + 'gen_install_commands', 'm_gen_install_commands')
+        self.add_patch(base + 'util.subp', 'm_subp')
+        self.add_patch(base + 'os.environ.copy', 'm_environ')
+
+        self.distroinfo = distro.DistroInfo('ubuntu', 'debian')
+        self.m_distro_get_distroinfo.return_value = self.distroinfo
+        self.m_distro_rpm_get_dist_id.return_value = '7'
+        self.m_distro_get_architecture.return_value = 'amd64'
+        self.m_platform_machine.return_value = 'amd64'
+        self.m_environ.return_value = {}
+        self.env = {'DEBIAN_FRONTEND': 'noninteractive'}
+        self.target = self.tmp_dir()
+
+    def test_grub_install_raise_exception_on_no_devices(self):
+        devices = []
+        with self.assertRaises(ValueError):
+            install_grub.install_grub(devices, self.target, False, {})
+
+    def test_grub_install_raise_exception_on_no_target(self):
+        devices = ['foobar']
+        with self.assertRaises(ValueError):
+            install_grub.install_grub(devices, None, False, {})
+
+    def test_grub_install_raise_exception_on_s390x(self):
+        self.m_distro_get_architecture.return_value = 's390x'
+        self.m_platform_machine.return_value = 's390x'
+        devices = ['foobar']
+        with self.assertRaises(RuntimeError):
+            install_grub.install_grub(devices, self.target, False, {})
+
+    def test_grub_install_raise_exception_on_armv7(self):
+        self.m_distro_get_architecture.return_value = 'armhf'
+        self.m_platform_machine.return_value = 'armv7l'
+        devices = ['foobar']
+        with self.assertRaises(RuntimeError):
+            install_grub.install_grub(devices, self.target, False, {})
+
+    def test_grub_install_raise_exception_on_arm64_no_uefi(self):
+        self.m_distro_get_architecture.return_value = 'arm64'
+        self.m_platform_machine.return_value = 'aarch64'
+        uefi = False
+        devices = ['foobar']
+        with self.assertRaises(RuntimeError):
+            install_grub.install_grub(devices, self.target, uefi, {})
+
+    def test_grub_install_ubuntu(self):
+        devices = ['/dev/disk-a-part1']
+        uefi = False
+        grubcfg = {}
+        grub_conf = self.tmp_path('grubconf')
+        new_params = []
+        self.m_get_grub_package_name.return_value = ('grub-pc', 'i386-pc')
+        self.m_get_grub_config_file.return_value = grub_conf
+        self.m_get_carryover_params.return_value = new_params
+        self.m_get_grub_install_command.return_value = 'grub-install'
+        self.m_gen_install_commands.return_value = (
+            [['/bin/true']], [['/bin/false']])
+
+        install_grub.install_grub(devices, self.target, uefi, grubcfg)
+
+        self.m_distro_get_distroinfo.assert_called_with(target=self.target)
+        self.m_distro_get_architecture.assert_called_with(target=self.target)
+        self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
+        self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+        self.m_get_grub_config_file.assert_called_with(self.target,
+                                                       self.distroinfo.family)
+        self.m_get_carryover_params.assert_called_with(self.distroinfo)
+        self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
+        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+                                                    grub_conf, new_params)
+        self.m_get_grub_install_command.assert_called_with(
+            uefi, self.distroinfo, self.target)
+        self.m_gen_install_commands.assert_called_with(
+            'grub-pc', 'grub-install', self.distroinfo, devices, None)
+
+        self.m_subp.assert_has_calls([
+            mock.call(['/bin/true'], env=self.env, capture=True,
+                      target=self.target),
+            mock.call(['/bin/false'], env=self.env, capture=True,
+                      target=self.target),
+        ])
+
+    def test_uefi_grub_install_ubuntu(self):
+        devices = ['/dev/disk-a-part1']
+        uefi = True
+        update_nvram = True
+        grubcfg = {'update_nvram': update_nvram}
+        grub_conf = self.tmp_path('grubconf')
+        new_params = []
+        grub_name = 'grub-efi-amd64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub-install'
+        self.m_get_grub_package_name.return_value = (grub_name, grub_target)
+        self.m_get_grub_config_file.return_value = grub_conf
+        self.m_get_carryover_params.return_value = new_params
+        self.m_get_grub_install_command.return_value = grub_cmd
+        self.m_gen_uefi_install_commands.return_value = (
+            [['/bin/true']], [['/bin/false']])
+
+        install_grub.install_grub(devices, self.target, uefi, grubcfg)
+
+        self.m_distro_get_distroinfo.assert_called_with(target=self.target)
+        self.m_distro_get_architecture.assert_called_with(target=self.target)
+        self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
+        self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+        self.m_get_grub_config_file.assert_called_with(self.target,
+                                                       self.distroinfo.family)
+        self.m_get_carryover_params.assert_called_with(self.distroinfo)
+        self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
+        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+                                                    grub_conf, new_params)
+        self.m_get_grub_install_command.assert_called_with(
+            uefi, self.distroinfo, self.target)
+        self.m_gen_uefi_install_commands.assert_called_with(
+            grub_name, grub_target, grub_cmd, update_nvram, self.distroinfo,
+            devices, self.target)
+
+        self.m_subp.assert_has_calls([
+            mock.call(['/bin/true'], env=self.env, capture=True,
+                      target=self.target),
+            mock.call(['/bin/false'], env=self.env, capture=True,
+                      target=self.target),
+        ])
+
+    def test_uefi_grub_install_ubuntu_multiple_esp(self):
+        devices = ['/dev/disk-a-part1']
+        uefi = True
+        update_nvram = True
+        grubcfg = {'update_nvram': update_nvram}
+        grub_conf = self.tmp_path('grubconf')
+        new_params = []
+        grub_name = 'grub-efi-amd64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = install_grub.GRUB_MULTI_INSTALL
+        self.m_get_grub_package_name.return_value = (grub_name, grub_target)
+        self.m_get_grub_config_file.return_value = grub_conf
+        self.m_get_carryover_params.return_value = new_params
+        self.m_get_grub_install_command.return_value = grub_cmd
+        self.m_gen_uefi_install_commands.return_value = (
+            [['/bin/true']], [['/bin/false']])
+
+        install_grub.install_grub(devices, self.target, uefi, grubcfg)
+
+        self.m_distro_get_distroinfo.assert_called_with(target=self.target)
+        self.m_distro_get_architecture.assert_called_with(target=self.target)
+        self.assertEqual(0, self.m_distro_rpm_get_dist_id.call_count)
+        self.m_get_grub_package_name.assert_called_with('amd64', uefi, None)
+        self.m_get_grub_config_file.assert_called_with(self.target,
+                                                       self.distroinfo.family)
+        self.m_get_carryover_params.assert_called_with(self.distroinfo)
+        self.m_prepare_grub_dir.assert_called_with(self.target, grub_conf)
+        self.m_write_grub_config.assert_called_with(self.target, grubcfg,
+                                                    grub_conf, new_params)
+        self.m_get_grub_install_command.assert_called_with(
+            uefi, self.distroinfo, self.target)
+        self.m_gen_uefi_install_commands.assert_called_with(
+            grub_name, grub_target, grub_cmd, update_nvram, self.distroinfo,
+            devices, self.target)
+
+        self.m_subp.assert_has_calls([
+            mock.call(['/bin/true'], env=self.env, capture=True,
+                      target=self.target),
+            mock.call(['/bin/false'], env=self.env, capture=True,
+                      target=self.target),
+        ])
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_commands_net_meta.py b/tests/unittests/test_commands_net_meta.py
new file mode 100644
index 0000000..76da74b
--- /dev/null
+++ b/tests/unittests/test_commands_net_meta.py
@@ -0,0 +1,111 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+import os
+
+from mock import MagicMock, call
+
+from .helpers import CiTestCase, simple_mocked_open
+
+from curtin.commands.net_meta import net_meta
+
+
+class NetMetaTarget:
+    def __init__(self, target, mode=None, devices=None):
+        self.target = target
+        self.mode = mode
+        self.devices = devices
+
+
+class TestNetMeta(CiTestCase):
+
+    def setUp(self):
+        super(TestNetMeta, self).setUp()
+
+        self.add_patch('curtin.util.run_hook_if_exists', 'm_run_hook')
+        self.add_patch('curtin.util.load_command_environment', 'm_command_env')
+        self.add_patch('curtin.config.load_command_config', 'm_command_config')
+        self.add_patch('curtin.config.dump_config', 'm_dump_config')
+        self.add_patch('os.environ', 'm_os_environ')
+
+        self.args = NetMetaTarget(
+            target='net-meta-target'
+        )
+
+        self.base_network_config = {
+            'network': {
+                'version': 1,
+                'config': {
+                    'type': 'physical',
+                    'name': 'interface0',
+                    'mac_address': '52:54:00:12:34:00',
+                    'subnets': {
+                        'type': 'dhcp4'
+                    }
+                }
+            }
+        }
+
+        self.disabled_network_config = {
+            'network': {
+                'version': 1,
+                'config': 'disabled'
+            }
+        }
+
+        self.output_network_path = self.tmp_path('my-network-config')
+        self.expected_exit_code = 0
+        self.m_run_hook.return_value = False
+        self.m_command_env.return_value = {}
+        self.m_command_config.return_value = self.base_network_config
+        self.m_os_environ.get.return_value = self.output_network_path
+
+        self.dump_content = 'yaml-format-network-config'
+        self.m_dump_config.return_value = self.dump_content
+
+    def test_net_meta_with_disabled_network(self):
+        self.args.mode = 'disabled'
+
+        with self.assertRaises(SystemExit) as cm:
+            with simple_mocked_open(content='') as m_open:
+                net_meta(self.args)
+
+        self.assertEqual(self.expected_exit_code, cm.exception.code)
+        self.m_run_hook.assert_called_with(
+            self.args.target, 'network-config')
+        self.assertEqual(1, self.m_run_hook.call_count)
+        self.assertEqual(0, self.m_command_env.call_count)
+        self.assertEqual(0, self.m_command_config.call_count)
+
+        self.assertEquals(self.args.mode, 'disabled')
+        self.assertEqual(0, self.m_os_environ.get.call_count)
+        self.assertEqual(0, self.m_dump_config.call_count)
+        self.assertFalse(os.path.exists(self.output_network_path))
+        self.assertEqual(0, m_open.call_count)
+
+    def test_net_meta_with_config_network(self):
+        network_config = self.disabled_network_config
+        self.m_command_config.return_value = network_config
+
+        expected_m_command_env_calls = 2
+        expected_m_command_config_calls = 2
+        m_file = MagicMock()
+
+        with self.assertRaises(SystemExit) as cm:
+            with simple_mocked_open(content='') as m_open:
+                m_open.return_value = m_file
+                net_meta(self.args)
+
+        self.assertEqual(self.expected_exit_code, cm.exception.code)
+        self.m_run_hook.assert_called_with(
+            self.args.target, 'network-config')
+        self.assertEquals(self.args.mode, 'custom')
+        self.assertEqual(
+            expected_m_command_env_calls, self.m_command_env.call_count)
+        self.assertEqual(
+            expected_m_command_config_calls, self.m_command_env.call_count)
+        self.m_dump_config.assert_called_with(network_config)
+        self.assertEqual(
+            [call(self.output_network_path, 'w')], m_open.call_args_list)
+        self.assertEqual(
+            [call(self.dump_content)],
+            m_file.__enter__.return_value.write.call_args_list)
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index ff38240..2349456 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -1,10 +1,11 @@
 # This file is part of curtin. See LICENSE file for copyright and license info.
 
 import os
-from mock import call, patch, MagicMock
+from mock import call, patch
 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
@@ -16,8 +17,10 @@ class TestGetFlashKernelPkgs(CiTestCase):
     def setUp(self):
         super(TestGetFlashKernelPkgs, self).setUp()
         self.add_patch('curtin.util.subp', 'mock_subp')
-        self.add_patch('curtin.util.get_architecture', 'mock_get_architecture')
-        self.add_patch('curtin.util.is_uefi_bootable', 'mock_is_uefi_bootable')
+        self.add_patch('curtin.distro.get_architecture',
+                       'mock_get_architecture')
+        self.add_patch('curtin.util.is_uefi_bootable',
+                       'mock_is_uefi_bootable')
 
     def test__returns_none_when_uefi(self):
         self.assertIsNone(curthooks.get_flash_kernel_pkgs(uefi=True))
@@ -195,7 +198,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)
@@ -304,7 +309,7 @@ class TestSetupKernelImgConf(CiTestCase):
     def setUp(self):
         super(TestSetupKernelImgConf, self).setUp()
         self.add_patch('platform.machine', 'mock_machine')
-        self.add_patch('curtin.util.get_architecture', 'mock_arch')
+        self.add_patch('curtin.distro.get_architecture', 'mock_arch')
         self.add_patch('curtin.util.write_file', 'mock_write_file')
         self.target = 'not-a-real-target'
         self.add_patch('curtin.distro.lsb_release', 'mock_lsb_release')
@@ -374,7 +379,7 @@ class TestInstallMissingPkgs(CiTestCase):
     def setUp(self):
         super(TestInstallMissingPkgs, self).setUp()
         self.add_patch('platform.machine', 'mock_machine')
-        self.add_patch('curtin.util.get_architecture', 'mock_arch')
+        self.add_patch('curtin.distro.get_architecture', 'mock_arch')
         self.add_patch('curtin.distro.get_installed_packages',
                        'mock_get_installed_packages')
         self.add_patch('curtin.util.load_command_environment',
@@ -533,39 +538,27 @@ class TestSetupGrub(CiTestCase):
         self.target = self.tmp_dir()
         self.distro_family = distro.DISTROS.debian
         self.add_patch('curtin.distro.lsb_release', 'mock_lsb_release')
-        self.mock_lsb_release.return_value = {
-            'codename': 'xenial',
-        }
+        self.mock_lsb_release.return_value = {'codename': 'xenial'}
         self.add_patch('curtin.util.is_uefi_bootable',
                        'mock_is_uefi_bootable')
         self.mock_is_uefi_bootable.return_value = False
-        self.add_patch('curtin.util.subp', 'mock_subp')
-        self.subp_output = []
-        self.mock_subp.side_effect = iter(self.subp_output)
         self.add_patch('curtin.commands.block_meta.devsync', 'mock_devsync')
-        self.add_patch('curtin.util.get_architecture', 'mock_arch')
-        self.mock_arch.return_value = 'amd64'
-        self.add_patch(
-            'curtin.util.ChrootableTarget', 'mock_chroot', autospec=False)
-        self.mock_in_chroot = MagicMock()
-        self.mock_in_chroot.__enter__.return_value = self.mock_in_chroot
-        self.in_chroot_subp_output = []
-        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.util.subp', 'mock_subp')
+        self.add_patch('curtin.commands.curthooks.install_grub',
+                       'm_install_grub')
+        self.add_patch('curtin.commands.curthooks.configure_grub_debconf',
+                       'm_configure_grub_debconf')
 
     def test_uses_old_grub_install_devices_in_cfg(self):
         cfg = {
             'grub_install_devices': ['/dev/vdb']
         }
-        self.subp_output.append(('', ''))
+        updated_cfg = {
+            'install_devices': ['/dev/vdb']
+        }
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--os-family=%s' % self.distro_family,
-                self.target, '/dev/vdb'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False, grubcfg=updated_cfg)
 
     def test_uses_install_devices_in_grubcfg(self):
         cfg = {
@@ -573,17 +566,14 @@ class TestSetupGrub(CiTestCase):
                 'install_devices': ['/dev/vdb'],
             },
         }
-        self.subp_output.append(('', ''))
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--os-family=%s' % self.distro_family,
-                self.target, '/dev/vdb'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False, grubcfg=cfg.get('grub'))
 
+    @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,
@@ -597,20 +587,18 @@ class TestSetupGrub(CiTestCase):
                 ]
             },
         }
-        self.subp_output.append(('', ''))
         m_exists.return_value = True
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--os-family=%s' % self.distro_family,
-                self.target, '/dev/vdb'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=False,
+            grubcfg={'install_devices': ['/dev/vdb']})
 
+    @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 +615,7 @@ class TestSetupGrub(CiTestCase):
                         'id': 'vdb-part1',
                         'type': 'partition',
                         'device': 'vdb',
+                        'flag': 'boot',
                         'number': 1,
                     },
                     {
@@ -647,17 +636,13 @@ class TestSetupGrub(CiTestCase):
                 'update_nvram': False,
             },
         }
-        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(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--uefi',
-                '--os-family=%s' % distro.DISTROS.redhat, self.target,
-                '/dev/vdb1'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb1'], self.target, uefi=True,
+            grubcfg={'update_nvram': False, 'install_devices': ['/dev/vdb1']}
+        )
 
     def test_grub_install_installs_to_none_if_install_devices_None(self):
         cfg = {
@@ -665,15 +650,13 @@ class TestSetupGrub(CiTestCase):
                 'install_devices': None,
             },
         }
-        self.subp_output.append(('', ''))
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--os-family=%s' % self.distro_family,
-                self.target, 'none'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['none'], self.target, uefi=False,
+            grubcfg={'install_devices': None}
+        )
 
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_grub_install_uefi_updates_nvram_skips_remove_and_reorder(self):
         self.add_patch('curtin.distro.install_packages', 'mock_install')
         self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
@@ -687,7 +670,6 @@ class TestSetupGrub(CiTestCase):
                 'reorder_uefi': False,
             },
         }
-        self.subp_output.append(('', ''))
         self.mock_haspkg.return_value = False
         self.mock_efibootmgr.return_value = {
             'current': '0000',
@@ -700,14 +682,11 @@ class TestSetupGrub(CiTestCase):
             }
         }
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                'sh', '-c', 'exec "$0" "$@" 2>&1',
-                'install-grub', '--uefi', '--update-nvram',
-                '--os-family=%s' % self.distro_family,
-                self.target, '/dev/vdb'],),
-            self.mock_subp.call_args_list[0][0])
+        self.m_install_grub.assert_called_with(
+            ['/dev/vdb'], self.target, uefi=True, grubcfg=cfg.get('grub')
+        )
 
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_grub_install_uefi_updates_nvram_removes_old_loaders(self):
         self.add_patch('curtin.distro.install_packages', 'mock_install')
         self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
@@ -721,7 +700,6 @@ class TestSetupGrub(CiTestCase):
                 'reorder_uefi': False,
             },
         }
-        self.subp_output.append(('', ''))
         self.mock_efibootmgr.return_value = {
             'current': '0000',
             'entries': {
@@ -742,22 +720,19 @@ class TestSetupGrub(CiTestCase):
                 },
             }
         }
-        self.in_chroot_subp_output.append(('', ''))
-        self.in_chroot_subp_output.append(('', ''))
         self.mock_haspkg.return_value = False
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ['efibootmgr', '-B', '-b'],
-            self.mock_in_chroot_subp.call_args_list[0][0][0][:3])
-        self.assertEquals(
-            ['efibootmgr', '-B', '-b'],
-            self.mock_in_chroot_subp.call_args_list[1][0][0][:3])
-        self.assertEquals(
-            set(['0001', '0002']),
-            set([
-                self.mock_in_chroot_subp.call_args_list[0][0][0][3],
-                self.mock_in_chroot_subp.call_args_list[1][0][0][3]]))
 
+        expected_calls = [
+            call(['efibootmgr', '-B', '-b', '0001'],
+                 capture=True, target=self.target),
+            call(['efibootmgr', '-B', '-b', '0002'],
+                 capture=True, target=self.target),
+        ]
+        self.assertEqual(sorted(expected_calls),
+                         sorted(self.mock_subp.call_args_list))
+
+    @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_grub_install_uefi_updates_nvram_reorders_loaders(self):
         self.add_patch('curtin.distro.install_packages', 'mock_install')
         self.add_patch('curtin.distro.has_pkg_available', 'mock_haspkg')
@@ -771,7 +746,6 @@ class TestSetupGrub(CiTestCase):
                 'reorder_uefi': True,
             },
         }
-        self.subp_output.append(('', ''))
         self.mock_efibootmgr.return_value = {
             'current': '0001',
             'order': ['0000', '0001'],
@@ -787,23 +761,122 @@ class TestSetupGrub(CiTestCase):
                 },
             }
         }
-        self.in_chroot_subp_output.append(('', ''))
         self.mock_haspkg.return_value = False
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            (['efibootmgr', '-o', '0001,0000'],),
-            self.mock_in_chroot_subp.call_args_list[0][0])
+        self.assertEquals([
+            call(['efibootmgr', '-o', '0001,0000'], target=self.target)],
+            self.mock_subp.call_args_list)
+
+
+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

Follow ups