From 1b92018360c99bc05f412f37d55ecd667fd391f9 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Mon, 2 Dec 2024 16:28:25 -0700 Subject: [PATCH] python: setup: arch: Switch to systemd-boot if necessary Signed-off-by: Nathan Chancellor --- python/setup/arch.py | 134 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/python/setup/arch.py b/python/setup/arch.py index 02626200..44aa810a 100755 --- a/python/setup/arch.py +++ b/python/setup/arch.py @@ -3,6 +3,7 @@ # Copyright (C) 2022-2023 Nathan Chancellor from argparse import ArgumentParser +from collections import UserDict import getpass import os from pathlib import Path @@ -29,6 +30,27 @@ UCODE_VENDOR = 'intel' +class CmdlineOptions(UserDict): + + def __init__(self, initial_argument): + if isinstance(initial_argument, str): + super().__init__() + + for item in initial_argument.split(' '): + if item := item.strip(): + if '=' in item: + key, value = item.split('=', maxsplit=1) + else: + key, value = item, None + self.data[key] = value + else: + super().__init__(initial_argument) + + def __str__(self): + return ' '.join( + sorted(f"{key}={value}" if value else key for key, value in self.data.items())) + + def add_mods_to_mkinitcpio(modules): mkinitcpio_conf, conf_text = lib.utils.path_and_text('/etc/mkinitcpio.conf') @@ -101,8 +123,10 @@ def configure_systemd_boot(init=True, conf='linux.conf'): systemd_boot_update_hook.write_text(systemd_boot_update_hook_txt, encoding='utf-8') systemd_boot_update_hook.chmod(0o644) - # If we already set up the configuration, no need to go through all this - # again, unless we are not doing the initial configuration + # If we already set up the configuration (either via this function or + # switch_to_systemd_boot(), depending on what setup was installed prior to + # running this setup), no need to go through all this again, unless we are + # not doing the initial configuration if (linux_conf := (boot_entries := Path('/boot/loader/entries')) / conf).exists() and init: return @@ -119,23 +143,13 @@ def configure_systemd_boot(init=True, conf='linux.conf'): linux_conf_text = linux_conf.read_text(encoding='utf-8') if not (match := re.search('^options (.*)$', linux_conf_text, flags=re.M)): raise RuntimeError(f"Could not find 'options' line in {linux_conf}?") - current_options_str = match.groups()[0] - current_options = {opt for elem in current_options_str.split(' ') if (opt := elem.strip())} - new_options = current_options.copy() - - # Add 'console=' if necessary (when connected by serial console in a - # virtual machine) - if lib.setup.is_virtual_machine() and 'DISPLAY' not in os.environ: - new_options.add('console=ttyS0,115200n8') - - # Enable the performance governor - new_options.add('cpufreq.default_governor=performance') + current_options = CmdlineOptions(match.groups()[0]) - # Mitigate SMT RSB vulnerability - new_options.add('kvm.mitigate_smt_rsb=1') + new_options = current_options.copy() + new_options.update(get_cmdline_additions()) if current_options != new_options: - new_text = linux_conf_text.replace(current_options_str, ' '.join(sorted(new_options))) + new_text = linux_conf_text.replace(str(current_options), str(new_options)) linux_conf.write_text(new_text, encoding='utf-8') # Ensure that the new configuration is the default on the machine. @@ -189,6 +203,20 @@ def fix_fstab(): subprocess.run(['dos2unix', '/etc/fstab'], check=True) +def get_cmdline_additions(): + options = CmdlineOptions({ + # Enable the performance governor + 'cpufreq.default_governor': 'performance', + # Mitigate SMT RSB vulnerability + 'kvm.mitigate_smt_rsb': '1', + }) + # Add 'console=' if necessary (when connected by serial console in a + # virtual machine) + if lib.setup.is_virtual_machine() and 'DISPLAY' not in os.environ: + options['console'] = 'ttyS0,115200n8' + return options + + def pacman_install(subargs): lib.setup.pacman(['-S', '--noconfirm', *subargs]) @@ -465,6 +493,79 @@ def setup_user(username, userpass): lib.setup.setup_ssh_authorized_keys(username) +def switch_to_systemd_boot(dryrun=False): + # If we are not booted in UEFI mode, we cannot switch to systemd-boot + if not Path('/sys/firmware/efi').exists(): + return + + # If systemd-boot is already installed, we are good to go + if lib.setup.using_systemd_boot(): + return + + # Install systemd-boot to ESP + if dryrun: + print('$ bootctl install') + else: + subprocess.run(['bootctl', 'install'], check=True) + + # Create initial loader.conf + loader_conf_txt = 'default linux.conf\ntimeout 3\n' + if dryrun: + print('$ mkdir /boot/loader') + print(f"$ echo '{loader_conf_txt}' > /boot/loader/loader.conf") + else: + Path('/boot/loader').mkdir(exist_ok=True) + Path('/boot/loader/loader.conf').write_text(loader_conf_txt, encoding='utf-8') + + # Get partition UUID and filesystem type of / + partuuid, fstype = subprocess.run(['findmnt', '-n', '-o', 'PARTUUID,FSTYPE', '/'], + capture_output=True, + check=True, + text=True).stdout.strip().split(' ') + + # Default cmdline options + cmdline_options = CmdlineOptions({ + 'root': f"PARTUUID={partuuid}", + 'rootfstype': fstype, + 'rw': None, + }) + cmdline_options.update(get_cmdline_additions()) + # Copy over any cmdline options that we added in grub, as those might be + # necessary for the machine to work properly. + if (grub_default := Path('/etc/default/grub')).exists(): + grub_default_txt = grub_default.read_text(encoding='utf-8') + if match := re.search('^GRUB_CMDLINE_LINUX_DEFAULT="(.*)"$', grub_default_txt, flags=re.M): + default_skip = ( + 'loglevel=', # I do not mind seeing logs + 'quiet', # I do not mind the boot up noise + ) + filtered_str = ' '.join(item for item in match.groups()[0].split(' ') + if not item.startswith(default_skip)) + cmdline_options.update(CmdlineOptions(filtered_str)) + if match := re.search('^GRUB_CMDLINE_LINUX="(.*)"$', grub_default_txt, flags=re.M): + cmdline_options.update(CmdlineOptions(match.groups()[0])) + + # Create initial linux.conf + linux_conf_txt = ( + 'title Arch Linux (linux)\n' + 'linux /vmlinuz-linux\n' + f"initrd /{UCODE_VENDOR}-ucode.img\n" if UCODE_VENDOR else '', + 'initrd /initramfs-linux.img\n' + f"options {' '.join(sorted(cmdline_options))}\n", + ) + if dryrun: + print('$ mkdir /boot/loader/entries') + print(f"$ echo '{linux_conf_txt}' > /boot/loader/entries/linux.conf") + else: + Path('/boot/loader/entries').mkdir(exist_ok=True) + Path('/boot/loader/entries/linux.conf').write_text(linux_conf_txt, encoding='utf-8') + + # Remove grub + lib.setup.remove_if_installed('grub') + if (boot_grub := Path('/boot/grub')).exists(): + shutil.rmtree(boot_grub) + + def uncomment_pacman_option(conf, option, old_value=None, new_value=None): if old_value and new_value: return re.sub(f"^#{option} = {old_value}", f"{option} = {new_value}", conf, flags=re.M) @@ -502,6 +603,7 @@ def vmware_adjustments(): password = getpass.getpass(prompt='Password for Arch Linux user account: ') prechecks() + switch_to_systemd_boot() configure_systemd_boot() pacman_settings() pacman_key_setup()