From 8dcb4aa8b7ffaac1e3354f94af417a6a53dcfa2d Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Thu, 15 Oct 2026 05:06:17 +0200 Subject: [PATCH] --wip-- [skip ci] --- Cargo.lock | 109 +++++++++++++++++++++++++++++++++ Cargo.toml | 1 + ipxe/build.rs | 1 + ipxe/src/embed.ipxe | 3 +- src/boot.ipxe | 19 ++++++ src/cli.rs | 2 + src/environment.rs | 76 ++++++++++++----------- src/main.rs | 144 +++++++++++++++++++++++++++++++++++--------- 8 files changed, 290 insertions(+), 65 deletions(-) create mode 100644 src/boot.ipxe diff --git a/Cargo.lock b/Cargo.lock index feb6127..0cac29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,70 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "base64" version = "0.23.1" @@ -293,6 +357,7 @@ name = "crete" version = "0.1.3" dependencies = [ "async-walkdir", + "axum", "byteorder", "clap", "clap_complete", @@ -862,6 +927,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.11.1" @@ -875,6 +946,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1213,6 +1285,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.3" @@ -1234,6 +1312,12 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c8c750f1a07ea702bbd212bd999fceece9b3d1508b17023b3e174583124b" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minijinja" version = "2.24.0" @@ -1792,6 +1876,29 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2044,6 +2151,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2082,6 +2190,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index 0a39bd7..5ebf523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ serde_yaml = "0.9.34" thiserror = "2.0.21" tokio = { version = "1.52.1", features = ["full"] } ipxe = { path = "./ipxe" } +axum = { version = "0.8.9", features = ["macros"] } socket2 = "0.6.5" socket-pktinfo = "0.4.1" diff --git a/ipxe/build.rs b/ipxe/build.rs index 720fbe3..fbdd3bf 100644 --- a/ipxe/build.rs +++ b/ipxe/build.rs @@ -23,6 +23,7 @@ fn main() { .get() .to_string(), ]) + .arg("DOWNLOAD_PROTO_HTTP=1") .arg(format!( "EMBED={}", manifest_dir diff --git a/ipxe/src/embed.ipxe b/ipxe/src/embed.ipxe index f24cea6..4ff63af 100644 --- a/ipxe/src/embed.ipxe +++ b/ipxe/src/embed.ipxe @@ -1,5 +1,4 @@ #!ipxe dhcp - -echo "Hello, World!" +chain boot.ipxe diff --git a/src/boot.ipxe b/src/boot.ipxe new file mode 100644 index 0000000..e704798 --- /dev/null +++ b/src/boot.ipxe @@ -0,0 +1,19 @@ +#!ipxe +echo Starting ${serial} + +:start +goto node_${serial} || exit +# Default behavior (non install mode) is to exit iPXE script + +{% for cluster in clusters%} +{% for _, node in cluster.nodes|items %} +{%- if node.install.serial -%} +# {{ cluster.name }}/{{ node.hostname }} +:node_{{ node.install.serial }} +imgfree +kernel https://pxe.factory.talos.dev/image/{{ node.schematic }}/v{{ cluster.version.talos }}/kernel-{{ node.arch }} {{ node.kernelArgs|join(" ") }} {% if node.install.auto %}talos.config={{httpUrl}}/talos/{{cluster.name}}/{{node.hostname}}.yaml{% endif +%} +initrd https://pxe.factory.talos.dev/image/{{ node.schematic }}/v{{ cluster.version.talos }}/initramfs-{{ node.arch }}.xz +boot +{% endif %} +{% endfor %} +{% endfor %} diff --git a/src/cli.rs b/src/cli.rs index 902a5b6..cfcf512 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -52,6 +52,8 @@ pub enum Commands { ShellCompletions, /// Serve config files over tftp Serve { + #[arg(short, long, default_value = "3000")] + port: u16, #[arg(long, default_value = "0.0.0.0")] ip: IpAddr, }, diff --git a/src/environment.rs b/src/environment.rs index 615a2ec..683123c 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -25,46 +25,50 @@ fn apply_netmask( Ok((*ip & *netmask).to_string()) } +pub fn configure_environment(env: &mut Environment) { + // Configure jinja + env.set_trim_blocks(true); + env.set_lstrip_blocks(true); + env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); + env.set_auto_escape_callback(|_| AutoEscape::None); + + // Add filters + env.add_filter("to_prefix", |netmask: ViaDeserialize| { + let mask = netmask.to_bits(); + let prefix = mask.leading_ones(); + + if mask.checked_shl(prefix).unwrap_or(0) == 0 { + Ok(prefix as u8) + } else { + Err(minijinja::Error::new( + minijinja::ErrorKind::InvalidOperation, + "invalid IP prefix length", + )) + } + }); + + env.add_function("apply_netmask", apply_netmask); + + // Helper function for getting the path to kubeconfig files + env.add_filter("kubeconfig", move |names: Vec| { + names + .iter() + .map(|name| { + get_configs_path() + .join(name) + .join("kubeconfig") + .to_string_lossy() + .to_string() + }) + .collect::>() + }); +} + impl<'a> PathEnvironment<'a> { pub fn new(path: &Path) -> Self { let mut env = Environment::new(); - // Configure jinja - env.set_trim_blocks(true); - env.set_lstrip_blocks(true); - env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); - env.set_auto_escape_callback(|_| AutoEscape::None); - - // Add filters - env.add_filter("to_prefix", |netmask: ViaDeserialize| { - let mask = netmask.to_bits(); - let prefix = mask.leading_ones(); - - if mask.checked_shl(prefix).unwrap_or(0) == 0 { - Ok(prefix as u8) - } else { - Err(minijinja::Error::new( - minijinja::ErrorKind::InvalidOperation, - "invalid IP prefix length", - )) - } - }); - - env.add_function("apply_netmask", apply_netmask); - - // Helper function for getting the path to kubeconfig files - env.add_filter("kubeconfig", move |names: Vec| { - names - .iter() - .map(|name| { - get_configs_path() - .join(name) - .join("kubeconfig") - .to_string_lossy() - .to_string() - }) - .collect::>() - }); + configure_environment(&mut env); // Add path loader env.set_loader(path_loader(&path)); diff --git a/src/main.rs b/src/main.rs index 7bfdcbe..ddb19a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,27 @@ mod cli; +use std::net::SocketAddr; +use std::path::PathBuf; use std::pin::pin; +use axum::extract::Request; +use axum::http::Uri; +use axum::response::IntoResponse; +use axum::routing::get; +use axum::{Router, debug_handler}; use clap::{CommandFactory, Parser}; use clap_complete::{Shell, generate as generate_complete}; use crete::cluster::Cluster; -use crete::environment::PathEnvironment; +use crete::environment::{PathEnvironment, configure_environment}; use crete::talosconfig::{get_current_context, set_context}; use crete::tftp::ServerError; use crete::{get_configs_path, get_repo_path, set_repo_path, tftp}; use futures::StreamExt; use futures::future::join_all; use ipxe::IPXE_DIR; -use minijinja::context; +use minijinja::{Environment, context}; use regex::Regex; +use reqwest::{StatusCode, header}; use thiserror::Error; use crate::cli::{Cli, Commands, GlobalOpts}; @@ -93,6 +101,99 @@ async fn generate(opts: &GlobalOpts) -> Result<(), Error> { Ok(()) } +#[debug_handler] +async fn handler(req: Request) -> impl IntoResponse { + println!("{req:?}"); + let uri: Uri = req + .headers() + .iter() + .find(|(name, _)| header::HOST.eq(name)) + .map(|(_, value)| value) + .expect("Host header should exist") + .to_str() + .expect("Header value should be valid ASCII") + .parse() + .expect("Host should be valid Uri"); + + let host = uri.host().expect("Uri should have host"); + let port = uri.port().map(|port| port.as_u16()).unwrap_or(80); + + let Some(path) = req.uri().path().strip_prefix("/") else { + return StatusCode::BAD_REQUEST.into_response(); + }; + + let Some((file, content_type)) = get_file(path, format!("http://{host}:{port}")).await else { + return StatusCode::NOT_FOUND.into_response(); + }; + + let path = PathBuf::from(path); + let filename = path.file_name().unwrap(); + let filename = filename.to_str().unwrap(); + + let content_type = [(header::CONTENT_TYPE, content_type)]; + let disposition = [( + header::CONTENT_DISPOSITION, + format!("inline; filename=\"{filename}\""), + )]; + + (content_type, disposition, file).into_response() +} + +// TODO: Make this a proper async function +fn get_file( + filename: impl ToString, + http_url: impl ToString, +) -> impl Future, &'static str)>> { + // TODO: Make this a static that is initialized once + let re = + Regex::new(r"^talos/(?.+)/(?.+)\.ya?ml$").expect("regex should be valid"); + + let filename: String = filename.to_string(); + let re = re.clone(); + + async move { + const IPXE_PATTERN: &str = "ipxe/"; + if filename == format!("{IPXE_PATTERN}boot.ipxe") { + let mut env = Environment::new(); + + configure_environment(&mut env); + + let template = env + .template_from_str(include_str!("./boot.ipxe")) + .expect("Template should be valid"); + + let clusters = Cluster::get_all().await; + let content = template + .render(context! { + clusters => clusters.values().collect::>(), + httpUrl => http_url.to_string() + }) + .unwrap(); + + Some((content.into(), "text/plain; charset=utf-8")) + } else if let Some(filename) = filename.strip_prefix(IPXE_PATTERN) { + Some(( + IPXE_DIR.get_file(filename)?.contents().to_owned(), + "application/octet-stream", + )) + } else if let Some(caps) = re.captures(&filename) { + let cluster = &caps["cluster"]; + let node = &caps["node"]; + + let clusters = Cluster::get_all().await; + let cluster = clusters.get(cluster)?; + + let node = cluster.node(node)?; + + let config = node.config(cluster).await; + + Some((config.into(), "application/yaml; charset=utf-8")) + } else { + None + } + } +} + #[tokio::main] async fn main() -> Result<(), Error> { let cli = Cli::parse(); @@ -107,36 +208,25 @@ async fn main() -> Result<(), Error> { "crete", &mut std::io::stdout(), ), - Commands::Serve { ip } => { - let re = Regex::new(r"talos/(?.+)/(?.+)\.ya?ml") - .expect("regex should be valid"); + Commands::Serve { port, ip } => { + tokio::spawn(async move { + let app = Router::new().fallback_service(get(handler)); + let addr: SocketAddr = (ip, port).into(); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - tftp::serve(ip, move |filename, _| { + println!("Http server started on {addr}"); + axum::serve(listener, app).await.unwrap(); + }); + + tftp::serve(ip, move |filename, ip| { let filename = filename.to_owned(); - let re = re.clone(); - async move { - const IPXE_PATTERN: &str = "ipxe/"; - if let Some(filename) = filename.strip_prefix(IPXE_PATTERN) { - Some(IPXE_DIR.get_file(filename)?.contents().to_owned()) - } else if let Some(caps) = re.captures(&filename) { - let cluster = &caps["cluster"]; - let node = &caps["node"]; - - let clusters = Cluster::get_all().await; - let cluster = clusters.get(cluster)?; - - let node = cluster.node(node)?; - - let config = node.config(cluster).await; - - Some(config.into()) - } else { - None - } + get_file(filename, format!("http://{ip}:{port}")) + .await + .map(|(file, _)| file) } }) - .await? + .await?; } };