Skip to content

Commit 811481e

Browse files
committed
libvirt: add --bind-storage-ro support for bootc upgrades
Implement --bind-storage-ro flag for `bcvk libvirt run` to enable bootc upgrade workflows from persistent VMs by mounting host container storage read-only. Assisted-by: Claude Code Signed-off-by: Colin Walters <[email protected]>
1 parent 37d3b7a commit 811481e

File tree

10 files changed

+639
-2
lines changed

10 files changed

+639
-2
lines changed

Cargo.lock

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

crates/integration-tests/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ fn main() {
204204
tests::libvirt_verb::test_libvirt_error_handling();
205205
Ok(())
206206
}),
207+
Trial::test("libvirt_bind_storage_ro", || {
208+
tests::libvirt_verb::test_libvirt_bind_storage_ro();
209+
Ok(())
210+
}),
207211
];
208212

209213
// Run the tests and exit with the result

crates/integration-tests/src/tests/libvirt_verb.rs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,58 @@ fn cleanup_domain(domain_name: &str) {
382382
}
383383
}
384384

385+
/// Wait for SSH to become available on a domain with a timeout
386+
fn wait_for_ssh_available(
387+
bck: &str,
388+
domain_name: &str,
389+
timeout_secs: u64,
390+
) -> Result<(), Box<dyn std::error::Error>> {
391+
let start_time = std::time::Instant::now();
392+
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
393+
394+
println!(
395+
"Waiting for SSH to become available on domain: {}",
396+
domain_name
397+
);
398+
399+
loop {
400+
// Try a simple SSH command to test connectivity
401+
let ssh_test = Command::new("timeout")
402+
.args([
403+
"10s", // Short timeout for individual SSH attempts
404+
bck,
405+
"libvirt",
406+
"ssh",
407+
domain_name,
408+
"--",
409+
"echo",
410+
"ssh-ready",
411+
])
412+
.output();
413+
414+
match ssh_test {
415+
Ok(output) if output.status.success() => {
416+
println!("✓ SSH is now available");
417+
return Ok(());
418+
}
419+
Ok(_) => {
420+
// SSH command failed, but that's expected while VM is booting
421+
}
422+
Err(e) => {
423+
println!("SSH test error (expected while booting): {}", e);
424+
}
425+
}
426+
427+
// Check if we've exceeded the timeout
428+
if start_time.elapsed() >= timeout_duration {
429+
return Err(format!("Timeout waiting for SSH after {} seconds", timeout_secs).into());
430+
}
431+
432+
// Wait 5 seconds before next attempt
433+
std::thread::sleep(std::time::Duration::from_secs(5));
434+
}
435+
}
436+
385437
/// Test VM startup and shutdown with libvirt run
386438
pub fn test_libvirt_vm_lifecycle() {
387439
// Skip if running in CI/container environment without libvirt
@@ -511,6 +563,239 @@ pub fn test_libvirt_vm_lifecycle() {
511563
println!("VM lifecycle test completed");
512564
}
513565

566+
/// Test container storage binding functionality end-to-end
567+
pub fn test_libvirt_bind_storage_ro() {
568+
let bck = get_bck_command().unwrap();
569+
let test_image = get_test_image();
570+
571+
// First check if libvirt supports readonly virtiofs
572+
println!("Checking libvirt capabilities...");
573+
let status_output = Command::new(&bck)
574+
.args(&["libvirt", "status", "--format", "json"])
575+
.output()
576+
.expect("Failed to get libvirt status");
577+
578+
if !status_output.status.success() {
579+
let stderr = String::from_utf8_lossy(&status_output.stderr);
580+
panic!("Failed to get libvirt status: {}", stderr);
581+
}
582+
583+
let status: serde_json::Value =
584+
serde_json::from_slice(&status_output.stdout).expect("Failed to parse libvirt status JSON");
585+
586+
let supports_readonly = status["supports_readonly_virtiofs"]
587+
.as_bool()
588+
.expect("Missing supports_readonly_virtiofs field in status output");
589+
590+
if !supports_readonly {
591+
println!("Skipping test: libvirt does not support readonly virtiofs");
592+
println!("libvirt version: {:?}", status["version"]);
593+
println!("Requires libvirt 6.2+ for readonly virtiofs support");
594+
return;
595+
}
596+
597+
// Generate unique domain name for this test
598+
let domain_name = format!(
599+
"test-bind-storage-{}",
600+
std::time::SystemTime::now()
601+
.duration_since(std::time::UNIX_EPOCH)
602+
.unwrap()
603+
.as_secs()
604+
);
605+
606+
println!("Testing --bind-storage-ro with domain: {}", domain_name);
607+
608+
// Cleanup any existing domain with this name
609+
let _ = Command::new("virsh")
610+
.args(&["destroy", &domain_name])
611+
.output();
612+
let _ = Command::new("virsh")
613+
.args(&["undefine", &domain_name])
614+
.output();
615+
616+
// Create domain with --bind-storage-ro flag
617+
println!("Creating libvirt domain with --bind-storage-ro...");
618+
let create_output = Command::new("timeout")
619+
.args([
620+
"300s", // 5 minute timeout for domain creation
621+
&bck,
622+
"libvirt",
623+
"run",
624+
"--name",
625+
&domain_name,
626+
"--bind-storage-ro",
627+
"--filesystem",
628+
"ext4",
629+
&test_image,
630+
])
631+
.output()
632+
.expect("Failed to run libvirt run with --bind-storage-ro");
633+
634+
let create_stdout = String::from_utf8_lossy(&create_output.stdout);
635+
let create_stderr = String::from_utf8_lossy(&create_output.stderr);
636+
637+
println!("Create stdout: {}", create_stdout);
638+
println!("Create stderr: {}", create_stderr);
639+
640+
if !create_output.status.success() {
641+
cleanup_domain(&domain_name);
642+
panic!(
643+
"Failed to create domain with --bind-storage-ro: {}",
644+
create_stderr
645+
);
646+
}
647+
648+
println!("Successfully created domain: {}", domain_name);
649+
650+
// Check that the domain was created with virtiofs filesystem
651+
println!("Checking domain XML for virtiofs filesystem...");
652+
let dumpxml_output = Command::new("virsh")
653+
.args(&["dumpxml", &domain_name])
654+
.output()
655+
.expect("Failed to dump domain XML");
656+
657+
if !dumpxml_output.status.success() {
658+
cleanup_domain(&domain_name);
659+
let stderr = String::from_utf8_lossy(&dumpxml_output.stderr);
660+
panic!("Failed to dump domain XML: {}", stderr);
661+
}
662+
663+
let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout);
664+
println!(
665+
"Domain XML snippet: {}",
666+
&domain_xml[..std::cmp::min(500, domain_xml.len())]
667+
);
668+
669+
// Verify that the domain XML contains virtiofs configuration
670+
assert!(
671+
domain_xml.contains("type='virtiofs'") || domain_xml.contains("driver type='virtiofs'"),
672+
"Domain XML should contain virtiofs filesystem configuration"
673+
);
674+
675+
// Verify that the filesystem has the correct tag
676+
assert!(
677+
domain_xml.contains("hoststorage") || domain_xml.contains("dir='hoststorage'"),
678+
"Domain XML should reference the hoststorage tag for container storage"
679+
);
680+
681+
// Verify that the domain XML contains readonly element for virtiofs
682+
assert!(
683+
domain_xml.contains("<readonly/>"),
684+
"Domain XML should contain readonly element for --bind-storage-ro"
685+
);
686+
687+
// Check metadata for bind-storage-ro configuration
688+
if domain_xml.contains("bootc:bind-storage-ro") {
689+
assert!(
690+
domain_xml.contains("<bootc:bind-storage-ro>true</bootc:bind-storage-ro>"),
691+
"Domain metadata should indicate bind-storage-ro is enabled"
692+
);
693+
}
694+
695+
println!("✓ Domain XML contains expected virtiofs configuration");
696+
println!("✓ Container storage mount is configured as read-only");
697+
println!("✓ hoststorage tag is present in filesystem configuration");
698+
699+
// Wait for VM to boot and SSH to become available
700+
if let Err(e) = wait_for_ssh_available(&bck, &domain_name, 180) {
701+
cleanup_domain(&domain_name);
702+
panic!("Failed to establish SSH connection: {}", e);
703+
}
704+
705+
// Create mount point and mount virtiofs filesystem
706+
println!("Creating mount point and mounting virtiofs filesystem...");
707+
let mount_setup = Command::new("timeout")
708+
.args([
709+
"30s",
710+
&bck,
711+
"libvirt",
712+
"ssh",
713+
&domain_name,
714+
"--",
715+
"sudo",
716+
"mkdir",
717+
"-p",
718+
"/run/virtiofs-mnt-hoststorage",
719+
])
720+
.output()
721+
.expect("Failed to create mount point");
722+
723+
if !mount_setup.status.success() {
724+
let stderr = String::from_utf8_lossy(&mount_setup.stderr);
725+
println!("Warning: Failed to create mount point: {}", stderr);
726+
}
727+
728+
let mount_cmd = Command::new("timeout")
729+
.args([
730+
"30s",
731+
&bck,
732+
"libvirt",
733+
"ssh",
734+
&domain_name,
735+
"--",
736+
"sudo",
737+
"mount",
738+
"-t",
739+
"virtiofs",
740+
"hoststorage",
741+
"/run/virtiofs-mnt-hoststorage",
742+
])
743+
.output()
744+
.expect("Failed to mount virtiofs");
745+
746+
if !mount_cmd.status.success() {
747+
cleanup_domain(&domain_name);
748+
let stderr = String::from_utf8_lossy(&mount_cmd.stderr);
749+
panic!("Failed to mount virtiofs filesystem: {}", stderr);
750+
}
751+
752+
// Test SSH connection and verify container storage mount inside VM
753+
println!("Testing SSH connection and checking container storage mount...");
754+
let st = Command::new("timeout")
755+
.args([
756+
"60s",
757+
&bck,
758+
"libvirt",
759+
"ssh",
760+
&domain_name,
761+
"--",
762+
"ls",
763+
"-la",
764+
"/run/virtiofs-mnt-hoststorage/overlay",
765+
])
766+
.status()
767+
.expect("Failed to run SSH command to check container storage");
768+
769+
assert!(st.success());
770+
771+
// Verify that the mount is read-only
772+
println!("Verifying that the mount is read-only...");
773+
let ro_test_st = Command::new("timeout")
774+
.args([
775+
"30s",
776+
&bck,
777+
"libvirt",
778+
"ssh",
779+
&domain_name,
780+
"--",
781+
"touch",
782+
"/run/virtiofs-mnt-hoststorage/test-write",
783+
])
784+
.status()
785+
.expect("Failed to run SSH command to test read-only mount");
786+
787+
assert!(
788+
!ro_test_st.success(),
789+
"Mount should be read-only, but write operation succeeded"
790+
);
791+
println!("✓ Mount is correctly configured as read-only.");
792+
793+
// Cleanup domain before completing test
794+
cleanup_domain(&domain_name);
795+
796+
println!("✓ --bind-storage-ro end-to-end test passed");
797+
}
798+
514799
/// Test error handling for invalid configurations
515800
pub fn test_libvirt_error_handling() {
516801
let bck = get_bck_command().unwrap();

crates/kit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ thiserror = "1.0"
2323
rustix = { "version" = "1", features = ["thread", "net", "fs", "pipe", "system", "process", "mount"] }
2424
serde = { version = "1.0.199", features = ["derive"] }
2525
serde_json = "1.0.116"
26+
serde_yaml = "0.9"
2627
tokio = { version = "1", features = ["full"] }
2728
tracing = { workspace = true }
2829
tracing-subscriber = { workspace = true }

0 commit comments

Comments
 (0)