← Back to team overview

curtin-dev team mailing list archive

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

 

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

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

For more details, see:
https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/384582
-- 
Your team curtin developers is requested to review the proposed merge of ~raharper/curtin:ubuntu-devel-new-upstream-snapshot-20200526 into curtin:ubuntu/devel.
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/curtin/__init__.py b/curtin/__init__.py
index 7114b3b..2e1a0ed 100644
--- a/curtin/__init__.py
+++ b/curtin/__init__.py
@@ -34,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 35a91c6..35e3a64 100644
--- a/curtin/block/__init__.py
+++ b/curtin/block/__init__.py
@@ -1315,8 +1315,8 @@ 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_get_probert_data():
diff --git a/curtin/block/bcache.py b/curtin/block/bcache.py
index 188b4e0..c1a8d26 100644
--- a/curtin/block/bcache.py
+++ b/curtin/block/bcache.py
@@ -318,11 +318,11 @@ def validate_bcache_ready(bcache_device, bcache_sys_path):
         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'))]
+        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' % (
diff --git a/curtin/block/lvm.py b/curtin/block/lvm.py
index da29c7b..bd0f1aa 100644
--- a/curtin/block/lvm.py
+++ b/curtin/block/lvm.py
@@ -23,7 +23,7 @@ def _filter_lvm_info(lvtool, match_field, query_field, match_key, args=None):
                           '-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]
 
 
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 f012ae0..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)
 
@@ -188,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():
@@ -285,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:
@@ -512,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_meta.py b/curtin/commands/block_meta.py
index f2bb8da..ff0f2e9 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -760,7 +760,9 @@ def verify_ptable_flag(devpath, expected_flag, sfdisk_info=None):
         elif expected_flag == 'logical':
             (_parent, partnumber) = block.get_blockdev_for_partition(devpath)
             found_flag = 'logical' if int(partnumber) > 4 else None
-    else:
+
+    # 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' % (
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index 4afe00c..d66afa7 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -26,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
@@ -307,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:
@@ -682,28 +683,6 @@ 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:
@@ -711,38 +690,13 @@ def setup_grub(cfg, target, osfamily=DISTROS.debian):
     else:
         instdevs = ["none"]
 
-    if 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)
+    install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg)
 
-    with util.ChrootableTarget(target):
-        args = ['install-grub']
-        if 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")
-            if len(instdevs) > 1:
-                instdevs = [instdevs[0]]
-                LOG.debug("Selecting primary EFI boot device %s for install",
-                          instdevs[0])
-
-        args.append('--os-family=%s' % osfamily)
-        args.append(target)
-
-        # 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 uefi_bootable and grubcfg.get('update_nvram', True):
+    if uefi_bootable and update_nvram:
         uefi_remove_duplicate_entries(grubcfg, target)
         uefi_reorder_loaders(grubcfg, target)
 
@@ -1152,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')
@@ -1207,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)
@@ -1759,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"):
+
+        # 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
 
-    # all other paths lead to grub
-    setup_grub(cfg, target, osfamily=osfamily)
+        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 1f62e7a..43b0c19 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -357,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()
@@ -429,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):
@@ -549,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 e285f98..494b142 100644
--- a/curtin/storage_config.py
+++ b/curtin/storage_config.py
@@ -34,12 +34,13 @@ GPT_GUID_TO_CURTIN_MAP = {
 MBR_TYPE_TO_CURTIN_MAP = {
     '0XF': ('extended', 'f'),
     '0X5': ('extended', 'f'),
-    '0X80': ('boot', '80'),
     '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'))
@@ -820,16 +821,20 @@ class BlockdevParser(ProbertParser):
                 entry['size'] *= 512
 
             ptype = blockdev_data.get('ID_PART_ENTRY_TYPE')
-            # use PART_ENTRY_FLAGS if set, msdos
-            ptype_flag = blockdev_data.get('ID_PART_ENTRY_FLAGS')
-            if ptype_flag:
-                ptype = ptype_flag
             flag_name, _flag_code = ptable_uuid_to_flag_entry(ptype)
 
