feat: Include boot.ipxe script

This commit is contained in:
2026-10-20 02:59:13 +02:00
parent 620f36d0bb
commit 8149ffbc37
6 changed files with 107 additions and 43 deletions
+1 -2
View File
@@ -1,5 +1,4 @@
#!ipxe #!ipxe
dhcp dhcp
chain boot.ipxe
echo "Hello, World!"
+19
View File
@@ -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 %}
+40 -36
View File
@@ -25,46 +25,50 @@ fn apply_netmask(
Ok((*ip & *netmask).to_string()) 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<Ipv4Addr>| {
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<String>| {
names
.iter()
.map(|name| {
get_configs_path()
.join(name)
.join("kubeconfig")
.to_string_lossy()
.to_string()
})
.collect::<Vec<_>>()
});
}
impl<'a> PathEnvironment<'a> { impl<'a> PathEnvironment<'a> {
pub fn new(path: &Path) -> Self { pub fn new(path: &Path) -> Self {
let mut env = Environment::new(); let mut env = Environment::new();
// Configure jinja configure_environment(&mut env);
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<Ipv4Addr>| {
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<String>| {
names
.iter()
.map(|name| {
get_configs_path()
.join(name)
.join("kubeconfig")
.to_string_lossy()
.to_string()
})
.collect::<Vec<_>>()
});
// Add path loader // Add path loader
env.set_loader(path_loader(&path)); env.set_loader(path_loader(&path));
+25 -2
View File
@@ -1,9 +1,14 @@
use crete::cluster::Cluster; use crete::cluster::Cluster;
use crete::environment::configure_environment;
use ipxe::IPXE_DIR; use ipxe::IPXE_DIR;
use minijinja::{Environment, context};
use regex::Regex; use regex::Regex;
// TODO: Make this a proper async function // TODO: Make this a proper async function
pub fn get_file(filename: impl ToString) -> impl Future<Output = Option<(Vec<u8>, &'static str)>> { pub fn get_file(
filename: impl ToString,
http_url: impl ToString,
) -> impl Future<Output = Option<(Vec<u8>, &'static str)>> {
// TODO: Make this a static that is initialized once // TODO: Make this a static that is initialized once
let re = let re =
Regex::new(r"^talos/(?<cluster>.+)/(?<node>.+)\.ya?ml$").expect("regex should be valid"); Regex::new(r"^talos/(?<cluster>.+)/(?<node>.+)\.ya?ml$").expect("regex should be valid");
@@ -13,7 +18,25 @@ pub fn get_file(filename: impl ToString) -> impl Future<Output = Option<(Vec<u8>
async move { async move {
const IPXE_PATTERN: &str = "ipxe/"; 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::<Vec<_>>(),
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(( Some((
IPXE_DIR.get_file(filename)?.contents().to_owned(), IPXE_DIR.get_file(filename)?.contents().to_owned(),
"application/octet-stream", "application/octet-stream",
+16 -1
View File
@@ -2,6 +2,7 @@ use std::path::PathBuf;
use axum::debug_handler; use axum::debug_handler;
use axum::extract::Request; use axum::extract::Request;
use axum::http::Uri;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use reqwest::{StatusCode, header}; use reqwest::{StatusCode, header};
@@ -9,11 +10,25 @@ use crate::file::get_file;
#[debug_handler] #[debug_handler]
pub async fn handler(req: Request) -> impl IntoResponse { 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 { let Some(path) = req.uri().path().strip_prefix("/") else {
return StatusCode::BAD_REQUEST.into_response(); 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(); return StatusCode::NOT_FOUND.into_response();
}; };
+6 -2
View File
@@ -114,9 +114,13 @@ async fn main() -> Result<(), Error> {
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
}); });
tftp::serve(ip, move |filename, _| { tftp::serve(ip, move |filename, ip| {
let filename = filename.to_owned(); 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?; .await?;
} }