diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index 55d355c..69b1920 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -283,6 +283,10 @@ fn main() { tests::libvirt_verb::test_libvirt_bind_storage_ro(); Ok(()) }), + Trial::test("libvirt_transient_vm", || { + tests::libvirt_verb::test_libvirt_transient_vm(); + Ok(()) + }), Trial::test("libvirt_base_disk_creation_and_reuse", || { tests::libvirt_base_disks::test_base_disk_creation_and_reuse(); Ok(()) diff --git a/crates/integration-tests/src/tests/libvirt_verb.rs b/crates/integration-tests/src/tests/libvirt_verb.rs index cf164ff..b405ebf 100644 --- a/crates/integration-tests/src/tests/libvirt_verb.rs +++ b/crates/integration-tests/src/tests/libvirt_verb.rs @@ -12,6 +12,7 @@ use std::process::Command; use crate::{ get_bck_command, get_test_image, run_bcvk, run_bcvk_nocapture, LIBVIRT_INTEGRATION_TEST_LABEL, }; +use bcvk::xml_utils::parse_xml_dom; /// Test libvirt list functionality (lists domains) pub fn test_libvirt_list_functionality() { @@ -807,3 +808,157 @@ pub fn test_libvirt_error_handling() { println!("libvirt error handling validated"); } + +/// Test transient VM functionality +pub fn test_libvirt_transient_vm() { + let test_image = get_test_image(); + + // Generate unique domain name for this test + let domain_name = format!( + "test-transient-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + + println!("Testing transient VM with domain: {}", domain_name); + + // Cleanup any existing domain with this name + cleanup_domain(&domain_name); + + // Create transient domain + println!("Creating transient libvirt domain..."); + let create_output = run_bcvk(&[ + "libvirt", + "run", + "--name", + &domain_name, + "--label", + LIBVIRT_INTEGRATION_TEST_LABEL, + "--transient", + "--filesystem", + "ext4", + &test_image, + ]) + .expect("Failed to run libvirt run with --transient"); + + println!("Create stdout: {}", create_output.stdout); + println!("Create stderr: {}", create_output.stderr); + + if !create_output.success() { + cleanup_domain(&domain_name); + panic!( + "Failed to create transient domain: {}", + create_output.stderr + ); + } + + println!("Successfully created transient domain: {}", domain_name); + + // Verify domain is transient using virsh dominfo + println!("Verifying domain is marked as transient..."); + let dominfo_output = Command::new("virsh") + .args(&["dominfo", &domain_name]) + .output() + .expect("Failed to run virsh dominfo"); + + if !dominfo_output.status.success() { + cleanup_domain(&domain_name); + let stderr = String::from_utf8_lossy(&dominfo_output.stderr); + panic!("Failed to get domain info: {}", stderr); + } + + let dominfo = String::from_utf8_lossy(&dominfo_output.stdout); + println!("Domain info:\n{}", dominfo); + + // Verify "Persistent: no" appears in dominfo + assert!( + dominfo.contains("Persistent:") && dominfo.contains("no"), + "Domain should be marked as non-persistent (transient). dominfo: {}", + dominfo + ); + println!("✓ Domain is correctly marked as transient (Persistent: no)"); + + // Verify domain XML contains transient disk element + println!("Checking domain XML for transient disk configuration..."); + let dumpxml_output = Command::new("virsh") + .args(&["dumpxml", &domain_name]) + .output() + .expect("Failed to dump domain XML"); + + let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout); + + // Parse the XML properly using our XML parser + let xml_dom = parse_xml_dom(&domain_xml).expect("Failed to parse domain XML"); + + // Verify domain XML contains transient disk element + let has_transient = xml_dom.find("transient").is_some(); + assert!( + has_transient, + "Domain XML should contain transient disk element" + ); + println!("✓ Domain XML contains transient disk element"); + + // Extract the base disk path from the domain XML using proper XML parsing + let base_disk_path = xml_dom + .find("source") + .and_then(|source_node| source_node.attributes.get("file")) + .map(|s| s.to_string()); + + println!("Base disk path: {:?}", base_disk_path); + + // Stop the domain (this should make it disappear since it's transient) + println!("Stopping transient domain (should disappear)..."); + let destroy_output = Command::new("virsh") + .args(&["destroy", &domain_name]) + .output() + .expect("Failed to run virsh destroy"); + + if !destroy_output.status.success() { + let stderr = String::from_utf8_lossy(&destroy_output.stderr); + panic!("Failed to stop domain: {}", stderr); + } + + // Poll for domain disappearance with timeout + println!("Verifying domain has disappeared..."); + let start_time = std::time::Instant::now(); + let timeout = std::time::Duration::from_secs(10); + let mut domain_disappeared = false; + + while start_time.elapsed() < timeout { + let list_output = Command::new("virsh") + .args(&["list", "--all", "--name"]) + .output() + .expect("Failed to list domains"); + + let domain_list = String::from_utf8_lossy(&list_output.stdout); + if !domain_list.contains(&domain_name) { + domain_disappeared = true; + break; + } + + // Wait briefly before checking again + std::thread::sleep(std::time::Duration::from_millis(200)); + } + + assert!( + domain_disappeared, + "Transient domain should have disappeared after shutdown within {} seconds", + timeout.as_secs() + ); + println!("✓ Transient domain disappeared after shutdown"); + + // Verify base disk still exists (only the overlay was removed) + if let Some(ref disk_path) = base_disk_path { + println!("Verifying base disk still exists: {}", disk_path); + let disk_exists = std::path::Path::new(disk_path).exists(); + assert!( + disk_exists, + "Base disk should still exist after transient domain shutdown" + ); + println!("✓ Base disk still exists (not deleted)"); + } + + println!("✓ Transient VM test passed"); +} diff --git a/crates/kit/src/libvirt/domain.rs b/crates/kit/src/libvirt/domain.rs index da0d64e..cc5770f 100644 --- a/crates/kit/src/libvirt/domain.rs +++ b/crates/kit/src/libvirt/domain.rs @@ -31,6 +31,7 @@ pub struct DomainBuilder { memory: Option, // in MB vcpus: Option, disk_path: Option, + transient_disk: bool, // Use transient disk with temporary overlay network: Option, vnc_port: Option, kernel_args: Option, @@ -58,6 +59,7 @@ impl DomainBuilder { memory: None, vcpus: None, disk_path: None, + transient_disk: false, network: None, vnc_port: None, kernel_args: None, @@ -95,6 +97,12 @@ impl DomainBuilder { self } + /// Enable transient disk (creates temporary overlay, base disk opened read-only) + pub fn with_transient_disk(mut self, transient: bool) -> Self { + self.transient_disk = transient; + self + } + /// Set network configuration pub fn with_network(mut self, network: &str) -> Self { self.network = Some(network.to_string()); @@ -305,6 +313,11 @@ impl DomainBuilder { writer.write_empty_element("driver", &[("name", "qemu"), ("type", disk_type)])?; writer.write_empty_element("source", &[("file", disk_path)])?; writer.write_empty_element("target", &[("dev", "vda"), ("bus", "virtio")])?; + if self.transient_disk { + // shareBacking='yes' allows multiple VMs to share the backing image + // Libvirt creates a temporary QCOW2 overlay for writes + writer.write_empty_element("transient", &[("shareBacking", "yes")])?; + } writer.end_element("disk")?; } diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs index 3380258..8606d78 100644 --- a/crates/kit/src/libvirt/run.rs +++ b/crates/kit/src/libvirt/run.rs @@ -1,7 +1,8 @@ -//! libvirt run command - run a bootable container as a persistent VM +//! libvirt run command - run a bootable container as a VM //! //! This module provides the core functionality for creating and managing -//! libvirt-based VMs from bootc container images. +//! libvirt-based VMs from bootc container images. Supports both persistent +//! VMs (survive shutdown) and transient VMs (disappear on shutdown). use camino::{Utf8Path, Utf8PathBuf}; use clap::{Parser, ValueEnum}; @@ -26,6 +27,19 @@ pub(super) fn virsh_command(connect_uri: Option<&str>) -> Result, args: &[&str], err_msg: &str) -> Result<()> { + let output = virsh_command(connect_uri)? + .args(args) + .output() + .with_context(|| format!("Failed to run virsh command: {:?}", args))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(color_eyre::eyre::eyre!("{}: {}", err_msg, stderr)); + } + Ok(()) +} + /// Firmware type for virtual machines #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] #[clap(rename_all = "kebab-case")] @@ -102,6 +116,10 @@ pub struct LibvirtRunOpts { /// User-defined labels for organizing VMs (comma not allowed in labels) #[clap(long)] pub label: Vec, + + /// Create a transient VM that disappears on shutdown/reboot + #[clap(long)] + pub transient: bool, } impl LibvirtRunOpts { @@ -168,12 +186,17 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRunOpts) - println!("Using base disk image: {}", base_disk_path); - // Phase 2: Clone the base disk to create a VM-specific disk - let disk_path = - crate::libvirt::base_disks::clone_from_base(&base_disk_path, &vm_name, connect_uri) - .with_context(|| "Failed to clone VM disk from base")?; - - println!("Created VM disk: {}", disk_path); + // Phase 2: Clone the base disk to create a VM-specific disk (or use base directly if transient) + let disk_path = if opts.transient { + println!("Transient mode: using base disk directly with overlay"); + base_disk_path + } else { + let cloned_disk = + crate::libvirt::base_disks::clone_from_base(&base_disk_path, &vm_name, connect_uri) + .with_context(|| "Failed to clone VM disk from base")?; + println!("Created VM disk: {}", cloned_disk); + cloned_disk + }; // Phase 3: Create libvirt domain println!("Creating libvirt domain..."); @@ -642,6 +665,7 @@ fn create_libvirt_domain_from_disk( .with_memory(memory.into()) .with_vcpus(opts.cpus) .with_disk(disk_path.as_str()) + .with_transient_disk(opts.transient) .with_network("none") // Use QEMU args for SSH networking instead .with_firmware(opts.firmware) .with_tpm(!opts.disable_tpm) @@ -747,34 +771,28 @@ fn create_libvirt_domain_from_disk( let xml_path = format!("/tmp/{}.xml", domain_name); std::fs::write(&xml_path, domain_xml).with_context(|| "Failed to write domain XML")?; - // Define the domain - let output = global_opts - .virsh_command() - .args(&["define", &xml_path]) - .output() - .with_context(|| "Failed to run virsh define")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(color_eyre::eyre::eyre!( - "Failed to define libvirt domain: {}", - stderr - )); - } - - // Start the domain by default (compatibility) - let output = global_opts - .virsh_command() - .args(&["start", domain_name]) - .output() - .with_context(|| "Failed to start domain")?; + let connect_uri = global_opts.connect.as_deref(); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(color_eyre::eyre::eyre!( - "Failed to start libvirt domain: {}", - stderr - )); + // Create domain (transient or persistent) + if opts.transient { + // Create transient domain (single command - domain disappears on shutdown) + run_virsh_cmd( + connect_uri, + &["create", &xml_path], + "Failed to create transient libvirt domain", + )?; + } else { + // Define and start the domain (persistent) + run_virsh_cmd( + connect_uri, + &["define", &xml_path], + "Failed to define libvirt domain", + )?; + run_virsh_cmd( + connect_uri, + &["start", domain_name], + "Failed to start libvirt domain", + )?; } // Clean up temporary XML file diff --git a/docs/src/man/bcvk-libvirt-run.md b/docs/src/man/bcvk-libvirt-run.md index b2ffe8a..798cbdc 100644 --- a/docs/src/man/bcvk-libvirt-run.md +++ b/docs/src/man/bcvk-libvirt-run.md @@ -106,6 +106,10 @@ Run a bootable container as a persistent VM User-defined labels for organizing VMs (comma not allowed in labels) +**--transient** + + Create a transient VM that disappears on shutdown/reboot + # EXAMPLES