diff --git a/.editorconfig b/.editorconfig index b1a8ad6..9945d04 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,25 +5,25 @@ root = true [*] -end_of_line = lf insert_final_newline = true -charset = utf-8 trim_trailing_whitespace = true -indent_style = space -indent_size = 4 +charset = utf-8 + +[*.md] +max_line_length = 78 +trim_trailing_whitespace = false [*.rs] -indent_size = 4 +indent_style = space +indent_size = 4 max_line_length = 100 [*.toml] indent_size = 2 [*.{yml,yaml}] -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false +indent_style = space +indent_size = 2 [Makefile] indent_style = tab diff --git a/Cargo.lock b/Cargo.lock index 947c832..b0357dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,52 +114,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "env_logger" -version = "0.10.2" +name = "env_filter" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", ] [[package]] -name = "heck" -version = "0.5.0" +name = "env_logger" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] [[package]] -name = "hermit-abi" +name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "humantime" -version = "2.2.0" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] -name = "is-terminal" -version = "0.4.16" +name = "jiff" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "jiff-static" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "kvm-install-vm" @@ -203,6 +214,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -250,6 +276,26 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.11.1" @@ -267,15 +313,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "unicode-ident" version = "1.0.18" @@ -296,9 +333,9 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" [[package]] name = "virt" -version = "0.3.2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5408c59dc1b3383e0da017a85a6799dd6e3d52790849ececadbf403513743fa6" +checksum = "77a05f77c836efa9be343b5419663cf829d75203b813579993cdd9c44f51767e" dependencies = [ "libc", "uuid", @@ -307,23 +344,14 @@ dependencies = [ [[package]] name = "virt-sys" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d9a603af8e27b33f1c8d721cb5caafcf77810c79e6ea02d2436906a14683fd" +checksum = "c504e459878f09177f41bf2f8bb3e9a8af4fca7a09e73152fee02535d501601c" dependencies = [ "libc", "pkg-config", ] -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys", -] - [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index b7d7c93..9e5c0cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,10 @@ repository = "https://github.com/giovtorres/kvm-install-vm" [dependencies] anyhow = "1.0" -clap = { version = "4.4", features = ["derive"] } -env_logger = "0.10" +clap = { version = "4.5", features = ["derive"] } +env_logger = "0.11" log = "0.4" -virt = "0.3.1" +virt = "0.4" [build-dependencies] pkg-config = "0.3" \ No newline at end of file diff --git a/build.rs b/build.rs index d3bf350..c185134 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,6 @@ fn main() { println!("cargo:rustc-link-lib=virt"); - + // // Try using pkg-config to find libvirt // match pkg_config::probe_library("libvirt") { // Ok(_) => println!("Found libvirt via pkg-config"), diff --git a/src/cli.rs b/src/cli.rs index 3ba2c9e..7f800da 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,26 +1,40 @@ -use clap::Parser; +use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] -#[command(author, version, about)] pub struct Cli { - #[arg(short = 'n', long)] - pub name: String, + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + Create { + #[arg(short = 'n', long)] + name: String, + + #[arg(short = 't', long, default_value = "centos8")] + distro: String, - #[arg(short = 't', long, default_value = "centos8")] - pub distro: String, + #[arg(short = 'c', long, default_value_t = 1)] + vcpus: u32, - #[arg(short = 'c', long, default_value_t = 1)] - pub vcpus: u32, + #[arg(short = 'm', long, default_value_t = 1024)] + memory_mb: u32, - #[arg(short = 'm', long, default_value_t = 1024)] - pub memory_mb: u32, + #[arg(short = 'd', long, default_value_t = 10)] + disk_size_gb: u32, - #[arg(short = 'd', long, default_value_t = 10)] - pub disk_size_gb: u32, + #[arg(long)] + graphics: bool, - #[arg(long)] - pub graphics: bool, + #[arg(long)] + dry_run: bool, + }, + Destroy { + #[arg(short = 'n', long)] + name: String, - #[arg(long)] - pub dry_run: bool, + #[arg(short = 'r', long)] + remove_disk: bool, + }, } diff --git a/src/main.rs b/src/main.rs index 7e28351..d18acb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use clap::Parser; -use kvm_install_vm::{Cli, vm::VirtualMachine}; +use kvm_install_vm::{Cli, cli::Commands, vm::VirtualMachine}; use std::process; fn main() { @@ -7,42 +7,68 @@ fn main() { env_logger::init(); // Parse command line arguments - let args = Cli::parse(); - - println!("Starting kvm-install-vm Rust implementation..."); - println!("VM Name: {}", args.name); - println!("Distribution: {}", args.distro); - - println!("Configuration:"); - println!(" vCPUs: {}", args.vcpus); - println!(" Memory: {} MB", args.memory_mb); - println!(" Disk Size: {} GB", args.disk_size_gb); - - let disk_path = format!("/home/giovanni/virt/images/{}.qcow2", args.name); - let vm_name = args.name.clone(); - - let mut vm = VirtualMachine::new( - args.name, - args.vcpus, - args.memory_mb, - args.disk_size_gb, - disk_path, - // args.distro, - ); - - if let Err(e) = vm.connect(None) { - eprintln!("Failed to connect to libvirt: {}", e); - process::exit(1); - } + let cli = Cli::parse(); + + match &cli.command { + Commands::Create { + name, + distro, + vcpus, + memory_mb, + disk_size_gb, + graphics, + dry_run, + } => { + println!("Starting kvm-install-vm Rust implementation..."); + println!("VM Name: {}", name); + println!("Distribution: {}", distro); + + println!("Configuration:"); + println!(" vCPUs: {}", vcpus); + println!(" Memory: {} MB", memory_mb); + println!(" Disk Size: {} GB", disk_size_gb); + println!(" Graphics: {}", graphics); + + if *dry_run { + println!("Dry run mode - not creating VM"); + return; + } + + let disk_path = format!("/home/giovanni/virt/images/{}.qcow2", name); + let vm_name = name.clone(); + + let mut vm = + VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, disk_path); + + if let Err(e) = vm.connect(None) { + eprintln!("Failed to connect to libvirt: {}", e); + process::exit(1); + } + + match vm.create() { + Ok(domain) => { + println!("Successfully created VM: {}", vm_name); + println!("Domain ID: {}", domain.get_id().unwrap_or(0)); + } + Err(e) => { + eprintln!("Failed to create VM: {}", e); + process::exit(1); + } + } + } + + Commands::Destroy { name, remove_disk } => { + println!("Destroying VM: {}", name); - match vm.create() { - Ok(domain) => { - println!("Successfully created VM: {}", vm_name); - println!("Domain ID: {}", domain.get_id().unwrap_or(0)); - }, - Err(e) => { - eprintln!("Failed to create VM: {}", e); - process::exit(1); + match VirtualMachine::destroy(name, None, *remove_disk) { + Ok(()) => { + println!("VM '{}' destroy operation completed successfully", name); + } + Err(e) => { + eprintln!("Failed to destroy VM '{}': {}", name, e); + process::exit(1); + } + } } } } diff --git a/src/vm.rs b/src/vm.rs index ec5362d..49d1c65 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,20 +1,26 @@ -use anyhow::{Result, Context}; +use anyhow::{Context, Result}; use std::path::Path; use virt::connect::Connect; use virt::domain::Domain; pub struct VirtualMachine { - name: String, - vcpus: u32, - memory_mb: u32, - disk_size_gb: u32, - disk_path: String, + pub name: String, + pub vcpus: u32, + pub memory_mb: u32, + pub disk_size_gb: u32, + pub disk_path: String, // distro: String, - connection: Option, + pub connection: Option, } impl VirtualMachine { - pub fn new(name: String, vcpus: u32, memory_mb: u32, disk_size_gb: u32, disk_path: String) -> Self { + pub fn new( + name: String, + vcpus: u32, + memory_mb: u32, + disk_size_gb: u32, + disk_path: String, + ) -> Self { VirtualMachine { name, vcpus, @@ -55,12 +61,21 @@ impl VirtualMachine { fn create_disk_image(&self) -> Result<()> { // Create disk image using qemu-img let output = std::process::Command::new("qemu-img") - .args(&["create", "-f", "qcow2", &self.disk_path, &format!("{}G", self.disk_size_gb)]) + .args(&[ + "create", + "-f", + "qcow2", + &self.disk_path, + &format!("{}G", self.disk_size_gb), + ]) .output() .context("Failed to execute qemu-img command")?; if !output.status.success() { - return Err(anyhow::anyhow!("Failed to create disk image: {:?}", output.stderr)); + return Err(anyhow::anyhow!( + "Failed to create disk image: {:?}", + output.stderr + )); } Ok(()) @@ -68,7 +83,8 @@ impl VirtualMachine { fn generate_domain_xml(&self) -> Result { // Generate domain XML - let xml = format!(r#" + let xml = format!( + r#" {} {} @@ -95,8 +111,106 @@ impl VirtualMachine { - "#, self.name, self.memory_mb, self.vcpus, self.disk_path); + "#, + self.name, self.memory_mb, self.vcpus, self.disk_path + ); Ok(xml) } + + // Destroy method for instance + pub fn destroy_instance(&mut self, remove_disk: bool) -> Result<()> { + // Ensure connection is established + if self.connection.is_none() { + return Err(anyhow::anyhow!("Connection not established")); + } + + Self::destroy(&self.name, Some("qemu:///session"), remove_disk) + } + + // Static destroy method + pub fn destroy(name: &str, uri: Option<&str>, remove_disk: bool) -> Result<()> { + let uri = uri.or(Some("qemu:///session")); + let conn = Connect::open(uri).context("Failed to connect to libvirt")?; + + let domain = match Domain::lookup_by_name(&conn, name) { + Ok(dom) => dom, + Err(e) => { + return Err(anyhow::anyhow!("Failed to find domain {}: {}", name, e)); + } + }; + + // Extract disk paths before destroying the domain + let xml = domain.get_xml_desc(0).context("Failed to get domain XML")?; + let disk_paths = extract_disk_paths_from_xml(&xml); + + // Check domain state first + if domain.is_active().context("Failed to check domain state")? { + println!("Stopping running domain '{}'...", name); + match domain.destroy() { + Ok(_) => println!("Domain stopped successfully"), + Err(e) => println!( + "Warning: Failed to stop domain cleanly: {}. Continuing with undefine...", + e + ), + } + } else { + println!("Domain '{}' is already stopped", name); + } + + // Undefine the domain with flags + use virt::sys; + + let flags = sys::VIR_DOMAIN_UNDEFINE_MANAGED_SAVE + | sys::VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA + | sys::VIR_DOMAIN_UNDEFINE_NVRAM; + + unsafe { + let result = sys::virDomainUndefineFlags(domain.as_ptr(), flags); + if result < 0 { + return Err(anyhow::anyhow!("Failed to undefine domain")); + } + } + + println!("Domain {} successfully undefined", name); + + // Handle disk removal if requested + if remove_disk && !disk_paths.is_empty() { + println!("Removing disk images..."); + for path in &disk_paths { + match std::fs::remove_file(path) { + Ok(_) => println!("Successfully removed disk: {}", path), + Err(e) => println!("Warning: Failed to remove disk {}: {}", path, e), + } + } + } else if !disk_paths.is_empty() { + println!("Note: The following disk images were not deleted:"); + for path in &disk_paths { + println!(" - {}", path); + } + } + + println!("Domain {} completely destroyed", name); + Ok(()) + } +} + +fn extract_disk_paths_from_xml(xml: &str) -> Vec { + let mut disk_paths = Vec::new(); + + for line in xml.lines() { + if line.contains(" Vec { @@ -10,43 +10,93 @@ fn get_args(args: &[&str]) -> Vec { } #[test] -fn test_cli_defaults() { - let args = get_args(&["--name", "test-vm"]); +fn test_cli_create_defaults() { + let args = get_args(&["create", "--name", "test-vm"]); let cli = Cli::parse_from(args); - assert_eq!(cli.name, "test-vm"); - assert_eq!(cli.distro, "centos8"); - assert_eq!(cli.vcpus, 1); - assert_eq!(cli.disk_size_gb, 10); - assert_eq!(cli.memory_mb, 1024); - assert_eq!(cli.graphics, false); - assert_eq!(cli.dry_run, false); + match cli.command { + Commands::Create { + name, + distro, + vcpus, + memory_mb, + disk_size_gb, + graphics, + dry_run + } => { + assert_eq!(name, "test-vm"); + assert_eq!(distro, "centos8"); + assert_eq!(vcpus, 1); + assert_eq!(disk_size_gb, 10); + assert_eq!(memory_mb, 1024); + assert_eq!(graphics, false); + assert_eq!(dry_run, false); + }, + _ => panic!("Expected Create command"), + } } #[test] -fn test_cli_custom_values() { +fn test_cli_create_custom_values() { let args = get_args(&[ - "--name", - "custom-vm", - "--distro", - "ubuntu2004", - "--vcpus", - "4", - "--memory-mb", - "4096", - "--disk-size-gb", - "50", + "create", + "--name", "custom-vm", + "--distro", "ubuntu2004", + "--vcpus", "4", + "--memory-mb", "4096", + "--disk-size-gb", "50", "--graphics", "--dry-run", ]); let cli = Cli::parse_from(args); - assert_eq!(cli.name, "custom-vm"); - assert_eq!(cli.distro, "ubuntu2004"); - assert_eq!(cli.vcpus, 4); - assert_eq!(cli.disk_size_gb, 50); - assert_eq!(cli.memory_mb, 4096); - assert_eq!(cli.graphics, true); - assert_eq!(cli.dry_run, true); + match cli.command { + Commands::Create { + name, + distro, + vcpus, + memory_mb, + disk_size_gb, + graphics, + dry_run + } => { + assert_eq!(name, "custom-vm"); + assert_eq!(distro, "ubuntu2004"); + assert_eq!(vcpus, 4); + assert_eq!(disk_size_gb, 50); + assert_eq!(memory_mb, 4096); + assert_eq!(graphics, true); + assert_eq!(dry_run, true); + }, + _ => panic!("Expected Create command"), + } } + +#[test] +fn test_cli_destroy_defaults() { + let args = get_args(&["destroy", "--name", "test-vm"]); + let cli = Cli::parse_from(args); + + match cli.command { + Commands::Destroy { name, remove_disk } => { + assert_eq!(name, "test-vm"); + assert_eq!(remove_disk, false); + }, + _ => panic!("Expected Destroy command"), + } +} + +#[test] +fn test_cli_destroy_with_disk_removal() { + let args = get_args(&["destroy", "--name", "test-vm", "--remove-disk"]); + let cli = Cli::parse_from(args); + + match cli.command { + Commands::Destroy { name, remove_disk } => { + assert_eq!(name, "test-vm"); + assert_eq!(remove_disk, true); + }, + _ => panic!("Expected Destroy command"), + } +} \ No newline at end of file diff --git a/tests/vm_tests.rs b/tests/vm_tests.rs new file mode 100644 index 0000000..012e33e --- /dev/null +++ b/tests/vm_tests.rs @@ -0,0 +1,290 @@ +#[cfg(test)] +mod tests { + use kvm_install_vm::vm::VirtualMachine; + use std::fs; + use std::process::Command; + use anyhow::Result; + use virt::domain::Domain; + + // Helper function to check if a VM with a given name exists + fn domain_exists(name: &str) -> bool { + let output = Command::new("virsh") + .args(["list", "--all", "--name"]) + .output() + .expect("Failed to execute virsh command"); + + let output_str = String::from_utf8_lossy(&output.stdout); + output_str.lines().any(|line| line.trim() == name) + } + + // Custom function to generate domain XML for testing + fn generate_test_domain_xml(vm: &VirtualMachine) -> String { + format!(r#" + + {} + {} + {} + + hvm + + + + + + + + + + + + + + + + + + + + + "#, vm.name, vm.memory_mb, vm.vcpus, vm.disk_path) + } + + // Extract disk paths function for testing + fn extract_disk_paths_from_xml(xml: &str) -> Vec { + let mut disk_paths = Vec::new(); + + for line in xml.lines() { + if line.contains(" Result<()> { + // Skip if domain already exists + if domain_exists(name) { + return Ok(()); + } + + let temp_dir = std::env::temp_dir(); + let disk_path = temp_dir.join(format!("{}.qcow2", name)); + + // Create a minimal VM for testing + let mut vm = VirtualMachine::new( + name.to_string(), + 1, + 512, + 1, + disk_path.to_string_lossy().to_string(), + ); + + vm.connect(None)?; + + // Create disk if it doesn't exist + if !disk_path.exists() { + Command::new("qemu-img") + .args([ + "create", + "-f", "qcow2", + disk_path.to_string_lossy().as_ref(), + "1G" + ]) + .output() + .expect("Failed to create test disk image"); + } + + // Define the domain but don't start it (to keep tests faster) + let conn = vm.connection.as_ref().unwrap(); + let xml = generate_test_domain_xml(&vm); + Domain::define_xml(conn, &xml)?; + + Ok(()) + } + + // Helper to clean up any leftover test resources + fn cleanup_test_resources(name: &str) { + if domain_exists(name) { + let _ = Command::new("virsh") + .args(["destroy", name]) + .output(); + + let _ = Command::new("virsh") + .args(["undefine", name, "--managed-save", "--snapshots-metadata", "--nvram"]) + .output(); + } + + let temp_dir = std::env::temp_dir(); + let disk_path = temp_dir.join(format!("{}.qcow2", name)); + if disk_path.exists() { + let _ = fs::remove_file(disk_path); + } + } + + #[test] + fn test_create_new_vm_instance() { + let vm = VirtualMachine::new( + "test-vm".to_string(), + 2, + 1024, + 10, + "/tmp/test-vm.qcow2".to_string(), + ); + + assert_eq!(vm.name, "test-vm"); + assert_eq!(vm.vcpus, 2); + assert_eq!(vm.memory_mb, 1024); + assert_eq!(vm.disk_size_gb, 10); + assert_eq!(vm.disk_path, "/tmp/test-vm.qcow2"); + assert!(vm.connection.is_none()); + } + + #[test] + fn test_extract_disk_paths() { + let xml = r#" + + test-vm + + + + + + + + + + + + + + "#; + + let disk_paths = extract_disk_paths_from_xml(xml); + + assert_eq!(disk_paths.len(), 2); + assert!(disk_paths.contains(&"/path/to/disk1.qcow2".to_string())); + assert!(disk_paths.contains(&"/path/to/disk2.qcow2".to_string())); + } + + // This test requires libvirt to be running + // Use #[ignore] to skip it in normal test runs + #[test] + #[ignore] + fn test_connect_to_libvirt() -> Result<()> { + let mut vm = VirtualMachine::new( + "test-connect-vm".to_string(), + 1, + 512, + 1, + "/tmp/test-connect-vm.qcow2".to_string(), + ); + + vm.connect(None)?; + assert!(vm.connection.is_some()); + + Ok(()) + } + + // This test creates and then destroys a VM + // It's marked as ignored because it makes actual system changes + #[test] + #[ignore] + fn test_create_and_destroy_vm() -> Result<()> { + let test_name = "test-create-destroy-vm"; + let temp_dir = std::env::temp_dir(); + let disk_path = temp_dir.join(format!("{}.qcow2", test_name)); + + // Clean up any previous test resources + cleanup_test_resources(test_name); + + // Create a new VM + let mut vm = VirtualMachine::new( + test_name.to_string(), + 1, + 512, + 1, + disk_path.to_string_lossy().to_string(), + ); + + vm.connect(None)?; + + // Create disk if it doesn't exist + if !disk_path.exists() { + Command::new("qemu-img") + .args([ + "create", + "-f", "qcow2", + disk_path.to_string_lossy().as_ref(), + "1G" + ]) + .output() + .expect("Failed to create test disk image"); + } + + // Create the VM via helper function since we can't call the private method + let conn = vm.connection.as_ref().unwrap(); + let xml = generate_test_domain_xml(&vm); + let domain = Domain::define_xml(conn, &xml)?; + domain.create()?; + + // Verify it exists + assert!(domain_exists(test_name)); + + // Now destroy it + vm.destroy_instance(false)?; + + // Verify it no longer exists + assert!(!domain_exists(test_name)); + + // Disk should still exist since we used remove_disk=false + assert!(disk_path.exists()); + + // Clean up disk + let _ = fs::remove_file(disk_path); + + Ok(()) + } + + // Test destroying a VM with the static method + #[test] + #[ignore] + fn test_destroy_static_method() -> Result<()> { + let test_name = "test-static-destroy-vm"; + + // Create a test VM first + create_test_vm(test_name)?; + + // Verify it exists + assert!(domain_exists(test_name)); + + // Destroy it with the static method + VirtualMachine::destroy(test_name, None, true)?; + + // Verify it no longer exists + assert!(!domain_exists(test_name)); + + // Disk should be gone since we used remove_disk=true + let temp_dir = std::env::temp_dir(); + let disk_path = temp_dir.join(format!("{}.qcow2", test_name)); + assert!(!disk_path.exists()); + + Ok(()) + } + + // Test destroying a non-existent VM (should return error) + #[test] + fn test_destroy_nonexistent_vm() { + let result = VirtualMachine::destroy("definitely-nonexistent-vm", None, false); + assert!(result.is_err()); + } +} \ No newline at end of file