curtin-dev team mailing list archive
-
curtin-dev team
-
Mailing list archive
-
Message #00230
Re: [Merge] ~raharper/curtin:fix/replace-grub-shell-helper into curtin:master
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.
References