WIP: automating VM deployment

This commit is contained in:
Maxime Van Hees
2025-08-26 16:50:59 +02:00
parent 1bb731711b
commit 4b4f3371b0
10 changed files with 1213 additions and 1 deletions

View File

@@ -0,0 +1,136 @@
use crate::cloudhv::builder::CloudHvBuilder;
use crate::hostcheck::host_check_deps;
use crate::image_prep::{image_prepare, Flavor as ImgFlavor, ImagePrepOptions, NetPlanOpts};
use rhai::{Engine, EvalAltResult, Map};
fn builder_new(id: &str) -> CloudHvBuilder {
CloudHvBuilder::new(id)
}
// Functional, chainable-style helpers (consume and return the builder)
fn builder_memory_mb(mut b: CloudHvBuilder, mb: i64) -> CloudHvBuilder {
if mb > 0 {
b.memory_mb(mb as u32);
}
b
}
fn builder_vcpus(mut b: CloudHvBuilder, v: i64) -> CloudHvBuilder {
if v > 0 {
b.vcpus(v as u32);
}
b
}
fn builder_disk(mut b: CloudHvBuilder, path: &str) -> CloudHvBuilder {
b.disk(path);
b
}
fn builder_disk_from_flavor(mut b: CloudHvBuilder, flavor: &str) -> CloudHvBuilder {
b.disk_from_flavor(flavor);
b
}
fn builder_cmdline(mut b: CloudHvBuilder, c: &str) -> CloudHvBuilder {
b.cmdline(c);
b
}
fn builder_extra_arg(mut b: CloudHvBuilder, a: &str) -> CloudHvBuilder {
b.extra_arg(a);
b
}
fn builder_no_default_net(mut b: CloudHvBuilder) -> CloudHvBuilder {
b.no_default_net();
b
}
fn builder_launch(mut b: CloudHvBuilder) -> Result<String, Box<EvalAltResult>> {
b.launch().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("cloudhv builder launch failed: {}", e).into(),
rhai::Position::NONE,
))
})
}
// Noob-friendly one-shot wrapper
fn vm_easy_launch(flavor: &str, id: &str, memory_mb: i64, vcpus: i64) -> Result<String, Box<EvalAltResult>> {
// Preflight
let report = host_check_deps().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("host_check failed: {}", e).into(),
rhai::Position::NONE,
))
})?;
if !report.ok {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("missing dependencies: {:?}", report.critical).into(),
rhai::Position::NONE,
)));
}
// Prepare image to raw using defaults (DHCPv4 + placeholder v6 + disable cloud-init net)
let img_flavor = match flavor {
"ubuntu" | "Ubuntu" | "UBUNTU" => ImgFlavor::Ubuntu,
"alpine" | "Alpine" | "ALPINE" => ImgFlavor::Alpine,
_ => ImgFlavor::Ubuntu,
};
let prep_opts = ImagePrepOptions {
flavor: img_flavor,
id: id.to_string(),
source: None,
target_dir: None,
net: NetPlanOpts::default(),
disable_cloud_init_net: true,
};
let prep = image_prepare(&prep_opts).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("image_prepare failed: {}", e).into(),
rhai::Position::NONE,
))
})?;
// Build and launch
let mut b = CloudHvBuilder::new(id);
b.disk(&prep.raw_disk);
if memory_mb > 0 {
b.memory_mb(memory_mb as u32);
}
if vcpus > 0 {
b.vcpus(vcpus as u32);
}
b.launch().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("vm_easy_launch failed at launch: {}", e).into(),
rhai::Position::NONE,
))
})
}
pub fn register_cloudhv_builder_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register type
engine.register_type_with_name::<CloudHvBuilder>("CloudHvBuilder");
// Factory
engine.register_fn("cloudhv_builder", builder_new);
// Chainable methods (functional style)
engine.register_fn("memory_mb", builder_memory_mb);
engine.register_fn("vcpus", builder_vcpus);
engine.register_fn("disk", builder_disk);
engine.register_fn("disk_from_flavor", builder_disk_from_flavor);
engine.register_fn("cmdline", builder_cmdline);
engine.register_fn("extra_arg", builder_extra_arg);
engine.register_fn("no_default_net", builder_no_default_net);
// Action
engine.register_fn("launch", builder_launch);
// One-shot wrapper
engine.register_fn("vm_easy_launch", vm_easy_launch);
Ok(())
}

View File

