Thanks Chad, I'll update with some suggested changes.

Diff comments:

> 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
> @@ -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"

No, we exported ENV values, but the shell code used those to write to 
/etc/default/grub.d/50-curtin.cfg etc or more recently, replace values directly 
in /etc/default/grub to *avoid* including a .d file for curtin;

> -
> -    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"

As noted above, the environment variables were used as additional config values 
to the shell-code that we are now replacing.

> -
> -    # 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)

I'll fix the default to match docs.

> +    if uefi_bootable and update_nvram:
>          uefi_remove_old_loaders(grubcfg, target)
>  
>      LOG.debug("installing grub to %s [replace_default=%s]",

Probably, +1

> -              instdevs, replace_default)
> +              instdevs, grubcfg.get('replace_default'))
> +    install_grub(instdevs, target, uefi=uefi_bootable, grubcfg=grubcfg)

I think so

>  
> -    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)
>  
> diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
> new file mode 100644
> index 0000000..c1571b1
> --- /dev/null
> +++ b/curtin/commands/install_grub.py
> @@ -0,0 +1,366 @@
> +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 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 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(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 + ' '
> +
> +    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, 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 = grubcfg.get('replace_linux_default', True)
> +    if replace_default:
> +        replace_grub_cmdline_linux_default(target, 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)
> +    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 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)

Yes (the log from curthooks).

> +    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")

I'll drop.

> +    # verify_target_device_mount(target)
> +    grub_name, grub_target = get_grub_package(target_arch, uefi, 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, 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/distro.py b/curtin/distro.py
> index 1f62e7a..061665b 100644
> --- a/curtin/distro.py
> +++ b/curtin/distro.py
> @@ -549,4 +550,29 @@ 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 mounts
> +    with ChrootableTarget(target) as in_chroot:
> +        out, _ = in_chroot.subp(['rpm', '-E', '%_arch'], capture=True)

The comment is not clear enough; so I'll fix. We use ChrootableTarget when we 
require host mounts present in the target.  We prefer target=target as it's 
faster (no mount/unmount) however we don't know that we require 
ChrootableTarget until we have a failure due to missing mounts.

> +    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)
> +

I can add an exception.

> +
>  # vi: ts=4 expandtab syntax=python


-- 
https://code.launchpad.net/~raharper/curtin/+git/curtin/+merge/382931
Your team curtin developers is subscribed to branch curtin:master.

-- 
Mailing list: https://launchpad.net/~curtin-dev
Post to     : curtin-dev@lists.launchpad.net
Unsubscribe : https://launchpad.net/~curtin-dev
More help   : https://help.launchpad.net/ListHelp

Reply via email to