cleaning up

This commit is contained in:
lda
2026-04-14 01:28:21 +07:00 Unverified
parent 42cdc720a3
commit 826a41ae64
12 changed files with 98 additions and 175 deletions
+11 -20
View File
@@ -24,8 +24,8 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Command { pub enum Command {
/// Show device status rows from neighbor/device data. /// Show merged device inventory rows.
Status(StatusArgs), Inventory(InventoryArgs),
/// Show DHCP leases, optionally enriched with current neighbor state. /// Show DHCP leases, optionally enriched with current neighbor state.
Leases(LeasesArgs), Leases(LeasesArgs),
/// Send Wake-on-LAN packets from a query or explicit MAC/IP pair. /// Send Wake-on-LAN packets from a query or explicit MAC/IP pair.
@@ -70,12 +70,12 @@ pub struct WakeArgs {
#[derive(Args)] #[derive(Args)]
#[command(after_long_help = "Examples: #[command(after_long_help = "Examples:
wakey status bedroom-pc wakey inventory bedroom-pc
wakey status --mac aa:bb:cc:dd:ee:ff wakey inventory --mac aa:bb:cc:dd:ee:ff
wakey status --dev br-lan --nud reachable wakey inventory --dev br-lan --nud reachable
If only the positional query is provided, it is treated as free-form input and resolved through the smart selector path.")] If only the positional query is provided, it is treated as free-form input and resolved through the smart selector path.")]
pub struct StatusArgs { pub struct InventoryArgs {
/// Free-form device query. /// Free-form device query.
pub query: Option<String>, pub query: Option<String>,
/// Explicit name/text filter. /// Explicit name/text filter.
@@ -138,21 +138,12 @@ pub async fn run(cli: Cli) -> Result<()> {
init_tracing(cli.verbose); init_tracing(cli.verbose);
match cli.command { match cli.command {
Command::Status(args) => { Command::Inventory(args) => {
let as_json = args.json; let as_json = args.json;
let query = status_args_to_query(args); let query = inventory_args_to_query(args);
let selected_name = query.name.clone(); let selected_name = query.name.clone();
debug!(?query, json = as_json, "dispatching status command"); debug!(?query, json = as_json, "dispatching inventory command");
let status = if query.name.is_some() let status = wakey::inventory(query).await?;
&& query.filter.ips.is_empty()
&& query.filter.devs.is_empty()
&& query.filter.nuds.is_empty()
&& query.filter.macs.is_empty()
{
wakey::get_status_for_input(query.name.clone().unwrap_or_default()).await?
} else {
wakey::get_status(query).await?
};
if as_json { if as_json {
println!("{}", serde_json::to_string_pretty(&status)?); println!("{}", serde_json::to_string_pretty(&status)?);
} else { } else {
@@ -216,7 +207,7 @@ pub async fn run(cli: Cli) -> Result<()> {
Ok(()) Ok(())
} }
fn status_args_to_query(args: StatusArgs) -> DeviceQuery { fn inventory_args_to_query(args: InventoryArgs) -> DeviceQuery {
if let Some(query) = args.query.as_ref() if let Some(query) = args.query.as_ref()
&& args.name.is_none() && args.name.is_none()
&& args.ips.is_empty() && args.ips.is_empty()
+33 -22
View File
@@ -5,32 +5,43 @@ use wakey_core::{DeviceInventory, DhcpLeaseWithState, InterfaceSummary, WakeResu
pub fn render_status_table(status: &DeviceInventory) -> Table { pub fn render_status_table(status: &DeviceInventory) -> Table {
let mut table = base_table(); let mut table = base_table();
table.set_header(vec!["Name", "IP", "MAC", "Presence", "Interfaces"]); table.set_header(vec!["Name", "IP", "MAC", "Presence", "Interfaces"]);
for row in &status.devices { for device in &status.devices {
table.add_row(vec![ let name = device
Cell::new( .names
row.names
.first() .first()
.cloned() .cloned()
.unwrap_or_else(|| "(unnamed)".into()), .unwrap_or_else(|| "(unnamed)".into());
), let ips: Vec<String> = if device.ips.is_empty() {
Cell::new( vec![String::new()]
row.ips } else {
.iter() device.ips.iter().map(ToString::to_string).collect()
.map(ToString::to_string) };
.collect::<Vec<_>>() let macs: Vec<String> = if device.macs.is_empty() {
.join(", "), vec![String::new()]
), } else {
Cell::new( device.macs.iter().map(ToString::to_string).collect()
row.macs };
.iter() let interfaces: Vec<String> = if device.interfaces.is_empty() {
.map(ToString::to_string) vec![String::new()]
.collect::<Vec<_>>() } else {
.join(", "), device.interfaces.clone()
), };
Cell::new(format!("{:?}", row.presence)),
Cell::new(row.interfaces.join(", ")), let row_count = ips.len().max(macs.len()).max(interfaces.len());
for idx in 0..row_count {
table.add_row(vec![
Cell::new(if idx == 0 { name.as_str() } else { "" }),
Cell::new(ips.get(idx).cloned().unwrap_or_default()),
Cell::new(macs.get(idx).cloned().unwrap_or_default()),
Cell::new(if idx == 0 {
format!("{:?}", device.presence)
} else {
String::new()
}),
Cell::new(interfaces.get(idx).cloned().unwrap_or_default()),
]); ]);
} }
}
table table
} }
+4 -4
View File
@@ -2,10 +2,10 @@ pub mod service;
pub mod utils; pub mod utils;
pub use service::{ pub use service::{
StatusResponse, broadcast_wake_targets, get_interface_summaries, get_interface_summary, broadcast_wake_targets, get_interface_summaries, get_interface_summary, get_ips, get_leases,
get_ips, get_leases, get_status, get_status_for_input, inventory, leases_without_state, inventory, leases_without_state, list_interfaces, merge_devices, query_to_device_query,
list_interfaces, merge_devices, query_to_device_query, resolve_devices, resolve_query, resolve_devices, resolve_query, resolve_selector, resolve_wake_targets, wake_explicit,
resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets, wake_from_query, wake_targets,
}; };
#[cfg(test)] #[cfg(test)]
-2
View File
@@ -2,14 +2,12 @@ pub mod interfaces;
pub mod inventory; pub mod inventory;
pub mod leases; pub mod leases;
pub mod query; pub mod query;
pub mod status;
pub mod wake; pub mod wake;
pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips, list_interfaces}; pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips, list_interfaces};
pub use inventory::{inventory, merge_devices, resolve_devices}; pub use inventory::{inventory, merge_devices, resolve_devices};
pub use leases::{get_leases, leases_without_state}; pub use leases::{get_leases, leases_without_state};
pub use query::{query_to_device_query, resolve_query, resolve_selector}; pub use query::{query_to_device_query, resolve_query, resolve_selector};
pub use status::{StatusResponse, get_status, get_status_for_input};
pub use wake::{ pub use wake::{
broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets, broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
}; };
-24
View File
@@ -1,24 +0,0 @@
use anyhow::Result;
use tracing::{debug, instrument};
use wakey_core::{DeviceInventory, DeviceQuery};
use crate::service::inventory::inventory;
use crate::service::query::resolve_query;
/// Service status payload expressed in terms of merged device inventory.
pub type StatusResponse = DeviceInventory;
/// Return status payload from merged device inventory.
#[instrument(skip_all, fields(name = ?query.name))]
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
let inventory = inventory(query).await?;
debug!(devices = inventory.devices.len(), "built status response");
Ok(inventory)
}
/// Convenience wrapper around [`get_status`] for free-form user input.
#[instrument(skip_all)]
pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResponse> {
let query = resolve_query(input).await?;
get_status(query).await
}
+10 -10
View File
@@ -1,6 +1,6 @@
use std::net::IpAddr; use std::net::IpAddr;
use wakey::{broadcast_wake_targets, get_interface_summaries, get_status, get_status_for_input}; use wakey::{broadcast_wake_targets, get_interface_summaries, inventory, resolve_query};
use wakey_core::{DeviceFilters, DeviceQuery}; use wakey_core::{DeviceFilters, DeviceQuery};
#[tokio::test] #[tokio::test]
@@ -17,22 +17,22 @@ async fn interfaces_real_router_prints_interface_summaries() -> anyhow::Result<(
#[tokio::test] #[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"] #[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_default_query_returns_rows_or_empty_cleanly() -> anyhow::Result<()> { async fn inventory_real_router_default_query_returns_rows_or_empty_cleanly() -> anyhow::Result<()> {
let status = get_status(DeviceQuery::default()).await?; let inv = inventory(DeviceQuery::default()).await?;
println!("{}", serde_json::to_string_pretty(&status)?); println!("{}", serde_json::to_string_pretty(&inv)?);
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"] #[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_for_interface_filter_succeeds() -> anyhow::Result<()> { async fn inventory_real_router_for_interface_filter_succeeds() -> anyhow::Result<()> {
let interfaces = get_interface_summaries().await?; let interfaces = get_interface_summaries().await?;
let first = interfaces let first = interfaces
.first() .first()
.map(|iface| iface.ifname.clone()) .map(|iface| iface.ifname.clone())
.expect("expected at least one interface"); .expect("expected at least one interface");
let status = get_status(DeviceQuery { let inv = inventory(DeviceQuery {
name: None, name: None,
filter: DeviceFilters { filter: DeviceFilters {
devs: vec![first.clone()], devs: vec![first.clone()],
@@ -42,22 +42,22 @@ async fn status_real_router_for_interface_filter_succeeds() -> anyhow::Result<()
.await?; .await?;
println!("filtered dev: {first}"); println!("filtered dev: {first}");
println!("{}", serde_json::to_string_pretty(&status)?); println!("{}", serde_json::to_string_pretty(&inv)?);
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test]
#[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"] #[ignore = "runs against live router data; use on-device or via scripts/test_remote.ps1"]
async fn status_real_router_string_input_for_interface_succeeds() -> anyhow::Result<()> { async fn inventory_real_router_string_input_for_interface_succeeds() -> anyhow::Result<()> {
let interfaces = get_interface_summaries().await?; let interfaces = get_interface_summaries().await?;
let first = interfaces let first = interfaces
.first() .first()
.map(|iface| iface.ifname.clone()) .map(|iface| iface.ifname.clone())
.expect("expected at least one interface"); .expect("expected at least one interface");
let status = get_status_for_input(first.clone()).await?; let inv = inventory(resolve_query(first.clone()).await?).await?;
println!("selector: {first}"); println!("selector: {first}");
println!("{}", serde_json::to_string_pretty(&status)?); println!("{}", serde_json::to_string_pretty(&inv)?);
Ok(()) Ok(())
} }
+1 -14
View File
@@ -39,7 +39,7 @@ export type AuditEvent = {
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
}; };
export type CommandKind = "status" | "devs" | "leases" | "inventory" | "wake"; export type CommandKind = "devs" | "leases" | "inventory" | "wake";
export type EnrollTokenStatus = { export type EnrollTokenStatus = {
enroll_token: string; enroll_token: string;
@@ -127,19 +127,6 @@ function buildCommandPayload(kind: CommandKind, query: string): { command: Recor
if (kind === "devs") { if (kind === "devs") {
return { command: { kind: "devs", dev: null, up_only: false } }; return { command: { kind: "devs", dev: null, up_only: false } };
} }
if (kind === "status") {
return {
command: {
kind: "status",
query: query || null,
name: null,
ips: [],
devs: [],
nuds: [],
macs: [],
},
};
}
if (kind === "leases") { if (kind === "leases") {
return { command: { kind: "leases", include_state: true } }; return { command: { kind: "leases", include_state: true } };
} }
-1
View File
@@ -50,7 +50,6 @@ export function CommandsPage({ agents, selectedAgentId, onSelectAgent, onAfterCo
Command Command
<select value={kind} onChange={(e) => setKind(e.target.value as CommandKind)}> <select value={kind} onChange={(e) => setKind(e.target.value as CommandKind)}>
<option value="devs">devs</option> <option value="devs">devs</option>
<option value="status">status</option>
<option value="leases">leases</option> <option value="leases">leases</option>
<option value="inventory">inventory</option> <option value="inventory">inventory</option>
<option value="wake">wake</option> <option value="wake">wake</option>
+1 -20
View File
@@ -2,8 +2,7 @@ use anyhow::Result;
use tracing::{debug, info, instrument}; use tracing::{debug, info, instrument};
use crate::protocol::{ use crate::protocol::{
AgentCommand, CommandResult, DevsRequest, InventoryRequest, LeasesRequest, StatusRequest, AgentCommand, CommandResult, DevsRequest, InventoryRequest, LeasesRequest, WakeRequest,
WakeRequest,
}; };
#[instrument(skip_all)] #[instrument(skip_all)]
@@ -11,7 +10,6 @@ pub async fn dispatch_command(command: AgentCommand) -> Result<CommandResult> {
let kind = command_kind(&command); let kind = command_kind(&command);
info!(command = %kind, "dispatching command into local wakey services"); info!(command = %kind, "dispatching command into local wakey services");
match command { match command {
AgentCommand::Status(req) => dispatch_status(req).await,
AgentCommand::Leases(req) => dispatch_leases(req).await, AgentCommand::Leases(req) => dispatch_leases(req).await,
AgentCommand::Devs(req) => dispatch_devs(req).await, AgentCommand::Devs(req) => dispatch_devs(req).await,
AgentCommand::Inventory(req) => dispatch_inventory(req).await, AgentCommand::Inventory(req) => dispatch_inventory(req).await,
@@ -19,22 +17,6 @@ pub async fn dispatch_command(command: AgentCommand) -> Result<CommandResult> {
} }
} }
async fn dispatch_status(req: StatusRequest) -> Result<CommandResult> {
let query = req.into_device_query();
let status = if query.name.is_some()
&& query.filter.ips.is_empty()
&& query.filter.devs.is_empty()
&& query.filter.nuds.is_empty()
&& query.filter.macs.is_empty()
{
wakey::get_status_for_input(query.name.clone().unwrap_or_default()).await?
} else {
wakey::get_status(query).await?
};
debug!(devices = status.devices.len(), "dispatched status command");
Ok(CommandResult::Status(status))
}
async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> { async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> {
let leases = wakey::get_leases(wakey_core::LeaseQuery { let leases = wakey::get_leases(wakey_core::LeaseQuery {
include_state: req.include_state, include_state: req.include_state,
@@ -90,7 +72,6 @@ async fn dispatch_wake(req: WakeRequest) -> Result<CommandResult> {
fn command_kind(command: &AgentCommand) -> &'static str { fn command_kind(command: &AgentCommand) -> &'static str {
match command { match command {
AgentCommand::Status(_) => "status",
AgentCommand::Leases(_) => "leases", AgentCommand::Leases(_) => "leases",
AgentCommand::Devs(_) => "devs", AgentCommand::Devs(_) => "devs",
AgentCommand::Inventory(_) => "inventory", AgentCommand::Inventory(_) => "inventory",
+34 -52
View File
@@ -66,48 +66,6 @@ pub struct ErrorPayload {
pub retryable: Option<bool>, pub retryable: Option<bool>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusRequest {
pub query: Option<String>,
pub name: Option<String>,
#[serde(default)]
pub ips: Vec<IpAddr>,
#[serde(default)]
pub devs: Vec<String>,
#[serde(default)]
pub nuds: Vec<wakey_core::NeighborState>,
#[serde(default)]
#[serde(with = "mac::vec_mac")]
pub macs: Vec<MacAddr>,
}
impl StatusRequest {
pub fn into_device_query(self) -> DeviceQuery {
if let Some(query) = self.query.as_ref()
&& self.name.is_none()
&& self.ips.is_empty()
&& self.devs.is_empty()
&& self.nuds.is_empty()
&& self.macs.is_empty()
{
return DeviceQuery {
name: Some(query.clone()),
..Default::default()
};
}
DeviceQuery {
name: self.name.or(self.query),
filter: DeviceFilters {
ips: self.ips,
devs: self.devs,
nuds: self.nuds,
macs: self.macs,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeasesRequest { pub struct LeasesRequest {
#[serde(default)] #[serde(default)]
@@ -138,15 +96,41 @@ pub struct InventoryRequest {
impl InventoryRequest { impl InventoryRequest {
pub fn into_device_query(self) -> DeviceQuery { pub fn into_device_query(self) -> DeviceQuery {
StatusRequest { into_device_query(
query: self.query, self.query, self.name, self.ips, self.devs, self.nuds, self.macs,
name: self.name, )
ips: self.ips,
devs: self.devs,
nuds: self.nuds,
macs: self.macs,
} }
.into_device_query() }
fn into_device_query(
query: Option<String>,
name: Option<String>,
ips: Vec<IpAddr>,
devs: Vec<String>,
nuds: Vec<wakey_core::NeighborState>,
macs: Vec<MacAddr>,
) -> DeviceQuery {
if let Some(q) = query.as_ref()
&& name.is_none()
&& ips.is_empty()
&& devs.is_empty()
&& nuds.is_empty()
&& macs.is_empty()
{
return DeviceQuery {
name: Some(q.clone()),
..Default::default()
};
}
DeviceQuery {
name: name.or(query),
filter: DeviceFilters {
ips,
devs,
nuds,
macs,
},
} }
} }
@@ -163,7 +147,6 @@ pub struct WakeRequest {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum AgentCommand { pub enum AgentCommand {
Status(StatusRequest),
Leases(LeasesRequest), Leases(LeasesRequest),
Devs(DevsRequest), Devs(DevsRequest),
Inventory(InventoryRequest), Inventory(InventoryRequest),
@@ -173,7 +156,6 @@ pub enum AgentCommand {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommandResult { pub enum CommandResult {
Status(DeviceInventory),
Leases { rows: Vec<DhcpLeaseWithState> }, Leases { rows: Vec<DhcpLeaseWithState> },
Devs { rows: Vec<InterfaceSummary> }, Devs { rows: Vec<InterfaceSummary> },
Inventory(DeviceInventory), Inventory(DeviceInventory),
-1
View File
@@ -237,7 +237,6 @@ where
fn command_kind(command: &AgentCommand) -> &'static str { fn command_kind(command: &AgentCommand) -> &'static str {
match command { match command {
AgentCommand::Status(_) => "status",
AgentCommand::Leases(_) => "leases", AgentCommand::Leases(_) => "leases",
AgentCommand::Devs(_) => "devs", AgentCommand::Devs(_) => "devs",
AgentCommand::Inventory(_) => "inventory", AgentCommand::Inventory(_) => "inventory",
-1
View File
@@ -264,7 +264,6 @@ pub async fn run_command(
fn command_kind(command: &AgentCommand) -> &'static str { fn command_kind(command: &AgentCommand) -> &'static str {
match command { match command {
AgentCommand::Status(_) => "status",
AgentCommand::Leases(_) => "leases", AgentCommand::Leases(_) => "leases",
AgentCommand::Devs(_) => "devs", AgentCommand::Devs(_) => "devs",
AgentCommand::Inventory(_) => "inventory", AgentCommand::Inventory(_) => "inventory",