Skip to content

Commit a62bc2b

Browse files
committed
libvirt: Add support for --transient
The use case here is ephemeral VMs for CI tests; we don't want to leak them across host updates and it's more efficient to not create persistent disks for them. Signed-off-by: Colin Walters <walters@verbum.org>
1 parent de51faf commit a62bc2b

5 files changed

Lines changed: 228 additions & 35 deletions

File tree

crates/integration-tests/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ fn main() {
283283
tests::libvirt_verb::test_libvirt_bind_storage_ro();
284284
Ok(())
285285
}),
286+
Trial::test("libvirt_transient_vm", || {
287+
tests::libvirt_verb::test_libvirt_transient_vm();
288+
Ok(())
289+
}),
286290
Trial::test("libvirt_base_disk_creation_and_reuse", || {
287291
tests::libvirt_base_disks::test_base_disk_creation_and_reuse();
288292
Ok(())

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::process::Command;
1212
use crate::{
1313
get_bck_command, get_test_image, run_bcvk, run_bcvk_nocapture, LIBVIRT_INTEGRATION_TEST_LABEL,
1414
};
15+
use bcvk::xml_utils::parse_xml_dom;
1516

1617
/// Test libvirt list functionality (lists domains)
1718
pub fn test_libvirt_list_functionality() {
@@ -807,3 +808,158 @@ pub fn test_libvirt_error_handling() {
807808

808809
println!("libvirt error handling validated");
809810
}
811+
812+
/// Test transient VM functionality
813+
pub fn test_libvirt_transient_vm() {
814+
let bck = get_bck_command().unwrap();
815+
let test_image = get_test_image();
816+
817+
// Generate unique domain name for this test
818+
let domain_name = format!(
819+
"test-transient-{}",
820+
std::time::SystemTime::now()
821+
.duration_since(std::time::UNIX_EPOCH)
822+
.unwrap()
823+
.as_secs()
824+
);
825+
826+
println!("Testing transient VM with domain: {}", domain_name);
827+
828+
// Cleanup any existing domain with this name
829+
cleanup_domain(&domain_name);
830+
831+
// Create transient domain
832+
println!("Creating transient libvirt domain...");
833+
let create_output = run_bcvk(&[
834+
"libvirt",
835+
"run",
836+
"--name",
837+
&domain_name,
838+
"--label",
839+
LIBVIRT_INTEGRATION_TEST_LABEL,
840+
"--transient",
841+
"--filesystem",
842+
"ext4",
843+
&test_image,
844+
])
845+
.expect("Failed to run libvirt run with --transient");
846+
847+
println!("Create stdout: {}", create_output.stdout);
848+
println!("Create stderr: {}", create_output.stderr);
849+
850+
if !create_output.success() {
851+
cleanup_domain(&domain_name);
852+
panic!(
853+
"Failed to create transient domain: {}",
854+
create_output.stderr
855+
);
856+
}
857+
858+
println!("Successfully created transient domain: {}", domain_name);
859+
860+
// Verify domain is transient using virsh dominfo
861+
println!("Verifying domain is marked as transient...");
862+
let dominfo_output = Command::new("virsh")
863+
.args(&["dominfo", &domain_name])
864+
.output()
865+
.expect("Failed to run virsh dominfo");
866+
867+
if !dominfo_output.status.success() {
868+
cleanup_domain(&domain_name);
869+
let stderr = String::from_utf8_lossy(&dominfo_output.stderr);
870+
panic!("Failed to get domain info: {}", stderr);
871+
}
872+
873+
let dominfo = String::from_utf8_lossy(&dominfo_output.stdout);
874+
println!("Domain info:\n{}", dominfo);
875+
876+
// Verify "Persistent: no" appears in dominfo
877+
assert!(
878+
dominfo.contains("Persistent:") && dominfo.contains("no"),
879+
"Domain should be marked as non-persistent (transient). dominfo: {}",
880+
dominfo
881+
);
882+
println!("✓ Domain is correctly marked as transient (Persistent: no)");
883+
884+
// Verify domain XML contains transient disk element
885+
println!("Checking domain XML for transient disk configuration...");
886+
let dumpxml_output = Command::new("virsh")
887+
.args(&["dumpxml", &domain_name])
888+
.output()
889+
.expect("Failed to dump domain XML");
890+
891+
let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout);
892+
893+
// Parse the XML properly using our XML parser
894+
let xml_dom = parse_xml_dom(&domain_xml).expect("Failed to parse domain XML");
895+
896+
// Verify domain XML contains transient disk element
897+
let has_transient = xml_dom.find("transient").is_some();
898+
assert!(
899+
has_transient,
900+
"Domain XML should contain transient disk element"
901+
);
902+
println!("✓ Domain XML contains transient disk element");
903+
904+
// Extract the base disk path from the domain XML using proper XML parsing
905+
let base_disk_path = xml_dom
906+
.find("source")
907+
.and_then(|source_node| source_node.attributes.get("file"))
908+
.map(|s| s.to_string());
909+
910+
println!("Base disk path: {:?}", base_disk_path);
911+
912+
// Stop the domain (this should make it disappear since it's transient)
913+
println!("Stopping transient domain (should disappear)...");
914+
let destroy_output = Command::new("virsh")
915+
.args(&["destroy", &domain_name])
916+
.output()
917+
.expect("Failed to run virsh destroy");
918+
919+
if !destroy_output.status.success() {
920+
let stderr = String::from_utf8_lossy(&destroy_output.stderr);
921+
panic!("Failed to stop domain: {}", stderr);
922+
}
923+
924+
// Poll for domain disappearance with timeout
925+
println!("Verifying domain has disappeared...");
926+
let start_time = std::time::Instant::now();
927+
let timeout = std::time::Duration::from_secs(10);
928+
let mut domain_disappeared = false;
929+
930+
while start_time.elapsed() < timeout {
931+
let list_output = Command::new("virsh")
932+
.args(&["list", "--all", "--name"])
933+
.output()
934+
.expect("Failed to list domains");
935+
936+
let domain_list = String::from_utf8_lossy(&list_output.stdout);
937+
if !domain_list.contains(&domain_name) {
938+
domain_disappeared = true;
939+
break;
940+
}
941+
942+
// Wait briefly before checking again
943+
std::thread::sleep(std::time::Duration::from_millis(200));
944+
}
945+
946+
assert!(
947+
domain_disappeared,
948+
"Transient domain should have disappeared after shutdown within {} seconds",
949+
timeout.as_secs()
950+
);
951+
println!("✓ Transient domain disappeared after shutdown");
952+
953+
// Verify base disk still exists (only the overlay was removed)
954+
if let Some(ref disk_path) = base_disk_path {
955+
println!("Verifying base disk still exists: {}", disk_path);
956+
let disk_exists = std::path::Path::new(disk_path).exists();
957+
assert!(
958+
disk_exists,
959+
"Base disk should still exist after transient domain shutdown"
960+
);
961+
println!("✓ Base disk still exists (not deleted)");
962+
}
963+
964+
println!("✓ Transient VM test passed");
965+
}

crates/kit/src/libvirt/domain.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub struct DomainBuilder {
3131
memory: Option<u64>, // in MB
3232
vcpus: Option<u32>,
3333
disk_path: Option<String>,
34+
transient_disk: bool, // Use transient disk with temporary overlay
3435
network: Option<String>,
3536
vnc_port: Option<u16>,
3637
kernel_args: Option<String>,
@@ -58,6 +59,7 @@ impl DomainBuilder {
5859
memory: None,
5960
vcpus: None,
6061
disk_path: None,
62+
transient_disk: false,
6163
network: None,
6264
vnc_port: None,
6365
kernel_args: None,
@@ -95,6 +97,12 @@ impl DomainBuilder {
9597
self
9698
}
9799

100+
/// Enable transient disk (changes written to temporary overlay)
101+
pub fn with_transient_disk(mut self, transient: bool) -> Self {
102+
self.transient_disk = transient;
103+
self
104+
}
105+
98106
/// Set network configuration
99107
pub fn with_network(mut self, network: &str) -> Self {
100108
self.network = Some(network.to_string());
@@ -305,6 +313,9 @@ impl DomainBuilder {
305313
writer.write_empty_element("driver", &[("name", "qemu"), ("type", disk_type)])?;
306314
writer.write_empty_element("source", &[("file", disk_path)])?;
307315
writer.write_empty_element("target", &[("dev", "vda"), ("bus", "virtio")])?;
316+
if self.transient_disk {
317+
writer.write_empty_element("transient", &[])?;
318+
}
308319
writer.end_element("disk")?;
309320
}
310321

crates/kit/src/libvirt/run.rs

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
//! libvirt run command - run a bootable container as a persistent VM
1+
//! libvirt run command - run a bootable container as a VM
22
//!
33
//! This module provides the core functionality for creating and managing
4-
//! libvirt-based VMs from bootc container images.
4+
//! libvirt-based VMs from bootc container images. Supports both persistent
5+
//! VMs (survive shutdown) and transient VMs (disappear on shutdown).
56
67
use camino::{Utf8Path, Utf8PathBuf};
78
use clap::{Parser, ValueEnum};
@@ -26,6 +27,19 @@ pub(super) fn virsh_command(connect_uri: Option<&str>) -> Result<std::process::C
2627
Ok(cmd)
2728
}
2829

30+
/// Run a virsh command and handle errors consistently
31+
pub(crate) fn run_virsh_cmd(connect_uri: Option<&str>, args: &[&str], err_msg: &str) -> Result<()> {
32+
let output = virsh_command(connect_uri)?
33+
.args(args)
34+
.output()
35+
.with_context(|| format!("Failed to run virsh command: {:?}", args))?;
36+
if !output.status.success() {
37+
let stderr = String::from_utf8_lossy(&output.stderr);
38+
return Err(color_eyre::eyre::eyre!("{}: {}", err_msg, stderr));
39+
}
40+
Ok(())
41+
}
42+
2943
/// Firmware type for virtual machines
3044
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
3145
#[clap(rename_all = "kebab-case")]
@@ -102,6 +116,10 @@ pub struct LibvirtRunOpts {
102116
/// User-defined labels for organizing VMs (comma not allowed in labels)
103117
#[clap(long)]
104118
pub label: Vec<String>,
119+
120+
/// Create a transient VM that disappears on shutdown/reboot
121+
#[clap(long)]
122+
pub transient: bool,
105123
}
106124

107125
impl LibvirtRunOpts {
@@ -168,12 +186,17 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRunOpts) -
168186

169187
println!("Using base disk image: {}", base_disk_path);
170188

171-
// Phase 2: Clone the base disk to create a VM-specific disk
172-
let disk_path =
173-
crate::libvirt::base_disks::clone_from_base(&base_disk_path, &vm_name, connect_uri)
174-
.with_context(|| "Failed to clone VM disk from base")?;
175-
176-
println!("Created VM disk: {}", disk_path);
189+
// Phase 2: Clone the base disk to create a VM-specific disk (or use base directly if transient)
190+
let disk_path = if opts.transient {
191+
println!("Transient mode: using base disk directly with overlay");
192+
base_disk_path
193+
} else {
194+
let cloned_disk =
195+
crate::libvirt::base_disks::clone_from_base(&base_disk_path, &vm_name, connect_uri)
196+
.with_context(|| "Failed to clone VM disk from base")?;
197+
println!("Created VM disk: {}", cloned_disk);
198+
cloned_disk
199+
};
177200

178201
// Phase 3: Create libvirt domain
179202
println!("Creating libvirt domain...");
@@ -642,6 +665,7 @@ fn create_libvirt_domain_from_disk(
642665
.with_memory(memory.into())
643666
.with_vcpus(opts.cpus)
644667
.with_disk(disk_path.as_str())
668+
.with_transient_disk(opts.transient)
645669
.with_network("none") // Use QEMU args for SSH networking instead
646670
.with_firmware(opts.firmware)
647671
.with_tpm(!opts.disable_tpm)
@@ -747,34 +771,28 @@ fn create_libvirt_domain_from_disk(
747771
let xml_path = format!("/tmp/{}.xml", domain_name);
748772
std::fs::write(&xml_path, domain_xml).with_context(|| "Failed to write domain XML")?;
749773

750-
// Define the domain
751-
let output = global_opts
752-
.virsh_command()
753-
.args(&["define", &xml_path])
754-
.output()
755-
.with_context(|| "Failed to run virsh define")?;
756-
757-
if !output.status.success() {
758-
let stderr = String::from_utf8_lossy(&output.stderr);
759-
return Err(color_eyre::eyre::eyre!(
760-
"Failed to define libvirt domain: {}",
761-
stderr
762-
));
763-
}
764-
765-
// Start the domain by default (compatibility)
766-
let output = global_opts
767-
.virsh_command()
768-
.args(&["start", domain_name])
769-
.output()
770-
.with_context(|| "Failed to start domain")?;
774+
let connect_uri = global_opts.connect.as_deref();
771775

772-
if !output.status.success() {
773-
let stderr = String::from_utf8_lossy(&output.stderr);
774-
return Err(color_eyre::eyre::eyre!(
775-
"Failed to start libvirt domain: {}",
776-
stderr
777-
));
776+
// Create domain (transient or persistent)
777+
if opts.transient {
778+
// Create transient domain (single command - domain disappears on shutdown)
779+
run_virsh_cmd(
780+
connect_uri,
781+
&["create", &xml_path],
782+
"Failed to create transient libvirt domain",
783+
)?;
784+
} else {
785+
// Define and start the domain (persistent)
786+
run_virsh_cmd(
787+
connect_uri,
788+
&["define", &xml_path],
789+
"Failed to define libvirt domain",
790+
)?;
791+
run_virsh_cmd(
792+
connect_uri,
793+
&["start", domain_name],
794+
"Failed to start libvirt domain",
795+
)?;
778796
}
779797

780798
// Clean up temporary XML file

docs/src/man/bcvk-libvirt-run.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ Run a bootable container as a persistent VM
106106

107107
User-defined labels for organizing VMs (comma not allowed in labels)
108108

109+
**--transient**
110+
111+
Create a transient VM that disappears on shutdown/reboot
112+
109113
<!-- END GENERATED OPTIONS -->
110114

111115
# EXAMPLES

0 commit comments

Comments
 (0)