← Back to team overview

curtin-dev team mailing list archive

[Merge] ~raharper/curtin:fix/replace-grub-shell-helper into curtin:master

 

Ryan Harper has proposed merging ~raharper/curtin:fix/replace-grub-shell-helper into curtin:master.

Commit message:
Replace grub-shell-helper with install_grub command

The install_grub command implemented in shell code inside
helpers/common lacked unittests.  We've had some recent and
ongoing changes in this space which need greater unittest
coverage.  Add a new command line option and update
curthooks to utilize it.

Also:
  - Move util.get_architecture into distro class

Requested reviews:
  curtin developers (curtin-dev)

For more details, see:
https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/382931
-- 
Your team curtin developers is requested to review the proposed merge of ~raharper/curtin:fix/replace-grub-shell-helper into curtin:master.
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/curthooks.py b/curtin/commands/curthooks.py
index 4afe00c..f705711 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,15 @@ 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)
+              instdevs, grubcfg.get('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)
 
@@ -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)
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
new file mode 100644
index 0000000..de36b0c
--- /dev/null
+++ b/curtin/commands/install_grub.py
@@ -0,0 +1,353 @@
+import os
+import re
+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(target_arch, uefi, rhel_ver=None):
+    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 while
+            # curtin uses grub2-efi-x64-modules to generate grubx64.efi.
+            # Either works just check that one of them is installed.
+            grub_name = "grub2-efi-x64"
+            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(distroinfo):
+    if distroinfo.family == distro.DISTROS.redhat:
+        return '/etc/default/grub'
+    # ubuntu writes to /etc/default/grub.d/50-curtin-settings.cfg
+    # to avoid tripping prompts on upgrade LP: #564853
+    return '/etc/default/grub.d/50-curtin-settings.cfg'
+
+
+def prepare_grub_dir(target, grub_cfg):
+    util.ensure_dir(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 + ' '
+
+    if wrap(preferred_sep) in cmdline:
+        lead, extra = cmdline.split(wrap(preferred_sep))
+    elif wrap(legacy_sep) in cmdline:
+        lead, extra = cmdline.split(wrap(legacy_sep))
+    else:
+        extra = ""
+        lead = cmdline
+
+    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, grubconf, new_args):
+    newline = 'GRUB_CMDLINE_LINUX_DEFAULT="%s"' % " ".join(new_args)
+    target_grubconf = target_path(target, grubconf)
+    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 += newline
+        updated_content += content[existing.end():]
+    else:
+        omode = 'a+'
+        updated_content = newline + '\n'
+
+    util.write_file(target_grubconf, updated_content, omode=omode)
+    LOG.debug('updated %s to set: %s', grubconf, newline)
+
+
+def write_grub_config(target, grubcfg, grub_conf, new_params):
+    replace_default = grubcfg.get('replace_linux_default', True)
+    if replace_default:
+        replace_grub_cmdline_linux_default(target, grub_conf, new_params)
+
+    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)
+    probe_content = [
+        '# Curtin disable grub os prober that might find other OS installs.',
+        'GRUB_DISABLE_OS_PROBER="%s"' % ('false' if probe_os else '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):
+    grub_install_cmd = 'grub-install'
+    if distroinfo.family == distro.DISTROS.debian:
+        # prefer grub-multi-install if present
+        if uefi and os.path.exists(GRUB_MULTI_INSTALL):
+            grub_install_cmd = GRUB_MULTI_INSTALL
+    elif distroinfo.family == distro.DISTROS.redhat:
+        grub_install_cmd = 'grub2-install'
+
+    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 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("install_grub: target=%s devices=%s", target, devices)
+    update_nvram = config.value_as_boolean(grubcfg.get('update_nvram', True))
+    distroinfo = distro.get_distroinfo(target=target)
+    target_arch = distro.get_architecture(target=target)
+    rhel_ver = (distro.rpm_get_dist_id(target)
+                if distroinfo.family == distro.DISTROS.redhat else None)
+
+    if target_arch == "s390x":
+        raise RuntimeError("Grub is not valid for s390x")
+    # verify_target_device_mount(target)
+    grub_name, grub_target = get_grub_package(target_arch, rhel_ver)
+    grub_conf = get_grub_config_file(distroinfo)
+    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)
+    print(grub_cmd)
+    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)
+
+    LOG.debug('Grub install cmds:\n%s', str(install_cmds + post_cmds))
+    with util.ChrootableTarget(target):
+        for cmd in install_cmds + post_cmds:
+            util.subp(cmd, 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/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..c503cc6 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -429,6 +429,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 +550,27 @@ 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):
+    out, _ = subp(['rpm', '-E', '%_arch'], capture=True, target=target)
+    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)
+
+
 # vi: ts=4 expandtab syntax=python
