Compare commits
3
Commits
981da39acf
...
0712e87e6f
No files matched your search
@@ -0,0 +1,60 @@
|
|||||||
|
# lda-ipjs
|
||||||
|
|
||||||
|
Typed Rust wrappers around Linux `ip -j ...` output, with optional experimental rtnetlink backends.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
`lda-ipjs` exists so `wakey` can ask Linux networking questions in typed Rust instead of:
|
||||||
|
|
||||||
|
- parsing plain-text shell output
|
||||||
|
- scattering `tokio::process::Command::new("ip")` everywhere
|
||||||
|
- mixing product logic with Linux networking trivia
|
||||||
|
|
||||||
|
## Current contract
|
||||||
|
|
||||||
|
Stable default behavior:
|
||||||
|
|
||||||
|
- `address::get(...)` uses JSON (`ip -j address show`)
|
||||||
|
- `link::get(...)` uses JSON (`ip -j link show`)
|
||||||
|
- `neighbor::get(...)` uses JSON (`ip -j neigh show`)
|
||||||
|
|
||||||
|
Optional experimental behavior:
|
||||||
|
|
||||||
|
- feature: `experimental-nl`
|
||||||
|
- enables rtnetlink-backed implementations
|
||||||
|
- intended for places where one-pass kernel queries are materially better than repeated `ip -j` calls
|
||||||
|
|
||||||
|
This means the public API is:
|
||||||
|
|
||||||
|
- `get(...)` for the default backend
|
||||||
|
- `get_with_backend(Backend::Json | Backend::Netlink)` when backend choice matters
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
- `subcommands::address`
|
||||||
|
- typed address/interface-address data
|
||||||
|
- good place for subnet/broadcast derivation later
|
||||||
|
- `subcommands::link`
|
||||||
|
- typed link/interface data
|
||||||
|
- useful for `ifindex -> ifname` mapping and interface metadata
|
||||||
|
- `subcommands::neighbor`
|
||||||
|
- typed neighbor-table data
|
||||||
|
- currently the most useful experimental netlink surface
|
||||||
|
|
||||||
|
## Backend policy
|
||||||
|
|
||||||
|
JSON is the normal path.
|
||||||
|
|
||||||
|
Use netlink only when:
|
||||||
|
|
||||||
|
- the call is hot enough to matter
|
||||||
|
- repeated shelling out is obviously wasteful
|
||||||
|
- the netlink implementation is at least as coherent as the JSON one
|
||||||
|
|
||||||
|
Today that mainly applies to `neighbor::nl`.
|
||||||
|
|
||||||
|
## Relationship to wakey
|
||||||
|
|
||||||
|
`wakey` is the product.
|
||||||
|
|
||||||
|
`lda-ipjs` is a Linux networking adapter crate underneath it.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::{io, process::Output};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use super::LinkOutput;
|
||||||
|
|
||||||
|
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||||
|
let output = _get(dev).await.context("Can not run command")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(String::from_utf8_lossy(&output.stderr).into_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_slice(&output.stdout).context("Deserialize failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _get(dev: Option<&str>) -> io::Result<Output> {
|
||||||
|
let mut cmd = tokio::process::Command::new("ip");
|
||||||
|
cmd.args(["-j", "link", "show"]);
|
||||||
|
|
||||||
|
if let Some(d) = dev {
|
||||||
|
cmd.arg(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.output().await
|
||||||
|
}
|
||||||
@@ -1 +1,35 @@
|
|||||||
//! this is for link. You need link; at least to build an index -> name map. i Need It. sometimes.
|
//! Typed wrappers for `ip -j link show`.
|
||||||
|
|
||||||
|
pub mod json;
|
||||||
|
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||||
|
pub mod nl;
|
||||||
|
|
||||||
|
pub use crate::subcommands::Backend;
|
||||||
|
use crate::utils::serialize::mac::option_mac;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LinkOutput {
|
||||||
|
pub ifindex: u32,
|
||||||
|
pub ifname: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub operstate: Option<String>,
|
||||||
|
#[serde(default, with = "option_mac")]
|
||||||
|
pub address: Option<MacAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||||
|
get_with_backend(Backend::Json, dev).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_with_backend(
|
||||||
|
backend: Backend,
|
||||||
|
dev: Option<&str>,
|
||||||
|
) -> anyhow::Result<Vec<LinkOutput>> {
|
||||||
|
match backend {
|
||||||
|
Backend::Json => json::get(dev).await,
|
||||||
|
#[cfg(all(unix, feature = "experimental-nl"))]
|
||||||
|
Backend::Netlink => nl::get(dev).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#![cfg(unix)]
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use rtnetlink::packet_route::link::LinkAttribute;
|
||||||
|
|
||||||
|
use super::LinkOutput;
|
||||||
|
|
||||||
|
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<LinkOutput>> {
|
||||||
|
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||||
|
tokio::spawn(conn);
|
||||||
|
|
||||||
|
let mut req = handle.link().get();
|
||||||
|
if let Some(dev) = dev {
|
||||||
|
req = req.match_name(dev.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream = req.execute();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
while let Some(link) = stream.try_next().await? {
|
||||||
|
let mut ifname = None;
|
||||||
|
let mut operstate = None;
|
||||||
|
let mut address = None;
|
||||||
|
|
||||||
|
for attr in link.attributes {
|
||||||
|
match attr {
|
||||||
|
LinkAttribute::IfName(name) => ifname = Some(name),
|
||||||
|
LinkAttribute::Address(bytes) => {
|
||||||
|
address = match bytes.len() {
|
||||||
|
6 => bytes.first_chunk::<6>().map(|&b| macaddr::MacAddr::from(b)),
|
||||||
|
8 => bytes.first_chunk::<8>().map(|&b| macaddr::MacAddr::from(b)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkAttribute::OperState(state) => operstate = Some(format!("{state:?}")),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ifname) = ifname {
|
||||||
|
out.push(LinkOutput {
|
||||||
|
ifindex: link.header.index,
|
||||||
|
ifname,
|
||||||
|
operstate,
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod address;
|
pub mod address;
|
||||||
|
pub mod link;
|
||||||
pub mod neighbor;
|
pub mod neighbor;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ use futures::TryStreamExt;
|
|||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use rtnetlink::packet_route::{
|
use rtnetlink::packet_route::{
|
||||||
AddressFamily,
|
AddressFamily,
|
||||||
link::LinkAttribute,
|
|
||||||
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{NUDState, NeighborItem};
|
use super::{NUDState, NeighborItem};
|
||||||
|
use crate::subcommands::link;
|
||||||
|
|
||||||
/// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
|
/// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
|
||||||
/// Non-empty slice = match ANY in the set.
|
/// Non-empty slice = match ANY in the set.
|
||||||
@@ -35,8 +35,12 @@ pub async fn get(
|
|||||||
let nud_set: HashSet<&NUDState> = nuds.iter().collect();
|
let nud_set: HashSet<&NUDState> = nuds.iter().collect();
|
||||||
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
|
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
|
||||||
|
|
||||||
// Cache ifindex -> name
|
// Prefetch all links once; link lookups were the ugliest and most expensive part.
|
||||||
let mut ifname_cache: HashMap<u32, String> = HashMap::new();
|
let ifname_cache: HashMap<u32, String> = link::nl::get(None)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|link| (link.ifindex, link.ifname))
|
||||||
|
.collect();
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
|
||||||
'row: while let Some(msg) = neighbor_data.try_next().await? {
|
'row: while let Some(msg) = neighbor_data.try_next().await? {
|
||||||
@@ -72,28 +76,7 @@ pub async fn get(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve ifindex -> name (cached)
|
// Resolve ifindex -> name (cached)
|
||||||
let dev = match ifname_cache.get(&msg.header.ifindex) {
|
let dev = ifname_cache.get(&msg.header.ifindex).cloned();
|
||||||
Some(name) => Some(name.clone()),
|
|
||||||
None => {
|
|
||||||
let name = handle
|
|
||||||
.link()
|
|
||||||
.get()
|
|
||||||
.match_index(msg.header.ifindex)
|
|
||||||
.execute()
|
|
||||||
.try_next()
|
|
||||||
.await?
|
|
||||||
.and_then(|link| {
|
|
||||||
link.attributes.into_iter().find_map(|a| match a {
|
|
||||||
LinkAttribute::IfName(n) => Some(n),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if let Some(ref n) = name {
|
|
||||||
ifname_cache.insert(msg.header.ifindex, n.clone());
|
|
||||||
}
|
|
||||||
name
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (Some(ip), Some(dev)) = (ip, dev) else {
|
let (Some(ip), Some(dev)) = (ip, dev) else {
|
||||||
continue 'row;
|
continue 'row;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ param(
|
|||||||
[string]$password,
|
[string]$password,
|
||||||
[switch]$Verbose,
|
[switch]$Verbose,
|
||||||
[string]$RemoteTestPath = "/root/.bin/test",
|
[string]$RemoteTestPath = "/root/.bin/test",
|
||||||
[string]$RemoteHost = "[email protected]"
|
[string]$RemoteHost = "[email protected]",
|
||||||
|
[switch]$Ignored
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -54,10 +55,11 @@ foreach ($testBinary in $testBinaries) {
|
|||||||
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet
|
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet
|
||||||
|
|
||||||
# Run on target
|
# Run on target
|
||||||
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
|
$parts = @()
|
||||||
if ($TestName) {
|
if ($TestName) { $parts += $TestName }
|
||||||
$testArgs = "$TestName $testArgs"
|
if ($Ignored) { $parts += "--ignored" }
|
||||||
}
|
if ($Verbose) { $parts += "--nocapture"; $parts += "--show-output" }
|
||||||
|
$testArgs = $parts -join " "
|
||||||
|
|
||||||
Invoke-Ssh -Cmd "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs" -Remote $RemoteHost -Pass $password
|
Invoke-Ssh -Cmd "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs" -Remote $RemoteHost -Pass $password
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-1
@@ -1,7 +1,8 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use wakey_core::parse::mac;
|
use wakey_core::parse::mac;
|
||||||
use wakey_core::{
|
use wakey_core::{
|
||||||
DeviceFilters, DhcpLeaseWithState, NeighborEntry, Status, WakeResult, WakeTargetResult,
|
Device, DeviceFilters, DeviceInventory, DhcpLeaseWithState, NeighborEntry, Status, WakeResult,
|
||||||
|
WakeTargetResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -50,6 +51,23 @@ pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn legacy_status_from_inventory(
|
||||||
|
inventory: DeviceInventory,
|
||||||
|
name: Option<String>,
|
||||||
|
filters: DeviceFilters,
|
||||||
|
) -> LegacyStatusResponse {
|
||||||
|
let table = inventory
|
||||||
|
.devices
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(legacy_status_rows_from_device)
|
||||||
|
.collect();
|
||||||
|
LegacyStatusResponse {
|
||||||
|
name,
|
||||||
|
table,
|
||||||
|
filters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
|
pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
|
||||||
LegacyStatusRow {
|
LegacyStatusRow {
|
||||||
ip: row.ip,
|
ip: row.ip,
|
||||||
@@ -59,6 +77,36 @@ pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn legacy_status_rows_from_device(device: Device) -> Vec<LegacyStatusRow> {
|
||||||
|
if !device.neighbors.is_empty() {
|
||||||
|
return device
|
||||||
|
.neighbors
|
||||||
|
.into_iter()
|
||||||
|
.map(legacy_status_row)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallback_mac = device.macs.first().copied();
|
||||||
|
let fallback_dev = device.interfaces.first().cloned();
|
||||||
|
let fallback_state = match device.presence {
|
||||||
|
wakey_core::Presence::Online => wakey_core::NeighborState::Reachable,
|
||||||
|
wakey_core::Presence::LikelyOnline => wakey_core::NeighborState::Stale,
|
||||||
|
wakey_core::Presence::Offline => wakey_core::NeighborState::Failed,
|
||||||
|
wakey_core::Presence::Unknown => wakey_core::NeighborState::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
device
|
||||||
|
.ips
|
||||||
|
.into_iter()
|
||||||
|
.map(|ip| LegacyStatusRow {
|
||||||
|
ip,
|
||||||
|
dev: fallback_dev.clone(),
|
||||||
|
mac: fallback_mac,
|
||||||
|
state: fallback_state,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> {
|
pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> {
|
||||||
leases.into_iter().map(legacy_lease_row).collect()
|
leases.into_iter().map(legacy_lease_row).collect()
|
||||||
}
|
}
|
||||||
@@ -110,6 +158,25 @@ mod tests {
|
|||||||
assert_eq!(legacy.table[0].state, NeighborState::Reachable);
|
assert_eq!(legacy.table[0].state, NeighborState::Reachable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_inventory_to_legacy_status_shape() {
|
||||||
|
let inventory = DeviceInventory {
|
||||||
|
devices: vec![Device::from_parts(
|
||||||
|
vec![NeighborEntry {
|
||||||
|
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||||
|
dev: Some("br-lan".into()),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
state: NeighborState::Reachable,
|
||||||
|
}],
|
||||||
|
vec![],
|
||||||
|
)],
|
||||||
|
};
|
||||||
|
let legacy = legacy_status_from_inventory(inventory, Some("pc".into()), DeviceFilters::default());
|
||||||
|
assert_eq!(legacy.name.as_deref(), Some("pc"));
|
||||||
|
assert_eq!(legacy.table.len(), 1);
|
||||||
|
assert_eq!(legacy.table[0].state, NeighborState::Reachable);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_leases_to_legacy_shape() {
|
fn maps_leases_to_legacy_shape() {
|
||||||
let leases = vec![DhcpLeaseWithState {
|
let leases = vec![DhcpLeaseWithState {
|
||||||
|
|||||||
+35
-3
@@ -71,7 +71,12 @@ pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
||||||
let table = wakey_linux::devices::query_status(&query).await?;
|
let inventory = inventory(query.clone()).await?;
|
||||||
|
let table = inventory
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.flat_map(device_to_status_rows)
|
||||||
|
.collect();
|
||||||
Ok(Status {
|
Ok(Status {
|
||||||
name: query.name,
|
name: query.name,
|
||||||
table,
|
table,
|
||||||
@@ -123,13 +128,13 @@ pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
||||||
let status = get_status(query.clone()).await?;
|
let neighbors = wakey_linux::devices::query_status(&query).await?;
|
||||||
let leases = get_leases(LeaseQuery {
|
let leases = get_leases(LeaseQuery {
|
||||||
include_state: false,
|
include_state: false,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Ok(DeviceInventory {
|
Ok(DeviceInventory {
|
||||||
devices: merge_devices(status.table, leases, &query),
|
devices: merge_devices(neighbors, leases, &query),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +226,33 @@ const fn presence_rank(presence: Presence) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> {
|
||||||
|
if !device.neighbors.is_empty() {
|
||||||
|
return device.neighbors.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallback_mac = device.macs.first().copied();
|
||||||
|
let fallback_dev = device.interfaces.first().cloned();
|
||||||
|
let fallback_state = match device.presence {
|
||||||
|
Presence::Online => wakey_core::NeighborState::Reachable,
|
||||||
|
Presence::LikelyOnline => wakey_core::NeighborState::Stale,
|
||||||
|
Presence::Offline => wakey_core::NeighborState::Failed,
|
||||||
|
Presence::Unknown => wakey_core::NeighborState::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
device
|
||||||
|
.ips
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|ip| NeighborEntry {
|
||||||
|
ip,
|
||||||
|
dev: fallback_dev.clone(),
|
||||||
|
mac: fallback_mac,
|
||||||
|
state: fallback_state,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn http_app(static_root: std::path::PathBuf) -> Router {
|
pub fn http_app(static_root: std::path::PathBuf) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/api", route::api_router())
|
.nest("/api", route::api_router())
|
||||||
|
|||||||
+7
-3
@@ -4,10 +4,14 @@ use axum_extra::extract::Query;
|
|||||||
pub use wakey_core::{DeviceQuery, NamePath};
|
pub use wakey_core::{DeviceQuery, NamePath};
|
||||||
|
|
||||||
pub async fn get_status_json(Query(query): Query<DeviceQuery>) -> impl IntoResponse {
|
pub async fn get_status_json(Query(query): Query<DeviceQuery>) -> impl IntoResponse {
|
||||||
match crate::get_status(query).await {
|
match crate::inventory(query.clone()).await {
|
||||||
Ok(status) => (
|
Ok(inventory) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(crate::compat::legacy_status_from_domain(status)),
|
Json(crate::compat::legacy_status_from_inventory(
|
||||||
|
inventory,
|
||||||
|
query.name,
|
||||||
|
query.filter,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
Err(error) => ApiError {
|
Err(error) => ApiError {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use wakey::inventory;
|
||||||
|
use wakey_core::DeviceQuery;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[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<()> {
|
||||||
|
let inventory = inventory(DeviceQuery::default()).await?;
|
||||||
|
println!("{}", serde_json::to_string_pretty(&inventory)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user