error handling, coderabbit
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_key FROM agent_devices WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_key",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3f24dfb51be2c3051dc8a8f732bd3e3fe9ca6b2649e1202d3725693b8011f269"
|
||||||
|
}
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_macs (agent_id, device_key, mac) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "47a9d2f464cc766b5bc83f8ef05cb34b2c0f3d5c1a7770b13f70b60deb76bbbf"
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_hostnames (agent_id, device_key, hostname) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "652cd104ad380746235ab932b00decfce78be6756f39f4ff3787a0cac9df7554"
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_ips (agent_id, device_key, ip) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "97aef0a2b276a9ba64840f10b7fb15de5ecd12cb3aa709043a9e03da193e6a5b"
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "INSERT INTO agent_devices (agent_id, device_key, presence, display_name, first_seen_unix, last_seen_unix)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT (agent_id, device_key) DO UPDATE SET\n presence = excluded.presence,\n last_seen_unix = excluded.last_seen_unix",
|
"query": "INSERT INTO agent_devices (agent_id, device_key, presence, display_name, first_seen_unix, last_seen_unix)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT (agent_id, device_key) DO UPDATE SET\n presence = excluded.presence,\n display_name = excluded.display_name,\n last_seen_unix = excluded.last_seen_unix",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -8,5 +8,5 @@
|
|||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "990e14f1dd4093b75780e0177bba88c04379776f539f4d2823bee16aec5990f3"
|
"hash": "a29f58cb7dd1b1bc510a0d04ecd1dbc7250824ba089dbc36ed9ac2e6dc920fd7"
|
||||||
}
|
}
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_facts (agent_id, device_key, fact_json) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "dd591c18060a1b6432eecacb1d1b6fb1fc518a4a7c134c11b60d42dc3c1b179d"
|
|
||||||
}
|
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "COUNT(*)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f5a815d4d6e3d088aba3cd01607764d89afaae6108aad0d17e93b1bfc9f7e772"
|
||||||
|
}
|
||||||
+34
-4
@@ -153,6 +153,25 @@ export type WakeFleetDeviceResponse = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface APIErrorResponse {
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public details: any,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "APIError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
...init,
|
...init,
|
||||||
@@ -164,11 +183,22 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const raw = await res.text();
|
const raw = await res.text();
|
||||||
let detail = raw;
|
let parsed: APIErrorResponse | null = null;
|
||||||
try {
|
try {
|
||||||
detail = JSON.stringify(JSON.parse(raw), null, 2);
|
parsed = JSON.parse(raw);
|
||||||
} catch {}
|
// assume APIError
|
||||||
throw new Error(`${res.status} ${res.statusText}\n${detail}`);
|
if (parsed?.error?.details) {
|
||||||
|
parsed.error.details = JSON.stringify(parsed.error.details);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new APIError(
|
||||||
|
`${res.status} ${res.statusText}\n${raw}`,
|
||||||
|
parsed?.error?.code ?? "unknown",
|
||||||
|
parsed?.error?.details ?? parsed?.error?.message ?? raw,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json() as Promise<T>;
|
return res.json() as Promise<T>;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
|
|||||||
presence,
|
presence,
|
||||||
known,
|
known,
|
||||||
agentId: agentId === "all" ? "" : agentId,
|
agentId: agentId === "all" ? "" : agentId,
|
||||||
|
visibility: "all",
|
||||||
limit: 500,
|
limit: 500,
|
||||||
}),
|
}),
|
||||||
fetchKnownDevices(),
|
fetchKnownDevices(),
|
||||||
@@ -373,7 +374,9 @@ function FleetDeviceRow({
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`grid gap-3 rounded-md border border-l-[3px] ${presenceBorder} bg-card px-3 py-3 text-sm xl:grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] xl:items-center xl:gap-2`}>
|
<div
|
||||||
|
className={`grid gap-3 rounded-md border border-l-[3px] ${presenceBorder} bg-card px-3 py-3 text-sm xl:grid-cols-[minmax(10rem,1.3fr)_8rem_minmax(9rem,1fr)_minmax(10rem,1fr)_minmax(10rem,1fr)_8rem_8rem] xl:items-center xl:gap-2`}
|
||||||
|
>
|
||||||
<button className="min-w-0 text-left" type="button" onClick={onDetails}>
|
<button className="min-w-0 text-left" type="button" onClick={onDetails}>
|
||||||
<div className="truncate font-medium">{device.display_name}</div>
|
<div className="truncate font-medium">{device.display_name}</div>
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
|||||||
@@ -96,14 +96,19 @@ export function FleetDeviceDetailsDialog({
|
|||||||
(kd) => kd.device_id !== currentDevice.known_device?.device_id,
|
(kd) => kd.device_id !== currentDevice.known_device?.device_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
async function wrap(fn: () => Promise<void>) {
|
async function wrap(fn: () => Promise<void | { changed?: boolean }>) {
|
||||||
setError("");
|
setError("");
|
||||||
setActionBusy(true);
|
setActionBusy(true);
|
||||||
try {
|
try {
|
||||||
await fn();
|
const res = await fn();
|
||||||
onChanged();
|
if (!res || res.changed !== false) {
|
||||||
} catch (err) {
|
onChanged();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
setError(String(err));
|
setError(String(err));
|
||||||
|
if (err && (err.changed || err.partial)) {
|
||||||
|
onChanged();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setActionBusy(false);
|
setActionBusy(false);
|
||||||
}
|
}
|
||||||
@@ -125,12 +130,25 @@ export function FleetDeviceDetailsDialog({
|
|||||||
const target = knownDevices.find((kd) => kd.device_id === targetDeviceId);
|
const target = knownDevices.find((kd) => kd.device_id === targetDeviceId);
|
||||||
await wrap(async () => {
|
await wrap(async () => {
|
||||||
const ids = fullKnown ? unattachedIdentifiers : rowIdentifiers;
|
const ids = fullKnown ? unattachedIdentifiers : rowIdentifiers;
|
||||||
for (const id of ids) {
|
if (ids.length === 0) return;
|
||||||
await attachDeviceIdentifier(targetDeviceId, id);
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
ids.map((id) => attachDeviceIdentifier(targetDeviceId, id)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const changed = results.some((r) => r.status === "fulfilled");
|
||||||
|
const errors = results.filter((r) => r.status === "rejected");
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
const err = errors[0].reason as Error;
|
||||||
|
if (changed) (err as any).changed = true;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
`Attached ${ids.length} identifier(s) to "${target?.display_name ?? targetDeviceId}"`,
|
`Attached ${ids.length} identifier(s) to "${target?.display_name ?? targetDeviceId}"`,
|
||||||
);
|
);
|
||||||
|
return { changed };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -29,7 +29,19 @@ async fn main() -> Result<()> {
|
|||||||
if let Some(config) = global_config {
|
if let Some(config) = global_config {
|
||||||
args.config = config.to_path_buf();
|
args.config = config.to_path_buf();
|
||||||
}
|
}
|
||||||
let existing_config = config::load_config(&args.config).ok();
|
let existing_config = match config::load_config(&args.config) {
|
||||||
|
Ok(cfg) => Some(cfg),
|
||||||
|
Err(e) => {
|
||||||
|
if e.root_cause()
|
||||||
|
.downcast_ref::<std::io::Error>()
|
||||||
|
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound)
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
let resolved_server_url = if let Some(server_url) = args.server_url.as_deref() {
|
let resolved_server_url = if let Some(server_url) = args.server_url.as_deref() {
|
||||||
server_url.to_string()
|
server_url.to_string()
|
||||||
} else if let Some(cfg) = existing_config.as_ref() {
|
} else if let Some(cfg) = existing_config.as_ref() {
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT device_key FROM agent_devices WHERE agent_id = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "device_key",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3f24dfb51be2c3051dc8a8f732bd3e3fe9ca6b2649e1202d3725693b8011f269"
|
||||||
|
}
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_macs (agent_id, device_key, mac) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "47a9d2f464cc766b5bc83f8ef05cb34b2c0f3d5c1a7770b13f70b60deb76bbbf"
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_hostnames (agent_id, device_key, hostname) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "652cd104ad380746235ab932b00decfce78be6756f39f4ff3787a0cac9df7554"
|
|
||||||
}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_ips (agent_id, device_key, ip) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "97aef0a2b276a9ba64840f10b7fb15de5ecd12cb3aa709043a9e03da193e6a5b"
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "INSERT INTO agent_devices (agent_id, device_key, presence, display_name, first_seen_unix, last_seen_unix)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT (agent_id, device_key) DO UPDATE SET\n presence = excluded.presence,\n last_seen_unix = excluded.last_seen_unix",
|
"query": "INSERT INTO agent_devices (agent_id, device_key, presence, display_name, first_seen_unix, last_seen_unix)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT (agent_id, device_key) DO UPDATE SET\n presence = excluded.presence,\n display_name = excluded.display_name,\n last_seen_unix = excluded.last_seen_unix",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -8,5 +8,5 @@
|
|||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "990e14f1dd4093b75780e0177bba88c04379776f539f4d2823bee16aec5990f3"
|
"hash": "a29f58cb7dd1b1bc510a0d04ecd1dbc7250824ba089dbc36ed9ac2e6dc920fd7"
|
||||||
}
|
}
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT INTO agent_device_facts (agent_id, device_key, fact_json) VALUES (?1, ?2, ?3)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 3
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "dd591c18060a1b6432eecacb1d1b6fb1fc518a4a7c134c11b60d42dc3c1b179d"
|
|
||||||
}
|
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "COUNT(*)",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f5a815d4d6e3d088aba3cd01607764d89afaae6108aad0d17e93b1bfc9f7e772"
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use serde::Deserialize;
|
|||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::AppState;
|
use crate::runtime::AppState;
|
||||||
use crate::state::{AlertState, AuditEvent, AuditEventFilter};
|
use crate::state::{AlertState, AuditEvent, AuditEventFilter};
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ pub struct AlertRuleConfig {
|
|||||||
pub async fn active_alerts(
|
pub async fn active_alerts(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<ActiveAlertsQuery>,
|
Query(query): Query<ActiveAlertsQuery>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let alerts = evaluate_alerts(
|
let alerts = evaluate_alerts(
|
||||||
&state,
|
&state,
|
||||||
AlertRuleConfig {
|
AlertRuleConfig {
|
||||||
@@ -73,7 +73,7 @@ pub async fn active_alerts(
|
|||||||
pub async fn alert_history(
|
pub async fn alert_history(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<AlertHistoryQuery>,
|
Query(query): Query<AlertHistoryQuery>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let limit = query.limit.unwrap_or(100).clamp(1, 500);
|
let limit = query.limit.unwrap_or(100).clamp(1, 500);
|
||||||
let history = state
|
let history = state
|
||||||
.store
|
.store
|
||||||
@@ -81,7 +81,7 @@ pub async fn alert_history(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
warn!(error = %err, "failed reading alert transition history");
|
warn!(error = %err, "failed reading alert transition history");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"alert_history_failed",
|
"alert_history_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -112,7 +112,7 @@ async fn alerts_stream_socket(state: AppState, mut socket: WebSocket, config: Al
|
|||||||
let alerts = match evaluate_alerts(&state, config.clone()).await {
|
let alerts = match evaluate_alerts(&state, config.clone()).await {
|
||||||
Ok(alerts) => alerts,
|
Ok(alerts) => alerts,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(code = %err.0, "failed to evaluate alerts for stream");
|
warn!(code = %err.code, "failed to evaluate alerts for stream");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -152,7 +152,7 @@ async fn alerts_stream_socket(state: AppState, mut socket: WebSocket, config: Al
|
|||||||
async fn evaluate_alerts(
|
async fn evaluate_alerts(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
config: AlertRuleConfig,
|
config: AlertRuleConfig,
|
||||||
) -> Result<Vec<AlertState>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Vec<AlertState>, ApiError> {
|
||||||
let lookback_seconds = config.lookback_seconds.unwrap_or(900).clamp(60, 86_400);
|
let lookback_seconds = config.lookback_seconds.unwrap_or(900).clamp(60, 86_400);
|
||||||
let timeout_threshold = config.timeout_threshold.unwrap_or(3).max(1);
|
let timeout_threshold = config.timeout_threshold.unwrap_or(3).max(1);
|
||||||
let auth_rejected_threshold = config.auth_rejected_threshold.unwrap_or(3).max(1);
|
let auth_rejected_threshold = config.auth_rejected_threshold.unwrap_or(3).max(1);
|
||||||
@@ -182,7 +182,7 @@ async fn evaluate_alerts(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
warn!(error = %err, "failed reading timeout audit events");
|
warn!(error = %err, "failed reading timeout audit events");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"alerts_query_failed",
|
"alerts_query_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -201,7 +201,7 @@ async fn evaluate_alerts(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
warn!(error = %err, "failed reading auth-rejected audit events");
|
warn!(error = %err, "failed reading auth-rejected audit events");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"alerts_query_failed",
|
"alerts_query_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -220,7 +220,7 @@ async fn evaluate_alerts(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
warn!(error = %err, "failed reading enroll-rejected audit events");
|
warn!(error = %err, "failed reading enroll-rejected audit events");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"alerts_query_failed",
|
"alerts_query_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::response::IntoResponse;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::AppState;
|
use crate::runtime::AppState;
|
||||||
use crate::state::AuditEventFilter;
|
use crate::state::AuditEventFilter;
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ pub struct AuditEventResponse {
|
|||||||
pub async fn list_audit_events(
|
pub async fn list_audit_events(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<ListAuditEventsQuery>,
|
Query(query): Query<ListAuditEventsQuery>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let filter = AuditEventFilter {
|
let filter = AuditEventFilter {
|
||||||
agent_id: query.agent_id,
|
agent_id: query.agent_id,
|
||||||
request_id: query.request_id,
|
request_id: query.request_id,
|
||||||
@@ -71,7 +71,7 @@ pub async fn list_audit_events(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to list audit events");
|
warn!(error = %err, "failed to list audit events");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"list_audit_events_failed",
|
"list_audit_events_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use tracing::{info, info_span, warn};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use wakey_agent::protocol::{AgentCommand, ErrorPayload, RequestId, ServerMessage};
|
use wakey_agent::protocol::{AgentCommand, ErrorPayload, RequestId, ServerMessage};
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::{AgentReply, AppState, SessionEvent};
|
use crate::runtime::{AgentReply, AppState, SessionEvent};
|
||||||
use crate::state::AuditEventInput;
|
use crate::state::AuditEventInput;
|
||||||
|
|
||||||
@@ -35,9 +35,7 @@ pub struct RelayCommandResponse {
|
|||||||
pub error: Option<ErrorPayload>,
|
pub error: Option<ErrorPayload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_agents(
|
pub async fn list_agents(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
||||||
let enrolled = state.store.list_agents_with_nicknames().await;
|
let enrolled = state.store.list_agents_with_nicknames().await;
|
||||||
let sessions = state.sessions.read().await;
|
let sessions = state.sessions.read().await;
|
||||||
|
|
||||||
@@ -57,7 +55,7 @@ pub async fn run_command(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(agent_id): AxumPath<String>,
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
Json(req): Json<RelayCommandRequest>,
|
Json(req): Json<RelayCommandRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match relay_agent_command(&state, &agent_id, req.command, req.timeout_ms).await {
|
match relay_agent_command(&state, &agent_id, req.command, req.timeout_ms).await {
|
||||||
Ok(response) => Ok((StatusCode::OK, Json(response))),
|
Ok(response) => Ok((StatusCode::OK, Json(response))),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
@@ -69,7 +67,7 @@ pub async fn relay_agent_command(
|
|||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
command: AgentCommand,
|
command: AgentCommand,
|
||||||
timeout_ms: Option<u64>,
|
timeout_ms: Option<u64>,
|
||||||
) -> Result<RelayCommandResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<RelayCommandResponse, ApiError> {
|
||||||
let request_id_string = format!("req-{}", Uuid::new_v4());
|
let request_id_string = format!("req-{}", Uuid::new_v4());
|
||||||
let command_kind = command_kind(&command);
|
let command_kind = command_kind(&command);
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
@@ -82,7 +80,7 @@ pub async fn relay_agent_command(
|
|||||||
let _span_guard = span.enter();
|
let _span_guard = span.enter();
|
||||||
|
|
||||||
let request_id = RequestId::try_from(request_id_string.clone()).map_err(|err| {
|
let request_id = RequestId::try_from(request_id_string.clone()).map_err(|err| {
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"invalid_request_id",
|
"invalid_request_id",
|
||||||
&err,
|
&err,
|
||||||
@@ -95,7 +93,7 @@ pub async fn relay_agent_command(
|
|||||||
}
|
}
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
warn!("command rejected: agent not connected");
|
warn!("command rejected: agent not connected");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"agent_not_connected",
|
"agent_not_connected",
|
||||||
"agent is not connected",
|
"agent is not connected",
|
||||||
@@ -151,7 +149,7 @@ pub async fn relay_agent_command(
|
|||||||
{
|
{
|
||||||
warn!(error = %audit_err, "failed to append audit event for command send failure");
|
warn!(error = %audit_err, "failed to append audit event for command send failure");
|
||||||
}
|
}
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::BAD_GATEWAY,
|
StatusCode::BAD_GATEWAY,
|
||||||
"agent_send_failed",
|
"agent_send_failed",
|
||||||
&format!("failed to send command to agent: {err}"),
|
&format!("failed to send command to agent: {err}"),
|
||||||
@@ -236,7 +234,7 @@ pub async fn relay_agent_command(
|
|||||||
{
|
{
|
||||||
warn!(error = %err, "failed to append audit event for dropped response");
|
warn!(error = %err, "failed to append audit event for dropped response");
|
||||||
}
|
}
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::BAD_GATEWAY,
|
StatusCode::BAD_GATEWAY,
|
||||||
"agent_response_dropped",
|
"agent_response_dropped",
|
||||||
"agent response channel dropped",
|
"agent response channel dropped",
|
||||||
@@ -265,7 +263,7 @@ pub async fn relay_agent_command(
|
|||||||
{
|
{
|
||||||
warn!(error = %err, "failed to append audit event for timeout");
|
warn!(error = %err, "failed to append audit event for timeout");
|
||||||
}
|
}
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::GATEWAY_TIMEOUT,
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
"agent_timeout",
|
"agent_timeout",
|
||||||
"agent did not answer before timeout",
|
"agent did not answer before timeout",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::response::IntoResponse;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::AppState;
|
use crate::runtime::AppState;
|
||||||
use crate::state::{DeviceIdentifierInput, KnownDeviceInput};
|
use crate::state::{DeviceIdentifierInput, KnownDeviceInput};
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ pub struct MergeKnownDeviceRequest {
|
|||||||
pub async fn create_known_device(
|
pub async fn create_known_device(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<CreateKnownDeviceRequest>,
|
Json(req): Json<CreateKnownDeviceRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let input = KnownDeviceInput {
|
let input = KnownDeviceInput {
|
||||||
display_name: req.display_name,
|
display_name: req.display_name,
|
||||||
pinned: req.pinned,
|
pinned: req.pinned,
|
||||||
@@ -78,7 +78,7 @@ pub async fn create_known_device(
|
|||||||
Ok(device) => Ok((StatusCode::CREATED, Json(known_device_response(device)))),
|
Ok(device) => Ok((StatusCode::CREATED, Json(known_device_response(device)))),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to create known device");
|
warn!(error = %err, "failed to create known device");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"create_known_device_failed",
|
"create_known_device_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -89,7 +89,7 @@ pub async fn create_known_device(
|
|||||||
|
|
||||||
pub async fn list_known_devices(
|
pub async fn list_known_devices(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.list_known_devices().await {
|
match state.store.list_known_devices().await {
|
||||||
Ok(devices) => Ok((
|
Ok(devices) => Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -102,7 +102,7 @@ pub async fn list_known_devices(
|
|||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to list known devices");
|
warn!(error = %err, "failed to list known devices");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"list_known_devices_failed",
|
"list_known_devices_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -114,7 +114,7 @@ pub async fn list_known_devices(
|
|||||||
pub async fn forget_known_device(
|
pub async fn forget_known_device(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(device_id): AxumPath<String>,
|
AxumPath(device_id): AxumPath<String>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.forget_known_device(&device_id).await {
|
match state.store.forget_known_device(&device_id).await {
|
||||||
Ok(forgotten) => Ok((
|
Ok(forgotten) => Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -125,7 +125,7 @@ pub async fn forget_known_device(
|
|||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to forget known device");
|
warn!(error = %err, "failed to forget known device");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"forget_known_device_failed",
|
"forget_known_device_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -138,7 +138,7 @@ pub async fn attach_device_identifier(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(device_id): AxumPath<String>,
|
AxumPath(device_id): AxumPath<String>,
|
||||||
Json(req): Json<DeviceIdentifierRequest>,
|
Json(req): Json<DeviceIdentifierRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let input = DeviceIdentifierInput {
|
let input = DeviceIdentifierInput {
|
||||||
kind: req.kind,
|
kind: req.kind,
|
||||||
value: req.value,
|
value: req.value,
|
||||||
@@ -150,14 +150,14 @@ pub async fn attach_device_identifier(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
||||||
Ok(None) => Err(json_error(
|
Ok(None) => Err(ApiError::new(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"known_device_not_found",
|
"known_device_not_found",
|
||||||
"known device not found",
|
"known device not found",
|
||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to attach device identifier");
|
warn!(error = %err, "failed to attach device identifier");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"attach_device_identifier_failed",
|
"attach_device_identifier_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -169,21 +169,21 @@ pub async fn attach_device_identifier(
|
|||||||
pub async fn detach_device_identifier(
|
pub async fn detach_device_identifier(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath((device_id, identifier_key)): AxumPath<(String, String)>,
|
AxumPath((device_id, identifier_key)): AxumPath<(String, String)>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state
|
match state
|
||||||
.store
|
.store
|
||||||
.detach_device_identifier(&device_id, &identifier_key)
|
.detach_device_identifier(&device_id, &identifier_key)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
||||||
Ok(None) => Err(json_error(
|
Ok(None) => Err(ApiError::new(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"known_device_not_found",
|
"known_device_not_found",
|
||||||
"known device not found",
|
"known device not found",
|
||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to detach device identifier");
|
warn!(error = %err, "failed to detach device identifier");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"detach_device_identifier_failed",
|
"detach_device_identifier_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -196,21 +196,21 @@ pub async fn merge_known_device(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(device_id): AxumPath<String>,
|
AxumPath(device_id): AxumPath<String>,
|
||||||
Json(req): Json<MergeKnownDeviceRequest>,
|
Json(req): Json<MergeKnownDeviceRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state
|
match state
|
||||||
.store
|
.store
|
||||||
.merge_known_devices(&device_id, &req.source_device_id)
|
.merge_known_devices(&device_id, &req.source_device_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
Ok(Some(device)) => Ok((StatusCode::OK, Json(known_device_response(device)))),
|
||||||
Ok(None) => Err(json_error(
|
Ok(None) => Err(ApiError::new(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"known_device_not_found",
|
"known_device_not_found",
|
||||||
"target or source known device not found",
|
"target or source known device not found",
|
||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to merge known devices");
|
warn!(error = %err, "failed to merge known devices");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"merge_known_device_failed",
|
"merge_known_device_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::response::IntoResponse;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::{AppState, SessionEvent};
|
use crate::runtime::{AppState, SessionEvent};
|
||||||
use crate::state::AuditEventInput;
|
use crate::state::AuditEventInput;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ pub async fn healthz() -> &'static str {
|
|||||||
pub async fn enroll(
|
pub async fn enroll(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<EnrollRequest>,
|
Json(req): Json<EnrollRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.enroll(&req.enroll_token).await {
|
match state.store.enroll(&req.enroll_token).await {
|
||||||
Ok(issued) => {
|
Ok(issued) => {
|
||||||
info!(agent_id = %issued.agent_id, "agent enrollment accepted");
|
info!(agent_id = %issued.agent_id, "agent enrollment accepted");
|
||||||
@@ -119,7 +119,7 @@ pub async fn enroll(
|
|||||||
{
|
{
|
||||||
warn!(error = %audit_err, "failed to append audit event for enroll rejection");
|
warn!(error = %audit_err, "failed to append audit event for enroll rejection");
|
||||||
}
|
}
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"enrollment_rejected",
|
"enrollment_rejected",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -131,7 +131,7 @@ pub async fn enroll(
|
|||||||
pub async fn issue_enroll_token(
|
pub async fn issue_enroll_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<IssueEnrollTokenQuery>,
|
Query(query): Query<IssueEnrollTokenQuery>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let ttl = std::time::Duration::from_secs(
|
let ttl = std::time::Duration::from_secs(
|
||||||
query
|
query
|
||||||
.ttl_seconds
|
.ttl_seconds
|
||||||
@@ -175,7 +175,7 @@ pub async fn issue_enroll_token(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to issue enroll token");
|
warn!(error = %err, "failed to issue enroll token");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"issue_enroll_token_failed",
|
"issue_enroll_token_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -186,7 +186,7 @@ pub async fn issue_enroll_token(
|
|||||||
|
|
||||||
pub async fn list_enroll_tokens(
|
pub async fn list_enroll_tokens(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.list_enroll_tokens().await {
|
match state.store.list_enroll_tokens().await {
|
||||||
Ok(tokens) => {
|
Ok(tokens) => {
|
||||||
if let Err(err) = state
|
if let Err(err) = state
|
||||||
@@ -220,7 +220,7 @@ pub async fn list_enroll_tokens(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to list enroll tokens");
|
warn!(error = %err, "failed to list enroll tokens");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"list_enroll_tokens_failed",
|
"list_enroll_tokens_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -232,7 +232,7 @@ pub async fn list_enroll_tokens(
|
|||||||
pub async fn revoke_enroll_token(
|
pub async fn revoke_enroll_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(token): AxumPath<String>,
|
AxumPath(token): AxumPath<String>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.revoke_enroll_token(&token).await {
|
match state.store.revoke_enroll_token(&token).await {
|
||||||
Ok(revoked) => {
|
Ok(revoked) => {
|
||||||
if let Err(err) = state
|
if let Err(err) = state
|
||||||
@@ -267,7 +267,7 @@ pub async fn revoke_enroll_token(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to revoke enroll token");
|
warn!(error = %err, "failed to revoke enroll token");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"revoke_enroll_token_failed",
|
"revoke_enroll_token_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -279,7 +279,7 @@ pub async fn revoke_enroll_token(
|
|||||||
pub async fn revoke_agent(
|
pub async fn revoke_agent(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(agent_id): AxumPath<String>,
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match state.store.revoke_agent(&agent_id).await {
|
match state.store.revoke_agent(&agent_id).await {
|
||||||
Ok(revoked) => {
|
Ok(revoked) => {
|
||||||
if revoked {
|
if revoked {
|
||||||
@@ -322,7 +322,7 @@ pub async fn revoke_agent(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to revoke agent credentials");
|
warn!(error = %err, "failed to revoke agent credentials");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"revoke_agent_failed",
|
"revoke_agent_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -335,7 +335,7 @@ pub async fn set_agent_nickname(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(agent_id): AxumPath<String>,
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
Json(req): Json<SetAgentNicknameRequest>,
|
Json(req): Json<SetAgentNicknameRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let normalized = req
|
let normalized = req
|
||||||
.nickname
|
.nickname
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -389,7 +389,7 @@ pub async fn set_agent_nickname(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to update agent nickname");
|
warn!(error = %err, "failed to update agent nickname");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"set_agent_nickname_failed",
|
"set_agent_nickname_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use axum::response::IntoResponse;
|
|||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use wakey_agent::protocol::{AgentCommand, InventoryRequest, WakeRequest};
|
use wakey_agent::protocol::{AgentCommand, InventoryRequest, WakeRequest};
|
||||||
|
|
||||||
|
use crate::api::ApiError;
|
||||||
use crate::api::commands::relay_agent_command;
|
use crate::api::commands::relay_agent_command;
|
||||||
use crate::api::json_error;
|
|
||||||
use crate::runtime::AppState;
|
use crate::runtime::AppState;
|
||||||
|
|
||||||
mod build;
|
mod build;
|
||||||
@@ -29,12 +29,12 @@ use types::{
|
|||||||
pub async fn list_fleet_devices(
|
pub async fn list_fleet_devices(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<ListFleetDevicesQuery>,
|
Query(query): Query<ListFleetDevicesQuery>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
match load_fleet_devices(&state, &query).await {
|
match load_fleet_devices(&state, &query).await {
|
||||||
Ok(devices) => Ok((StatusCode::OK, Json(devices))),
|
Ok(devices) => Ok((StatusCode::OK, Json(devices))),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to list fleet devices");
|
warn!(error = %err, "failed to list fleet devices");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"list_fleet_devices_failed",
|
"list_fleet_devices_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -46,7 +46,7 @@ pub async fn list_fleet_devices(
|
|||||||
pub async fn refresh_fleet_devices(
|
pub async fn refresh_fleet_devices(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RefreshFleetDevicesRequest>,
|
Json(req): Json<RefreshFleetDevicesRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let mut agent_ids = if req.agent_ids.is_empty() {
|
let mut agent_ids = if req.agent_ids.is_empty() {
|
||||||
let sessions = state.sessions.read().await;
|
let sessions = state.sessions.read().await;
|
||||||
sessions.keys().cloned().collect::<Vec<_>>()
|
sessions.keys().cloned().collect::<Vec<_>>()
|
||||||
@@ -131,11 +131,11 @@ pub async fn refresh_fleet_devices(
|
|||||||
.map(|error| error.message)
|
.map(|error| error.message)
|
||||||
.or_else(|| Some("inventory command failed".into())),
|
.or_else(|| Some("inventory command failed".into())),
|
||||||
}),
|
}),
|
||||||
Err((status, body)) => results.push(RefreshFleetAgentResult {
|
Err(err) => results.push(RefreshFleetAgentResult {
|
||||||
agent_id,
|
agent_id,
|
||||||
status: "error".into(),
|
status: "error".into(),
|
||||||
accepted: 0,
|
accepted: 0,
|
||||||
error: Some(format!("{status}: {}", body.0)),
|
error: Some(format!("{}: {}", err.status, err.message)),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ pub async fn refresh_fleet_devices(
|
|||||||
pub async fn wake_fleet_device(
|
pub async fn wake_fleet_device(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<WakeFleetDeviceRequest>,
|
Json(req): Json<WakeFleetDeviceRequest>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let query = ListFleetDevicesQuery {
|
let query = ListFleetDevicesQuery {
|
||||||
query: None,
|
query: None,
|
||||||
presence: None,
|
presence: None,
|
||||||
@@ -163,7 +163,7 @@ pub async fn wake_fleet_device(
|
|||||||
};
|
};
|
||||||
let devices = load_fleet_devices(&state, &query).await.map_err(|err| {
|
let devices = load_fleet_devices(&state, &query).await.map_err(|err| {
|
||||||
warn!(error = %err, "failed loading fleet devices for wake");
|
warn!(error = %err, "failed loading fleet devices for wake");
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"load_fleet_devices_failed",
|
"load_fleet_devices_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
@@ -174,7 +174,7 @@ pub async fn wake_fleet_device(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|device| device.device_key == req.device_key)
|
.find(|device| device.device_key == req.device_key)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"fleet_device_not_found",
|
"fleet_device_not_found",
|
||||||
"fleet device not found",
|
"fleet device not found",
|
||||||
@@ -189,7 +189,7 @@ pub async fn wake_fleet_device(
|
|||||||
None => device.recommended_route,
|
None => device.recommended_route,
|
||||||
}
|
}
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
json_error(
|
ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"wake_route_unavailable",
|
"wake_route_unavailable",
|
||||||
"no wakeable connected MAC-backed route is available",
|
"no wakeable connected MAC-backed route is available",
|
||||||
@@ -197,21 +197,21 @@ pub async fn wake_fleet_device(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let Some(mac) = route.mac else {
|
let Some(mac) = route.mac else {
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"wake_route_unavailable",
|
"wake_route_unavailable",
|
||||||
"selected route does not include a MAC address",
|
"selected route does not include a MAC address",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
if !route.connected {
|
if !route.connected {
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"wake_route_unavailable",
|
"wake_route_unavailable",
|
||||||
"selected route agent is not connected",
|
"selected route agent is not connected",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !route.wakeable {
|
if !route.wakeable {
|
||||||
return Err(json_error(
|
return Err(ApiError::new(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"wake_route_unavailable",
|
"wake_route_unavailable",
|
||||||
"selected route is not wakeable",
|
"selected route is not wakeable",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::response::IntoResponse;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::api::json_error;
|
use crate::api::ApiError;
|
||||||
use crate::runtime::AppState;
|
use crate::runtime::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -17,9 +17,7 @@ pub struct StateStatsResponse {
|
|||||||
pub expired_enroll_token_count: usize,
|
pub expired_enroll_token_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn state_stats(
|
pub async fn state_stats(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
|
||||||
State(state): State<AppState>,
|
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
|
||||||
match state.store.stats().await {
|
match state.store.stats().await {
|
||||||
Ok(stats) => Ok((
|
Ok(stats) => Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
@@ -33,7 +31,7 @@ pub async fn state_stats(
|
|||||||
)),
|
)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = %err, "failed to read state stats");
|
warn!(error = %err, "failed to read state stats");
|
||||||
Err(json_error(
|
Err(ApiError::new(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"state_stats_failed",
|
"state_stats_failed",
|
||||||
&err.to_string(),
|
&err.to_string(),
|
||||||
|
|||||||
@@ -17,18 +17,55 @@ pub use control::{
|
|||||||
revoke_agent, revoke_enroll_token, set_agent_nickname, state_stats, wake_fleet_device,
|
revoke_agent, revoke_enroll_token, set_agent_nickname, state_stats, wake_fleet_device,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn json_error(
|
use axum::response::{IntoResponse, Response};
|
||||||
status: StatusCode,
|
use serde::{Deserialize, Serialize};
|
||||||
code: &str,
|
|
||||||
message: &str,
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
) -> (StatusCode, Json<serde_json::Value>) {
|
pub struct ApiErrorResponse {
|
||||||
(
|
pub error: ApiErrorDetail,
|
||||||
status,
|
}
|
||||||
Json(serde_json::json!({
|
|
||||||
"error": {
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
"code": code,
|
pub struct ApiErrorDetail {
|
||||||
"message": message,
|
pub code: String,
|
||||||
}
|
pub message: String,
|
||||||
})),
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
)
|
pub details: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiError {
|
||||||
|
pub status: StatusCode,
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub details: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiError {
|
||||||
|
pub fn new(status: StatusCode, code: impl Into<String>, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
code: code.into(),
|
||||||
|
message: message.into(),
|
||||||
|
details: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_details(mut self, details: serde_json::Value) -> Self {
|
||||||
|
self.details = Some(details);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let body = Json(ApiErrorResponse {
|
||||||
|
error: ApiErrorDetail {
|
||||||
|
code: self.code,
|
||||||
|
message: self.message,
|
||||||
|
details: self.details,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
(self.status, body).into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ impl DaemonConfig {
|
|||||||
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(2_592_000)
|
||||||
|
.max(1),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pid_file_raw = args
|
let pid_file_raw = args
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ mod tests {
|
|||||||
use super::helpers::test_helpers::TestStore;
|
use super::helpers::test_helpers::TestStore;
|
||||||
|
|
||||||
async fn insert_token(store: &Store, token: &str, expires_at_unix: u64) {
|
async fn insert_token(store: &Store, token: &str, expires_at_unix: u64) {
|
||||||
sqlx::query(
|
let expires = expires_at_unix as i64;
|
||||||
|
sqlx::query!(
|
||||||
"INSERT OR REPLACE INTO enroll_tokens (token, expires_at_unix) VALUES (?1, ?2)",
|
"INSERT OR REPLACE INTO enroll_tokens (token, expires_at_unix) VALUES (?1, ?2)",
|
||||||
|
token,
|
||||||
|
expires
|
||||||
)
|
)
|
||||||
.bind(token)
|
|
||||||
.bind(expires_at_unix as i64)
|
|
||||||
.execute(&store.pool)
|
.execute(&store.pool)
|
||||||
.await
|
.await
|
||||||
.expect("insert should succeed");
|
.expect("insert should succeed");
|
||||||
@@ -82,12 +83,13 @@ mod tests {
|
|||||||
.expect("gc should succeed");
|
.expect("gc should succeed");
|
||||||
|
|
||||||
assert_eq!(removed, 1);
|
assert_eq!(removed, 1);
|
||||||
let exists =
|
let exists = sqlx::query_scalar!(
|
||||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1")
|
"SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1",
|
||||||
.bind("enr-expired-gc-test")
|
"enr-expired-gc-test"
|
||||||
.fetch_one(&ts.store().pool)
|
)
|
||||||
.await
|
.fetch_one(&ts.store().pool)
|
||||||
.expect("read should succeed");
|
.await
|
||||||
|
.expect("read should succeed");
|
||||||
assert_eq!(exists, 0);
|
assert_eq!(exists, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +105,13 @@ mod tests {
|
|||||||
.expect_err("expired token should be rejected");
|
.expect_err("expired token should be rejected");
|
||||||
|
|
||||||
assert!(err.to_string().contains("expired"));
|
assert!(err.to_string().contains("expired"));
|
||||||
let exists =
|
let exists = sqlx::query_scalar!(
|
||||||
sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1")
|
"SELECT COUNT(*) FROM enroll_tokens WHERE token = ?1",
|
||||||
.bind("enr-expired-enroll-test")
|
"enr-expired-enroll-test"
|
||||||
.fetch_one(&ts.store().pool)
|
)
|
||||||
.await
|
.fetch_one(&ts.store().pool)
|
||||||
.expect("read should succeed");
|
.await
|
||||||
|
.expect("read should succeed");
|
||||||
assert_eq!(exists, 0);
|
assert_eq!(exists, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ impl Store {
|
|||||||
let snapshot_time = now_unix();
|
let snapshot_time = now_unix();
|
||||||
let snapshot_time_i64 = i64::try_from(snapshot_time).context("snapshot time overflow")?;
|
let snapshot_time_i64 = i64::try_from(snapshot_time).context("snapshot time overflow")?;
|
||||||
|
|
||||||
let existing_keys: Vec<String> =
|
let existing_keys: Vec<String> = sqlx::query_scalar!(
|
||||||
sqlx::query_scalar("SELECT device_key FROM agent_devices WHERE agent_id = ?1")
|
"SELECT device_key FROM agent_devices WHERE agent_id = ?1",
|
||||||
.bind(agent_id)
|
agent_id
|
||||||
.fetch_all(&mut *tx)
|
)
|
||||||
.await
|
.fetch_all(&mut *tx)
|
||||||
.context("failed fetching existing keys")?;
|
.await
|
||||||
|
.context("failed fetching existing keys")?;
|
||||||
|
|
||||||
for device in devices {
|
for device in devices {
|
||||||
let Some(device_id) = &device.id else {
|
let Some(device_id) = &device.id else {
|
||||||
@@ -47,6 +48,7 @@ impl Store {
|
|||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
ON CONFLICT (agent_id, device_key) DO UPDATE SET
|
ON CONFLICT (agent_id, device_key) DO UPDATE SET
|
||||||
presence = excluded.presence,
|
presence = excluded.presence,
|
||||||
|
display_name = excluded.display_name,
|
||||||
last_seen_unix = excluded.last_seen_unix",
|
last_seen_unix = excluded.last_seen_unix",
|
||||||
agent_id,
|
agent_id,
|
||||||
device_key,
|
device_key,
|
||||||
@@ -69,17 +71,20 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed deleting device macs")?;
|
.context("failed deleting device macs")?;
|
||||||
|
|
||||||
for mac in &device.macs {
|
if !device.macs.is_empty() {
|
||||||
let mac_str = mac.to_string().to_ascii_lowercase();
|
let mut builder = sqlx::QueryBuilder::new(
|
||||||
sqlx::query!(
|
"INSERT INTO agent_device_macs (agent_id, device_key, mac) ",
|
||||||
"INSERT INTO agent_device_macs (agent_id, device_key, mac) VALUES (?1, ?2, ?3)",
|
);
|
||||||
agent_id,
|
builder.push_values(&device.macs, |mut b, mac| {
|
||||||
device_key,
|
b.push_bind(agent_id)
|
||||||
mac_str
|
.push_bind(device_key.clone())
|
||||||
)
|
.push_bind(mac.to_string().to_ascii_lowercase());
|
||||||
.execute(&mut *tx)
|
});
|
||||||
.await
|
builder
|
||||||
.context("failed inserting device mac")?;
|
.build()
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed inserting device macs")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -91,17 +96,20 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed deleting device ips")?;
|
.context("failed deleting device ips")?;
|
||||||
|
|
||||||
for ip in &device.ips {
|
if !device.ips.is_empty() {
|
||||||
let ip_str = ip.to_string();
|
let mut builder = sqlx::QueryBuilder::new(
|
||||||
sqlx::query!(
|
"INSERT INTO agent_device_ips (agent_id, device_key, ip) ",
|
||||||
"INSERT INTO agent_device_ips (agent_id, device_key, ip) VALUES (?1, ?2, ?3)",
|
);
|
||||||
agent_id,
|
builder.push_values(&device.ips, |mut b, ip| {
|
||||||
device_key,
|
b.push_bind(agent_id)
|
||||||
ip_str
|
.push_bind(device_key.clone())
|
||||||
)
|
.push_bind(ip.to_string());
|
||||||
.execute(&mut *tx)
|
});
|
||||||
.await
|
builder
|
||||||
.context("failed inserting device ip")?;
|
.build()
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed inserting device ips")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -113,16 +121,20 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed deleting device hostnames")?;
|
.context("failed deleting device hostnames")?;
|
||||||
|
|
||||||
for hostname in &device.names {
|
if !device.names.is_empty() {
|
||||||
sqlx::query!(
|
let mut builder = sqlx::QueryBuilder::new(
|
||||||
"INSERT INTO agent_device_hostnames (agent_id, device_key, hostname) VALUES (?1, ?2, ?3)",
|
"INSERT INTO agent_device_hostnames (agent_id, device_key, hostname) ",
|
||||||
agent_id,
|
);
|
||||||
device_key,
|
builder.push_values(&device.names, |mut b, hostname| {
|
||||||
hostname
|
b.push_bind(agent_id)
|
||||||
)
|
.push_bind(device_key.clone())
|
||||||
.execute(&mut *tx)
|
.push_bind(hostname);
|
||||||
.await
|
});
|
||||||
.context("failed inserting device hostname")?;
|
builder
|
||||||
|
.build()
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed inserting device hostnames")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@@ -134,18 +146,24 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed deleting device facts")?;
|
.context("failed deleting device facts")?;
|
||||||
|
|
||||||
for observation in &device.observations {
|
if !device.observations.is_empty() {
|
||||||
let fact_json =
|
let mut facts_json = Vec::with_capacity(device.observations.len());
|
||||||
serde_json::to_string(observation).context("failed serializing fact")?;
|
for obs in &device.observations {
|
||||||
sqlx::query!(
|
facts_json.push(serde_json::to_string(obs).context("failed serializing fact")?);
|
||||||
"INSERT INTO agent_device_facts (agent_id, device_key, fact_json) VALUES (?1, ?2, ?3)",
|
}
|
||||||
agent_id,
|
let mut builder = sqlx::QueryBuilder::new(
|
||||||
device_key,
|
"INSERT INTO agent_device_facts (agent_id, device_key, fact_json) ",
|
||||||
fact_json
|
);
|
||||||
)
|
builder.push_values(facts_json, |mut b, fact| {
|
||||||
.execute(&mut *tx)
|
b.push_bind(agent_id)
|
||||||
.await
|
.push_bind(device_key.clone())
|
||||||
.context("failed inserting device fact")?;
|
.push_bind(fact);
|
||||||
|
});
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed inserting device facts")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,13 +352,25 @@ fn assemble_device_rows(
|
|||||||
.get(&key)
|
.get(&key)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|row| macaddr::MacAddr::try_from(*row).ok())
|
.filter_map(|row| match macaddr::MacAddr::try_from(*row) {
|
||||||
|
Ok(mac) => Some(mac),
|
||||||
|
Err(e) => {
|
||||||
|
::tracing::warn!(error = %e, agent_id = %device.agent_id, device_key = %device.device_key, raw_mac = %row.mac, "failed to parse mac from agent_device_macs row");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let ips: Vec<std::net::IpAddr> = ip_map
|
let ips: Vec<std::net::IpAddr> = ip_map
|
||||||
.get(&key)
|
.get(&key)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|row| std::net::IpAddr::try_from(*row).ok())
|
.filter_map(|row| match std::net::IpAddr::try_from(*row) {
|
||||||
|
Ok(ip) => Some(ip),
|
||||||
|
Err(e) => {
|
||||||
|
::tracing::warn!(error = %e, agent_id = %device.agent_id, device_key = %device.device_key, raw_ip = %row.ip, "failed to parse ip from agent_device_ips row");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let hostnames = hostname_map.get(&key).cloned().unwrap_or_default();
|
let hostnames = hostname_map.get(&key).cloned().unwrap_or_default();
|
||||||
let facts = fact_map.get(&key).cloned().unwrap_or_default();
|
let facts = fact_map.get(&key).cloned().unwrap_or_default();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub async fn open_sqlite_pool(path: &Path) -> Result<SqlitePool> {
|
|||||||
.with_context(|| format!("failed to open SQLite state db {}", path.display()))
|
.with_context(|| format!("failed to open SQLite state db {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sql_count(pool: &SqlitePool, table: &str) -> Result<i64> {
|
pub async fn sql_count(pool: &SqlitePool, table: &'static str) -> Result<i64> {
|
||||||
let sql = format!("SELECT COUNT(*) FROM {table}");
|
let sql = format!("SELECT COUNT(*) FROM {table}");
|
||||||
sqlx::query_scalar::<_, i64>(&sql)
|
sqlx::query_scalar::<_, i64>(&sql)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ impl TestStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
impl Drop for TestStore {
|
impl Drop for TestStore {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.store.take();
|
self.store.take();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::parse::mac;
|
|||||||
/// Variant order defines `Ord`: Offline < Unknown < LikelyOnline < Online.
|
/// Variant order defines `Ord`: Offline < Unknown < LikelyOnline < Online.
|
||||||
/// `std::cmp::max` picks the most-online signal when merging.
|
/// `std::cmp::max` picks the most-online signal when merging.
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, PartialEq, Eq, Clone, Copy, Hash, Ord, PartialOrd, Serialize, serde::Deserialize, Default,
|
Debug, PartialEq, Eq, Clone, Copy, Hash, Ord, PartialOrd, Serialize, Deserialize, Default,
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Presence {
|
pub enum Presence {
|
||||||
@@ -143,11 +143,7 @@ impl Device {
|
|||||||
if let Some(dev) = neighbor.dev.as_deref() {
|
if let Some(dev) = neighbor.dev.as_deref() {
|
||||||
interfaces.insert(dev);
|
interfaces.insert(dev);
|
||||||
}
|
}
|
||||||
presence = std::cmp::max(
|
presence = std::cmp::max(presence, Presence::from(neighbor.state));
|
||||||
presence_rank(presence),
|
|
||||||
presence_rank(neighbor.state.into()),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
}
|
||||||
let mut observed_non_remove = false;
|
let mut observed_non_remove = false;
|
||||||
for observation in &observations {
|
for observation in &observations {
|
||||||
@@ -163,11 +159,7 @@ impl Device {
|
|||||||
if observation.action != "remove" {
|
if observation.action != "remove" {
|
||||||
observed_non_remove = true;
|
observed_non_remove = true;
|
||||||
}
|
}
|
||||||
presence = std::cmp::max(
|
presence = std::cmp::max(presence, observation_presence(observation));
|
||||||
presence_rank(presence),
|
|
||||||
presence_rank(observation_presence(observation)),
|
|
||||||
)
|
|
||||||
.into();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if neighbors.is_empty()
|
if neighbors.is_empty()
|
||||||
@@ -207,26 +199,6 @@ fn observation_presence(observation: &DeviceObservationFact) -> Presence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn presence_rank(presence: Presence) -> u8 {
|
|
||||||
match presence {
|
|
||||||
Presence::Online => 3,
|
|
||||||
Presence::LikelyOnline => 2,
|
|
||||||
Presence::Unknown => 1,
|
|
||||||
Presence::Offline => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for Presence {
|
|
||||||
fn from(value: u8) -> Self {
|
|
||||||
match value {
|
|
||||||
3 => Self::Online,
|
|
||||||
2 => Self::LikelyOnline,
|
|
||||||
0 => Self::Offline,
|
|
||||||
_ => Self::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collection of merged discovered devices.
|
/// Collection of merged discovered devices.
|
||||||
#[derive(Debug, Default, Clone, Serialize)]
|
#[derive(Debug, Default, Clone, Serialize)]
|
||||||
pub struct DeviceInventory {
|
pub struct DeviceInventory {
|
||||||
|
|||||||
@@ -138,21 +138,29 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_online_neighbor() {
|
fn deserialize_online_neighbor() {
|
||||||
NeighborEntry::deserialize(serde_json::json!({
|
let entry = NeighborEntry::deserialize(serde_json::json!({
|
||||||
"ip" : "192.168.100.94",
|
"ip" : "192.168.100.94",
|
||||||
"dev" : "br-lan",
|
"dev" : "br-lan",
|
||||||
"mac" : "04:7C:16:79:6D:EE",
|
"mac" : "04:7C:16:79:6D:EE",
|
||||||
"state" : "REACHABLE"
|
"state" : "REACHABLE"
|
||||||
}))
|
}))
|
||||||
.expect("all fields must pass");
|
.expect("all fields must pass");
|
||||||
|
assert_eq!(entry.ip, IpAddr::from([192, 168, 100, 94]));
|
||||||
|
assert_eq!(entry.dev, Some("br-lan".to_string()));
|
||||||
|
assert_eq!(entry.mac, Some("04:7c:16:79:6d:ee".parse().expect("mac")));
|
||||||
|
assert_eq!(entry.state, NeighborState::Reachable);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_failed_neighbor() {
|
fn deserialize_failed_neighbor() {
|
||||||
NeighborEntry::deserialize(serde_json::json!({
|
let entry = NeighborEntry::deserialize(serde_json::json!({
|
||||||
"ip" : "192.168.100.94",
|
"ip" : "192.168.100.94",
|
||||||
"dev" : "br-lan",
|
"dev" : "br-lan",
|
||||||
"state" : "FAILED"
|
"state" : "FAILED"
|
||||||
}))
|
}))
|
||||||
.expect("default");
|
.expect("default");
|
||||||
|
assert_eq!(entry.ip, IpAddr::from([192, 168, 100, 94]));
|
||||||
|
assert_eq!(entry.dev, Some("br-lan".to_string()));
|
||||||
|
assert_eq!(entry.mac, None);
|
||||||
|
assert_eq!(entry.state, NeighborState::Failed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user