Browse Source

refactor: simplified logging

rust
Giovanni Torres 1 year ago
parent
commit
a8f415af5f
  1. 302
      Cargo.lock
  2. 4
      Cargo.toml
  3. 3
      src/cli.rs
  4. 164
      src/cloudinit.rs
  5. 111
      src/config.rs
  6. 4
      src/lib.rs
  7. 102
      src/main.rs
  8. 264
      src/vm.rs
  9. 36
      tests/cloud_init_tests.rs
  10. 49
      tests/config_tests.rs
  11. 48
      tests/image_manager_tests.rs
  12. 75
      tests/integration_test.rs
  13. 81
      tests/vm_tests2.rs

302
Cargo.lock generated

@ -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"

4
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"

3
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,
}

164
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<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");
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<String> {
// 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."
))
}
}

111
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<String, DistroInfo>,
#[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<P: AsRef<Path>>(path: P) -> Result<Self> {
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<Self> {
// 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<P: AsRef<Path>>(&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(),
}
}
}
}

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

102
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);
}
}
}
}
}
}

264
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<Connect>,
}
@ -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<PathBuf> {
let image_path = self.get_image_path(distro_info);
if image_path.exists() {
info!("Cloud image already exists: {}", image_path.display());
println!("Cloud image already exists: {}", image_path.display());
return Ok(image_path);
}
// Create image directory if it doesn't exist
if !self.image_dir.exists() {
fs::create_dir_all(&self.image_dir)
.context("Failed to create image directory")?;
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<PathBuf> {
let disk_path = vm_dir.join(format!("{}.qcow2", vm_name));
// Create VM directory if it doesn't exist
fs::create_dir_all(vm_dir)
.context("Failed to create VM directory")?;
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<Domain> {
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<String> {
}
disk_paths
}
}

36
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"));
}
}
}
}

49
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(())
}
}

48
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(())
}
}

75
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(())
}
}

81
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("<name>test-xml-vm</name>"));
assert!(xml.contains("<memory"));
@ -73,10 +63,10 @@ fn test_domain_creation_xml() -> Result<()> {
assert!(xml.contains("<interface type='user'>"));
assert!(!xml.contains("<interface type='network'>"));
}
// 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(())
}
}

Loading…
Cancel
Save