101 lines
3.0 KiB
Rust
101 lines
3.0 KiB
Rust
//! In-memory client state registry — the "who has contacted us" table the
|
|
//! web UI displays. Not persisted: PXE sessions are ephemeral by nature.
|
|
|
|
use crate::arch::ClientArch;
|
|
use parking_lot::RwLock;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::net::IpAddr;
|
|
use std::sync::Arc;
|
|
use time::OffsetDateTime;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ClientEvent {
|
|
DhcpDiscover,
|
|
DhcpRequest,
|
|
PxeBootServerRequest,
|
|
TftpRead { file: String },
|
|
HttpScriptFetch { target: String },
|
|
HttpIsoAsset { file: String },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ClientSnapshot {
|
|
pub mac: String,
|
|
pub last_ip: Option<IpAddr>,
|
|
pub arch: Option<ClientArch>,
|
|
pub hostname: Option<String>,
|
|
#[serde(with = "time::serde::rfc3339")]
|
|
pub first_seen: OffsetDateTime,
|
|
#[serde(with = "time::serde::rfc3339")]
|
|
pub last_seen: OffsetDateTime,
|
|
// Events are left with default serialization (9-tuple) — they're
|
|
// diagnostic only and not consumed by the UI today.
|
|
pub events: Vec<(OffsetDateTime, ClientEvent)>,
|
|
/// The boot target (ISO id) last selected via the iPXE menu, if any.
|
|
pub selected_target: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct ClientRegistry {
|
|
inner: RwLock<HashMap<String, ClientSnapshot>>,
|
|
}
|
|
|
|
impl ClientRegistry {
|
|
#[must_use]
|
|
pub fn new() -> Arc<Self> {
|
|
Arc::new(Self::default())
|
|
}
|
|
|
|
pub fn record(
|
|
&self,
|
|
mac: &str,
|
|
ip: Option<IpAddr>,
|
|
arch: Option<ClientArch>,
|
|
event: ClientEvent,
|
|
) {
|
|
let mut guard = self.inner.write();
|
|
let now = OffsetDateTime::now_utc();
|
|
let entry = guard.entry(mac.to_string()).or_insert_with(|| ClientSnapshot {
|
|
mac: mac.to_string(),
|
|
last_ip: ip,
|
|
arch,
|
|
hostname: None,
|
|
first_seen: now,
|
|
last_seen: now,
|
|
events: Vec::new(),
|
|
selected_target: None,
|
|
});
|
|
entry.last_seen = now;
|
|
if ip.is_some() { entry.last_ip = ip; }
|
|
if arch.is_some() { entry.arch = arch; }
|
|
entry.events.push((now, event));
|
|
// Cap event history per client to keep memory bounded.
|
|
const MAX_EVENTS: usize = 64;
|
|
if entry.events.len() > MAX_EVENTS {
|
|
let drop_n = entry.events.len() - MAX_EVENTS;
|
|
entry.events.drain(..drop_n);
|
|
}
|
|
}
|
|
|
|
pub fn set_selected_target(&self, mac: &str, target: Option<String>) {
|
|
let mut guard = self.inner.write();
|
|
if let Some(c) = guard.get_mut(mac) {
|
|
c.selected_target = target;
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn list(&self) -> Vec<ClientSnapshot> {
|
|
let guard = self.inner.read();
|
|
let mut v: Vec<_> = guard.values().cloned().collect();
|
|
v.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
|
|
v
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn get(&self, mac: &str) -> Option<ClientSnapshot> {
|
|
self.inner.read().get(mac).cloned()
|
|
}
|
|
}
|