Skip to content

Commit 346ac16

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 <[email protected]>
1 parent d2d89c0 commit 346ac16

File tree

5 files changed

+229
-35
lines changed

5 files changed

+229
-35
lines changed

crates/integration-tests/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ fn main() {
287287
tests::libvirt_verb::test_libvirt_bind_storage_ro();
288288
Ok(())
289289
}),
290+
Trial::test("libvirt_transient_vm", || {
291+
tests::libvirt_verb::test_libvirt_transient_vm();
292+
Ok(())
293+
}),
290294
Trial::test("libvirt_base_disk_creation_and_reuse", || {
291295
tests::libvirt_base_disks::test_base_disk_creation_and_reuse();
292296
Ok(())

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

Lines changed: 155 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() {
@@ -941,3 +942,157 @@ pub fn test_libvirt_error_handling() {
941942

942943
println!("libvirt error handling validated");
943944
}
945+
946+
/// Test transient VM functionality
947+
pub fn test_libvirt_transient_vm() {
948+
let test_image = get_test_image();
949+
950+
// Generate unique domain name for this test
951+
let domain_name = format!(
952+
"test-transient-{}",
953+
std::time::SystemTime::now()
954+
.duration_since(std::time::UNIX_EPOCH)
955+
.unwrap()
956+
.as_secs()
957+
);
958+
959+
println!("Testing transient VM with domain: {}", domain_name);
960+
961+
// Cleanup any existing domain with this name
962+
cleanup_domain(&domain_name);
963+
964+
// Create transient domain
965+
println!("Creating transient libvirt domain...");
966+
let create_output = run_bcvk(&[
967+
"libvirt",
968+
"run",
969+
"--name",
970+
&domain_name,
971+
"--label",
972+
LIBVIRT_INTEGRATION_TEST_LABEL,
973+
"--transient",
974+
"--filesystem",
975+
"ext4",
976+
&test_image,
977+
])
978+
.expect("Failed to run libvirt run with --transient");
979+
980+
println!("Create stdout: {}", create_output.stdout);
981+
println!("Create stderr: {}", create_output.stderr);
982+
983+
if !create_output.success() {
984+
cleanup_domain(&domain_name);
985+
panic!(
986+
"Failed to create transient domain: {}",
987+
create_output.stderr
988+
);
989+
}
990+
991+
println!("Successfully created transient domain: {}", domain_name);
992+
993+
// Verify domain is transient using virsh dominfo
994+
println!("Verifying domain is marked as transient...");
995+
let dominfo_output = Command::new("virsh")
996+
.args(&["dominfo", &domain_name])
997+
.output()
998+
.expect("Failed to run virsh dominfo");
999+
1000+
if !dominfo_output.status.success() {
1001+
cleanup_domain(&domain_name);
1002+
let stderr = String::from_utf8_lossy(&dominfo_output.stderr);
1003+
panic!("Failed to get domain info: {}", stderr);
1004+
}
1005+
1006+
let dominfo = String::from_utf8_lossy(&dominfo_output.stdout);
1007+
println!("Domain info:\n{}", dominfo);
1008+
1009+
// Verify "Persistent: no" appears in dominfo
1010+
assert!(
1011+
dominfo.contains("Persistent:") && dominfo.contains("no"),
1012+
"Domain should be marked as non-persistent (transient). dominfo: {}",
1013+
dominfo
1014+
);
1015+
println!("✓ Domain is correctly marked as transient (Persistent: no)");
1016+
1017+
// Verify domain XML contains transient disk element
1018+
println!("Checking domain XML for transient disk configuration...");
1019+
let dumpxml_output = Command::new("virsh")
1020+
.args(&["dumpxml", &domain_name])
1021+
.output()
1022+
.expect("Failed to dump domain XML");
1023+
1024+
let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout);
1025+
1026+
// Parse the XML properly using our XML parser
1027+
let xml_dom = parse_xml_dom(&domain_xml).expect("Failed to parse domain XML");
1028+
1029+
// Verify domain XML contains transient disk element
1030+
let has_transient = xml_dom.find("transient").is_some();
1031+
assert!(
1032+
has_transient,
1033+
"Domain XML should contain transient disk element"
1034+
);
1035+
println!("✓ Domain XML contains transient disk element");
1036+
1037+
// Extract the base disk path from the domain XML using proper XML parsing
1038+
let base_disk_path = xml_dom
1039+
.find("source")
1040+
.and_then(|source_node| source_node.attributes.get("file"))
1041+
.map(|s| s.to_string());
1042+
1043+
println!("Base disk path: {:?}", base_disk_path);
1044+
1045+
// Stop the domain (this should make it disappear since it's transient)
1046+
println!("Stopping transient domain (should disappear)...");
1047+
let destroy_output = Command::new("virsh")
1048+
.args(&["destroy", &domain_name])
1049+
.output()
1050+
.expect("Failed to run virsh destroy");
1051+
1052+
if !destroy_output.status.success() {
1053+
let stderr = String::from_utf8_lossy(&destroy_output.stderr);
1054+
panic!("Failed to stop domain: {}", stderr);
1055+
}
1056+
1057+
// Poll for domain disappearance with timeout
1058+
println!("Verifying domain has disappeared...");
1059+
let start_time = std::time::Instant::now();
1060+
let timeout = std::time::Duration::from_secs(10);
1061+
let mut domain_disappeared = false;
1062+
1063+
while start_time.elapsed() < timeout {
1064+
let list_output = Command::new("virsh")
1065+
.args(&["list", "--all", "--name"])
1066+
.output()
1067+
.expect("Failed to list domains");
1068+
1069+
let domain_list = String::from_utf8_lossy(&list_output.stdout);
1070+
if !domain_list.contains(&domain_name) {
1071+
domain_disappeared = true;
1072+
break;
1073+
}
1074+
1075+
// Wait briefly before checking again
1076+
std::thread::sleep(std::time::Duration::from_millis(200));
1077+
}
1078+
1079+
assert!(
1080+
domain_disappeared,
1081+
"Transient domain should have disappeared after shutdown within {} seconds",
1082+
timeout.as_secs()
1083+
);
1084+
println!("✓ Transient domain disappeared after shutdown");
1085+
1086+
// Verify base disk still exists (only the overlay was removed)
1087+
if let Some(ref disk_path) = base_disk_path {
1088+
println!("Verifying base disk still exists: {}", disk_path);
1089+
let disk_exists = std::path::Path::new(disk_path).exists();
1090+
assert!(
1091+
disk_exists,
1092+
"Base disk should still exist after transient domain shutdown"
1093+
);
1094+
println!("✓ Base disk still exists (not deleted)");
1095+
}
1096+
1097+
println!("✓ Transient VM test passed");
1098+
}

crates/kit/src/libvirt/domain.rs

Lines changed: 13 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 (creates temporary overlay, base disk opened read-only)
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,11 @@ 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+
// shareBacking='yes' allows multiple VMs to share the backing image
318+
// Libvirt creates a temporary QCOW2 overlay for writes
319+
writer.write_empty_element("transient", &[("shareBacking", "yes")])?;
320+
}
308321
writer.end_element("disk")?;
309322
}
310323

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)