Compare commits
4
Commits
42cdc720a3
...
458123428e
No files matched your search
+23
-44
@@ -8,7 +8,7 @@ use anyhow::Result;
|
|||||||
use clap::{ArgAction, Args, Parser, Subcommand};
|
use clap::{ArgAction, Args, Parser, Subcommand};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
|
use wakey_core::{InterfaceSummary, InventoryQuery, InventoryQueryBuilder, Query, WakeResult};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "wakey")]
|
#[command(name = "wakey")]
|
||||||
@@ -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,15 @@ 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.iter().find_map(|term| match term {
|
||||||
debug!(?query, json = as_json, "dispatching status command");
|
Query::Text(text) => Some(text.clone()),
|
||||||
let status = if query.name.is_some()
|
_ => None,
|
||||||
&& query.filter.ips.is_empty()
|
});
|
||||||
&& query.filter.devs.is_empty()
|
debug!(?query, json = as_json, "dispatching inventory command");
|
||||||
&& query.filter.nuds.is_empty()
|
let status = wakey::inventory(query).await?;
|
||||||
&& 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,29 +210,14 @@ pub async fn run(cli: Cli) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status_args_to_query(args: StatusArgs) -> DeviceQuery {
|
fn inventory_args_to_query(args: InventoryArgs) -> InventoryQuery {
|
||||||
if let Some(query) = args.query.as_ref()
|
InventoryQueryBuilder::new()
|
||||||
&& args.name.is_none()
|
.maybe_text(args.name.or(args.query))
|
||||||
&& args.ips.is_empty()
|
.ips(args.ips)
|
||||||
&& args.devs.is_empty()
|
.interfaces(args.devs)
|
||||||
&& args.nuds.is_empty()
|
.neighbor_states(args.nuds)
|
||||||
&& args.macs.is_empty()
|
.macs(args.macs)
|
||||||
{
|
.build()
|
||||||
return DeviceQuery {
|
|
||||||
name: Some(query.clone()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceQuery {
|
|
||||||
name: args.name.or(args.query),
|
|
||||||
filter: DeviceFilters {
|
|
||||||
ips: args.ips,
|
|
||||||
devs: args.devs,
|
|
||||||
nuds: args.nuds,
|
|
||||||
macs: args.macs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_wake_args(args: &WakeArgs) -> Result<()> {
|
fn validate_wake_args(args: &WakeArgs) -> Result<()> {
|
||||||
|
|||||||
+36
-25
@@ -5,31 +5,42 @@ 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() {
|
||||||
),
|
vec![String::new()]
|
||||||
Cell::new(
|
} else {
|
||||||
row.ips
|
device.ips.iter().map(ToString::to_string).collect()
|
||||||
.iter()
|
};
|
||||||
.map(ToString::to_string)
|
let macs: Vec<String> = if device.macs.is_empty() {
|
||||||
.collect::<Vec<_>>()
|
vec![String::new()]
|
||||||
.join(", "),
|
} else {
|
||||||
),
|
device.macs.iter().map(ToString::to_string).collect()
|
||||||
Cell::new(
|
};
|
||||||
row.macs
|
let interfaces: Vec<String> = if device.interfaces.is_empty() {
|
||||||
.iter()
|
vec![String::new()]
|
||||||
.map(ToString::to_string)
|
} else {
|
||||||
.collect::<Vec<_>>()
|
device.interfaces.clone()
|
||||||
.join(", "),
|
};
|
||||||
),
|
|
||||||
Cell::new(format!("{:?}", row.presence)),
|
let row_count = ips.len().max(macs.len()).max(interfaces.len());
|
||||||
Cell::new(row.interfaces.join(", ")),
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-9
@@ -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, merge_devices, query_to_inventory_query, resolve_devices,
|
||||||
list_interfaces, merge_devices, query_to_device_query, resolve_devices, resolve_query,
|
resolve_query, resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query,
|
||||||
resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
wake_targets,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -20,8 +20,8 @@ mod tests {
|
|||||||
async fn resolve_query_parses_ip() {
|
async fn resolve_query_parses_ip() {
|
||||||
let query = resolve_query("192.168.1.10").await.expect("resolve query");
|
let query = resolve_query("192.168.1.10").await.expect("resolve query");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
query.filter.ips,
|
query,
|
||||||
vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))]
|
vec![Query::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)))]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,13 +30,16 @@ mod tests {
|
|||||||
let query = resolve_query("aa:bb:cc:dd:ee:ff")
|
let query = resolve_query("aa:bb:cc:dd:ee:ff")
|
||||||
.await
|
.await
|
||||||
.expect("resolve query");
|
.expect("resolve query");
|
||||||
assert_eq!(query.filter.macs.len(), 1);
|
assert_eq!(
|
||||||
|
query,
|
||||||
|
vec![Query::Mac("aa:bb:cc:dd:ee:ff".parse().expect("mac"))]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn resolve_query_parses_nud() {
|
async fn resolve_query_parses_nud() {
|
||||||
let query = resolve_query("reachable").await.expect("resolve query");
|
let query = resolve_query("reachable").await.expect("resolve query");
|
||||||
assert_eq!(query.filter.nuds, vec![NeighborState::Reachable]);
|
assert_eq!(query, vec![Query::NeighborState(NeighborState::Reachable)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -80,7 +83,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
nud_state: None,
|
nud_state: None,
|
||||||
}];
|
}];
|
||||||
let devices = merge_devices(neighbors, leases, &wakey_core::DeviceQuery::default());
|
let devices = merge_devices(neighbors, leases, &wakey_core::InventoryQuery::default());
|
||||||
assert_eq!(devices.len(), 1);
|
assert_eq!(devices.len(), 1);
|
||||||
assert_eq!(devices[0].presence, Presence::Online);
|
assert_eq!(devices[0].presence, Presence::Online);
|
||||||
assert_eq!(devices[0].names, vec!["pc".to_string()]);
|
assert_eq!(devices[0].names, vec!["pc".to_string()]);
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ use anyhow::Result;
|
|||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
use wakey_core::InterfaceSummary;
|
use wakey_core::InterfaceSummary;
|
||||||
|
|
||||||
/// Return interface names only.
|
|
||||||
///
|
|
||||||
/// This is the old, lightweight interface listing surface kept for compatibility.
|
|
||||||
pub async fn list_interfaces() -> Result<Vec<String>> {
|
|
||||||
Ok(wakey_linux::devices::devs_sorted().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return condensed interface summaries useful for CLI and wake routing.
|
/// Return condensed interface summaries useful for CLI and wake routing.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
|
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
|
||||||
|
|||||||
+88
-19
@@ -1,6 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wakey_core::{
|
use wakey_core::{
|
||||||
Device, DeviceInventory, DeviceQuery, DhcpLease, DhcpLeaseWithState, NeighborEntry, Presence,
|
Device, DeviceInventory, DhcpLease, DhcpLeaseWithState, InventoryQuery, NeighborEntry,
|
||||||
|
Presence, Query,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::service::leases::get_leases;
|
use crate::service::leases::get_leases;
|
||||||
@@ -17,8 +18,8 @@ pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
|||||||
/// This is the current center of gravity for the service layer. Higher-level
|
/// This is the current center of gravity for the service layer. Higher-level
|
||||||
/// status and wake flows should prefer deriving from inventory rather than
|
/// status and wake flows should prefer deriving from inventory rather than
|
||||||
/// directly from raw Linux source rows.
|
/// directly from raw Linux source rows.
|
||||||
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
pub async fn inventory(query: InventoryQuery) -> Result<DeviceInventory> {
|
||||||
let neighbors = wakey_linux::devices::query_status(&query).await?;
|
let neighbors = wakey_linux::devices::query_neighbors(&query).await?;
|
||||||
let leases = get_leases(wakey_core::LeaseQuery {
|
let leases = get_leases(wakey_core::LeaseQuery {
|
||||||
include_state: false,
|
include_state: false,
|
||||||
})
|
})
|
||||||
@@ -35,7 +36,7 @@ pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
|||||||
pub fn merge_devices(
|
pub fn merge_devices(
|
||||||
neighbors: Vec<NeighborEntry>,
|
neighbors: Vec<NeighborEntry>,
|
||||||
leases: Vec<DhcpLeaseWithState>,
|
leases: Vec<DhcpLeaseWithState>,
|
||||||
query: &DeviceQuery,
|
query: &InventoryQuery,
|
||||||
) -> Vec<Device> {
|
) -> Vec<Device> {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
@@ -58,34 +59,45 @@ pub fn merge_devices(
|
|||||||
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if let Some(name) = &query.name {
|
let mut texts: Vec<&str> = Vec::new();
|
||||||
devices.retain(|device| device.names.iter().any(|n| n == name));
|
let mut devs: Vec<&str> = Vec::new();
|
||||||
|
let mut ips = Vec::new();
|
||||||
|
let mut macs = Vec::new();
|
||||||
|
let mut nuds = Vec::new();
|
||||||
|
|
||||||
|
for term in query {
|
||||||
|
match term {
|
||||||
|
Query::Text(v) => texts.push(v.as_str()),
|
||||||
|
Query::Interface(v) => devs.push(v.as_str()),
|
||||||
|
Query::Ip(v) => ips.push(*v),
|
||||||
|
Query::Mac(v) => macs.push(*v),
|
||||||
|
Query::NeighborState(v) => nuds.push(*v),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !query.filter.devs.is_empty() {
|
|
||||||
|
if !texts.is_empty() {
|
||||||
|
devices.retain(|device| device.names.iter().any(|n| texts.iter().any(|t| n == t)));
|
||||||
|
}
|
||||||
|
if !devs.is_empty() {
|
||||||
devices.retain(|device| {
|
devices.retain(|device| {
|
||||||
device
|
device
|
||||||
.interfaces
|
.interfaces
|
||||||
.iter()
|
.iter()
|
||||||
.any(|iface| query.filter.devs.contains(iface))
|
.any(|iface| devs.iter().any(|d| iface == d))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if !query.filter.ips.is_empty() {
|
if !ips.is_empty() {
|
||||||
devices.retain(|device| device.ips.iter().any(|ip| query.filter.ips.contains(ip)));
|
devices.retain(|device| device.ips.iter().any(|ip| ips.contains(ip)));
|
||||||
}
|
}
|
||||||
if !query.filter.macs.is_empty() {
|
if !macs.is_empty() {
|
||||||
devices.retain(|device| {
|
devices.retain(|device| device.macs.iter().any(|mac| macs.contains(mac)));
|
||||||
device
|
|
||||||
.macs
|
|
||||||
.iter()
|
|
||||||
.any(|mac| query.filter.macs.contains(mac))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if !query.filter.nuds.is_empty() {
|
if !nuds.is_empty() {
|
||||||
devices.retain(|device| {
|
devices.retain(|device| {
|
||||||
device
|
device
|
||||||
.neighbors
|
.neighbors
|
||||||
.iter()
|
.iter()
|
||||||
.any(|neighbor| query.filter.nuds.contains(&neighbor.state))
|
.any(|neighbor| nuds.contains(&neighbor.state))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,3 +117,60 @@ const fn presence_rank(presence: Presence) -> u8 {
|
|||||||
Presence::Offline => 0,
|
Presence::Offline => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
use wakey_core::{DhcpLease, InventoryQueryBuilder, NeighborState};
|
||||||
|
|
||||||
|
fn sample_neighbors() -> Vec<NeighborEntry> {
|
||||||
|
vec![NeighborEntry {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||||
|
dev: Some("br-lan".to_string()),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
state: NeighborState::Reachable,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_leases() -> Vec<DhcpLeaseWithState> {
|
||||||
|
vec![DhcpLeaseWithState {
|
||||||
|
lease_line: DhcpLease {
|
||||||
|
expires_epoch: 1,
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||||
|
mac: "aa:bb:cc:dd:ee:ff".parse().expect("mac"),
|
||||||
|
name: Some("pc".to_string()),
|
||||||
|
},
|
||||||
|
nud_state: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_applies_and_across_categories() {
|
||||||
|
let query = InventoryQueryBuilder::new()
|
||||||
|
.maybe_text(Some("pc".to_string()))
|
||||||
|
.interfaces(vec!["br-lan".to_string()])
|
||||||
|
.neighbor_states(vec![NeighborState::Reachable])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
|
||||||
|
let no_match_query = InventoryQueryBuilder::new()
|
||||||
|
.maybe_text(Some("pc".to_string()))
|
||||||
|
.interfaces(vec!["eth9".to_string()])
|
||||||
|
.build();
|
||||||
|
let out = merge_devices(sample_neighbors(), sample_leases(), &no_match_query);
|
||||||
|
assert!(out.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_allows_or_within_same_category() {
|
||||||
|
let query = InventoryQueryBuilder::new()
|
||||||
|
.neighbor_states(vec![NeighborState::Stale, NeighborState::Reachable])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let out = merge_devices(sample_neighbors(), sample_leases(), &query);
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-4
@@ -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};
|
||||||
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_inventory_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,
|
||||||
};
|
};
|
||||||
+8
-41
@@ -1,12 +1,12 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput};
|
use wakey_core::{InventoryQuery, Query, QueryInput};
|
||||||
|
|
||||||
/// Resolve free-form user input into the legacy `DeviceQuery` filter shape.
|
/// Resolve free-form user input into an `InventoryQuery` filter shape.
|
||||||
///
|
///
|
||||||
/// This is the compatibility entrypoint used by CLI and HTTP paths that still
|
/// This is the compatibility entrypoint used by CLI and HTTP paths that still
|
||||||
/// speak in terms of `DeviceQuery`.
|
/// speak in terms of query/filter payloads.
|
||||||
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
|
pub async fn resolve_query(input: impl Into<String>) -> Result<InventoryQuery> {
|
||||||
query_to_device_query(resolve_selector(input).await?)
|
query_to_inventory_query(resolve_selector(input).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Classify one piece of free-form user input into a typed selector.
|
/// Classify one piece of free-form user input into a typed selector.
|
||||||
@@ -25,43 +25,10 @@ pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the newer selector-oriented `Query` model into a `DeviceQuery`.
|
/// Convert the newer selector-oriented `Query` model into an `InventoryQuery`.
|
||||||
///
|
///
|
||||||
/// This keeps the old filter-based service and HTTP surfaces working while the
|
/// This keeps the old filter-based service and HTTP surfaces working while the
|
||||||
/// internals migrate toward selector- and device-oriented APIs.
|
/// internals migrate toward selector- and device-oriented APIs.
|
||||||
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
|
pub fn query_to_inventory_query(query: Query) -> Result<InventoryQuery> {
|
||||||
Ok(match query {
|
Ok(vec![query])
|
||||||
Query::Ip(ip_addr) => DeviceQuery {
|
|
||||||
filter: DeviceFilters {
|
|
||||||
ips: vec![ip_addr],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Query::Mac(mac_addr) => DeviceQuery {
|
|
||||||
filter: DeviceFilters {
|
|
||||||
macs: vec![mac_addr],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Query::Interface(dev) => DeviceQuery {
|
|
||||||
filter: DeviceFilters {
|
|
||||||
devs: vec![dev],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Query::NeighborState(state) => DeviceQuery {
|
|
||||||
filter: DeviceFilters {
|
|
||||||
nuds: vec![state],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Query::Text(name) => DeviceQuery {
|
|
||||||
name: Some(name),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+1
-14
@@ -1,14 +1 @@
|
|||||||
// pub const LDA_MACS: [[u8; 6]; 2] = [
|
// Intentionally left empty.
|
||||||
// // ether
|
|
||||||
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
|
|
||||||
// // wifi
|
|
||||||
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// is it time to lookup host lda.lan for this...
|
|
||||||
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
|
|
||||||
|
|
||||||
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
|
|
||||||
// this is so bad
|
|
||||||
pub mod ping;
|
|
||||||
pub mod query;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// this ENTIRE file is redundant... or?
|
|
||||||
|
|
||||||
use std::{net::IpAddr, time::Duration};
|
|
||||||
|
|
||||||
use tokio::{
|
|
||||||
net::{TcpStream, ToSocketAddrs},
|
|
||||||
time::timeout,
|
|
||||||
};
|
|
||||||
use wakey_core::NeighborState;
|
|
||||||
|
|
||||||
use crate::utils::query::get_mac;
|
|
||||||
|
|
||||||
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
|
||||||
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
}
|
|
||||||
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
|
|
||||||
todo!("use icmp")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
|
|
||||||
match get_mac(Some(addr.into()), None, &[] as &[NeighborState]).await {
|
|
||||||
Err(_) => 0,
|
|
||||||
Ok(l) => l
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| e.state)
|
|
||||||
.max()
|
|
||||||
.map(NeighborState::rank)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
pub mod dev {
|
|
||||||
pub use wakey_linux::devices::devs_sorted;
|
|
||||||
}
|
|
||||||
pub mod leases {
|
|
||||||
pub use wakey_linux::dhcp::enrich_leases_with_nud_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod parser {
|
|
||||||
pub use wakey_core::QueryInput as QueryType;
|
|
||||||
pub use wakey_linux::devices::classify_query as parse_query;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use leases::*;
|
|
||||||
pub use macs::*;
|
|
||||||
|
|
||||||
pub mod macs {
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use wakey_core::{NeighborEntry, NeighborState};
|
|
||||||
|
|
||||||
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
|
||||||
wakey_linux::devices::get_ips(machine_name).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_macs(
|
|
||||||
machine_names: &[impl AsRef<str>],
|
|
||||||
ips: &[IpAddr],
|
|
||||||
devs: &[impl AsRef<str>],
|
|
||||||
state: &[NeighborState],
|
|
||||||
macs: &[macaddr::MacAddr],
|
|
||||||
) -> Result<Vec<NeighborEntry>> {
|
|
||||||
wakey_linux::devices::get_neighbors(machine_names, ips, devs, state, macs).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_mac(
|
|
||||||
ip: Option<IpAddr>,
|
|
||||||
dev: Option<&str>,
|
|
||||||
state: &[NeighborState],
|
|
||||||
) -> Result<Vec<NeighborEntry>> {
|
|
||||||
let ips: Vec<IpAddr> = ip.into_iter().collect();
|
|
||||||
let devs: Vec<&str> = dev.into_iter().collect();
|
|
||||||
get_macs(&[] as &[&str], &ips, &devs, state, &[]).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use wakey::inventory;
|
use wakey::inventory;
|
||||||
use wakey_core::DeviceQuery;
|
use wakey_core::InventoryQuery;
|
||||||
|
|
||||||
#[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 inventory_real_router_prints_device_inventory() -> anyhow::Result<()> {
|
async fn inventory_real_router_prints_device_inventory() -> anyhow::Result<()> {
|
||||||
let inventory = inventory(DeviceQuery::default()).await?;
|
let inventory = inventory(InventoryQuery::default()).await?;
|
||||||
println!("{}", serde_json::to_string_pretty(&inventory)?);
|
println!("{}", serde_json::to_string_pretty(&inventory)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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::{InventoryQuery, Query};
|
||||||
|
|
||||||
#[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"]
|
||||||
@@ -17,47 +17,40 @@ 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(InventoryQuery::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(vec![Query::Interface(first.clone())]).await?;
|
||||||
name: None,
|
|
||||||
filter: DeviceFilters {
|
|
||||||
devs: vec![first.clone()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.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
@@ -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 } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -69,7 +51,7 @@ async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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_inventory_query()).await?;
|
||||||
debug!(
|
debug!(
|
||||||
rows = inventory.devices.len(),
|
rows = inventory.devices.len(),
|
||||||
"dispatched inventory command"
|
"dispatched inventory command"
|
||||||
@@ -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",
|
||||||
|
|||||||
+23
-55
@@ -4,7 +4,8 @@ use std::fmt;
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use wakey_core::parse::mac;
|
use wakey_core::parse::mac;
|
||||||
use wakey_core::{
|
use wakey_core::{
|
||||||
DeviceFilters, DeviceInventory, DeviceQuery, DhcpLeaseWithState, InterfaceSummary, WakeResult,
|
DeviceInventory, DhcpLeaseWithState, InterfaceSummary, InventoryQuery, InventoryQueryBuilder,
|
||||||
|
WakeResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@@ -66,48 +67,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)]
|
||||||
@@ -137,19 +96,30 @@ pub struct InventoryRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InventoryRequest {
|
impl InventoryRequest {
|
||||||
pub fn into_device_query(self) -> DeviceQuery {
|
pub fn into_inventory_query(self) -> InventoryQuery {
|
||||||
StatusRequest {
|
into_inventory_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_inventory_query(
|
||||||
|
query: Option<String>,
|
||||||
|
name: Option<String>,
|
||||||
|
ips: Vec<IpAddr>,
|
||||||
|
devs: Vec<String>,
|
||||||
|
nuds: Vec<wakey_core::NeighborState>,
|
||||||
|
macs: Vec<MacAddr>,
|
||||||
|
) -> InventoryQuery {
|
||||||
|
InventoryQueryBuilder::new()
|
||||||
|
.maybe_text(name.or(query))
|
||||||
|
.ips(ips)
|
||||||
|
.interfaces(devs)
|
||||||
|
.neighbor_states(nuds)
|
||||||
|
.macs(macs)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WakeRequest {
|
pub struct WakeRequest {
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
@@ -163,7 +133,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 +142,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),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ pub use device::{Device, DeviceId, DeviceInventory, Presence};
|
|||||||
pub use dhcp::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
|
pub use dhcp::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
|
||||||
pub use interface::{InterfaceAddr, InterfaceSummary};
|
pub use interface::{InterfaceAddr, InterfaceSummary};
|
||||||
pub use neighbor::{NeighborEntry, NeighborParseError, NeighborState, parse_neighbor_line};
|
pub use neighbor::{NeighborEntry, NeighborParseError, NeighborState, parse_neighbor_line};
|
||||||
pub use query::{DeviceFilters, DeviceQuery, NamePath, Query, QueryInput};
|
pub use query::{InventoryQuery, InventoryQueryBuilder, NamePath, Query, QueryInput};
|
||||||
pub use wake::{WakeResult, WakeStatus, WakeTarget, WakeTargetResult};
|
pub use wake::{WakeResult, WakeStatus, WakeTarget, WakeTargetResult};
|
||||||
@@ -1,37 +1,53 @@
|
|||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use crate::model::NeighborState;
|
use crate::model::NeighborState;
|
||||||
|
|
||||||
/// Legacy-compatible query shape used by HTTP and service adapters.
|
/// Canonical inventory query represented as an AND of selector terms.
|
||||||
///
|
pub type InventoryQuery = Vec<Query>;
|
||||||
/// `name` carries free-form text selection, while `filter` carries explicit
|
|
||||||
/// machine-readable filters such as IPs, MACs, interfaces, and neighbor states.
|
#[derive(Debug, Default, Clone)]
|
||||||
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
|
pub struct InventoryQueryBuilder {
|
||||||
pub struct DeviceQuery {
|
terms: InventoryQuery,
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub filter: DeviceFilters,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicit device filters for source- and service-level queries.
|
impl InventoryQueryBuilder {
|
||||||
#[serde_as]
|
pub fn new() -> Self {
|
||||||
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
|
Self::default()
|
||||||
pub struct DeviceFilters {
|
}
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
|
||||||
#[serde(default)]
|
pub fn maybe_text(mut self, text: Option<String>) -> Self {
|
||||||
pub ips: Vec<IpAddr>,
|
if let Some(text) = text {
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
self.terms.push(Query::Text(text));
|
||||||
#[serde(default)]
|
}
|
||||||
pub devs: Vec<String>,
|
self
|
||||||
#[serde_as(as = "OneOrMany<_>")]
|
}
|
||||||
#[serde(default)]
|
|
||||||
pub nuds: Vec<NeighborState>,
|
pub fn ips(mut self, values: impl IntoIterator<Item = IpAddr>) -> Self {
|
||||||
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
|
self.terms.extend(values.into_iter().map(Query::Ip));
|
||||||
#[serde(default)]
|
self
|
||||||
pub macs: Vec<MacAddr>,
|
}
|
||||||
|
|
||||||
|
pub fn interfaces(mut self, values: impl IntoIterator<Item = String>) -> Self {
|
||||||
|
self.terms.extend(values.into_iter().map(Query::Interface));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn neighbor_states(mut self, values: impl IntoIterator<Item = NeighborState>) -> Self {
|
||||||
|
self.terms
|
||||||
|
.extend(values.into_iter().map(Query::NeighborState));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn macs(mut self, values: impl IntoIterator<Item = MacAddr>) -> Self {
|
||||||
|
self.terms.extend(values.into_iter().map(Query::Mac));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> InventoryQuery {
|
||||||
|
self.terms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path helper for routes that receive a single `{name}` segment.
|
/// Path helper for routes that receive a single `{name}` segment.
|
||||||
@@ -51,7 +67,7 @@ pub enum QueryInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Higher-level typed selector used by the service layer.
|
/// Higher-level typed selector used by the service layer.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Query {
|
pub enum Query {
|
||||||
Text(String),
|
Text(String),
|
||||||
Ip(IpAddr),
|
Ip(IpAddr),
|
||||||
|
|||||||
@@ -1,59 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use lda_ipjs::subcommands::address;
|
use lda_ipjs::subcommands::address;
|
||||||
use std::collections::HashSet;
|
|
||||||
use wakey_core::{InterfaceAddr, InterfaceSummary};
|
use wakey_core::{InterfaceAddr, InterfaceSummary};
|
||||||
|
|
||||||
/// Discover interface names from Linux without requiring `ip`.
|
|
||||||
///
|
|
||||||
/// This is the lowest-common-denominator interface listing path and is used for
|
|
||||||
/// quick existence checks and legacy string-only callers.
|
|
||||||
pub async fn list_devs() -> HashSet<String> {
|
|
||||||
fn get_dev() -> HashSet<String> {
|
|
||||||
let mut devs: HashSet<String> = HashSet::new();
|
|
||||||
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
|
|
||||||
for e in rd.flatten() {
|
|
||||||
if e.file_type()
|
|
||||||
.map(|ft| {
|
|
||||||
if ft.is_symlink() {
|
|
||||||
std::fs::metadata(e.path())
|
|
||||||
.map(|m| m.is_dir())
|
|
||||||
.unwrap_or(false)
|
|
||||||
} else {
|
|
||||||
ft.is_dir()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
&& let Ok(name) = e.file_name().into_string()
|
|
||||||
&& name != "lo"
|
|
||||||
&& !name.is_empty()
|
|
||||||
{
|
|
||||||
devs.insert(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
|
|
||||||
for line in txt.lines().skip(2) {
|
|
||||||
if let Some((name, _rest)) = line.split_once(':') {
|
|
||||||
let n = name.trim().to_string();
|
|
||||||
if n != "lo" && !n.is_empty() {
|
|
||||||
devs.insert(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
devs
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::task::spawn_blocking(get_dev)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn devs_sorted() -> Vec<String> {
|
|
||||||
let mut v: Vec<String> = list_devs().await.into_iter().collect();
|
|
||||||
v.sort();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build condensed interface summaries from Linux address data.
|
/// Build condensed interface summaries from Linux address data.
|
||||||
///
|
///
|
||||||
/// On Unix this prefers the `ipjs` netlink-backed address path; elsewhere it
|
/// On Unix this prefers the `ipjs` netlink-backed address path; elsewhere it
|
||||||
@@ -100,7 +48,10 @@ pub async fn list_interface_summaries() -> Result<Vec<InterfaceSummary>> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether a named interface exists according to [`list_devs`].
|
/// Return whether a named non-loopback interface exists.
|
||||||
pub async fn has_dev(name: &str) -> bool {
|
pub async fn has_dev(name: &str) -> bool {
|
||||||
list_devs().await.contains(name)
|
match list_interface_summaries().await {
|
||||||
|
Ok(interfaces) => interfaces.iter().any(|iface| iface.ifname == name),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,6 @@ mod interfaces;
|
|||||||
mod neighbors;
|
mod neighbors;
|
||||||
mod query;
|
mod query;
|
||||||
|
|
||||||
pub use interfaces::{devs_sorted, has_dev, list_devs, list_interface_summaries};
|
pub use interfaces::{has_dev, list_interface_summaries};
|
||||||
pub use neighbors::{get_ips, get_neighbors, query_status};
|
pub use neighbors::{get_ips, get_neighbors, query_neighbors};
|
||||||
pub use query::classify_query;
|
pub use query::classify_query;
|
||||||
@@ -3,7 +3,7 @@ use futures::future::try_join_all;
|
|||||||
use lda_ipjs::subcommands::neighbor;
|
use lda_ipjs::subcommands::neighbor;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use wakey_core::{DeviceQuery, NeighborEntry, NeighborState};
|
use wakey_core::{InventoryQuery, NeighborEntry, NeighborState, Query};
|
||||||
|
|
||||||
/// Resolve a hostname through the local resolver and return all reported IPs.
|
/// Resolve a hostname through the local resolver and return all reported IPs.
|
||||||
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||||
@@ -106,16 +106,25 @@ pub async fn get_neighbors(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper around [`get_neighbors`] using the legacy `DeviceQuery`.
|
/// Convenience wrapper around [`get_neighbors`] using an `InventoryQuery` filter.
|
||||||
pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> {
|
pub async fn query_neighbors(query: &InventoryQuery) -> Result<Vec<NeighborEntry>> {
|
||||||
get_neighbors(
|
let mut names: Vec<&str> = Vec::new();
|
||||||
query.name.as_slice(),
|
let mut ips = Vec::new();
|
||||||
&query.filter.ips,
|
let mut devs: Vec<&str> = Vec::new();
|
||||||
&query.filter.devs,
|
let mut nuds = Vec::new();
|
||||||
&query.filter.nuds,
|
let mut macs = Vec::new();
|
||||||
&query.filter.macs,
|
|
||||||
)
|
for term in query {
|
||||||
.await
|
match term {
|
||||||
|
Query::Text(v) => names.push(v.as_str()),
|
||||||
|
Query::Ip(v) => ips.push(*v),
|
||||||
|
Query::Interface(v) => devs.push(v.as_str()),
|
||||||
|
Query::NeighborState(v) => nuds.push(*v),
|
||||||
|
Query::Mac(v) => macs.push(*v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_neighbors(&names, &ips, &devs, &nuds, &macs).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_ipjs_state(value: NeighborState) -> lda_ipjs::subcommands::neighbor::NUDState {
|
fn to_ipjs_state(value: NeighborState) -> lda_ipjs::subcommands::neighbor::NUDState {
|
||||||
|
|||||||
Reference in new issue
Block a user