From d3a96b1c3caa5630a829b8d500cebf358ba84712 Mon Sep 17 00:00:00 2001 From: nikstur Date: Fri, 30 Dec 2022 16:22:06 +0100 Subject: [PATCH] lanzatool: intgeration test infrastrucutre + gc tests --- flake.nix | 17 +- rust/lanzatool/tests/common/mod.rs | 155 ++++++++++++++++++ .../lanzatool/tests/fixtures/uefi-keys/db.key | 52 ++++++ .../lanzatool/tests/fixtures/uefi-keys/db.pem | 29 ++++ rust/lanzatool/tests/gc.rs | 38 +++++ 5 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 rust/lanzatool/tests/common/mod.rs create mode 100644 rust/lanzatool/tests/fixtures/uefi-keys/db.key create mode 100644 rust/lanzatool/tests/fixtures/uefi-keys/db.pem create mode 100644 rust/lanzatool/tests/gc.rs diff --git a/flake.nix b/flake.nix index ff3621e..0aedd7c 100644 --- a/flake.nix +++ b/flake.nix @@ -50,14 +50,14 @@ { src , target ? null , doCheck ? true + , extraArgs ? { } }: let - cleanedSrc = craneLib.cleanCargoSource src; commonArgs = { - src = cleanedSrc; + inherit src; CARGO_BUILD_TARGET = target; inherit doCheck; - }; + } // extraArgs; cargoArtifacts = craneLib.buildDepsOnly commonArgs; in @@ -73,7 +73,7 @@ }; lanzabooteCrane = buildRustApp { - src = ./rust/lanzaboote; + src = craneLib.cleanCargoSource ./rust/lanzaboote; target = "x86_64-unknown-uefi"; doCheck = false; }; @@ -82,6 +82,13 @@ lanzatoolCrane = buildRustApp { src = ./rust/lanzatool; + extraArgs = { + TEST_SYSTEMD = pkgs.systemd; + checkInputs = with pkgs; [ + binutils-unwrapped + sbsigntool + ]; + }; }; lanzatool-unwrapped = lanzatoolCrane.package; @@ -134,6 +141,8 @@ lanzaboote lanzatool ]; + + TEST_SYSTEMD = pkgs.systemd; }; checks.x86_64-linux = { diff --git a/rust/lanzatool/tests/common/mod.rs b/rust/lanzatool/tests/common/mod.rs new file mode 100644 index 0000000..5df635f --- /dev/null +++ b/rust/lanzatool/tests/common/mod.rs @@ -0,0 +1,155 @@ +use std::ffi::OsStr; +use std::fs; +use std::io::Write; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; +use std::process::Output; + +use anyhow::{Context, Result}; +use assert_cmd::Command; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use serde_json::json; + +/// Create a mock generation link. +/// +/// Creates the generation link using the specified version inside a mock profiles directory +/// (mimicking /nix/var/nix/profiles). Returns the path to the generation link. +pub fn setup_generation_link( + tmpdir: &Path, + profiles_directory: &Path, + version: u64, +) -> Result { + let toplevel = setup_toplevel(tmpdir).context("Failed to setup toplevel")?; + + let bootspec = json!({ + "v1": { + "init": format!("init-v{}", version), + "initrd": toplevel.join("initrd"), + "kernel": toplevel.join("kernel"), + "kernelParams": [ + "amd_iommu=on", + "amd_iommu=pt", + "iommu=pt", + "kvm.ignore_msrs=1", + "kvm.report_ignored_msrs=0", + "udev.log_priority=3", + "systemd.unified_cgroup_hierarchy=1", + "loglevel=4" + ], + "label": "LanzaOS", + "toplevel": toplevel, + "system": "x86_64-linux", + "specialisation": {}, + "extensions": { + "lanzaboote": { "osRelease": toplevel.join("os-release") } + } + } + }); + + let generation_link_path = profiles_directory.join(format!("system-{}-link", version)); + fs::create_dir(&generation_link_path)?; + + let bootspec_path = generation_link_path.join("boot.json"); + let mut file = fs::File::create(bootspec_path)?; + file.write_all(&serde_json::to_vec(&bootspec)?)?; + + Ok(generation_link_path) +} + +/// Setup a mock toplevel inside a temporary directory. +/// +/// Accepts the temporary directory as a parameter so that the invoking function retains control of +/// it (and when it goes out of scope). +fn setup_toplevel(tmpdir: &Path) -> Result { + // Generate a random toplevel name so that multiple toplevel paths can live alongside each + // other in the same directory. + let toplevel = tmpdir.join(format!("toplevel-{}", random_string(8))); + fs::create_dir(&toplevel)?; + + let test_systemd = systemd_location_from_env()?; + let test_systemd_stub = format!("{test_systemd}/lib/systemd/boot/efi/linuxx64.efi.stub"); + + let initrd_path = toplevel.join("initrd"); + let kernel_path = toplevel.join("kernel"); + let systemd_path = toplevel.join("systemd"); + let os_release_path = toplevel.join("os-release"); + + // To simplify the test setup, we use the systemd stub for all PE binaries used by lanzatool. + // Lanzatool doesn't care whether its actually a kernel or initrd but only whether it can + // manipulate the PE binary with objcopy and/or sign it with sbsigntool. For testing lanzatool + // in isolation this should suffice. + fs::copy(&test_systemd_stub, initrd_path)?; + fs::copy(&test_systemd_stub, kernel_path)?; + symlink(&test_systemd, systemd_path)?; + setup_os_release(&os_release_path).context("Failed to setup os-release")?; + + Ok(toplevel) +} + +fn random_string(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +fn setup_os_release(path: &Path) -> Result<()> { + let content = r#" + ID=lanzaos + NAME=LanzaOS + PRETTY_NAME="LanzaOS 23.05 (Goat)" + VERSION="23.05 (Goat)" + "#; + + let mut file = fs::File::create(path)?; + file.write_all(content.as_bytes())?; + Ok(()) +} + +/// Call the `lanzaboote install` command. +pub fn lanzaboote_install( + config_limit: u64, + esp_mountpoint: &Path, + generation_links: impl IntoIterator>, +) -> Result { + // To simplify the test setup, we use the systemd stub here instead of the lanzaboote stub. See + // the comment in setup_toplevel for details. + let test_systemd = systemd_location_from_env()?; + let test_systemd_stub = format!("{test_systemd}/lib/systemd/boot/efi/linuxx64.efi.stub"); + + let mut cmd = Command::cargo_bin("lanzatool")?; + let output = cmd + .env("LANZABOOTE_STUB", test_systemd_stub) + .arg("install") + .arg("--public-key") + .arg("tests/fixtures/uefi-keys/db.pem") + .arg("--private-key") + .arg("tests/fixtures/uefi-keys/db.key") + .arg("--configuration-limit") + .arg(config_limit.to_string()) + .arg(esp_mountpoint) + .args(generation_links) + .output()?; + + // Print debugging output. + // This is a weird hack to make cargo test capture the output. + // See https://github.com/rust-lang/rust/issues/12309 + print!("{}", String::from_utf8(output.stdout.clone())?); + print!("{}", String::from_utf8(output.stderr.clone())?); + + // Also walk the entire ESP mountpoint and print each path for debugging + for entry in walkdir::WalkDir::new(esp_mountpoint) { + println!("{}", entry?.path().display()); + } + + Ok(output) +} + +/// Read location of systemd installation from an environment variable. +fn systemd_location_from_env() -> Result { + let error_msg = "TEST_SYSTEMD environment variable is not set. TEST_SYSTEMD has to point to a systemd installation. +On a system with Nix installed, you can set it with: export TEST_SYSTEMD=$(nix-build '' -A systemd)"; + std::env::var("TEST_SYSTEMD").context(error_msg) +} diff --git a/rust/lanzatool/tests/fixtures/uefi-keys/db.key b/rust/lanzatool/tests/fixtures/uefi-keys/db.key new file mode 100644 index 0000000..c2a5ab4 --- /dev/null +++ b/rust/lanzatool/tests/fixtures/uefi-keys/db.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +3cEKjh9NywA7eLWDxRxEf/vq3vdWeFY= +-----END PRIVATE KEY----- diff --git a/rust/lanzatool/tests/fixtures/uefi-keys/db.pem b/rust/lanzatool/tests/fixtures/uefi-keys/db.pem new file mode 100644 index 0000000..d383ba0 --- /dev/null +++ b/rust/lanzatool/tests/fixtures/uefi-keys/db.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+TCCAuGgAwIBAgIRAPmrvtqrYB4tl9V5HFxxLYowDQYJKoZIhvcNAQELBQAw +LjEVMBMGA1UEBhMMRGF0YWJhc2UgS2V5MRUwEwYDVQQDEwxEYXRhYmFzZSBLZXkw +HhcNMjIxMTIzMTI1NjU1WhcNMjcxMTIzMTI1NjU1WjAuMRUwEwYDVQQGEwxEYXRh +YmFzZSBLZXkxFTATBgNVBAMTDERhdGFiYXNlIEtleTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAK4tln0LCpqCkQeKqN1ddrxffWH+hNNwA3re40TRWTUp +3lowTMTOGukkfVpyqPpx8YyxEuxWL5r0xTxxfMpmGOz2tS5/KkzAKVIFnkf1LXWY +kMXS87ZYFP7qlt5T+Af9clzjgIVtjkhJAnF/gmJDzqkqsY+MWrRKympwSX8K957F +vkuNIreohHbWb6P4C/7sgtYBEWGALOO15nIoeGZT4cGnp40Mcz/6xaAnAfBMOG0q +A/bIxVllrkhIcc3BhShRhmVL8XZHXxcS11c4hUmzAtdNKrhl2l3A3AsdJbuVsN9q +AMMZ1+9BkQT/0q6z9coOib+8aA790WKFF+bbVDRgr6gaMK12MqGa3UkKFf1pCylH +xLNaPtxPFU11g1IK0z47OTxTJ93jH0x3g1uylc8zRs8Kbmry2PSwEQ3CEnoKDD4y +ito3ffhh3pxySUL7eHNFCPgVy6BUavHm9DKMfyB00iAGf5u9qSSGzfy4fcTXMrt2 +sbW/3YfOXLKtf0OyUTx5TZZavcqGU10BLGK1pG3uvdn12tf8KGUXoxx8C/0NWtQS +KVkdItqjvKNPx1UgpLHZ2NttfGC0TP4vpphpj309m+A7/ItS8Uou5dnsnfcE6Kie +y2jCd+mZHEvpC13F5O6eIrOTKiXu4h6etyh6qYX+wqFG8ew8yvJ2oVhw4PkZw01z +AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAbh22 +GBG/orDJrOClvt8qABL60ojnilLh4J5BwGDwBcXE/wETvXb+o3GsjTqqHdXYCQx0 +e3SpR/xQCfKzuaE4ysaszKFcNbhaezLmo/pzD2v0UA6Jr56re/jkYMtLQL4+LAUP +kCH66vc6qETQwtAzV3CYQ5TirwLLD6z3luPhm2FzyXTU9eydbsRp7FuSKq8mVXKP +cskrVJSjWD7fOsy/sZq3S+xmA3w1C8XRAgz+xlfIQkuu0mAa3jJ3JGjDyu1lAZ6N +oUc897npQ2tpYyGRQ/zOmKSE9oUlcEqXLPk2nDgenhG0oAdM/jJ54rnuR2x3zhBx +gyPEvWVzjzX8E//9Kc6VocisnI/tckibUPHlMJFHFBXiU8m6vO28cCRl4tRsH/K9 +jOnm+ztraiXI7mqBfTchR7Ga0EmnWFf38y3/YkrbVYbjK6o/+/T72Sje0MfCAvTl +KLOtvaQWQm36KP/2PH3DZ/k/v8+U76oeUBoI/DDH8E4rR1cUORvUYH8MZyPIKM4e +GN3+/zjr5gw70iLC/p8C+8rCzxwoernIMcFlL+SaeybnzXvH3VqO/XAFA/lxMu4V +K7Gzrtr5n+FiSTXytdIu18o6Av65XN5hzwQvVeWIx/E5Jry/JlSQgho8CnqLK8g5 +USdRgvRJN7xJM3szP93f7aNgIYSys2cCxtZxunU= +-----END CERTIFICATE----- diff --git a/rust/lanzatool/tests/gc.rs b/rust/lanzatool/tests/gc.rs new file mode 100644 index 0000000..1763b39 --- /dev/null +++ b/rust/lanzatool/tests/gc.rs @@ -0,0 +1,38 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use tempfile::tempdir; + +mod common; + +#[test] +fn keep_only_configured_number_of_generations() -> Result<()> { + let esp_mountpoint = tempdir()?; + let tmpdir = tempdir()?; + let profiles = tempdir()?; + let generation_links: Vec = [1, 2, 3] + .into_iter() + .map(|v| { + common::setup_generation_link(tmpdir.path(), profiles.path(), v) + .expect("Failed to setup generation link") + }) + .collect(); + let stub_count = || count_files(&esp_mountpoint.path().join("EFI/Linux")).unwrap(); + + // Install all 3 generations. + let output0 = common::lanzaboote_install(0, esp_mountpoint.path(), generation_links.clone())?; + assert!(output0.status.success()); + assert_eq!(stub_count(), 3); + + // Call `lanzatool install` again with a config limit of 2 and assert that one is deleted. + let output1 = common::lanzaboote_install(2, esp_mountpoint.path(), generation_links)?; + assert!(output1.status.success()); + assert_eq!(stub_count(), 2); + + Ok(()) +} + +fn count_files(path: &Path) -> Result { + Ok(fs::read_dir(path)?.count()) +}