Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions crates/integration-tests/src/tests/libvirt_verb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,128 @@ fn test_libvirt_run_transient_vm() -> Result<()> {
}
integration_test!(test_libvirt_run_transient_vm);

/// Test transient VM with --replace functionality
///
/// This tests that `bcvk libvirt run --transient --replace` works correctly:
/// 1. Create a transient VM
/// 2. Replace it with another transient VM using --replace
/// 3. Verify the replacement works (no errors about undefine on transient domains)
fn test_libvirt_run_transient_replace() -> Result<()> {
let test_image = get_test_image();

// Generate unique domain name for this test
let domain_name = format!("test-transient-replace-{}", random_suffix());

println!(
"Testing transient VM with --replace, domain: {}",
domain_name
);

// Cleanup any existing domain with this name
cleanup_domain(&domain_name);

// Create initial transient domain
println!("Creating initial transient 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");

if !create_output.success() {
cleanup_domain(&domain_name);
panic!(
"Failed to create initial transient domain: {}",
create_output.stderr
);
}
println!("✓ Initial transient domain created: {}", domain_name);

// Verify domain is transient
let dominfo_output = Command::new("virsh")
.args(&["dominfo", &domain_name])
.output()
.expect("Failed to run virsh dominfo");

let dominfo = String::from_utf8_lossy(&dominfo_output.stdout);
assert!(
dominfo.contains("Persistent:") && dominfo.contains("no"),
"Domain should be transient. dominfo: {}",
dominfo
);
println!("✓ Initial domain is transient (Persistent: no)");

// Now replace the transient domain with another transient domain
println!("Replacing transient domain with --transient --replace...");
let replace_output = run_bcvk(&[
"libvirt",
"run",
"--name",
&domain_name,
"--label",
LIBVIRT_INTEGRATION_TEST_LABEL,
"--transient",
"--replace",
"--filesystem",
"ext4",
&test_image,
])
.expect("Failed to run libvirt run with --transient --replace");

println!("Replace stdout: {}", replace_output.stdout);
println!("Replace stderr: {}", replace_output.stderr);

if !replace_output.success() {
cleanup_domain(&domain_name);
panic!(
"Failed to replace transient domain: {}",
replace_output.stderr
);
}
println!("✓ Successfully replaced transient domain");

// Verify the new domain exists and is transient
let dominfo_output = Command::new("virsh")
.args(&["dominfo", &domain_name])
.output()
.expect("Failed to run virsh dominfo after replace");

if !dominfo_output.status.success() {
cleanup_domain(&domain_name);
panic!("Domain should exist after --transient --replace");
}

let dominfo = String::from_utf8_lossy(&dominfo_output.stdout);
assert!(
dominfo.contains("Persistent:") && dominfo.contains("no"),
"Replaced domain should still be transient. dominfo: {}",
dominfo
);
println!("✓ Replaced domain is transient (Persistent: no)");

// Verify it's running
assert!(
dominfo.contains("running") || dominfo.contains("idle"),
"Replaced transient domain should be running. dominfo: {}",
dominfo
);
println!("✓ Replaced transient domain is running");

// Cleanup
cleanup_domain(&domain_name);
println!("✓ Transient --replace test passed");
Ok(())
}
integration_test!(test_libvirt_run_transient_replace);

/// Test automatic bind mount functionality with systemd mount units
/// Also validates kernel argument (--karg) functionality
fn test_libvirt_run_bind_mounts() -> Result<()> {
Expand Down
68 changes: 64 additions & 4 deletions crates/kit/src/libvirt/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@
use clap::Parser;
use color_eyre::Result;

/// Check if a domain is persistent (vs transient)
///
/// Returns true if the domain is persistent, false if transient.
/// Transient domains disappear when destroyed, so they don't need undefine.
fn is_domain_persistent(
global_opts: &crate::libvirt::LibvirtOptions,
vm_name: &str,
) -> Result<bool> {
let output = global_opts
.virsh_command()
.args(&["dominfo", vm_name])
.output()
.map_err(|e| color_eyre::eyre::eyre!("Failed to get domain info: {}", e))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(color_eyre::eyre::eyre!(
"Failed to get domain info for '{}': {}",
vm_name,
stderr
));
}

let stdout = String::from_utf8_lossy(&output.stdout);
// Look for "Persistent: yes" or "Persistent: no"
for line in stdout.lines() {
if let Some(value) = line.strip_prefix("Persistent:") {
return Ok(value.trim() == "yes");
}
}

// Default to persistent if we can't determine
Ok(true)
}

/// Options for removing a libvirt domain
#[derive(Debug, Parser)]
pub struct LibvirtRmOpts {
Expand All @@ -29,6 +64,7 @@ fn remove_vm_impl(
global_opts: &crate::libvirt::LibvirtOptions,
vm_name: &str,
state: &str,
is_persistent: bool,
domain_info: &crate::domain_list::PodmanBootcDomain,
stop_if_running: bool,
) -> Result<()> {
Expand All @@ -51,6 +87,11 @@ fn remove_vm_impl(
stderr
));
}

// Transient VMs disappear after destroy, so we're done
if !is_persistent {
return Ok(());
}
} else {
return Err(color_eyre::eyre::eyre!(
"VM '{}' is running. Cannot remove without stopping.",
Expand Down Expand Up @@ -104,16 +145,31 @@ pub fn remove_vm_forced(
};

// Check if domain exists and get its state
let state = lister
.get_domain_state(vm_name)
.map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", vm_name))?;
let state = match lister.get_domain_state(vm_name) {
Ok(s) => s,
Err(_) => {
// Domain doesn't exist - this is OK for replace scenarios
// where a transient VM was already destroyed
return Ok(());
}
};

// Check if domain is persistent (transient VMs disappear after destroy)
let is_persistent = is_domain_persistent(global_opts, vm_name)?;

// Get domain info for disk cleanup
let domain_info = lister
.get_domain_info(vm_name)
.with_context(|| format!("Failed to get info for VM '{}'", vm_name))?;

remove_vm_impl(global_opts, vm_name, &state, &domain_info, stop_if_running)
remove_vm_impl(
global_opts,
vm_name,
&state,
is_persistent,
&domain_info,
stop_if_running,
)
}

/// Execute the libvirt rm command
Expand All @@ -132,6 +188,9 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRmOpts) ->
.get_domain_state(&opts.name)
.map_err(|_| color_eyre::eyre::eyre!("VM '{}' not found", opts.name))?;

// Check if domain is persistent (transient VMs disappear after destroy)
let is_persistent = is_domain_persistent(global_opts, &opts.name)?;

// Get domain info for display
let domain_info = lister
.get_domain_info(&opts.name)
Expand Down Expand Up @@ -175,6 +234,7 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRmOpts) ->
global_opts,
&opts.name,
&state,
is_persistent,
&domain_info,
opts.stop || opts.force,
)?;
Expand Down