device page, fmt;
This commit is contained in:
@@ -85,10 +85,7 @@ pub async fn get_with_handle(
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(AddrInfo {
|
.push(AddrInfo {
|
||||||
family,
|
family,
|
||||||
cidr: InterfaceCidr {
|
cidr: InterfaceCidr { local, prefixlen },
|
||||||
local,
|
|
||||||
prefixlen,
|
|
||||||
},
|
|
||||||
broadcast,
|
broadcast,
|
||||||
scope,
|
scope,
|
||||||
label,
|
label,
|
||||||
|
|||||||
+10
-7
@@ -25,12 +25,7 @@ pub fn render_leases_table(leases: &[DhcpLeaseWithState]) -> Table {
|
|||||||
Cell::new(lease.lease_line.ip.to_string()),
|
Cell::new(lease.lease_line.ip.to_string()),
|
||||||
Cell::new(lease.lease_line.mac.to_string()),
|
Cell::new(lease.lease_line.mac.to_string()),
|
||||||
Cell::new(lease.lease_line.name.clone().unwrap_or_default()),
|
Cell::new(lease.lease_line.name.clone().unwrap_or_default()),
|
||||||
Cell::new(
|
Cell::new(lease.nud_state.map(|v| v.to_string()).unwrap_or_default()),
|
||||||
lease
|
|
||||||
.nud_state
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
table
|
table
|
||||||
@@ -51,7 +46,15 @@ pub fn render_wake_table(result: &WakeResult) -> Table {
|
|||||||
|
|
||||||
pub fn render_devs_table(devs: &[InterfaceSummary]) -> Table {
|
pub fn render_devs_table(devs: &[InterfaceSummary]) -> Table {
|
||||||
let mut table = base_table();
|
let mut table = base_table();
|
||||||
table.set_header(vec!["Ifname", "State", "MAC", "Family", "CIDR", "Broadcast", "Scope/Label"]);
|
table.set_header(vec![
|
||||||
|
"Ifname",
|
||||||
|
"State",
|
||||||
|
"MAC",
|
||||||
|
"Family",
|
||||||
|
"CIDR",
|
||||||
|
"Broadcast",
|
||||||
|
"Scope/Label",
|
||||||
|
]);
|
||||||
|
|
||||||
for dev in devs {
|
for dev in devs {
|
||||||
if dev.addrs.is_empty() {
|
if dev.addrs.is_empty() {
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
|||||||
.iter()
|
.iter()
|
||||||
.flat_map(device_to_status_rows)
|
.flat_map(device_to_status_rows)
|
||||||
.collect();
|
.collect();
|
||||||
debug!(rows = table.len(), devices = inventory.devices.len(), "built status response");
|
debug!(
|
||||||
|
rows = table.len(),
|
||||||
|
devices = inventory.devices.len(),
|
||||||
|
"built status response"
|
||||||
|
);
|
||||||
Ok(Status {
|
Ok(Status {
|
||||||
name: query.name,
|
name: query.name,
|
||||||
table,
|
table,
|
||||||
|
|||||||
+5
-1
@@ -89,7 +89,11 @@ fn broadcast_wake_targets_from_interfaces(
|
|||||||
anyhow::bail!("no broadcast-capable interfaces found");
|
anyhow::bail!("no broadcast-capable interfaces found");
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(targets = targets.len(), interfaces = interfaces.len(), "built broadcast wake targets");
|
debug!(
|
||||||
|
targets = targets.len(),
|
||||||
|
interfaces = interfaces.len(),
|
||||||
|
"built broadcast wake targets"
|
||||||
|
);
|
||||||
Ok(targets)
|
Ok(targets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ function chooseWakeTarget(device: DeviceRow): string {
|
|||||||
: (device.macs[0] || device.ips[0] || "");
|
: (device.macs[0] || device.ips[0] || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function summarize(values: string[]): string {
|
||||||
|
if (!values.length) return "-";
|
||||||
|
if (values.length === 1) return values[0];
|
||||||
|
return `${values[0]} (+${values.length - 1})`;
|
||||||
|
}
|
||||||
|
|
||||||
function loadHistory(): WakeEvent[] {
|
function loadHistory(): WakeEvent[] {
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(WAKE_HISTORY_KEY);
|
const raw = window.localStorage.getItem(WAKE_HISTORY_KEY);
|
||||||
@@ -337,46 +343,42 @@ export function DevicesPage({ agents, selectedAgentId, onSelectAgent, onAfterWak
|
|||||||
<p className="muted">Showing {filtered.length} of {rows.length}</p>
|
<p className="muted">Showing {filtered.length} of {rows.length}</p>
|
||||||
{error && <pre className="error">{error}</pre>}
|
{error && <pre className="error">{error}</pre>}
|
||||||
<div className="list device-list">
|
<div className="list device-list">
|
||||||
<div className="row plain device-row device-header" style={{ fontWeight: 600 }}>
|
<div className="row plain device-row device-header">
|
||||||
<span
|
<span
|
||||||
className="sortable-col"
|
className="sortable-col device-cell"
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => setSort((s) => ({ key: "name", dir: s.key === "name" && s.dir === "asc" ? "desc" : "asc" }))}
|
onClick={() => setSort((s) => ({ key: "name", dir: s.key === "name" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||||
>
|
>
|
||||||
Name {sort.key === "name" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
Name {sort.key === "name" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="sortable-col"
|
className="sortable-col device-cell"
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => setSort((s) => ({ key: "ip", dir: s.key === "ip" && s.dir === "asc" ? "desc" : "asc" }))}
|
onClick={() => setSort((s) => ({ key: "ip", dir: s.key === "ip" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||||
>
|
>
|
||||||
IP {sort.key === "ip" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
IP {sort.key === "ip" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="sortable-col"
|
className="sortable-col device-cell"
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => setSort((s) => ({ key: "mac", dir: s.key === "mac" && s.dir === "asc" ? "desc" : "asc" }))}
|
onClick={() => setSort((s) => ({ key: "mac", dir: s.key === "mac" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||||
>
|
>
|
||||||
MAC {sort.key === "mac" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
MAC {sort.key === "mac" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="sortable-col"
|
className="sortable-col device-cell"
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={() => setSort((s) => ({ key: "presence", dir: s.key === "presence" && s.dir === "asc" ? "desc" : "asc" }))}
|
onClick={() => setSort((s) => ({ key: "presence", dir: s.key === "presence" && s.dir === "asc" ? "desc" : "asc" }))}
|
||||||
>
|
>
|
||||||
Presence {sort.key === "presence" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
Presence {sort.key === "presence" ? (sort.dir === "asc" ? "▲" : "▼") : ""}
|
||||||
</span>
|
</span>
|
||||||
<span>Interfaces</span>
|
<span className="device-cell">Interfaces</span>
|
||||||
<span></span>
|
<span className="device-cell device-action"></span>
|
||||||
</div>
|
</div>
|
||||||
{filtered.map((row) => (
|
{filtered.map((row) => (
|
||||||
<div className="row plain device-row" key={row.id}>
|
<div className="row plain device-row" key={row.id}>
|
||||||
<span>{row.name}</span>
|
<span className="device-cell" data-label="Name" title={row.name}>{row.name}</span>
|
||||||
<span className="muted">{row.ips.join(", ") || "-"}</span>
|
<span className="device-cell muted" data-label="IP" title={row.ips.join(", ") || "-"}>{summarize(row.ips)}</span>
|
||||||
<span className="muted">{row.macs.join(", ") || "-"}</span>
|
<span className="device-cell muted" data-label="MAC" title={row.macs.join(", ") || "-"}>{summarize(row.macs)}</span>
|
||||||
<span className="pill">{row.presence}</span>
|
<span className="device-cell" data-label="Presence"><span className="pill">{row.presence}</span></span>
|
||||||
<span>{row.interfaces.join(", ") || "-"}</span>
|
<span className="device-cell" data-label="Interfaces" title={row.interfaces.join(", ") || "-"}>{summarize(row.interfaces)}</span>
|
||||||
<span>
|
<span className="device-cell device-action" data-label="">
|
||||||
<button onClick={() => void wakeDevice(row)} disabled={wakeBusyId === row.id || !selectedAgentId}>
|
<button onClick={() => void wakeDevice(row)} disabled={wakeBusyId === row.id || !selectedAgentId}>
|
||||||
{wakeBusyId === row.id ? "Waking..." : "Wake"}
|
{wakeBusyId === row.id ? "Waking..." : "Wake"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+79
-21
@@ -168,33 +168,91 @@ input, select { width: 100%; padding: 0.45rem 0.55rem; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-row {
|
.device-row {
|
||||||
align-items: stretch;
|
display: grid;
|
||||||
}
|
grid-template-columns: minmax(110px, 1.1fr) minmax(160px, 2fr) minmax(130px, 1.5fr) minmax(110px, 0.9fr) minmax(120px, 1fr) auto;
|
||||||
|
|
||||||
.device-main {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-main strong {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.55rem;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
justify-content: flex-end;
|
|
||||||
|
.device-header {
|
||||||
|
font-weight: 600;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #091429;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-cell {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-col {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-action {
|
||||||
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.device-row {
|
.device-header {
|
||||||
flex-direction: column;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-meta {
|
.device-row {
|
||||||
justify-content: flex-start;
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"name action"
|
||||||
|
"presence presence"
|
||||||
|
"ip ip"
|
||||||
|
"mac mac"
|
||||||
|
"interfaces interfaces";
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label]::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label="Name"] {
|
||||||
|
grid-area: name;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label="Presence"] {
|
||||||
|
grid-area: presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label="IP"] {
|
||||||
|
grid-area: ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label="MAC"] {
|
||||||
|
grid-area: mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-cell[data-label="Interfaces"] {
|
||||||
|
grid-area: interfaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row .device-action {
|
||||||
|
grid-area: action;
|
||||||
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-wake {
|
.quick-wake {
|
||||||
|
|||||||
@@ -40,26 +40,40 @@ async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> {
|
|||||||
include_state: req.include_state,
|
include_state: req.include_state,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
debug!(rows = leases.len(), include_state = req.include_state, "dispatched leases command");
|
debug!(
|
||||||
|
rows = leases.len(),
|
||||||
|
include_state = req.include_state,
|
||||||
|
"dispatched leases command"
|
||||||
|
);
|
||||||
Ok(CommandResult::Leases { rows: leases })
|
Ok(CommandResult::Leases { rows: leases })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
||||||
let mut devs = if let Some(name) = &req.dev {
|
let mut devs = if let Some(name) = &req.dev {
|
||||||
wakey::get_interface_summary(name).await?.into_iter().collect()
|
wakey::get_interface_summary(name)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
wakey::get_interface_summaries().await?
|
wakey::get_interface_summaries().await?
|
||||||
};
|
};
|
||||||
if req.up_only {
|
if req.up_only {
|
||||||
devs.retain(|dev| dev.operstate == "up");
|
devs.retain(|dev| dev.operstate == "up");
|
||||||
}
|
}
|
||||||
debug!(rows = devs.len(), up_only = req.up_only, "dispatched devs command");
|
debug!(
|
||||||
|
rows = devs.len(),
|
||||||
|
up_only = req.up_only,
|
||||||
|
"dispatched devs command"
|
||||||
|
);
|
||||||
Ok(CommandResult::Devs { rows: devs })
|
Ok(CommandResult::Devs { rows: devs })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
||||||
let inventory = wakey::inventory(req.into_device_query()).await?;
|
let inventory = wakey::inventory(req.into_device_query()).await?;
|
||||||
debug!(rows = inventory.devices.len(), "dispatched inventory command");
|
debug!(
|
||||||
|
rows = inventory.devices.len(),
|
||||||
|
"dispatched inventory command"
|
||||||
|
);
|
||||||
Ok(CommandResult::Inventory(inventory))
|
Ok(CommandResult::Inventory(inventory))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ struct EnrollResponse {
|
|||||||
server_url: Option<String>,
|
server_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) -> Result<AgentConfig> {
|
pub async fn enroll(
|
||||||
|
server_url: &str,
|
||||||
|
enroll_token: &str,
|
||||||
|
config_path: &Path,
|
||||||
|
) -> Result<AgentConfig> {
|
||||||
let server_url = normalize_server_url(server_url);
|
let server_url = normalize_server_url(server_url);
|
||||||
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
let endpoint = format!("{server_url}/api/v1/agents/enroll");
|
||||||
info!(endpoint = %endpoint, config_path = %config_path.display(), "starting agent enrollment");
|
info!(endpoint = %endpoint, config_path = %config_path.display(), "starting agent enrollment");
|
||||||
@@ -68,7 +72,10 @@ mod tests {
|
|||||||
use std::net::{SocketAddr, TcpListener};
|
use std::net::{SocketAddr, TcpListener};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
fn spawn_enroll_server(response_body: &'static str, status: &'static str) -> (String, thread::JoinHandle<()>) {
|
fn spawn_enroll_server(
|
||||||
|
response_body: &'static str,
|
||||||
|
status: &'static str,
|
||||||
|
) -> (String, thread::JoinHandle<()>) {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test listener");
|
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test listener");
|
||||||
let addr: SocketAddr = listener.local_addr().expect("local addr");
|
let addr: SocketAddr = listener.local_addr().expect("local addr");
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
@@ -103,7 +110,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_server_url_trims_slash() {
|
fn normalize_server_url_trims_slash() {
|
||||||
assert_eq!(normalize_server_url("https://example.com/"), "https://example.com");
|
assert_eq!(
|
||||||
|
normalize_server_url("https://example.com/"),
|
||||||
|
"https://example.com"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ mod config;
|
|||||||
mod dispatch;
|
mod dispatch;
|
||||||
mod enroll;
|
mod enroll;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod session;
|
|
||||||
mod serve;
|
mod serve;
|
||||||
|
mod session;
|
||||||
mod tracing;
|
mod tracing;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -29,7 +29,9 @@ async fn main() -> Result<()> {
|
|||||||
if args.reload_running {
|
if args.reload_running {
|
||||||
match serve::reload_daemon(&args.pid_file) {
|
match serve::reload_daemon(&args.pid_file) {
|
||||||
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
|
Ok(()) => println!("reload=signaled pid_file={}", args.pid_file.display()),
|
||||||
Err(err) => ::tracing::warn!(error = %err, pid_file = %args.pid_file.display(), "enroll completed but daemon reload failed"),
|
Err(err) => {
|
||||||
|
::tracing::warn!(error = %err, pid_file = %args.pid_file.display(), "enroll completed but daemon reload failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,9 @@ fn init_config(args: InitConfigArgs) -> Result<()> {
|
|||||||
server_url: args
|
server_url: args
|
||||||
.server_url
|
.server_url
|
||||||
.unwrap_or_else(|| "https://control-plane.example.com".to_string()),
|
.unwrap_or_else(|| "https://control-plane.example.com".to_string()),
|
||||||
agent_id: args.agent_id.unwrap_or_else(|| "REPLACE_ME_AGENT_ID".to_string()),
|
agent_id: args
|
||||||
|
.agent_id
|
||||||
|
.unwrap_or_else(|| "REPLACE_ME_AGENT_ID".to_string()),
|
||||||
agent_token: args
|
agent_token: args
|
||||||
.agent_token
|
.agent_token
|
||||||
.unwrap_or_else(|| "REPLACE_ME_AGENT_TOKEN".to_string()),
|
.unwrap_or_else(|| "REPLACE_ME_AGENT_TOKEN".to_string()),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use macaddr::MacAddr;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use wakey_core::{
|
|
||||||
DeviceFilters, DeviceInventory, DeviceQuery, DhcpLeaseWithState, InterfaceSummary, NeighborEntry,
|
|
||||||
Status, WakeResult,
|
|
||||||
};
|
|
||||||
use wakey_core::parse::mac;
|
use wakey_core::parse::mac;
|
||||||
|
use wakey_core::{
|
||||||
|
DeviceFilters, DeviceInventory, DeviceQuery, DhcpLeaseWithState, InterfaceSummary,
|
||||||
|
NeighborEntry, Status, WakeResult,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RequestId(String);
|
pub struct RequestId(String);
|
||||||
|
|||||||
@@ -67,9 +67,12 @@ async fn run_once(config: &AgentConfig) -> Result<()> {
|
|||||||
}
|
}
|
||||||
let (mut sink, mut source) = stream.split();
|
let (mut sink, mut source) = stream.split();
|
||||||
|
|
||||||
send_json(&mut sink, &ClientMessage::Hello {
|
send_json(
|
||||||
|
&mut sink,
|
||||||
|
&ClientMessage::Hello {
|
||||||
agent_id: config.agent_id.clone(),
|
agent_id: config.agent_id.clone(),
|
||||||
})
|
},
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
send_json(
|
send_json(
|
||||||
&mut sink,
|
&mut sink,
|
||||||
@@ -223,7 +226,8 @@ where
|
|||||||
S: SinkExt<Message> + Unpin,
|
S: SinkExt<Message> + Unpin,
|
||||||
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
|
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let payload = serde_json::to_string(message).context("failed to serialize websocket message")?;
|
let payload =
|
||||||
|
serde_json::to_string(message).context("failed to serialize websocket message")?;
|
||||||
debug!(message_type = %client_message_kind(message), "sending websocket message");
|
debug!(message_type = %client_message_kind(message), "sending websocket message");
|
||||||
sink.send(Message::Text(payload))
|
sink.send(Message::Text(payload))
|
||||||
.await
|
.await
|
||||||
@@ -318,7 +322,10 @@ mod tests {
|
|||||||
impl Sink<Message> for RecordingSink {
|
impl Sink<Message> for RecordingSink {
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
||||||
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<(), Self::Error>> {
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,11 +338,17 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_flush(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<(), Self::Error>> {
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_close(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Result<(), Self::Error>> {
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,10 @@ pub async fn run_command(
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
state.pending.lock().await.remove(&request_id_string);
|
state.pending.lock().await.remove(&request_id_string);
|
||||||
warn!(timeout_ms = timeout.as_millis() as u64, "agent command timed out");
|
warn!(
|
||||||
|
timeout_ms = timeout.as_millis() as u64,
|
||||||
|
"agent command timed out"
|
||||||
|
);
|
||||||
if let Err(err) = state
|
if let Err(err) = state
|
||||||
.store
|
.store
|
||||||
.append_audit_event(AuditEventInput {
|
.append_audit_event(AuditEventInput {
|
||||||
|
|||||||
@@ -242,7 +242,11 @@ pub async fn revoke_enroll_token(
|
|||||||
agent_id: None,
|
agent_id: None,
|
||||||
request_id: None,
|
request_id: None,
|
||||||
event_type: "enroll_token_revoke".into(),
|
event_type: "enroll_token_revoke".into(),
|
||||||
outcome: if revoked { "ok".into() } else { "not_found".into() },
|
outcome: if revoked {
|
||||||
|
"ok".into()
|
||||||
|
} else {
|
||||||
|
"not_found".into()
|
||||||
|
},
|
||||||
latency_ms: None,
|
latency_ms: None,
|
||||||
message: if revoked {
|
message: if revoked {
|
||||||
"revoked enroll token".into()
|
"revoked enroll token".into()
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
mod alerts;
|
||||||
|
mod audit;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod control;
|
mod control;
|
||||||
mod audit;
|
|
||||||
mod alerts;
|
|
||||||
|
|
||||||
pub use commands::{list_agents, run_command};
|
|
||||||
pub use alerts::{active_alerts, alert_history, alerts_stream};
|
pub use alerts::{active_alerts, alert_history, alerts_stream};
|
||||||
pub use audit::list_audit_events;
|
pub use audit::list_audit_events;
|
||||||
|
pub use commands::{list_agents, run_command};
|
||||||
pub use control::{
|
pub use control::{
|
||||||
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeEnrollTokenResponse, StateStatsResponse,
|
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeEnrollTokenResponse, StateStatsResponse,
|
||||||
enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,
|
enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::cli::{IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeEnrollTokenArgs, StateStatsArgs};
|
use crate::cli::{
|
||||||
|
IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeEnrollTokenArgs, StateStatsArgs,
|
||||||
|
};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
|
|
||||||
@@ -13,7 +15,10 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
if let Some(url) = args.public_url {
|
if let Some(url) = args.public_url {
|
||||||
let base = config::normalize_public_url(&url);
|
let base = config::normalize_public_url(&url);
|
||||||
let ttl_seconds = settings.ttl.as_secs().max(1);
|
let ttl_seconds = settings.ttl.as_secs().max(1);
|
||||||
let endpoint = format!("{}?ttl_seconds={ttl_seconds}", config::issue_token_endpoint(&base));
|
let endpoint = format!(
|
||||||
|
"{}?ttl_seconds={ttl_seconds}",
|
||||||
|
config::issue_token_endpoint(&base)
|
||||||
|
);
|
||||||
tracing::info!(endpoint = %endpoint, "requesting live enroll token from running control-plane daemon");
|
tracing::info!(endpoint = %endpoint, "requesting live enroll token from running control-plane daemon");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
@@ -50,7 +55,12 @@ pub async fn issue_enroll_token(args: IssueEnrollTokenArgs) -> Result<()> {
|
|||||||
tracing::info!(data_dir = %settings.data_dir.display(), state_file = %settings.state_file.display(), ttl_seconds = settings.ttl.as_secs(), "issuing enroll token via offline state file fallback");
|
tracing::info!(data_dir = %settings.data_dir.display(), state_file = %settings.state_file.display(), ttl_seconds = settings.ttl.as_secs(), "issuing enroll token via offline state file fallback");
|
||||||
let store = state::Store::load_or_init(&settings.state_file, args.enroll_tokens, settings.ttl)
|
let store = state::Store::load_or_init(&settings.state_file, args.enroll_tokens, settings.ttl)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to initialize store {}",
|
||||||
|
settings.state_file.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let issued = store.issue_enroll_token(settings.ttl).await?;
|
let issued = store.issue_enroll_token(settings.ttl).await?;
|
||||||
println!("enroll_token={}", issued.enroll_token);
|
println!("enroll_token={}", issued.enroll_token);
|
||||||
println!("expires_at_unix={}", issued.expires_at_unix);
|
println!("expires_at_unix={}", issued.expires_at_unix);
|
||||||
@@ -66,8 +76,7 @@ pub async fn list_enroll_tokens(args: ListEnrollTokensArgs) -> Result<()> {
|
|||||||
if let Some(base) = settings.public_url.as_deref() {
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/api/v1/control/enroll-tokens?include_expired={}",
|
"{}/api/v1/control/enroll-tokens?include_expired={}",
|
||||||
base,
|
base, args.include_expired
|
||||||
args.include_expired
|
|
||||||
);
|
);
|
||||||
let response = reqwest::get(&url)
|
let response = reqwest::get(&url)
|
||||||
.await
|
.await
|
||||||
@@ -100,9 +109,15 @@ pub async fn list_enroll_tokens(args: ListEnrollTokensArgs) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store =
|
||||||
|
state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to initialize store {}",
|
||||||
|
settings.state_file.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let tokens = store.list_enroll_tokens(args.include_expired).await?;
|
let tokens = store.list_enroll_tokens(args.include_expired).await?;
|
||||||
if args.json {
|
if args.json {
|
||||||
println!(
|
println!(
|
||||||
@@ -146,9 +161,15 @@ pub async fn revoke_enroll_token(args: RevokeEnrollTokenArgs) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store =
|
||||||
|
state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to initialize store {}",
|
||||||
|
settings.state_file.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let removed = store.revoke_enroll_token(&args.token).await?;
|
let removed = store.revoke_enroll_token(&args.token).await?;
|
||||||
println!("token={} revoked={}", args.token, removed);
|
println!("token={} revoked={}", args.token, removed);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -184,13 +205,22 @@ pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
|||||||
println!("schema_version={}", body.schema_version);
|
println!("schema_version={}", body.schema_version);
|
||||||
println!("agent_count={}", body.agent_count);
|
println!("agent_count={}", body.agent_count);
|
||||||
println!("enroll_token_count={}", body.enroll_token_count);
|
println!("enroll_token_count={}", body.enroll_token_count);
|
||||||
println!("expired_enroll_token_count={}", body.expired_enroll_token_count);
|
println!(
|
||||||
|
"expired_enroll_token_count={}",
|
||||||
|
body.expired_enroll_token_count
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let store = state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
let store =
|
||||||
|
state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("failed to initialize store {}", settings.state_file.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to initialize store {}",
|
||||||
|
settings.state_file.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let stats = store.stats().await?;
|
let stats = store.stats().await?;
|
||||||
if args.json {
|
if args.json {
|
||||||
println!(
|
println!(
|
||||||
@@ -203,6 +233,9 @@ pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
|||||||
println!("schema_version={}", stats.schema_version);
|
println!("schema_version={}", stats.schema_version);
|
||||||
println!("agent_count={}", stats.agent_count);
|
println!("agent_count={}", stats.agent_count);
|
||||||
println!("enroll_token_count={}", stats.enroll_token_count);
|
println!("enroll_token_count={}", stats.enroll_token_count);
|
||||||
println!("expired_enroll_token_count={}", stats.expired_enroll_token_count);
|
println!(
|
||||||
|
"expired_enroll_token_count={}",
|
||||||
|
stats.expired_enroll_token_count
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::response::Redirect;
|
use axum::response::Redirect;
|
||||||
use axum::routing::{get, post};
|
|
||||||
use axum::routing::get_service;
|
use axum::routing::get_service;
|
||||||
|
use axum::routing::{get, post};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
|
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -25,9 +25,7 @@ use crate::ws;
|
|||||||
|
|
||||||
mod admin;
|
mod admin;
|
||||||
mod process;
|
mod process;
|
||||||
pub use admin::{
|
pub use admin::{issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats};
|
||||||
issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,
|
|
||||||
};
|
|
||||||
pub use process::reload_daemon;
|
pub use process::reload_daemon;
|
||||||
use process::{remove_pid_file, write_pid_file};
|
use process::{remove_pid_file, write_pid_file};
|
||||||
|
|
||||||
@@ -69,8 +67,14 @@ fn public_api_routes() -> Router<AppState> {
|
|||||||
|
|
||||||
fn control_api_routes() -> Router<AppState> {
|
fn control_api_routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/control/enroll-token", post(api::issue_enroll_token))
|
.route(
|
||||||
.route("/api/v1/control/enroll-tokens", get(api::list_enroll_tokens))
|
"/api/v1/control/enroll-token",
|
||||||
|
post(api::issue_enroll_token),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/enroll-tokens",
|
||||||
|
get(api::list_enroll_tokens),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/control/enroll-tokens/{token}",
|
"/api/v1/control/enroll-tokens/{token}",
|
||||||
axum::routing::delete(api::revoke_enroll_token),
|
axum::routing::delete(api::revoke_enroll_token),
|
||||||
@@ -128,6 +132,7 @@ pub async fn serve(daemon: config::DaemonConfig) -> Result<()> {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
let mut server = server;
|
||||||
let mut hup = signal(SignalKind::hangup()).context("failed to install SIGHUP handler")?;
|
let mut hup = signal(SignalKind::hangup()).context("failed to install SIGHUP handler")?;
|
||||||
let mut gc_tick = tokio::time::interval(Duration::from_secs(300));
|
let mut gc_tick = tokio::time::interval(Duration::from_secs(300));
|
||||||
gc_tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
gc_tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ const SEEDED_ENROLL_TOKEN_PREFIX: &[u8] = b"seeded_enroll_token:";
|
|||||||
const SCHEMA_VERSION: u32 = 1;
|
const SCHEMA_VERSION: u32 = 1;
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
pub async fn load_or_init(path: &Path, enroll_tokens: Vec<String>, seed_ttl: Duration) -> Result<Self> {
|
pub async fn load_or_init(
|
||||||
|
path: &Path,
|
||||||
|
enroll_tokens: Vec<String>,
|
||||||
|
seed_ttl: Duration,
|
||||||
|
) -> Result<Self> {
|
||||||
let db_path = path.to_path_buf();
|
let db_path = path.to_path_buf();
|
||||||
if let Some(parent) = db_path.parent() {
|
if let Some(parent) = db_path.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
@@ -38,7 +42,9 @@ impl Store {
|
|||||||
let enroll_tree = db
|
let enroll_tree = db
|
||||||
.open_tree("enroll_tokens")
|
.open_tree("enroll_tokens")
|
||||||
.context("failed to open enroll_tokens tree")?;
|
.context("failed to open enroll_tokens tree")?;
|
||||||
let agents_tree = db.open_tree("agents").context("failed to open agents tree")?;
|
let agents_tree = db
|
||||||
|
.open_tree("agents")
|
||||||
|
.context("failed to open agents tree")?;
|
||||||
let audit_events_tree = db
|
let audit_events_tree = db
|
||||||
.open_tree("audit_events")
|
.open_tree("audit_events")
|
||||||
.context("failed to open audit_events tree")?;
|
.context("failed to open audit_events tree")?;
|
||||||
@@ -92,13 +98,17 @@ impl Store {
|
|||||||
anyhow::bail!("invalid or already-used enroll token");
|
anyhow::bail!("invalid or already-used enroll token");
|
||||||
};
|
};
|
||||||
|
|
||||||
let expires_at_unix = decode_expiry(raw_expiry.as_ref())
|
let expires_at_unix =
|
||||||
.context("failed decoding enroll token expiry")?;
|
decode_expiry(raw_expiry.as_ref()).context("failed decoding enroll token expiry")?;
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
if expires_at_unix <= now {
|
if expires_at_unix <= now {
|
||||||
let _ = self.enroll_tokens.remove(enroll_token.as_bytes());
|
let _ = self.enroll_tokens.remove(enroll_token.as_bytes());
|
||||||
self.flush().ok();
|
self.flush().ok();
|
||||||
warn!(expires_at_unix, now_unix = now, "rejecting expired enroll token");
|
warn!(
|
||||||
|
expires_at_unix,
|
||||||
|
now_unix = now,
|
||||||
|
"rejecting expired enroll token"
|
||||||
|
);
|
||||||
anyhow::bail!("enroll token has expired");
|
anyhow::bail!("enroll token has expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +122,8 @@ impl Store {
|
|||||||
self.agents
|
self.agents
|
||||||
.insert(agent_id.as_bytes(), agent_token.as_bytes())
|
.insert(agent_id.as_bytes(), agent_token.as_bytes())
|
||||||
.context("failed persisting agent credentials")?;
|
.context("failed persisting agent credentials")?;
|
||||||
self.flush().context("failed flushing state db after enroll")?;
|
self.flush()
|
||||||
|
.context("failed flushing state db after enroll")?;
|
||||||
info!(agent_id = %agent_id, "issued persistent agent credentials");
|
info!(agent_id = %agent_id, "issued persistent agent credentials");
|
||||||
|
|
||||||
Ok(IssuedAgent {
|
Ok(IssuedAgent {
|
||||||
@@ -141,7 +152,8 @@ impl Store {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for item in self.enroll_tokens.iter() {
|
for item in self.enroll_tokens.iter() {
|
||||||
let (token, value) = item.context("failed iterating enroll token tree")?;
|
let (token, value) = item.context("failed iterating enroll token tree")?;
|
||||||
let expires_at_unix = decode_expiry(value.as_ref()).context("failed decoding token expiry")?;
|
let expires_at_unix =
|
||||||
|
decode_expiry(value.as_ref()).context("failed decoding token expiry")?;
|
||||||
let expired = expires_at_unix <= now;
|
let expired = expires_at_unix <= now;
|
||||||
if !include_expired && expired {
|
if !include_expired && expired {
|
||||||
continue;
|
continue;
|
||||||
@@ -169,7 +181,8 @@ impl Store {
|
|||||||
.context("failed removing enroll token")?
|
.context("failed removing enroll token")?
|
||||||
.is_some();
|
.is_some();
|
||||||
if removed {
|
if removed {
|
||||||
self.flush().context("failed flushing db after enroll token revoke")?;
|
self.flush()
|
||||||
|
.context("failed flushing db after enroll token revoke")?;
|
||||||
}
|
}
|
||||||
Ok(removed)
|
Ok(removed)
|
||||||
}
|
}
|
||||||
@@ -180,8 +193,8 @@ impl Store {
|
|||||||
let mut expired_enroll_token_count = 0usize;
|
let mut expired_enroll_token_count = 0usize;
|
||||||
for item in self.enroll_tokens.iter() {
|
for item in self.enroll_tokens.iter() {
|
||||||
let (_, value) = item.context("failed iterating enroll token tree")?;
|
let (_, value) = item.context("failed iterating enroll token tree")?;
|
||||||
let expires_at =
|
let expires_at = decode_expiry(value.as_ref())
|
||||||
decode_expiry(value.as_ref()).context("failed decoding token expiry during stats")?;
|
.context("failed decoding token expiry during stats")?;
|
||||||
enroll_token_count = enroll_token_count.saturating_add(1);
|
enroll_token_count = enroll_token_count.saturating_add(1);
|
||||||
if expires_at <= now {
|
if expires_at <= now {
|
||||||
expired_enroll_token_count = expired_enroll_token_count.saturating_add(1);
|
expired_enroll_token_count = expired_enroll_token_count.saturating_add(1);
|
||||||
@@ -372,7 +385,9 @@ impl Store {
|
|||||||
let (_, raw) = item.context("failed iterating alert transition tree")?;
|
let (_, raw) = item.context("failed iterating alert transition tree")?;
|
||||||
let transition: AlertTransition =
|
let transition: AlertTransition =
|
||||||
serde_json::from_slice(raw.as_ref()).context("failed decoding alert transition")?;
|
serde_json::from_slice(raw.as_ref()).context("failed decoding alert transition")?;
|
||||||
if let Some(since) = since_unix && transition.ts_unix < since {
|
if let Some(since) = since_unix
|
||||||
|
&& transition.ts_unix < since
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.push(transition);
|
out.push(transition);
|
||||||
@@ -384,15 +399,11 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&self) -> Result<()> {
|
fn flush(&self) -> Result<()> {
|
||||||
self.meta
|
self.meta.flush().context("failed to flush meta tree")?;
|
||||||
.flush()
|
|
||||||
.context("failed to flush meta tree")?;
|
|
||||||
self.enroll_tokens
|
self.enroll_tokens
|
||||||
.flush()
|
.flush()
|
||||||
.context("failed to flush enroll token tree")?;
|
.context("failed to flush enroll token tree")?;
|
||||||
self.agents
|
self.agents.flush().context("failed to flush agents tree")?;
|
||||||
.flush()
|
|
||||||
.context("failed to flush agents tree")?;
|
|
||||||
self.audit_events
|
self.audit_events
|
||||||
.flush()
|
.flush()
|
||||||
.context("failed to flush audit event tree")?;
|
.context("failed to flush audit event tree")?;
|
||||||
@@ -406,7 +417,11 @@ impl Store {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed_bootstrap_enroll_tokens(&self, enroll_tokens: &[String], seed_ttl: Duration) -> Result<()> {
|
fn seed_bootstrap_enroll_tokens(
|
||||||
|
&self,
|
||||||
|
enroll_tokens: &[String],
|
||||||
|
seed_ttl: Duration,
|
||||||
|
) -> Result<()> {
|
||||||
for token in enroll_tokens {
|
for token in enroll_tokens {
|
||||||
let token = token.trim();
|
let token = token.trim();
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
@@ -414,21 +429,32 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let marker_key = seeded_enroll_token_key(token);
|
let marker_key = seeded_enroll_token_key(token);
|
||||||
if self
|
if self.meta.contains_key(&marker_key).with_context(|| {
|
||||||
.meta
|
format!(
|
||||||
.contains_key(&marker_key)
|
"failed reading bootstrap marker in {}",
|
||||||
.with_context(|| format!("failed reading bootstrap marker in {}", self.db_path.display()))?
|
self.db_path.display()
|
||||||
{
|
)
|
||||||
|
})? {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let expires_at = now_unix().saturating_add(seed_ttl.as_secs().max(1));
|
let expires_at = now_unix().saturating_add(seed_ttl.as_secs().max(1));
|
||||||
self.enroll_tokens
|
self.enroll_tokens
|
||||||
.insert(token.as_bytes(), &expires_at.to_le_bytes())
|
.insert(token.as_bytes(), &expires_at.to_le_bytes())
|
||||||
.with_context(|| format!("failed to seed enroll token into {}", self.db_path.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to seed enroll token into {}",
|
||||||
|
self.db_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
self.meta
|
self.meta
|
||||||
.insert(marker_key, &expires_at.to_le_bytes())
|
.insert(marker_key, &expires_at.to_le_bytes())
|
||||||
.with_context(|| format!("failed to persist bootstrap marker into {}", self.db_path.display()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to persist bootstrap marker into {}",
|
||||||
|
self.db_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -475,8 +501,12 @@ impl Store {
|
|||||||
self.meta
|
self.meta
|
||||||
.insert(SCHEMA_VERSION_KEY, &SCHEMA_VERSION.to_le_bytes())
|
.insert(SCHEMA_VERSION_KEY, &SCHEMA_VERSION.to_le_bytes())
|
||||||
.context("failed writing schema version")?;
|
.context("failed writing schema version")?;
|
||||||
self.flush().context("failed flushing db after schema init")?;
|
self.flush()
|
||||||
info!(schema_version = SCHEMA_VERSION, "initialized state schema version");
|
.context("failed flushing db after schema init")?;
|
||||||
|
info!(
|
||||||
|
schema_version = SCHEMA_VERSION,
|
||||||
|
"initialized state schema version"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -535,16 +565,24 @@ fn matches_audit_filter(event: &AuditEvent, filter: &AuditEventFilter) -> bool {
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(event_type) = filter.event_type.as_deref() && event.event_type != event_type {
|
if let Some(event_type) = filter.event_type.as_deref()
|
||||||
|
&& event.event_type != event_type
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(outcome) = filter.outcome.as_deref() && event.outcome != outcome {
|
if let Some(outcome) = filter.outcome.as_deref()
|
||||||
|
&& event.outcome != outcome
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(since_unix) = filter.since_unix && event.ts_unix < since_unix {
|
if let Some(since_unix) = filter.since_unix
|
||||||
|
&& event.ts_unix < since_unix
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(until_unix) = filter.until_unix && event.ts_unix > until_unix {
|
if let Some(until_unix) = filter.until_unix
|
||||||
|
&& event.ts_unix > until_unix
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +597,8 @@ mod tests {
|
|||||||
use super::Store;
|
use super::Store;
|
||||||
|
|
||||||
async fn make_store() -> (Store, std::path::PathBuf) {
|
async fn make_store() -> (Store, std::path::PathBuf) {
|
||||||
let dir = std::env::temp_dir().join(format!("wakey-cp-store-test-{}", uuid::Uuid::new_v4()));
|
let dir =
|
||||||
|
std::env::temp_dir().join(format!("wakey-cp-store-test-{}", uuid::Uuid::new_v4()));
|
||||||
let db_path = dir.join("state.db");
|
let db_path = dir.join("state.db");
|
||||||
let store = Store::load_or_init(&db_path, Vec::new(), Duration::from_secs(60))
|
let store = Store::load_or_init(&db_path, Vec::new(), Duration::from_secs(60))
|
||||||
.await
|
.await
|
||||||
@@ -785,9 +824,10 @@ mod tests {
|
|||||||
.enroll("enr-bootstrap-once")
|
.enroll("enr-bootstrap-once")
|
||||||
.await
|
.await
|
||||||
.expect_err("bootstrap token should not resurrect after restart");
|
.expect_err("bootstrap token should not resurrect after restart");
|
||||||
assert!(err
|
assert!(
|
||||||
.to_string()
|
err.to_string()
|
||||||
.contains("invalid or already-used enroll token"));
|
.contains("invalid or already-used enroll token")
|
||||||
|
);
|
||||||
|
|
||||||
cleanup_dir(&dir);
|
cleanup_dir(&dir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use opentelemetry::trace::TracerProvider as _;
|
|
||||||
use opentelemetry::global;
|
use opentelemetry::global;
|
||||||
|
use opentelemetry::trace::TracerProvider as _;
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
use opentelemetry_sdk::Resource;
|
use opentelemetry_sdk::Resource;
|
||||||
use opentelemetry_sdk::trace::{SdkTracerProvider, Tracer};
|
use opentelemetry_sdk::trace::{SdkTracerProvider, Tracer};
|
||||||
@@ -28,7 +28,10 @@ pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<()> {
|
|||||||
.with(filter)
|
.with(filter)
|
||||||
.with(fmt::layer().json())
|
.with(fmt::layer().json())
|
||||||
.init();
|
.init();
|
||||||
tracing::info!(json_logs = telemetry.json_logs, "tracing initialized without otlp exporter");
|
tracing::info!(
|
||||||
|
json_logs = telemetry.json_logs,
|
||||||
|
"tracing initialized without otlp exporter"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if let Some(otel_layer) = otel {
|
} else if let Some(otel_layer) = otel {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
@@ -42,7 +45,10 @@ pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<()> {
|
|||||||
.with(filter)
|
.with(filter)
|
||||||
.with(fmt::layer())
|
.with(fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
tracing::info!(json_logs = telemetry.json_logs, "tracing initialized without otlp exporter");
|
tracing::info!(
|
||||||
|
json_logs = telemetry.json_logs,
|
||||||
|
"tracing initialized without otlp exporter"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -50,14 +56,8 @@ pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<()> {
|
|||||||
|
|
||||||
fn build_otel_layer(
|
fn build_otel_layer(
|
||||||
telemetry: &TelemetryConfig,
|
telemetry: &TelemetryConfig,
|
||||||
) -> Result<
|
) -> Result<Option<tracing_opentelemetry::OpenTelemetryLayer<tracing_subscriber::Registry, Tracer>>>
|
||||||
Option<
|
{
|
||||||
tracing_opentelemetry::OpenTelemetryLayer<
|
|
||||||
tracing_subscriber::Registry,
|
|
||||||
Tracer,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
> {
|
|
||||||
let Some(endpoint) = telemetry.otlp_endpoint.as_deref() else {
|
let Some(endpoint) = telemetry.otlp_endpoint.as_deref() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,11 +201,7 @@ async fn process_agent_text(
|
|||||||
}
|
}
|
||||||
anyhow::bail!("agent auth rejected");
|
anyhow::bail!("agent auth rejected");
|
||||||
}
|
}
|
||||||
state
|
state.sessions.write().await.insert(
|
||||||
.sessions
|
|
||||||
.write()
|
|
||||||
.await
|
|
||||||
.insert(
|
|
||||||
agent_id.clone(),
|
agent_id.clone(),
|
||||||
AgentSession {
|
AgentSession {
|
||||||
connection_id: connection_id.to_string(),
|
connection_id: connection_id.to_string(),
|
||||||
@@ -275,7 +271,11 @@ fn now_duration_ms(duration: std::time::Duration) -> u64 {
|
|||||||
duration.as_millis() as u64
|
duration.as_millis() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_current_session(state: &AppState, agent_id: &str, connection_id: &str) -> Result<()> {
|
async fn ensure_current_session(
|
||||||
|
state: &AppState,
|
||||||
|
agent_id: &str,
|
||||||
|
connection_id: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let sessions = state.sessions.read().await;
|
let sessions = state.sessions.read().await;
|
||||||
if is_current_session(&sessions, agent_id, connection_id) {
|
if is_current_session(&sessions, agent_id, connection_id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user