From a8f415af5fa0422ead23a634a49e873c66d9da2c Mon Sep 17 00:00:00 2001 From: Giovanni Torres Date: Thu, 20 Mar 2025 11:56:44 -0400 Subject: [PATCH] refactor: simplified logging --- Cargo.lock | 302 +++++++++++++++++++++++++++-------- Cargo.toml | 4 +- src/cli.rs | 3 + src/cloudinit.rs | 164 +++++++++++++------ src/config.rs | 111 +++++++------ src/lib.rs | 4 +- src/main.rs | 102 +++++++++--- src/vm.rs | 264 +++++++++++++++++++++--------- tests/cloud_init_tests.rs | 36 ++--- tests/config_tests.rs | 49 +++--- tests/image_manager_tests.rs | 48 +++--- tests/integration_test.rs | 75 ++++----- tests/vm_tests2.rs | 81 ++++------ 13 files changed, 828 insertions(+), 415 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b925eb..0273fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils 0.8.21", +] + [[package]] name = "crossbeam-deque" version = "0.7.4" @@ -252,7 +261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" dependencies = [ "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -264,7 +273,7 @@ checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg", "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", "memoffset", @@ -278,7 +287,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -293,6 +302,21 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "dirs" version = "6.0.0" @@ -340,29 +364,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -849,30 +850,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jiff" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -890,7 +867,6 @@ dependencies = [ "anyhow", "clap", "dirs", - "env_logger", "futures-util", "indicatif", "log", @@ -901,6 +877,9 @@ dependencies = [ "tokio", "tokio-fs", "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", "virt", ] @@ -954,6 +933,15 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -1018,6 +1006,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1099,6 +1103,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1153,13 +1163,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] -name = "portable-atomic-util" -version = "0.2.4" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" @@ -1202,7 +1209,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -1213,8 +1220,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1225,9 +1241,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1455,6 +1477,15 @@ dependencies = [ "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1578,13 +1609,33 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1598,6 +1649,47 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1632,7 +1724,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures", ] @@ -1697,7 +1789,7 @@ checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" dependencies = [ "crossbeam-deque", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "futures", "lazy_static", "log", @@ -1787,9 +1879,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.33" @@ -1797,6 +1913,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1858,6 +2004,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2013,6 +2165,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 7805ce7..9f83d63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ repository = "https://github.com/giovtorres/kvm-install-vm" anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } dirs = "6.0" -env_logger = "0.11" futures-util = "0.3" indicatif = "0.17" log = "0.4" @@ -19,6 +18,9 @@ reqwest = { version = "0.12", features = ["stream"] } serde = { version = "1.0", features = ["derive"] } tempfile = "3.19" toml = "0.8" +tracing = "0.1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } virt = "0.4" tokio = { version = "1.44", features = ["full"] } tokio-fs = "0.1" diff --git a/src/cli.rs b/src/cli.rs index 14b3389..72798fd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,6 +2,9 @@ use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] pub struct Cli { + #[arg(short = 'v', long, global = true)] + pub verbose: bool, + #[command(subcommand)] pub command: Commands, } diff --git a/src/cloudinit.rs b/src/cloudinit.rs index c9e644e..a315c0d 100644 --- a/src/cloudinit.rs +++ b/src/cloudinit.rs @@ -1,8 +1,9 @@ -// cloudinit.rs - New file +// cloudinit.rs - Simplified version without logger dependency use anyhow::{Context, Result}; -use std::path::{Path, PathBuf}; use std::fs; +use std::path::{Path, PathBuf}; use std::process::Command; +use tracing::{debug, error, info, instrument, trace, warn}; /// Represents a cloud-init configuration manager pub struct CloudInitManager; @@ -19,11 +20,8 @@ impl CloudInitManager { cloud_init_disable: &str, ) -> Result<(String, String)> { // Create meta-data content - let meta_data = format!( - "instance-id: {}\nlocal-hostname: {}\n", - vm_name, vm_name - ); - + let meta_data = format!("instance-id: {}\nlocal-hostname: {}\n", vm_name, vm_name); + // Create user-data content with cloud-init multipart format let user_data = format!( r#"Content-Type: multipart/mixed; boundary="==BOUNDARY==" @@ -75,93 +73,153 @@ runcmd: timezone = timezone, cloud_init_disable = cloud_init_disable ); - + Ok((user_data, meta_data)) } - + /// Create a cloud-init ISO from user-data and meta-data + #[instrument(skip(user_data, meta_data), fields(vm_name = %vm_name))] pub fn create_cloud_init_iso( work_dir: &Path, vm_name: &str, user_data: &str, - meta_data: &str + meta_data: &str, ) -> 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"); let meta_data_path = work_dir.join("meta-data"); let iso_path = work_dir.join(format!("{}-cidata.iso", vm_name)); - + + debug!("User data path: {}", user_data_path.display()); + debug!("Meta data path: {}", meta_data_path.display()); + debug!("ISO path: {}", iso_path.display()); + // Make sure the directory exists - fs::create_dir_all(work_dir) - .context("Failed to create working directory")?; - + if !work_dir.exists() { + debug!("Creating working directory: {}", work_dir.display()); + fs::create_dir_all(work_dir).context("Failed to create working directory")?; + } + // Write files - fs::write(&user_data_path, user_data) - .context("Failed to write user-data file")?; - fs::write(&meta_data_path, meta_data) - .context("Failed to write meta-data file")?; - + debug!("Writing user-data file"); + trace!("User data content: {}", user_data); + fs::write(&user_data_path, user_data).context("Failed to write user-data file")?; + + debug!("Writing meta-data file"); + trace!("Meta data content: {}", meta_data); + fs::write(&meta_data_path, meta_data).context("Failed to write meta-data file")?; + // Check for genisoimage or mkisofs + debug!("Checking for ISO creation tools"); + + // Check if genisoimage is available let mut cmd; - if Command::new("genisoimage").arg("--version").output().is_ok() { + + // Fixed approach - avoid directly chaining methods that create temporary values + let has_genisoimage = { + 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"); cmd = Command::new("genisoimage"); cmd.args(&[ - "-output", iso_path.to_str().unwrap(), - "-volid", "cidata", - "-joliet", "-rock", - user_data_path.to_str().unwrap(), - meta_data_path.to_str().unwrap(), - ]); - } else if Command::new("mkisofs").arg("--version").output().is_ok() { - cmd = Command::new("mkisofs"); - cmd.args(&[ - "-o", iso_path.to_str().unwrap(), - "-V", "cidata", - "-J", "-r", + "-output", + iso_path.to_str().unwrap(), + "-volid", + "cidata", + "-joliet", + "-rock", user_data_path.to_str().unwrap(), meta_data_path.to_str().unwrap(), ]); + debug!("genisoimage command: {:?}", cmd); } else { - return Err(anyhow::anyhow!("Neither genisoimage nor mkisofs found. Please install one of these tools.")); + // Check if mkisofs is available + let has_mkisofs = { + 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"); + cmd = Command::new("mkisofs"); + cmd.args(&[ + "-o", + iso_path.to_str().unwrap(), + "-V", + "cidata", + "-J", + "-r", + user_data_path.to_str().unwrap(), + meta_data_path.to_str().unwrap(), + ]); + debug!("mkisofs command: {:?}", cmd); + } else { + error!("Neither genisoimage nor mkisofs found"); + return Err(anyhow::anyhow!( + "Neither genisoimage nor mkisofs found. Please install one of these tools." + )); + } } - + // Run the command - let status = cmd.status() + debug!("Executing ISO creation command"); + let status = cmd + .status() .context("Failed to execute ISO creation command")?; - + if !status.success() { + error!("ISO creation command failed with status: {}", status); return Err(anyhow::anyhow!("Failed to create cloud-init ISO")); } - + + info!("ISO created successfully: {}", iso_path.display()); + println!("ISO created successfully: {}", iso_path.display()); + // Remove temporary files - fs::remove_file(user_data_path) - .context("Failed to clean up user-data file")?; - fs::remove_file(meta_data_path) - .context("Failed to clean up meta-data file")?; - + debug!("Cleaning up temporary files"); + fs::remove_file(&user_data_path).context("Failed to clean up user-data file")?; + fs::remove_file(&meta_data_path).context("Failed to clean up meta-data file")?; + + 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 - let possible_keys = [ - "id_rsa.pub", - "id_ed25519.pub", - "id_dsa.pub", - ]; - + let possible_keys = ["id_rsa.pub", "id_ed25519.pub", "id_dsa.pub"]; + if let Some(home) = dirs::home_dir() { let ssh_dir = home.join(".ssh"); - + for key_name in possible_keys.iter() { let key_path = ssh_dir.join(key_name); if key_path.exists() { - return fs::read_to_string(&key_path) - .context(format!("Failed to read SSH key from {}", key_path.display())); + return fs::read_to_string(&key_path).context(format!( + "Failed to read SSH key from {}", + key_path.display() + )); } } } - - Err(anyhow::anyhow!("No SSH public key found. Please generate an SSH keypair using 'ssh-keygen' or specify one with the '-k' flag.")) + + Err(anyhow::anyhow!( + "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/config.rs b/src/config.rs index bd3b89d..d5b38ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,14 @@ +use crate::vm::DistroInfo; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::path::{Path, PathBuf}; use std::fs; -use anyhow::{Context, Result}; -use crate::vm::DistroInfo; +use std::path::{Path, PathBuf}; #[derive(Debug, Deserialize, Serialize)] pub struct Config { pub distros: HashMap, - + #[serde(default)] pub defaults: DefaultConfig, } @@ -27,7 +27,7 @@ pub struct DefaultConfig { impl Default for DefaultConfig { fn default() -> Self { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - + DefaultConfig { memory_mb: 1024, vcpus: 1, @@ -43,16 +43,18 @@ impl Default for DefaultConfig { impl Config { /// Load configuration from a specified path pub fn from_file>(path: P) -> Result { - let content = fs::read_to_string(&path) - .context(format!("Failed to read config file: {}", path.as_ref().display()))?; - + let content = fs::read_to_string(&path).context(format!( + "Failed to read config file: {}", + path.as_ref().display() + ))?; + // Changed from serde_yaml to toml - let config: Config = toml::from_str(&content) - .context("Failed to parse TOML configuration")?; - + let config: Config = + toml::from_str(&content).context("Failed to parse TOML configuration")?; + Ok(config) } - + /// Load configuration from the default locations pub fn load() -> Result { // Check for config in user's config directory @@ -63,7 +65,7 @@ impl Config { return Self::from_file(user_config); } } - + // Check for config in user's home directory if let Some(home_dir) = dirs::home_dir() { // Changed file extension from yaml to toml @@ -71,44 +73,48 @@ impl Config { if home_config.exists() { return Self::from_file(home_config); } - + // Legacy location let old_config = home_dir.join(".kivrc"); if old_config.exists() { // TODO: Convert legacy config format if needed - println!("Legacy .kivrc file found but not supported. Please convert to TOML format."); + println!( + "Legacy .kivrc file found but not supported. Please convert to TOML format." + ); } } - + // Check for system-wide config // Changed file extension from yaml to toml let system_config = Path::new("/etc/kvm-install-vm/config.toml"); if system_config.exists() { return Self::from_file(system_config); } - + // If no config files found, return default config Ok(Self::default()) } - + /// Save configuration to a file pub fn save_to_file>(&self, path: P) -> Result<()> { // Changed from serde_yaml to toml - let toml_content = toml::to_string_pretty(self) - .context("Failed to serialize configuration to TOML")?; - + let toml_content = + toml::to_string_pretty(self).context("Failed to serialize configuration to TOML")?; + // Ensure parent directory exists if let Some(parent) = path.as_ref().parent() { fs::create_dir_all(parent) .context(format!("Failed to create directory: {}", parent.display()))?; } - - fs::write(&path, toml_content) - .context(format!("Failed to write config to: {}", path.as_ref().display()))?; - + + fs::write(&path, toml_content).context(format!( + "Failed to write config to: {}", + path.as_ref().display() + ))?; + Ok(()) } - + /// Save configuration to the default user location pub fn save_to_user_config(&self) -> Result<()> { if let Some(config_dir) = dirs::config_dir() { @@ -119,10 +125,11 @@ impl Config { Err(anyhow::anyhow!("Could not determine user config directory")) } } - + /// Get distribution info by name pub fn get_distro(&self, name: &str) -> Result<&DistroInfo> { - self.distros.get(name) + self.distros + .get(name) .ok_or_else(|| anyhow::anyhow!("Distribution '{}' not found in configuration", name)) } } @@ -131,27 +138,33 @@ impl Default for Config { fn default() -> Self { // Create a default configuration with some common distributions let mut distros = HashMap::new(); - + // CentOS 8 - distros.insert("centos8".to_string(), DistroInfo { - qcow_filename: "CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2".to_string(), - os_variant: "centos8".to_string(), - image_url: "https://cloud.centos.org/centos/8/x86_64/images".to_string(), - login_user: "centos".to_string(), - sudo_group: "wheel".to_string(), - cloud_init_disable: "systemctl disable cloud-init.service".to_string(), - }); - + distros.insert( + "centos8".to_string(), + DistroInfo { + qcow_filename: "CentOS-8-GenericCloud-8.1.1911-20200113.3.x86_64.qcow2".to_string(), + os_variant: "centos8".to_string(), + image_url: "https://cloud.centos.org/centos/8/x86_64/images".to_string(), + login_user: "centos".to_string(), + sudo_group: "wheel".to_string(), + cloud_init_disable: "systemctl disable cloud-init.service".to_string(), + }, + ); + // Ubuntu 20.04 - distros.insert("ubuntu2004".to_string(), DistroInfo { - qcow_filename: "ubuntu-20.04-server-cloudimg-amd64.img".to_string(), - os_variant: "ubuntu20.04".to_string(), - image_url: "https://cloud-images.ubuntu.com/releases/20.04/release".to_string(), - login_user: "ubuntu".to_string(), - sudo_group: "sudo".to_string(), - cloud_init_disable: "systemctl disable cloud-init.service".to_string(), - }); - + distros.insert( + "ubuntu2004".to_string(), + DistroInfo { + qcow_filename: "ubuntu-20.04-server-cloudimg-amd64.img".to_string(), + os_variant: "ubuntu20.04".to_string(), + image_url: "https://cloud-images.ubuntu.com/releases/20.04/release".to_string(), + login_user: "ubuntu".to_string(), + sudo_group: "sudo".to_string(), + cloud_init_disable: "systemctl disable cloud-init.service".to_string(), + }, + ); + // Fedora 35 distros.insert("fedora35".to_string(), DistroInfo { qcow_filename: "Fedora-Cloud-Base-35-1.2.x86_64.qcow2".to_string(), @@ -161,10 +174,10 @@ impl Default for Config { sudo_group: "wheel".to_string(), cloud_init_disable: "systemctl disable cloud-init.service".to_string(), }); - + Config { distros, defaults: DefaultConfig::default(), } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index f4ecdf3..d2c36e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod cli; -pub mod vm; -pub mod config; pub mod cloudinit; +pub mod config; +pub mod vm; pub use cli::Cli; diff --git a/src/main.rs b/src/main.rs index 9987d90..1aea061 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,42 @@ use clap::Parser; use kvm_install_vm::{Cli, cli::Commands, vm::VirtualMachine}; +use std::io::Write; use std::process; +use tracing::{debug, error, info}; -fn main() { - env_logger::init(); +/// Helper function to print status messages +fn _print_status(msg: &str, success: bool) { + if success { + println!("- {} ... \x1b[32mOK\x1b[0m", msg); + } else { + println!("- {} ... \x1b[31mFAILED\x1b[0m", msg); + } +} + +/// Helper to print a message with ellipsis without a newline +fn print_status_start(msg: &str) { + print!("- {} ... ", msg); + std::io::stdout().flush().unwrap_or(()); +} + +/// Simple progress message +fn print_progress(msg: &str) { + println!("- {}", msg); +} +fn main() { let cli = Cli::parse(); + + // Simple logging setup + if cli.verbose { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .init(); + } else { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .init(); + } match &cli.command { Commands::Create { @@ -17,53 +48,73 @@ fn main() { graphics, dry_run, } => { - println!("Starting kvm-install-vm Rust implementation..."); - println!("VM Name: {}", name); - println!("Distribution: {}", distro); + info!("Starting VM creation process for: {}", name); + print_progress(&format!("Starting kvm-install-vm for VM: {}", name)); + print_progress(&format!("Distribution: {}", distro)); - println!("Configuration:"); - println!(" vCPUs: {}", vcpus); - println!(" Memory: {} MB", memory_mb); - println!(" Disk Size: {} GB", disk_size_gb); - println!(" Graphics: {}", graphics); + debug!("Configuration: vCPUs={}, Memory={}MB, Disk={}GB, Graphics={}", + vcpus, memory_mb, disk_size_gb, graphics); if *dry_run { - println!("Dry run mode - not creating VM"); + print_progress("Dry run mode - not creating VM"); return; } let disk_path = format!("/home/giovanni/virt/images/{}.qcow2", name); + debug!("Using disk path: {}", disk_path); let vm_name = name.clone(); - let mut vm = - VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, disk_path); + print_status_start("Creating VM instance"); + let mut vm = VirtualMachine::new(name.clone(), *vcpus, *memory_mb, *disk_size_gb, disk_path); + println!("\x1b[32mOK\x1b[0m"); + print_status_start("Connecting to libvirt"); if let Err(e) = vm.connect(None) { - eprintln!("Failed to connect to libvirt: {}", e); + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to connect to libvirt: {}", e); process::exit(1); } + 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.get_id().unwrap_or(0)); + println!("Domain ID: {}", domain_id); } Err(e) => { - eprintln!("Failed to create VM: {}", e); + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to create VM: {}", e); process::exit(1); } } } Commands::Destroy { name, remove_disk } => { - println!("Destroying VM: {}", name); + info!("Starting VM destruction process for: {}", name); + print_progress(&format!("Destroying VM: {}", name)); + + debug!("Destroying parameters - Name: {}, Remove Disk: {}", name, remove_disk); + print_status_start("Destroying virtual machine"); match VirtualMachine::destroy(name, None, *remove_disk) { Ok(()) => { - println!("VM '{}' destroy operation completed successfully", name); + println!("\x1b[32mOK\x1b[0m"); + print_progress(&format!("VM '{}' destroy operation completed successfully", name)); + info!("VM '{}' destroyed successfully", name); } Err(e) => { - eprintln!("Failed to destroy VM '{}': {}", name, e); + println!("\x1b[31mFAILED\x1b[0m"); + eprintln!(" Error: {}", e); + error!("Failed to destroy VM '{}': {}", name, e); process::exit(1); } } @@ -74,18 +125,25 @@ fn main() { running, inactive, } => { - println!("Listing virtual machines..."); + info!("Listing VMs"); + print_progress("Listing virtual machines..."); // 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); + match VirtualMachine::print_domain_list(None, show_all, *running, *inactive) { - Ok(()) => {} + Ok(()) => { + info!("VM listing completed successfully"); + } Err(e) => { + error!("Failed to list domains: {}", e); eprintln!("Failed to list domains: {}", e); process::exit(1); } } } } -} +} \ No newline at end of file diff --git a/src/vm.rs b/src/vm.rs index c6e1d62..dc2f493 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -9,17 +9,26 @@ use std::path::{Path, PathBuf}; use std::process::Command; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use tracing::{debug, error, info, instrument, trace, warn}; 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 + }}; +} + pub struct VirtualMachine { pub name: String, pub vcpus: u32, pub memory_mb: u32, pub disk_size_gb: u32, pub disk_path: String, - // distro: String, pub connection: Option, } @@ -90,75 +99,80 @@ impl ImageManager { /// 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")?; + 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); - + 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 + 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(()) } @@ -168,50 +182,68 @@ impl ImageManager { base_image: &Path, vm_dir: &Path, vm_name: &str, - disk_size_gb: u32 + 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")?; - + 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 status = Command::new("qemu-img") + + let mut command = Command::new("qemu-img"); + let qemu_cmd = command .args(&[ "create", - "-f", "qcow2", - "-F", "qcow2", - "-b", &base_image.to_string_lossy(), + "-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)); + return Err(anyhow::anyhow!( + "qemu-img create failed with status: {}", + status + )); } - + // 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 status = Command::new("qemu-img") + + let mut command = Command::new("qemu-img"); + let resize_cmd = command .args(&[ "resize", &disk_path.to_string_lossy(), &format!("{}G", disk_size_gb), - ]) + ]); + + 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)); + return Err(anyhow::anyhow!( + "qemu-img resize failed with status: {}", + status + )); } } - + Ok(disk_path) } } @@ -230,57 +262,129 @@ impl VirtualMachine { memory_mb, disk_path, disk_size_gb, - // distro, connection: None, } } + #[instrument(skip(self), fields(vm_name = %self.name))] pub fn connect(&mut self, uri: Option<&str>) -> Result<()> { // Connect to libvirt daemon, default to "qemu:///session" if no URI provided let uri = uri.or(Some("qemu:///session")); - self.connection = Some(Connect::open(uri).context("Failed to connect to libvirt")?); - Ok(()) + debug!("Connecting to libvirt with URI: {:?}", uri); + + match Connect::open(uri) { + Ok(conn) => { + debug!("Successfully connected to libvirt"); + self.connection = Some(conn); + info!("Connected to libvirt daemon"); + Ok(()) + } + Err(e) => { + error!("Failed to connect to libvirt: {}", e); + Err(anyhow::anyhow!("Failed to connect to libvirt: {}", e)) + } + } } + #[instrument(skip(self), fields(vm_name = %self.name))] pub fn create(&mut self) -> Result { + info!("Creating VM: {}", self.name); + debug!( + "VM parameters: vcpus={}, memory={}MB, disk={}GB, disk_path={}", + self.vcpus, self.memory_mb, self.disk_size_gb, self.disk_path + ); + // Ensure connection is established let conn = match &self.connection { - Some(c) => c, - None => return Err(anyhow::anyhow!("Connection not established")), + Some(c) => { + debug!("Using existing libvirt connection"); + c + } + None => { + error!("Connection not established before create() call"); + return Err(anyhow::anyhow!("Connection not established")); + } }; + // Check if disk image exists and create if needed if !Path::new(&self.disk_path).exists() { + debug!("Disk image doesn't exist, creating it"); self.create_disk_image()?; + } else { + debug!("Using existing disk image: {}", self.disk_path); } + // Generate XML definition + debug!("Generating domain XML definition"); let xml = self.generate_domain_xml()?; + trace!("Generated XML: {}", xml); + + // Define domain from XML + debug!("Defining domain from XML"); + let domain = match Domain::define_xml(&conn, &xml) { + Ok(d) => { + info!("Domain defined successfully"); + d + } + Err(e) => { + error!("Failed to define domain from XML: {}", e); + return Err(anyhow::anyhow!("Failed to define domain from XML: {}", e)); + } + }; - let domain = Domain::define_xml(&conn, &xml).context("Failed to define domain from XML")?; - domain.create().context("Failed to start the domain")?; + // Start the domain + debug!("Starting the domain"); + match domain.create() { + Ok(_) => { + info!("Domain started successfully"); + } + Err(e) => { + error!("Failed to start the domain: {}", e); + return Err(anyhow::anyhow!("Failed to start the domain: {}", e)); + } + }; + info!("VM creation completed successfully"); Ok(domain) } fn create_disk_image(&self) -> Result<()> { - // Create disk image using qemu-img - let output = std::process::Command::new("qemu-img") - .args(&[ - "create", - "-f", - "qcow2", - &self.disk_path, - &format!("{}G", self.disk_size_gb), - ]) + info!("Creating disk image: {}", self.disk_path); + debug!("Disk size: {}GB, Format: qcow2", self.disk_size_gb); + + // Create parent directory if it doesn't exist + if let Some(parent) = Path::new(&self.disk_path).parent() { + if !parent.exists() { + debug!("Creating parent directory: {}", parent.display()); + fs::create_dir_all(parent) + .context(format!("Failed to create directory: {}", parent.display()))?; + } + } + + // Build the command + let mut cmd = Command::new("qemu-img"); + cmd.args(&[ + "create", + "-f", + "qcow2", + &self.disk_path, + &format!("{}G", self.disk_size_gb), + ]); + + debug!("Executing command: {:?}", cmd); + + // Execute the command + let output = cmd .output() .context("Failed to execute qemu-img command")?; if !output.status.success() { - return Err(anyhow::anyhow!( - "Failed to create disk image: {:?}", - output.stderr - )); + let stderr = String::from_utf8_lossy(&output.stderr); + error!("Failed to create disk image: {}", stderr); + return Err(anyhow::anyhow!("Failed to create disk image: {}", stderr)); } + info!("Successfully created disk image"); Ok(()) } @@ -348,15 +452,20 @@ impl VirtualMachine { // Check domain state first if domain.is_active().context("Failed to check domain state")? { + info!("Stopping running domain '{}'...", name); println!("Stopping running domain '{}'...", name); match domain.destroy() { - Ok(_) => println!("Domain stopped successfully"), - Err(e) => println!( - "Warning: Failed to stop domain cleanly: {}. Continuing with undefine...", - e - ), + 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); + }, } } else { + info!("Domain '{}' is already stopped", name); println!("Domain '{}' is already stopped", name); } @@ -371,24 +480,35 @@ impl VirtualMachine { } } + info!("Domain {} successfully undefined", name); println!("Domain {} successfully undefined", name); // Handle disk removal if requested if remove_disk && !disk_paths.is_empty() { + info!("Removing disk images..."); println!("Removing disk images..."); for path in &disk_paths { match std::fs::remove_file(path) { - Ok(_) => println!("Successfully removed disk: {}", path), - Err(e) => println!("Warning: Failed to remove disk {}: {}", path, e), + 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() { + info!("Note: The following disk images were not deleted:"); println!("Note: The following disk images were not deleted:"); for path in &disk_paths { + info!(" - {}", path); println!(" - {}", path); } } + info!("Domain {} completely destroyed", name); println!("Domain {} completely destroyed", name); Ok(()) } @@ -519,4 +639,4 @@ fn extract_disk_paths_from_xml(xml: &str) -> Vec { } disk_paths -} +} \ No newline at end of file diff --git a/tests/cloud_init_tests.rs b/tests/cloud_init_tests.rs index 195dc82..bc0bcfd 100644 --- a/tests/cloud_init_tests.rs +++ b/tests/cloud_init_tests.rs @@ -13,9 +13,9 @@ fn test_create_cloud_init_config() -> Result<()> { "testuser", "UTC", "wheel", - "systemctl disable cloud-init" + "systemctl disable cloud-init", )?; - + // Verify user_data contains expected elements assert!(user_data.contains("hostname: test-vm")); assert!(user_data.contains("fqdn: test-vm.example.local")); @@ -23,11 +23,11 @@ fn test_create_cloud_init_config() -> Result<()> { assert!(user_data.contains("groups: ['wheel']")); assert!(user_data.contains("ssh-rsa AAAAB3NzaC1yc2E... test-key")); assert!(user_data.contains("timezone: UTC")); - + // Verify meta_data contains expected elements assert!(meta_data.contains("instance-id: test-vm")); assert!(meta_data.contains("local-hostname: test-vm")); - + Ok(()) } @@ -38,39 +38,35 @@ fn test_create_cloud_init_iso() -> Result<()> { .arg("--version") .output() .is_ok(); - + let has_mkisofs = std::process::Command::new("mkisofs") .arg("--version") .output() .is_ok(); - + if !has_genisoimage && !has_mkisofs { println!("Skipping test_create_cloud_init_iso: neither genisoimage nor mkisofs available"); return Ok(()); } - + let temp_dir = tempdir()?; - + // Create test cloud-init data let user_data = "#cloud-config\nhostname: test-vm\n"; let meta_data = "instance-id: test-vm\nlocal-hostname: test-vm\n"; - + // Create ISO - let iso_path = CloudInitManager::create_cloud_init_iso( - temp_dir.path(), - "test-vm", - user_data, - meta_data - )?; - + let iso_path = + CloudInitManager::create_cloud_init_iso(temp_dir.path(), "test-vm", user_data, meta_data)?; + // Verify ISO was created assert!(iso_path.exists()); assert!(fs::metadata(&iso_path)?.len() > 0); - + // Verify the temporary files were cleaned up assert!(!temp_dir.path().join("user-data").exists()); assert!(!temp_dir.path().join("meta-data").exists()); - + Ok(()) } @@ -82,10 +78,10 @@ fn test_find_ssh_public_key() { Ok(key) => { assert!(!key.is_empty()); assert!(key.starts_with("ssh-")); - }, + } Err(e) => { // The error should mention SSH keys assert!(format!("{}", e).contains("SSH")); } } -} \ No newline at end of file +} diff --git a/tests/config_tests.rs b/tests/config_tests.rs index 2735b40..09c1841 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -7,11 +7,11 @@ use tempfile::tempdir; #[test] fn test_config_defaults() { let config = Config::default(); - + // Check that default distributions exist assert!(config.distros.contains_key("centos8")); assert!(config.distros.contains_key("ubuntu2004")); - + // Check default settings assert_eq!(config.defaults.memory_mb, 1024); assert_eq!(config.defaults.vcpus, 1); @@ -23,18 +23,21 @@ fn test_config_serialization_deserialization() -> Result<()> { // Create a temporary directory for the test let temp_dir = tempdir()?; let config_path = temp_dir.path().join("test-config.toml"); - + // Create a test configuration let mut test_distros = HashMap::new(); - test_distros.insert("test-distro".to_string(), kvm_install_vm::vm::DistroInfo { - qcow_filename: "test-image.qcow2".to_string(), - os_variant: "test-os".to_string(), - image_url: "https://example.com/images".to_string(), - login_user: "testuser".to_string(), - sudo_group: "wheel".to_string(), - cloud_init_disable: "systemctl disable cloud-init".to_string(), - }); - + test_distros.insert( + "test-distro".to_string(), + kvm_install_vm::vm::DistroInfo { + qcow_filename: "test-image.qcow2".to_string(), + os_variant: "test-os".to_string(), + image_url: "https://example.com/images".to_string(), + login_user: "testuser".to_string(), + sudo_group: "wheel".to_string(), + cloud_init_disable: "systemctl disable cloud-init".to_string(), + }, + ); + let test_defaults = DefaultConfig { memory_mb: 2048, vcpus: 2, @@ -44,16 +47,16 @@ fn test_config_serialization_deserialization() -> Result<()> { dns_domain: "test.local".to_string(), timezone: "UTC".to_string(), }; - + let original_config = Config { distros: test_distros, defaults: test_defaults, }; - + // Save and then reload the configuration original_config.save_to_file(&config_path)?; let loaded_config = Config::from_file(&config_path)?; - + // Verify the loaded configuration matches the original assert_eq!(loaded_config.distros.len(), original_config.distros.len()); assert!(loaded_config.distros.contains_key("test-distro")); @@ -63,7 +66,7 @@ fn test_config_serialization_deserialization() -> Result<()> { ); assert_eq!(loaded_config.defaults.memory_mb, 2048); assert_eq!(loaded_config.defaults.vcpus, 2); - + Ok(()) } @@ -88,31 +91,31 @@ fn test_config_from_toml_string() -> Result<()> { dns_domain = "custom.local" timezone = "America/New_York" "#; - + // Parse the TOML string let config: Config = toml::from_str(toml_str)?; - + // Verify the configuration assert_eq!(config.distros.len(), 1); assert!(config.distros.contains_key("test-distro")); assert_eq!(config.defaults.memory_mb, 4096); assert_eq!(config.defaults.vcpus, 4); assert_eq!(config.defaults.timezone, "America/New_York"); - + Ok(()) } #[test] fn test_get_distro() -> Result<()> { let config = Config::default(); - + // Test existing distro let centos = config.get_distro("centos8")?; assert_eq!(centos.os_variant, "centos8"); - + // Test non-existent distro let result = config.get_distro("nonexistent-distro"); assert!(result.is_err()); - + Ok(()) -} \ No newline at end of file +} diff --git a/tests/image_manager_tests.rs b/tests/image_manager_tests.rs index d5cce7a..cc6f903 100644 --- a/tests/image_manager_tests.rs +++ b/tests/image_manager_tests.rs @@ -21,7 +21,7 @@ fn create_test_distro() -> DistroInfo { fn test_image_manager_initialization() { let temp_dir = tempdir().unwrap(); let image_manager = ImageManager::new(temp_dir.path()); - + // Test that the image directory is set correctly assert_eq!( image_manager.get_image_path(&create_test_distro()), @@ -34,21 +34,21 @@ fn test_image_existence_check() -> Result<()> { let temp_dir = tempdir()?; let image_manager = ImageManager::new(temp_dir.path()); let distro = create_test_distro(); - + // Initially, the image should not exist assert!(!image_manager.image_exists(&distro)); - + // Create an empty file at the image path let image_path = temp_dir.path().join("test-image.qcow2"); fs::write(&image_path, b"test content")?; - + // Now the image should be reported as existing assert!(image_manager.image_exists(&distro)); - + Ok(()) } -// This test is marked as ignored because it would attempt to download +// This test is marked as ignored because it would attempt to download // a real cloud image, which we don't want in automated testing #[test] #[ignore] @@ -56,7 +56,7 @@ fn test_ensure_image() -> Result<()> { let rt = Runtime::new()?; let temp_dir = tempdir()?; let image_manager = ImageManager::new(temp_dir.path()); - + // This distro has a real URL that would be downloaded let distro = DistroInfo { qcow_filename: "cirros-0.5.1-x86_64-disk.img".to_string(), @@ -66,60 +66,64 @@ fn test_ensure_image() -> Result<()> { sudo_group: "wheel".to_string(), cloud_init_disable: "systemctl disable cloud-init".to_string(), }; - + // Use the tokio runtime to run the async function let image_path = rt.block_on(image_manager.ensure_image(&distro))?; - + // Verify the image was downloaded assert!(image_path.exists()); assert!(fs::metadata(&image_path)?.len() > 0); - + Ok(()) } #[test] fn test_prepare_vm_disk() -> Result<()> { // Skip if qemu-img is not available - if std::process::Command::new("qemu-img").arg("--version").output().is_err() { + if std::process::Command::new("qemu-img") + .arg("--version") + .output() + .is_err() + { println!("Skipping test_prepare_vm_disk: qemu-img not available"); return Ok(()); } - + let temp_dir = tempdir()?; let image_manager = ImageManager::new(temp_dir.path()); - + // Create a dummy base image let base_image = temp_dir.path().join("base.qcow2"); std::process::Command::new("qemu-img") .args(&["create", "-f", "qcow2", &base_image.to_string_lossy(), "1G"]) .output()?; - + // Create VM directory let vm_dir = temp_dir.path().join("vms/test-vm"); fs::create_dir_all(&vm_dir)?; - + // Prepare VM disk let disk_path = image_manager.prepare_vm_disk( &base_image, &vm_dir, "test-vm", - 5 // 5GB disk + 5, // 5GB disk )?; - + // Verify the disk was created assert!(disk_path.exists()); - + // Check disk properties with qemu-img info let output = std::process::Command::new("qemu-img") .args(&["info", "--output=json", &disk_path.to_string_lossy()]) .output()?; - + let info_str = String::from_utf8(output.stdout)?; assert!(info_str.contains("qcow2")); - + // Fix: Convert Cow to a regular str for comparison let base_path_str = base_image.to_string_lossy().to_string(); assert!(info_str.contains(&base_path_str)); - + Ok(()) -} \ No newline at end of file +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 5e49ce6..570a3ed 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,8 +1,8 @@ // tests/integration_test.rs use anyhow::Result; use kvm_install_vm::{ - vm::{DistroInfo, ImageManager, VirtualMachine}, cloudinit::CloudInitManager, + vm::{DistroInfo, ImageManager, VirtualMachine}, }; use std::fs; use tempfile::tempdir; @@ -12,28 +12,38 @@ use tempfile::tempdir; #[test] fn test_vm_preparation_flow() -> Result<()> { // Skip if qemu-img is not available - if std::process::Command::new("qemu-img").arg("--version").output().is_err() { + if std::process::Command::new("qemu-img") + .arg("--version") + .output() + .is_err() + { println!("Skipping test_vm_preparation_flow: qemu-img not available"); return Ok(()); } - + // Skip if neither genisoimage nor mkisofs is available - let has_iso_tool = std::process::Command::new("genisoimage").arg("--version").output().is_ok() || - std::process::Command::new("mkisofs").arg("--version").output().is_ok(); - + let has_iso_tool = std::process::Command::new("genisoimage") + .arg("--version") + .output() + .is_ok() + || std::process::Command::new("mkisofs") + .arg("--version") + .output() + .is_ok(); + if !has_iso_tool { println!("Skipping test_vm_preparation_flow: neither genisoimage nor mkisofs available"); return Ok(()); } - + // Create temporary directories let temp_dir = tempdir()?; let image_dir = temp_dir.path().join("images"); let vm_dir = temp_dir.path().join("vms"); - + fs::create_dir_all(&image_dir)?; fs::create_dir_all(&vm_dir)?; - + // Create a test distro let distro = DistroInfo { qcow_filename: "test-integration.qcow2".to_string(), @@ -43,32 +53,27 @@ fn test_vm_preparation_flow() -> Result<()> { sudo_group: "wheel".to_string(), cloud_init_disable: "systemctl disable cloud-init".to_string(), }; - + // Create a base image (since we won't download one) let base_image = image_dir.join(&distro.qcow_filename); std::process::Command::new("qemu-img") .args(&["create", "-f", "qcow2", &base_image.to_string_lossy(), "1G"]) .output()?; - + // Initialize image manager let image_manager = ImageManager::new(&image_dir); - + // Prepare VM directory let vm_name = "test-integration-vm"; let vm_dir_path = vm_dir.join(vm_name); fs::create_dir_all(&vm_dir_path)?; - + // Prepare VM disk - let disk_path = image_manager.prepare_vm_disk( - &base_image, - &vm_dir_path, - vm_name, - 5 - )?; - + let disk_path = image_manager.prepare_vm_disk(&base_image, &vm_dir_path, vm_name, 5)?; + // Create mock SSH key let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7RXIKhCmT... test@example.com"; - + // Create cloud-init configuration let (user_data, meta_data) = CloudInitManager::create_cloud_init_config( vm_name, @@ -77,17 +82,13 @@ fn test_vm_preparation_flow() -> Result<()> { "testuser", "UTC", "wheel", - "systemctl disable cloud-init" + "systemctl disable cloud-init", )?; - + // Create cloud-init ISO - let iso_path = CloudInitManager::create_cloud_init_iso( - &vm_dir_path, - vm_name, - &user_data, - &meta_data - )?; - + let iso_path = + CloudInitManager::create_cloud_init_iso(&vm_dir_path, vm_name, &user_data, &meta_data)?; + // Initialize VM (but don't create it) let vm = VirtualMachine::new( vm_name.to_string(), @@ -96,25 +97,25 @@ fn test_vm_preparation_flow() -> Result<()> { 5, disk_path.to_string_lossy().to_string(), ); - - // Since we can't call generate_domain_xml() directly, we'll verify the + + // Since we can't call generate_domain_xml() directly, we'll verify the // components we've prepared instead - + // Verify the preparation was successful assert!(disk_path.exists()); assert!(iso_path.exists()); - + // We can verify the VM setup parameters assert_eq!(vm.name, "test-integration-vm"); assert_eq!(vm.vcpus, 2); assert_eq!(vm.memory_mb, 1024); assert_eq!(vm.disk_size_gb, 5); - + // For a more complete test that would require libvirt, we could: // 1. Connect to libvirt // 2. Create the VM // 3. Inspect the actual XML via virsh // But we'll skip that to keep this a non-libvirt test - + Ok(()) -} \ No newline at end of file +} diff --git a/tests/vm_tests2.rs b/tests/vm_tests2.rs index addf227..66bd78a 100644 --- a/tests/vm_tests2.rs +++ b/tests/vm_tests2.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use kvm_install_vm::vm::{VirtualMachine, DomainState}; +use kvm_install_vm::vm::{DomainState, VirtualMachine}; use tempfile::tempdir; // Test VM creation and initialization without actually interacting with libvirt @@ -12,7 +12,7 @@ fn test_vm_initialization() { 10, "/tmp/test-vm.qcow2".to_string(), ); - + assert_eq!(vm.name, "test-vm"); assert_eq!(vm.vcpus, 2); assert_eq!(vm.memory_mb, 1024); @@ -29,17 +29,12 @@ fn test_domain_creation_xml() -> Result<()> { // Create a temporary directory for disk images let temp_dir = tempdir()?; let disk_path = temp_dir.path().join("test-xml-vm.qcow2"); - + // Create a test disk image std::process::Command::new("qemu-img") - .args(&[ - "create", - "-f", "qcow2", - &disk_path.to_string_lossy(), - "1G" - ]) + .args(&["create", "-f", "qcow2", &disk_path.to_string_lossy(), "1G"]) .output()?; - + // Create VM let mut vm = VirtualMachine::new( "test-xml-vm".to_string(), @@ -48,24 +43,19 @@ fn test_domain_creation_xml() -> Result<()> { 10, disk_path.to_string_lossy().to_string(), ); - + // Connect to libvirt vm.connect(None)?; - - + // Use virsh to dump and inspect XML // This is a workaround since we can't call generate_domain_xml directly let output = std::process::Command::new("virsh") - .args(&[ - "-c", "qemu:///session", - "dumpxml", - "test-xml-vm", - ]) + .args(&["-c", "qemu:///session", "dumpxml", "test-xml-vm"]) .output()?; - + if output.status.success() { let xml = String::from_utf8(output.stdout)?; - + // Check for relevant XML elements assert!(xml.contains("test-xml-vm")); assert!(xml.contains(" Result<()> { assert!(xml.contains("")); assert!(!xml.contains("")); } - + // Clean up let _ = VirtualMachine::destroy("test-xml-vm", None, true); - + Ok(()) } @@ -93,10 +83,10 @@ fn test_connect_to_libvirt() -> Result<()> { 1, "/tmp/test-connect-vm.qcow2".to_string(), ); - + vm.connect(None)?; assert!(vm.connection.is_some()); - + Ok(()) } @@ -105,7 +95,7 @@ fn test_connect_to_libvirt() -> Result<()> { fn test_domain_list_and_print() -> Result<()> { // Test the domain listing functionality let domains = VirtualMachine::list_domains(None)?; - + // Print the domains for debug purposes println!("Found {} domains:", domains.len()); for domain in &domains { @@ -113,20 +103,20 @@ fn test_domain_list_and_print() -> Result<()> { "Domain: {}, ID: {:?}, State: {:?}", domain.name, domain.id, domain.state ); - + // State consistency checks if domain.state == DomainState::Shutoff { assert_eq!(domain.id, None); } - + if domain.state == DomainState::Running { assert!(domain.id.is_some()); } } - + // Test the print function (just make sure it doesn't crash) VirtualMachine::print_domain_list(None, true, false, false)?; - + Ok(()) } @@ -138,17 +128,12 @@ fn test_create_and_destroy_vm() -> Result<()> { // Create a temporary directory for disk images let temp_dir = tempdir()?; let disk_path = temp_dir.path().join("test-create-destroy.qcow2"); - + // Create a test disk image std::process::Command::new("qemu-img") - .args(&[ - "create", - "-f", "qcow2", - &disk_path.to_string_lossy(), - "1G" - ]) + .args(&["create", "-f", "qcow2", &disk_path.to_string_lossy(), "1G"]) .output()?; - + // Create VM let mut vm = VirtualMachine::new( "test-create-destroy".to_string(), @@ -157,29 +142,25 @@ fn test_create_and_destroy_vm() -> Result<()> { 1, disk_path.to_string_lossy().to_string(), ); - + vm.connect(None)?; - + // Create the VM let domain = vm.create()?; - + // Verify domain exists let domain_name = domain.get_name()?; assert_eq!(domain_name, "test-create-destroy"); - + // Try to destroy it VirtualMachine::destroy("test-create-destroy", None, true)?; - + // Verify it's gone using virsh let output = std::process::Command::new("virsh") - .args(&[ - "-c", "qemu:///session", - "dominfo", - "test-create-destroy", - ]) + .args(&["-c", "qemu:///session", "dominfo", "test-create-destroy"]) .output()?; - + assert!(!output.status.success()); - + Ok(()) -} \ No newline at end of file +}