diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..59a0236 --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,52 @@ +use std::fmt; + +#[derive(Debug)] +pub struct DomainInfo { + pub id: Option, // None if domain is inactive + pub name: String, + pub state: DomainState, +} + +#[derive(Debug, PartialEq)] +pub enum DomainState { + Running, + Paused, + Shutdown, + Shutoff, + Crashed, + Unknown, +} + +// Implement Display for DomainState for nice formatting +impl fmt::Display for DomainState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DomainState::Running => write!(f, "running"), + DomainState::Paused => write!(f, "paused"), + DomainState::Shutdown => write!(f, "shutdown"), + DomainState::Shutoff => write!(f, "shut off"), + DomainState::Crashed => write!(f, "crashed"), + DomainState::Unknown => write!(f, "unknown"), + } + } +} + +pub fn extract_disk_paths_from_xml(xml: &str) -> Vec { + let mut disk_paths = Vec::new(); + + for line in xml.lines() { + if line.contains(">(image_dir: P) -> Self { + ImageManager { + image_dir: image_dir.as_ref().to_path_buf(), + } + } + + /// Check if a cloud image exists locally + pub fn image_exists(&self, distro_info: &DistroInfo) -> bool { + let image_path = self.image_dir.join(&distro_info.qcow_filename); + image_path.exists() + } + + /// Get the full path to a cloud image (whether it exists or not) + pub fn get_image_path(&self, distro_info: &DistroInfo) -> PathBuf { + self.image_dir.join(&distro_info.qcow_filename) + } + + /// Download a cloud image if it doesn't already exist locally + pub async fn ensure_image(&self, distro_info: &DistroInfo) -> Result { + let image_path = self.get_image_path(distro_info); + + if image_path.exists() { + info!("Cloud image already exists: {}", image_path.display()); + println!("Cloud image already exists: {}", image_path.display()); + return Ok(image_path); + } + + // Create image directory if it doesn't exist + if !self.image_dir.exists() { + fs::create_dir_all(&self.image_dir).context("Failed to create image directory")?; + } + + info!("Downloading cloud image: {}", distro_info.qcow_filename); + println!("Downloading cloud image: {}", distro_info.qcow_filename); + + // Construct download URL + let url = format!( + "{}/{}", + distro_info.image_url.trim_end_matches('/'), + distro_info.qcow_filename + ); + + debug!("From URL: {}", url); + println!("From URL: {}", url); + + // Download the file with progress indication + self.download_file(&url, &image_path) + .await + .context("Failed to download cloud image")?; + + Ok(image_path) + } + + /// Download a file with progress indication + async fn download_file(&self, url: &str, dest: &Path) -> Result { + // Create a temporary file for downloading + let temp_path = dest.with_extension("part"); + + // Create parent directory if needed + if let Some(parent) = temp_path.parent() { + fs::create_dir_all(parent)?; + } + + // Begin the download + let res = reqwest::get(url).await?; + let total_size = res.content_length().unwrap_or(0); + + // Setup progress bar + let pb = ProgressBar::new(total_size); + pb.set_style(ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-")); + + // Download the file in chunks, writing each chunk to disk + let mut file = File::create(&temp_path).await?; + let mut downloaded: u64 = 0; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk).await?; + downloaded += chunk.len() as u64; + pb.set_position(downloaded); + } + + // Ensure everything is written to disk + file.flush().await?; + + // Finalize the download by renaming the temp file + tokio::fs::rename(&temp_path, &dest).await?; + + pb.finish_with_message(format!("Downloaded {}", dest.display())); + + Ok(dest.to_path_buf()) + } + + /// Download a cloud image with resume capability + #[instrument(skip(self), fields(distro = %distro_info.qcow_filename))] + pub async fn download_image_with_resume(&self, distro_info: &DistroInfo) -> Result { + let image_path = self.image_dir.join(&distro_info.qcow_filename); + let part_path = image_path.with_extension("part"); + + // Create image directory if it doesn't exist + if !self.image_dir.exists() { + fs::create_dir_all(&self.image_dir).context("Failed to create image directory")?; + } + + // Check if the image already exists + if image_path.exists() { + info!("Cloud image already exists: {}", image_path.display()); + println!("Cloud image already exists: {}", image_path.display()); + return Ok(image_path); + } + + // Construct download URL + let url = format!( + "{}/{}", + distro_info.image_url.trim_end_matches('/'), + distro_info.qcow_filename + ); + + info!("Downloading cloud image: {}", distro_info.qcow_filename); + println!("Downloading cloud image: {}", distro_info.qcow_filename); + debug!("From URL: {}", url); + + // Check if partial download exists + let resume_download = part_path.exists(); + if resume_download { + info!("Partial download found. Resuming from previous download"); + println!("Partial download found. Resuming from previous download"); + + let client = reqwest::Client::new(); + let file_size = part_path.metadata()?.len(); + + debug!("Resuming from byte position: {}", file_size); + + // Create a request with Range header + let mut req = client.get(&url); + req = req.header("Range", format!("bytes={}-", file_size)); + + // Download the rest of the file + let res = req.send().await?; + + // Check if the server supports resume + if res.status() == reqwest::StatusCode::PARTIAL_CONTENT { + let total_size = match res.content_length() { + Some(len) => file_size + len, + None => file_size, // Just show the current size if total is unknown + }; + + // Setup progress bar + let pb = ProgressBar::new(total_size); + pb.set_style(ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-")); + pb.set_position(file_size); + + // Open the existing part file for appending + let mut file = tokio::fs::OpenOptions::new() + .append(true) + .open(&part_path) + .await?; + + let mut downloaded = file_size; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk).await?; + downloaded += chunk.len() as u64; + pb.set_position(downloaded); + } + + // Ensure everything is written to disk + file.flush().await?; + + // Finalize the download by renaming the temp file + tokio::fs::rename(&part_path, &image_path).await?; + + pb.finish_with_message(format!("Downloaded {}", image_path.display())); + + return Ok(image_path); + } else { + warn!("Server does not support resume. Starting a new download"); + println!("Server does not support resume. Starting a new download"); + } + } + + // If we got here, we need to do a full download + self.download_file(&url, &image_path).await?; + + Ok(image_path) + } + + /// Create a resized version of a cloud image + pub async fn create_resized_image( + &self, + source_path: &Path, + target_path: &Path, + size_gb: u32, + ) -> Result<()> { + info!( + "Creating resized image: {} ({}GB)", + target_path.display(), + size_gb + ); + + // Create parent directory if needed + if let Some(parent) = target_path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + // First, create a copy of the source image + let mut cmd = Command::new("qemu-img"); + cmd.args(&[ + "create", + "-f", "qcow2", + "-F", "qcow2", + "-b", source_path.to_str().unwrap(), + target_path.to_str().unwrap(), + ]); + + debug!("Executing command: {:?}", cmd); + let status = cmd.status().context("Failed to execute qemu-img create command")?; + + if !status.success() { + return Err(anyhow::anyhow!("Failed to create disk image copy")); + } + + // Then resize it to the desired size + let mut resize_cmd = Command::new("qemu-img"); + resize_cmd.args(&[ + "resize", + target_path.to_str().unwrap(), + &format!("{}G", size_gb), + ]); + + debug!("Executing command: {:?}", resize_cmd); + let resize_status = resize_cmd.status().context("Failed to execute qemu-img resize command")?; + + if !resize_status.success() { + return Err(anyhow::anyhow!("Failed to resize disk image")); + } + + info!("Successfully created and resized disk image"); + Ok(()) + } + + /// Verify the integrity of a downloaded image + pub fn verify_image(&self, distro_info: &DistroInfo) -> Result { + let image_path = self.get_image_path(distro_info); + + if !image_path.exists() { + return Ok(false); + } + + info!("Verifying image integrity: {}", image_path.display()); + + // Use qemu-img check to verify the image + let mut cmd = Command::new("qemu-img"); + cmd.args(&[ + "check", + image_path.to_str().unwrap(), + ]); + + debug!("Executing command: {:?}", cmd); + let output = cmd.output().context("Failed to execute qemu-img check command")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + warn!("Image verification failed: {}", stderr); + return Ok(false); + } + + info!("Image verification successful"); + Ok(true) + } + + /// Delete an image from the image directory + pub fn delete_image(&self, distro_info: &DistroInfo) -> Result<()> { + let image_path = self.get_image_path(distro_info); + + if image_path.exists() { + info!("Deleting image: {}", image_path.display()); + fs::remove_file(&image_path).context("Failed to delete image file")?; + info!("Image deleted successfully"); + } else { + info!("Image does not exist, nothing to delete"); + } + + Ok(()) + } + + /// List all available images in the image directory + pub fn list_images(&self) -> Result> { + if !self.image_dir.exists() { + return Ok(Vec::new()); + } + + let mut images = Vec::new(); + + for entry in fs::read_dir(&self.image_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() && path.extension().unwrap_or_default() == "qcow2" { + images.push(path); + } + } + + Ok(images) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d2c36e5..84d0462 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,16 @@ pub mod cli; pub mod cloudinit; pub mod config; +pub mod domain; +pub mod image; pub mod vm; +pub mod network; pub use cli::Cli; +pub use cli::Commands; +pub use config::Config; +pub use domain::DomainInfo; +pub use domain::DomainState; +pub use vm::VirtualMachine; +pub use vm::DistroInfo; +pub use image::ImageManager; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1aea061..e1860d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use clap::Parser; -use kvm_install_vm::{Cli, cli::Commands, vm::VirtualMachine}; +use kvm_install_vm::{ + Cli, Commands, Config, VirtualMachine +}; use std::io::Write; use std::process; use tracing::{debug, error, info}; @@ -60,14 +62,28 @@ fn main() { return; } - let disk_path = format!("/home/giovanni/virt/images/{}.qcow2", name); - debug!("Using disk path: {}", disk_path); - let vm_name = name.clone(); + // Load configuration + print_status_start("Loading configuration"); + let config = match Config::load() { + Ok(config) => { + println!("\x1b[32mOK\x1b[0m"); + config + }, + Err(e) => { + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to load configuration: {}", e); + process::exit(1); + } + }; + // Initialize VM instance print_status_start("Creating VM instance"); - let mut vm = VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, disk_path); + let vm_name = name.clone(); + let mut vm = VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, String::new()); println!("\x1b[32mOK\x1b[0m"); + // Connect to libvirt print_status_start("Connecting to libvirt"); if let Err(e) = vm.connect(None) { println!("\x1b[31mFAILED\x1b[0m"); @@ -77,25 +93,43 @@ fn main() { } println!("\x1b[32mOK\x1b[0m"); - print_status_start("Creating virtual machine"); - match vm.create() { - Ok(domain) => { - println!("\x1b[32mOK\x1b[0m"); - let domain_id = domain.get_id().unwrap_or(0); - - info!("Successfully created VM: {}", vm_name); - info!("Domain ID: {}", domain_id); - - println!("Successfully created VM: {}", vm_name); - println!("Domain ID: {}", domain_id); - } - Err(e) => { - println!("\x1b[31mFAILED\x1b[0m"); - eprintln!(" Error: {}", e); - error!("Failed to create VM: {}", e); - process::exit(1); + // Create VM with proper image handling + // Set up a runtime for async operations + let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + rt.block_on(async { + print_status_start("Preparing VM image"); + match vm.prepare_image(distro, &config).await { + Ok(_) => { + println!("\x1b[32mOK\x1b[0m"); + + print_status_start("Creating virtual machine"); + match vm.create() { + Ok(domain) => { + println!("\x1b[32mOK\x1b[0m"); + let domain_id = domain.get_id().unwrap_or(0); + + info!("Successfully created VM: {}", vm_name); + info!("Domain ID: {}", domain_id); + + println!("Successfully created VM: {}", vm_name); + println!("Domain ID: {}", domain_id); + } + Err(e) => { + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to create VM: {}", e); + process::exit(1); + } + } + }, + Err(e) => { + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to prepare VM image: {}", e); + process::exit(1); + } } - } + }); } Commands::Destroy { name, remove_disk } => { diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/vm.rs b/src/vm.rs index dc2f493..3015fc5 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -3,7 +3,6 @@ use futures_util::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; use reqwest; use serde::{Deserialize, Serialize}; -use std::fmt; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -14,14 +13,9 @@ use virt::connect::Connect; use virt::domain::Domain; use virt::sys; -// Create a simple macro for logging commands -#[macro_export] -macro_rules! log_cmd { - ($cmd:expr) => {{ - debug!("Executing command: {:?}", $cmd); - $cmd - }}; -} +use crate::config::Config; +use crate::domain::{DomainInfo, DomainState, extract_disk_paths_from_xml}; +use crate::cloudinit::CloudInitManager; pub struct VirtualMachine { pub name: String, @@ -46,37 +40,6 @@ pub struct ImageManager { image_dir: PathBuf, } -#[derive(Debug)] -pub struct DomainInfo { - pub id: Option, // None if domain is inactive - pub name: String, - pub state: DomainState, -} - -#[derive(Debug, PartialEq)] -pub enum DomainState { - Running, - Paused, - Shutdown, - Shutoff, - Crashed, - Unknown, -} - -// Implement Display for DomainState for nice formatting -impl fmt::Display for DomainState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DomainState::Running => write!(f, "running"), - DomainState::Paused => write!(f, "paused"), - DomainState::Shutdown => write!(f, "shutdown"), - DomainState::Shutoff => write!(f, "shut off"), - DomainState::Crashed => write!(f, "crashed"), - DomainState::Unknown => write!(f, "unknown"), - } - } -} - impl ImageManager { /// Create a new ImageManager with the specified image directory pub fn new>(image_dir: P) -> Self { @@ -133,7 +96,7 @@ impl ImageManager { } /// Download a file with progress indication - async fn download_file(&self, url: &str, dest: &Path) -> Result<()> { + async fn download_file(&self, url: &str, dest: &Path) -> Result { // Create a temporary file for downloading let temp_path = dest.with_extension("part"); @@ -173,78 +136,106 @@ impl ImageManager { pb.finish_with_message(format!("Downloaded {}", dest.display())); - Ok(()) + Ok(dest.to_path_buf()) } - - /// Prepare a VM disk from a cloud image - pub fn prepare_vm_disk( - &self, - base_image: &Path, - vm_dir: &Path, - vm_name: &str, - disk_size_gb: u32, - ) -> Result { - let disk_path = vm_dir.join(format!("{}.qcow2", vm_name)); - - // Create VM directory if it doesn't exist - fs::create_dir_all(vm_dir).context("Failed to create VM directory")?; - - // Create base disk by copying from the cloud image - info!("Creating disk from cloud image: {}", disk_path.display()); - println!("Creating disk from cloud image: {}", disk_path.display()); - - let mut command = Command::new("qemu-img"); - let qemu_cmd = command - .args(&[ - "create", - "-f", - "qcow2", - "-F", - "qcow2", - "-b", - &base_image.to_string_lossy(), - &disk_path.to_string_lossy(), - ]); - - debug!("Executing command: {:?}", qemu_cmd); - let status = qemu_cmd - .status() - .context("Failed to execute qemu-img create command")?; - - if !status.success() { - return Err(anyhow::anyhow!( - "qemu-img create failed with status: {}", - status - )); + + /// Download a cloud image with resume capability + #[instrument(skip(self), fields(distro = %distro_info.qcow_filename))] + pub async fn download_image_with_resume(&self, distro_info: &DistroInfo) -> Result { + let image_path = self.image_dir.join(&distro_info.qcow_filename); + let part_path = image_path.with_extension("part"); + + // Create image directory if it doesn't exist + if !self.image_dir.exists() { + fs::create_dir_all(&self.image_dir).context("Failed to create image directory")?; } - - // Resize disk if requested - if disk_size_gb > 0 { - info!("Resizing disk to {}GB", disk_size_gb); - println!("Resizing disk to {}GB", disk_size_gb); - - let mut command = Command::new("qemu-img"); - let resize_cmd = command - .args(&[ - "resize", - &disk_path.to_string_lossy(), - &format!("{}G", disk_size_gb), - ]); + + // Check if the image already exists + if image_path.exists() { + info!("Cloud image already exists: {}", image_path.display()); + println!("Cloud image already exists: {}", image_path.display()); + return Ok(image_path); + } + + // Construct download URL + let url = format!( + "{}/{}", + distro_info.image_url.trim_end_matches('/'), + distro_info.qcow_filename + ); + + info!("Downloading cloud image: {}", distro_info.qcow_filename); + println!("Downloading cloud image: {}", distro_info.qcow_filename); + debug!("From URL: {}", url); + + // Check if partial download exists + let resume_download = part_path.exists(); + if resume_download { + info!("Partial download found. Resuming from previous download"); + println!("Partial download found. Resuming from previous download"); + + let client = reqwest::Client::new(); + let file_size = part_path.metadata()?.len(); + + debug!("Resuming from byte position: {}", file_size); + + // Create a request with Range header + let mut req = client.get(&url); + req = req.header("Range", format!("bytes={}-", file_size)); + + // Download the rest of the file + let res = req.send().await?; + + // Check if the server supports resume + if res.status() == reqwest::StatusCode::PARTIAL_CONTENT { + let total_size = match res.content_length() { + Some(len) => file_size + len, + None => file_size, // Just show the current size if total is unknown + }; + + // Setup progress bar + let pb = ProgressBar::new(total_size); + pb.set_style(ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .progress_chars("#>-")); + pb.set_position(file_size); + + // Open the existing part file for appending + let mut file = tokio::fs::OpenOptions::new() + .append(true) + .open(&part_path) + .await?; + + let mut downloaded = file_size; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk).await?; + downloaded += chunk.len() as u64; + pb.set_position(downloaded); + } - debug!("Executing command: {:?}", resize_cmd); - let status = resize_cmd - .status() - .context("Failed to execute qemu-img resize command")?; - - if !status.success() { - return Err(anyhow::anyhow!( - "qemu-img resize failed with status: {}", - status - )); + // Ensure everything is written to disk + file.flush().await?; + + // Finalize the download by renaming the temp file + tokio::fs::rename(&part_path, &image_path).await?; + + pb.finish_with_message(format!("Downloaded {}", image_path.display())); + + return Ok(image_path); + } else { + warn!("Server does not support resume. Starting a new download"); + println!("Server does not support resume. Starting a new download"); } } - - Ok(disk_path) + + // If we got here, we need to do a full download + self.download_file(&url, &image_path).await?; + + Ok(image_path) } } @@ -286,6 +277,99 @@ impl VirtualMachine { } } + // Prepare the VM image for creation + #[instrument(skip(self, config), fields(vm_name = %self.name))] + pub async fn prepare_image(&mut self, distro: &str, config: &Config) -> Result<()> { + info!("Preparing image for VM: {}", self.name); + + // Get distribution info + let distro_info = config.get_distro(distro)?; + debug!("Using distro: {}", distro); + + // Setup image manager + let image_dir = PathBuf::from(&config.defaults.image_dir); + let image_manager = ImageManager::new(&image_dir); + + // Ensure we have the cloud image + info!("Checking for cloud image"); + let cloud_image = image_manager.ensure_image(distro_info).await?; + debug!("Cloud image path: {}", cloud_image.display()); + + // Create VM directory if it doesn't exist + let vm_dir = PathBuf::from(&config.defaults.vm_dir).join(&self.name); + if !vm_dir.exists() { + fs::create_dir_all(&vm_dir).context("Failed to create VM directory")?; + } + + // Create disk path for the VM + self.disk_path = vm_dir.join(format!("{}.qcow2", self.name)) + .to_string_lossy() + .to_string(); + debug!("Disk path: {}", self.disk_path); + + // Create disk image from the cloud image + info!("Creating disk image for VM"); + let mut cmd = Command::new("qemu-img"); + cmd.args([ + "create", + "-f", "qcow2", + "-F", "qcow2", + "-b", cloud_image.to_str().unwrap(), + &self.disk_path, + ]); + + debug!("Running command: {:?}", cmd); + let status = cmd.status().context("Failed to execute qemu-img command")?; + + if !status.success() { + return Err(anyhow::anyhow!("Failed to create disk image")); + } + + // Resize disk if needed + if self.disk_size_gb > 10 { + info!("Resizing disk to {}GB", self.disk_size_gb); + let mut resize_cmd = Command::new("qemu-img"); + resize_cmd.args([ + "resize", + &self.disk_path, + &format!("{}G", self.disk_size_gb), + ]); + + debug!("Running command: {:?}", resize_cmd); + let resize_status = resize_cmd.status().context("Failed to resize disk")?; + + if !resize_status.success() { + return Err(anyhow::anyhow!("Failed to resize disk image")); + } + } + + // Create cloud-init configuration + info!("Creating cloud-init configuration"); + let ssh_key = CloudInitManager::find_ssh_public_key()?; + + let (user_data, meta_data) = CloudInitManager::create_cloud_init_config( + &self.name, + &config.defaults.dns_domain, + &ssh_key, + &distro_info.login_user, + &config.defaults.timezone, + &distro_info.sudo_group, + &distro_info.cloud_init_disable, + )?; + + // Create cloud-init ISO + let iso_path = CloudInitManager::create_cloud_init_iso( + &vm_dir, + &self.name, + &user_data, + &meta_data, + )?; + debug!("Cloud-init ISO created at: {}", iso_path.display()); + + info!("Image preparation completed successfully"); + Ok(()) + } + #[instrument(skip(self), fields(vm_name = %self.name))] pub fn create(&mut self) -> Result { info!("Creating VM: {}", self.name); @@ -389,7 +473,7 @@ impl VirtualMachine { } fn generate_domain_xml(&self) -> Result { - // Generate domain XML + // Generate domain XML with proper name tag let xml = format!( r#" @@ -532,29 +616,34 @@ impl VirtualMachine { // Process active domains for domain in active_domains { let name = domain.get_name().context("Failed to get domain name")?; - // domain.get_id() already returns an Option, so we don't need .ok() let id = domain.get_id(); - - // Get domain state + + // Get state let state = match domain.get_state() { - Ok((state, _reason)) => match state { - virt::sys::VIR_DOMAIN_RUNNING => DomainState::Running, - virt::sys::VIR_DOMAIN_PAUSED => DomainState::Paused, - virt::sys::VIR_DOMAIN_SHUTDOWN => DomainState::Shutdown, - virt::sys::VIR_DOMAIN_SHUTOFF => DomainState::Shutoff, - virt::sys::VIR_DOMAIN_CRASHED => DomainState::Crashed, - _ => DomainState::Unknown, - }, + Ok((state, _)) => { + match state { + virt::sys::VIR_DOMAIN_RUNNING => DomainState::Running, + virt::sys::VIR_DOMAIN_PAUSED => DomainState::Paused, + virt::sys::VIR_DOMAIN_SHUTDOWN => DomainState::Shutdown, + virt::sys::VIR_DOMAIN_SHUTOFF => DomainState::Shutoff, + virt::sys::VIR_DOMAIN_CRASHED => DomainState::Crashed, + _ => DomainState::Unknown, + } + } Err(_) => DomainState::Unknown, }; - domain_infos.push(DomainInfo { id, name, state }); + 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, @@ -562,81 +651,39 @@ impl VirtualMachine { }); } - // 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 with filtering options - pub fn print_domain_list( - uri: Option<&str>, - show_all: bool, - show_running: bool, - show_inactive: bool, - ) -> Result<()> { + pub fn print_domain_list(uri: Option<&str>, show_all: bool, show_running: bool, show_inactive: bool) -> Result<()> { + // Get domain list let domains = Self::list_domains(uri)?; if domains.is_empty() { - println!("No domains found"); - return Ok(()); - } - - // Determine filtering logic - let use_filters = !show_all && (show_running || show_inactive); - - // Filter domains based on flags if needed - let filtered_domains: Vec<_> = if use_filters { - domains - .into_iter() - .filter(|domain| { - (show_running && domain.state == DomainState::Running) - || (show_inactive && domain.id.is_none()) - }) - .collect() - } else { - domains - }; - - if filtered_domains.is_empty() { - println!("No domains found matching the specified criteria"); + println!("No domains found."); return Ok(()); } // Print header - println!("{:<5} {:<30} {:<10}", "ID", "Name", "State"); - println!("{:-<5} {:-<30} {:-<10}", "", "", ""); + println!("{:<5} {:<20} {:<10}", "ID", "Name", "State"); + println!("{:<5} {:<20} {:<10}", "-----", "--------------------", "----------"); - // Print domains - for domain in filtered_domains { - let id_str = match domain.id { + // Print domain information + for domain in domains { + let id = 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 { - let mut disk_paths = Vec::new(); + let is_running = domain.state == DomainState::Running; + let is_inactive = domain.state == DomainState::Shutoff; - for line in xml.lines() { - if line.contains("