Browse Source

feat: download and bcreate image

rust
Giovanni Torres 12 months ago
parent
commit
eddb9b4247
  1. 22
      src/cloudinit.rs
  2. 2
      src/domain.rs
  3. 96
      src/image.rs
  4. 6
      src/lib.rs
  5. 44
      src/main.rs
  6. 1
      src/network.rs
  7. 231
      src/vm.rs

22
src/cloudinit.rs

@ -87,7 +87,7 @@ runcmd:
) -> Result<PathBuf> {
info!("Creating cloud-init ISO for VM: {}", vm_name);
println!("Creating cloud-init ISO for VM: {}", vm_name);
debug!("Work directory: {}", work_dir.display());
let user_data_path = work_dir.join("user-data");
@ -115,19 +115,17 @@ runcmd:
// Check for genisoimage or mkisofs
debug!("Checking for ISO creation tools");
// Check if genisoimage is available
let mut cmd;
// Fixed approach - avoid directly chaining methods that create temporary values
let has_genisoimage = {
let result = Command::new("genisoimage")
.arg("--version")
.output();
let result = Command::new("genisoimage").arg("--version").output();
debug!("Checking for genisoimage: {:?}", result.is_ok());
result.is_ok()
};
if has_genisoimage {
info!("Using genisoimage to create ISO");
println!("Using genisoimage to create ISO");
@ -146,13 +144,11 @@ runcmd:
} else {
// Check if mkisofs is available
let has_mkisofs = {
let result = Command::new("mkisofs")
.arg("--version")
.output();
let result = Command::new("mkisofs").arg("--version").output();
debug!("Checking for mkisofs: {:?}", result.is_ok());
result.is_ok()
};
if has_mkisofs {
info!("Using mkisofs to create ISO");
println!("Using mkisofs to create ISO");
@ -198,7 +194,7 @@ runcmd:
info!("Cloud-init ISO creation completed successfully");
Ok(iso_path)
}
/// Find an SSH public key
pub fn find_ssh_public_key() -> Result<String> {
// Try to find a suitable key file
@ -222,4 +218,4 @@ runcmd:
"No SSH public key found. Please generate an SSH keypair using 'ssh-keygen' or specify one with the '-k' flag."
))
}
}
}

2
src/domain.rs

@ -49,4 +49,4 @@ pub fn extract_disk_paths_from_xml(xml: &str) -> Vec<String> {
}
disk_paths
}
}

96
src/image.rs

@ -1,5 +1,5 @@
use anyhow::{Context, Result};
use crate::vm::DistroInfo;
use anyhow::{Context, Result};
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest;
@ -112,61 +112,61 @@ impl ImageManager {
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()
@ -174,41 +174,41 @@ impl ImageManager {
.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)
}
@ -236,14 +236,19 @@ impl ImageManager {
let mut cmd = Command::new("qemu-img");
cmd.args(&[
"create",
"-f", "qcow2",
"-F", "qcow2",
"-b", source_path.to_str().unwrap(),
"-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")?;
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"));
@ -258,7 +263,9 @@ impl ImageManager {
]);
debug!("Executing command: {:?}", resize_cmd);
let resize_status = resize_cmd.status().context("Failed to execute qemu-img resize command")?;
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"));
@ -271,29 +278,28 @@ impl ImageManager {
/// 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(),
]);
cmd.args(&["check", image_path.to_str().unwrap()]);
debug!("Executing command: {:?}", cmd);
let output = cmd.output().context("Failed to execute qemu-img check command")?;
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)
}
@ -301,7 +307,7 @@ impl ImageManager {
/// 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")?;
@ -309,7 +315,7 @@ impl ImageManager {
} else {
info!("Image does not exist, nothing to delete");
}
Ok(())
}
@ -318,18 +324,18 @@ impl ImageManager {
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)
}
}
}

6
src/lib.rs

@ -3,14 +3,14 @@ pub mod cloudinit;
pub mod config;
pub mod domain;
pub mod image;
pub mod vm;
pub mod network;
pub mod vm;
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 image::ImageManager;
pub use vm::DistroInfo;
pub use image::ImageManager;
pub use vm::VirtualMachine;

44
src/main.rs