-            # logical partitions are not tagged in data, however
-            # the partition number > 4 (ie, not primary nor extended)
-            if ptable and ptable.get('label') == 'dos' and entry['number'] > 4:
-                flag_name = 'logical'
+            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
diff --git a/curtin/util.py b/curtin/util.py
index afef58d..be063d7 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -799,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
diff --git a/debian/changelog b/debian/changelog
index d0a4c9e..e1a75e1 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,23 @@
+curtin (20.1-0ubuntu1) groovy; urgency=medium
+
+  * New upstream release.
+    - Release 20.1 (LP: #1880741)
+    - Handle multiple separators which were found in TestAllindata vmtest
+    - verify_ptable_flag: dos primary partitions use ptable_uuid map for flag
+      (LP: #1878890)
+    - 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]
+
+ -- Ryan Harper <ryan.harper@xxxxxxxxxxxxx>  Tue, 26 May 2020 16:31:39 -0500
+
 curtin (19.3-68-g6cbdc02d-0ubuntu1) groovy; urgency=medium
 
   * New upstream snapshot.
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/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/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/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/unittests/test_apt_custom_sources_list.py b/tests/unittests/test_apt_custom_sources_list.py
index bf004b1..dafc478 100644
--- a/tests/unittests/test_apt_custom_sources_list.py
+++ b/tests/unittests/test_apt_custom_sources_list.py
@@ -100,11 +100,12 @@ class TestAptSourceConfigSourceList(CiTestCase):
     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",
@@ -143,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)
@@ -155,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 6ae5579..6556399 100644
--- a/tests/unittests/test_apt_source.py
+++ b/tests/unittests/test_apt_source.py
@@ -90,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
 
@@ -457,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"
@@ -495,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)
@@ -577,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"]
@@ -628,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
@@ -645,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"]
@@ -958,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_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_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index 4cc9299..b768cdc 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -2446,4 +2446,115 @@ class TestVerifySize(CiTestCase):
         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_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 c126f3a..2349456 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -1,7 +1,7 @@
 # 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
@@ -17,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))
@@ -307,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')
@@ -377,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',
@@ -536,41 +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_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 = {
@@ -578,14 +566,9 @@ 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')
@@ -604,16 +587,11 @@ class TestSetupGrub(CiTestCase):
                 ]
             },
         }
-        self.subp_output.append(('', ''))
-        self.subp_output.append(('', ''))
         m_exists.return_value = True
         curthooks.setup_grub(cfg, self.target, osfamily=self.distro_family)
-        self.assertEquals(
-            ([
-                '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')
@@ -658,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, 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 = {
@@ -676,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')
@@ -698,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',
@@ -711,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')
@@ -732,7 +700,6 @@ class TestSetupGrub(CiTestCase):
                 'reorder_uefi': False,
             },
         }
-        self.subp_output.append(('', ''))
         self.mock_efibootmgr.return_value = {
             'current': '0000',
             'entries': {
@@ -753,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')
@@ -782,7 +746,6 @@ class TestSetupGrub(CiTestCase):
                 'reorder_uefi': True,
             },
         }
-        self.subp_output.append(('', ''))
         self.mock_efibootmgr.return_value = {
             'current': '0001',
             'order': ['0000', '0001'],
@@ -798,12 +761,11 @@ 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):
@@ -853,8 +815,8 @@ class TestUefiRemoveDuplicateEntries(CiTestCase):
             call(['efibootmgr', '--bootnum=0001', '--delete-bootnum'],
                  target=self.target),
             call(['efibootmgr', '--bootnum=0003', '--delete-bootnum'],
-                 target=self.target)],
-            self.m_subp.call_args_list)
+                 target=self.target)
+            ], self.m_subp.call_args_list)
 
     @patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
     def test_uefi_remove_duplicate_entries_no_change(self):
diff --git a/tests/unittests/test_distro.py b/tests/unittests/test_distro.py
index c994963..eb62dd8 100644
--- a/tests/unittests/test_distro.py
+++ b/tests/unittests/test_distro.py
@@ -490,4 +490,48 @@ class TestHasPkgAvailable(CiTestCase):
         self.assertEqual(pkg == self.package, result)
         m_subp.assert_has_calls([mock.call('list', opts=['--cacheonly'])])
 
