Name update
This commit is contained in:
+57
-57
@@ -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);
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
//! > Boot from Local HDD
|
||||
//! Installers
|
||||
//! > Linux Installers -> submenu of Linux ISOs
|
||||
//! > Windows Installers -> submenu of Windows ISOs (gated by Settings::windows_enabled)
|
||||
//! > Windows Installers -> submenu of Windows ISOs (controlled by Settings::windows_enabled)
|
||||
//! Tools
|
||||
//! > Utilities -> memtest, etc. (embedded assets only)
|
||||
//! > PXEForge Shell -> drop to iPXE shell with branded prompt
|
||||
//! > OpenPXE Shell -> drop to iPXE shell with branded prompt
|
||||
//! > Network Card Info -> ifstat / config / route dump
|
||||
//! Gated Deployment -> join the gate queue
|
||||
//! Queued Deployment -> join the deployment queue
|
||||
//! ```
|
||||
//!
|
||||
//! ## iPXE is entirely backend — users do not see or write iPXE
|
||||
@@ -22,12 +22,12 @@
|
||||
//! those knobs into iPXE primitives (chain, menu, item, choose, etc.).
|
||||
//! There is intentionally no UI path to upload a custom `.ipxe` script.
|
||||
|
||||
use pxeforge_core::{Settings, TimeoutAction};
|
||||
use pxeforge_iso_store::{BootEntry, BootKind, IsoMeta};
|
||||
use pxeforge_iso_store::introspect::DistroFamily;
|
||||
use openpxe_core::{Settings, TimeoutAction};
|
||||
use openpxe_iso_store::{BootEntry, BootKind, IsoMeta};
|
||||
use openpxe_iso_store::introspect::DistroFamily;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Top-level PXEForge boot menu. Serialized identically for BIOS and UEFI
|
||||
/// Top-level OpenPXE boot menu. Serialized identically for BIOS and UEFI
|
||||
/// clients because iPXE normalises the menu primitives across firmwares.
|
||||
#[must_use]
|
||||
pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> String {
|
||||
@@ -35,7 +35,7 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let timeout_ms = settings.boot_menu_timeout_secs.saturating_mul(1000);
|
||||
let default_item = match settings.timeout_action {
|
||||
TimeoutAction::GatedDeployment => "gate",
|
||||
TimeoutAction::QueuedDeployment => "queue",
|
||||
// Stay -> iPXE's `--timeout 0` is "no timeout". Pick any default
|
||||
// label; the client waits for keypress. We use the same label as
|
||||
// LocalHdd to keep the menu's pre-highlight stable.
|
||||
@@ -43,12 +43,12 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
};
|
||||
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "# PXEForge top-level menu - auto-generated, do not edit");
|
||||
let _ = writeln!(s, "# OpenPXE top-level menu - auto-generated, do not edit");
|
||||
let _ = writeln!(s, "set base-url {base}");
|
||||
let _ = writeln!(s, "set esc:hex 1b");
|
||||
let _ = writeln!(s, "set cls ${{esc:string}}[2J");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu PXEForge - network boot menu");
|
||||
let _ = writeln!(s, "menu OpenPXE - network boot menu");
|
||||
let _ = writeln!(s, "item --gap -- ------------------------- Default -------------------------");
|
||||
let _ = writeln!(s, "item local Boot from Local HDD");
|
||||
let _ = writeln!(s, "item --gap -- ----------------------- Installers -----------------------");
|
||||
@@ -66,8 +66,8 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
}
|
||||
let _ = writeln!(s, "item --gap -- -------------------------- Tools --------------------------");
|
||||
let _ = writeln!(s, "item tools Tools >");
|
||||
let _ = writeln!(s, "item --gap -- ---------------------- Gated Deployment ---------------------");
|
||||
let _ = writeln!(s, "item gate Gated Deployment (join queue)");
|
||||
let _ = writeln!(s, "item --gap -- ---------------------- Queued Deployment ---------------------");
|
||||
let _ = writeln!(s, "item queue Queued Deployment (join queue)");
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --key x exit Exit iPXE");
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> Str
|
||||
let _ = writeln!(s, "iseq ${{target}} linux && chain {base}/boot/_linux_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} tools && chain {base}/boot/_tools_menu.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} gate && chain {base}/boot/_gate.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} queue && chain {base}/boot/_queue.ipxe || goto menu");
|
||||
let _ = writeln!(s, "iseq ${{target}} exit && exit || goto menu");
|
||||
let _ = writeln!(s, "goto menu");
|
||||
s
|
||||
@@ -101,7 +101,7 @@ pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) ->
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "set base-url {base}");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu PXEForge - {title}");
|
||||
let _ = writeln!(s, "menu OpenPXE - {title}");
|
||||
let filter: fn(DistroFamily) -> bool =
|
||||
if is_windows { is_windows_family } else { is_linux_family };
|
||||
let mut count = 0;
|
||||
@@ -156,9 +156,9 @@ pub fn render_tools_menu(base_url: &str) -> String {
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "set base-url {base}");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu PXEForge - Tools");
|
||||
let _ = writeln!(s, "menu OpenPXE - Tools");
|
||||
let _ = writeln!(s, "item --key u util Utilities (memtest, ...)");
|
||||
let _ = writeln!(s, "item --key s shell PXEForge Shell");
|
||||
let _ = writeln!(s, "item --key s shell OpenPXE Shell");
|
||||
let _ = writeln!(s, "item --key n nic Network Card Info");
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item --key r reboot Reboot Computer");
|
||||
@@ -202,7 +202,7 @@ pub fn render_util(base_url: &str) -> String {
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, ":menu");
|
||||
let _ = writeln!(s, "menu PXEForge - Utilities");
|
||||
let _ = writeln!(s, "menu OpenPXE - Utilities");
|
||||
let _ = writeln!(s, "item memtest MemTest86+ (RAM diagnostic)");
|
||||
let _ = writeln!(s, "item --gap");
|
||||
let _ = writeln!(s, "item back < Back");
|
||||
@@ -213,14 +213,14 @@ pub fn render_util(base_url: &str) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// "PXEForge Shell" — iPXE shell, branded.
|
||||
/// "OpenPXE Shell" — iPXE shell, branded.
|
||||
#[must_use]
|
||||
pub fn render_shell(base_url: &str) -> String {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "echo ==========================================");
|
||||
let _ = writeln!(s, "echo PXEForge Shell");
|
||||
let _ = writeln!(s, "echo OpenPXE Shell");
|
||||
let _ = writeln!(s, "echo 'exit' returns to the main menu");
|
||||
let _ = writeln!(s, "echo ==========================================");
|
||||
let _ = writeln!(s, "shell");
|
||||
@@ -247,20 +247,20 @@ pub fn render_nic_info(base_url: &str) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// Gated Deployment entry point. Joins the queue, then enters a long-poll
|
||||
/// Queued Deployment entry point. Joins the queue, then enters a long-poll
|
||||
/// loop (iPXE repeats the chain on 3xx redirects / HTTP errors until a
|
||||
/// real script comes back).
|
||||
#[must_use]
|
||||
pub fn render_gate_entry(base_url: &str) -> String {
|
||||
pub fn render_queue_entry(base_url: &str) -> String {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "#!ipxe");
|
||||
let _ = writeln!(s, "# Gated Deployment - join the queue and wait for operator");
|
||||
let _ = writeln!(s, "echo Joining gate queue...");
|
||||
let _ = writeln!(s, "# Queued Deployment - join the queue and wait for operator");
|
||||
let _ = writeln!(s, "echo Joining deployment queue...");
|
||||
// imgfetch writes the body to a file in iPXE's transient FS; we read
|
||||
// the gate id out of the Location-style header by asking the server
|
||||
// the queue entry id out of the Location-style header by asking the server
|
||||
// to put it in the response body as a single token.
|
||||
let _ = writeln!(s, "chain --replace {base}/api/gate/join?mac=${{mac}}");
|
||||
let _ = writeln!(s, "chain --replace {base}/api/queue/join?mac=${{mac}}");
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! HTTP server — single axum app that serves:
|
||||
//! - `/` the web UI (static assets from `pxeforge-webui`)
|
||||
//! - `/` the web UI (static assets from `openpxe-webui`)
|
||||
//! - `/api/*` JSON API for the web UI
|
||||
//! - `/boot.ipxe` the generated top-level iPXE boot menu
|
||||
//! - `/boot/<entry>.ipxe` per-entry iPXE scripts (one per boot target)
|
||||
|
||||
@@ -12,7 +12,7 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use pxeforge_core::LogLine;
|
||||
use openpxe_core::LogLine;
|
||||
use serde_json::json;
|
||||
use std::convert::Infallible;
|
||||
use std::time::Duration;
|
||||
@@ -57,7 +57,7 @@ pub async fn clear(State(state): State<AppState>) -> Json<serde_json::Value> {
|
||||
state.log_bus.clear();
|
||||
state
|
||||
.log_bus
|
||||
.push("info", "pxeforge::terminal", "log buffer cleared by operator");
|
||||
.push("info", "openpxe::terminal", "log buffer cleared by operator");
|
||||
Json(json!({ "ok": true }))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use pxeforge_core::{ClientRegistry, GateQueue, HostBindings, LogBus, Metrics, SettingsStore};
|
||||
use pxeforge_iso_store::{IsoStore, NfsManager, SmbManager};
|
||||
use openpxe_core::{ClientRegistry, DeploymentQueue, HostBindings, LogBus, Metrics, SettingsStore};
|
||||
use openpxe_iso_store::{IsoStore, NfsManager, SmbManager};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -8,7 +8,7 @@ pub struct AppState {
|
||||
pub iso_store: IsoStore,
|
||||
pub clients: Arc<ClientRegistry>,
|
||||
pub settings: Arc<SettingsStore>,
|
||||
pub gates: Arc<GateQueue>,
|
||||
pub queue: Arc<DeploymentQueue>,
|
||||
/// Per-MAC iPXE script overrides. When a client matching one of
|
||||
/// these MACs requests `/boot.ipxe`, we chain straight to the
|
||||
/// configured target instead of rendering the menu.
|
||||
|
||||
@@ -36,7 +36,7 @@ pub async fn run_command(
|
||||
|
||||
// Echo the typed command into the live log so the Terminal tab shows
|
||||
// operator activity in-band with server-emitted log lines.
|
||||
state.log_bus.push("info", "pxeforge::terminal", format!("> {line}"));
|
||||
state.log_bus.push("info", "openpxe::terminal", format!("> {line}"));
|
||||
|
||||
let argv = shell_split(line);
|
||||
if argv.is_empty() {
|
||||
@@ -60,9 +60,9 @@ pub async fn run_command(
|
||||
output.clone()
|
||||
};
|
||||
if ok {
|
||||
state.log_bus.push("info", "pxeforge::terminal", mirror);
|
||||
state.log_bus.push("info", "openpxe::terminal", mirror);
|
||||
} else {
|
||||
state.log_bus.push("warn", "pxeforge::terminal", mirror);
|
||||
state.log_bus.push("warn", "openpxe::terminal", mirror);
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(json!({ "output": output, "ok": ok })))
|
||||
@@ -73,12 +73,12 @@ async fn dispatch(state: &AppState, argv: &[String]) -> Result<String, String> {
|
||||
let tail = &argv[1..];
|
||||
match head {
|
||||
"help" | "?" => Ok(HELP_TEXT.to_string()),
|
||||
"version" => Ok(format!("pxeforge {}", env!("CARGO_PKG_VERSION"))),
|
||||
"version" => Ok(format!("openpxe {}", env!("CARGO_PKG_VERSION"))),
|
||||
"uptime" => Ok(uptime_string(state)),
|
||||
"status" => Ok(status_text(state)),
|
||||
"isos" | "images" => Ok(isos_text(state)),
|
||||
"clients" => Ok(clients_text(state)),
|
||||
"gate" => gate_command(state, tail).await,
|
||||
"queue" => gate_command(state, tail).await,
|
||||
"nfs" => nfs_command(state, tail).await,
|
||||
"smb" => smb_command(state, tail).await,
|
||||
"log" => log_command(state, tail),
|
||||
@@ -96,18 +96,18 @@ async fn dispatch(state: &AppState, argv: &[String]) -> Result<String, String> {
|
||||
fn status_text(s: &AppState) -> String {
|
||||
let isos = s.iso_store.list();
|
||||
let clients = s.clients.list();
|
||||
let gates = s.gates.list();
|
||||
let gates = s.queue.list();
|
||||
let smb = s.smb.as_ref().map(|m| m.snapshot());
|
||||
let nfs = s.nfs.list();
|
||||
let nfs_active = nfs.iter().filter(|m| m.mounted).count();
|
||||
format!(
|
||||
"PXEForge {ver}\n\
|
||||
"OpenPXE {ver}\n\
|
||||
base url: {base}\n\
|
||||
interface: {nic}\n\
|
||||
uptime: {up}\n\
|
||||
isos: {n_isos} (local: {n_local}, nfs: {n_nfs})\n\
|
||||
clients: {n_clients}\n\
|
||||
gates: {n_gates}\n\
|
||||
queue: {n_entries}\n\
|
||||
smb: {smb}\n\
|
||||
nfs mounts: {n_total} configured ({n_active} active)\n",
|
||||
ver = env!("CARGO_PKG_VERSION"),
|
||||
@@ -115,10 +115,10 @@ fn status_text(s: &AppState) -> String {
|
||||
nic = if s.nic_name.is_empty() { "?" } else { s.nic_name.as_str() },
|
||||
up = uptime_string(s),
|
||||
n_isos = isos.len(),
|
||||
n_local = isos.iter().filter(|i| matches!(i.source, pxeforge_iso_store::IsoSource::Local)).count(),
|
||||
n_nfs = isos.iter().filter(|i| !matches!(i.source, pxeforge_iso_store::IsoSource::Local)).count(),
|
||||
n_local = isos.iter().filter(|i| matches!(i.source, openpxe_iso_store::IsoSource::Local)).count(),
|
||||
n_nfs = isos.iter().filter(|i| !matches!(i.source, openpxe_iso_store::IsoSource::Local)).count(),
|
||||
n_clients = clients.len(),
|
||||
n_gates = gates.len(),
|
||||
n_entries = gates.len(),
|
||||
smb = smb.map_or_else(|| "(disabled)".into(), |s| format!("{s:?}")),
|
||||
n_total = nfs.len(),
|
||||
n_active = nfs_active,
|
||||
@@ -138,8 +138,8 @@ fn isos_text(s: &AppState) -> String {
|
||||
);
|
||||
for i in isos {
|
||||
let src = match i.source {
|
||||
pxeforge_iso_store::IsoSource::Local => "local".to_string(),
|
||||
pxeforge_iso_store::IsoSource::Nfs { mount_id, .. } => format!("nfs:{mount_id}"),
|
||||
openpxe_iso_store::IsoSource::Local => "local".to_string(),
|
||||
openpxe_iso_store::IsoSource::Nfs { mount_id, .. } => format!("nfs:{mount_id}"),
|
||||
};
|
||||
let _ = writeln!(
|
||||
out,
|
||||
@@ -188,7 +188,7 @@ fn clients_text(s: &AppState) -> String {
|
||||
async fn gate_command(s: &AppState, args: &[String]) -> Result<String, String> {
|
||||
match args.first().map(String::as_str) {
|
||||
None | Some("list") => {
|
||||
let gs = s.gates.list();
|
||||
let gs = s.queue.list();
|
||||
if gs.is_empty() {
|
||||
return Ok("(no gates)".into());
|
||||
}
|
||||
@@ -217,28 +217,28 @@ async fn gate_command(s: &AppState, args: &[String]) -> Result<String, String> {
|
||||
if !found {
|
||||
return Err(format!("no such boot entry: {target}"));
|
||||
}
|
||||
let ids: Vec<_> = s.gates.list().into_iter().map(|g| g.id).collect();
|
||||
let n = s.gates.assign(&ids, target);
|
||||
let ids: Vec<_> = s.queue.list().into_iter().map(|g| g.id).collect();
|
||||
let n = s.queue.assign(&ids, target);
|
||||
Ok(format!("assigned {n} gates -> {target}"))
|
||||
}
|
||||
Some("assign") => {
|
||||
let gate_id = args
|
||||
let entry_id = args
|
||||
.get(1)
|
||||
.ok_or_else(|| "usage: gate assign <gate_id> <iso_boot_entry_id>".to_string())?;
|
||||
.ok_or_else(|| "usage: gate assign <entry_id> <iso_boot_entry_id>".to_string())?;
|
||||
let target = args
|
||||
.get(2)
|
||||
.ok_or_else(|| "usage: gate assign <gate_id> <iso_boot_entry_id>".to_string())?;
|
||||
let n = s.gates.assign(std::slice::from_ref(gate_id), target);
|
||||
.ok_or_else(|| "usage: gate assign <entry_id> <iso_boot_entry_id>".to_string())?;
|
||||
let n = s.queue.assign(std::slice::from_ref(entry_id), target);
|
||||
if n == 0 {
|
||||
return Err(format!("no such gate: {gate_id}"));
|
||||
return Err(format!("no such gate: {entry_id}"));
|
||||
}
|
||||
Ok(format!("assigned 1 gate -> {target}"))
|
||||
}
|
||||
Some("release") => {
|
||||
let gate_id = args.get(1).ok_or_else(|| "usage: gate release <gate_id>".to_string())?;
|
||||
match s.gates.release(gate_id) {
|
||||
Some(_) => Ok(format!("released {gate_id}")),
|
||||
None => Err(format!("no such gate: {gate_id}")),
|
||||
let entry_id = args.get(1).ok_or_else(|| "usage: gate release <entry_id>".to_string())?;
|
||||
match s.queue.release(entry_id) {
|
||||
Some(_) => Ok(format!("released {entry_id}")),
|
||||
None => Err(format!("no such gate: {entry_id}")),
|
||||
}
|
||||
}
|
||||
Some(other) => Err(format!(
|
||||
@@ -269,8 +269,8 @@ async fn nfs_command(s: &AppState, args: &[String]) -> Result<String, String> {
|
||||
"{:<24} {:<6} {:<7} {:<6} {}:{}",
|
||||
truncate(&m.id, 24),
|
||||
match m.version {
|
||||
pxeforge_iso_store::NfsVersion::V3 => "v3",
|
||||
pxeforge_iso_store::NfsVersion::V41 => "v4.1",
|
||||
openpxe_iso_store::NfsVersion::V3 => "v3",
|
||||
openpxe_iso_store::NfsVersion::V41 => "v4.1",
|
||||
},
|
||||
status,
|
||||
m.iso_count,
|
||||
@@ -292,12 +292,12 @@ async fn nfs_command(s: &AppState, args: &[String]) -> Result<String, String> {
|
||||
.split_once(':')
|
||||
.ok_or_else(|| "target must be 'server:/export'".to_string())?;
|
||||
let version = match args.get(2).map(String::as_str) {
|
||||
Some("v3") => pxeforge_iso_store::NfsVersion::V3,
|
||||
Some("v41") | None => pxeforge_iso_store::NfsVersion::V41,
|
||||
Some("v3") => openpxe_iso_store::NfsVersion::V3,
|
||||
Some("v41") | None => openpxe_iso_store::NfsVersion::V41,
|
||||
Some(other) => return Err(format!("unknown nfs version: {other} (expect v3 or v41)")),
|
||||
};
|
||||
let read_only = !matches!(args.get(3).map(String::as_str), Some("rw"));
|
||||
let req = pxeforge_iso_store::NfsAddRequest {
|
||||
let req = openpxe_iso_store::NfsAddRequest {
|
||||
server: server.to_string(),
|
||||
export: export.to_string(),
|
||||
version,
|
||||
@@ -453,7 +453,7 @@ pub fn shell_split(input: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
const HELP_TEXT: &str = "\
|
||||
PXEForge terminal — available commands:
|
||||
OpenPXE terminal — available commands:
|
||||
|
||||
help show this help
|
||||
version print server version
|
||||
@@ -463,9 +463,9 @@ PXEForge terminal — available commands:
|
||||
isos list registered ISOs
|
||||
clients list PXE clients seen this session
|
||||
gate list list gated-deployment queue
|
||||
gate assign <gate_id> <target> assign one gate to a boot entry
|
||||
gate assign <entry_id> <target> assign one gate to a boot entry
|
||||
gate assign-all <target> assign every waiting gate
|
||||
gate release <gate_id> release one gate
|
||||
gate release <entry_id> release one gate
|
||||
|
||||
nfs list list NFS mounts
|
||||
nfs mount <s>:<e> [v3|v41] [ro|rw] add and mount an NFS share
|
||||
|
||||
Reference in New Issue
Block a user