diff --git a/src/cli.rs b/src/cli.rs index 679f089..14b3389 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,10 +40,10 @@ pub enum Commands { List { #[arg(short = 'a', long, help = "Show all domains (including inactive ones)")] all: bool, - + #[arg(short = 's', long, help = "Show only running domains")] running: bool, - + #[arg(short = 'i', long, help = "Show only inactive domains")] inactive: bool, }, diff --git a/src/main.rs b/src/main.rs index fde867e..fd5611c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,41 +68,47 @@ fn main() { } } } - - Commands::List { all, running, inactive } => { + + Commands::List { + all, + running, + inactive, + } => { println!("Listing virtual machines..."); - + // Determine which types of domains to list let filters = (*all, *running, *inactive); - + // If no specific flags are provided, default to showing all domains let show_all = filters == (false, false, false) || *all; - + match VirtualMachine::list_domains(None) { Ok(domains) => { // Filter domains based on flags - let filtered_domains: Vec<_> = domains.into_iter() + let filtered_domains: Vec<_> = domains + .into_iter() .filter(|domain| { if show_all { return true; } - - if *running && domain.state == kvm_install_vm::vm::DomainState::Running { + + if *running && domain.state == kvm_install_vm::vm::DomainState::Running + { return true; } - + if *inactive && domain.id.is_none() { return true; } - + false }) .collect(); - + // Print header println!("{:<5} {:<30} {:<10}", "ID", "Name", "State"); println!("{:-<5} {:-<30} {:-<10}", "", "", ""); - + // Print domains if filtered_domains.is_empty() { println!("No domains found matching the specified criteria"); @@ -112,7 +118,7 @@ fn main() { Some(id) => id.to_string(), None => "-".to_string(), }; - + println!("{:<5} {:<30} {:<10}", id_str, domain.name, domain.state); } } @@ -124,4 +130,4 @@ fn main() { } } } -} \ No newline at end of file +} diff --git a/src/vm.rs b/src/vm.rs index d0d8d17..f488315 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -1,11 +1,10 @@ use anyhow::{Context, Result}; -use std::path::Path; use std::fmt; +use std::path::Path; use virt::connect::Connect; use virt::domain::Domain; use virt::sys; - pub struct VirtualMachine { pub name: String, pub vcpus: u32, @@ -18,7 +17,7 @@ pub struct VirtualMachine { #[derive(Debug)] pub struct DomainInfo { - pub id: Option, // None if domain is inactive + pub id: Option, // None if domain is inactive pub name: String, pub state: DomainState, } @@ -226,88 +225,84 @@ impl VirtualMachine { } pub fn list_domains(uri: Option<&str>) -> Result> { - let uri = uri.or(Some("qemu:///session")); - let conn = Connect::open(uri).context("Failed to connect to libvirt")?; - - let mut domain_infos = Vec::new(); - - // Get active domains - let active_domains = conn.list_all_domains(sys::VIR_CONNECT_LIST_DOMAINS_ACTIVE) - .context("Failed to list active domains")?; - - // Get inactive domains - let inactive_domains = conn.list_all_domains(sys::VIR_CONNECT_LIST_DOMAINS_INACTIVE) - .context("Failed to list inactive domains")?; - - // Process active domains - for domain in active_domains { - let name = domain.get_name().context("Failed to get domain name")?; - let id = domain.get_id(); - - // Get domain state - let state = match domain.get_state() { - Ok((state, _reason)) => { - match state { - sys::VIR_DOMAIN_RUNNING => DomainState::Running, - sys::VIR_DOMAIN_PAUSED => DomainState::Paused, - sys::VIR_DOMAIN_SHUTDOWN => DomainState::Shutdown, - sys::VIR_DOMAIN_SHUTOFF => DomainState::Shutoff, - sys::VIR_DOMAIN_CRASHED => DomainState::Crashed, - _ => DomainState::Unknown, - } - }, - Err(_) => DomainState::Unknown, - }; - - domain_infos.push(DomainInfo { - id, - name, - state, - }); - } - - // Process inactive domains - for domain in inactive_domains { - let name = domain.get_name().context("Failed to get domain name")?; - - domain_infos.push(DomainInfo { - id: None, - name, - state: DomainState::Shutoff, - }); - } - - // Sort domains by name for consistent output - domain_infos.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(domain_infos) - } - - /// Pretty print the list of domains - pub fn print_domain_list(uri: Option<&str>) -> Result<()> { - let domains = Self::list_domains(uri)?; - - if domains.is_empty() { - println!("No domains found"); - return Ok(()); - } - - // Print header - println!("{:<5} {:<30} {:<10}", "ID", "Name", "State"); - println!("{:-<5} {:-<30} {:-<10}", "", "", ""); - - // Print domains - for domain in domains { - let id_str = match domain.id { - Some(id) => id.to_string(), - None => "-".to_string(), - }; - - println!("{:<5} {:<30} {:<10}", id_str, domain.name, domain.state); - } - - Ok(()) - } + let uri = uri.or(Some("qemu:///session")); + let conn = Connect::open(uri).context("Failed to connect to libvirt")?; + + let mut domain_infos = Vec::new(); + + // Get active domains + let active_domains = conn + .list_all_domains(sys::VIR_CONNECT_LIST_DOMAINS_ACTIVE) + .context("Failed to list active domains")?; + + // Get inactive domains + let inactive_domains = conn + .list_all_domains(sys::VIR_CONNECT_LIST_DOMAINS_INACTIVE) + .context("Failed to list inactive domains")?; + + // Process active domains + for domain in active_domains { + let name = domain.get_name().context("Failed to get domain name")?; + let id = domain.get_id(); + + // Get domain state + let state = match domain.get_state() { + Ok((state, _reason)) => match state { + sys::VIR_DOMAIN_RUNNING => DomainState::Running, + sys::VIR_DOMAIN_PAUSED => DomainState::Paused, + sys::VIR_DOMAIN_SHUTDOWN => DomainState::Shutdown, + sys::VIR_DOMAIN_SHUTOFF => DomainState::Shutoff, + sys::VIR_DOMAIN_CRASHED => DomainState::Crashed, + _ => DomainState::Unknown, + }, + Err(_) => DomainState::Unknown, + }; + + domain_infos.push(DomainInfo { id, name, state }); + } + + // Process inactive domains + for domain in inactive_domains { + let name = domain.get_name().context("Failed to get domain name")?; + + domain_infos.push(DomainInfo { + id: None, + name, + state: DomainState::Shutoff, + }); + } + + // Sort domains by name for consistent output + domain_infos.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(domain_infos) + } + + /// Pretty print the list of domains + pub fn print_domain_list(uri: Option<&str>) -> Result<()> { + let domains = Self::list_domains(uri)?; + + if domains.is_empty() { + println!("No domains found"); + return Ok(()); + } + + // Print header + println!("{:<5} {:<30} {:<10}", "ID", "Name", "State"); + println!("{:-<5} {:-<30} {:-<10}", "", "", ""); + + // Print domains + for domain in domains { + let id_str = match domain.id { + Some(id) => id.to_string(), + None => "-".to_string(), + }; + + println!("{:<5} {:<30} {:<10}", id_str, domain.name, domain.state); + } + + Ok(()) + } } fn extract_disk_paths_from_xml(xml: &str) -> Vec { diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 7f21109..cdbace8 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -15,14 +15,14 @@ fn test_cli_create_defaults() { let cli = Cli::parse_from(args); match cli.command { - Commands::Create { - name, - distro, - vcpus, - memory_mb, - disk_size_gb, - graphics, - dry_run + Commands::Create { + name, + distro, + vcpus, + memory_mb, + disk_size_gb, + graphics, + dry_run, } => { assert_eq!(name, "test-vm"); assert_eq!(distro, "centos8"); @@ -31,7 +31,7 @@ fn test_cli_create_defaults() { assert_eq!(memory_mb, 1024); assert_eq!(graphics, false); assert_eq!(dry_run, false); - }, + } _ => panic!("Expected Create command"), } } @@ -40,11 +40,16 @@ fn test_cli_create_defaults() { fn test_cli_create_custom_values() { let args = get_args(&[ "create", - "--name", "custom-vm", - "--distro", "ubuntu2004", - "--vcpus", "4", - "--memory-mb", "4096", - "--disk-size-gb", "50", + "--name", + "custom-vm", + "--distro", + "ubuntu2004", + "--vcpus", + "4", + "--memory-mb", + "4096", + "--disk-size-gb", + "50", "--graphics", "--dry-run", ]); @@ -52,14 +57,14 @@ fn test_cli_create_custom_values() { let cli = Cli::parse_from(args); match cli.command { - Commands::Create { - name, - distro, - vcpus, - memory_mb, - disk_size_gb, - graphics, - dry_run + Commands::Create { + name, + distro, + vcpus, + memory_mb, + disk_size_gb, + graphics, + dry_run, } => { assert_eq!(name, "custom-vm"); assert_eq!(distro, "ubuntu2004"); @@ -68,7 +73,7 @@ fn test_cli_create_custom_values() { assert_eq!(memory_mb, 4096); assert_eq!(graphics, true); assert_eq!(dry_run, true); - }, + } _ => panic!("Expected Create command"), } } @@ -82,7 +87,7 @@ fn test_cli_destroy_defaults() { Commands::Destroy { name, remove_disk } => { assert_eq!(name, "test-vm"); assert_eq!(remove_disk, false); - }, + } _ => panic!("Expected Destroy command"), } } @@ -96,7 +101,7 @@ fn test_cli_destroy_with_disk_removal() { 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/list_vms.rs b/tests/list_vms.rs index 29f79ac..487efa0 100644 --- a/tests/list_vms.rs +++ b/tests/list_vms.rs @@ -1,8 +1,8 @@ #[cfg(test)] mod domain_list_tests { use clap::Parser; + use kvm_install_vm::vm::{DomainState, VirtualMachine}; use kvm_install_vm::{Cli, cli::Commands}; - use kvm_install_vm::vm::{VirtualMachine, DomainState}; use std::ffi::OsString; // Helper function for CLI tests @@ -19,11 +19,15 @@ mod domain_list_tests { let cli = Cli::parse_from(args); match cli.command { - Commands::List { all, running, inactive } => { + Commands::List { + all, + running, + inactive, + } => { assert_eq!(all, false); assert_eq!(running, false); assert_eq!(inactive, false); - }, + } _ => panic!("Expected List command"), } } @@ -34,11 +38,15 @@ mod domain_list_tests { let cli = Cli::parse_from(args); match cli.command { - Commands::List { all, running, inactive } => { + Commands::List { + all, + running, + inactive, + } => { assert_eq!(all, true); assert_eq!(running, true); assert_eq!(inactive, false); - }, + } _ => panic!("Expected List command"), } } @@ -50,26 +58,28 @@ mod domain_list_tests { fn test_domain_listing() -> anyhow::Result<()> { // This will connect to libvirt and list domains let domains = VirtualMachine::list_domains(None)?; - + // We can't assert much about the actual domains without knowing the test environment, // but we can check that the function runs without errors and returns a Vec println!("Found {} domains", domains.len()); - + for domain in &domains { - println!("Domain: {}, ID: {:?}, State: {:?}", - domain.name, domain.id, domain.state); - + println!( + "Domain: {}, ID: {:?}, State: {:?}", + domain.name, domain.id, domain.state + ); + // Check that inactive domains have no ID if domain.state == DomainState::Shutoff { assert_eq!(domain.id, None); } - + // If it's running, it should have an ID if domain.state == DomainState::Running { assert!(domain.id.is_some()); } } - + Ok(()) } -} \ No newline at end of file +} diff --git a/tests/vm_tests.rs b/tests/vm_tests.rs index 012e33e..1db335c 100644 --- a/tests/vm_tests.rs +++ b/tests/vm_tests.rs @@ -1,9 +1,9 @@ #[cfg(test)] mod tests { + use anyhow::Result; 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 @@ -12,14 +12,15 @@ mod tests { .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#" + format!( + r#" {} {} @@ -46,7 +47,9 @@ mod tests { - "#, vm.name, vm.memory_mb, vm.vcpus, vm.disk_path) + "#, + vm.name, vm.memory_mb, vm.vcpus, vm.disk_path + ) } // Extract disk paths function for testing @@ -79,7 +82,7 @@ mod tests { 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(), @@ -90,15 +93,16 @@ mod tests { ); 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" + "create", + "-f", + "qcow2", + disk_path.to_string_lossy().as_ref(), + "1G", ]) .output() .expect("Failed to create test disk image"); @@ -115,12 +119,16 @@ mod tests { // 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(["destroy", name]) - .output(); - - let _ = Command::new("virsh") - .args(["undefine", name, "--managed-save", "--snapshots-metadata", "--nvram"]) + .args([ + "undefine", + name, + "--managed-save", + "--snapshots-metadata", + "--nvram", + ]) .output(); } @@ -170,7 +178,7 @@ mod tests { "#; 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())); @@ -191,7 +199,7 @@ mod tests { vm.connect(None)?; assert!(vm.connection.is_some()); - + Ok(()) } @@ -203,10 +211,10 @@ mod tests { 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(), @@ -217,41 +225,42 @@ mod tests { ); 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" + "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(()) } @@ -260,24 +269,24 @@ mod tests { #[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(()) } @@ -287,4 +296,4 @@ mod tests { let result = VirtualMachine::destroy("definitely-nonexistent-vm", None, false); assert!(result.is_err()); } -} \ No newline at end of file +}