+
+class TestGetArchitecture(CiTestCase):
+
+    def setUp(self):
+        super(TestGetArchitecture, self).setUp()
+        self.target = paths.target_path('mytarget')
+        self.add_patch('curtin.util.subp', 'm_subp')
+        self.add_patch('curtin.distro.get_osfamily', 'm_get_osfamily')
+        self.add_patch('curtin.distro.dpkg_get_architecture',
+                       'm_dpkg_get_arch')
+        self.add_patch('curtin.distro.rpm_get_architecture',
+                       'm_rpm_get_arch')
+        self.m_get_osfamily.return_value = distro.DISTROS.debian
+
+    def test_osfamily_none_calls_get_osfamily(self):
+        distro.get_architecture(target=self.target, osfamily=None)
+        self.assertEqual([mock.call(target=self.target)],
+                         self.m_get_osfamily.call_args_list)
+
+    def test_unhandled_osfamily_raises_value_error(self):
+        osfamily = distro.DISTROS.arch
+        with self.assertRaises(ValueError):
+            distro.get_architecture(target=self.target, osfamily=osfamily)
+        self.assertEqual(0, self.m_dpkg_get_arch.call_count)
+        self.assertEqual(0, self.m_rpm_get_arch.call_count)
+
+    def test_debian_osfamily_calls_dpkg_get_arch(self):
+        osfamily = distro.DISTROS.debian
+        expected_result = self.m_dpkg_get_arch.return_value
+        result = distro.get_architecture(target=self.target, osfamily=osfamily)
+        self.assertEqual(expected_result, result)
+        self.assertEqual([mock.call(target=self.target)],
+                         self.m_dpkg_get_arch.call_args_list)
+        self.assertEqual(0, self.m_rpm_get_arch.call_count)
+
+    def test_redhat_osfamily_calls_rpm_get_arch(self):
+        osfamily = distro.DISTROS.redhat
+        expected_result = self.m_rpm_get_arch.return_value
+        result = distro.get_architecture(target=self.target, osfamily=osfamily)
+        self.assertEqual(expected_result, result)
+        self.assertEqual([mock.call(target=self.target)],
+                         self.m_rpm_get_arch.call_args_list)
+        self.assertEqual(0, self.m_dpkg_get_arch.call_count)
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/unittests/test_storage_config.py b/tests/unittests/test_storage_config.py
index ecdc565..a38f9cd 100644
--- a/tests/unittests/test_storage_config.py
+++ b/tests/unittests/test_storage_config.py
@@ -405,6 +405,40 @@ class TestBlockdevParser(CiTestCase):
         self.assertDictEqual(expected_dict,
                              self.bdevp.asdict(blockdev))
 
+    def test_blockdev_detects_dos_bootable_flag(self):
+        self.probe_data = _get_data(
+            'probert_storage_msdos_mbr_extended_v2.json')
+        self.bdevp = BlockdevParser(self.probe_data)
+        blockdev = self.bdevp.blockdev_data['/dev/vdb1']
+        expected_dict = {
+            'id': 'partition-vdb1',
+            'type': 'partition',
+            'device': 'disk-vdb',
+            'number': 1,
+            'offset': 1048576,
+            'size': 536870912,
+            'flag': 'boot',
+        }
+        self.assertDictEqual(expected_dict,
+                             self.bdevp.asdict(blockdev))
+
+    def test_blockdev_detects_dos_bootable_flag_on_logical_partitions(self):
+        self.probe_data = _get_data('probert_storage_lvm.json')
+        self.bdevp = BlockdevParser(self.probe_data)
+        blockdev = self.bdevp.blockdev_data['/dev/vda5']
+        blockdev['ID_PART_ENTRY_FLAGS'] = '0x80'
+        expected_dict = {
+            'id': 'partition-vda5',
+            'type': 'partition',
+            'device': 'disk-vda',
+            'number': 5,
+            'offset': 3223322624,
+            'size': 2147483648,
+            'flag': 'boot',
+        }
+        self.assertDictEqual(expected_dict,
+                             self.bdevp.asdict(blockdev))
+
     def test_blockdev_asdict_disk_omits_ptable_if_none_present(self):
         blockdev = self.bdevp.blockdev_data['/dev/sda']
         del blockdev['ID_PART_TABLE_TYPE']
diff --git a/tests/vmtests/__init__.py b/tests/vmtests/__init__.py
index 222adcc..32cd5fd 100644
--- a/tests/vmtests/__init__.py
+++ b/tests/vmtests/__init__.py
@@ -601,6 +601,7 @@ class VMBaseClass(TestCase):
     arch_skip = []
     boot_timeout = BOOT_TIMEOUT
     collect_scripts = []
+    crashdump = False
     extra_collect_scripts = []
     conf_file = "examples/tests/basic.yaml"
     nr_cpus = None
@@ -967,6 +968,25 @@ class VMBaseClass(TestCase):
                     for service in ["systemd.mask=snapd.seeded.service",
                                     "systemd.mask=snapd.service"]])
 
