Init commit: basic functional
This commit is contained in:
commit
60cd7b5c81
2511
Cargo.lock
generated
Normal file
2511
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "orpheus"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = {version = "4", features = ["derive"]}
|
||||||
|
actix-web = "4"
|
||||||
|
env_logger = "0.9"
|
||||||
|
tera = "1"
|
||||||
|
log = "0.4"
|
||||||
|
figment = {version = "0.10", features = ["yaml"]}
|
||||||
|
serde_yaml_bw = "2"
|
||||||
|
yaml-rust2 = "0.10"
|
||||||
|
yaml-merge-keys = "0.8"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "2"
|
||||||
|
mac_address2 = "2"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
target = "x86_64-unknown-linux-musl"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
target = "x86_64-unknown-linux-musl"
|
||||||
53
inventory.yaml
Normal file
53
inventory.yaml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
webserver: "http://192.168.64.134:8080"
|
||||||
|
|
||||||
|
default: &default
|
||||||
|
name: _
|
||||||
|
version: alpine-3.21
|
||||||
|
type: lts
|
||||||
|
cmdline:
|
||||||
|
- str
|
||||||
|
# - ds=nocloud;{{webserver}}/k3s-agent
|
||||||
|
# - modloop={{webserver}}/{{version}}/modloop-{{type}}
|
||||||
|
|
||||||
|
.agent: &agent
|
||||||
|
version: alpine-3.19
|
||||||
|
type: lts
|
||||||
|
cmdline: []
|
||||||
|
# - ds=nocloud;{{webserver}}/k3s-agent
|
||||||
|
# - modloop={{webserver}}/{{version}}/modloop-{{type}}
|
||||||
|
# - apkovl={{webserver}}/apkovl/some-apkovl.tar.gz
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
"34:1a:4c:10:a7:08":
|
||||||
|
<<: *default
|
||||||
|
name: "hydra-0"
|
||||||
|
version: test-2.3
|
||||||
|
type: lts
|
||||||
|
cmdline:
|
||||||
|
- ds: k3s-server
|
||||||
|
- modloop
|
||||||
|
- apkovl: nginx.apkov.tar.gz
|
||||||
|
- console=tty0
|
||||||
|
- console=ASA0
|
||||||
|
|
||||||
|
|
||||||
|
"34:1a:4c:10:dc:f1":
|
||||||
|
<<: *default
|
||||||
|
<<<: *agent
|
||||||
|
name: "hydra-1"
|
||||||
|
|
||||||
|
"04:1a:4c:10:dc:f1":
|
||||||
|
<<: *agent
|
||||||
|
name: "hydra-2"
|
||||||
|
cmdline:
|
||||||
|
- init_debug
|
||||||
|
- modloop
|
||||||
|
|
||||||
|
# "34:1a:4c:10:a7:a3": # hydra-3
|
||||||
|
# "34:1a:4c:10:e2:36": # hydra-4
|
||||||
|
# "34:1a:4c:10:a6:00": # hydra-5
|
||||||
|
# "ab:cd:ef:01:23:45":
|
||||||
|
|
||||||
|
...
|
||||||
39
src/cli.rs
Normal file
39
src/cli.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use log::Level;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Path to the inventory file
|
||||||
|
#[arg(short, long, default_value = "inventory.yaml")]
|
||||||
|
pub inventory: String,
|
||||||
|
|
||||||
|
/// Host to bind the server
|
||||||
|
#[arg(short, long, default_value = "0.0.0.0")]
|
||||||
|
pub address: String,
|
||||||
|
|
||||||
|
/// Port to bind the server
|
||||||
|
#[arg(short, long, default_value_t = 8080)]
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
/// Enable debug logging
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub debug: bool,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "warn")]
|
||||||
|
pub loging: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub(crate) enum Commands {
|
||||||
|
Check,
|
||||||
|
Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_args() -> Cli {
|
||||||
|
Cli::parse()
|
||||||
|
}
|
||||||
207
src/config.rs
Normal file
207
src/config.rs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
use crate::Inventory;
|
||||||
|
use figment::{
|
||||||
|
providers::{Format, YamlExtended},
|
||||||
|
Error as FigError, Figment,
|
||||||
|
};
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use mac_address2::MacAddress;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
// #[error("Template error: {0}")]
|
||||||
|
#[error(transparent)]
|
||||||
|
Template(#[from] tera::Error),
|
||||||
|
#[error("Teplate result error: {0}")]
|
||||||
|
TemplateResult(#[from] serde_json::Error),
|
||||||
|
#[error("Inventory parsing error: {0}")]
|
||||||
|
InventorySerialization(#[from] FigError),
|
||||||
|
#[error("Inventory hosts error: {0}")]
|
||||||
|
InventoryHosts(String),
|
||||||
|
#[error("Inventory read error: {0}")]
|
||||||
|
InventoryHostsFileOpen(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_inventory(inventory_file: String) -> Result<Inventory, Error> {
|
||||||
|
// let file = File::open(&inventory_file).map_err(|e| Error::InventoryHostsFileOpen(e))?;
|
||||||
|
|
||||||
|
let inventory: Inventory = Figment::new()
|
||||||
|
.merge(YamlExtended::file(inventory_file))
|
||||||
|
.extract()
|
||||||
|
.map_err(|e| Error::InventorySerialization(e))?;
|
||||||
|
|
||||||
|
return Ok(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn parse_inventory<R: Read>(inventory_reader: R) -> Result<Inventory, Error> {
|
||||||
|
// let inventory: Inventory = YamlExtended::string(inventory_reader)
|
||||||
|
// .extract()
|
||||||
|
// .map_err(|e| Error::InventorySerialization(e))?;
|
||||||
|
//
|
||||||
|
// // Validate hosts
|
||||||
|
// if inventory.hosts.is_empty() {
|
||||||
|
// return Err(Error::InventoryHosts(
|
||||||
|
// "The 'hosts' entry is empty".to_string(),
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Validate each host entry
|
||||||
|
// for (mac, _) in &inventory.hosts {
|
||||||
|
// let _ = mac.parse::<MacAddress>().map_err(|e| {
|
||||||
|
// Error::InventoryHosts(format!(
|
||||||
|
// "The 'hosts' entry contains invalid MAC address: {}, error: {}",
|
||||||
|
// &mac, e
|
||||||
|
// ))
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Ok(inventory)
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn validate_template(inventory: &Inventory, tera: &Tera) -> Result<(), Error> {
|
||||||
|
for (_, host) in &inventory.hosts {
|
||||||
|
let context = tera::Context::from_serialize(json!(
|
||||||
|
{ "webserver": &inventory.webserver,
|
||||||
|
"host": host
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.map_err(|e| Error::Template(e))?;
|
||||||
|
|
||||||
|
// Try rendering the template for this host
|
||||||
|
let rendered_text = tera
|
||||||
|
.render("boot", &context)
|
||||||
|
.map_err(|e| Error::Template(e))?;
|
||||||
|
|
||||||
|
warn!("{}", &rendered_text);
|
||||||
|
warn!("json rendered: {:#?}", json!(&rendered_text));
|
||||||
|
|
||||||
|
let _: serde_json::Value =
|
||||||
|
serde_json::from_str(&rendered_text).map_err(|e| Error::TemplateResult(e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// #[derive(Debug)]
|
||||||
|
// struct Inv<'a> {
|
||||||
|
// data: saphyr::Yaml<'a>,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fn parse_inventory2<'a, R: Read>(yaml_str: &'a str) -> Result<Inv<'a>, Error> {
|
||||||
|
// let yaml_str = std::fs::read_to_string("inventory.yaml")?;
|
||||||
|
// let inventory: Yaml = Yaml::load_from_str(&yaml_str).unwrap()[0].clone();
|
||||||
|
// // let mut decoder = YamlDecoder::read(yaml_str.as_bytes());
|
||||||
|
// // let out = decoder.decode().map_err(|e| Error::InventoryParse(e))?;
|
||||||
|
//
|
||||||
|
// // let inventory: Inventory = out
|
||||||
|
// println!("{:#?}", inventory);
|
||||||
|
// return unimplemented!();
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn valid_inventory() {
|
||||||
|
// let inventory = r#"
|
||||||
|
// default:
|
||||||
|
// key: value
|
||||||
|
// hosts:
|
||||||
|
// aa:bb:cc:dd:ee:ff:
|
||||||
|
// key: value
|
||||||
|
// aa-bb-cc-dd-ee-ff:
|
||||||
|
// host_key: host2_value
|
||||||
|
// "#;
|
||||||
|
// let result = parse_inventory(inventory.as_bytes());
|
||||||
|
// result.unwrap();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn invalid_inventory() {
|
||||||
|
// let inventory = r#"
|
||||||
|
// default:
|
||||||
|
// key: value
|
||||||
|
// hosts:
|
||||||
|
// "#;
|
||||||
|
// assert!(parse_inventory(inventory.as_bytes()).is_err());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn invalid_host() {
|
||||||
|
// let inventory = r#"
|
||||||
|
// default:
|
||||||
|
// key: value
|
||||||
|
// hosts:
|
||||||
|
// my_test_host:
|
||||||
|
// key: value
|
||||||
|
// "#;
|
||||||
|
// assert!(parse_inventory(inventory.as_bytes()).is_err());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn valid_template() {
|
||||||
|
// let inventory_str = r#"
|
||||||
|
// default:
|
||||||
|
// key1: value1
|
||||||
|
// hosts:
|
||||||
|
// aa:bb:cc:dd:ee:ff:
|
||||||
|
// host_key: host_value
|
||||||
|
// "#;
|
||||||
|
// let inventory: Inventory = serde_yaml_bw::from_str(inventory_str).unwrap();
|
||||||
|
// let template = r#"
|
||||||
|
// {
|
||||||
|
// "host": "{{ host.host_key }}",
|
||||||
|
// "default": "{{ default.key1 }}"
|
||||||
|
// }
|
||||||
|
// "#;
|
||||||
|
// let mut tera = Tera::default();
|
||||||
|
// tera.add_raw_templates(vec![("boot", template)]).unwrap();
|
||||||
|
// validate_template(&inventory, &tera).unwrap();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn template_unexisted_variable_render() {
|
||||||
|
// let inventory_str = r#"
|
||||||
|
// default:
|
||||||
|
// key1: value1
|
||||||
|
// hosts:
|
||||||
|
// aa:bb:cc:dd:ee:ff:
|
||||||
|
// host_key: host_value
|
||||||
|
// "#;
|
||||||
|
// let inventory: Inventory = serde_yaml_bw::from_str(inventory_str).unwrap();
|
||||||
|
// let template = r#"
|
||||||
|
// "{{ unexisted.variable }}",
|
||||||
|
// "#;
|
||||||
|
// let mut tera = Tera::default();
|
||||||
|
// tera.add_raw_templates(vec![("boot", template)]).unwrap();
|
||||||
|
// assert!(validate_template(&inventory, &tera).is_err());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// #[test]
|
||||||
|
// fn template_result_invalid_json() {
|
||||||
|
// let inventory_str = r#"
|
||||||
|
// default:
|
||||||
|
// key1: value1
|
||||||
|
// hosts:
|
||||||
|
// aa:bb:cc:dd:ee:ff:
|
||||||
|
// host_key: host1_value
|
||||||
|
// "#;
|
||||||
|
// let inventory: Inventory = serde_yaml_bw::from_str(inventory_str).unwrap();
|
||||||
|
// let template = r#"
|
||||||
|
// Not a valid JSON string containing variable: {{ host.host_key }}
|
||||||
|
// "#;
|
||||||
|
// let mut tera = Tera::default();
|
||||||
|
// tera.add_raw_templates(vec![("boot", template)]).unwrap();
|
||||||
|
// assert!(validate_template(&inventory, &tera).is_err());
|
||||||
|
// }
|
||||||
|
}
|
||||||
24
src/handlers/boot.rs
Normal file
24
src/handlers/boot.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use crate::inventory::Inventory;
|
||||||
|
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
pub async fn boot_handler(
|
||||||
|
mac: web::Path<String>,
|
||||||
|
inventory: web::Data<Arc<Inventory>>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let mac_address = mac.into_inner();
|
||||||
|
|
||||||
|
if let Ok(boot_paramter) = inventory.get(&mac_address) {
|
||||||
|
return HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(format!("{}", json!(boot_paramter)));
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(json!({"error": "Unknown host ", "host": mac_address }).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/handlers/mod.rs
Normal file
1
src/handlers/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod boot;
|
||||||
269
src/inventory.rs
Normal file
269
src/inventory.rs
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
use serde::de::{self, Deserializer, MapAccess, Visitor};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml_bw::Value as YamlValue;
|
||||||
|
use std::cmp;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub(crate) struct Inventory {
|
||||||
|
pub webserver: String,
|
||||||
|
pub hosts: HashMap<String, Host>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct Host {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) version: String,
|
||||||
|
pub(crate) r#type: String,
|
||||||
|
pub(crate) cmdline: Vec<CmdParams>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) struct BootParam {
|
||||||
|
kernel: String,
|
||||||
|
initrd: Vec<String>,
|
||||||
|
cmdline: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) enum CmdParams {
|
||||||
|
DataSource(String),
|
||||||
|
Apkovl(String),
|
||||||
|
Modloop,
|
||||||
|
String(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inventory {
|
||||||
|
fn build_cmdline(&self, host: &str) -> String {
|
||||||
|
let host = self.hosts.get(host).unwrap();
|
||||||
|
let mut result_string = String::new();
|
||||||
|
for param in &host.cmdline {
|
||||||
|
match param {
|
||||||
|
CmdParams::DataSource(path) => result_string.push_str(&format!(
|
||||||
|
"ds=nocloud;s={}/tiny-cloud/{}",
|
||||||
|
self.webserver, path
|
||||||
|
)),
|
||||||
|
CmdParams::Apkovl(path) => {
|
||||||
|
result_string.push_str(&format!("apkovl={}/apkovl/{}", self.webserver, path))
|
||||||
|
}
|
||||||
|
CmdParams::Modloop => result_string.push_str(&format!(
|
||||||
|
"modloop={}/{}/modloop-{}",
|
||||||
|
self.webserver, host.version, host.r#type
|
||||||
|
)),
|
||||||
|
CmdParams::String(cmdstring) => result_string.push_str(&cmdstring),
|
||||||
|
};
|
||||||
|
result_string.push_str(" ");
|
||||||
|
}
|
||||||
|
// remove last space
|
||||||
|
result_string = result_string.trim().into();
|
||||||
|
return result_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, mac_addr: &str) -> Result<BootParam, ()> {
|
||||||
|
let host = self.hosts.get(mac_addr).unwrap();
|
||||||
|
let boot_param = BootParam {
|
||||||
|
kernel: format!(
|
||||||
|
"{}/{}/vmlinuz-{}",
|
||||||
|
self.webserver, host.version, host.r#type
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
initrd: vec![format!(
|
||||||
|
"{}/{}/initramfs-{}",
|
||||||
|
self.webserver, host.version, host.r#type
|
||||||
|
)
|
||||||
|
.into()],
|
||||||
|
cmdline: self.build_cmdline(&mac_addr),
|
||||||
|
message: format!("Booting host: {}", &host.name).into(),
|
||||||
|
};
|
||||||
|
return Ok(boot_param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for CmdParams {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<CmdParams, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct CmdParamsVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for CmdParamsVisitor {
|
||||||
|
type Value = CmdParams;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a cmdline parameter in the form of a string or map")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<CmdParams, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
if value == "modloop" {
|
||||||
|
return Ok(CmdParams::Modloop);
|
||||||
|
} else {
|
||||||
|
return Ok(CmdParams::String(value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<M>(self, mut access: M) -> Result<CmdParams, M::Error>
|
||||||
|
where
|
||||||
|
M: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
if let Some((key, value)) = access.next_entry::<String, Option<String>>()? {
|
||||||
|
match key.as_str() {
|
||||||
|
"ds" => {
|
||||||
|
let value =
|
||||||
|
value.ok_or_else(|| de::Error::missing_field("ds value"))?;
|
||||||
|
Ok(CmdParams::DataSource(value))
|
||||||
|
}
|
||||||
|
"apkovl" => {
|
||||||
|
let value =
|
||||||
|
value.ok_or_else(|| de::Error::missing_field("apkovl value"))?;
|
||||||
|
Ok(CmdParams::Apkovl(value))
|
||||||
|
}
|
||||||
|
"modloop" => Ok(CmdParams::Modloop),
|
||||||
|
_ => Err(de::Error::unknown_field(&key, &["ds", "apkovl", "modloop"])),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(de::Error::custom("Expected a key-value pair"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(CmdParamsVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_yaml_bw as serde_yaml;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmdparams_deserialization_string() {
|
||||||
|
let yaml_data = "---\nconsole=tty0";
|
||||||
|
let cmd_param: CmdParams = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(cmd_param, CmdParams::String("console=tty0".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmdparams_deserialization_modloop() {
|
||||||
|
let yaml_data = "---\nmodloop";
|
||||||
|
let cmd_param: CmdParams = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(cmd_param, CmdParams::Modloop);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmdparams_deserialization_ds() {
|
||||||
|
let yaml_data = "---\nds: k3s-server";
|
||||||
|
let cmd_param: CmdParams = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(cmd_param, CmdParams::DataSource("k3s-server".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmdparams_deserialization_apkovl() {
|
||||||
|
let yaml_data = "---\napkovl: nginx.apkov.tar.gz";
|
||||||
|
let cmd_param: CmdParams = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cmd_param,
|
||||||
|
CmdParams::Apkovl("nginx.apkov.tar.gz".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cmdparams_deserialization_invalid() {
|
||||||
|
let yaml_data = "---\nunknown: value";
|
||||||
|
let result: Result<CmdParams, _> = serde_yaml::from_str(yaml_data);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// test Host deserialization
|
||||||
|
#[test]
|
||||||
|
fn host_deserialization_valid() {
|
||||||
|
let yaml_data = r#"---
|
||||||
|
name: hostname
|
||||||
|
version: alpine-v3.21
|
||||||
|
type: test
|
||||||
|
cmdline:
|
||||||
|
- console=tty0
|
||||||
|
"#;
|
||||||
|
let host: Host = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
host,
|
||||||
|
Host {
|
||||||
|
name: "hostname".to_string(),
|
||||||
|
version: "alpine-v3.21".to_string(),
|
||||||
|
r#type: "test".to_string(),
|
||||||
|
cmdline: vec!(CmdParams::String("console=tty0".to_string()))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let yaml_data = r#"---
|
||||||
|
name: hostname
|
||||||
|
version: alpine-v3.21
|
||||||
|
type: test
|
||||||
|
cmdline: []
|
||||||
|
"#;
|
||||||
|
let host: Host = serde_yaml::from_str(yaml_data).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
host,
|
||||||
|
Host {
|
||||||
|
name: "hostname".to_string(),
|
||||||
|
version: "alpine-v3.21".to_string(),
|
||||||
|
r#type: "test".to_string(),
|
||||||
|
cmdline: vec!()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_deserialization_invalid() {
|
||||||
|
let yaml_data = r#"---
|
||||||
|
version: alpine-v3.21
|
||||||
|
type: test
|
||||||
|
cmdline: []
|
||||||
|
"#;
|
||||||
|
let host: Result<Host, _> = serde_yaml::from_str(yaml_data);
|
||||||
|
assert!(host.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cmdline() {
|
||||||
|
use serde_json::json;
|
||||||
|
let host = Host {
|
||||||
|
name: "test".into(),
|
||||||
|
version: "test".into(),
|
||||||
|
r#type: "test".into(),
|
||||||
|
cmdline: vec![
|
||||||
|
CmdParams::String("test".into()),
|
||||||
|
CmdParams::Apkovl("apkovl.tar.gz".into()),
|
||||||
|
CmdParams::Modloop,
|
||||||
|
CmdParams::DataSource("test-ds-config".into()),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut hosts: HashMap<String, Host> = HashMap::new();
|
||||||
|
hosts.insert("a:b:c:d:e:f".into(), host.clone());
|
||||||
|
|
||||||
|
let inventory = Inventory {
|
||||||
|
webserver: "http://example.net".into(),
|
||||||
|
hosts: hosts,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmd_string = inventory.build_cmdline("a:b:c:d:e:f");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cmd_string,
|
||||||
|
"test apkovl=http://example.net/apkovl/apkovl.tar.gz modloop=http://example.net/test/modloop-test ds=nocloud;s=http://example.net/tiny-cloud/test-ds-config"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json!(inventory.get("a:b:c:d:e:f").unwrap()),
|
||||||
|
json!({
|
||||||
|
"kernel": "http://example.net/test/vmlinuz-test",
|
||||||
|
"initrd": ["http://example.net/test/initramfs-test"],
|
||||||
|
"cmdline": "test apkovl=http://example.net/apkovl/apkovl.tar.gz modloop=http://example.net/test/modloop-test ds=nocloud;s=http://example.net/tiny-cloud/test-ds-config",
|
||||||
|
"message": "Booting host: test"})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/main.rs
Normal file
73
src/main.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
mod cli;
|
||||||
|
mod config;
|
||||||
|
mod handlers;
|
||||||
|
mod inventory;
|
||||||
|
|
||||||
|
use crate::cli::*;
|
||||||
|
use crate::config::{load_inventory, validate_template};
|
||||||
|
use crate::handlers::boot::boot_handler;
|
||||||
|
use crate::inventory::Inventory;
|
||||||
|
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use env_logger;
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
let cli = parse_args();
|
||||||
|
std::env::set_var("RUST_LOG", cli.loging);
|
||||||
|
|
||||||
|
env_logger::init(); // Initialize the logger
|
||||||
|
let inventory: Inventory = match load_inventory(cli.inventory) {
|
||||||
|
Ok(inv) => inv,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error loading inventory: {:#?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// create template engine instance
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
match tera.add_template_file("templates/boot.json.tmpl", Some("boot")) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Error loading template: {}, reason:\n{}",
|
||||||
|
e,
|
||||||
|
e.source().unwrap()
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = validate_template(&inventory, &tera) {
|
||||||
|
error!("Template validation error: {}, {:#?}", e, e.source());
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap objects into Atomic Reference Counter to use in multithead
|
||||||
|
// Actix environment
|
||||||
|
let inventory = Arc::new(inventory);
|
||||||
|
let tera = Arc::new(tera);
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Check => {
|
||||||
|
print!("inventory is valid: {:#?}", inventory);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Commands::Server => {
|
||||||
|
return HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(web::Data::new(inventory.clone()))
|
||||||
|
.app_data(web::Data::new(tera.clone()))
|
||||||
|
.route("/v1/boot/{mac}", web::get().to(boot_handler))
|
||||||
|
})
|
||||||
|
.bind((cli.address, cli.port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user