v0.5.2: FleetDM login split, 3-slot branding, unattended installs

Authentication / login:
- Separate the local username/password form from the SSO "Sign in with …"
  button (FleetDM-style divider + optional IdP logo); credential fields no
  longer double as the SSO trigger. Settings → SSO copy now says SAML is live.

Branding — three slots (light / dark / client) on one row:
- Light/Dark feed the top-left mark + sign-in page by active theme (with
  cross-theme fallback; theme toggle swaps the logo live). Client feeds the
  PXE boot-menu background. Favicon pinned to the bundled mark via a new
  /assets/favicon.svg endpoint. Legacy single logo migrates to dark + client.
- BrandingStore refactored to per-slot storage; /api/branding/logo/:slot.

Unattended installs (Storage → Advanced):
- New UnattendedStore (iso-store) + /api/unattended upload/list/delete and a
  public templated serve at /unattended/:id (+ NoCloud seed dir for
  autoinstall). Accepts .ks/.cfg/.seed/.yaml/.yml/.xml/user-data; classified
  on upload; stored in its own unattended/ dir, never the ISO listing/menu.
- {{HOSTNAME}}/{{IP}}/{{MAC}} substituted per host at serve time.

Host pins + Queue profiles:
- HostBinding + QueueEntry carry an optional DeployProfile (auto_hostname /
  auto_ip / unattended_file). Hosts pin form + a per-device Queue "Profile"
  button collect them. On boot, a matched MAC has the right kernel arg
  injected (inst.ks= / preseed url= / autoinstall ds=nocloud-net) and the
  hostname/IP templated into the served answer file. DHCP stays proxy-only.

Storage:
- Remote shares default protocol is now NFS; updated descriptive copy.

235 tests green, clippy clean. Still a single static musl binary, pure Rust.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-05-31 16:11:04 -04:00
co-authored by Claude Opus 4.8
parent cbcd63bb14
commit 7adf5e2918
19 changed files with 2295 additions and 330 deletions
+32
View File
@@ -21,6 +21,7 @@ use time::OffsetDateTime;
use tokio::sync::Notify;
use uuid::Uuid;
use crate::profile::DeployProfile;
use crate::ClientArch;
/// Per-client queue state visible to the WebUI.
@@ -37,6 +38,11 @@ pub struct QueueEntry {
#[serde(with = "time::serde::rfc3339")]
pub last_poll_at: OffsetDateTime,
pub assigned_target: Option<String>,
/// v0.5.2: optional per-device deployment profile set via the queue
/// "Profile" button (auto hostname / IP / unattended file). Flattened
/// so the JSON stays flat alongside the other queue fields.
#[serde(default, flatten)]
pub profile: DeployProfile,
}
#[derive(Debug)]
@@ -49,6 +55,7 @@ struct QueueEntryInner {
joined_at: OffsetDateTime,
last_poll_at: OffsetDateTime,
assigned_target: Option<String>,
profile: DeployProfile,
/// Broadcast primitive that wakes the long-poll as soon as an
/// assignment lands — no polling on our side, no sleep-loops.
notify: Arc<Notify>,
@@ -65,6 +72,7 @@ impl QueueEntryInner {
joined_at: self.joined_at,
last_poll_at: self.last_poll_at,
assigned_target: self.assigned_target.clone(),
profile: self.profile.clone(),
}
}
}
@@ -110,6 +118,7 @@ impl DeploymentQueue {
joined_at: now,
last_poll_at: now,
assigned_target: None,
profile: DeployProfile::default(),
notify: Arc::new(Notify::new()),
};
let snap = inner.snapshot();
@@ -133,6 +142,29 @@ impl DeploymentQueue {
Some(g.snapshot())
}
/// Operator sets (or clears) the deployment profile for a queued
/// device via the WebUI "Profile" button. Returns the updated
/// snapshot, or `None` if the entry has since been released.
pub fn set_profile(&self, entry_id: &str, profile: DeployProfile) -> Option<QueueEntry> {
let mut guard = self.inner.write();
let g = guard.get_mut(entry_id)?;
g.profile = profile.normalized();
Some(g.snapshot())
}
/// Look up the deployment profile for a queued MAC, if any. Used by
/// the boot chain to inject an unattended file / template the
/// hostname + IP when an assigned device chains to its target.
#[must_use]
pub fn profile_for_mac(&self, mac: &str) -> Option<DeployProfile> {
let guard = self.inner.read();
guard
.values()
.find(|g| g.mac == mac)
.map(|g| g.profile.clone())
.filter(|p| !p.is_empty())
}
/// Operator assigns an ISO entry (boot_entry id) to one or more clients.
/// Returns the number of queue entries that were updated. Entries not in the
/// queue are silently skipped.