diff --git a/crates/integration-tests/src/tests/libvirt_verb.rs b/crates/integration-tests/src/tests/libvirt_verb.rs index 95ba0fc..dd7d642 100644 --- a/crates/integration-tests/src/tests/libvirt_verb.rs +++ b/crates/integration-tests/src/tests/libvirt_verb.rs @@ -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<()> { diff --git a/crates/kit/src/libvirt/rm.rs b/crates/kit/src/libvirt/rm.rs index 3078d41..cf742de 100644 --- a/crates/kit/src/libvirt/rm.rs +++ b/crates/kit/src/libvirt/rm.rs @@ -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 { + 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 { @@ -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<()> { @@ -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.", @@ -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 @@ -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) @@ -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, )?;