Name update

This commit is contained in:
Miles Ward
2026-05-06 14:13:38 -04:00
parent e1b7154b51
commit 20c585e3ed
56 changed files with 755 additions and 802 deletions
+57 -57
View File
@@ -4,7 +4,7 @@
//!
//! | Group | Purpose |
//! |------------------|-------------------------------------------------------|
//! | `/` | Web UI (served from `pxeforge-webui`) |
//! | `/` | Web UI (served from `openpxe-webui`) |
//! | `/boot.ipxe` | Top-level iPXE menu |
//! | `/boot/_*.ipxe` | Submenu scripts (hierarchy: linux, windows, tools, …) |
//! | `/boot/<id>.ipxe`| Per-entry boot script |
@@ -14,7 +14,7 @@
//! | `/api/*` | JSON/HTML API for the web UI |
use crate::ipxe_script::{
render_entry, render_family_menu, render_gate_entry, render_local_hdd,
render_entry, render_family_menu, render_queue_entry, render_local_hdd,
render_menu, render_nic_info, render_shell, render_tools_menu, render_util,
};
use crate::iso_fs;
@@ -29,9 +29,9 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use pxeforge_core::{ClientEvent, Settings};
use pxeforge_ipxe_assets::asset_bytes;
use pxeforge_iso_store::{IsoMeta, NfsAddRequest};
use openpxe_core::{ClientEvent, Settings};
use openpxe_ipxe_assets::asset_bytes;
use openpxe_iso_store::{IsoMeta, NfsAddRequest};
use serde::Deserialize;
use serde_json::json;
use std::time::Duration;
@@ -45,7 +45,7 @@ pub fn build_router(state: AppState) -> Router {
.route("/assets/app.js", get(ui_js))
.route("/assets/app.css", get(ui_css))
.route("/assets/logo.svg", get(ui_logo))
.route("/assets/anvil-forge.svg", get(ui_anvil_forge))
.route("/assets/loader.svg", get(ui_loader))
// iPXE script endpoints.
.route("/boot.ipxe", get(boot_top_menu))
.route("/boot/:filename", get(boot_sub))
@@ -64,11 +64,11 @@ pub fn build_router(state: AppState) -> Router {
.route("/api/clients", get(api_list_clients))
.route("/api/status", get(api_status))
.route("/api/settings", get(api_get_settings).put(api_put_settings))
.route("/api/gate", get(api_list_gates))
.route("/api/gate/join", get(api_gate_join))
.route("/api/gate/poll/:gate_id", get(api_gate_poll))
.route("/api/gate/assign", post(api_gate_assign))
.route("/api/gate/:gate_id", delete(api_gate_release))
.route("/api/queue", get(api_list_queue))
.route("/api/queue/join", get(api_queue_join))
.route("/api/queue/poll/:entry_id", get(api_queue_poll))
.route("/api/queue/assign", post(api_queue_assign))
.route("/api/queue/:entry_id", delete(api_queue_release))
// Phase 4: NFS share manager.
.route("/api/nfs", get(api_nfs_list).post(api_nfs_add))
.route("/api/nfs/:id", delete(api_nfs_remove))
@@ -98,7 +98,7 @@ pub fn build_router(state: AppState) -> Router {
// ─── UI ────────────────────────────────────────────────────────────────────
async fn index(State(state): State<AppState>) -> Response {
let html = pxeforge_webui::index_html(&state.public_base_url);
let html = openpxe_webui::index_html(&state.public_base_url);
([(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"))], html)
.into_response()
}
@@ -106,28 +106,28 @@ async fn index(State(state): State<AppState>) -> Response {
async fn ui_js() -> Response {
(
[(header::CONTENT_TYPE, HeaderValue::from_static("application/javascript"))],
pxeforge_webui::app_js(),
openpxe_webui::app_js(),
).into_response()
}
async fn ui_css() -> Response {
(
[(header::CONTENT_TYPE, HeaderValue::from_static("text/css"))],
pxeforge_webui::app_css(),
openpxe_webui::app_css(),
).into_response()
}
async fn ui_logo() -> Response {
(
[(header::CONTENT_TYPE, HeaderValue::from_static("image/svg+xml"))],
pxeforge_webui::logo_svg(),
openpxe_webui::logo_svg(),
).into_response()
}
async fn ui_anvil_forge() -> Response {
async fn ui_loader() -> Response {
(
[(header::CONTENT_TYPE, HeaderValue::from_static("image/svg+xml"))],
pxeforge_webui::anvil_forge_svg(),
openpxe_webui::loader_svg(),
).into_response()
}
@@ -146,7 +146,7 @@ async fn boot_top_menu(
State(state): State<AppState>,
Query(p): Query<BootMenuParams>,
) -> Response {
state.metrics.record_http(pxeforge_core::HttpRoute::BootScript);
state.metrics.record_http(openpxe_core::HttpRoute::BootScript);
let isos = state.iso_store.list();
let settings = state.settings.snapshot();
let base = &state.public_base_url;
@@ -158,7 +158,7 @@ async fn boot_top_menu(
if let Some(mac) = p.mac.as_deref() {
if let Some(binding) = state.hosts.lookup(mac) {
tracing::info!(
target: "pxeforge::http",
target: "openpxe::http",
mac = %binding.mac, target = %binding.target,
"host binding applied"
);
@@ -168,7 +168,7 @@ async fn boot_top_menu(
// `/boot/<name>` route, so the URL is identical.
return text_plain(format!(
"#!ipxe\n\
echo PXEForge: per-MAC binding -> {target}\n\
echo OpenPXE: per-MAC binding -> {target}\n\
chain {base}/boot/{target}.ipxe || chain {base}/boot.ipxe\n"
));
}
@@ -203,7 +203,7 @@ async fn boot_sub(
"_util" => render_util(base),
"_shell" => render_shell(base),
"_nic" => render_nic_info(base),
"_gate" => render_gate_entry(base),
"_queue" => render_queue_entry(base),
other => {
for iso in &isos {
for entry in &iso.boot_entries {
@@ -391,7 +391,7 @@ async fn readyz(State(state): State<AppState>) -> Response {
// 1. At least one iPXE binary must be bundled (without one, TFTP 404s
// and no client boots).
// 2. The ISO directory must exist and be readable.
let assets = pxeforge_ipxe_assets::list_assets();
let assets = openpxe_ipxe_assets::list_assets();
let mut problems: Vec<&str> = Vec::new();
if assets.is_empty() {
problems.push("no iPXE binaries bundled (run scripts/fetch-ipxe.sh before building)");
@@ -420,7 +420,7 @@ async fn api_status(State(state): State<AppState>) -> Json<serde_json::Value> {
let nfs_active = nfs.iter().filter(|m| m.mounted).count();
let isos = state.iso_store.list();
let clients = state.clients.list();
let gates = state.gates.list();
let gates = state.queue.list();
// Phase 4: dashboard tracks "imaging" as gates with an assignment
// already issued — they're the ones actively chaining a boot script.
let imaging = gates.iter().filter(|g| g.assigned_target.is_some()).count();
@@ -430,9 +430,9 @@ async fn api_status(State(state): State<AppState>) -> Json<serde_json::Value> {
// gauges fresh without a dedicated scrape-time hook.
state.metrics.set_iso_count(isos.len() as u64);
state.metrics.set_client_count(clients.len() as u64);
state.metrics.set_gate_counts(gates.len() as u64, imaging as u64);
state.metrics.set_queue_counts(gates.len() as u64, imaging as u64);
state.metrics.set_nfs_active(nfs_active as u64);
state.metrics.record_http(pxeforge_core::HttpRoute::Api);
state.metrics.record_http(openpxe_core::HttpRoute::Api);
let now = time::OffsetDateTime::now_utc();
let uptime_secs = (now - state.started_at).whole_seconds().max(0);
Json(json!({
@@ -440,10 +440,10 @@ async fn api_status(State(state): State<AppState>) -> Json<serde_json::Value> {
"public_base_url": state.public_base_url,
"iso_count": isos.len(),
"client_count": clients.len(),
"gate_count": gates.len(),
"queue_count": gates.len(),
"imaging_count": imaging,
"waiting_count": waiting,
"ipxe_assets": pxeforge_ipxe_assets::list_assets(),
"ipxe_assets": openpxe_ipxe_assets::list_assets(),
"settings": state.settings.snapshot(),
"smb": smb,
"nfs_count": nfs.len(),
@@ -468,7 +468,7 @@ async fn api_put_settings(
// Guardrail: Windows boot requires the wimboot shim to be bundled.
// Without it, clients chain a non-existent /ipxe/wimboot and stall.
if new.windows_enabled {
let assets = pxeforge_ipxe_assets::list_assets();
let assets = openpxe_ipxe_assets::list_assets();
if !assets.iter().any(|n| n == "wimboot") {
return (
StatusCode::BAD_REQUEST,
@@ -497,12 +497,12 @@ async fn api_put_settings(
StatusCode::NO_CONTENT.into_response()
}
// ─── Gated Deployment API ─────────────────────────────────────────────────
// ─── Queued Deployment API ─────────────────────────────────────────────────
async fn api_list_gates(State(state): State<AppState>) -> Json<serde_json::Value> {
async fn api_list_queue(State(state): State<AppState>) -> Json<serde_json::Value> {
Json(json!({
"gates": state.gates.list(),
"count": state.gates.list().len(),
"entries": state.queue.list(),
"count": state.queue.list().len(),
}))
}
@@ -514,9 +514,9 @@ struct GateJoinParams {
}
/// Called by iPXE via `chain --replace`. We respond with a tiny iPXE
/// script that hard-loops on `/api/gate/poll/<id>`. iPXE keeps fetching
/// script that hard-loops on `/api/queue/poll/<id>`. iPXE keeps fetching
/// until poll returns an actual boot script.
async fn api_gate_join(
async fn api_queue_join(
State(state): State<AppState>,
Query(p): Query<GateJoinParams>,
headers: HeaderMap,
@@ -528,10 +528,10 @@ async fn api_gate_join(
.and_then(|s| s.split(',').next())
.and_then(|s| s.trim().parse().ok());
let gate = state.gates.join(&mac, ip, None);
let gate = state.queue.join(&mac, ip, None);
state.clients.record(
&mac, ip, None,
ClientEvent::HttpScriptFetch { target: "gate-join".into() },
ClientEvent::HttpScriptFetch { target: "queue-join".into() },
);
let base = &state.public_base_url;
@@ -540,11 +540,11 @@ async fn api_gate_join(
"#!ipxe\n\
echo\n\
echo ==========================================\n\
echo Gated Deployment - Gate Position {}\n\
echo Queued Deployment - Gate Position {}\n\
echo Waiting for operator to assign an image\n\
echo (Ctrl-B returns to the iPXE shell)\n\
echo ==========================================\n\
chain {base}/api/gate/poll/{}\n",
chain {base}/api/queue/poll/{}\n",
gate.position, gate.id
);
text_plain(script)
@@ -553,11 +553,11 @@ async fn api_gate_join(
/// Long-poll endpoint. Waits up to 25s for an assignment; if none, returns
/// a script that loops back to itself. 25s keeps us well inside typical
/// HTTP idle timeouts for iPXE and intermediaries.
async fn api_gate_poll(
async fn api_queue_poll(
State(state): State<AppState>,
AxumPath(gate_id): AxumPath<String>,
AxumPath(entry_id): AxumPath<String>,
) -> Response {
let Some(notify) = state.gates.notifier(&gate_id) else {
let Some(notify) = state.queue.notifier(&entry_id) else {
// Gate was released; send client back to the main menu.
let base = &state.public_base_url;
return text_plain(format!("#!ipxe\nchain {base}/boot.ipxe\n"));
@@ -566,7 +566,7 @@ async fn api_gate_poll(
// Wait for an assignment or timeout.
let _ = tokio::time::timeout(Duration::from_secs(25), notify.notified()).await;
let snap = state.gates.touch(&gate_id);
let snap = state.queue.touch(&entry_id);
let base = &state.public_base_url;
match snap {
// Bind `target` directly so we can't observe an Option::None between
@@ -580,14 +580,14 @@ async fn api_gate_poll(
Some(g) if g.assigned_target.is_some() => {
let target = g.assigned_target.clone().unwrap_or_default();
tracing::info!(
target: "pxeforge::gate",
gate_id=%gate_id, mac=%g.mac, target=%target,
target: "openpxe::queue",
entry_id=%entry_id, mac=%g.mac, target=%target,
"gate assignment delivered"
);
text_plain(format!(
"#!ipxe\n\
echo Gate assignment received: {target}\n\
chain {base}/boot/{target}.ipxe || chain {base}/api/gate/poll/{gate_id}\n"
chain {base}/boot/{target}.ipxe || chain {base}/api/queue/poll/{entry_id}\n"
))
}
Some(g) => {
@@ -596,7 +596,7 @@ async fn api_gate_poll(
text_plain(format!(
"#!ipxe\n\
echo Gate Position {} - still waiting\n\
chain {base}/api/gate/poll/{gate_id}\n",
chain {base}/api/queue/poll/{entry_id}\n",
g.position
))
}
@@ -610,17 +610,17 @@ struct GateAssignBody {
/// `/boot/<id>.ipxe`.
target: String,
/// Gate ids to assign. Empty = assign to all currently queued gates.
gate_ids: Vec<String>,
entry_ids: Vec<String>,
}
async fn api_gate_assign(
async fn api_queue_assign(
State(state): State<AppState>,
Json(body): Json<GateAssignBody>,
) -> Json<serde_json::Value> {
let ids = if body.gate_ids.is_empty() {
state.gates.list().into_iter().map(|g| g.id).collect::<Vec<_>>()
let ids = if body.entry_ids.is_empty() {
state.queue.list().into_iter().map(|g| g.id).collect::<Vec<_>>()
} else {
body.gate_ids
body.entry_ids
};
// Guard: target must exist as a BootEntry id.
let found = state.iso_store.list().into_iter().any(|i| {
@@ -629,15 +629,15 @@ async fn api_gate_assign(
if !found {
return Json(json!({ "ok": false, "error": "unknown target" }));
}
let n = state.gates.assign(&ids, &body.target);
let n = state.queue.assign(&ids, &body.target);
Json(json!({ "ok": true, "assigned": n, "target": body.target }))
}
async fn api_gate_release(
async fn api_queue_release(
State(state): State<AppState>,
AxumPath(gate_id): AxumPath<String>,
AxumPath(entry_id): AxumPath<String>,
) -> StatusCode {
match state.gates.release(&gate_id) {
match state.queue.release(&entry_id) {
Some(_) => StatusCode::NO_CONTENT,
None => StatusCode::NOT_FOUND,
}
@@ -783,11 +783,11 @@ async fn api_metrics(State(state): State<AppState>) -> Response {
state
.metrics
.set_client_count(state.clients.list().len() as u64);
let gates = state.gates.list();
let gates = state.queue.list();
let imaging = gates.iter().filter(|g| g.assigned_target.is_some()).count();
state
.metrics
.set_gate_counts(gates.len() as u64, imaging as u64);
.set_queue_counts(gates.len() as u64, imaging as u64);
state
.metrics
.set_nfs_active(state.nfs.list().iter().filter(|m| m.mounted).count() as u64);