sub-logic is own function
- lda
This commit is contained in:
@@ -1,17 +1,186 @@
|
|||||||
# wakey
|
# wakey
|
||||||
|
|
||||||
router shit
|
`wakey` is a Wake-on-LAN and LAN-observability tool for a Linux router.
|
||||||
|
|
||||||
## whatever i did must be BEAUTIFUL
|
It started as a small web UI running on-device. It is now being reshaped into a
|
||||||
|
service-first project with:
|
||||||
|
|
||||||
read [in vietnamese](https://ldlda.com/blogs/else/ssh-ax3000cv2-update-firmware-0-10-2)
|
- a reusable core model
|
||||||
|
- a Linux/OpenWrt adapter layer
|
||||||
|
- a CLI for operators
|
||||||
|
- a temporary legacy HTTP/static adapter during migration
|
||||||
|
|
||||||
## now i NEED the split arch to be on
|
## What it does
|
||||||
|
|
||||||
like RN rn rn so i can have web outside and the lil agent can be sitting here configuring itself (Parse/append to rc.local files? add to init.d? fuhh)
|
`wakey` currently focuses on a small set of router-side jobs:
|
||||||
|
|
||||||
I NEED IT
|
- inspect LAN neighbor state
|
||||||
|
- read DHCP lease data
|
||||||
|
- merge those facts into a higher-level device inventory
|
||||||
|
- list useful interface/broadcast information
|
||||||
|
- send Wake-on-LAN packets
|
||||||
|
|
||||||
I NEEED IT
|
In practice that means commands like:
|
||||||
|
|
||||||
I NEEEEEED it.
|
```sh
|
||||||
|
wakey status bedroom-pc
|
||||||
|
wakey leases --include-state
|
||||||
|
wakey devs
|
||||||
|
wakey wake bedroom-pc
|
||||||
|
wakey wake --mac aa:bb:cc:dd:ee:ff
|
||||||
|
wakey http --host :: --port 12012
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workspace layout
|
||||||
|
|
||||||
|
- `wakey-core`
|
||||||
|
- shared types and parsing
|
||||||
|
- device, neighbor, DHCP, interface, and wake models
|
||||||
|
- `wakey-linux`
|
||||||
|
- Linux/OpenWrt adapter
|
||||||
|
- DHCP lease loading, interface summaries, neighbor lookup, WoL sending
|
||||||
|
- `wakey`
|
||||||
|
- service layer, CLI, and temporary HTTP/static adapter
|
||||||
|
- `ipjs`
|
||||||
|
- typed wrappers around Linux `ip -j ...` data
|
||||||
|
- JSON-first, with optional experimental netlink backends
|
||||||
|
|
||||||
|
## Current architecture
|
||||||
|
|
||||||
|
Inside the `wakey` crate:
|
||||||
|
|
||||||
|
- `src/service`
|
||||||
|
- the real use-case layer
|
||||||
|
- status, leases, inventory, interfaces, wake, and query resolution
|
||||||
|
- `src/http`
|
||||||
|
- temporary legacy HTTP/static adapter
|
||||||
|
- compatibility mapping for the current `/static` client
|
||||||
|
- `src/legacy`
|
||||||
|
- transitional compatibility wrappers kept during the migration
|
||||||
|
|
||||||
|
The long-term direction is:
|
||||||
|
|
||||||
|
- keep the service layer stable
|
||||||
|
- keep HTTP as an adapter, not the architecture
|
||||||
|
- eventually move toward an agent + control-plane model
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
`wakey` is usable as a local/operator CLI.
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
Show device status rows using a free-form selector:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey status bedroom-pc
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use explicit filters:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey status --dev br-lan --nud reachable
|
||||||
|
wakey status --mac aa:bb:cc:dd:ee:ff
|
||||||
|
wakey status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Leases
|
||||||
|
|
||||||
|
Show DHCP leases:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey leases
|
||||||
|
wakey leases --include-state
|
||||||
|
wakey leases --include-state --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wake
|
||||||
|
|
||||||
|
Query mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey wake bedroom-pc
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicit/manual mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey wake --mac aa:bb:cc:dd:ee:ff
|
||||||
|
wakey wake --mac aa:bb:cc:dd:ee:ff --ip 192.168.1.255
|
||||||
|
wakey wake --mac aa:bb:cc:dd:ee:ff --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- query mode and explicit `--mac/--ip` mode are mutually exclusive
|
||||||
|
- `--ip` requires `--mac`
|
||||||
|
- `--mac` without `--ip` fans out to interface broadcast targets
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
|
||||||
|
Show condensed interface summaries:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey devs
|
||||||
|
wakey devs br-lan
|
||||||
|
wakey devs --up
|
||||||
|
wakey devs --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Temporary HTTP adapter
|
||||||
|
|
||||||
|
The old web/static app can still be served during migration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wakey http --host :: --port 12012
|
||||||
|
```
|
||||||
|
|
||||||
|
This should be treated as a compatibility surface, not the long-term product
|
||||||
|
shape.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
This repo has two useful testing modes:
|
||||||
|
|
||||||
|
- local compile checks
|
||||||
|
- live on-device tests against the real router/runtime environment
|
||||||
|
|
||||||
|
### Local
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo check
|
||||||
|
cargo test --no-run
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
### On-device
|
||||||
|
|
||||||
|
Some integration tests are intentionally `#[ignore]` because they use real
|
||||||
|
router state. Use the PowerShell helper:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Ignored -NoCapture
|
||||||
|
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_inventory -Ignored -NoCapture
|
||||||
|
```
|
||||||
|
|
||||||
|
You can further narrow execution with `-Filter` to select individual Rust test
|
||||||
|
functions inside a test binary.
|
||||||
|
|
||||||
|
## Build target
|
||||||
|
|
||||||
|
This project is primarily aimed at an OpenWrt/Linux ARM router target. The
|
||||||
|
workspace is commonly built for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
armv7-unknown-linux-musleabihf
|
||||||
|
```
|
||||||
|
|
||||||
|
Some crates and tests are Linux-specific by design.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `ipjs` is JSON-first by default; experimental netlink paths exist where they
|
||||||
|
are worth keeping.
|
||||||
|
- the current web client is temporary and kept alive through explicit
|
||||||
|
compatibility mapping
|
||||||
|
- the service layer is the part intended to survive the migration
|
||||||
|
|||||||
@@ -43,6 +43,51 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
|
|||||||
- Uploads to `<RemotePath>.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD.
|
- Uploads to `<RemotePath>.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD.
|
||||||
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
|
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
|
||||||
- `publish.ps1` — Optional version bump + build + tag (and `cargo publish` only if you pass `-Publish`).
|
- `publish.ps1` — Optional version bump + build + tag (and `cargo publish` only if you pass `-Publish`).
|
||||||
|
- `test_remote.ps1` — Build ARM Linux test binaries locally, upload them to the router, and run them there.
|
||||||
|
|
||||||
|
## Remote test runner
|
||||||
|
|
||||||
|
`test_remote.ps1` is the main way to run Linux/router-specific tests that do not
|
||||||
|
make sense on Windows.
|
||||||
|
|
||||||
|
Basic examples:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./scripts/test_remote.ps1 -Package wakey
|
||||||
|
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Ignored -NoCapture
|
||||||
|
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Filter status_real_router_default_query_succeeds -Ignored -NoCapture
|
||||||
|
./scripts/test_remote.ps1 -AllPackages
|
||||||
|
```
|
||||||
|
|
||||||
|
Important semantics:
|
||||||
|
|
||||||
|
- `-BinaryFilter`
|
||||||
|
- selects which compiled Rust test executable(s) to run
|
||||||
|
- `-Filter`
|
||||||
|
- filters test functions inside a selected Rust test binary
|
||||||
|
- `-Ignored`
|
||||||
|
- runs `#[ignore]` tests
|
||||||
|
- `-IncludeIgnored`
|
||||||
|
- includes ignored tests in addition to normal ones
|
||||||
|
- `-List`
|
||||||
|
- lists tests inside the remote binary instead of executing them
|
||||||
|
|
||||||
|
Useful flags:
|
||||||
|
|
||||||
|
- `-BuildProfile debug|release`
|
||||||
|
- `-Exact`
|
||||||
|
- `-NoCapture`
|
||||||
|
- `-ShowOutput`
|
||||||
|
- `-Threads <n>`
|
||||||
|
- `-RemoteHost root@<router-ip>`
|
||||||
|
- `-RemoteTestPath /tmp/tmp/wakey-test`
|
||||||
|
|
||||||
|
Implementation notes:
|
||||||
|
|
||||||
|
- test executables are discovered via Cargo JSON messages, not regexing human output
|
||||||
|
- binaries are batch-uploaded per package run
|
||||||
|
- each package run gets a unique remote temp directory for safer concurrent use
|
||||||
|
- packages with no test executables are skipped instead of treated as failures
|
||||||
|
|
||||||
## CI (Gitea)
|
## CI (Gitea)
|
||||||
|
|
||||||
|
|||||||
+112
-15
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use wakey_core::{WakeResult, WakeTarget};
|
use wakey_core::{InterfaceSummary, WakeResult, WakeTarget};
|
||||||
|
|
||||||
use crate::service::interfaces::get_interface_summaries;
|
use crate::service::interfaces::get_interface_summaries;
|
||||||
use crate::service::inventory::resolve_devices;
|
use crate::service::inventory::resolve_devices;
|
||||||
@@ -24,25 +24,14 @@ pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
|
|||||||
///
|
///
|
||||||
/// This is used by explicit manual wake mode when only a MAC address is supplied.
|
/// This is used by explicit manual wake mode when only a MAC address is supplied.
|
||||||
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
|
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
|
||||||
Ok(get_interface_summaries()
|
let interfaces = get_interface_summaries().await?;
|
||||||
.await?
|
broadcast_wake_targets_from_interfaces(&interfaces, mac)
|
||||||
.into_iter()
|
|
||||||
.flat_map(|iface| iface.addrs.into_iter())
|
|
||||||
.filter_map(|addr| addr.broadcast)
|
|
||||||
.map(|ip| WakeTarget {
|
|
||||||
ip: Some(IpAddr::V4(ip)),
|
|
||||||
mac: Some(mac),
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wake a device explicitly by MAC, optionally targeting a specific IP/broadcast.
|
/// Wake a device explicitly by MAC, optionally targeting a specific IP/broadcast.
|
||||||
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
|
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
|
||||||
let targets = match ip {
|
let targets = match ip {
|
||||||
Some(ip) => vec![WakeTarget {
|
Some(ip) => explicit_wake_targets_for_ip(mac, ip),
|
||||||
ip: Some(ip),
|
|
||||||
mac: Some(mac),
|
|
||||||
}],
|
|
||||||
None => broadcast_wake_targets(mac).await?,
|
None => broadcast_wake_targets(mac).await?,
|
||||||
};
|
};
|
||||||
wake_targets(targets).await
|
wake_targets(targets).await
|
||||||
@@ -65,3 +54,111 @@ pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTa
|
|||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn explicit_wake_targets_for_ip(mac: MacAddr, ip: IpAddr) -> Vec<WakeTarget> {
|
||||||
|
vec![WakeTarget {
|
||||||
|
ip: Some(ip),
|
||||||
|
mac: Some(mac),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast_wake_targets_from_interfaces(
|
||||||
|
interfaces: &[InterfaceSummary],
|
||||||
|
mac: MacAddr,
|
||||||
|
) -> Result<Vec<WakeTarget>> {
|
||||||
|
let targets: Vec<WakeTarget> = interfaces
|
||||||
|
.iter()
|
||||||
|
.flat_map(|iface| iface.addrs.iter())
|
||||||
|
.filter_map(|addr| addr.broadcast)
|
||||||
|
.map(|ip| WakeTarget {
|
||||||
|
ip: Some(IpAddr::V4(ip)),
|
||||||
|
mac: Some(mac),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if targets.is_empty() {
|
||||||
|
anyhow::bail!("no broadcast-capable interfaces found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
use super::{broadcast_wake_targets_from_interfaces, explicit_wake_targets_for_ip};
|
||||||
|
use wakey_core::{InterfaceAddr, InterfaceSummary};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_wake_target_for_ip_builds_one_complete_target() {
|
||||||
|
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
|
||||||
|
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255));
|
||||||
|
|
||||||
|
let targets = explicit_wake_targets_for_ip(mac, ip);
|
||||||
|
|
||||||
|
assert_eq!(targets.len(), 1);
|
||||||
|
assert_eq!(targets[0].ip, Some(ip));
|
||||||
|
assert_eq!(targets[0].mac, Some(mac));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn broadcast_wake_targets_from_interfaces_errors_when_no_broadcast_exists() {
|
||||||
|
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
|
||||||
|
let interfaces = vec![InterfaceSummary {
|
||||||
|
ifindex: 1,
|
||||||
|
ifname: "eth0".into(),
|
||||||
|
operstate: "up".into(),
|
||||||
|
mac: None,
|
||||||
|
addrs: vec![InterfaceAddr {
|
||||||
|
family: Some("inet".into()),
|
||||||
|
cidr: Some("192.168.1.10/24".into()),
|
||||||
|
broadcast: None,
|
||||||
|
scope: Some("global".into()),
|
||||||
|
label: None,
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let err = broadcast_wake_targets_from_interfaces(&interfaces, mac)
|
||||||
|
.expect_err("should error without broadcast-capable interfaces");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("no broadcast-capable interfaces found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn broadcast_wake_targets_from_interfaces_builds_targets_from_broadcast_rows() {
|
||||||
|
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
|
||||||
|
let interfaces = vec![InterfaceSummary {
|
||||||
|
ifindex: 2,
|
||||||
|
ifname: "br-lan".into(),
|
||||||
|
operstate: "up".into(),
|
||||||
|
mac: None,
|
||||||
|
addrs: vec![
|
||||||
|
InterfaceAddr {
|
||||||
|
family: Some("inet".into()),
|
||||||
|
cidr: Some("192.168.1.1/24".into()),
|
||||||
|
broadcast: Some(Ipv4Addr::new(192, 168, 1, 255)),
|
||||||
|
scope: Some("global".into()),
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
InterfaceAddr {
|
||||||
|
family: Some("inet6".into()),
|
||||||
|
cidr: Some("fe80::1/64".into()),
|
||||||
|
broadcast: None,
|
||||||
|
scope: Some("link".into()),
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let targets = broadcast_wake_targets_from_interfaces(&interfaces, mac)
|
||||||
|
.expect("broadcast target resolution should succeed");
|
||||||
|
|
||||||
|
assert_eq!(targets.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
targets[0].ip,
|
||||||
|
Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))
|
||||||
|
);
|
||||||
|
assert_eq!(targets[0].mac, Some(mac));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user