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