Browse Source

refactor: extract domain and image from vm

rust
Giovanni Torres 12 months ago
parent
commit
1c3c366b6d
  1. 52
      src/domain.rs
  2. 335
      src/image.rs
  3. 10
      src/lib.rs
  4. 80
      src/main.rs
  5. 0
      src/network.rs
  6. 407
      src/vm.rs

52
src/domain.rs

@ -0,0 +1,52 @@
use std::fmt;
#[derive(Debug)]
pub struct DomainInfo {
pub id: Option<u32>, // 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<String> {
let mut disk_paths = Vec::new();
for line in xml.lines() {
if line.contains("<source file=") {
if let Some(start) = line.find("file='") {
if let Some(end) = line[start + 6..].find('\'') {
disk_paths.push(line[start + 6..start + 6 + end].to_string());
}
} else if let Some(start) = line.find("file=\"") {
if let Some(end) = line[start + 6..].find('\"') {
disk_paths.push(line[start + 6..start + 6 + end].to_string());
}
}
}
}
disk_paths
}

335
src/image.rs

@ -0,0 +1,335 @@
use anyhow::{Context, Result};
use crate::vm::DistroInfo;
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tracing::{debug, info, instrument, warn};
pub struct ImageManager {
image_dir: PathBuf,
}
impl ImageManager {
/// Create a new ImageManager with the specified image directory
pub fn new<P: AsRef<Path>>(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<PathBuf> {
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<PathBuf> {
// 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<PathBuf> {
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<bool> {
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<Vec<PathBuf>> {
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)
}
}

10
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;

80
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 } => {

0
src/network.rs

407
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<u32>, // 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<P: AsRef<Path>>(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<PathBuf> {
// 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<PathBuf> {
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<PathBuf> {
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<Domain> {
info!("Creating VM: {}", self.name);
@ -389,7 +473,7 @@ impl VirtualMachine {
}
fn generate_domain_xml(&self) -> Result<String> {
// Generate domain XML
// Generate domain XML with proper name tag
let xml = format!(
r#"
<domain type='kvm'>
@ -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<u32>, 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<String> {
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("<source file=") {
if let Some(start) = line.find("file='") {
if let Some(end) = line[start + 6..].find('\'') {
disk_paths.push(line[start + 6..start + 6 + end].to_string());
}
} else if let Some(start) = line.find("file=\"") {
if let Some(end) = line[start + 6..].find('\"') {
disk_paths.push(line[start + 6..start + 6 + end].to_string());
}
if show_all ||
(show_running && is_running) ||
(show_inactive && is_inactive) {
println!("{:<5} {:<20} {:<10}", id, domain.name, domain.state);
}
}
}
disk_paths
Ok(())
}
}
Loading…
Cancel
Save