From ae8e11cbfc6fe6d43e93335dcb840265f44501cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 4 Sep 2022 14:57:14 +0200 Subject: [PATCH] add kexec installer --- build-images.sh | 9 ++++ nix/kexec-installer-test.nix | 77 +++++++++++++++++++++++++++++++ nix/kexec-installer.nix | 89 ++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 nix/kexec-installer-test.nix create mode 100644 nix/kexec-installer.nix diff --git a/build-images.sh b/build-images.sh index a3aea03..3d5a13f 100755 --- a/build-images.sh +++ b/build-images.sh @@ -26,11 +26,20 @@ build_kexec_bundle() { echo "$tmp/kexec-bundle-$arch" } +build_kexec_installer() { + declare -r tag=$1 arch=$2 tmp=$3 + # run the test once we have kvm support in github actions + # ignore=$(nix-build ./nix/kexec-installer-test.nix -I "nixpkgs=https://github.com/NixOS/nixpkgs/archive/${tag}.tar.gz" --argstr system "$arch") + out=$(nix-build '' -o "$tmp/kexec-installer-$arch" -I nixos-config=./nix/kexec-installer.nix -I "nixpkgs=https://github.com/NixOS/nixpkgs/archive/${tag}.tar.gz" --argstr system "$arch" -A config.system.build.kexecTarball) + echo "$out/tarball/nixos-kexec-installer-$arch.tar.xz" +} + main() { declare -r tag=${1:-nixos-unstable} arch=${2:-x86_64-linux} tmp="$(mktemp -d)" trap 'rm -rf -- "$tmp"' EXIT readarray -t assets < <( + build_kexec_installer "$tag" "$arch" "$tmp" build_kexec_bundle "$tag" "$arch" "$tmp" build_netboot_image "$tag" "$arch" "$tmp" ) diff --git a/nix/kexec-installer-test.nix b/nix/kexec-installer-test.nix new file mode 100644 index 0000000..17a0eed --- /dev/null +++ b/nix/kexec-installer-test.nix @@ -0,0 +1,77 @@ +{ pkgs ? import {} }: + +let + makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); + makeTest' = args: makeTest args { + inherit pkgs; + inherit (pkgs) system; + }; + +in makeTest' { + name = "kexec-installer"; + meta = with pkgs.lib.maintainers; { + maintainers = [ mic92 ]; + }; + + nodes = { + node1 = { ... }: { + virtualisation.vlans = [ ]; + virtualisation.memorySize = 4 * 1024; + virtualisation.diskSize = 4 * 1024; + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + services.openssh.enable = true; + }; + + node2 = { pkgs, modulesPath, ... }: { + virtualisation.vlans = [ ]; + environment.systemPackages = [ pkgs.hello ]; + imports = [ + ./kexec-installer.nix + ]; + }; + }; + + testScript = { nodes, ... }: '' + # Test whether reboot via kexec works. + node1.wait_for_unit("multi-user.target") + node1.succeed('kexec --load /run/current-system/kernel --initrd /run/current-system/initrd --command-line "$(&2 &", check_return=False) + node1.connected = False + node1.connect() + node1.wait_for_unit("multi-user.target") + + # Check if the machine with netboot-minimal.nix profile boots up + node2.wait_for_unit("multi-user.target") + node2.shutdown() + + node1.wait_for_unit("sshd.service") + host_ed25519_before = node1.succeed("cat /etc/ssh/ssh_host_ed25519_key.pub") + + node1.succeed('ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -q -N ""') + root_ed25519_before = node1.succeed('tee /root/.ssh/authorized_keys < /root/.ssh/id_ed25519.pub') + # Kexec node1 to the toplevel of node2 via the kexec-boot script + node1.succeed('touch /run/foo') + node1.fail('hello') + node1.succeed('mkdir -p /root/kexec') + node1.succeed('mkdir -p /root/kexec') + node1.succeed('tar -xf ${nodes.node2.config.system.build.kexecTarball}/tarball/nixos-kexec-installer-${pkgs.system}.tar.xz -C /root/kexec') + node1.execute('/root/kexec/kexec-boot') + # wait for machine to kexec + node1.execute('sleep 9999', check_return=False) + node1.succeed('! test -e /run/foo') + node1.succeed('hello') + node1.succeed('[ "$(hostname)" = "node2" ]') + node1.wait_for_unit("sshd.service") + + host_ed25519_after = node1.succeed("cat /etc/ssh/ssh_host_ed25519_key.pub") + assert host_ed25519_before == host_ed25519_after, f"{host_ed25519_before} != {host_ed25519_after}" + + root_ed25519_after = node1.succeed("cat /root/.ssh/authorized_keys") + assert root_ed25519_before == root_ed25519_after, f"{root_ed25519_before} != {root_ed25519_after}" + + node1.shutdown() + ''; +} diff --git a/nix/kexec-installer.nix b/nix/kexec-installer.nix new file mode 100644 index 0000000..6115a77 --- /dev/null +++ b/nix/kexec-installer.nix @@ -0,0 +1,89 @@ +{ config, lib, modulesPath, pkgs, ... }: +{ + imports = [ + (modulesPath + "/installer/netboot/netboot-minimal.nix") + ]; + + # We are stateless, so just default to latest. + system.stateVersion = config.system.nixos.version; + + # This is a variant of the upstream kexecScript that also allows embedding + # a ssh key. + system.build.kexecBoot = lib.mkForce (pkgs.writeScript "kexec-boot" '' + #!/usr/bin/env bash + set -ex + shopt -s nullglob + SCRIPT_DIR=$( cd -- "$( dirname -- "''${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + INITRD_TMP=$(mktemp -d) + cd "$INITRD_TMP" + pwd + mkdir initrd initrd/ssh + pushd initrd + if [ -e /root/.ssh/authorized_keys ]; then + cat /root/.ssh/authorized_keys >> ssh/authorized_keys + fi + if [ -e /etc/ssh/authorized_keys.d/root ]; then + cat /etc/ssh/authorized_keys.d/root >> ssh/authorized_keys + fi + for p in /etc/ssh/ssh_host_*; do + cp -a "$p" ssh + done + find -type f | cpio -o -H newc | gzip -9 > ../extra.gz + popd + cat "''${SCRIPT_DIR}/initrd.gz" extra.gz > final.gz + + "$SCRIPT_DIR/kexec" --load "''${SCRIPT_DIR}/bzImage" \ + --initrd=final.gz \ + --command-line "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}" + + # kexec will map the new kernel in memory so we can remove the kernel at this point + rm -r "$INITRD_TMP" + + # Disconnect our background kexec from the terminal + if [[ -e /dev/kmsg ]]; then + # this makes logging visible in `dmesg`, or the system consol or tools like journald + exec > /dev/kmsg 2>&1 + else + exec > /dev/null 2>&1 + fi + # We will kexec in background so we can cleanly finish the script before the hosts go down. + # This makes integration with tools like terraform easier. + nohup bash -c "sleep 6 && '$SCRIPT_DIR/kexec' -e" & + ''); + + system.build.kexecTarball = pkgs.callPackage (pkgs.path + "/nixos/lib/make-system-tarball.nix") { + fileName = "nixos-kexec-installer-${pkgs.stdenv.hostPlatform.system}"; + contents = [ + { + target = "/initrd.gz"; + source = "${config.system.build.netbootRamdisk}/initrd"; + } + { + target = "/bzImage"; + source = "${config.system.build.kernel}/${config.system.boot.loader.kernelFile}"; + } + { + target = "/kexec-boot"; + source = config.system.build.kexecBoot; + } + { + target = "/kexec"; + source = "${pkgs.pkgsStatic.kexec-tools}/bin/kexec"; + } + ]; + }; + + documentation.enable = false; + # Not really needed. Saves a few bytes and the only service we are running is sshd, which we want to be reachable. + networking.firewall.enable = false; + + # Restore ssh host and user keys if they are available. + # This avoids warnings of unknown ssh keys. + boot.initrd.postMountCommands = '' + mkdir -p /mnt-root/etc/ssh /mnt-root/root/.ssh + if [[ -f /ssh/authorized_keys ]]; then + cp ssh/authorized_keys /mnt-root/root/.ssh/ + fi + cp ssh/ssh_host_* /mnt-root/etc/ssh + ''; +}