v0.3.0 — rebrand: PXEForge → OpenPXE, Gated → Queued Deployment
Full rename to match the openpxe.com brand. The product now reads as a
polished open-source project rather than a personal-tool nickname:
the anvil/forge metaphor is gone, replaced with the rainbow-horizon
brand mark from the marketing site.
## Naming changes
**PXEForge → OpenPXE** everywhere it's user-visible or developer-
facing:
- All 8 crate package names (`pxeforge-*` → `openpxe-*`).
- The bin crate dir + binary (`crates/pxeforge` → `crates/openpxe`,
`bin = "openpxe"`).
- Env vars: `PXEFORGE_*` → `OPENPXE_*` (no compat shim — pre-beta).
- Tracing targets: `pxeforge::*` → `openpxe::*`.
- Prometheus metrics: `pxeforge_*` → `openpxe_*` (pre-beta; nobody
has dashboards on these yet).
- Container image: `gitea.milesward.dev/mward4/openpxe:0.3.0`.
- All in-tree paths: `/var/lib/openpxe/{isos,work,smb}`,
`/usr/share/openpxe/ipxe`, `/etc/openpxe/...`.
- Unraid template renamed `pxeforge.xml` → `openpxe.xml`.
- README, NEXT_PHASE.md, architecture.md, comments, and the WebUI
brand string.
**Gated Deployment → Queued Deployment** as the user-facing concept:
- `Settings::TimeoutAction::GatedDeployment` →
`QueuedDeployment` (with `#[serde(alias = "gated_deployment")]`
so v0.2.0 settings.json files keep deserializing).
- Rust types: `Gate` → `QueueEntry`, `GateQueue` → `DeploymentQueue`,
`GateInner` → `QueueEntryInner`.
- File: `crates/core/src/gate.rs` → `crates/core/src/queue.rs`.
- HTTP routes: `/api/gate/*` → `/api/queue/*`. The JSON list key
flipped from `"gates"` to `"entries"` to match.
- iPXE shortcut: `/boot/_gate.ipxe` → `/boot/_queue.ipxe`. The
top-level menu's item id is now `queue` instead of `gate`.
- WebUI sidebar tab: "Forge Gate" → "Queue".
- Field on `AppState`: `gates` → `queue`.
## Brand assets
The anvil + forging-sparks logos are dropped:
- `logo.svg` is now a 24×24 medallion filled with the
`rainbow-horizon` gradient from openpxe.com (sliding hue rotation
via SMIL on the gradient stops, no JS needed).
- `anvil-forge.svg` renamed to `loader.svg` and rebuilt as a 64×64
louder version of the same disc — used for page-load transitions
and the imaging-progress widget. Adds a subtle scale pulse and a
white inner-glow so it has dimensionality on either theme.
## CSS rename
- `.forge-progress` → `.queue-progress`
- `.forge-progress .anvil` → `.queue-progress .mark`
- `@keyframes forge-sheen` → `queue-sheen`
- `.loader .anvil` → `.loader .mark`
- "Heating the forge…" loader text → "Loading…"
The rest of the layout is untouched. Light/dark theme tokens and the
sidebar/topbar structure carry over from v0.2.0 unchanged — the
brief was "keeping the UI similar."
## Validation
- `cargo build --workspace` — clean.
- `cargo clippy --workspace --all-targets` — no warnings.
- `cargo test --workspace` — **66 tests passing**, same as v0.2.0.
- Local smoke run against the rebuilt release binary verifies:
- `/boot.ipxe` emits `Queued Deployment` + `item queue` + chains
`/boot/_queue.ipxe`
- `/api/queue` returns `{count, entries}`
- `/metrics` emits `openpxe_queue_count` (renamed)
- `/assets/logo.svg` and `/assets/loader.svg` serve the new
rainbow brand SVGs
- `/api/status` reports version `0.3.0`
## Migration notes for operators on v0.2.0
- Container image path changed: pull
`gitea.milesward.dev/mward4/openpxe:0.3.0` (not `pxeforge:`).
- Bind mounts: `/var/lib/openpxe/{isos,work,smb}` (not `pxeforge`).
Move the host path or update the template.
- Env vars: replace `PXEFORGE_*` with `OPENPXE_*`. The Unraid
template at `deploy/unraid/openpxe.xml` is already updated.
- `settings.json` carries over transparently — the
`gated_deployment` value is accepted as an alias.
- HTTP API: any external scripts that hit `/api/gate/*` need to
switch to `/api/queue/*`. The JSON envelope key is `entries`
instead of `gates`.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
//! Spins up the real axum router against a temp ISO store + settings store,
|
||||
//! then walks an imaginary iPXE client through: dashboard status → upload
|
||||
//! ISO → fetch top-level boot menu → fetch per-entry script → Range-GET the
|
||||
//! ISO. Also drives the Gated Deployment flow end-to-end: two clients join,
|
||||
//! ISO. Also drives the Queued Deployment flow end-to-end: two clients join,
|
||||
//! operator assigns, both polls return the chain script with retry fallback.
|
||||
//!
|
||||
//! This is the closest we can get to "real PXE client" without QEMU; the
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use pxeforge_core::{ClientRegistry, GateQueue, HostBindings, LogBus, Metrics, SettingsStore};
|
||||
use pxeforge_http_api::{build_router, AppState};
|
||||
use pxeforge_iso_store::{IsoStore, NfsManager};
|
||||
use openpxe_core::{ClientRegistry, DeploymentQueue, HostBindings, LogBus, Metrics, SettingsStore};
|
||||
use openpxe_http_api::{build_router, AppState};
|
||||
use openpxe_iso_store::{IsoStore, NfsManager};
|
||||
use tempfile::tempdir;
|
||||
use tower::ServiceExt;
|
||||
|
||||
@@ -87,7 +87,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
|
||||
let iso_store = IsoStore::new(dir.path().join("isos"));
|
||||
iso_store.ensure_dirs().await.unwrap();
|
||||
let clients = ClientRegistry::new();
|
||||
let gates = GateQueue::new();
|
||||
let gates = DeploymentQueue::new();
|
||||
let settings = SettingsStore::load_or_default(dir.path());
|
||||
let nfs = NfsManager::new(dir.path(), iso_store.clone());
|
||||
iso_store.set_nfs_root(nfs.mount_root());
|
||||
@@ -97,7 +97,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
|
||||
let state = AppState {
|
||||
iso_store,
|
||||
clients,
|
||||
gates,
|
||||
queue: gates,
|
||||
settings,
|
||||
hosts,
|
||||
metrics,
|
||||
@@ -198,7 +198,7 @@ async fn iso_range_request_slices_correctly() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gated_deployment_full_flow() {
|
||||
async fn queued_deployment_full_flow() {
|
||||
let (state, _dir) = build_state().await;
|
||||
let app = build_router(state.clone());
|
||||
|
||||
@@ -210,22 +210,22 @@ async fn gated_deployment_full_flow() {
|
||||
.await.unwrap();
|
||||
|
||||
// Two clients join.
|
||||
let (_, join1) = get(&app, "/api/gate/join?mac=aa:bb:cc:00:00:01").await;
|
||||
let (_, join2) = get(&app, "/api/gate/join?mac=aa:bb:cc:00:00:02").await;
|
||||
let (_, join1) = get(&app, "/api/queue/join?mac=aa:bb:cc:00:00:01").await;
|
||||
let (_, join2) = get(&app, "/api/queue/join?mac=aa:bb:cc:00:00:02").await;
|
||||
let s1 = String::from_utf8(join1).unwrap();
|
||||
let s2 = String::from_utf8(join2).unwrap();
|
||||
assert!(s1.contains("Gate Position 1"));
|
||||
assert!(s2.contains("Gate Position 2"));
|
||||
|
||||
let gate1_id = s1.lines().find_map(|l| l.strip_prefix("chain http://127.0.0.1/api/gate/poll/"))
|
||||
let gate1_id = s1.lines().find_map(|l| l.strip_prefix("chain http://127.0.0.1/api/queue/poll/"))
|
||||
.unwrap().to_string();
|
||||
let gate2_id = s2.lines().find_map(|l| l.strip_prefix("chain http://127.0.0.1/api/gate/poll/"))
|
||||
let gate2_id = s2.lines().find_map(|l| l.strip_prefix("chain http://127.0.0.1/api/queue/poll/"))
|
||||
.unwrap().to_string();
|
||||
|
||||
// Kick off a long-poll for client 1 in the background. Then assign.
|
||||
let app2 = app.clone();
|
||||
let poll_future = tokio::spawn(async move {
|
||||
let uri = format!("/api/gate/poll/{gate1_id}");
|
||||
let uri = format!("/api/queue/poll/{gate1_id}");
|
||||
get(&app2, &uri).await
|
||||
});
|
||||
|
||||
@@ -233,15 +233,15 @@ async fn gated_deployment_full_flow() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
|
||||
// Operator assigns.
|
||||
let body = format!(r#"{{"target":"fake-alpine-linux","gate_ids":["{gate2_id}"]}}"#);
|
||||
let (s, b) = post_json(&app, "/api/gate/assign", &body).await;
|
||||
let body = format!(r#"{{"target":"fake-alpine-linux","entry_ids":["{gate2_id}"]}}"#);
|
||||
let (s, b) = post_json(&app, "/api/queue/assign", &body).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let assign_json = String::from_utf8(b).unwrap();
|
||||
assert!(assign_json.contains(r#""assigned":1"#), "assign response: {assign_json}");
|
||||
|
||||
// Now assign to gate 1 too so the background poll wakes.
|
||||
let body = r#"{"target":"fake-alpine-linux","gate_ids":[]}"#;
|
||||
post_json(&app, "/api/gate/assign", body).await;
|
||||
let body = r#"{"target":"fake-alpine-linux","entry_ids":[]}"#;
|
||||
post_json(&app, "/api/queue/assign", body).await;
|
||||
|
||||
let (poll_status, poll_body) = poll_future.await.unwrap();
|
||||
assert_eq!(poll_status, StatusCode::OK);
|
||||
@@ -251,12 +251,12 @@ async fn gated_deployment_full_flow() {
|
||||
"poll response should chain the boot script:\n{poll_s}"
|
||||
);
|
||||
// Retry-on-error fallback must be present.
|
||||
assert!(poll_s.contains("|| chain http://127.0.0.1/api/gate/poll/"),
|
||||
assert!(poll_s.contains("|| chain http://127.0.0.1/api/queue/poll/"),
|
||||
"retry fallback missing");
|
||||
|
||||
// Bad target must be rejected.
|
||||
let (_, bad) = post_json(&app, "/api/gate/assign",
|
||||
r#"{"target":"does-not-exist","gate_ids":[]}"#).await;
|
||||
let (_, bad) = post_json(&app, "/api/queue/assign",
|
||||
r#"{"target":"does-not-exist","entry_ids":[]}"#).await;
|
||||
let bad_s = String::from_utf8(bad).unwrap();
|
||||
assert!(bad_s.contains(r#""ok":false"#), "expected rejection: {bad_s}");
|
||||
}
|
||||
@@ -329,7 +329,7 @@ async fn ui_assets_served_offline() {
|
||||
("/assets/app.js", "application/javascript"),
|
||||
("/assets/app.css", "text/css"),
|
||||
("/assets/logo.svg", "image/svg+xml"),
|
||||
("/assets/anvil-forge.svg", "image/svg+xml"),
|
||||
("/assets/loader.svg", "image/svg+xml"),
|
||||
] {
|
||||
let res = app
|
||||
.clone()
|
||||
@@ -406,14 +406,14 @@ async fn terminal_help_and_status_round_trip() {
|
||||
let (s, b) = post_json(&app, "/api/terminal", r#"{"command":""}"#).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
|
||||
assert!(v["output"].as_str().unwrap().contains("PXEForge terminal"));
|
||||
assert!(v["output"].as_str().unwrap().contains("OpenPXE terminal"));
|
||||
|
||||
// status -> contains the version banner.
|
||||
let (s, b) = post_json(&app, "/api/terminal", r#"{"command":"status"}"#).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
|
||||
let out = v["output"].as_str().unwrap();
|
||||
assert!(out.starts_with("PXEForge"), "unexpected status output: {out}");
|
||||
assert!(out.starts_with("OpenPXE"), "unexpected status output: {out}");
|
||||
assert!(out.contains("isos:"), "status missing iso line: {out}");
|
||||
|
||||
// Unknown command -> ok=false plus help hint.
|
||||
@@ -477,7 +477,7 @@ async fn windows_iso_renders_clean_wimboot_script_with_no_trust_store_writes() {
|
||||
.uri("/api/settings")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"boot_menu_timeout_secs":600,"timeout_action":"gated_deployment",
|
||||
r#"{"boot_menu_timeout_secs":600,"timeout_action":"queued_deployment",
|
||||
"windows_enabled":false,"smb_host_override":"","extra_kernel_args":"",
|
||||
"default_local_hdd":true,"gate_wait_max_secs":0,"dns_server":""}"#
|
||||
.to_string(),
|
||||
@@ -623,19 +623,19 @@ async fn metrics_endpoint_emits_prometheus_format() {
|
||||
let body = String::from_utf8(body.to_vec()).unwrap();
|
||||
// Spot-check the must-have metric families.
|
||||
for name in [
|
||||
"pxeforge_dhcp_replies_total",
|
||||
"pxeforge_tftp_transfers_total",
|
||||
"pxeforge_http_requests_total",
|
||||
"pxeforge_iso_count",
|
||||
"pxeforge_uptime_seconds",
|
||||
"pxeforge_build_info",
|
||||
"openpxe_dhcp_replies_total",
|
||||
"openpxe_tftp_transfers_total",
|
||||
"openpxe_http_requests_total",
|
||||
"openpxe_iso_count",
|
||||
"openpxe_uptime_seconds",
|
||||
"openpxe_build_info",
|
||||
] {
|
||||
assert!(body.contains(name), "missing metric {name} in:\n{body}");
|
||||
}
|
||||
// Each name appears exactly once as a `# TYPE` declaration.
|
||||
for name in [
|
||||
"pxeforge_dhcp_replies_total",
|
||||
"pxeforge_iso_count",
|
||||
"openpxe_dhcp_replies_total",
|
||||
"openpxe_iso_count",
|
||||
] {
|
||||
let count = body.matches(&format!("# TYPE {name}")).count();
|
||||
assert_eq!(count, 1, "{name} TYPE line appears {count} times");
|
||||
|
||||
Reference in New Issue
Block a user