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/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/file.rs b/src/file.rs index 0bf8dab..a37223c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,9 +1,14 @@ use crete::cluster::Cluster; +use crete::environment::configure_environment; use ipxe::IPXE_DIR; +use minijinja::{Environment, context}; use regex::Regex; // TODO: Make this a proper async function -pub fn get_file(filename: impl ToString) -> impl Future, &'static str)>> { +pub 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"); @@ -13,7 +18,25 @@ pub fn get_file(filename: impl ToString) -> 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::>(), + 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", diff --git a/src/http.rs b/src/http.rs index 08eefa5..5d725f1 100644 --- a/src/http.rs +++ b/src/http.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use axum::debug_handler; use axum::extract::Request; +use axum::http::Uri; use axum::response::IntoResponse; use reqwest::{StatusCode, header}; @@ -9,11 +10,25 @@ use crate::file::get_file; #[debug_handler] pub async fn handler(req: Request) -> impl IntoResponse { + 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).await else { + let Some((file, content_type)) = get_file(path, format!("http://{host}:{port}")).await else { return StatusCode::NOT_FOUND.into_response(); }; diff --git a/src/main.rs b/src/main.rs index ec232de..8a0fb13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,9 +114,13 @@ async fn main() -> Result<(), Error> { axum::serve(listener, app).await.unwrap(); }); - tftp::serve(ip, move |filename, _| { + tftp::serve(ip, move |filename, ip| { let filename = filename.to_owned(); - async move { get_file(filename).await.map(|(file, _)| file) } + async move { + get_file(filename, format!("http://{ip}:{port}")) + .await + .map(|(file, _)| file) + } }) .await?; }