WIP: automating VM deployment
This commit is contained in:
136
packages/system/virt/src/rhai/cloudhv_builder.rs
Normal file
136
packages/system/virt/src/rhai/cloudhv_builder.rs
Normal 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(())
|
||||
}
|
48
packages/system/virt/src/rhai/hostcheck.rs
Normal file
48
packages/system/virt/src/rhai/hostcheck.rs
Normal 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(())
|
||||
}
|
98
packages/system/virt/src/rhai/image_prep.rs
Normal file
98
packages/system/virt/src/rhai/image_prep.rs
Normal 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(())
|
||||
}
|
Reference in New Issue
Block a user