diff --git a/.ci/check.sh b/.ci/check.sh index 162c138..7f656a5 100755 --- a/.ci/check.sh +++ b/.ci/check.sh @@ -7,6 +7,6 @@ set -euxo pipefail cargo fmt -- --check cargo clippy --all -- -D warnings -cargo test --all +cargo test -- --skip integration codespell reuse lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5363b57..cdf8424 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: pacman -Sy --needed --noconfirm archlinux-keyring pacman -Syu --needed --noconfirm clang dbus git mold pkgconf rust - name: Run cargo test - run: cargo test --release + run: cargo test --release -- --skip integration deny: runs-on: ubuntu-latest container: diff --git a/Cargo.lock b/Cargo.lock index ec9596f..355d796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,12 +37,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "assert_cmd" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -159,7 +180,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -203,7 +224,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -275,6 +296,17 @@ dependencies = [ "log", ] +[[package]] +name = "bstr" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -289,9 +321,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "camino" @@ -328,15 +360,18 @@ dependencies = [ name = "caterpillar" version = "0.1.0" dependencies = [ + "assert_cmd", "async-std", "config", "dbus-launch", "dbus-udisks2", + "fslock", "futures", "once_cell", "regex", "rstest", "semver", + "serial_test", "strum", "strum_macros", "temp-dir", @@ -347,6 +382,7 @@ dependencies = [ "tokio", "tracing", "version-compare", + "which", "zbus", "zvariant", ] @@ -428,6 +464,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dbus" version = "0.9.7" @@ -473,6 +522,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -489,6 +544,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "enumflags2" version = "0.7.7" @@ -507,7 +574,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -558,6 +625,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.28" @@ -629,7 +706,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -740,6 +817,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "indexmap" version = "2.0.0" @@ -770,6 +856,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -840,9 +935,19 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" @@ -997,6 +1102,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -1034,7 +1162,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -1088,6 +1216,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +dependencies = [ + "anstyle", + "difflib", + "itertools", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1262,14 +1418,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.11" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys 0.4.7", "windows-sys", ] @@ -1285,6 +1441,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.18" @@ -1311,14 +1473,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" dependencies = [ "itoa", "ryu", @@ -1333,7 +1495,33 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", +] + +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "fslock", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", ] [[package]] @@ -1386,6 +1574,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + [[package]] name = "socket2" version = "0.4.9" @@ -1447,9 +1641,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -1485,10 +1679,16 @@ dependencies = [ "cfg-if", "fastrand 2.0.0", "redox_syscall", - "rustix 0.38.11", + "rustix 0.38.13", "windows-sys", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "testdir" version = "0.7.3" @@ -1526,7 +1726,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -1567,7 +1767,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -1587,9 +1787,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", @@ -1616,7 +1816,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", ] [[package]] @@ -1674,6 +1874,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -1707,7 +1916,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", "wasm-bindgen-shared", ] @@ -1741,7 +1950,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.32", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1762,6 +1971,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.13", +] + [[package]] name = "whoami" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 3b3393b..3b4f6ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,14 @@ zbus = {version = "3.12.0", default-features = false, features = ["tokio"]} zvariant = "3.12.0" [dev-dependencies] +assert_cmd = "2.0.12" dbus-launch = "0.2.0" +fslock = "0.2.1" rstest = "0.17.0" +serial_test = { version = "2.0.0", features = ["file_locks"] } temp-dir = "0.1.11" testdir = "0.7.3" testresult = "0.3.0" tmpdir = "1.0.0" tracing = "0.1.37" +which = "4.4.2" diff --git a/README.md b/README.md index 36992b6..98b8c75 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,30 @@ cargo build --frozen --release --all-features ## Tests -Tests can be executed using +Unit tests can be executed using ```shell -cargo test --all +cargo test -- --skip integration ``` +**NOTE**: The integration test setup requires quite some space (ca. 10 - 20 GiB) and can only be run serially (which takes quite long). + +```shell +cargo test integration +``` + +The integration tests require the following tools to be available on the test system: + +- *guestmount* ([libguestfs](https://libguestfs.org/)) +- *guestunmount* ([libguestfs](https://libguestfs.org/)) +- *mkosi* ([mkosi](https://github.com/systemd/mkosi)) +- *openssl* ([openssl](https://www.openssl.org)) +- *pacman* ([pacman](https://archlinux.org/pacman/)) +- *qemu-img* ([QEMU](https://archlinux.org/pacman/)) +- *qemu-system-x86_64* ([QEMU](https://archlinux.org/pacman/)) +- *rauc* ([RAUC](https://rauc.io)) +- *sleep* ([coreutils](https://www.gnu.org/software/coreutils/)) + ## License All code contributions are dual-licensed under the terms of the [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) and [MIT](https://spdx.org/licenses/MIT.html). diff --git a/tests/common/cmd.rs b/tests/common/cmd.rs new file mode 100644 index 0000000..212bf14 --- /dev/null +++ b/tests/common/cmd.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use rstest::fixture; +use std::fmt::Display; +use std::fmt::Formatter; +use std::path::Path; +use std::path::PathBuf; +use which::which; + +/// A command available in PATH +pub struct Cmd { + path: PathBuf, +} + +impl Cmd { + pub fn new(name: String) -> Result { + Ok(Self { + path: which(&name)?, + }) + } + + pub fn path(&self) -> &Path { + &self.path + } +} + +impl Display for Cmd { + fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.path().display()) + } +} + +#[fixture] +/// The guestmount command +pub fn cmd_guestmount() -> Result { + Cmd::new("guestmount".to_string()) +} + +#[fixture] +/// The guestunmount command +pub fn cmd_guestunmount() -> Result { + Cmd::new("guestunmount".to_string()) +} + +#[fixture] +/// The mkosi command +pub fn cmd_mkosi() -> Result { + Cmd::new("mkosi".to_string()) +} + +#[fixture] +/// The openssl command +pub fn cmd_openssl() -> Result { + Cmd::new("openssl".to_string()) +} + +#[fixture] +/// The qemu-img command +pub fn cmd_qemu_img() -> Result { + Cmd::new("qemu-img".to_string()) +} + +#[fixture] +/// The qemu-system-x86_64 command +pub fn cmd_qemu_system() -> Result { + Cmd::new("qemu-system-x86_64".to_string()) +} + +#[fixture] +/// The rauc command +pub fn cmd_rauc() -> Result { + Cmd::new("rauc".to_string()) +} + +#[fixture] +/// The sleep command +pub fn cmd_sleep() -> Result { + Cmd::new("sleep".to_string()) +} diff --git a/tests/common/error.rs b/tests/common/error.rs new file mode 100644 index 0000000..d9a824e --- /dev/null +++ b/tests/common/error.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use assert_cmd::assert::AssertError; + +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum TestError { + #[error("Running an external command failed: {0}")] + ExternalCommand(AssertError), + #[error("Something is missing: {0}")] + Missing(String), + #[error("An external command is missing: {0}")] + CommandMissing(which::Error), + #[error("An I/O error occurred: {0}")] + IO(std::io::Error), +} + +impl From for TestError { + fn from(value: AssertError) -> Self { + TestError::ExternalCommand(value) + } +} + +impl From for TestError { + fn from(value: which::Error) -> Self { + TestError::CommandMissing(value) + } +} + +impl From for TestError { + fn from(value: std::io::Error) -> Self { + TestError::IO(value) + } +} diff --git a/tests/common/image.rs b/tests/common/image.rs new file mode 100644 index 0000000..fb585da --- /dev/null +++ b/tests/common/image.rs @@ -0,0 +1,550 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use std::fs::copy; +use std::fs::create_dir_all; +use std::fs::remove_dir; +use std::path::Path; +use std::path::PathBuf; + +use assert_cmd::Command; +use rstest::fixture; +use strum_macros::EnumString; +use testdir::testdir; +use testresult::TestResult; + +use super::cmd::cmd_mkosi; +use super::cmd_qemu_img; +use super::cmd_qemu_system; +use super::input_path_ovmf_code; +use super::input_path_ovmf_vars; +use super::mkosi_dir_ab_image; +use super::mkosi_dir_base_image; +use super::mkosi_dir_bundle_image; +use super::mkosi_dir_single_image; +use super::output_dir; +use super::output_dir_ab_override; +use super::output_path_ab_image; +use super::output_path_base_image; +use super::output_path_ovmf_vars; +use super::output_path_single_image; +use super::public_key_infrastructure; +use super::remove_files; +use super::Cmd; +use super::RaucBundle; +use super::TestError; + +#[derive(Clone, Copy, Debug, strum::Display, EnumString, PartialEq)] +#[non_exhaustive] +/// A type of a disk image +pub enum DiskType { + #[strum(to_string = "empty")] + Empty, + #[strum(to_string = "multiple")] + Multiple, + #[strum(to_string = "single")] + Single, +} + +#[derive(Clone, Copy, Debug, strum::Display, EnumString, PartialEq)] +#[non_exhaustive] +/// A filesystem of a disk image +pub enum FileSystem { + #[strum(to_string = "btrfs")] + Btrfs, + #[strum(to_string = "ext4")] + Ext4, + #[strum(to_string = "vfat")] + Vfat, +} + +#[derive(Clone, Debug)] +/// An update image, containing zero or more updates +pub struct UpdateImage { + path: PathBuf, + filesystem: FileSystem, + disk_type: DiskType, +} + +impl UpdateImage { + pub fn new(path: PathBuf, filesystem: FileSystem, size: DiskType) -> Self { + Self { + path, + filesystem, + disk_type: size, + } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn filesystem(&self) -> &FileSystem { + &self.filesystem + } + + pub fn disk_type(&self) -> &DiskType { + &self.disk_type + } + + /// prepare bundle disk for a named test by resetting the disk and copying update bundles into it + pub fn prepare_test( + &self, + qemu_img: &Cmd, + guestmount: &Cmd, + guestunmount: &Cmd, + update_bundles: Vec<(RaucBundle, PathBuf)>, + ) -> TestResult { + reset_image(&qemu_img, self.path())?; + + if !update_bundles.is_empty() { + let mount_dir = testdir!().join("bundle_disk_write_mount"); + create_dir_all(&mount_dir)?; + + // mount the disk + Command::new(&guestmount.path()) + .arg("-a") + .arg(format!("{}", self.path().display())) + .arg("-m") + .arg("/dev/sda1") + .arg("--rw") + .arg(format!("{}", &mount_dir.display())) + .assert() + .try_success()?; + + // copy update bundles to disk + for (bundle, target) in update_bundles { + if let Some(target_parent) = target.parent() { + create_dir_all(&mount_dir.join(target_parent))?; + } + copy(&bundle.path(), &mount_dir.join(target))?; + } + + // unmount persistence partition + Command::new(&guestunmount.path()) + .arg(format!("{}", &mount_dir.display())) + .assert() + .try_success()?; + + remove_dir(mount_dir)?; + } + + Ok(()) + } +} + +/// A disk image to test with +#[derive(Debug)] +pub struct TestImage { + path: PathBuf, + efi: PathBuf, + rootfs: PathBuf, +} + +impl TestImage { + pub fn new(path: PathBuf, efi: PathBuf, rootfs: PathBuf) -> Self { + TestImage { path, efi, rootfs } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn efi(&self) -> &Path { + &self.efi + } + + pub fn rootfs(&self) -> &Path { + &self.rootfs + } + + /// Prepare an image for a test by resetting it, deploying a payload and creating another snapshot + pub fn prepare_for_test( + &self, + qemu_img: &Cmd, + guestmount: &Cmd, + guestunmount: &Cmd, + ) -> TestResult { + reset_image(&qemu_img, self.path())?; + + let payload = PathBuf::from(env!("CARGO_BIN_EXE_caterpillar")); + let mount_dir = testdir!().join("write_mount"); + create_dir_all(&mount_dir)?; + + // mount the first partition (persistence partition) + Command::new(&guestmount.path()) + .arg("-a") + .arg(format!("{}", self.path().display())) + .arg("-m") + .arg("/dev/sda1") + .arg("--rw") + .arg(format!("{}", &mount_dir.display())) + .assert() + .try_success()?; + + // copy payload to persistence partition + copy(&payload, &mount_dir.join(payload.file_name().unwrap()))?; + + // unmount persistence partition + Command::new(&guestunmount.path()) + .arg(format!("{}", &mount_dir.display())) + .assert() + .try_success()?; + + remove_dir(mount_dir)?; + Ok(()) + } +} + +/// Convert a virtual machine image from raw to qcow2 +pub fn convert_image(qemu_img: &Cmd, input: &str, output: &str) -> Result<(), TestError> { + Command::new(qemu_img.path()) + .arg("convert") + .arg("-c") + .arg("-f") + .arg("raw") + .arg("-O") + .arg("qcow2") + .arg("--") + .arg(input) + .arg(output) + .assert() + .try_success()?; + Ok(()) +} + +/// Create a snapshot of a virtual machine image +/// +/// If `name` is `None` the snapshot is called `"base"`. +pub fn snapshot_image(qemu_img: &Cmd, path: &Path, name: Option<&str>) -> Result<(), TestError> { + Command::new(qemu_img.path()) + .arg("snapshot") + .arg("-c") + .arg(name.unwrap_or("base")) + .arg(format!("{}", path.display())) + .assert() + .try_success()?; + Ok(()) +} + +/// Reset a virtual machine image to its "base" snapshot +pub fn reset_image(qemu_img: &Cmd, path: &Path) -> Result<(), TestError> { + Command::new(qemu_img.path()) + .arg("snapshot") + .arg("-a") + .arg("base") + .arg(format!("{}", path.display())) + .assert() + .try_success()?; + Ok(()) +} + +#[fixture] +/// A basic virtual machine image (tar file) +/// +/// This tar file is used to create `single_image` and `ab_image` from. +pub fn base_image( + cmd_mkosi: Result, + mkosi_dir_base_image: PathBuf, + output_path_base_image: PathBuf, + output_dir: PathBuf, +) -> Result { + if !output_path_base_image.exists() { + println!( + "{} does not exist yet. Building...", + output_path_base_image.display() + ); + + Command::new(format!("{}", cmd_mkosi?)) + .arg("--output-dir") + .arg(format!("{}", &output_dir.display())) + .arg("--force") + .arg("build") + .current_dir(mkosi_dir_base_image) + .assert() + .try_success()?; + + // remove unnecessary files to save space + remove_files(&[ + &format!("{}.efi", &output_path_base_image.display()), + &format!("{}.initrd", &output_path_base_image.display()), + &format!("{}.vmlinuz", &output_path_base_image.display()), + &format!("{}-initrd", &output_path_base_image.display()), + &format!("{}-initrd.cpio.zst", &output_path_base_image.display()), + ])?; + } + + Ok(output_path_base_image) +} + +#[fixture] +/// An intermediate single system virtual machine image +/// +/// This image's main purpose is to be used in the context of generating EFI boot loader entries for `ab_image`. +pub fn single_image( + cmd_mkosi: Result, + cmd_qemu_img: Result, + base_image: Result, + mkosi_dir_single_image: PathBuf, + output_path_single_image: PathBuf, + output_dir: PathBuf, +) -> Result { + let output_path = PathBuf::from(format!("{}.qcow2", output_path_single_image.display())); + + if !output_path.exists() { + println!( + "{} does not exist yet. Building...", + output_path_single_image.display() + ); + + let qemu_img = cmd_qemu_img?; + + Command::new(cmd_mkosi?.path()) + .arg("--base-tree") + .arg(format!("{}", &base_image?.display())) + .arg("--output-dir") + .arg(format!("{}", &output_dir.display())) + .arg("--force") + .arg("build") + .current_dir(mkosi_dir_single_image) + .assert() + .try_success()?; + + convert_image( + &qemu_img, + &format!("{}.raw", &output_path_single_image.display()), + &format!("{}", &output_path.display()), + )?; + + snapshot_image(&qemu_img, &output_path, None)?; + + // remove unnecessary files to save space + remove_files(&[ + &format!("{}", &output_path_single_image.display()), + &format!("{}.efi", &output_path_single_image.display()), + &format!("{}.initrd", &output_path_single_image.display()), + &format!("{}.raw", &output_path_single_image.display()), + &format!("{}.vmlinuz", &output_path_single_image.display()), + &format!("{}-initrd", &output_path_single_image.display()), + &format!("{}-initrd.cpio.zst", &output_path_single_image.display()), + ])?; + } + + Ok(output_path) +} + +#[fixture] +/// An A/B test image +/// +/// The test image is a .qcow2 virtual machine image with five partitions: +/// - the first serves as writable location for persistence +/// - the second and third partition are ESPs for two different target root filesystems +/// - the fourth and fifth partition are root filesystems that are tied to their respective ESPs +pub fn ab_image( + cmd_mkosi: Result, + cmd_qemu_img: Result, + public_key_infrastructure: Result<(PathBuf, PathBuf), TestError>, + base_image: Result, + output_dir_ab_override: PathBuf, + mkosi_dir_ab_image: PathBuf, + output_path_ab_image: PathBuf, + output_dir: PathBuf, +) -> Result { + let output = TestImage::new( + PathBuf::from(format!("{}.qcow2", output_path_ab_image.display())), + PathBuf::from(format!("{}.esp_a.raw", output_path_ab_image.display())), + PathBuf::from(format!( + "{}.root-x86-64_a.raw", + output_path_ab_image.display() + )), + ); + + if !output.path().exists() { + println!( + "{} does not exist yet. Building...", + output.path().display() + ); + + if let Err(error) = &public_key_infrastructure { + eprintln!("error creating PKI: {:?}", error); + assert!(false); + } + + let qemu_img = cmd_qemu_img?; + + Command::new(cmd_mkosi?.path()) + .arg("--base-tree") + .arg(format!("{}", &base_image?.display(),)) + .arg("--extra-tree") + .arg(format!("{}", &output_dir_ab_override.display())) + .arg("--output-dir") + .arg(format!("{}", &output_dir.display())) + .arg("--force") + .arg("build") + .current_dir(&mkosi_dir_ab_image) + .assert() + .try_success()?; + + convert_image( + &qemu_img, + &format!("{}.raw", &output_path_ab_image.display()), + &format!("{}", &output.path().display()), + )?; + snapshot_image(&qemu_img, &output.path(), None)?; + + // remove unnecessary files to save space + remove_files(&[ + &format!("{}", &output_path_ab_image.display()), + &format!("{}.efi", &output_path_ab_image.display()), + &format!("{}.raw", &output_path_ab_image.display()), + &format!("{}.esp_b.raw", &output_path_ab_image.display()), + &format!("{}.initrd", &output_path_ab_image.display()), + &format!("{}.linux-generic.raw", &output_path_ab_image.display()), + &format!("{}.root-x86-64_b.raw", &output_path_ab_image.display()), + &format!("{}.vmlinuz", &output_path_ab_image.display()), + &format!("{}-initrd", &output_path_ab_image.display()), + &format!("{}-initrd.cpio.zst", &output_path_ab_image.display()), + ])?; + } + + Ok(output) +} + +#[fixture] +/// A fixture for providing OVMF vars prepared for a test setup +/// +/// The OVMF vars contain EFI bootloader entries for EFI partitions of the test setup. +pub fn ovmf_vars( + cmd_qemu_system: Result, + input_path_ovmf_code: Result, + input_path_ovmf_vars: Result, + output_path_ovmf_vars: PathBuf, + single_image: Result, + ab_image: Result, +) -> Result { + if !output_path_ovmf_vars.exists() { + println!( + "{} does not exist yet. Creating...", + &output_path_ovmf_vars.display() + ); + + let test_dir = testdir!(); + let tmp_file = test_dir.join(&output_path_ovmf_vars.file_name().unwrap()); + println!("Copy template OVMF vars to temporary file..."); + copy(&input_path_ovmf_vars?, &tmp_file)?; + + Command::new(format!("{}", cmd_qemu_system?)) + .arg("-boot") + .arg("order=d,menu=on,reboot-timeout=5000") + .arg("-m") + .arg("size=3072") + .arg("-machine") + .arg("type=q35,smm=on,accel=kvm,usb=on") + .arg("-smbios") + .arg("type=11,value=io.systemd.credential:set_efi_boot_entries=yes") + .arg("-drive") + .arg(format!( + "if=pflash,format=raw,unit=0,file={},read-only=on", + input_path_ovmf_code?.display() + )) + .arg("-drive") + .arg(format!( + "file={},format=raw,if=pflash,readonly=off,unit=1", + tmp_file.display() + )) + .arg("-drive") + .arg(format!("format=qcow2,file={}", &single_image?.display())) + .arg("-drive") + .arg(format!("format=qcow2,file={}", &ab_image?.path().display())) + .arg("-nographic") + .arg("-nodefaults") + .arg("-chardev") + .arg("stdio,mux=on,id=console,signal=off") + .arg("-serial") + .arg("chardev:console") + .arg("-mon") + .arg("console") + .assert() + .try_success()?; + + println!("Copy template OVMF vars to output..."); + copy(&tmp_file, &output_path_ovmf_vars)?; + } + Ok(output_path_ovmf_vars) +} + +#[fixture] +/// A fixture to provide empty update images used in test setups +/// +/// The created `UpdateImage`s are of varying type (`DiskType`) and filesystem (`FileSystem`) +pub fn bundle_disks( + cmd_mkosi: Result, + cmd_qemu_img: Result, + mkosi_dir_bundle_image: PathBuf, + output_dir: PathBuf, +) -> Result, TestError> { + let mut paths = vec![]; + let disk_types = [DiskType::Empty, DiskType::Multiple, DiskType::Single]; + let filesystems = [FileSystem::Btrfs, FileSystem::Ext4, FileSystem::Vfat]; + let mkosi = cmd_mkosi?; + let qemu_img = cmd_qemu_img?; + + for filesystem in filesystems { + for disk_type in disk_types { + let path: PathBuf = [ + format!("{}", output_dir.display()), + format!("{}_{}.qcow2", filesystem, disk_type), + ] + .iter() + .collect(); + + if !path.exists() { + println!("{} does not exist yet. Generating...", path.display()); + Command::new(mkosi.path()) + .arg("--output") + .arg(format!("{}_{}", filesystem, disk_type)) + .arg("--output-dir") + .arg(format!("{}", &output_dir.display())) + .arg("--repart-dir") + .arg(format!("repart/{}/{}", filesystem, disk_type)) + .arg("--force") + .arg("build") + .current_dir(&mkosi_dir_bundle_image) + .assert() + .try_success()?; + + convert_image( + &qemu_img, + &format!( + "{}", + &path + .with_file_name(format!("{}_{}.raw", filesystem, disk_type)) + .display() + ), + &format!("{}", &path.display()), + )?; + + snapshot_image(&qemu_img, &path, None)?; + + // remove unnecessary files to save space + remove_files(&[ + &format!( + "{}", + &path + .with_file_name(format!("{}_{}.raw", filesystem, disk_type)) + .display() + ), + &format!( + "{}", + &path + .with_file_name(format!("{}_{}", filesystem, disk_type)) + .display() + ), + ])?; + } + + paths.push(UpdateImage::new(path, filesystem, disk_type)); + } + } + + Ok(paths) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..ee3a6dc --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use assert_cmd::Command; +use std::fs::copy; +use std::path::PathBuf; +use testdir::testdir; +use testresult::TestResult; + +mod cmd; +pub use cmd::cmd_guestmount; +pub use cmd::cmd_guestunmount; +pub use cmd::cmd_qemu_img; +pub use cmd::cmd_qemu_system; +pub use cmd::cmd_rauc; +pub use cmd::cmd_sleep; +pub use cmd::Cmd; + +mod error; +pub use error::TestError; + +mod image; +pub use image::ab_image; +pub use image::bundle_disks; +pub use image::ovmf_vars; +use image::reset_image; +pub use image::DiskType; +pub use image::FileSystem; +pub use image::TestImage; +pub use image::UpdateImage; + +mod rauc; +pub use rauc::rauc_bundles; +pub use rauc::RaucBundle; + +mod path; +pub use path::input_path_ovmf_code; +use path::input_path_ovmf_vars; +use path::mkosi_dir_ab_image; +use path::mkosi_dir_base_image; +use path::mkosi_dir_bundle_image; +use path::mkosi_dir_single_image; +use path::output_dir; +use path::output_dir_ab_override; +use path::output_path_ab_image; +use path::output_path_base_image; +use path::output_path_ovmf_vars; +use path::output_path_single_image; +use path::remove_files; + +mod pki; +pub use pki::public_key_infrastructure; + +/// Run a test using QEMU +/// +/// A prepared A/B image (containing the caterpillar payload) is booted into, using a pre-configured EFI bootloader, while an image containing zero or more RAUC update bundles is attached +pub fn run_test( + qemu_system: &Cmd, + qemu_img: &Cmd, + sleep: &Cmd, + ovmf_code: PathBuf, + ovmf_vars: PathBuf, + ab_image: TestImage, + bundle_disk: UpdateImage, + name: &str, +) -> TestResult { + // NOTE: we need to wait for the images to settle + Command::new(&sleep.path()) + .arg("1") + .assert() + .try_success()?; + + let tmp_ovmf_vars = testdir!().join(&ovmf_vars.file_name().unwrap()); + copy(&ovmf_vars, &tmp_ovmf_vars)?; + + Command::new(&qemu_system.path()) + .arg("-boot") + .arg("order=d,menu=on,reboot-timeout=5000") + .arg("-m") + .arg("size=3072") + .arg("-machine") + .arg("type=q35,smm=on,accel=kvm,usb=on") + .arg("-smbios") + .arg(format!( + "type=11,value=io.systemd.credential:test_environment={}", + name + )) + .arg("-drive") + .arg(format!( + "if=pflash,format=raw,unit=0,file={},read-only=on", + &ovmf_code.display() + )) + .arg("-drive") + .arg(format!( + "file={},format=raw,if=pflash,readonly=off,unit=1", + &tmp_ovmf_vars.display() + )) + .arg("-drive") + .arg(format!("format=qcow2,file={}", ab_image.path().display())) + .arg("-drive") + .arg(format!( + "format=qcow2,file={}", + bundle_disk.path().display() + )) + .arg("-nographic") + .arg("-nodefaults") + .arg("-chardev") + .arg("stdio,mux=on,id=console,signal=off") + .arg("-serial") + .arg("chardev:console") + .arg("-mon") + .arg("console") + .assert() + .try_success()?; + + reset_image(&qemu_img, &bundle_disk.path())?; + reset_image(&qemu_img, &ab_image.path())?; + + // NOTE: we need to wait for the images to settle + Command::new(&sleep.path()) + .arg("1") + .assert() + .try_success()?; + + Ok(()) +} diff --git a/tests/common/path.rs b/tests/common/path.rs new file mode 100644 index 0000000..5662a53 --- /dev/null +++ b/tests/common/path.rs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use super::TestError; +use rstest::fixture; +use std::fs::remove_file; +use std::path::PathBuf; + +/// Remove files in a list of files +pub fn remove_files(files: &[&str]) -> Result<(), std::io::Error> { + for file in files { + if let Err(error) = remove_file(file) { + eprintln!("Unable to remove {}: {}", file, error); + }; + } + Ok(()) +} + +#[fixture] +/// A fixture to provide the first matching location of OVMF code files +pub fn input_path_ovmf_code() -> Result { + let candidates = [PathBuf::from("/usr/share/edk2/x64/OVMF_CODE.4m.fd")]; + + match candidates.iter().find(|&candidate| candidate.exists()) { + Some(candidate) => Ok(candidate.clone()), + None => return Err(TestError::Missing("OVMF code".to_string())), + } +} + +#[fixture] +/// A fixture to provide the first matching location of OVMF variable files +pub fn input_path_ovmf_vars() -> Result { + let candidates = [PathBuf::from("/usr/share/edk2/x64/OVMF_VARS.4m.fd")]; + + match candidates.iter().find(|&candidate| candidate.exists()) { + Some(candidate) => Ok(candidate.clone()), + None => return Err(TestError::Missing("OVMF code".to_string())), + } +} + +#[fixture] +/// A fixture to provide the output dir for integration tests +pub fn output_dir() -> PathBuf { + PathBuf::from(env!("CARGO_TARGET_TMPDIR")) +} + +#[fixture] +/// A fixture to provide the output path for the base image +pub fn output_path_base_image(output_dir: PathBuf) -> PathBuf { + PathBuf::from(&output_dir).join("base") +} + +#[fixture] +/// A fixture to provide the output path for the single image +pub fn output_path_single_image(output_dir: PathBuf) -> PathBuf { + PathBuf::from(&output_dir).join("single") +} + +#[fixture] +/// A fixture to provide the output path for the A/B image +pub fn output_path_ab_image(output_dir: PathBuf) -> PathBuf { + PathBuf::from(&output_dir).join("ab") +} + +#[fixture] +/// A fixture to provide the output path for the override directory of the A/B image +pub fn output_dir_ab_override(output_dir: PathBuf) -> PathBuf { + PathBuf::from(&output_dir).join("override_for_ab") +} + +#[fixture] +/// A fixture to provide the output path for modified OVMF vars file +/// +/// This location is used for the OVMF vars that contain EFI bootloader entries for the A/B image +pub fn output_path_ovmf_vars(output_dir: PathBuf) -> PathBuf { + PathBuf::from(&output_dir).join("ovmf_vars.fd") +} + +#[fixture] +/// A fixture to provide the integration tests directory of the project +fn tests_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests") +} + +#[fixture] +/// A fixture to provide the directory of the mkosi base image setup +pub fn mkosi_dir_base_image(tests_dir: PathBuf) -> PathBuf { + PathBuf::from(tests_dir).join("mkosi").join("base_image") +} + +#[fixture] +/// A fixture to provide the directory of the mkosi single image setup +pub fn mkosi_dir_single_image(tests_dir: PathBuf) -> PathBuf { + PathBuf::from(tests_dir).join("mkosi").join("single_image") +} + +#[fixture] +/// A fixture to provide the directory of the mkosi A/B image setup +pub fn mkosi_dir_ab_image(tests_dir: PathBuf) -> PathBuf { + PathBuf::from(tests_dir).join("mkosi").join("ab_image") +} + +#[fixture] +/// A fixture to provide the directory of the mkosi update image setup +pub fn mkosi_dir_bundle_image(tests_dir: PathBuf) -> PathBuf { + PathBuf::from(tests_dir).join("mkosi").join("bundle_image") +} diff --git a/tests/common/pki.rs b/tests/common/pki.rs new file mode 100644 index 0000000..898d484 --- /dev/null +++ b/tests/common/pki.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use std::fs::create_dir_all; +use std::path::PathBuf; + +use assert_cmd::Command; +use rstest::fixture; + +use super::cmd::cmd_openssl; +use super::path::output_dir; +use super::path::output_dir_ab_override; +use super::Cmd; +use super::TestError; + +#[fixture] +/// A fixture to define the output paths of the PKI +fn output_paths_pki(output_dir: PathBuf, output_dir_ab_override: PathBuf) -> (PathBuf, PathBuf) { + ( + PathBuf::from(&output_dir).join("system.key"), + PathBuf::from(&output_dir_ab_override) + .join("etc") + .join("rauc") + .join("system.pem"), + ) +} + +#[fixture] +/// A fixture to create and provide the public key infrastructure +pub fn public_key_infrastructure( + output_paths_pki: (PathBuf, PathBuf), + cmd_openssl: Result, +) -> Result<(PathBuf, PathBuf), TestError> { + if !(output_paths_pki.0.exists() && output_paths_pki.1.exists()) { + println!( + "{} and {} do not exist yet. Generating...", + &output_paths_pki.0.display(), + &output_paths_pki.1.display() + ); + + create_dir_all(output_paths_pki.1.parent().unwrap())?; + + Command::new(format!("{}", cmd_openssl?)) + .arg("req") + .arg("-x509") + .arg("-newkey") + .arg("rsa:4096") + .arg("-nodes") + .arg("-keyout") + .arg(format!("{}", &output_paths_pki.0.display())) + .arg("-out") + .arg(format!("{}", &output_paths_pki.1.display())) + .arg("-subj") + .arg("/O=Test/CN=systems-device") + .assert() + .try_success()?; + + assert!(output_paths_pki.0.exists()); + assert!(output_paths_pki.1.exists()); + } + Ok(output_paths_pki) +} diff --git a/tests/common/rauc.rs b/tests/common/rauc.rs new file mode 100644 index 0000000..207b4b3 --- /dev/null +++ b/tests/common/rauc.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use assert_cmd::Command; +use rstest::fixture; +use semver::Version; +use std::fs::copy; +use std::fs::create_dir_all; +use std::fs::remove_dir_all; +use std::fs::File; +use std::io::BufWriter; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use testdir::testdir; + +use super::ab_image; +use super::cmd_rauc; +use super::output_dir; +use super::public_key_infrastructure; +use super::Cmd; +use super::TestError; +use super::TestImage; + +#[derive(Clone, Debug)] +/// A RAUC update bundle +pub struct RaucBundle { + path: PathBuf, + version: Version, + compatible: String, +} + +impl RaucBundle { + pub fn new(path: PathBuf, version: Version, compatible: String) -> Self { + RaucBundle { + path, + version, + compatible, + } + } + + pub fn path(&self) -> &Path { + self.path.as_path() + } + + pub fn version(&self) -> &Version { + &self.version + } + + pub fn compatible(&self) -> &str { + &self.compatible + } +} + +#[fixture] +/// A fixture to describe which RAUC update bundles to create +fn output_rauc_bundles(output_dir: PathBuf) -> Vec { + vec![ + RaucBundle::new( + output_dir.join("update.raucb"), + Version::new(1, 0, 0), + "system".to_string(), + ), + RaucBundle::new( + output_dir.join("update2.raucb"), + Version::new(2, 0, 0), + "system".to_string(), + ), + ] +} + +#[fixture] +/// A fixture to provide RAUC update bundles +pub fn rauc_bundles( + cmd_rauc: Result, + output_rauc_bundles: Vec, + ab_image: Result, + public_key_infrastructure: Result<(PathBuf, PathBuf), TestError>, +) -> Result, TestError> { + let image_data = ab_image?; + let (private_key, public_key) = public_key_infrastructure?; + let rauc = format!("{}", cmd_rauc?); + let names = ("esp.vfat", "root.img"); + for bundle in output_rauc_bundles.iter() { + if !bundle.path().exists() { + eprintln!( + "RAUC update bundle {} does not exist yet. Creating...", + bundle.path().display() + ); + + let bundle_dir = testdir!().join("rauc_bundle_dir"); + create_dir_all(&bundle_dir)?; + + { + let mut f = BufWriter::new(File::create(bundle_dir.join("manifest.raucm"))?); + + writeln!(f, "[update]")?; + writeln!(f, "compatible={}", bundle.compatible())?; + writeln!(f, "version={}", bundle.version().to_string())?; + writeln!(f, "[bundle]")?; + writeln!(f, "format=verity")?; + writeln!(f, "[image.efi]")?; + writeln!(f, "filename={}", names.0)?; + writeln!(f, "[image.rootfs]")?; + writeln!(f, "filename={}", names.1)?; + } + + println!( + "Copy efi ({}) and rootfs ({}) to bundle dir...", + image_data.efi().display(), + image_data.rootfs().display() + ); + copy(image_data.efi(), bundle_dir.join(names.0))?; + copy(image_data.rootfs(), bundle_dir.join(names.1))?; + Command::new(&rauc) + .arg("bundle") + .arg("--key") + .arg(format!("{}", &private_key.display())) + .arg("--cert") + .arg(format!("{}", &public_key.display())) + .arg(format!("{}", bundle_dir.display())) + .arg(format!("{}", bundle.path().display())) + .assert() + .try_success()?; + remove_dir_all(bundle_dir)?; + } + } + Ok(output_rauc_bundles) +} diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..9b92558 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,290 @@ +// SPDX-FileCopyrightText: 2023 David Runge +// SPDX-License-Identifier: Apache-2.0 OR MIT +use rstest::rstest; +use std::path::PathBuf; +use testresult::TestResult; + +mod common; +use common::ab_image; +use common::bundle_disks; +use common::cmd_guestmount; +use common::cmd_guestunmount; +use common::cmd_qemu_img; +use common::cmd_qemu_system; +use common::cmd_sleep; +use common::input_path_ovmf_code; +use common::ovmf_vars; +use common::rauc_bundles; +use common::run_test; +use common::Cmd; +use common::DiskType; +use common::FileSystem; +use common::RaucBundle; +use common::TestError; +use common::TestImage; +use common::UpdateImage; + +use serial_test::file_serial; + +#[rstest] +#[case(FileSystem::Btrfs)] +#[case(FileSystem::Ext4)] +#[case(FileSystem::Vfat)] +#[file_serial] +fn integration_success_single( + cmd_qemu_img: Result, + cmd_qemu_system: Result, + cmd_guestmount: Result, + cmd_guestunmount: Result, + cmd_sleep: Result, + input_path_ovmf_code: Result, + ab_image: Result, + ovmf_vars: Result, + bundle_disks: Result, TestError>, + rauc_bundles: Result, TestError>, + #[case] filesystem: FileSystem, +) -> TestResult { + let name = "success_single"; + let disk_type = DiskType::Single; + + let qemu_img = cmd_qemu_img?; + let qemu_system = cmd_qemu_system?; + let guestmount = cmd_guestmount?; + let guestunmount = cmd_guestunmount?; + let sleep = cmd_sleep?; + let ovmf_vars = ovmf_vars?; + let update_bundles = rauc_bundles?; + let bundle_disk = match bundle_disks? + .iter() + .find(|x| x.filesystem().eq(&filesystem) && x.disk_type().eq(&disk_type)) + { + Some(bundle) => bundle.clone(), + None => return Err(testresult::TestError::from("foo")), + }; + let test_image = ab_image?; + println!("Built ab_image: {:?}", &test_image); + test_image.prepare_for_test(&qemu_img, &guestmount, &guestunmount)?; + + bundle_disk.prepare_test( + &qemu_img, + &guestmount, + &guestunmount, + vec![(update_bundles[0].clone(), PathBuf::from("update.raucb"))], + )?; + + println!("Created OVMF vars: {}", ovmf_vars.display()); + println!("Using bundle disk: {:?}", bundle_disk.path().display()); + println!("Created RAUC bundles: {:?}", update_bundles); + + run_test( + &qemu_system, + &qemu_img, + &sleep, + input_path_ovmf_code?, + ovmf_vars, + test_image, + bundle_disk, + name, + )?; + + Ok(()) +} + +#[rstest] +#[case(FileSystem::Btrfs)] +#[case(FileSystem::Ext4)] +#[case(FileSystem::Vfat)] +#[file_serial] +fn integration_success_multiple( + cmd_qemu_img: Result, + cmd_qemu_system: Result, + cmd_guestmount: Result, + cmd_guestunmount: Result, + cmd_sleep: Result, + input_path_ovmf_code: Result, + ab_image: Result, + ovmf_vars: Result, + bundle_disks: Result, TestError>, + rauc_bundles: Result, TestError>, + #[case] filesystem: FileSystem, +) -> TestResult { + let name = "success_multiple"; + let disk_type = DiskType::Multiple; + + let qemu_img = cmd_qemu_img?; + let qemu_system = cmd_qemu_system?; + let guestmount = cmd_guestmount?; + let guestunmount = cmd_guestunmount?; + let sleep = cmd_sleep?; + let ovmf_vars = ovmf_vars?; + let update_bundles = rauc_bundles?; + let bundle_disk = match bundle_disks? + .iter() + .find(|x| x.filesystem().eq(&filesystem) && x.disk_type().eq(&disk_type)) + { + Some(bundle) => bundle.clone(), + None => return Err(testresult::TestError::from("foo")), + }; + + let test_image = ab_image?; + println!("Built ab_image: {:?}", &test_image); + test_image.prepare_for_test(&qemu_img, &guestmount, &guestunmount)?; + + bundle_disk.prepare_test( + &qemu_img, + &guestmount, + &guestunmount, + vec![ + (update_bundles[0].clone(), PathBuf::from("update.raucb")), + (update_bundles[1].clone(), PathBuf::from("update2.raucb")), + ], + )?; + + println!("Created OVMF vars: {}", ovmf_vars.display()); + println!("Using bundle disk: {:?}", bundle_disk.path().display()); + println!("Created RAUC bundles: {:?}", update_bundles); + + run_test( + &qemu_system, + &qemu_img, + &sleep, + input_path_ovmf_code?, + ovmf_vars, + test_image, + bundle_disk, + name, + )?; + + Ok(()) +} + +#[rstest] +#[case(FileSystem::Btrfs)] +#[case(FileSystem::Ext4)] +#[case(FileSystem::Vfat)] +#[file_serial] +fn integration_success_override( + cmd_qemu_img: Result, + cmd_qemu_system: Result, + cmd_guestmount: Result, + cmd_guestunmount: Result, + cmd_sleep: Result, + input_path_ovmf_code: Result, + ab_image: Result, + ovmf_vars: Result, + bundle_disks: Result, TestError>, + rauc_bundles: Result, TestError>, + #[case] filesystem: FileSystem, +) -> TestResult { + let name = "success_override"; + let disk_type = DiskType::Multiple; + + let qemu_img = cmd_qemu_img?; + let qemu_system = cmd_qemu_system?; + let guestmount = cmd_guestmount?; + let guestunmount = cmd_guestunmount?; + let sleep = cmd_sleep?; + let ovmf_vars = ovmf_vars?; + let update_bundles = rauc_bundles?; + let bundle_disk = match bundle_disks? + .iter() + .find(|x| x.filesystem().eq(&filesystem) && x.disk_type().eq(&disk_type)) + { + Some(bundle) => bundle.clone(), + None => return Err(testresult::TestError::from("foo")), + }; + + let test_image = ab_image?; + println!("Built ab_image: {:?}", &test_image); + test_image.prepare_for_test(&qemu_img, &guestmount, &guestunmount)?; + + bundle_disk.prepare_test( + &qemu_img, + &guestmount, + &guestunmount, + vec![ + ( + update_bundles[0].clone(), + PathBuf::from("override/update.raucb"), + ), + (update_bundles[1].clone(), PathBuf::from("update2.raucb")), + ], + )?; + + println!("Created OVMF vars: {}", ovmf_vars.display()); + println!("Using bundle disk: {:?}", bundle_disk.path().display()); + println!("Created RAUC bundles: {:?}", update_bundles); + + run_test( + &qemu_system, + &qemu_img, + &sleep, + input_path_ovmf_code?, + ovmf_vars, + test_image, + bundle_disk, + name, + )?; + + Ok(()) +} + +#[rstest] +#[case(FileSystem::Btrfs)] +#[case(FileSystem::Ext4)] +#[case(FileSystem::Vfat)] +#[file_serial] +fn integration_skip_empty( + cmd_qemu_img: Result, + cmd_qemu_system: Result, + cmd_guestmount: Result, + cmd_guestunmount: Result, + cmd_sleep: Result, + input_path_ovmf_code: Result, + ab_image: Result, + ovmf_vars: Result, + bundle_disks: Result, TestError>, + rauc_bundles: Result, TestError>, + #[case] filesystem: FileSystem, +) -> TestResult { + let name = "skip_empty"; + let disk_type = DiskType::Empty; + + let qemu_img = cmd_qemu_img?; + let qemu_system = cmd_qemu_system?; + let guestmount = cmd_guestmount?; + let guestunmount = cmd_guestunmount?; + let sleep = cmd_sleep?; + let ovmf_vars = ovmf_vars?; + let update_bundles = rauc_bundles?; + let bundle_disk = match bundle_disks? + .iter() + .find(|x| x.filesystem().eq(&filesystem) && x.disk_type().eq(&disk_type)) + { + Some(bundle) => bundle.clone(), + None => return Err(testresult::TestError::from("foo")), + }; + + let test_image = ab_image?; + println!("Built ab_image: {:?}", &test_image); + test_image.prepare_for_test(&qemu_img, &guestmount, &guestunmount)?; + + bundle_disk.prepare_test(&qemu_img, &guestmount, &guestunmount, vec![])?; + + println!("Created OVMF vars: {}", ovmf_vars.display()); + println!("Using bundle disk: {:?}", bundle_disk.path().display()); + println!("Created RAUC bundles: {:?}", update_bundles); + + run_test( + &qemu_system, + &qemu_img, + &sleep, + input_path_ovmf_code?, + ovmf_vars, + test_image, + bundle_disk, + name, + )?; + + Ok(()) +} diff --git a/tests/mkosi/ab_image/mkosi.conf b/tests/mkosi/ab_image/mkosi.conf new file mode 100644 index 0000000..47d337a --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Distribution] +Distribution=arch + +[Output] +Bootable=yes +Format=disk +Output=ab +SplitArtifacts=yes diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/rauc/system.conf b/tests/mkosi/ab_image/mkosi.extra/etc/rauc/system.conf new file mode 100644 index 0000000..4ff0c2d --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/rauc/system.conf @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[system] +bootloader=efi +bundle-formats=-plain +compatible=system +data-directory=/mnt + +[keyring] +path=system.pem + +[slot.efi.0] +device=/dev/sda2 +parent=rootfs.0 +type=vfat + +[slot.efi.1] +device=/dev/sda3 +parent=rootfs.1 +type=vfat + +[slot.rootfs.0] +bootname=system0 +device=/dev/sda4 +type=raw + +[slot.rootfs.1] +bootname=system1 +device=/dev/sda5 +type=raw + +[handlers] +post-install=/usr/lib/rauc/post-install diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system-preset/00-ab_image.preset b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system-preset/00-ab_image.preset new file mode 100644 index 0000000..77da0aa --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system-preset/00-ab_image.preset @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later +enable mnt.mount +enable bootcounter.service +enable caterpillar.service +enable evaluate-tests.service +enable getty@tty1.service +enable rauc-status.service +enable sshd.service +disable archlinux-keyring-wkd-sync.timer diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/bootcounter.service b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/bootcounter.service new file mode 100644 index 0000000..bf5ab95 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/bootcounter.service @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +After=mnt.mount +Before=caterpillar.service +ConditionCredential=test_environment +Description=Count boot of A/B slots +Wants=mnt.mount + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/bootcounter.sh + +[Install] +WantedBy=multi-user.target diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/caterpillar.service b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/caterpillar.service new file mode 100644 index 0000000..4a56ba8 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/caterpillar.service @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +After=evaluate-tests.service mnt.mount +ConditionFileIsExecutable=/mnt/caterpillar +ConditionCredential=test_environment +Description=Update system with updates found on attached block devices +OnFailure=reevaluate-tests.service +OnSuccess=reevaluate-tests.service +Wants=evaluate-tests.service mnt.mount + +[Service] +ExecStart=/mnt/caterpillar + +[Install] +WantedBy=multi-user.target diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/evaluate-tests.service b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/evaluate-tests.service new file mode 100644 index 0000000..9dbe92d --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/evaluate-tests.service @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +After=bootcounter.service mnt.mount +Before=caterpillar.service +ConditionCredential=test_environment +Description=Evaluate tests for A/B boot +Wants=bootcounter.service mnt.mount + +[Service] +Type=oneshot +ExecStart=/usr/bin/evaluate-tests.sh + +[Install] +WantedBy=multi-user.target diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/mnt.mount b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/mnt.mount new file mode 100644 index 0000000..b8ed3ea --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/mnt.mount @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +Description=Mount persistent storage as /mnt +Conflicts=umount.target +Before=local-fs.target umount.target + +[Mount] +What=/dev/sda1 +Where=/mnt + +[Install] +WantedBy=local-fs.target diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/rauc-status.service b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/rauc-status.service new file mode 100644 index 0000000..d0dece6 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/rauc-status.service @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +After=mnt.mount +Before=caterpillar.service finish-ab-tests.service +ConditionCredential=test_environment +Description=Write RAUC status to persistent storage +Wants=mnt.mount + +[Service] +ExecStart=bash -c 'rauc status --output-format=json --detailed > /mnt/rauc-status.json' +RemainAfterExit=yes +Type=oneshot + +[Install] +WantedBy=multi-user.target diff --git a/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/reevaluate-tests.service b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/reevaluate-tests.service new file mode 100644 index 0000000..b592e22 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/etc/systemd/system/reevaluate-tests.service @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +After=bootcounter.service mnt.mount +ConditionCredential=test_environment +Description=Re-evaluate tests for A/B boot +Wants=bootcounter.service mnt.mount + +[Service] +Type=oneshot +Environment=REEVALUATE=1 +ExecStart=/usr/bin/evaluate-tests.sh diff --git a/tests/mkosi/ab_image/mkosi.extra/usr/bin/bootcounter.sh b/tests/mkosi/ab_image/mkosi.extra/usr/bin/bootcounter.sh new file mode 100755 index 0000000..116f9bc --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/usr/bin/bootcounter.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +set -eu + +readonly state_dir="${1:-/mnt}" +readonly state_booted_a="$state_dir/ab_tests_booted_a" +readonly state_booted_b="$state_dir/ab_tests_booted_b" + +if (( $(id -u) > 0 )); then + >&2 printf "This script must be run as root!\n" + exit 1 +fi + +if [[ ! -d "$state_dir" ]]; then + printf "Creating state dir %s!\n" "$state_dir" + mkdir -vp -- "$state_dir" +fi + +if [[ ! -f "$state_dir/ab_tests_booted_a" ]]; then + printf "0\n" > "$state_booted_a" +fi + +if [[ ! -f "$state_dir/ab_tests_booted_b" ]]; then + printf "0\n" > "$state_booted_b" +fi + +current_boot_a_count=$(< "$state_booted_a") +current_boot_b_count=$(< "$state_booted_b") + +if df | grep /dev/sda4 > /dev/null; then + booted_a=1 + printf "%s\n" "$(( booted_a + current_boot_a_count ))" > "$state_booted_a" + current_boot_a_count=$(< "$state_booted_a") +else + booted_a=0 +fi + +if df | grep /dev/sda5 > /dev/null; then + booted_b=1 + printf "%s\n" "$(( booted_b + current_boot_b_count ))" > "$state_booted_b" + current_boot_b_count=$(< "$state_booted_b") +else + booted_b=0 +fi + +printf "Boot count A/B: %s/ %s\n" "$current_boot_a_count" "$current_boot_b_count" diff --git a/tests/mkosi/ab_image/mkosi.extra/usr/bin/evaluate-tests.sh b/tests/mkosi/ab_image/mkosi.extra/usr/bin/evaluate-tests.sh new file mode 100755 index 0000000..ad8c96e --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/usr/bin/evaluate-tests.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +set -eu + +readonly state_dir="${1:-/mnt}" +readonly log_file="$state_dir/evaluate_tests.log" +readonly state_booted_a="$state_dir/ab_tests_booted_a" +readonly state_booted_b="$state_dir/ab_tests_booted_b" +readonly reevaluate="${REEVALUATE:-0}" +booted_a=0 +booted_b=0 + +touch "$log_file" + +if (( $(id -u) > 0 )); then + >&2 printf "This script must be run as root!\n" | tee -a "$log_file" + exit 1 +fi + +current_boot_a_count=$(< "$state_booted_a") +current_boot_b_count=$(< "$state_booted_b") + +if df | grep /dev/sda4 > /dev/null; then + booted_a=1 +fi + +if df | grep /dev/sda5 > /dev/null; then + booted_b=1 +fi + +printf "Boot count A/B: %s/ %s\n" "$current_boot_a_count" "$current_boot_b_count" | tee -a "$log_file" + +if (( booted_a == 1 )); then + printf "Booted into A\n" | tee -a "$log_file" +fi + +if (( booted_b == 1 )); then + printf "Booted into B\n" | tee -a "$log_file" +fi + +if (( reevaluate == 1 )); then + printf "Running test evaluation after payload\n" | tee -a "$log_file" +fi + +config="$(< "/run/credentials/@system/test_environment")" +printf "Evaluating test case '%s'\n" "$config" | tee -a "$log_file" + +case "$config" in + "success_single") + printf "Checking if slot B is booted...\n" | tee -a "$log_file" + if (( booted_b > 0 )); then + printf "Booted into slot B, powering off...\n" | tee -a "$log_file" + umount /mnt + sleep 1 + systemctl poweroff + else + printf "Not booted into slot B, exiting...\n" | tee -a "$log_file" + exit 1 + fi + ;; + "success_multiple") + target_version="2.0.0" + + printf "Checking if slot B is booted...\n" | tee -a "$log_file" + if (( booted_b > 0 )); then + printf "Booted into slot B, getting version...\n" | tee -a "$log_file" + version="$(jq '.slots[] | select(."rootfs.1") | ."rootfs.1".slot_status.bundle.version' /mnt/rauc-status.json | sed 's/"//g')" + + if [[ "$version" == "$target_version" ]]; then + printf "Booted into slot B with target version %s, powering off...\n" "$target_version" | tee -a "$log_file" + systemctl poweroff + else + printf "Slot B uses version %s instead of %s...\n" "$version" "$target_version" | tee -a "$log_file" + exit 1 + fi + else + printf "Not booted into slot B, exiting...\n" | tee -a "$log_file" + exit 1 + fi + ;; + "success_override") + target_version="1.0.0" + + printf "Checking if slot B is booted...\n" | tee -a "$log_file" + if (( booted_b > 0 )); then + printf "Booted into slot B, getting version...\n" | tee -a "$log_file" + version="$(jq '.slots[] | select(."rootfs.1") | ."rootfs.1".slot_status.bundle.version' /mnt/rauc-status.json | sed 's/"//g')" + + if [[ "$version" == "$target_version" ]]; then + printf "Booted into slot B with target version %s, powering off...\n" "$target_version" | tee -a "$log_file" + systemctl poweroff + else + printf "Slot B uses version %s instead of %s...\n" "$version" "$target_version" | tee -a "$log_file" + exit 1 + fi + else + printf "Not booted into slot B, exiting...\n" | tee -a "$log_file" + exit 1 + fi + ;; + "skip_empty") + if (( reevaluate == 1 )) && (( booted_a == 1 )) && (( current_boot_b_count == 0 )); then + printf "Slot B never booted and currently booted into slot A, powering off...\n" | tee -a "$log_file" + systemctl poweroff + else + printf "Test success criteria not met, exiting...\n" | tee -a "$log_file" + exit 1 + fi + ;; + *) + >&2 printf "Unknown configuration '%s' encountered!\n" "$config" | tee -a "$log_file" + exit 1 + ;; +esac diff --git a/tests/mkosi/ab_image/mkosi.extra/usr/lib/rauc/post-install b/tests/mkosi/ab_image/mkosi.extra/usr/lib/rauc/post-install new file mode 100755 index 0000000..6d05682 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.extra/usr/lib/rauc/post-install @@ -0,0 +1,57 @@ +#!/bin/bash +# +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# This post-install handler adjusts labels and UUIDs after updating. +# It applies to scenarios in which btrfs is used as root filesystem, which can only be handled using a RAUC slot type "raw". +# The modifications for btrfs are necessary, as otherwise two partitions will potentially have the same UUIDs after +# update, which leads to a filesystem error upon reboot (btrfs generation error). + +set -eu + +printf "Adjusting partitions after installation of update...\n" + +for i in $RAUC_TARGET_SLOTS; do + eval RAUC_SLOT_DEVICE=\$"RAUC_SLOT_DEVICE_${i}" + eval RAUC_SLOT_BOOTNAME=\$"RAUC_SLOT_BOOTNAME_${i}" + eval RAUC_SLOT_CLASS=\$"RAUC_SLOT_CLASS_${i}" + eval RAUC_SLOT_TYPE=\$"RAUC_SLOT_TYPE_${i}" + + case $RAUC_SLOT_BOOTNAME in + system0) + if [[ "$RAUC_SLOT_TYPE" == raw ]]; then + printf "Set new (random) UUID for updated raw (btrfs) partition %s\n" "$RAUC_SLOT_DEVICE" + btrfstune -fu "$RAUC_SLOT_DEVICE" + fi + + case $RAUC_SLOT_CLASS in + efi) + printf "Set new label for updated EFI partition %s\n" "$RAUC_SLOT_DEVICE" + fatlabel "$RAUC_SLOT_DEVICE" 'ESP_A' + ;; + rootfs) + printf "Set new label for updated (btrfs) rootfs partition %s\n" "$RAUC_SLOT_DEVICE" + btrfs filesystem label "$RAUC_SLOT_DEVICE" 'root_b' + ;; + esac + ;; + system1) + if [[ "$RAUC_SLOT_TYPE" == raw ]]; then + printf "Set new (random) UUID for updated raw (btrfs) partition %s\n" "$RAUC_SLOT_DEVICE" + btrfstune -fu "$RAUC_SLOT_DEVICE" + fi + + case $RAUC_SLOT_CLASS in + efi) + printf "Set new label for updated EFI partition %s\n" "$RAUC_SLOT_DEVICE" + fatlabel "$RAUC_SLOT_DEVICE" 'ESP_B' + ;; + rootfs) + printf "Set new label for updated (btrfs) rootfs partition %s\n" "$RAUC_SLOT_DEVICE" + btrfs filesystem label "$RAUC_SLOT_DEVICE" 'root_b' + ;; + esac + ;; + esac +done diff --git a/tests/mkosi/ab_image/mkosi.prepare b/tests/mkosi/ab_image/mkosi.prepare new file mode 100755 index 0000000..ba5348a --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.prepare @@ -0,0 +1,11 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +# shellcheck disable=SC2154 +if [[ "$container" != "mkosi" ]]; then + exec mkosi-chroot "$SCRIPT" "$@" +fi + +printf "Rename UKI for easier handling...\n" +mv -v -- /efi/EFI/Linux/*.efi /efi/EFI/Linux/linux.efi diff --git a/tests/mkosi/ab_image/mkosi.repart/00_persistent.conf b/tests/mkosi/ab_image/mkosi.repart/00_persistent.conf new file mode 100644 index 0000000..e45819f --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.repart/00_persistent.conf @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=btrfs +Label=persistent +SizeMinBytes=128M +SplitName=%t +Type=linux-generic diff --git a/tests/mkosi/ab_image/mkosi.repart/02_esp_a.conf b/tests/mkosi/ab_image/mkosi.repart/02_esp_a.conf new file mode 100644 index 0000000..95481c2 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.repart/02_esp_a.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/efi/EFI/Linux/linux.efi:/EFI/Linux/linux.efi +CopyFiles=/efi/loader:/loader +Format=vfat +Label=esp_a +SizeMinBytes=512M +SplitName=%t_a +Type=esp diff --git a/tests/mkosi/ab_image/mkosi.repart/02_esp_b.conf b/tests/mkosi/ab_image/mkosi.repart/02_esp_b.conf new file mode 100644 index 0000000..a62d315 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.repart/02_esp_b.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/efi/EFI/Linux/linux.efi:/EFI/Linux/linux.efi +CopyFiles=/efi/loader:/loader +Format=vfat +Label=esp_b +SizeMinBytes=512M +SplitName=%t_b +Type=esp diff --git a/tests/mkosi/ab_image/mkosi.repart/03_root_a.conf b/tests/mkosi/ab_image/mkosi.repart/03_root_a.conf new file mode 100644 index 0000000..660172a --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.repart/03_root_a.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/ +ExcludeFiles=/efi +Format=btrfs +Label=root_a +SizeMinBytes=2560M +SplitName=%t_a +Type=root diff --git a/tests/mkosi/ab_image/mkosi.repart/03_root_b.conf b/tests/mkosi/ab_image/mkosi.repart/03_root_b.conf new file mode 100644 index 0000000..265fb18 --- /dev/null +++ b/tests/mkosi/ab_image/mkosi.repart/03_root_b.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/ +ExcludeFiles=/efi +Format=btrfs +Label=root_b +SizeMinBytes=2560M +SplitName=%t_b +Type=root diff --git a/tests/mkosi/base_image/mkosi.conf b/tests/mkosi/base_image/mkosi.conf new file mode 100644 index 0000000..ef2e90d --- /dev/null +++ b/tests/mkosi/base_image/mkosi.conf @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Distribution] +Distribution=arch + +[Output] +Format=tar +Hostname=system +Output=base + +[Content] +Packages= + base + binutils + btrfs-progs + dosfstools + efibootmgr + jq + linux + openssh + rauc + squashfs-tools + sudo + systemd + tree + udisks2 +RootPassword=root diff --git a/tests/mkosi/base_image/mkosi.extra/etc/systemd/journald.conf.d/01-storage.conf b/tests/mkosi/base_image/mkosi.extra/etc/systemd/journald.conf.d/01-storage.conf new file mode 100644 index 0000000..6870678 --- /dev/null +++ b/tests/mkosi/base_image/mkosi.extra/etc/systemd/journald.conf.d/01-storage.conf @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Journal] +Storage=volatile diff --git a/tests/mkosi/base_image/mkosi.extra/etc/systemd/network/80-dhcp.network b/tests/mkosi/base_image/mkosi.extra/etc/systemd/network/80-dhcp.network new file mode 100644 index 0000000..e48d4b1 --- /dev/null +++ b/tests/mkosi/base_image/mkosi.extra/etc/systemd/network/80-dhcp.network @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Match] +Name=eth* +Name=en* + +[Network] +DHCP=yes diff --git a/tests/mkosi/base_image/mkosi.postinst b/tests/mkosi/base_image/mkosi.postinst new file mode 100755 index 0000000..bc7b2c1 --- /dev/null +++ b/tests/mkosi/base_image/mkosi.postinst @@ -0,0 +1,13 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +# shellcheck disable=SC2154 +if [[ "$container" != "mkosi" ]]; then + exec mkosi-chroot "$SCRIPT" "$@" +fi + +printf "Setup resolv.conf\n" +ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf +printf "Setup localtime\n" +ln -sf /usr/share/zoneinfo/UTC /etc/localtime diff --git a/tests/mkosi/base_image/mkosi.prepare b/tests/mkosi/base_image/mkosi.prepare new file mode 100755 index 0000000..7959243 --- /dev/null +++ b/tests/mkosi/base_image/mkosi.prepare @@ -0,0 +1,15 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +# shellcheck disable=SC2154 +if [[ "$container" != "mkosi" ]]; then + exec mkosi-chroot "$SCRIPT" "$@" +fi + +readonly user=arch + +printf "Add 'arch' user\n" +/usr/bin/useradd -m -U arch +printf "%s\n%s\n" "${user}" "${user}" | passwd "${user}" +printf "%s ALL=(ALL) NOPASSWD: ALL\n" "${user}" > "/etc/sudoers.d/${user}" diff --git a/tests/mkosi/bundle_image/mkosi.conf b/tests/mkosi/bundle_image/mkosi.conf new file mode 100644 index 0000000..47b0885 --- /dev/null +++ b/tests/mkosi/bundle_image/mkosi.conf @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Distribution] +Distribution=arch + +[Output] +Format=disk +SplitArtifacts=no diff --git a/tests/mkosi/bundle_image/repart/btrfs/empty/00_bundle.conf b/tests/mkosi/bundle_image/repart/btrfs/empty/00_bundle.conf new file mode 100644 index 0000000..e7177fa --- /dev/null +++ b/tests/mkosi/bundle_image/repart/btrfs/empty/00_bundle.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=btrfs +Label=bundle_disk_btrfs +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/btrfs/multiple/00_bundle.conf b/tests/mkosi/bundle_image/repart/btrfs/multiple/00_bundle.conf new file mode 100644 index 0000000..71789f2 --- /dev/null +++ b/tests/mkosi/bundle_image/repart/btrfs/multiple/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=btrfs +Label=bundle_disk_btrfs +SizeMinBytes=2G +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/btrfs/single/00_bundle.conf b/tests/mkosi/bundle_image/repart/btrfs/single/00_bundle.conf new file mode 100644 index 0000000..9f8de84 --- /dev/null +++ b/tests/mkosi/bundle_image/repart/btrfs/single/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=btrfs +Label=bundle_disk_btrfs +SizeMinBytes=1G +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/ext4/empty/00_bundle.conf b/tests/mkosi/bundle_image/repart/ext4/empty/00_bundle.conf new file mode 100644 index 0000000..63dd6ff --- /dev/null +++ b/tests/mkosi/bundle_image/repart/ext4/empty/00_bundle.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=ext4 +Label=bundle_disk_ext4 +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/ext4/multiple/00_bundle.conf b/tests/mkosi/bundle_image/repart/ext4/multiple/00_bundle.conf new file mode 100644 index 0000000..290f701 --- /dev/null +++ b/tests/mkosi/bundle_image/repart/ext4/multiple/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=ext4 +Label=bundle_disk_ext4 +SizeMinBytes=2G +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/ext4/single/00_bundle.conf b/tests/mkosi/bundle_image/repart/ext4/single/00_bundle.conf new file mode 100644 index 0000000..6b18b42 --- /dev/null +++ b/tests/mkosi/bundle_image/repart/ext4/single/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=ext4 +Label=bundle_disk_ext4 +SizeMinBytes=1G +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/vfat/empty/00_bundle.conf b/tests/mkosi/bundle_image/repart/vfat/empty/00_bundle.conf new file mode 100644 index 0000000..50cba1e --- /dev/null +++ b/tests/mkosi/bundle_image/repart/vfat/empty/00_bundle.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=vfat +Label=bundle_disk_vfat +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/vfat/multiple/00_bundle.conf b/tests/mkosi/bundle_image/repart/vfat/multiple/00_bundle.conf new file mode 100644 index 0000000..4059d86 --- /dev/null +++ b/tests/mkosi/bundle_image/repart/vfat/multiple/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=vfat +Label=bundle_disk_vfat +SizeMinBytes=2G +Type=linux-generic diff --git a/tests/mkosi/bundle_image/repart/vfat/single/00_bundle.conf b/tests/mkosi/bundle_image/repart/vfat/single/00_bundle.conf new file mode 100644 index 0000000..15fd44e --- /dev/null +++ b/tests/mkosi/bundle_image/repart/vfat/single/00_bundle.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +Format=vfat +Label=bundle_disk_vfat +SizeMinBytes=1G +Type=linux-generic diff --git a/tests/mkosi/single_image/mkosi.conf b/tests/mkosi/single_image/mkosi.conf new file mode 100644 index 0000000..481094f --- /dev/null +++ b/tests/mkosi/single_image/mkosi.conf @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Distribution] +Distribution=arch + +[Output] +Bootable=yes +Format=disk +Output=single +SplitArtifacts=no diff --git a/tests/mkosi/single_image/mkosi.extra/etc/systemd/system-preset/00-single_image.preset b/tests/mkosi/single_image/mkosi.extra/etc/systemd/system-preset/00-single_image.preset new file mode 100644 index 0000000..5bdc2f0 --- /dev/null +++ b/tests/mkosi/single_image/mkosi.extra/etc/systemd/system-preset/00-single_image.preset @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +enable efibootmgr-init.service +enable getty@tty1.service +enable sshd.service +disable archlinux-keyring-wkd-sync.timer diff --git a/tests/mkosi/single_image/mkosi.extra/etc/systemd/system/efibootmgr-init.service b/tests/mkosi/single_image/mkosi.extra/etc/systemd/system/efibootmgr-init.service new file mode 100644 index 0000000..4378c6e --- /dev/null +++ b/tests/mkosi/single_image/mkosi.extra/etc/systemd/system/efibootmgr-init.service @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Unit] +Description=Add EFI boot entries for A/B image +ConditionCredential=set_efi_boot_entries +ConditionPathExists=/dev/sdb1 +ConditionPathExists=/dev/sdb2 +ConditionPathExists=/dev/sdb3 +ConditionPathExists=/dev/sdb4 +ConditionPathExists=/dev/sdb5 + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/bin/mount /dev/sdb2 /mnt +ExecStart=bash -c 'echo "root=PARTUUID=$(blkid --output value --match-tag PARTUUID /dev/sdb5) console=ttyS0 systemd.tty.term.console=xterm-256color systemd.tty.columns.console=159 systemd.tty.rows.console=84 systemd.tty.term.ttyS0=xterm-256color systemd.tty.columns.ttyS0=159 systemd.tty.rows.ttyS0=84 rw" | iconv -f ascii -t ucs2 > /tmp/sdb3.txt; efibootmgr --create --disk /dev/sdb --part 3 --loader /EFI/Linux/linux.efi --label "system1" --unicode --append-binary-args /tmp/sdb3.txt' +ExecStart=bash -c 'echo "root=PARTUUID=$(blkid --output value --match-tag PARTUUID /dev/sdb4) console=ttyS0 systemd.tty.term.console=xterm-256color systemd.tty.columns.console=159 systemd.tty.rows.console=84 systemd.tty.term.ttyS0=xterm-256color systemd.tty.columns.ttyS0=159 systemd.tty.rows.ttyS0=84 rw" | iconv -f ascii -t ucs2 > /tmp/sdb2.txt; efibootmgr --create --disk /dev/sdb --part 2 --loader /EFI/Linux/linux.efi --label "system0" --unicode --append-binary-args /tmp/sdb2.txt' +ExecStart=/usr/bin/systemctl poweroff + +[Install] +WantedBy=multi-user.target diff --git a/tests/mkosi/single_image/mkosi.prepare b/tests/mkosi/single_image/mkosi.prepare new file mode 100755 index 0000000..ba5348a --- /dev/null +++ b/tests/mkosi/single_image/mkosi.prepare @@ -0,0 +1,11 @@ +#!/bin/bash +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +# shellcheck disable=SC2154 +if [[ "$container" != "mkosi" ]]; then + exec mkosi-chroot "$SCRIPT" "$@" +fi + +printf "Rename UKI for easier handling...\n" +mv -v -- /efi/EFI/Linux/*.efi /efi/EFI/Linux/linux.efi diff --git a/tests/mkosi/single_image/mkosi.repart/00_esp.conf b/tests/mkosi/single_image/mkosi.repart/00_esp.conf new file mode 100644 index 0000000..84a5a3f --- /dev/null +++ b/tests/mkosi/single_image/mkosi.repart/00_esp.conf @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/efi/EFI/Linux/linux.efi:/EFI/Linux/linux.efi +CopyFiles=/efi/EFI/BOOT:/EFI/BOOT +CopyFiles=/efi/EFI/systemd:/EFI/systemd +CopyFiles=/efi/loader:/loader +Format=vfat +Label=esp +SizeMinBytes=512M +Type=esp diff --git a/tests/mkosi/single_image/mkosi.repart/01_root.conf b/tests/mkosi/single_image/mkosi.repart/01_root.conf new file mode 100644 index 0000000..15b53ce --- /dev/null +++ b/tests/mkosi/single_image/mkosi.repart/01_root.conf @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 David Runge +# SPDX-License-Identifier: LGPL-3.0-or-later + +[Partition] +CopyFiles=/ +Format=btrfs +Label=root +SizeMinBytes=2560M +Type=root