+        # We set guest kernel panic=1 to trigger immediate rebooot, combined
+        # with the (xkvm) -no-reboot qemu parameter should prevent vmtests from
+        # wasting time in a soft-lockup loop. Add the params after the '---'
+        # separator to extend the parameters to the target system as well.
+        cmd.extend(["--no-reboot", "--append=panic=-1",
+                    "--append=softlockup_panic=1",
+                    "--append=hung_task_panic=1",
+                    "--append=nmi_watchdog=panic,1"])
+
+        # configure guest with crashdump to capture kernel failures for debug
+        if cls.crashdump:
+            # we need to install a kernel and modules so bump the memory by 2g
+            # for the ephemeral environment to hold it all
+            cls.mem = int(cls.mem) + 2048
+            logger.info(
+                'Enabling linux-crashdump during install, mem += 2048 = %s',
+                cls.mem)
+            cmd.extend(["--append=crashkernel=384M-5000M:192M"])
+
         # getting resolvconf configured is only fixed in bionic
         # the iscsi_auto handles resolvconf setup via call to
         # configure_networking in initramfs
@@ -1353,7 +1373,7 @@ class VMBaseClass(TestCase):
         target_disks.extend([output_disk])
 
         # create xkvm cmd
-        cmd = (["tools/xkvm", "-v", dowait] +
+        cmd = (["tools/xkvm", "-v", dowait, '--no-reboot'] +
                uefi_flags + netdevs +
                cls.mpath_diskargs(target_disks + extra_disks + nvme_disks) +
                ["--disk=file=%s,if=virtio,media=cdrom" % cls.td.seed_disk] +
@@ -2111,6 +2131,8 @@ def check_install_log(install_log, nrchars=200):
     # regexps expected in curtin output
     install_pass = INSTALL_PASS_MSG
     install_fail = "({})".format("|".join([
+                   'INFO:.* blocked for more than.*seconds.',
+                   'Kernel panic -',
                    'Installation failed',
                    'ImportError: No module named.*',
                    'Out of memory:',
diff --git a/tests/vmtests/image_sync.py b/tests/vmtests/image_sync.py
index 2559984..e460e02 100644
--- a/tests/vmtests/image_sync.py
+++ b/tests/vmtests/image_sync.py
@@ -34,7 +34,7 @@ def environ_get(key, default):
 
 IMAGE_SRC_URL = environ_get(
     'IMAGE_SRC_URL',
-    "http://maas.ubuntu.com/images/ephemeral-v3/daily/streams/v1/index.sjson";)
+    "http://images.maas.io/ephemeral-v3/daily/streams/v1/index.sjson";)
 IMAGE_DIR = environ_get("IMAGE_DIR", "/srv/images")
 
 KEYRING = environ_get(
diff --git a/tests/vmtests/test_fs_battery.py b/tests/vmtests/test_fs_battery.py
index ecd1729..bd44905 100644
--- a/tests/vmtests/test_fs_battery.py
+++ b/tests/vmtests/test_fs_battery.py
@@ -165,7 +165,8 @@ class TestFsBattery(VMBaseClass):
             "/etc /my/bind-ro-etc none bind,ro 0 0".split(),
         ]
         fstab_found = [
-            l.split() for l in self.load_collect_file("fstab").splitlines()]
+            line.split() for line in self.load_collect_file(
+                "fstab").splitlines()]
         self.assertEqual(expected, [e for e in expected if e in fstab_found])
 
     def test_mountinfo_has_mounts(self):
diff --git a/tests/vmtests/test_network.py b/tests/vmtests/test_network.py
index 601cad4..e6ea6e2 100644
--- a/tests/vmtests/test_network.py
+++ b/tests/vmtests/test_network.py
@@ -108,7 +108,9 @@ class TestNetworkBaseTestsAbs(VMBaseClass):
 
         eni_lines = eni.split('\n') + eni_cfg.split('\n')
         print("\n".join(eni_lines))
-        for line in [l for l in expected_eni.split('\n') if len(l) > 0]:
+        expected_eni_lines = [
+            line for line in expected_eni.split('\n') if len(line) > 0]
+        for line in expected_eni_lines:
             if line.startswith("#"):
                 continue
             if "hwaddress ether" in line:
@@ -489,4 +491,5 @@ class Centos70TestNetworkBasic(centos_relbase.centos70_xenial,
                                CentosTestNetworkBasicAbs):
     __test__ = True
 
+
 # vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_network_disabled.py b/tests/vmtests/test_network_disabled.py
new file mode 100644
index 0000000..b19ca64
--- /dev/null
+++ b/tests/vmtests/test_network_disabled.py
@@ -0,0 +1,72 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from .releases import base_vm_classes as relbase
+from .test_network import TestNetworkBaseTestsAbs
+
+from unittest import SkipTest
+
+import os
+
+
+class CurtinDisableNetworkRendering(TestNetworkBaseTestsAbs):
+    """ Test that curtin does not passthrough network config when
+    networking is disabled."""
+    conf_file = "examples/tests/network_disabled.yaml"
+
+    def test_cloudinit_network_not_created(self):
+        cc_passthrough = "cloud.cfg.d/50-curtin-networking.cfg"
+
+        pt_file = os.path.join(self.td.collect, 'etc_cloud',
+                               cc_passthrough)
+        self.assertFalse(os.path.exists(pt_file))
+
+    def test_cloudinit_network_passthrough(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+    def test_static_routes(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+    def test_ip_output(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+    def test_etc_resolvconf(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+
+class CurtinDisableCloudInitNetworking(TestNetworkBaseTestsAbs):
+    """ Test curtin can disable cloud-init networking in the target system """
+    conf_file = "examples/tests/network_config_disabled.yaml"
+
+    def test_etc_resolvconf(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+    def test_ip_output(self):
+        raise SkipTest('not available on %s' % self.__class__)
+
+
+class CurtinDisableCloudInitNetworkingVersion1(
+    CurtinDisableCloudInitNetworking
+):
+    """ Test curtin can disable cloud-init networking in the target system
+    with version key. """
+    conf_file = "examples/tests/network_config_disabled_with_version.yaml"
+
+
+class FocalCurtinDisableNetworkRendering(relbase.focal,
+                                         CurtinDisableNetworkRendering):
+    __test__ = True
+
+
+class FocalCurtinDisableCloudInitNetworkingVersion1(
+    relbase.focal,
+    CurtinDisableCloudInitNetworkingVersion1
+):
+    __test__ = True
+
+
+class FocalCurtinDisableCloudInitNetworking(relbase.focal,
+                                            CurtinDisableCloudInitNetworking):
+    __test__ = True
+
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tests/vmtests/test_old_apt_features.py b/tests/vmtests/test_old_apt_features.py
index 5a5415c..af479a9 100644
--- a/tests/vmtests/test_old_apt_features.py
+++ b/tests/vmtests/test_old_apt_features.py
@@ -10,7 +10,7 @@ import textwrap
 from . import VMBaseClass
 from .releases import base_vm_classes as relbase
 
-from curtin import util
+from curtin import distro
 from curtin.config import load_config
 
 
@@ -55,7 +55,7 @@ class TestOldAptAbs(VMBaseClass):
 
         exit 0
         """)]
-    arch = util.get_architecture()
+    arch = distro.get_architecture()
     target_arch = arch
     if target_arch in ['amd64', 'i386']:
         conf_file = "examples/tests/test_old_apt_features.yaml"
diff --git a/tests/vmtests/test_panic.py b/tests/vmtests/test_panic.py
new file mode 100644
index 0000000..fe4005e
--- /dev/null
+++ b/tests/vmtests/test_panic.py
@@ -0,0 +1,31 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from . import VMBaseClass, check_install_log
+from .releases import base_vm_classes as relbase
+
+
+class TestInstallPanic(VMBaseClass):
+    """ Test that a kernel panic exits the install mode immediately. """
+    expected_failure = True
+    collect_scripts = []
+    conf_file = "examples/tests/panic.yaml"
+    interactive = False
+
+    def test_install_log_finds_kernel_panic_error(self):
+        with open(self.install_log, 'rb') as lfh:
+            install_log = lfh.read().decode('utf-8', errors='replace')
+        errmsg, errors = check_install_log(install_log)
+        found_panic = False
+        print("errors: %s" % (len(errors)))
+        for idx, err in enumerate(errors):
+            print("%s:\n%s" % (idx, err))
+            if 'Kernel panic -' in err:
+                found_panic = True
+                break
+        self.assertTrue(found_panic)
+
+
+class FocalTestInstallPanic(relbase.focal, TestInstallPanic):
+    __test__ = True
+
+# vi: ts=4 expandtab syntax=python
diff --git a/tools/launch b/tools/launch
index db18c80..b49dd76 100755
--- a/tools/launch
+++ b/tools/launch
@@ -50,6 +50,7 @@ Usage: ${0##*/} [ options ] curtin install [args]
            --serial-log F  : log to F (default 'serial.log')
            --root-arg X pass 'X' through as the root= param when booting a
                         kernel.  default: $DEFAULT_ROOT_PARAM
+           --no-reboot  Pass '-no-reboot' through to QEMU
       -v | --verbose    be more verbose
            --no-install-deps  do not install insert '--install-deps'
                               on curtin command invocations
@@ -408,7 +409,7 @@ get_img_fmt() {
 
 main() {
     local short_opts="a:A:d:h:i:k:n:p:s:v"
-    long_opts="add:,append:,arch:,bios:,boot-image:,disk:,dowait,help,initrd:,kernel:,mem:,netdev:,no-dowait,no-proxy-config,power:,publish:,root-arg:,silent,serial-log:,smp:,uefi-nvram:,verbose,vnc:"
+    long_opts="add:,append:,arch:,bios:,boot-image:,disk:,dowait,help,initrd:,kernel:,mem:,netdev:,no-dowait,no-proxy-config,no-reboot,power:,publish:,root-arg:,silent,serial-log:,smp:,uefi-nvram:,verbose,vnc:"
     local getopt_out=""
     getopt_out=$(getopt --name "${0##*/}" \
         --options "${short_opts}" --long "${long_opts}" -- "$@") &&
@@ -461,6 +462,7 @@ main() {
                --no-dowait) pt[${#pt[@]}]="$cur"; dowait=false;;
                --no-install-deps) install_deps="";;
                --no-proxy-config) proxy_config=false;;
+               --no-reboot) pt[${#pt[@]}]="--no-reboot";;
                --power)
                 case "$next" in
                     off) pstate="poweroff";;
diff --git a/tools/xkvm b/tools/xkvm
index 4bb4343..30d206b 100755
--- a/tools/xkvm
+++ b/tools/xkvm
@@ -339,7 +339,7 @@ get_bios_opts() {
 
 main() {
     local short_opts="hd:n:v"
-    local long_opts="bios:,help,dowait,disk:,dry-run,kvm:,no-dowait,netdev:,uefi,uefi-nvram:,verbose"
+    local long_opts="bios:,help,dowait,disk:,dry-run,kvm:,no-dowait,no-reboot,netdev:,uefi,uefi-nvram:,verbose"
     local getopt_out=""
     getopt_out=$(getopt --name "${0##*/}" \
         --options "${short_opts}" --long "${long_opts}" -- "$@") &&
@@ -371,6 +371,7 @@ main() {
     #  We default to dowait=false if input and output are a terminal
     local dowait=""
     [ -t 0 -a -t 1 ] && dowait=false || dowait=true
+    local noreboot=false
     while [ $# -ne 0 ]; do
         cur=${1}; next=${2};
         case "$cur" in
@@ -384,6 +385,7 @@ main() {
             -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
             --dowait) dowait=true;;
             --no-dowait) dowait=false;;
+            --no-reboot) noreboot=true;;
             --bios) bios="$next"; shift;;
             --uefi) uefi=true;;
             --uefi-nvram) uefi=true; uefi_nvram="$next"; shift;;
@@ -683,6 +685,9 @@ main() {
     local rng_devices
     rng_devices=( -object "rng-random,filename=/dev/urandom,id=objrng0"
                   -device "$virtio_rng_device,rng=objrng0,id=rng0" )
+    if $noreboot; then
+        kvmcmd=( "${kvmcmd[@]}" -no-reboot )
+    fi
     cmd=( "${kvmcmd[@]}" "${archopts[@]}"
           "${bios_opts[@]}"
           "${bus_devices[@]}"
diff --git a/tox.ini b/tox.ini
index 6efc3f9..72d56d4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -48,7 +48,7 @@ commands = {envpython} -m flake8 {posargs:curtin}
 [testenv:py3-flake8]
 basepython = python3
 deps = {[testenv]deps}
-    flake8
+    flake8==3.8.1
 commands = {envpython} -m flake8 {posargs:curtin tests/}
 
 [testenv:py3-pyflakes]
@@ -144,6 +144,7 @@ deps = pycodestyle
 commands = {envpython} -m pyflakes {posargs:curtin/ tests/ tools/}
 deps = pyflakes
 
-[flake8]
-builtins = _
+[testenv:tip-flake8]
 exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build
+deps = flake8
+commands = {envpython} -m flake8 {posargs:curtin/ tests/ tools/}

Follow ups