Compare commits
3
Commits
No files matched your search
@@ -18,3 +18,5 @@ target/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.codex
|
||||||
Generated
+716
-7
File diff suppressed because it is too large.
Load diff
@@ -125,7 +125,7 @@ Example:
|
|||||||
data_dir = "/var/lib/wakey-control-plane"
|
data_dir = "/var/lib/wakey-control-plane"
|
||||||
bind = "0.0.0.0:8080"
|
bind = "0.0.0.0:8080"
|
||||||
public_url = "https://cp.example.com"
|
public_url = "https://cp.example.com"
|
||||||
state_file = "state.db"
|
state_file = "state.sqlite3"
|
||||||
pid_file = "wakey-control-plane.pid"
|
pid_file = "wakey-control-plane.pid"
|
||||||
ui_dist_dir = "/opt/wakey/ui/dist"
|
ui_dist_dir = "/opt/wakey/ui/dist"
|
||||||
command_timeout_ms = 30000
|
command_timeout_ms = 30000
|
||||||
@@ -140,9 +140,14 @@ json_logs = false
|
|||||||
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
If `telemetry.otlp_endpoint` is omitted, logs still work normally and only local
|
||||||
structured logs are emitted.
|
structured logs are emitted.
|
||||||
|
|
||||||
State is persisted in an embedded `sled` database (default
|
State is persisted in an embedded SQLite database (default
|
||||||
`/var/lib/wakey-control-plane/state.db`).
|
`/var/lib/wakey-control-plane/state.sqlite3`).
|
||||||
Relative paths in config are resolved under `data_dir`.
|
Relative paths in config are resolved under `data_dir`.
|
||||||
|
Legacy sled state can be imported explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey-control-plane import-sled-state --from-sled-state /var/lib/wakey-control-plane/state.db --to-state-file /var/lib/wakey-control-plane/state.sqlite3
|
||||||
|
```
|
||||||
|
|
||||||
Enroll tokens are now expiring and revocable. Issuance returns `expires_at_unix`.
|
Enroll tokens are now expiring and revocable. Issuance returns `expires_at_unix`.
|
||||||
Expired tokens are rejected on enroll and can be garbage-collected periodically
|
Expired tokens are rejected on enroll and can be garbage-collected periodically
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const fn presence_rank(presence: Presence) -> u8 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use wakey_core::{DhcpLease, InventoryQueryBuilder, NeighborState};
|
use wakey_core::{DeviceId, DhcpLease, InventoryQueryBuilder, NeighborState};
|
||||||
|
|
||||||
fn sample_neighbors() -> Vec<NeighborEntry> {
|
fn sample_neighbors() -> Vec<NeighborEntry> {
|
||||||
vec![NeighborEntry {
|
vec![NeighborEntry {
|
||||||
@@ -154,4 +154,25 @@ mod tests {
|
|||||||
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_uses_ip_observed_id_when_mac_is_absent() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices(
|
||||||
|
vec![NeighborEntry {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)),
|
||||||
|
dev: Some("br-lan".to_string()),
|
||||||
|
mac: None,
|
||||||
|
state: NeighborState::Stale,
|
||||||
|
}],
|
||||||
|
Vec::new(),
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
out[0].id,
|
||||||
|
Some(DeviceId::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20))))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,14 @@ reqwest = { version = "0.13", default-features = false, features = [
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sled = "0.34"
|
sled = "0.34"
|
||||||
|
sqlx = { version = "0.8", features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"sqlite",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
"uuid",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
toml = "1.1"
|
toml = "1.1"
|
||||||
tokio = { version = "1", features = [
|
tokio = { version = "1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
CREATE TABLE meta (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE enroll_tokens (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
expires_at_unix INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE agents (
|
||||||
|
agent_id TEXT PRIMARY KEY,
|
||||||
|
agent_token TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE agent_meta (
|
||||||
|
agent_id TEXT PRIMARY KEY REFERENCES agents(agent_id) ON DELETE CASCADE,
|
||||||
|
nickname TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE known_devices (
|
||||||
|
device_id TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
pinned INTEGER NOT NULL,
|
||||||
|
created_at_unix INTEGER NOT NULL,
|
||||||
|
updated_at_unix INTEGER NOT NULL,
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE device_identifiers (
|
||||||
|
identifier_key TEXT PRIMARY KEY,
|
||||||
|
device_id TEXT NOT NULL REFERENCES known_devices(device_id) ON DELETE CASCADE,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_at_unix INTEGER NOT NULL,
|
||||||
|
UNIQUE(kind, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX device_identifiers_device_id_idx ON device_identifiers(device_id);
|
||||||
|
|
||||||
|
CREATE TABLE audit_events (
|
||||||
|
event_key TEXT PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
ts_unix INTEGER NOT NULL,
|
||||||
|
actor_type TEXT NOT NULL,
|
||||||
|
actor_id TEXT,
|
||||||
|
agent_id TEXT,
|
||||||
|
request_id TEXT,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
outcome TEXT NOT NULL,
|
||||||
|
latency_ms INTEGER,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
metadata_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX audit_events_ts_unix_idx ON audit_events(ts_unix);
|
||||||
|
CREATE INDEX audit_events_agent_id_idx ON audit_events(agent_id);
|
||||||
|
CREATE INDEX audit_events_request_id_idx ON audit_events(request_id);
|
||||||
|
CREATE INDEX audit_events_event_type_outcome_idx ON audit_events(event_type, outcome);
|
||||||
|
|
||||||
|
CREATE TABLE active_alerts (
|
||||||
|
alert_id TEXT PRIMARY KEY,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
severity TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
agent_id TEXT,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
threshold INTEGER NOT NULL,
|
||||||
|
last_seen_unix INTEGER NOT NULL,
|
||||||
|
metadata_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE alert_transitions (
|
||||||
|
transition_key TEXT PRIMARY KEY,
|
||||||
|
transition_id TEXT NOT NULL UNIQUE,
|
||||||
|
ts_unix INTEGER NOT NULL,
|
||||||
|
alert_id TEXT NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
agent_id TEXT,
|
||||||
|
from_status TEXT,
|
||||||
|
to_status TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
metadata_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX alert_transitions_ts_unix_idx ON alert_transitions(ts_unix);
|
||||||
@@ -7,7 +7,7 @@ use tracing::{info, warn};
|
|||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::json_error;
|
||||||
use crate::runtime::{AppState, SessionEvent};
|
use crate::runtime::{AppState, SessionEvent};
|
||||||
use crate::state::AuditEventInput;
|
use crate::state::{AuditEventInput, DeviceIdentifierInput, KnownDeviceInput};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct EnrollRequest {
|
pub struct EnrollRequest {
|
||||||
@@ -72,6 +72,48 @@ pub struct StateStatsResponse {
|
|||||||
pub expired_enroll_token_count: usize,
|
pub expired_enroll_token_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CreateKnownDeviceRequest {
|
||||||
|
pub display_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pinned: bool,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub identifiers: Vec<DeviceIdentifierRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DeviceIdentifierRequest {
|
||||||
|
pub kind: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KnownDeviceResponse {
|
||||||
|
pub device_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub pinned: bool,
|
||||||
|
pub created_at_unix: u64,
|
||||||
|
pub updated_at_unix: u64,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
pub identifiers: Vec<DeviceIdentifierResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceIdentifierResponse {
|
||||||
|
pub identifier_key: String,
|
||||||
|
pub device_id: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub value: String,
|
||||||
|
pub created_at_unix: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ForgetKnownDeviceResponse {
|
||||||
|
pub device_id: String,
|
||||||
|
pub forgotten: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn healthz() -> &'static str {
|
pub async fn healthz() -> &'static str {
|
||||||
"ok"
|
"ok"
|
||||||
}
|
}
|
||||||
@@ -407,6 +449,116 @@ pub async fn set_agent_nickname(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_known_device(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(req): Json<CreateKnownDeviceRequest>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let input = KnownDeviceInput {
|
||||||
|
display_name: req.display_name,
|
||||||
|
pinned: req.pinned,
|
||||||
|
notes: req.notes,
|
||||||
|
identifiers: req
|
||||||
|
.identifiers
|
||||||
|
.into_iter()
|
||||||
|
.map(|identifier| DeviceIdentifierInput {
|
||||||
|
kind: identifier.kind,
|
||||||
|
value: identifier.value,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.store.create_known_device(input).await {
|
||||||
|
Ok(device) => Ok((StatusCode::CREATED, Json(known_device_response(device)))),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to create known device");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"create_known_device_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_known_devices(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
match state.store.list_known_devices().await {
|
||||||
|
Ok(devices) => Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(
|
||||||
|
devices
|
||||||
|
.into_iter()
|
||||||
|
.map(known_device_response)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to list known devices");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"list_known_devices_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn forget_known_device(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(device_id): AxumPath<String>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
match state.store.forget_known_device(&device_id).await {
|
||||||
|
Ok(forgotten) => Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(ForgetKnownDeviceResponse {
|
||||||
|
device_id,
|
||||||
|
forgotten,
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to forget known device");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"forget_known_device_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn attach_device_identifier(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(device_id): AxumPath<String>,
|
||||||
|
Json(req): Json<DeviceIdentifierRequest>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let input = DeviceIdentifierInput {
|
||||||
|
kind: req.kind,
|
||||||
|
value: req.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
match state
|
||||||
|
.store
|
||||||
|
.attach_device_identifier(&device_id, input)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
||||||
|
Ok(None) => Err(json_error(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"known_device_not_found",
|
||||||
|
"known device not found",
|
||||||
|
)),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to attach device identifier");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"attach_device_identifier_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn state_stats(
|
pub async fn state_stats(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
@@ -431,3 +583,25 @@ pub async fn state_stats(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn known_device_response(device: crate::state::KnownDevice) -> KnownDeviceResponse {
|
||||||
|
KnownDeviceResponse {
|
||||||
|
device_id: device.device_id,
|
||||||
|
display_name: device.display_name,
|
||||||
|
pinned: device.pinned,
|
||||||
|
created_at_unix: device.created_at_unix,
|
||||||
|
updated_at_unix: device.updated_at_unix,
|
||||||
|
notes: device.notes,
|
||||||
|
identifiers: device
|
||||||
|
.identifiers
|
||||||
|
.into_iter()
|
||||||
|
.map(|identifier| DeviceIdentifierResponse {
|
||||||
|
identifier_key: identifier.identifier_key,
|
||||||
|
device_id: identifier.device_id,
|
||||||
|
kind: identifier.kind,
|
||||||
|
value: identifier.value,
|
||||||
|
created_at_unix: identifier.created_at_unix,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ pub use audit::list_audit_events;
|
|||||||
pub use commands::{list_agents, run_command};
|
pub use commands::{list_agents, run_command};
|
||||||
pub use control::{
|
pub use control::{
|
||||||
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
|
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
|
||||||
StateStatsResponse, enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_agent,
|
StateStatsResponse, attach_device_identifier, create_known_device, enroll, forget_known_device,
|
||||||
|
healthz, issue_enroll_token, list_enroll_tokens, list_known_devices, revoke_agent,
|
||||||
revoke_enroll_token, set_agent_nickname, state_stats,
|
revoke_enroll_token, set_agent_nickname, state_stats,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ pub enum Command {
|
|||||||
RevokeAgent(RevokeAgentArgs),
|
RevokeAgent(RevokeAgentArgs),
|
||||||
/// Print state backend stats.
|
/// Print state backend stats.
|
||||||
StateStats(StateStatsArgs),
|
StateStats(StateStatsArgs),
|
||||||
|
/// Import a legacy sled state directory into a SQLite state file.
|
||||||
|
ImportSledState(ImportSledStateArgs),
|
||||||
/// Send SIGHUP to an already-running daemon.
|
/// Send SIGHUP to an already-running daemon.
|
||||||
Reload(ReloadArgs),
|
Reload(ReloadArgs),
|
||||||
}
|
}
|
||||||
@@ -236,6 +238,18 @@ pub struct StateStatsArgs {
|
|||||||
pub target: AdminTargetArgs,
|
pub target: AdminTargetArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct ImportSledStateArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
pub from_sled_state: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub to_state_file: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct ReloadArgs {
|
pub struct ReloadArgs {
|
||||||
#[arg(long, default_value = DEFAULT_PID_FILE)]
|
#[arg(long, default_value = DEFAULT_PID_FILE)]
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
let state_file_raw = args
|
let state_file_raw = args
|
||||||
.state_file
|
.state_file
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| PathBuf::from("state.db"));
|
.unwrap_or_else(|| PathBuf::from("state.sqlite3"));
|
||||||
let state_file = resolve_path(&data_dir, state_file_raw);
|
let state_file = resolve_path(&data_dir, state_file_raw);
|
||||||
|
|
||||||
let pid_file_raw = args
|
let pid_file_raw = args
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ impl DaemonConfig {
|
|||||||
.state_file
|
.state_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(file.state_file)
|
.or(file.state_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("state.db"));
|
.unwrap_or_else(|| PathBuf::from("state.sqlite3"));
|
||||||
let state_file = resolve_path(&data_dir, state_file_raw);
|
let state_file = resolve_path(&data_dir, state_file_raw);
|
||||||
|
|
||||||
let command_timeout = Duration::from_millis(
|
let command_timeout = Duration::from_millis(
|
||||||
@@ -255,7 +255,7 @@ fn resolve_state_access(
|
|||||||
&data_dir,
|
&data_dir,
|
||||||
cli_state_file
|
cli_state_file
|
||||||
.or(file.state_file)
|
.or(file.state_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("state.db")),
|
.unwrap_or_else(|| PathBuf::from("state.sqlite3")),
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolved_public_url = cli_public_url
|
let resolved_public_url = cli_public_url
|
||||||
@@ -288,8 +288,11 @@ mod tests {
|
|||||||
fn resolve_path_joins_relative_path() {
|
fn resolve_path_joins_relative_path() {
|
||||||
let out = resolve_path(
|
let out = resolve_path(
|
||||||
Path::new("/var/lib/wakey-control-plane"),
|
Path::new("/var/lib/wakey-control-plane"),
|
||||||
PathBuf::from("state.db"),
|
PathBuf::from("state.sqlite3"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
PathBuf::from("/var/lib/wakey-control-plane/state.sqlite3")
|
||||||
);
|
);
|
||||||
assert_eq!(out, PathBuf::from("/var/lib/wakey-control-plane/state.db"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,10 @@ async fn main() -> Result<()> {
|
|||||||
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
runtime::state_stats(args).await
|
runtime::state_stats(args).await
|
||||||
}
|
}
|
||||||
|
Command::ImportSledState(args) => {
|
||||||
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
|
runtime::import_sled_state(args).await
|
||||||
|
}
|
||||||
Command::Reload(args) => {
|
Command::Reload(args) => {
|
||||||
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
runtime::reload_daemon(&args.pid_file)
|
runtime::reload_daemon(&args.pid_file)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use anyhow::{Context, Result};
|
|||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeAgentArgs, RevokeEnrollTokenArgs,
|
ImportSledStateArgs, IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeAgentArgs,
|
||||||
StateStatsArgs,
|
RevokeEnrollTokenArgs, StateStatsArgs,
|
||||||
};
|
};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
@@ -276,3 +276,11 @@ pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
|||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn import_sled_state(args: ImportSledStateArgs) -> Result<()> {
|
||||||
|
state::Store::import_sled_state(&args.from_sled_state, &args.to_state_file, args.force).await?;
|
||||||
|
println!("from_sled_state={}", args.from_sled_state.display());
|
||||||
|
println!("to_state_file={}", args.to_state_file.display());
|
||||||
|
println!("imported=true");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -26,7 +26,9 @@ use crate::ws;
|
|||||||
mod admin;
|
mod admin;
|
||||||
mod process;
|
mod process;
|
||||||
pub use admin::revoke_agent;
|
pub use admin::revoke_agent;
|
||||||
pub use admin::{issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats};
|
pub use admin::{
|
||||||
|
import_sled_state, issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,
|
||||||
|
};
|
||||||
pub use process::reload_daemon;
|
pub use process::reload_daemon;
|
||||||
use process::{remove_pid_file, write_pid_file};
|
use process::{remove_pid_file, write_pid_file};
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ fn public_api_routes(ui_dist_dir: std::path::PathBuf) -> Router<AppState> {
|
|||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/ui", get(|| async { Redirect::temporary("/ui/") }))
|
.route("/ui", get(|| async { Redirect::temporary("/ui/") }))
|
||||||
|
.route("/", get(|| async { Redirect::temporary("/ui/") })) // same as caddyfile
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/ui/",
|
"/ui/",
|
||||||
get_service(ServeDir::new(ui_dist_dir).not_found_service(ServeFile::new(index_file))),
|
get_service(ServeDir::new(ui_dist_dir).not_found_service(ServeFile::new(index_file))),
|
||||||
@@ -86,6 +89,18 @@ fn control_api_routes() -> Router<AppState> {
|
|||||||
axum::routing::delete(api::revoke_enroll_token),
|
axum::routing::delete(api::revoke_enroll_token),
|
||||||
)
|
)
|
||||||
.route("/api/v1/control/state-stats", get(api::state_stats))
|
.route("/api/v1/control/state-stats", get(api::state_stats))
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/devices",
|
||||||
|
get(api::list_known_devices).post(api::create_known_device),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/devices/{device_id}",
|
||||||
|
axum::routing::delete(api::forget_known_device),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/devices/{device_id}/identifiers",
|
||||||
|
post(api::attach_device_identifier),
|
||||||
|
)
|
||||||
.route("/api/v1/control/audit/events", get(api::list_audit_events))
|
.route("/api/v1/control/audit/events", get(api::list_audit_events))
|
||||||
.route("/api/v1/control/alerts", get(api::active_alerts))
|
.route("/api/v1/control/alerts", get(api::active_alerts))
|
||||||
.route("/api/v1/control/alerts/history", get(api::alert_history))
|
.route("/api/v1/control/alerts/history", get(api::alert_history))
|
||||||
|
|||||||
@@ -2,4 +2,7 @@ mod store;
|
|||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use store::Store;
|
pub use store::Store;
|
||||||
pub use types::{AlertState, AuditEvent, AuditEventFilter, AuditEventInput};
|
pub use types::{
|
||||||
|
AlertState, AuditEvent, AuditEventFilter, AuditEventInput, DeviceIdentifierInput, KnownDevice,
|
||||||
|
KnownDeviceInput,
|
||||||
|
};
|
||||||
+1261
-372
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,40 @@ pub struct StateStats {
|
|||||||
pub expired_enroll_token_count: usize,
|
pub expired_enroll_token_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct KnownDevice {
|
||||||
|
pub device_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub pinned: bool,
|
||||||
|
pub created_at_unix: u64,
|
||||||
|
pub updated_at_unix: u64,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
pub identifiers: Vec<DeviceIdentifier>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceIdentifier {
|
||||||
|
pub identifier_key: String,
|
||||||
|
pub device_id: String,
|
||||||
|
pub kind: String,
|
||||||
|
pub value: String,
|
||||||
|
pub created_at_unix: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KnownDeviceInput {
|
||||||
|
pub display_name: String,
|
||||||
|
pub pinned: bool,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
pub identifiers: Vec<DeviceIdentifierInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DeviceIdentifierInput {
|
||||||
|
pub kind: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AuditEvent {
|
pub struct AuditEvent {
|
||||||
pub event_id: String,
|
pub event_id: String,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
@@ -32,11 +32,16 @@ impl From<NeighborState> for Presence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// MAC-first identifier for a device aggregate.
|
/// Observed identifier for a discovered device aggregate.
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize)]
|
///
|
||||||
pub struct DeviceId {
|
/// This is not a durable, user-approved identity. The control plane may attach
|
||||||
|
/// many observed identifiers to one saved device.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
||||||
|
pub enum DeviceId {
|
||||||
#[serde(with = "mac")]
|
#[serde(with = "mac")]
|
||||||
pub mac: MacAddr,
|
Mac(MacAddr),
|
||||||
|
Ip(IpAddr),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merged view of one discovered network identity.
|
/// Merged view of one discovered network identity.
|
||||||
@@ -91,10 +96,16 @@ impl Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let macs: Vec<MacAddr> = macs.into_iter().collect();
|
let macs: Vec<MacAddr> = macs.into_iter().collect();
|
||||||
|
let ips: Vec<IpAddr> = ips.into_iter().collect();
|
||||||
|
let id = macs
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
.map(DeviceId::Mac)
|
||||||
|
.or_else(|| ips.first().copied().map(DeviceId::Ip));
|
||||||
Self {
|
Self {
|
||||||
id: macs.first().copied().map(|mac| DeviceId { mac }),
|
id,
|
||||||
names: names.into_iter().map(|n| n.to_owned()).collect(),
|
names: names.into_iter().map(|n| n.to_owned()).collect(),
|
||||||
ips: ips.into_iter().collect(),
|
ips,
|
||||||
macs,
|
macs,
|
||||||
interfaces: interfaces.into_iter().map(|d| d.to_owned()).collect(),
|
interfaces: interfaces.into_iter().map(|d| d.to_owned()).collect(),
|
||||||
neighbors,
|
neighbors,
|
||||||
|
|||||||
Reference in new issue
Block a user