diff --git a/curtin/util.py b/curtin/util.py
index fa4f3f3..4c42e6b 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -790,12 +790,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/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..74ad7e1 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"]
diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
new file mode 100644
index 0000000..1c86539
--- /dev/null
+++ b/tests/unittests/test_commands_install_grub.py
@@ -0,0 +1,956 @@
+# This file is part of curtin. See LICENSE file for copyright and license info.
+
+from curtin import distro
+from curtin import util
+from curtin.commands import install_grub
+from .helpers import CiTestCase
+
+import mock
+import os
+
+
+class TestGetGrubPackage(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(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(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(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(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(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(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(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(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(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(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(target_arch, uefi, rhel_ver))
+
+    def test_invalid_rhel_version(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package('x86_64', uefi=False, rhel_ver='5')
+
+    def test_invalid_arch(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package(self.random_string(),
+                                          uefi=False, rhel_ver=None)
+
+    def test_invalid_arch_uefi(self):
+        with self.assertRaises(ValueError):
+            install_grub.get_grub_package(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))
+
+    @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))
+
+
+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(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(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_seporator(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_seporator(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_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 = "grub"
+        self.target_grubconf = os.path.join(self.target, self.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, self.grubconf, 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, self.grubconf, 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, self.grubconf, 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, self.grubconf, new_args)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+
+class TestWriteGrubConfig(CiTestCase):
+
+    def setUp(self):
+        super(TestWriteGrubConfig, self).setUp()
+        self.target = self.tmp_dir()
+        self.grubconf = "grub"
+        self.target_grubconf = os.path.join(self.target, self.grubconf)
+
+    def test_write_grub_config_defaults(self):
+        grubcfg = {}
+        new_params = ['foo=bar', 'wark=1']
+        expected = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"',
+             ("# 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)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    def test_write_grub_config_no_replace(self):
+        grubcfg = {'replace_linux_default': False}
+        new_params = ['foo=bar', 'wark=1']
+        expected = "\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)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    def test_write_grub_config_enable_probe(self):
+        grubcfg = {'probe_additional_os': True}
+        new_params = ['foo=bar', 'wark=1']
+        expected = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"',
+             ("# Curtin disable grub os prober that might find "
+              "other OS installs."),
+             'GRUB_DISABLE_OS_PROBER="false"',
+             '# Curtin configured GRUB_TERMINAL value',
+             'GRUB_TERMINAL="console"'])
+
+        install_grub.write_grub_config(
+            self.target, grubcfg, self.grubconf, new_params)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    def test_write_grub_config_invalid_probe(self):
+        grubcfg = {'probe_additional_os': '3'}
+        new_params = ['foo=bar', 'wark=1']
+        with self.assertRaises(ValueError):
+            install_grub.write_grub_config(
+                self.target, grubcfg, self.grubconf, new_params)
+
+    def test_write_grub_config_specify_terminal(self):
+        grubcfg = {'terminal': 'serial'}
+        new_params = ['foo=bar', 'wark=1']
+        expected = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"',
+             ("# 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)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    def test_write_grub_config_terminal_unmodified(self):
+        grubcfg = {'terminal': 'unmodified'}
+        new_params = ['foo=bar', 'wark=1']
+        expected = "\n".join([
+             'GRUB_CMDLINE_LINUX_DEFAULT="foo=bar wark=1"',
+             ("# 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)
+
+        with open(self.target_grubconf) as fh:
+            found = fh.read()
+        self.assertEqual(expected, found)
+
+    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))
+
+    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))
+
+    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))
+
+    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))
+
+
+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', 'm_get_grub_package')
+        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.distroinfo = distro.DistroInfo('myubuntu', 'mydebian')
+        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.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'
+        devices = ['foobar']
+        with self.assertRaises(RuntimeError):
+            install_grub.install_grub(devices, self.target, False, {})
+
+    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.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.assert_called_with('amd64', None)
+        self.m_get_grub_config_file.assert_called_with(self.distroinfo)
+        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.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'], capture=True),
+            mock.call(['/bin/false'], capture=True),
+        ])
+
+    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.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.assert_called_with('amd64', None)
+        self.m_get_grub_config_file.assert_called_with(self.distroinfo)
+        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.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'], capture=True),
+            mock.call(['/bin/false'], capture=True),
+        ])
+
+    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.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.assert_called_with('amd64', None)
+        self.m_get_grub_config_file.assert_called_with(self.distroinfo)
+        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.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'], capture=True),
+            mock.call(['/bin/false'], capture=True),
+        ])
+
+
+# vi: ts=4 expandtab syntax=python
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):

Follow ups