Skip to content

Commit 72c3c74

Browse files
committed
reinstall: Initial set of system-reinstall-bootc integration tests
This adds a few basic integration tests for system-reinstall-bootc, adds a system-reinstall option to tests-integration to run them, and executes them as part of the github action. Signed-off-by: ckyrouac <[email protected]>
1 parent 5abda6a commit 72c3c74

File tree

7 files changed

+218
-5
lines changed

7 files changed

+218
-5
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ jobs:
108108
# Install tests
109109
sudo bootc-integration-tests install-alongside localhost/bootc
110110
111+
# system-reinstall-bootc tests
112+
cargo build --release -p system-reinstall-bootc
113+
114+
# not sure why this is missing in the ubuntu image but just creating this directory allows the tests to pass
115+
sudo mkdir -p /run/sshd
116+
117+
sudo install -m 0755 target/release/system-reinstall-bootc /usr/bin/system-reinstall-bootc
118+
sudo podman pull quay.io/fedora/fedora-bootc:42
119+
sudo bootc-integration-tests system-reinstall quay.io/fedora/fedora-bootc:42 --test-threads=1
120+
111121
# And the fsverity case
112122
sudo podman run --privileged --pid=host localhost/bootc-fsverity bootc install to-existing-root --stateroot=other \
113123
--acknowledge-destructive --skip-fetch-check

Cargo.lock

Lines changed: 45 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests-integration/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ fn-error-context = { workspace = true }
1919
indoc = { workspace = true }
2020
libtest-mimic = "0.8.0"
2121
oci-spec = "0.7.0"
22+
rexpect = "0.5"
2223
rustix = { workspace = true }
2324
serde = { workspace = true, features = ["derive"] }
2425
serde_json = { workspace = true }

tests-integration/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ full privileges, but is *not* destructive.
2222

2323
This suite is *DESTRUCTIVE*, executing the bootc `install to-existing-root`
2424
style flow using the host root. Run it in a transient virtual machine.
25+
26+
### `system-reinstall`
27+
28+
This suite is *DESTRUCTIVE*, executing the `system-reinstall-bootc`
29+
tests. Run it in a transient virtual machine.

tests-integration/src/install.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ const NON_DEFAULT_STATEROOT: &str = "foo";
1616

