diff --git a/src/cloudinit.rs b/src/cloudinit.rs index a315c0d..03315d1 100644 --- a/src/cloudinit.rs +++ b/src/cloudinit.rs @@ -87,7 +87,7 @@ runcmd: ) -> Result { 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 { // 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." )) } -} \ No newline at end of file +} diff --git a/src/domain.rs b/src/domain.rs index 59a0236..811165b 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -49,4 +49,4 @@ pub fn extract_disk_paths_from_xml(xml: &str) -> Vec { } disk_paths -} \ No newline at end of file +} diff --git a/src/image.rs b/src/image.rs index a38f9c9..9c792f8 100644 --- a/src/image.rs +++ b/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 { 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 { 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) } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 84d0462..a37afce 100644 --- a/src/lib.rs +++ b/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; \ No newline at end of file +pub use vm::VirtualMachine; diff --git a/src/main.rs b/src/main.rs index e1860d6..c3eb9fb 100644 --- a/src/main.rs +++ b/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() { } } } -} \ No newline at end of file +} diff --git a/src/network.rs b/src/network.rs index e69de29..8b13789 100644 --- a/src/network.rs +++ b/src/network.rs @@ -0,0 +1 @@ + diff --git a/src/vm.rs b/src/vm.rs index 3015fc5..0045766 100644 --- a/src/vm.rs +++ b/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 { 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(()) } -} \ No newline at end of file + + pub fn domain_exists(&self) -> Result { + // 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 + } +}