@ -1,7 +1,5 @@
use clap::Parser;
use kvm_install_vm::{
Cli, Commands, Config, VirtualMachine
};
use kvm_install_vm::{Cli, Commands, Config, VirtualMachine};
use std::io::Write;
use std::process;
use tracing::{debug, error, info};
@ -28,7 +26,7 @@ fn print_progress(msg: &str) {
fn main() {
let cli = Cli::parse();
// Simple logging setup
if cli.verbose {
tracing_subscriber::fmt()
@ -54,8 +52,10 @@ fn main() {
print_progress(&format!("Starting kvm-install-vm for VM: {}", name));
print_progress(&format!("Distribution: {}", distro));
debug!("Configuration: vCPUs={}, Memory={}MB, Disk={}GB, Graphics={}",
vcpus, memory_mb, disk_size_gb, graphics);
debug!(
"Configuration: vCPUs={}, Memory={}MB, Disk={}GB, Graphics={}",
vcpus, memory_mb, disk_size_gb, graphics
);
if *dry_run {
print_progress("Dry run mode - not creating VM");
@ -68,7 +68,7 @@ fn main() {
Ok(config) => {
println!("\x1b[32mOK\x1b[0m");
config
},
}
Err(e) => {
println!("\x1b[31mFAILED\x1b[0m");
eprintln!(" Error: {}", e);
@ -80,7 +80,13 @@ fn main() {
// Initialize VM instance
print_status_start("Creating VM instance");
let vm_name = name.clone();
let mut vm = VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, String::new());
let mut vm = VirtualMachine::new(
name.clone(),
*vcpus,
*memory_mb,
*disk_size_gb,
String::new(),
);
println!("\x1b[32mOK\x1b[0m");
// Connect to libvirt
@ -101,7 +107,7 @@ fn main() {
match vm.prepare_image(distro, &config).await {
Ok(_) => {
println!("\x1b[32mOK\x1b[0m");
print_status_start("Creating virtual machine");
match vm.create() {
Ok(domain) => {
@ -121,7 +127,7 @@ fn main() {
process::exit(1);
}
}
},
}
Err(e) => {
println!("\x1b[31mFAILED\x1b[0m");
eprintln!(" Error: {}", e);
@ -136,13 +142,19 @@ fn main() {
info!("Starting VM destruction process for: {}", name);
print_progress(&format!("Destroying VM: {}", name));
debug!("Destroying parameters - Name: {}, Remove Disk: {}", name, remove_disk);
debug!(
"Destroying parameters - Name: {}, Remove Disk: {}",
name, remove_disk
);
print_status_start("Destroying virtual machine");
match VirtualMachine::destroy(name, None, *remove_disk) {
Ok(()) => {
println!("\x1b[32mOK\x1b[0m");
print_progress(&format!("VM '{}' destroy operation completed successfully", name));
print_progress(&format!(
"VM '{}' destroy operation completed successfully",
name
));
info!("VM '{}' destroyed successfully", name);
}
Err(e) => {
@ -165,8 +177,10 @@ fn main() {
// Determine which types of domains to list
let show_all = *all || (!*running && !*inactive);
debug!("List parameters - All: {}, Running: {}, Inactive: {}, Show all: {}",
all, running, inactive, show_all);
debug!(
"List parameters - All: {}, Running: {}, Inactive: {}, Show all: {}",
all, running, inactive, show_all
);
match VirtualMachine::print_domain_list(None, show_all, *running, *inactive) {
Ok(()) => {
@ -180,4 +194,4 @@ fn main() {
}
}
}
}
}

1
src/network.rs

@ -0,0 +1 @@

231
src/vm.rs

@ -13,9 +13,9 @@ use virt::connect::Connect;
use virt::domain::Domain;
use virt::sys;
use crate::cloudinit::CloudInitManager;
use crate::config::Config;
use crate::domain::{DomainInfo, DomainState, extract_disk_paths_from_xml};
use crate::cloudinit::CloudInitManager;
pub struct VirtualMachine {
pub name: String,
@ -138,61 +138,61 @@ impl ImageManager {
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()
@ -200,41 +200,41 @@ impl ImageManager {
.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)
}
}
@ -281,50 +281,70 @@ impl VirtualMachine {
#[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);
// First check if domain exists in libvirt
if let Ok(true) = self.domain_exists() {
return Err(anyhow::anyhow!(
"Domain '{}' already exists in libvirt",
self.name
));
}
// Check if VM files exist on disk
if self.vm_files_exist() {
return Err(anyhow::anyhow!(
"VM '{}' files already exist on disk. Use destroy command with --remove-disk flag first",
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))
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(),
"-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);
@ -334,19 +354,19 @@ impl VirtualMachine {
&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,
@ -356,16 +376,12 @@ impl VirtualMachine {
&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,
)?;
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(())
}
@ -390,6 +406,10 @@ impl VirtualMachine {
}
};
if self.domain_exists()? {
return Err(anyhow::anyhow!("Domain '{}' already exists", self.name));
}
// Check if disk image exists and create if needed
if !Path::new(&self.disk_path).exists() {
debug!("Disk image doesn't exist, creating it");
@ -458,9 +478,7 @@ impl VirtualMachine {
debug!("Executing command: {:?}", cmd);
// Execute the command
let output = cmd
.output()
.context("Failed to execute qemu-img command")?;
let output = cmd.output().context("Failed to execute qemu-img command")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@ -542,11 +560,17 @@ impl VirtualMachine {
Ok(_) => {
info!("Domain stopped successfully");
println!("Domain stopped successfully");
},
}
Err(e) => {
warn!("Warning: Failed to stop domain cleanly: {}. Continuing with undefine...", e);
println!("Warning: Failed to stop domain cleanly: {}. Continuing with undefine...", e);
},
warn!(
"Warning: Failed to stop domain cleanly: {}. Continuing with undefine...",
e
);
println!(
"Warning: Failed to stop domain cleanly: {}. Continuing with undefine...",
e
);
}
}
} else {
info!("Domain '{}' is already stopped", name);
@ -576,11 +600,11 @@ impl VirtualMachine {
Ok(_) => {
info!("Successfully removed disk: {}", path);
println!("Successfully removed disk: {}", path);
},
}
Err(e) => {
warn!("Warning: Failed to remove disk {}: {}", path, e);
println!("Warning: Failed to remove disk {}: {}", path, e);
},
}
}
}
} else if !disk_paths.is_empty() {
@ -617,33 +641,27 @@ impl VirtualMachine {
for domain in active_domains {
let name = domain.get_name().context("Failed to get domain name")?;
let id = domain.get_id();
// Get state
let state = match domain.get_state() {
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,
}
}
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,
@ -654,7 +672,12 @@ impl VirtualMachine {
Ok(domain_infos)
}
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)?;
@ -665,7 +688,10 @@ impl VirtualMachine {
// Print header
println!("{:<5} {:<20} {:<10}", "ID", "Name", "State");
println!("{:<5} {:<20} {:<10}", "-----", "--------------------", "----------");
println!(
"{:<5} {:<20} {:<10}",
"-----", "--------------------", "----------"
);
// Print domain information
for domain in domains {
@ -677,13 +703,60 @@ impl VirtualMachine {
let is_running = domain.state == DomainState::Running;
let is_inactive = domain.state == DomainState::Shutoff;
if show_all ||
(show_running && is_running) ||
(show_inactive && is_inactive) {
if show_all || (show_running && is_running) || (show_inactive && is_inactive) {
println!("{:<5} {:<20} {:<10}", id, domain.name, domain.state);
}
}
Ok(())
}
}
pub fn domain_exists(&self) -> Result<bool> {
// Ensure connection is established
let conn = match &self.connection {
Some(c) => c,
None => {
return Err(anyhow::anyhow!(
"Connection not established before domain_exists() call"
));
}
};
// Try to lookup the domain by name
match virt::domain::Domain::lookup_by_name(conn, &self.name) {
Ok(_) => {
// Domain exists
info!("Domain '{}' already exists", self.name);
Ok(true)
}
Err(e) => {
if e.code() == virt::error::ErrorNumber::DomExist {
debug!("Domain '{}' does not exist", self.name);
Ok(false)
} else {
// Some other error occurred
error!("Error checking if domain exists: {}", e);
Err(anyhow::anyhow!("Error checking if domain exists: {}", e))
}
}
}
}
pub fn vm_files_exist(&self) -> bool {
// Check if VM directory exists
if let Some(parent) = Path::new(&self.disk_path).parent() {
if parent.exists() {
debug!("VM directory already exists: {}", parent.display());
return true;
}
}
// Check if disk file exists
if Path::new(&self.disk_path).exists() {
debug!("VM disk already exists: {}", self.disk_path);
return true;
}
false
}
}

Loading…
Cancel
Save