Compare commits
4
Commits
No files matched your search
+1
-1
@@ -117,7 +117,7 @@ _Avoid_: Endpoint state, device presence
|
|||||||
- API arrays should be domain-sorted by default; UI may sort top-level fleet rows for presentation, but route ranking and recommended route selection are backend-owned.
|
- API arrays should be domain-sorted by default; UI may sort top-level fleet rows for presentation, but route ranking and recommended route selection are backend-owned.
|
||||||
- The main fleet API embeds **Endpoints** in each **Device** response; a separate endpoint API is unnecessary until payload size or access patterns require it.
|
- The main fleet API embeds **Endpoints** in each **Device** response; a separate endpoint API is unnecessary until payload size or access patterns require it.
|
||||||
- The control plane stores current per-agent endpoint snapshots, not endpoint event history.
|
- The control plane stores current per-agent endpoint snapshots, not endpoint event history.
|
||||||
- Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement; default retention is 30 days unless configured otherwise.
|
- Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement; default retention is 7 days unless configured otherwise.
|
||||||
- Live-source **Endpoints** use snapshot time as their observed time; hook-derived **Endpoints** use the hook row's recorded last-seen time.
|
- Live-source **Endpoints** use snapshot time as their observed time; hook-derived **Endpoints** use the hook row's recorded last-seen time.
|
||||||
- **DeviceId**, **Endpoint Key**, and **Agent Endpoint Key** are typed concepts; string keys are boundary serialization only.
|
- **DeviceId**, **Endpoint Key**, and **Agent Endpoint Key** are typed concepts; string keys are boundary serialization only.
|
||||||
- **DeviceId** is derived from a device's **Endpoints**: canonical MAC if any endpoint has a MAC, otherwise canonical IP, otherwise absent.
|
- **DeviceId** is derived from a device's **Endpoints**: canonical MAC if any endpoint has a MAC, otherwise canonical IP, otherwise absent.
|
||||||
|
|||||||
Generated
+5
-5
@@ -3598,7 +3598,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3619,7 +3619,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey-agent"
|
name = "wakey-agent"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3643,7 +3643,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey-control-plane"
|
name = "wakey-control-plane"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3672,7 +3672,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey-core"
|
name = "wakey-core"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"macaddr",
|
"macaddr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3684,7 +3684,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wakey-linux"
|
name = "wakey-linux"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures",
|
"futures",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ Known-device identifiers are source-independent ownership claims over MAC or per
|
|||||||
|
|
||||||
The control plane stores current per-agent endpoint snapshots as structured columns, not opaque `Device` JSON and not endpoint event history. Summary MAC/IP collections are derived from endpoints; separate MAC/IP storage should be removed or represented as views rather than maintained as independent truth.
|
The control plane stores current per-agent endpoint snapshots as structured columns, not opaque `Device` JSON and not endpoint event history. Summary MAC/IP collections are derived from endpoints; separate MAC/IP storage should be removed or represented as views rather than maintained as independent truth.
|
||||||
|
|
||||||
Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement. The default retention is 30 days unless configured otherwise. Live-source endpoints use snapshot time as their observed time; hook-derived endpoints use the hook row's recorded last-seen time.
|
Agent-local hook memory is retained by local last-seen age policy, not by control-plane acknowledgement. The default retention is 7 days unless configured otherwise. Live-source endpoints use snapshot time as their observed time; hook-derived endpoints use the hook row's recorded last-seen time.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,20 @@ export type FleetDeviceAgent = {
|
|||||||
last_seen_unix: number;
|
last_seen_unix: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FleetDeviceEndpoint = {
|
||||||
|
agent_id: string;
|
||||||
|
nickname?: string | null;
|
||||||
|
connected: boolean;
|
||||||
|
source: string;
|
||||||
|
mac: string | null;
|
||||||
|
ip: string | null;
|
||||||
|
hostname: string | null;
|
||||||
|
interface: string | null;
|
||||||
|
presence: string;
|
||||||
|
first_seen_unix: number | null;
|
||||||
|
last_seen_unix: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type FleetWakeRoute = {
|
export type FleetWakeRoute = {
|
||||||
route_id: string;
|
route_id: string;
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
@@ -111,7 +125,9 @@ export type FleetWakeRoute = {
|
|||||||
mac: string | null;
|
mac: string | null;
|
||||||
ip: string | null;
|
ip: string | null;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
|
interface: string | null;
|
||||||
source: string;
|
source: string;
|
||||||
|
presence: string;
|
||||||
last_seen_unix: number;
|
last_seen_unix: number;
|
||||||
wakeable: boolean;
|
wakeable: boolean;
|
||||||
};
|
};
|
||||||
@@ -126,6 +142,7 @@ export type FleetDevice = {
|
|||||||
hostnames: string[];
|
hostnames: string[];
|
||||||
agents: FleetDeviceAgent[];
|
agents: FleetDeviceAgent[];
|
||||||
sources: string[];
|
sources: string[];
|
||||||
|
endpoints: FleetDeviceEndpoint[];
|
||||||
first_seen_unix: number | null;
|
first_seen_unix: number | null;
|
||||||
last_seen_unix: number | null;
|
last_seen_unix: number | null;
|
||||||
presence: string;
|
presence: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Minus, Plus, Zap } from "lucide-react";
|
import { Copy, Minus, Plus, Zap } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
detachDeviceIdentifier,
|
detachDeviceIdentifier,
|
||||||
mergeKnownDevice,
|
mergeKnownDevice,
|
||||||
type FleetDevice,
|
type FleetDevice,
|
||||||
|
type FleetDeviceEndpoint,
|
||||||
type KnownDevice,
|
type KnownDevice,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -29,7 +30,12 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { DetailBlock, PresenceBadge } from "@/pages/fleet/FleetComponents";
|
import { DetailBlock, PresenceBadge } from "@/pages/fleet/FleetComponents";
|
||||||
import { agentLabel, identifiersFor, routeLabel } from "@/pages/fleet/utils";
|
import {
|
||||||
|
agentLabel,
|
||||||
|
formatSeen,
|
||||||
|
identifiersFor,
|
||||||
|
routeLabel,
|
||||||
|
} from "@/pages/fleet/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
device: FleetDevice | null;
|
device: FleetDevice | null;
|
||||||
@@ -225,6 +231,30 @@ export function FleetDeviceDetailsDialog({
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h3 className="text-sm font-medium">Endpoints</h3>
|
||||||
|
<Badge variant="outline">{device.endpoints.length}</Badge>
|
||||||
|
</div>
|
||||||
|
{device.endpoints.length ? (
|
||||||
|
<div className="divide-y overflow-hidden rounded-md border">
|
||||||
|
{device.endpoints.map((endpoint, index) => (
|
||||||
|
<EndpointRow
|
||||||
|
key={`${endpoint.agent_id}:${endpoint.source}:${endpoint.mac ?? ""}:${endpoint.ip ?? ""}:${index}`}
|
||||||
|
endpoint={endpoint}
|
||||||
|
onCopy={onCopy}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground">
|
||||||
|
No endpoint evidence reported.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
{/* ── Wake route ── */}
|
{/* ── Wake route ── */}
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@@ -513,3 +543,52 @@ export function FleetDeviceDetailsDialog({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EndpointRow({
|
||||||
|
endpoint,
|
||||||
|
onCopy,
|
||||||
|
}: {
|
||||||
|
endpoint: FleetDeviceEndpoint;
|
||||||
|
onCopy: (label: string, value: string) => void;
|
||||||
|
}) {
|
||||||
|
const target = [endpoint.mac, endpoint.ip].filter(Boolean).join(" / ");
|
||||||
|
const location = [endpoint.interface, endpoint.hostname]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" / ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2 p-3 text-sm sm:grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)_auto] sm:items-center">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
|
||||||
|
<PresenceBadge presence={endpoint.presence} />
|
||||||
|
<Badge variant="secondary">{endpoint.source.replace("_", " ")}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||||
|
{agentLabel(endpoint.agent_id, endpoint.nickname)}
|
||||||
|
{endpoint.connected ? " (connected)" : " (offline)"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate font-mono text-xs">{target || "-"}</p>
|
||||||
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
|
{location || "No interface or hostname"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2 sm:justify-end">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatSeen(endpoint.last_seen_unix)}
|
||||||
|
</span>
|
||||||
|
{target && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => onCopy("Endpoint", target)}
|
||||||
|
aria-label={`Copy endpoint ${target}`}
|
||||||
|
>
|
||||||
|
<Copy className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ export const presenceFilters = [
|
|||||||
"all",
|
"all",
|
||||||
"online",
|
"online",
|
||||||
"likely_online",
|
"likely_online",
|
||||||
"unknown",
|
|
||||||
"offline",
|
"offline",
|
||||||
|
"unknown",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const knownFilters: KnownFilter[] = ["all", "known", "unknown"];
|
export const knownFilters: KnownFilter[] = ["all", "known", "unknown"];
|
||||||
@@ -40,8 +40,8 @@ export function formatSeen(value: number | null): string {
|
|||||||
export function presenceRank(presence: string): number {
|
export function presenceRank(presence: string): number {
|
||||||
if (presence === "online") return 4;
|
if (presence === "online") return 4;
|
||||||
if (presence === "likely_online") return 3;
|
if (presence === "likely_online") return 3;
|
||||||
if (presence === "unknown") return 2;
|
if (presence === "offline") return 2;
|
||||||
if (presence === "offline") return 1;
|
if (presence === "unknown") return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export function agentLabel(agentId: string, nickname?: string | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function routeLabel(route: FleetWakeRoute): string {
|
export function routeLabel(route: FleetWakeRoute): string {
|
||||||
const status = route.connected ? "connected" : "offline";
|
const status = route.connected ? "connected" : "agent offline";
|
||||||
const target = route.mac
|
const target = route.mac
|
||||||
? `${route.mac}${route.ip ? ` / ${route.ip}` : ""}`
|
? `${route.mac}${route.ip ? ` / ${route.ip}` : ""}`
|
||||||
: (route.ip ?? "-");
|
: (route.ip ?? "-");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey-agent"
|
name = "wakey-agent"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use anyhow::{Context, Result};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
|
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
|
||||||
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-agent.pid";
|
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-agent.pid";
|
||||||
@@ -11,7 +12,7 @@ const WAKEY_OBSERVATION_STORE_ENV: &str = "WAKEY_OBSERVATION_STORE";
|
|||||||
const DEFAULT_DHCP_LEASES_PATH: &str = "/tmp/dhcp.leases";
|
const DEFAULT_DHCP_LEASES_PATH: &str = "/tmp/dhcp.leases";
|
||||||
const DEFAULT_MAC_NAME_CACHE_PATH: &str = "/tmp/wakey_mac_names.json";
|
const DEFAULT_MAC_NAME_CACHE_PATH: &str = "/tmp/wakey_mac_names.json";
|
||||||
const DEFAULT_OBSERVATION_STORE_PATH: &str = "/tmp/wakey_observations.json";
|
const DEFAULT_OBSERVATION_STORE_PATH: &str = "/tmp/wakey_observations.json";
|
||||||
pub const DEFAULT_OBSERVATION_RETENTION_DAYS: u64 = 30;
|
pub const DEFAULT_OBSERVATION_RETENTION_DAYS: u64 = 7;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct AgentConfig {
|
pub struct AgentConfig {
|
||||||
@@ -36,6 +37,20 @@ pub struct AgentConfig {
|
|||||||
pub observation_store_path: PathBuf,
|
pub observation_store_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static DEFAULT_CONFIG: LazyLock<AgentConfig> = LazyLock::new(|| AgentConfig {
|
||||||
|
server_url: "https://wakey.ldlda.com".to_string(),
|
||||||
|
agent_id: "REPLACE_ME_AGENT_ID".to_string(),
|
||||||
|
agent_token: "REPLACE_ME_AGENT_TOKEN".to_string(),
|
||||||
|
reconnect_base_ms: default_reconnect_base_ms(),
|
||||||
|
reconnect_max_ms: default_reconnect_max_ms(),
|
||||||
|
observation_sync_interval_seconds: default_observation_sync_interval_seconds(),
|
||||||
|
observation_retention_days: default_observation_retention_days(),
|
||||||
|
pid_file: default_pid_file(),
|
||||||
|
dhcp_leases_path: default_dhcp_leases_path(),
|
||||||
|
mac_name_cache_path: default_mac_name_cache_path(),
|
||||||
|
observation_store_path: default_observation_store_path(),
|
||||||
|
});
|
||||||
|
|
||||||
impl fmt::Debug for AgentConfig {
|
impl fmt::Debug for AgentConfig {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("AgentConfig")
|
f.debug_struct("AgentConfig")
|
||||||
@@ -214,4 +229,21 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_without_retention_uses_current_default() {
|
||||||
|
let config: AgentConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
server_url = "https://example.com"
|
||||||
|
agent_id = "agent-1"
|
||||||
|
agent_token = "secret"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("config should parse");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.observation_retention_days,
|
||||||
|
DEFAULT_OBSERVATION_RETENTION_DAYS
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::config::{AgentConfig, save_config_with_backup};
|
use crate::config::{AgentConfig, DEFAULT_CONFIG, save_config_with_backup};
|
||||||
|
|
||||||
pub struct EnrollOutcome {
|
pub struct EnrollOutcome {
|
||||||
pub config: AgentConfig,
|
pub config: AgentConfig,
|
||||||
@@ -55,35 +55,12 @@ pub async fn enroll(
|
|||||||
.await
|
.await
|
||||||
.context("failed to decode enrollment response")?;
|
.context("failed to decode enrollment response")?;
|
||||||
|
|
||||||
let config = AgentConfig {
|
let mut config = base_config
|
||||||
server_url: payload.server_url.unwrap_or(server_url),
|
.cloned()
|
||||||
agent_id: payload.agent_id,
|
.unwrap_or_else(|| (*DEFAULT_CONFIG).clone());
|
||||||
agent_token: payload.agent_token,
|
config.server_url = payload.server_url.unwrap_or(server_url);
|
||||||
reconnect_base_ms: base_config
|
config.agent_id = payload.agent_id;
|
||||||
.map(|config| config.reconnect_base_ms)
|
config.agent_token = payload.agent_token;
|
||||||
.unwrap_or(1_000),
|
|
||||||
reconnect_max_ms: base_config
|
|
||||||
.map(|config| config.reconnect_max_ms)
|
|
||||||
.unwrap_or(30_000),
|
|
||||||
observation_sync_interval_seconds: base_config
|
|
||||||
.map(|config| config.observation_sync_interval_seconds)
|
|
||||||
.unwrap_or(60),
|
|
||||||
observation_retention_days: base_config
|
|
||||||
.map(|config| config.observation_retention_days)
|
|
||||||
.unwrap_or(crate::config::DEFAULT_OBSERVATION_RETENTION_DAYS),
|
|
||||||
pid_file: base_config
|
|
||||||
.map(|config| config.pid_file.clone())
|
|
||||||
.unwrap_or_else(|| crate::config::DEFAULT_PID_FILE.into()),
|
|
||||||
dhcp_leases_path: base_config
|
|
||||||
.map(|config| config.dhcp_leases_path.clone())
|
|
||||||
.unwrap_or_else(|| "/tmp/dhcp.leases".into()),
|
|
||||||
mac_name_cache_path: base_config
|
|
||||||
.map(|config| config.mac_name_cache_path.clone())
|
|
||||||
.unwrap_or_else(|| "/tmp/wakey_mac_names.json".into()),
|
|
||||||
observation_store_path: base_config
|
|
||||||
.map(|config| config.observation_store_path.clone())
|
|
||||||
.unwrap_or_else(|| "/tmp/wakey_observations.json".into()),
|
|
||||||
};
|
|
||||||
let backup_path = save_config_with_backup(config_path, &config)?;
|
let backup_path = save_config_with_backup(config_path, &config)?;
|
||||||
info!(agent_id = %config.agent_id, config_path = %config_path.display(), "agent enrollment succeeded and config was written");
|
info!(agent_id = %config.agent_id, config_path = %config_path.display(), "agent enrollment succeeded and config was written");
|
||||||
Ok(EnrollOutcome {
|
Ok(EnrollOutcome {
|
||||||
|
|||||||
+1
-13
@@ -248,19 +248,7 @@ fn init_config(args: InitConfigArgs) -> Result<()> {
|
|||||||
let mut cfg = if let Some(from_config) = &args.from_config {
|
let mut cfg = if let Some(from_config) = &args.from_config {
|
||||||
config::load_config(from_config)?
|
config::load_config(from_config)?
|
||||||
} else {
|
} else {
|
||||||
config::AgentConfig {
|
(*config::DEFAULT_CONFIG).clone()
|
||||||
server_url: "https://wakey.ldlda.com".to_string(),
|
|
||||||
agent_id: "REPLACE_ME_AGENT_ID".to_string(),
|
|
||||||
agent_token: "REPLACE_ME_AGENT_TOKEN".to_string(),
|
|
||||||
reconnect_base_ms: 1_000,
|
|
||||||
reconnect_max_ms: 30_000,
|
|
||||||
observation_sync_interval_seconds: 60,
|
|
||||||
observation_retention_days: config::DEFAULT_OBSERVATION_RETENTION_DAYS,
|
|
||||||
pid_file: config::DEFAULT_PID_FILE.into(),
|
|
||||||
dhcp_leases_path: "/tmp/dhcp.leases".into(),
|
|
||||||
mac_name_cache_path: "/tmp/wakey_mac_names.json".into(),
|
|
||||||
observation_store_path: "/tmp/wakey_observations.json".into(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(server_url) = args.server_url {
|
if let Some(server_url) = args.server_url {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey-control-plane"
|
name = "wakey-control-plane"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
|||||||
@@ -59,29 +59,6 @@ CREATE TABLE agent_device_ips (
|
|||||||
CREATE INDEX agent_device_ips_ip_idx
|
CREATE INDEX agent_device_ips_ip_idx
|
||||||
ON agent_device_ips(ip);
|
ON agent_device_ips(ip);
|
||||||
|
|
||||||
CREATE TABLE agent_device_endpoints (
|
|
||||||
agent_id TEXT NOT NULL,
|
|
||||||
device_key TEXT NOT NULL,
|
|
||||||
endpoint_key TEXT NOT NULL,
|
|
||||||
source TEXT NOT NULL,
|
|
||||||
mac TEXT,
|
|
||||||
ip TEXT,
|
|
||||||
hostname TEXT,
|
|
||||||
interface TEXT,
|
|
||||||
presence TEXT NOT NULL,
|
|
||||||
first_seen_unix INTEGER NOT NULL,
|
|
||||||
last_seen_unix INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (agent_id, device_key, endpoint_key),
|
|
||||||
FOREIGN KEY (agent_id, device_key)
|
|
||||||
REFERENCES agent_devices(agent_id, device_key) ON DELETE CASCADE
|
|
||||||
) WITHOUT ROWID;
|
|
||||||
|
|
||||||
CREATE INDEX agent_device_endpoints_mac_idx
|
|
||||||
ON agent_device_endpoints(mac);
|
|
||||||
|
|
||||||
CREATE INDEX agent_device_endpoints_ip_idx
|
|
||||||
ON agent_device_endpoints(ip);
|
|
||||||
|
|
||||||
CREATE TABLE agent_device_hostnames (
|
CREATE TABLE agent_device_hostnames (
|
||||||
agent_id TEXT NOT NULL,
|
agent_id TEXT NOT NULL,
|
||||||
device_key TEXT NOT NULL,
|
device_key TEXT NOT NULL,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE TABLE agent_device_endpoints (
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
device_key TEXT NOT NULL,
|
||||||
|
endpoint_key TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
mac TEXT,
|
||||||
|
ip TEXT,
|
||||||
|
hostname TEXT,
|
||||||
|
interface TEXT,
|
||||||
|
presence TEXT NOT NULL,
|
||||||
|
first_seen_unix INTEGER NOT NULL,
|
||||||
|
last_seen_unix INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (agent_id, device_key, endpoint_key),
|
||||||
|
FOREIGN KEY (agent_id, device_key)
|
||||||
|
REFERENCES agent_devices(agent_id, device_key) ON DELETE CASCADE
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
|
CREATE INDEX agent_device_endpoints_mac_idx
|
||||||
|
ON agent_device_endpoints(mac);
|
||||||
|
|
||||||
|
CREATE INDEX agent_device_endpoints_ip_idx
|
||||||
|
ON agent_device_endpoints(ip);
|
||||||
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use wakey_core::Presence;
|
use wakey_core::{EndpointSource, Presence};
|
||||||
|
|
||||||
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
||||||
|
|
||||||
@@ -203,6 +203,7 @@ fn add_agent_device_to_entry(
|
|||||||
entry.pinned = summary.pinned;
|
entry.pinned = summary.pinned;
|
||||||
entry.known_device = Some(summary);
|
entry.known_device = Some(summary);
|
||||||
}
|
}
|
||||||
|
if agent_device.endpoints.is_empty() {
|
||||||
for mac in &agent_device.macs {
|
for mac in &agent_device.macs {
|
||||||
entry.macs.insert(*mac);
|
entry.macs.insert(*mac);
|
||||||
}
|
}
|
||||||
@@ -211,6 +212,18 @@ fn add_agent_device_to_entry(
|
|||||||
entry.ips.insert(*ip);
|
entry.ips.insert(*ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for endpoint in &agent_device.endpoints {
|
||||||
|
if let Some(mac) = endpoint.key.mac {
|
||||||
|
entry.macs.insert(mac);
|
||||||
|
}
|
||||||
|
if endpoint_ip_is_summary_eligible(endpoint.key.source, endpoint.presence)
|
||||||
|
&& let Some(ip) = endpoint.key.ip
|
||||||
|
{
|
||||||
|
entry.ips.insert(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for hostname in &agent_device.hostnames {
|
for hostname in &agent_device.hostnames {
|
||||||
if entry.display_name.is_none() {
|
if entry.display_name.is_none() {
|
||||||
entry.display_name = Some(hostname.clone());
|
entry.display_name = Some(hostname.clone());
|
||||||
@@ -285,11 +298,10 @@ fn add_agent_device_to_entry(
|
|||||||
&agent_id,
|
&agent_id,
|
||||||
endpoint.key.mac.as_ref(),
|
endpoint.key.mac.as_ref(),
|
||||||
endpoint.key.ip.as_ref(),
|
endpoint.key.ip.as_ref(),
|
||||||
&source,
|
|
||||||
);
|
);
|
||||||
let wakeable = status.connected && endpoint.key.mac.is_some();
|
let wakeable = status.connected && endpoint.key.mac.is_some();
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -311,10 +323,10 @@ fn add_agent_device_to_entry(
|
|||||||
for mac in &agent_device.macs {
|
for mac in &agent_device.macs {
|
||||||
let ip_for_mac = agent_device.ips.first().copied();
|
let ip_for_mac = agent_device.ips.first().copied();
|
||||||
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
||||||
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device");
|
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref());
|
||||||
let wakeable = status.connected;
|
let wakeable = status.connected;
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -338,9 +350,9 @@ fn add_agent_device_to_entry(
|
|||||||
&& let Some(ip) = agent_device.ips.first()
|
&& let Some(ip) = agent_device.ips.first()
|
||||||
{
|
{
|
||||||
let hostname = agent_device.hostnames.first().cloned();
|
let hostname = agent_device.hostnames.first().cloned();
|
||||||
let rid = route_id(&agent_id, None, Some(ip), "device");
|
let rid = route_id(&agent_id, None, Some(ip));
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -380,19 +392,66 @@ fn device_known_device(
|
|||||||
context: &FleetBuildContext,
|
context: &FleetBuildContext,
|
||||||
) -> Option<KnownDeviceSummary> {
|
) -> Option<KnownDeviceSummary> {
|
||||||
agent_device
|
agent_device
|
||||||
.macs
|
.endpoints
|
||||||
.first()
|
.iter()
|
||||||
|
.filter_map(|endpoint| endpoint.key.mac)
|
||||||
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
||||||
.and_then(|key| context.identifier_map.get(&key).cloned())
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
.or_else(|| {
|
||||||
|
agent_device
|
||||||
|
.macs
|
||||||
|
.iter()
|
||||||
|
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
||||||
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
agent_device
|
||||||
|
.endpoints
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| endpoint.key.ip)
|
||||||
|
.map(|ip| format!("ip:{ip}"))
|
||||||
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
agent_device
|
agent_device
|
||||||
.ips
|
.ips
|
||||||
.first()
|
.iter()
|
||||||
.map(|ip| format!("ip:{ip}"))
|
.map(|ip| format!("ip:{ip}"))
|
||||||
.and_then(|key| context.identifier_map.get(&key).cloned())
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint_ip_is_summary_eligible(source: EndpointSource, presence: Presence) -> bool {
|
||||||
|
match source {
|
||||||
|
EndpointSource::DhcpLease => true,
|
||||||
|
EndpointSource::Neighbor => presence != Presence::Offline,
|
||||||
|
EndpointSource::HookNeighbor | EndpointSource::HookDhcp => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_route(entry: &mut FleetAccumulator, candidate: FleetWakeRoute) {
|
||||||
|
match entry.routes.entry(candidate.route_id.clone()) {
|
||||||
|
std::collections::btree_map::Entry::Vacant(slot) => {
|
||||||
|
slot.insert(candidate);
|
||||||
|
}
|
||||||
|
std::collections::btree_map::Entry::Occupied(mut slot) => {
|
||||||
|
if compare_route_quality(&candidate, slot.get()).is_gt() {
|
||||||
|
slot.insert(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_route_quality(a: &FleetWakeRoute, b: &FleetWakeRoute) -> std::cmp::Ordering {
|
||||||
|
a.connected
|
||||||
|
.cmp(&b.connected)
|
||||||
|
.then_with(|| a.wakeable.cmp(&b.wakeable))
|
||||||
|
.then_with(|| a.ip.is_some().cmp(&b.ip.is_some()))
|
||||||
|
.then_with(|| a.presence.cmp(&b.presence))
|
||||||
|
.then_with(|| a.last_seen_unix.cmp(&b.last_seen_unix))
|
||||||
|
.then_with(|| source_quality_rank(&a.source).cmp(&source_quality_rank(&b.source)))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
||||||
KnownDeviceSummary {
|
KnownDeviceSummary {
|
||||||
device_id: device.device_id.clone(),
|
device_id: device.device_id.clone(),
|
||||||
@@ -404,16 +463,8 @@ pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
|||||||
impl FleetAccumulator {
|
impl FleetAccumulator {
|
||||||
fn into_response(self) -> FleetDevice {
|
fn into_response(self) -> FleetDevice {
|
||||||
let mut route_candidates = self.routes.into_values().collect::<Vec<_>>();
|
let mut route_candidates = self.routes.into_values().collect::<Vec<_>>();
|
||||||
route_candidates.sort_by(|a, b| {
|
route_candidates
|
||||||
b.connected
|
.sort_by(|a, b| compare_route_quality(b, a).then_with(|| a.agent_id.cmp(&b.agent_id)));
|
||||||
.cmp(&a.connected)
|
|
||||||
.then_with(|| b.wakeable.cmp(&a.wakeable))
|
|
||||||
.then_with(|| b.ip.is_some().cmp(&a.ip.is_some()))
|
|
||||||
.then_with(|| b.presence.cmp(&a.presence))
|
|
||||||
.then_with(|| b.last_seen_unix.cmp(&a.last_seen_unix))
|
|
||||||
.then_with(|| source_quality_rank(&b.source).cmp(&source_quality_rank(&a.source)))
|
|
||||||
.then_with(|| a.agent_id.cmp(&b.agent_id))
|
|
||||||
});
|
|
||||||
let recommended_route = route_candidates
|
let recommended_route = route_candidates
|
||||||
.iter()
|
.iter()
|
||||||
.find(|route| route.wakeable)
|
.find(|route| route.wakeable)
|
||||||
@@ -445,12 +496,12 @@ impl FleetAccumulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn endpoint_source_label(source: wakey_core::EndpointSource) -> &'static str {
|
fn endpoint_source_label(source: EndpointSource) -> &'static str {
|
||||||
match source {
|
match source {
|
||||||
wakey_core::EndpointSource::Neighbor => "neighbor",
|
EndpointSource::Neighbor => "neighbor",
|
||||||
wakey_core::EndpointSource::DhcpLease => "dhcp_lease",
|
EndpointSource::DhcpLease => "dhcp_lease",
|
||||||
wakey_core::EndpointSource::HookNeighbor => "hook_neighbor",
|
EndpointSource::HookNeighbor => "hook_neighbor",
|
||||||
wakey_core::EndpointSource::HookDhcp => "hook_dhcp",
|
EndpointSource::HookDhcp => "hook_dhcp",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,11 +515,10 @@ fn source_quality_rank(source: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>, source: &str) -> String {
|
fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}|{}|{}|{}",
|
"{}|{}|{}",
|
||||||
agent_id,
|
agent_id,
|
||||||
source,
|
|
||||||
mac.map(|m| m.to_string()).unwrap_or_default(),
|
mac.map(|m| m.to_string()).unwrap_or_default(),
|
||||||
ip.map(|i| i.to_string()).unwrap_or_default()
|
ip.map(|i| i.to_string()).unwrap_or_default()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -117,6 +117,16 @@ fn endpoint(
|
|||||||
mac: Option<&str>,
|
mac: Option<&str>,
|
||||||
ip: Option<&str>,
|
ip: Option<&str>,
|
||||||
last_seen_unix: u64,
|
last_seen_unix: u64,
|
||||||
|
) -> DeviceEndpoint {
|
||||||
|
endpoint_with_presence(source, mac, ip, Presence::LikelyOnline, last_seen_unix)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_with_presence(
|
||||||
|
source: EndpointSource,
|
||||||
|
mac: Option<&str>,
|
||||||
|
ip: Option<&str>,
|
||||||
|
presence: Presence,
|
||||||
|
last_seen_unix: u64,
|
||||||
) -> DeviceEndpoint {
|
) -> DeviceEndpoint {
|
||||||
DeviceEndpoint {
|
DeviceEndpoint {
|
||||||
key: EndpointKey::new(
|
key: EndpointKey::new(
|
||||||
@@ -127,7 +137,7 @@ fn endpoint(
|
|||||||
.expect("endpoint key"),
|
.expect("endpoint key"),
|
||||||
hostname: Some("lda".into()),
|
hostname: Some("lda".into()),
|
||||||
interface: Some("br-lan".into()),
|
interface: Some("br-lan".into()),
|
||||||
presence: Presence::LikelyOnline,
|
presence,
|
||||||
first_seen_unix: Some(1),
|
first_seen_unix: Some(1),
|
||||||
last_seen_unix: Some(last_seen_unix),
|
last_seen_unix: Some(last_seen_unix),
|
||||||
}
|
}
|
||||||
@@ -203,6 +213,112 @@ fn fleet_routes_use_endpoint_mac_ip_pairs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fleet_summary_keeps_current_dhcp_ip_when_neighbor_endpoint_is_offline() {
|
||||||
|
let mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
let mut row = offline_agent_device(
|
||||||
|
"agent-a",
|
||||||
|
"mac:aa:bb:cc:dd:ee:ff",
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
row.ips = vec![
|
||||||
|
"192.168.1.10".parse().unwrap(),
|
||||||
|
"192.168.1.99".parse().unwrap(),
|
||||||
|
];
|
||||||
|
row.endpoints = vec![
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::Neighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
Presence::Offline,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::DhcpLease,
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.10"),
|
||||||
|
Presence::Unknown,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
devices[0].ips,
|
||||||
|
vec!["192.168.1.10".parse::<IpAddr>().unwrap()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_ip_identifier_matches_endpoint_that_is_not_in_summary_ips() {
|
||||||
|
let known = KnownDevice {
|
||||||
|
device_id: "dev-1".into(),
|
||||||
|
display_name: "lda remembered".into(),
|
||||||
|
pinned: true,
|
||||||
|
created_at_unix: 1,
|
||||||
|
updated_at_unix: 1,
|
||||||
|
notes: None,
|
||||||
|
identifiers: vec![DeviceIdentifier {
|
||||||
|
identifier_key: "ip:192.168.1.99".into(),
|
||||||
|
device_id: "dev-1".into(),
|
||||||
|
kind: "ip".into(),
|
||||||
|
value: "192.168.1.99".into(),
|
||||||
|
created_at_unix: 1,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let mut ctx = context(&["agent-a"]);
|
||||||
|
ctx.identifier_map
|
||||||
|
.insert("ip:192.168.1.99".into(), known_device_summary(&known));
|
||||||
|
let mut row = agent_device("agent-a", "ip:192.168.1.99", None, None, 20);
|
||||||
|
row.endpoints = vec![endpoint_with_presence(
|
||||||
|
EndpointSource::HookNeighbor,
|
||||||
|
None,
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
Presence::Offline,
|
||||||
|
20,
|
||||||
|
)];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(vec![known], vec![row], &ctx);
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
assert_eq!(devices[0].device_key, "known:dev-1");
|
||||||
|
assert_eq!(devices[0].endpoints.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equivalent_endpoint_routes_collapse_to_best_source() {
|
||||||
|
let mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
let ip = "192.168.1.2";
|
||||||
|
let mut row = agent_device("agent-a", "mac:aa:bb:cc:dd:ee:ff", Some(mac), Some(ip), 20);
|
||||||
|
row.endpoints = vec![
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::HookNeighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some(ip),
|
||||||
|
Presence::LikelyOnline,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::Neighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some(ip),
|
||||||
|
Presence::Online,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
|
||||||
|
|
||||||
|
assert_eq!(devices[0].endpoints.len(), 2);
|
||||||
|
assert_eq!(devices[0].route_candidates.len(), 1);
|
||||||
|
assert_eq!(devices[0].route_candidates[0].source, "neighbor");
|
||||||
|
assert_eq!(devices[0].route_candidates[0].presence, Presence::Online);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn known_device_with_two_macs_absorbs_both_device_groups() {
|
fn known_device_with_two_macs_absorbs_both_device_groups() {
|
||||||
let known = KnownDevice {
|
let known = KnownDevice {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
|
|||||||
|
|
||||||
use crate::cli::{InitConfigArgs, ServeArgs};
|
use crate::cli::{InitConfigArgs, ServeArgs};
|
||||||
use crate::config::resolve::{load_file_config, normalize_public_url, resolve_path};
|
use crate::config::resolve::{load_file_config, normalize_public_url, resolve_path};
|
||||||
use crate::config::types::{WritableConfig, WritableTelemetry};
|
use crate::config::types::{DEFAULT_CONFIG, WritableConfig, WritableTelemetry};
|
||||||
|
|
||||||
pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||||
if args.stdout && args.config_file.is_some() {
|
if args.stdout && args.config_file.is_some() {
|
||||||
@@ -30,6 +30,7 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
Default::default()
|
Default::default()
|
||||||
};
|
};
|
||||||
let base_telemetry = base.telemetry.unwrap_or_default();
|
let base_telemetry = base.telemetry.unwrap_or_default();
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
|
|
||||||
let bind = match args.bind {
|
let bind = match args.bind {
|
||||||
Some(bind) => bind,
|
Some(bind) => bind,
|
||||||
@@ -43,7 +44,8 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
.unwrap_or_else(|| "<defaults>".to_string())
|
.unwrap_or_else(|| "<defaults>".to_string())
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
None => "0.0.0.0:8080"
|
None => defaults
|
||||||
|
.bind
|
||||||
.parse()
|
.parse()
|
||||||
.expect("static default bind should parse"),
|
.expect("static default bind should parse"),
|
||||||
},
|
},
|
||||||
@@ -52,37 +54,38 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
args.public_url
|
args.public_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or(base.public_url.as_deref())
|
.or(base.public_url.as_deref())
|
||||||
.unwrap_or("http://127.0.0.1:8080"),
|
.unwrap_or(&defaults.public_url),
|
||||||
);
|
);
|
||||||
|
|
||||||
let data_dir = args
|
let data_dir = args
|
||||||
.data_dir
|
.data_dir
|
||||||
.clone()
|
.clone()
|
||||||
.or(base.data_dir)
|
.or(base.data_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_DATA_DIR));
|
.unwrap_or_else(|| defaults.data_dir.clone());
|
||||||
|
|
||||||
let state_file_raw = args
|
let state_file_raw = args
|
||||||
.state_file
|
.state_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(base.state_file)
|
.or(base.state_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("state.sqlite3"));
|
.unwrap_or_else(|| defaults.state_file.clone());
|
||||||
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
|
||||||
.pid_file
|
.pid_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(base.pid_file)
|
.or(base.pid_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("wakey-control-plane.pid"));
|
.unwrap_or_else(|| defaults.pid_file.clone());
|
||||||
let pid_file = resolve_path(&data_dir, pid_file_raw);
|
let pid_file = resolve_path(&data_dir, pid_file_raw);
|
||||||
|
|
||||||
let ui_dist_dir = args
|
let ui_dist_dir = args
|
||||||
.ui_dist_dir
|
.ui_dist_dir
|
||||||
.clone()
|
.clone()
|
||||||
.or(base.ui_dist_dir)
|
.or(base.ui_dist_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from("ui/dist"));
|
.unwrap_or_else(|| defaults.ui_dist_dir.clone());
|
||||||
|
|
||||||
let bootstrap_enroll_tokens = if args.bootstrap_enroll_tokens.is_empty() {
|
let bootstrap_enroll_tokens = if args.bootstrap_enroll_tokens.is_empty() {
|
||||||
base.bootstrap_enroll_tokens.unwrap_or_default()
|
base.bootstrap_enroll_tokens
|
||||||
|
.unwrap_or_else(|| defaults.bootstrap_enroll_tokens.clone())
|
||||||
} else {
|
} else {
|
||||||
args.bootstrap_enroll_tokens.clone()
|
args.bootstrap_enroll_tokens.clone()
|
||||||
};
|
};
|
||||||
@@ -95,17 +98,17 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
command_timeout_ms: args
|
command_timeout_ms: args
|
||||||
.command_timeout_ms
|
.command_timeout_ms
|
||||||
.or(base.command_timeout_ms)
|
.or(base.command_timeout_ms)
|
||||||
.unwrap_or(30_000)
|
.unwrap_or(defaults.command_timeout_ms)
|
||||||
.max(1),
|
.max(1),
|
||||||
enroll_token_ttl_seconds: args
|
enroll_token_ttl_seconds: args
|
||||||
.enroll_token_ttl_seconds
|
.enroll_token_ttl_seconds
|
||||||
.or(base.enroll_token_ttl_seconds)
|
.or(base.enroll_token_ttl_seconds)
|
||||||
.unwrap_or(86_400)
|
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||||
.max(1),
|
.max(1),
|
||||||
observation_retention_seconds: args
|
observation_retention_seconds: args
|
||||||
.observation_retention_seconds
|
.observation_retention_seconds
|
||||||
.or(base.observation_retention_seconds)
|
.or(base.observation_retention_seconds)
|
||||||
.unwrap_or(2_592_000),
|
.unwrap_or(defaults.observation_retention_seconds),
|
||||||
pid_file,
|
pid_file,
|
||||||
ui_dist_dir,
|
ui_dist_dir,
|
||||||
bootstrap_enroll_tokens,
|
bootstrap_enroll_tokens,
|
||||||
@@ -113,16 +116,17 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
|||||||
otlp_endpoint: args
|
otlp_endpoint: args
|
||||||
.telemetry_otlp_endpoint
|
.telemetry_otlp_endpoint
|
||||||
.clone()
|
.clone()
|
||||||
.or(base_telemetry.otlp_endpoint),
|
.or(base_telemetry.otlp_endpoint)
|
||||||
|
.or_else(|| defaults.telemetry.otlp_endpoint.clone()),
|
||||||
service_name: args
|
service_name: args
|
||||||
.telemetry_service_name
|
.telemetry_service_name
|
||||||
.clone()
|
.clone()
|
||||||
.or(base_telemetry.service_name)
|
.or(base_telemetry.service_name)
|
||||||
.unwrap_or_else(|| "wakey-control-plane".to_string()),
|
.unwrap_or_else(|| defaults.telemetry.service_name.clone()),
|
||||||
json_logs: args
|
json_logs: args
|
||||||
.telemetry_json_logs
|
.telemetry_json_logs
|
||||||
.or(base_telemetry.json_logs)
|
.or(base_telemetry.json_logs)
|
||||||
.unwrap_or(false),
|
.unwrap_or(defaults.telemetry.json_logs),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,6 +180,7 @@ pub fn bootstrap_config_if_missing(args: &ServeArgs) -> Result<bool> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::write_init_config;
|
use super::write_init_config;
|
||||||
use crate::cli::InitConfigArgs;
|
use crate::cli::InitConfigArgs;
|
||||||
|
use crate::config::types::DEFAULT_CONFIG;
|
||||||
|
|
||||||
fn temp_test_dir(name: &str) -> std::path::PathBuf {
|
fn temp_test_dir(name: &str) -> std::path::PathBuf {
|
||||||
std::env::temp_dir().join(format!(
|
std::env::temp_dir().join(format!(
|
||||||
@@ -251,4 +256,51 @@ json_logs = true
|
|||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_config_renders_shared_default_template() {
|
||||||
|
let dir = temp_test_dir("defaults");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).expect("create temp dir");
|
||||||
|
let out = dir.join("out.toml");
|
||||||
|
|
||||||
|
write_init_config(&InitConfigArgs {
|
||||||
|
config_file: Some(out.clone()),
|
||||||
|
from_config: None,
|
||||||
|
stdout: false,
|
||||||
|
data_dir: None,
|
||||||
|
bind: None,
|
||||||
|
public_url: None,
|
||||||
|
state_file: None,
|
||||||
|
pid_file: None,
|
||||||
|
ui_dist_dir: None,
|
||||||
|
command_timeout_ms: None,
|
||||||
|
enroll_token_ttl_seconds: None,
|
||||||
|
observation_retention_seconds: None,
|
||||||
|
bootstrap_enroll_tokens: Vec::new(),
|
||||||
|
telemetry_otlp_endpoint: None,
|
||||||
|
telemetry_service_name: None,
|
||||||
|
telemetry_json_logs: None,
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
.expect("write default config");
|
||||||
|
|
||||||
|
let rendered = std::fs::read_to_string(&out).expect("read default config");
|
||||||
|
let parsed: toml::Value = toml::from_str(&rendered).expect("parse default config");
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
|
assert_eq!(
|
||||||
|
parsed["state_file"].as_str(),
|
||||||
|
defaults.data_dir.join(&defaults.state_file).to_str()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed["command_timeout_ms"].as_integer(),
|
||||||
|
Some(defaults.command_timeout_ms as i64)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed["telemetry"]["service_name"].as_str(),
|
||||||
|
Some(defaults.telemetry.service_name.as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,19 +9,20 @@ use crate::cli::{
|
|||||||
RevokeEnrollTokenArgs, ServeArgs, StateStatsArgs,
|
RevokeEnrollTokenArgs, ServeArgs, StateStatsArgs,
|
||||||
};
|
};
|
||||||
use crate::config::types::{
|
use crate::config::types::{
|
||||||
DaemonConfig, FileConfig, FileTelemetryConfig, IssueTokenSettings, StateAccessSettings,
|
DEFAULT_CONFIG, DaemonConfig, FileConfig, FileTelemetryConfig, IssueTokenSettings,
|
||||||
TelemetryConfig,
|
StateAccessSettings, TelemetryConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
pub fn from_serve_args(args: &ServeArgs) -> Result<Self> {
|
pub fn from_serve_args(args: &ServeArgs) -> Result<Self> {
|
||||||
let file = load_file_config(&args.config_file)?;
|
let file = load_file_config(&args.config_file)?;
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
|
|
||||||
let data_dir = args
|
let data_dir = args
|
||||||
.data_dir
|
.data_dir
|
||||||
.clone()
|
.clone()
|
||||||
.or(file.data_dir.clone())
|
.or(file.data_dir.clone())
|
||||||
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_DATA_DIR));
|
.unwrap_or_else(|| defaults.data_dir.clone());
|
||||||
|
|
||||||
let bind = match args.bind {
|
let bind = match args.bind {
|
||||||
Some(bind) => bind,
|
Some(bind) => bind,
|
||||||
@@ -32,7 +33,8 @@ impl DaemonConfig {
|
|||||||
args.config_file.display()
|
args.config_file.display()
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
None => "0.0.0.0:8080"
|
None => defaults
|
||||||
|
.bind
|
||||||
.parse()
|
.parse()
|
||||||
.expect("static default bind should parse"),
|
.expect("static default bind should parse"),
|
||||||
},
|
},
|
||||||
@@ -42,34 +44,34 @@ impl DaemonConfig {
|
|||||||
args.public_url
|
args.public_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or(file.public_url.as_deref())
|
.or(file.public_url.as_deref())
|
||||||
.unwrap_or("http://127.0.0.1:8080"),
|
.unwrap_or(&defaults.public_url),
|
||||||
);
|
);
|
||||||
|
|
||||||
let state_file_raw = args
|
let state_file_raw = args
|
||||||
.state_file
|
.state_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(file.state_file)
|
.or(file.state_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("state.sqlite3"));
|
.unwrap_or_else(|| defaults.state_file.clone());
|
||||||
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(
|
||||||
args.command_timeout_ms
|
args.command_timeout_ms
|
||||||
.or(file.command_timeout_ms)
|
.or(file.command_timeout_ms)
|
||||||
.unwrap_or(30_000)
|
.unwrap_or(defaults.command_timeout_ms)
|
||||||
.max(1),
|
.max(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let enroll_token_ttl = Duration::from_secs(
|
let enroll_token_ttl = Duration::from_secs(
|
||||||
args.enroll_token_ttl_seconds
|
args.enroll_token_ttl_seconds
|
||||||
.or(file.enroll_token_ttl_seconds)
|
.or(file.enroll_token_ttl_seconds)
|
||||||
.unwrap_or(86_400)
|
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||||
.max(1),
|
.max(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let observation_retention = Duration::from_secs(
|
let observation_retention = Duration::from_secs(
|
||||||
args.observation_retention_seconds
|
args.observation_retention_seconds
|
||||||
.or(file.observation_retention_seconds)
|
.or(file.observation_retention_seconds)
|
||||||
.unwrap_or(2_592_000)
|
.unwrap_or(defaults.observation_retention_seconds)
|
||||||
.max(1),
|
.max(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,17 +79,18 @@ impl DaemonConfig {
|
|||||||
.pid_file
|
.pid_file
|
||||||
.clone()
|
.clone()
|
||||||
.or(file.pid_file)
|
.or(file.pid_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("wakey-control-plane.pid"));
|
.unwrap_or_else(|| defaults.pid_file.clone());
|
||||||
let pid_file = resolve_path(&data_dir, pid_file_raw);
|
let pid_file = resolve_path(&data_dir, pid_file_raw);
|
||||||
|
|
||||||
let ui_dist_dir = args
|
let ui_dist_dir = args
|
||||||
.ui_dist_dir
|
.ui_dist_dir
|
||||||
.clone()
|
.clone()
|
||||||
.or(file.ui_dist_dir)
|
.or(file.ui_dist_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from("ui/dist"));
|
.unwrap_or_else(|| defaults.ui_dist_dir.clone());
|
||||||
|
|
||||||
let bootstrap_enroll_tokens = if args.bootstrap_enroll_tokens.is_empty() {
|
let bootstrap_enroll_tokens = if args.bootstrap_enroll_tokens.is_empty() {
|
||||||
file.bootstrap_enroll_tokens.unwrap_or_default()
|
file.bootstrap_enroll_tokens
|
||||||
|
.unwrap_or_else(|| defaults.bootstrap_enroll_tokens.clone())
|
||||||
} else {
|
} else {
|
||||||
args.bootstrap_enroll_tokens.clone()
|
args.bootstrap_enroll_tokens.clone()
|
||||||
};
|
};
|
||||||
@@ -131,6 +134,7 @@ pub fn resolve_path(data_dir: &Path, candidate: PathBuf) -> PathBuf {
|
|||||||
|
|
||||||
pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<IssueTokenSettings> {
|
pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<IssueTokenSettings> {
|
||||||
let file = load_file_config(&args.config_file)?;
|
let file = load_file_config(&args.config_file)?;
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
let state = resolve_state_access(
|
let state = resolve_state_access(
|
||||||
&args.config_file,
|
&args.config_file,
|
||||||
args.data_dir.clone(),
|
args.data_dir.clone(),
|
||||||
@@ -142,7 +146,7 @@ pub fn resolve_issue_token_settings(args: &IssueEnrollTokenArgs) -> Result<Issue
|
|||||||
let ttl = Duration::from_secs(
|
let ttl = Duration::from_secs(
|
||||||
args.ttl_seconds
|
args.ttl_seconds
|
||||||
.or(file.enroll_token_ttl_seconds)
|
.or(file.enroll_token_ttl_seconds)
|
||||||
.unwrap_or(86_400)
|
.unwrap_or(defaults.enroll_token_ttl_seconds)
|
||||||
.max(1),
|
.max(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -254,16 +258,17 @@ fn resolve_state_access(
|
|||||||
mode: &AdminTargetArgs,
|
mode: &AdminTargetArgs,
|
||||||
) -> Result<StateAccessSettings> {
|
) -> Result<StateAccessSettings> {
|
||||||
let file = load_file_config(config_file)?;
|
let file = load_file_config(config_file)?;
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
|
|
||||||
let data_dir = cli_data_dir
|
let data_dir = cli_data_dir
|
||||||
.or(file.data_dir)
|
.or(file.data_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from(crate::cli::DEFAULT_DATA_DIR));
|
.unwrap_or_else(|| defaults.data_dir.clone());
|
||||||
|
|
||||||
let state_file = resolve_path(
|
let state_file = resolve_path(
|
||||||
&data_dir,
|
&data_dir,
|
||||||
cli_state_file
|
cli_state_file
|
||||||
.or(file.state_file)
|
.or(file.state_file)
|
||||||
.unwrap_or_else(|| PathBuf::from("state.sqlite3")),
|
.unwrap_or_else(|| defaults.state_file.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolved_public_url = cli_public_url
|
let resolved_public_url = cli_public_url
|
||||||
@@ -280,7 +285,9 @@ fn resolve_state_access(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{normalize_public_url, resolve_path};
|
use super::{DaemonConfig, normalize_public_url, resolve_path};
|
||||||
|
use crate::cli::ServeArgs;
|
||||||
|
use crate::config::types::DEFAULT_CONFIG;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -303,4 +310,43 @@ mod tests {
|
|||||||
PathBuf::from("/var/lib/wakey-control-plane/state.sqlite3")
|
PathBuf::from("/var/lib/wakey-control-plane/state.sqlite3")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daemon_config_uses_shared_default_template() {
|
||||||
|
let config_file = std::env::temp_dir().join(format!(
|
||||||
|
"wakey-cc-missing-config-{}.toml",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_file(&config_file);
|
||||||
|
let resolved = DaemonConfig::from_serve_args(&ServeArgs {
|
||||||
|
data_dir: None,
|
||||||
|
bind: None,
|
||||||
|
public_url: None,
|
||||||
|
state_file: None,
|
||||||
|
bootstrap_enroll_tokens: Vec::new(),
|
||||||
|
command_timeout_ms: None,
|
||||||
|
enroll_token_ttl_seconds: None,
|
||||||
|
observation_retention_seconds: None,
|
||||||
|
pid_file: None,
|
||||||
|
ui_dist_dir: None,
|
||||||
|
config_file,
|
||||||
|
bootstrap_config: false,
|
||||||
|
})
|
||||||
|
.expect("resolve default config");
|
||||||
|
let defaults = &*DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
assert_eq!(resolved.data_dir, defaults.data_dir);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.state_file,
|
||||||
|
defaults.data_dir.join(&defaults.state_file)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.command_timeout,
|
||||||
|
std::time::Duration::from_millis(defaults.command_timeout_ms)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.telemetry.service_name,
|
||||||
|
defaults.telemetry.service_name
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -29,10 +30,11 @@ pub struct TelemetryConfig {
|
|||||||
|
|
||||||
impl Default for TelemetryConfig {
|
impl Default for TelemetryConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let defaults = &DEFAULT_CONFIG.telemetry;
|
||||||
Self {
|
Self {
|
||||||
otlp_endpoint: None,
|
otlp_endpoint: defaults.otlp_endpoint.clone(),
|
||||||
service_name: "wakey-control-plane".to_string(),
|
service_name: defaults.service_name.clone(),
|
||||||
json_logs: false,
|
json_logs: defaults.json_logs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,7 @@ pub(crate) struct FileTelemetryConfig {
|
|||||||
pub(crate) json_logs: Option<bool>,
|
pub(crate) json_logs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub(crate) struct WritableConfig {
|
pub(crate) struct WritableConfig {
|
||||||
pub(crate) data_dir: PathBuf,
|
pub(crate) data_dir: PathBuf,
|
||||||
pub(crate) bind: String,
|
pub(crate) bind: String,
|
||||||
@@ -76,7 +78,7 @@ pub(crate) struct WritableConfig {
|
|||||||
pub(crate) telemetry: WritableTelemetry,
|
pub(crate) telemetry: WritableTelemetry,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub(crate) struct WritableTelemetry {
|
pub(crate) struct WritableTelemetry {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub(crate) otlp_endpoint: Option<String>,
|
pub(crate) otlp_endpoint: Option<String>,
|
||||||
@@ -84,6 +86,24 @@ pub(crate) struct WritableTelemetry {
|
|||||||
pub(crate) json_logs: bool,
|
pub(crate) json_logs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) static DEFAULT_CONFIG: LazyLock<WritableConfig> = LazyLock::new(|| WritableConfig {
|
||||||
|
data_dir: crate::cli::DEFAULT_DATA_DIR.into(),
|
||||||
|
bind: "0.0.0.0:8080".to_string(),
|
||||||
|
public_url: "http://127.0.0.1:8080".to_string(),
|
||||||
|
state_file: "state.sqlite3".into(),
|
||||||
|
command_timeout_ms: 30_000,
|
||||||
|
enroll_token_ttl_seconds: 86_400,
|
||||||
|
observation_retention_seconds: 2_592_000,
|
||||||
|
pid_file: "wakey-control-plane.pid".into(),
|
||||||
|
ui_dist_dir: "ui/dist".into(),
|
||||||
|
bootstrap_enroll_tokens: Vec::new(),
|
||||||
|
telemetry: WritableTelemetry {
|
||||||
|
otlp_endpoint: None,
|
||||||
|
service_name: "wakey-control-plane".to_string(),
|
||||||
|
json_logs: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
pub struct IssueTokenSettings {
|
pub struct IssueTokenSettings {
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
pub state_file: PathBuf,
|
pub state_file: PathBuf,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey-core"
|
name = "wakey-core"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey-linux"
|
name = "wakey-linux"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user