Remove unused code

This commit is contained in:
Aleksandr Berkuta 2025-06-23 19:15:34 +03:00
parent 60cd7b5c81
commit 0b16a8069f
8 changed files with 130 additions and 240 deletions

View File

@ -20,6 +20,7 @@ mac_address2 = "2"
[profile.release] [profile.release]
target = "x86_64-unknown-linux-musl" target = "x86_64-unknown-linux-musl"
opt-level = "z"
[profile.dev] [profile.dev]
target = "x86_64-unknown-linux-musl" target = "x86_64-unknown-linux-musl"

114
Readme.md Normal file
View File

@ -0,0 +1,114 @@
# Orpheus
Orpheus is a backend for a [pixiecore](https://github.com/danderson/netboot/blob/main/pixiecore/README.md) API mode. It provides boot configurations per host with specified MAC-address.
## Enpoint Description
Orpheus usppose to respond on:
GET <server_url>/v1/boot/{mac_address}
with a JSON:
```json
{
"kernel": "https://files.local/kernel",
"initrd": ["https://files.local/initrd"],
"cmdline": "selinux=1 coreos.autologin",
"message": "Booting host"
}
```
according [pixiecore API specification](https://github.com/danderson/netboot/blob/main/pixiecore/README.api.md):
kernel (string): the URL of the kernel to boot.
initrd (list of strings): URLs of initrds to load. The kernel will flatten all the initrds into a single filesystem.
cmdline (string): commandline parameters for the kernel. The commandline is processed by Go's text/template library. Within the template, a URL function is available that takes a URL and rewrites it such that Pixiecore proxies the request.
message (string): A message to display before booting the provided configuration. Note that displaying this message is on a best-effort basis only, as particular implementations of the boot process may not support displaying text.
## Server Configuration
Orpheus using yaml file for a inventory description:
```yaml
webserver: "http://my.webserver.net"
hosts:
"aa:bb:cc:dd:ee:ff": &test
name: my_pxe_booted_host
version: version_of_kernel
type: type_of_kernel
cmdline:
- simple_string
- apkovl: path_to_apkovl_on_webserver.tar.gz
- modloop
- ds: path_to_data_source_on_webserver
"aa:bb:cc:dd:ee:00":
<<: *test
name: test2
```
To boot alpine linux in diskless mode is required three files:
- kernel
- initrd
- modloop
All three lies in one directory on the server, to boot succesfully you have to boot
kernel with the same version of initrd and modloop. So it is convinient to setup just
version and type of the desired kernel.
`webserver` - is http webserver in local network which serves file to a booting host.
`hosts` - is a map which contains mac addresses of the booting machines as a keys
and booting parameters as value.
`version` in the host patameter directs to the
kernel-version directory,
`type` - describes wich type of the file to request, usually it is `lts`, or `virt`.
Webserver suppose to have following directory structure:
In the root there is 3 types of directories - kernel directory, directory with name `apkovl` and `tiny-cloud`.
`kernel` directory can have any name (suppose to have name related to kernel version) and contain kernel, initrd and modloop files.
`apkovl` directory contain aplovl files.
`tini-cloud` directory contains subdirectories with files related to tiniy-cloud config files.
Here is an example
```
/
├── alpine-3.21 # kernel directory for alpine linux kernel `version` 3.21
│ ├── initramfs-lts # initrd with `lts` type
│ ├── initramfs-virt # initrd with `virt` is type
│ ├── modloop-lts
│ ├── modloop-virt
│ ├── vmlinuz-lts # kernel
│ └── vmlinuz-virt
├── alpine-3.19 # kernel directory for alpine linux version 3.19
│ ├── initramfs-lts
│ ├── initramfs-virt
│ ├── modloop-lts
│ ├── modloop-virt
│ ├── vmlinuz-lts
│ └── vmlinuz-virt
├── apkovl
│ ├── my_apkovl.tar.gz
│ └── backup.apkovl.tar.gz
└── tiny-cloud
├── server # contains tiny-cloud config files for a server node
│ ├── meta-data
│ ├── network-config
│ ├── user-data
│ └── vendor-data
└── agent # contains tiny-cloud config files for agent node
├── meta-data
├── network-config
├── user-data
└── vendor-data
```
## Usage
Run:
```bash
orpheus -i inventory.yaml server
```

View File

@ -1,7 +1,5 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use log::Level;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Cli { pub struct Cli {

View File

@ -1,207 +0,0 @@
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());
// }
}

View File

@ -1,10 +1,8 @@
use crate::inventory::Inventory; use crate::inventory::Inventory;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use log::{error, info, warn};
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
use tera::Tera;
pub async fn boot_handler( pub async fn boot_handler(
mac: web::Path<String>, mac: web::Path<String>,

View File

View File

@ -1,7 +1,9 @@
use figment::{
providers::{Format, YamlExtended},
Error as FigError, Figment,
};
use serde::de::{self, Deserializer, MapAccess, Visitor}; use serde::de::{self, Deserializer, MapAccess, Visitor};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml_bw::Value as YamlValue;
use std::cmp;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -135,6 +137,14 @@ impl<'de> Deserialize<'de> for CmdParams {
} }
} }
pub fn load_inventory(inventory_file: String) -> Result<Inventory, FigError> {
let inventory: Inventory = Figment::new()
.merge(YamlExtended::file(inventory_file))
.extract()?;
return Ok(inventory);
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,19 +1,15 @@
mod cli; mod cli;
mod config;
mod handlers; mod handlers;
mod inventory; mod inventory;
use crate::cli::*; use crate::cli::*;
use crate::config::{load_inventory, validate_template};
use crate::handlers::boot::boot_handler; use crate::handlers::boot::boot_handler;
use crate::inventory::Inventory; use crate::inventory::{load_inventory, Inventory};
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use env_logger; use env_logger;
use log::{error, info, warn}; use log::error;
use std::error::Error;
use std::sync::Arc; use std::sync::Arc;
use tera::Tera;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -21,6 +17,7 @@ async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", cli.loging); std::env::set_var("RUST_LOG", cli.loging);
env_logger::init(); // Initialize the logger env_logger::init(); // Initialize the logger
let inventory: Inventory = match load_inventory(cli.inventory) { let inventory: Inventory = match load_inventory(cli.inventory) {
Ok(inv) => inv, Ok(inv) => inv,
Err(e) => { Err(e) => {
@ -29,29 +26,9 @@ async fn main() -> std::io::Result<()> {
} }
}; };
// 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 // wrap objects into Atomic Reference Counter to use in multithead
// Actix environment // Actix environment
let inventory = Arc::new(inventory); let inventory = Arc::new(inventory);
let tera = Arc::new(tera);
match cli.command { match cli.command {
Commands::Check => { Commands::Check => {
@ -62,7 +39,6 @@ async fn main() -> std::io::Result<()> {
return HttpServer::new(move || { return HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(inventory.clone())) .app_data(web::Data::new(inventory.clone()))
.app_data(web::Data::new(tera.clone()))
.route("/v1/boot/{mac}", web::get().to(boot_handler)) .route("/v1/boot/{mac}", web::get().to(boot_handler))
}) })
.bind((cli.address, cli.port))? .bind((cli.address, cli.port))?