diff --git a/Cargo.lock b/Cargo.lock index 248ee26..65e1332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,7 @@ dependencies = [ "serde_yaml", "thiserror", "tokio", + "udp_sas_async", ] [[package]] @@ -1459,6 +1460,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "os_socketaddr" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa149f3ec09cadeb41968390fcd71e6ea038a9a373d1d4eb070f4d9484d4b244" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -2197,6 +2208,18 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "udp_sas_async" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "073390c0d7b1682a5f044db2f769b0564568f741b9a7e23b8494b79fc868d491" +dependencies = [ + "cc", + "libc", + "os_socketaddr", + "tokio", +] + [[package]] name = "unicode-ident" version = "1.0.26" @@ -2339,6 +2362,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2348,6 +2387,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index f300252..3a011fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ thiserror = "2.0.21" tokio = { version = "1.52.1", features = ["full"] } ipxe = { path = "./ipxe" } axum = { version = "0.8.9", features = ["macros"] } +udp_sas_async = { version = "0.2.0", features = ["async_tokio"] } [lib] test = false diff --git a/boot.ipxe b/boot.ipxe new file mode 100644 index 0000000..e69de29 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..fea2cd5 --- /dev/null +++ b/src/boot.ipxe @@ -0,0 +1,20 @@ +{% set httpUrl = "http://192.168.1.1:8000" -%} +#!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/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 4aea079..d6710b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,14 +10,14 @@ 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; @@ -145,7 +145,24 @@ fn get_file(filename: impl Into) -> impl Future async move { const IPXE_PATTERN: &str = "ipxe/"; - if let Some(filename) = filename.strip_prefix(IPXE_PATTERN) { + 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::>(), + }) + .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", diff --git a/src/tftp/packet.rs b/src/tftp/packet.rs index 28612a3..f9ac744 100644 --- a/src/tftp/packet.rs +++ b/src/tftp/packet.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use tokio::net::UdpSocket; +use udp_sas_async::tokio::UdpSocketSas; use crate::tftp::Error; @@ -309,16 +309,15 @@ impl Packet { Ok(()) } - pub async fn recv_from(socket: &UdpSocket) -> Result<(Self, SocketAddr), Error> { + pub async fn recv_from(socket: &UdpSocketSas) -> Result<(Self, SocketAddr), Error> { let mut buf = Vec::with_capacity(PACKET_SIZE); - let (_, src) = socket.recv_buf_from(&mut buf).await?; - + let (_, src) = socket.recv_from(&mut buf).await?; let packet = buf.try_into()?; Ok((packet, src)) } - pub async fn send_to(&self, socket: &UdpSocket, src: &SocketAddr) -> Result<(), Error> { + pub async fn send_to(&self, socket: &UdpSocketSas, src: SocketAddr) -> Result<(), Error> { let buf: Vec<_> = self.try_into()?; socket.send_to(&buf, src).await?; diff --git a/src/tftp/server.rs b/src/tftp/server.rs index 372e18f..e10a339 100644 --- a/src/tftp/server.rs +++ b/src/tftp/server.rs @@ -1,9 +1,9 @@ use std::io; -use std::net::SocketAddr; +use std::net::{IpAddr, SocketAddr}; use std::time::Duration; use thiserror::Error; -use tokio::net::UdpSocket; +use udp_sas_async::tokio::UdpSocketSas; use crate::tftp::packet::{ErrorCode, Mode}; use crate::tftp::{self, Packet}; @@ -29,23 +29,25 @@ pub enum Error { } pub async fn handle_connection( - src: SocketAddr, packet: Packet, + src: SocketAddr, + dest: IpAddr, callback: F, ) -> Result<(), Error> where F: Fn(&str) -> Fut, Fut: Future>>, { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - println!( - "{src}: Connecting from {}", - socket.local_addr().expect("Socket is bound") - ); + let socket = UdpSocketSas::bind( + format!("{dest}:0") + .parse() + .expect("Address should be valid"), + )?; + println!("{src}: Connecting from {}", socket.local_addr()); let Packet::Read(request) = packet else { Packet::error(ErrorCode::NotDefined, "Operation not supported") - .send_to(&socket, &src) + .send_to(&socket, src) .await?; return Err(Error::UnsupportedOperation(packet)); @@ -56,7 +58,7 @@ where let Some(data) = callback(filename).await else { Packet::error(ErrorCode::FileNotFound, "Unknown file") - .send_to(&socket, &src) + .send_to(&socket, src) .await?; return Err(Error::FileNotFound(filename.into())); @@ -72,7 +74,7 @@ where ErrorCode::NotDefined, format!("Mode {mode:?} is not supported"), ) - .send_to(&socket, &src) + .send_to(&socket, src) .await?; return Err(Error::UnsupportedMode(mode)); @@ -88,10 +90,9 @@ where // Send the first chunk if let Some((index, chunk)) = chunk { Packet::data((index + 1) as u16, chunk) - .send_to(&socket, &src) + .send_to(&socket, src) .await?; } - tokio::select! { recv = Packet::recv_from(&socket) => { timeout = 0; @@ -101,7 +102,7 @@ where // really know how to test it properly so we don't support it. if new_src != src { Packet::error(ErrorCode::Illegal, "Source changed") - .send_to(&socket, &src) + .send_to(&socket, src) .await?; return Err(Error::SourceChanged(new_src)); @@ -122,7 +123,7 @@ where } _ => { Packet::error(ErrorCode::Illegal, "Unexpected packet") - .send_to(&socket, &src) + .send_to(&socket, src) .await?; @@ -150,16 +151,21 @@ where F: Fn(&str) -> Fut + Clone + Send + 'static, Fut: Future>> + Send + 'static, { - let socket = UdpSocket::bind("0.0.0.0:69").await?; + let socket = UdpSocketSas::bind("0.0.0.0:69".parse().expect("Address should be valid"))?; println!("Server started"); loop { + let mut buf = Vec::with_capacity(0); + let (bytes, src, dest) = socket.recv_sas(&mut buf).await?; + println!("{src} -> {dest}"); + println!("{bytes}, {}", buf.len()); + let (packet, src) = Packet::recv_from(&socket).await?; let callback = callback.clone(); tokio::spawn({ async move { - if let Err(err) = handle_connection(src, packet, callback).await { + if let Err(err) = handle_connection(packet, src, dest, callback).await { println!("{src}: {err}") } }