@@ -0,0 +1,48 @@
use crate::hostcheck::{host_check_deps, HostCheckReport};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
fn report_to_map(r: &HostCheckReport) -> Map {
let mut m = Map::new();
m.insert("ok".into(), (r.ok as bool).into());
let mut crit = Array::new();
for s in &r.critical {
crit.push(s.clone().into());
}
m.insert("critical".into(), crit.into());
let mut opt = Array::new();
for s in &r.optional {
opt.push(s.clone().into());
}
m.insert("optional".into(), opt.into());
let mut notes = Array::new();
for s in &r.notes {
notes.push(s.clone().into());
}
m.insert("notes".into(), notes.into());
m
}
fn host_check() -> Result<Map, Box<EvalAltResult>> {
match host_check_deps() {
Ok(rep) => Ok(report_to_map(&rep)),
Err(e) => {
let mut m = Map::new();
m.insert("ok".into(), Dynamic::FALSE);
let mut crit = Array::new();
crit.push(format!("host_check failed: {}", e).into());
m.insert("critical".into(), crit.into());
m.insert("optional".into(), Array::new().into());
m.insert("notes".into(), Array::new().into());
Ok(m)
}
}
}
pub fn register_hostcheck_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
engine.register_fn("host_check", host_check);
Ok(())
}

View File

@@ -0,0 +1,98 @@
use crate::image_prep::{image_prepare, Flavor, ImagePrepOptions, NetPlanOpts};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
fn parse_flavor(s: &str) -> Result<Flavor, Box<EvalAltResult>> {
match s {
"ubuntu" | "Ubuntu" | "UBUNTU" => Ok(Flavor::Ubuntu),
"alpine" | "Alpine" | "ALPINE" => Ok(Flavor::Alpine),
other => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("image_prepare: invalid flavor '{}', allowed: ubuntu|alpine", other).into(),
rhai::Position::NONE,
))),
}
}
fn map_get_string(m: &Map, k: &str) -> Option<String> {
m.get(k).and_then(|v| if v.is_string() { Some(v.clone().cast::<String>()) } else { None })
}
fn map_get_bool(m: &Map, k: &str) -> Option<bool> {
m.get(k).and_then(|v| v.as_bool().ok())
}
fn net_from_map(m: Option<&Map>) -> NetPlanOpts {
let mut n = NetPlanOpts::default();
if let Some(mm) = m {
if let Some(b) = map_get_bool(mm, "dhcp4") {
n.dhcp4 = b;
}
if let Some(b) = map_get_bool(mm, "dhcp6") {
n.dhcp6 = b;
}
if let Some(s) = map_get_string(mm, "ipv6_addr") {
if !s.trim().is_empty() {
n.ipv6_addr = Some(s);
}
}
if let Some(s) = map_get_string(mm, "gw6") {
if !s.trim().is_empty() {
n.gw6 = Some(s);
}
}
}
n
}
fn image_prepare_rhai(opts: Map) -> Result<Map, Box<EvalAltResult>> {
// Required fields
let id = map_get_string(&opts, "id").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"image_prepare: missing required field 'id'".into(),
rhai::Position::NONE,
))
})?;
if id.trim().is_empty() {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"image_prepare: 'id' must not be empty".into(),
rhai::Position::NONE,
)));
}
let flavor_s = map_get_string(&opts, "flavor").unwrap_or_else(|| "ubuntu".into());
let flavor = parse_flavor(&flavor_s)?;
// Optional fields
let source = map_get_string(&opts, "source");
let target_dir = map_get_string(&opts, "target_dir");
let net = opts.get("net").and_then(|v| if v.is_map() { Some(v.clone().cast::<Map>()) } else { None });
let net_opts = net_from_map(net.as_ref());
let disable_cloud_init_net = map_get_bool(&opts, "disable_cloud_init_net").unwrap_or(true);
let o = ImagePrepOptions {
flavor,
id,
source,
target_dir,
net: net_opts,
disable_cloud_init_net,
};
let res = image_prepare(&o).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("image_prepare failed: {}", e).into(),
rhai::Position::NONE,
))
})?;
let mut out = Map::new();
out.insert("raw_disk".into(), res.raw_disk.into());
out.insert("root_uuid".into(), res.root_uuid.into());
out.insert("boot_uuid".into(), res.boot_uuid.into());
out.insert("work_qcow2".into(), res.work_qcow2.into());
Ok(out)
}
pub fn register_image_prep_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
engine.register_fn("image_prepare", image_prepare_rhai);
Ok(())
}