1717
/// Clear out and delete any ostree roots, leverage bootc hidden wipe-ostree command to get rid of
1818
/// otherwise hard to delete deployment files
19-
fn reset_root(sh: &Shell, image: &str) -> Result<()> {
19+
pub(crate) fn reset_root(sh: &Shell, image: &str) -> Result<()> {
2020
delete_ostree_deployments(sh, image)?;
2121
delete_ostree(sh)?;
2222
Ok(())
2323
}
2424

25-
fn delete_ostree(sh: &Shell) -> Result<(), anyhow::Error> {
25+
pub(crate) fn delete_ostree(sh: &Shell) -> Result<(), anyhow::Error> {
2626
if !Path::new("/ostree/").exists() {
2727
return Ok(());
2828
}
@@ -61,7 +61,7 @@ fn find_deployment_root() -> Result<Dir> {
6161
}
6262

6363
// Hook relatively cheap post-install tests here
64-
fn generic_post_install_verification() -> Result<()> {
64+
pub(crate) fn generic_post_install_verification() -> Result<()> {
6565
assert!(Utf8Path::new("/ostree/repo").try_exists()?);
6666
assert!(Utf8Path::new("/ostree/bootc/storage/overlay").try_exists()?);
6767
Ok(())
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use anyhow::{anyhow, Context, Result};
2+
use fn_error_context::context;
3+
use libtest_mimic::Trial;
4+
use rexpect::session::PtySession;
5+
use rustix::fs::statfs;
6+
use std::{
7+
fs::{self},
8+
path::Path,
9+
};
10+
11+
use crate::install;
12+
13+
const TIMEOUT: u64 = 120000;
14+
15+
fn get_deployment_dir() -> Result<std::path::PathBuf> {
16+
let base_path = Path::new("/ostree/deploy/default/deploy");
17+
18+
let entries: Vec<fs::DirEntry> = fs::read_dir(base_path)
19+
.with_context(|| format!("Failed to read directory: {}", base_path.display()))?
20+
.filter_map(|entry| match entry {
21+
Ok(e) if e.path().is_dir() => Some(e),
22+
_ => None,
23+
})
24+
.collect::<Vec<_>>();
25+
26+
assert_eq!(
27+
entries.len(),
28+
1,
29+
"Expected exactly one deployment directory"
30+
);
31+
32+
let deploy_dir_entry = &entries[0];
33+
assert!(
34+
deploy_dir_entry.file_type()?.is_dir(),
35+
"deployment directory entry is not a directory: {}",
36+
base_path.display()
37+
);
38+
39+
let hash = deploy_dir_entry.file_name();
40+
let hash_str = hash
41+
.to_str()
42+
.ok_or_else(|| anyhow!("Deployment directory name {:?} is not valid UTF-8", hash))?;
43+
44+
println!("Using deployment directory: {}", hash_str);
45+
46+
Ok(base_path.join(hash_str))
47+
}
48+
49+
#[context("System reinstall tests")]
50+
pub(crate) fn run(image: &str, testargs: libtest_mimic::Arguments) -> Result<()> {
51+
// Just leak the image name so we get a static reference as required by the test framework
52+
let image: &'static str = String::from(image).leak();
53+
54+
let tests = [
55+
Trial::test("default behavior", move || {
56+
let sh = &xshell::Shell::new()?;
57+
install::reset_root(sh, image)?;
58+
59+
let mut p: PtySession = rexpect::spawn(
60+
format!("/usr/bin/system-reinstall-bootc {image}").as_str(),
61+
Some(TIMEOUT),
62+
)?;
63+
64+
// Basic flow stdout verification
65+
p.exp_regex("Found only one user ([^:]+) with ([\\d]+) SSH authorized keys.")?;
66+
p.exp_string("Would you like to import its SSH authorized keys")?;
67+
p.exp_string("into the root user on the new bootc system?")?;
68+
p.exp_string("Then you can login as root@ using those keys. [Y/n]")?;
69+
p.send_line("a")?;
70+
71+
p.exp_string("Going to run command:")?;
72+
73+
p.exp_regex(format!("podman run --privileged --pid=host --user=root:root -v /var/lib/containers:/var/lib/containers -v /dev:/dev --security-opt label=type:unconfined_t -v /:/target -v /tmp/([^:]+):/bootc_authorized_ssh_keys/root {image} bootc install to-existing-root --acknowledge-destructive --skip-fetch-check --cleanup --root-ssh-authorized-keys /bootc_authorized_ssh_keys/root").as_str())?;
74+
p.exp_string("NOTICE: This will replace the installed operating system and reboot. Are you sure you want to continue? [y/N]")?;
75+
76+
p.send_line("y")?;
77+
78+
p.exp_string(format!("Installing image: docker://{image}").as_str())?;
79+
p.exp_string("Initializing ostree layout")?;
80+
p.exp_string("Operation complete, rebooting in 10 seconds. Press Ctrl-C to cancel reboot, or press enter to continue immediately.")?;
81+
p.send_control('c')?;
82+
83+
p.exp_eof()?;
84+
85+
install::generic_post_install_verification()?;
86+
87+
// Check for destructive cleanup and ssh key files
88+
let target_deployment_dir =
89+
get_deployment_dir().with_context(|| "Failed to get deployment directory")?;
90+
91+
let files = [
92+
"usr/lib/bootc/fedora-bootc-destructive-cleanup",
93+
"usr/lib/systemd/system/bootc-destructive-cleanup.service",
94+
"usr/lib/systemd/system/multi-user.target.wants/bootc-destructive-cleanup.service",
95+
"etc/tmpfiles.d/bootc-root-ssh.conf",
96+
];
97+
98+
for f in files {
99+
let full_path = target_deployment_dir.join(f);
100+
assert!(
101+
full_path.exists(),
102+
"File not found: {}",
103+
full_path.display()
104+
);
105+
}
106+
107+
Ok(())
108+
}),
109+
Trial::test("disk space check", move || {
110+
let sh = &xshell::Shell::new()?;
111+
install::reset_root(sh, image)?;
112+
113+
// Allocate a file with the size of the available space on the root partition
114+
let stat = statfs("/")?;
115+
let available_space_bytes: u64 = stat.f_bsize as u64 * stat.f_bavail as u64;
116+
let file_size = available_space_bytes - (250 * 1024 * 1024); //leave 250 MiB free
117+
118+
let tempfile = tempfile::Builder::new().tempfile_in("/")?;
119+
let tempfile_path = tempfile.path();
120+
121+
let file = std::fs::OpenOptions::new()
122+
.write(true)
123+
.create(true)
124+
.truncate(true)
125+
.open(tempfile_path)?;
126+
127+
rustix::fs::fallocate(&file, rustix::fs::FallocateFlags::empty(), 0, file_size)?;
128+
129+
// Run system-reinstall-bootc
130+
let mut p: PtySession = rexpect::spawn(
131+
format!("/usr/bin/system-reinstall-bootc {image}").as_str(),
132+
Some(TIMEOUT),
133+
)?;
134+
135+
p.exp_regex("Found only one user ([^:]+) with ([\\d]+) SSH authorized keys.")?;
136+
p.send_line("a")?;
137+
p.exp_string("NOTICE: This will replace the installed operating system and reboot. Are you sure you want to continue? [y/N]")?;
138+
p.send_line("y")?;
139+
p.exp_string("Insufficient free space")?;
140+
p.exp_eof()?;
141+
Ok(())
142+
}),
143+
];
144+
145+
libtest_mimic::run(&testargs, tests.into()).exit()
146+
}

tests-integration/src/tests-integration.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ mod hostpriv;
99
mod install;
1010
mod runvm;
1111
mod selinux;
12+
mod system_reinstall;
1213

1314
#[derive(Debug, Parser)]
1415
#[clap(name = "bootc-integration-tests", version, rename_all = "kebab-case")]
1516
pub(crate) enum Opt {
17+
SystemReinstall {
18+
/// Source container image reference
19+
image: String,
20+
#[clap(flatten)]
21+
testargs: libtest_mimic::Arguments,
22+
},
1623
InstallAlongside {
1724
/// Source container image reference
1825
image: String,
@@ -45,6 +52,7 @@ pub(crate) enum Opt {
4552
fn main() {
4653
let opt = Opt::parse();
4754
let r = match opt {
55+
Opt::SystemReinstall { image, testargs } => system_reinstall::run(&image, testargs),
4856
Opt::InstallAlongside { image, testargs } => install::run_alongside(&image, testargs),
4957
Opt::HostPrivileged { image, testargs } => hostpriv::run_hostpriv(&image, testargs),
5058
Opt::Container { testargs } => container::run(testargs),

0 commit comments

Comments
 (0)