model device endpoints for fleet wake routes
This commit is contained in:
@@ -181,7 +181,7 @@ const fn presence_rank(presence: Presence) -> u8 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use wakey_core::{DeviceId, DhcpLease, InventoryQueryBuilder, NeighborState};
|
use wakey_core::{DeviceId, DhcpLease, EndpointSource, InventoryQueryBuilder, NeighborState};
|
||||||
|
|
||||||
fn sample_neighbors() -> Vec<NeighborEntry> {
|
fn sample_neighbors() -> Vec<NeighborEntry> {
|
||||||
vec![NeighborEntry {
|
vec![NeighborEntry {
|
||||||
@@ -279,11 +279,14 @@ mod tests {
|
|||||||
Some(DeviceId::Mac("aa:bb:cc:dd:ee:ff".parse().expect("mac")))
|
Some(DeviceId::Mac("aa:bb:cc:dd:ee:ff".parse().expect("mac")))
|
||||||
);
|
);
|
||||||
assert_eq!(out[0].observations.len(), 1);
|
assert_eq!(out[0].observations.len(), 1);
|
||||||
|
assert_eq!(out[0].endpoints.len(), 1);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookDhcp);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
assert_eq!(out[0].presence, Presence::Unknown);
|
assert_eq!(out[0].presence, Presence::Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_devices_marks_remove_only_observation_offline() {
|
fn merge_devices_marks_neigh_remove_observation_offline() {
|
||||||
let query = InventoryQueryBuilder::new().build();
|
let query = InventoryQueryBuilder::new().build();
|
||||||
let out = merge_devices_with_observations(
|
let out = merge_devices_with_observations(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -302,5 +305,56 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(out.len(), 1);
|
assert_eq!(out.len(), 1);
|
||||||
assert_eq!(out[0].presence, Presence::Offline);
|
assert_eq!(out[0].presence, Presence::Offline);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookNeighbor);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_keeps_dhcp_remove_observation_unknown() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "dhcp".into(),
|
||||||
|
action: "remove".into(),
|
||||||
|
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||||
|
ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 30))),
|
||||||
|
hostname: None,
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].presence, Presence::Unknown);
|
||||||
|
assert_eq!(out[0].endpoints[0].key.source, EndpointSource::HookDhcp);
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_devices_uses_ip_id_for_ip_only_hook_endpoint_without_summary_ip() {
|
||||||
|
let query = InventoryQueryBuilder::new().build();
|
||||||
|
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 31));
|
||||||
|
let out = merge_devices_with_observations(
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![DeviceObservationFact {
|
||||||
|
kind: "neigh".into(),
|
||||||
|
action: "remove".into(),
|
||||||
|
mac: None,
|
||||||
|
ip: Some(ip),
|
||||||
|
hostname: None,
|
||||||
|
first_seen_unix: Some(10),
|
||||||
|
last_seen_unix: Some(20),
|
||||||
|
}],
|
||||||
|
&query,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(out.len(), 1);
|
||||||
|
assert_eq!(out[0].id, Some(DeviceId::Ip(ip)));
|
||||||
|
assert!(out[0].ips.is_empty());
|
||||||
|
assert_eq!(out[0].endpoints.len(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,29 @@ CREATE TABLE agent_device_ips (
|
|||||||
CREATE INDEX agent_device_ips_ip_idx
|
CREATE INDEX agent_device_ips_ip_idx
|
||||||
ON agent_device_ips(ip);
|
ON agent_device_ips(ip);
|
||||||
|
|
||||||
|
CREATE TABLE agent_device_endpoints (
|
||||||
|
agent_id TEXT NOT NULL,
|
||||||
|
device_key TEXT NOT NULL,
|
||||||
|
endpoint_key TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL,
|
||||||
|
mac TEXT,
|
||||||
|
ip TEXT,
|
||||||
|
hostname TEXT,
|
||||||
|
interface TEXT,
|
||||||
|
presence TEXT NOT NULL,
|
||||||
|
first_seen_unix INTEGER NOT NULL,
|
||||||
|
last_seen_unix INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (agent_id, device_key, endpoint_key),
|
||||||
|
FOREIGN KEY (agent_id, device_key)
|
||||||
|
REFERENCES agent_devices(agent_id, device_key) ON DELETE CASCADE
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
|
CREATE INDEX agent_device_endpoints_mac_idx
|
||||||
|
ON agent_device_endpoints(mac);
|
||||||
|
|
||||||
|
CREATE INDEX agent_device_endpoints_ip_idx
|
||||||
|
ON agent_device_endpoints(ip);
|
||||||
|
|
||||||
CREATE TABLE agent_device_hostnames (
|
CREATE TABLE agent_device_hostnames (
|
||||||
agent_id TEXT NOT NULL,
|
agent_id TEXT NOT NULL,
|
||||||
device_key TEXT NOT NULL,
|
device_key TEXT NOT NULL,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use wakey_core::Presence;
|
|||||||
|
|
||||||
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
||||||
|
|
||||||
use super::types::{FleetDevice, FleetDeviceAgent, FleetWakeRoute, ListFleetDevicesQuery};
|
use super::types::{
|
||||||
|
FleetDevice, FleetDeviceAgent, FleetDeviceEndpoint, FleetWakeRoute, ListFleetDevicesQuery,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct FleetBuildContext {
|
pub(crate) struct FleetBuildContext {
|
||||||
@@ -31,6 +33,7 @@ struct FleetAccumulator {
|
|||||||
hostnames: BTreeSet<String>,
|
hostnames: BTreeSet<String>,
|
||||||
sources: BTreeSet<String>,
|
sources: BTreeSet<String>,
|
||||||
agents: BTreeMap<String, FleetDeviceAgent>,
|
agents: BTreeMap<String, FleetDeviceAgent>,
|
||||||
|
endpoints: Vec<FleetDeviceEndpoint>,
|
||||||
first_seen_unix: Option<u64>,
|
first_seen_unix: Option<u64>,
|
||||||
last_seen_unix: Option<u64>,
|
last_seen_unix: Option<u64>,
|
||||||
presence: Presence,
|
presence: Presence,
|
||||||
@@ -49,6 +52,7 @@ impl Default for FleetAccumulator {
|
|||||||
hostnames: BTreeSet::new(),
|
hostnames: BTreeSet::new(),
|
||||||
sources: BTreeSet::new(),
|
sources: BTreeSet::new(),
|
||||||
agents: BTreeMap::new(),
|
agents: BTreeMap::new(),
|
||||||
|
endpoints: Vec::new(),
|
||||||
first_seen_unix: None,
|
first_seen_unix: None,
|
||||||
last_seen_unix: None,
|
last_seen_unix: None,
|
||||||
presence: Presence::Offline,
|
presence: Presence::Offline,
|
||||||
@@ -213,7 +217,6 @@ fn add_agent_device_to_entry(
|
|||||||
}
|
}
|
||||||
entry.hostnames.insert(hostname.clone());
|
entry.hostnames.insert(hostname.clone());
|
||||||
}
|
}
|
||||||
entry.sources.insert("device".to_string());
|
|
||||||
let first_seen = agent_device.device.first_seen();
|
let first_seen = agent_device.device.first_seen();
|
||||||
let last_seen = agent_device.device.last_seen();
|
let last_seen = agent_device.device.last_seen();
|
||||||
entry.first_seen_unix = Some(
|
entry.first_seen_unix = Some(
|
||||||
@@ -255,30 +258,83 @@ fn add_agent_device_to_entry(
|
|||||||
last_seen_unix: last_seen,
|
last_seen_unix: last_seen,
|
||||||
});
|
});
|
||||||
|
|
||||||
for mac in &agent_device.macs {
|
if agent_device.endpoints.is_empty() {
|
||||||
let ip_for_mac = agent_device.ips.first().copied();
|
entry.sources.insert("device".to_string());
|
||||||
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
}
|
||||||
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device");
|
|
||||||
let wakeable = status.connected && !device_offline;
|
for endpoint in &agent_device.endpoints {
|
||||||
|
let source = endpoint_source_label(endpoint.key.source).to_string();
|
||||||
|
entry.sources.insert(source.clone());
|
||||||
|
let endpoint_last_seen = endpoint.last_seen_unix.unwrap_or(last_seen);
|
||||||
|
let endpoint_first_seen = endpoint.first_seen_unix.or(Some(first_seen));
|
||||||
|
entry.endpoints.push(FleetDeviceEndpoint {
|
||||||
|
agent_id: agent_id.clone(),
|
||||||
|
nickname: status.nickname.clone(),
|
||||||
|
connected: status.connected,
|
||||||
|
source: source.clone(),
|
||||||
|
mac: endpoint.key.mac,
|
||||||
|
ip: endpoint.key.ip,
|
||||||
|
hostname: endpoint.hostname.clone(),
|
||||||
|
interface: endpoint.interface.clone(),
|
||||||
|
presence: endpoint.presence,
|
||||||
|
first_seen_unix: endpoint_first_seen,
|
||||||
|
last_seen_unix: Some(endpoint_last_seen),
|
||||||
|
});
|
||||||
|
|
||||||
|
let rid = route_id(
|
||||||
|
&agent_id,
|
||||||
|
endpoint.key.mac.as_ref(),
|
||||||
|
endpoint.key.ip.as_ref(),
|
||||||
|
&source,
|
||||||
|
);
|
||||||
|
let wakeable = status.connected && endpoint.key.mac.is_some();
|
||||||
entry.routes.insert(
|
entry.routes.insert(
|
||||||
// one per mac... when is one per mac/ip pair? this a regression.
|
|
||||||
rid.clone(),
|
rid.clone(),
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
nickname: status.nickname.clone(),
|
nickname: status.nickname.clone(),
|
||||||
connected: status.connected,
|
connected: status.connected,
|
||||||
mac: Some(*mac),
|
mac: endpoint.key.mac,
|
||||||
ip: ip_for_mac,
|
ip: endpoint.key.ip,
|
||||||
hostname: hostname_for_mac,
|
hostname: endpoint.hostname.clone(),
|
||||||
source: "device".to_string(),
|
interface: endpoint.interface.clone(),
|
||||||
last_seen_unix: last_seen,
|
source,
|
||||||
|
presence: endpoint.presence,
|
||||||
|
last_seen_unix: endpoint_last_seen,
|
||||||
wakeable,
|
wakeable,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if agent_device.macs.is_empty()
|
if agent_device.endpoints.is_empty() && !agent_device.macs.is_empty() {
|
||||||
|
for mac in &agent_device.macs {
|
||||||
|
let ip_for_mac = agent_device.ips.first().copied();
|
||||||
|
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
||||||
|
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device");
|
||||||
|
let wakeable = status.connected;
|
||||||
|
entry.routes.insert(
|
||||||
|
rid.clone(),
|
||||||
|
FleetWakeRoute {
|
||||||
|
route_id: rid,
|
||||||
|
agent_id: agent_id.clone(),
|
||||||
|
nickname: status.nickname.clone(),
|
||||||
|
connected: status.connected,
|
||||||
|
mac: Some(*mac),
|
||||||
|
ip: ip_for_mac,
|
||||||
|
hostname: hostname_for_mac,
|
||||||
|
interface: None,
|
||||||
|
source: "device".to_string(),
|
||||||
|
presence: device_presence,
|
||||||
|
last_seen_unix: last_seen,
|
||||||
|
wakeable,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if agent_device.endpoints.is_empty()
|
||||||
|
&& agent_device.macs.is_empty()
|
||||||
&& let Some(ip) = agent_device.ips.first()
|
&& let Some(ip) = agent_device.ips.first()
|
||||||
{
|
{
|
||||||
let hostname = agent_device.hostnames.first().cloned();
|
let hostname = agent_device.hostnames.first().cloned();
|
||||||
@@ -293,7 +349,9 @@ fn add_agent_device_to_entry(
|
|||||||
mac: None,
|
mac: None,
|
||||||
ip: Some(*ip),
|
ip: Some(*ip),
|
||||||
hostname,
|
hostname,
|
||||||
|
interface: None,
|
||||||
source: "device".to_string(),
|
source: "device".to_string(),
|
||||||
|
presence: device_presence,
|
||||||
last_seen_unix: last_seen,
|
last_seen_unix: last_seen,
|
||||||
wakeable: false,
|
wakeable: false,
|
||||||
},
|
},
|
||||||
@@ -350,7 +408,10 @@ impl FleetAccumulator {
|
|||||||
b.connected
|
b.connected
|
||||||
.cmp(&a.connected)
|
.cmp(&a.connected)
|
||||||
.then_with(|| b.wakeable.cmp(&a.wakeable))
|
.then_with(|| b.wakeable.cmp(&a.wakeable))
|
||||||
|
.then_with(|| b.ip.is_some().cmp(&a.ip.is_some()))
|
||||||
|
.then_with(|| b.presence.cmp(&a.presence))
|
||||||
.then_with(|| b.last_seen_unix.cmp(&a.last_seen_unix))
|
.then_with(|| b.last_seen_unix.cmp(&a.last_seen_unix))
|
||||||
|
.then_with(|| source_quality_rank(&b.source).cmp(&source_quality_rank(&a.source)))
|
||||||
.then_with(|| a.agent_id.cmp(&b.agent_id))
|
.then_with(|| a.agent_id.cmp(&b.agent_id))
|
||||||
});
|
});
|
||||||
let recommended_route = route_candidates
|
let recommended_route = route_candidates
|
||||||
@@ -374,6 +435,7 @@ impl FleetAccumulator {
|
|||||||
hostnames: self.hostnames.into_iter().collect(),
|
hostnames: self.hostnames.into_iter().collect(),
|
||||||
agents: self.agents.into_values().collect(),
|
agents: self.agents.into_values().collect(),
|
||||||
sources: self.sources.into_iter().collect(),
|
sources: self.sources.into_iter().collect(),
|
||||||
|
endpoints: self.endpoints,
|
||||||
first_seen_unix: self.first_seen_unix,
|
first_seen_unix: self.first_seen_unix,
|
||||||
last_seen_unix: self.last_seen_unix,
|
last_seen_unix: self.last_seen_unix,
|
||||||
presence: self.presence,
|
presence: self.presence,
|
||||||
@@ -383,6 +445,25 @@ impl FleetAccumulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint_source_label(source: wakey_core::EndpointSource) -> &'static str {
|
||||||
|
match source {
|
||||||
|
wakey_core::EndpointSource::Neighbor => "neighbor",
|
||||||
|
wakey_core::EndpointSource::DhcpLease => "dhcp_lease",
|
||||||
|
wakey_core::EndpointSource::HookNeighbor => "hook_neighbor",
|
||||||
|
wakey_core::EndpointSource::HookDhcp => "hook_dhcp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_quality_rank(source: &str) -> u8 {
|
||||||
|
match source {
|
||||||
|
"neighbor" => 4,
|
||||||
|
"dhcp_lease" => 3,
|
||||||
|
"hook_neighbor" => 2,
|
||||||
|
"hook_dhcp" => 1,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>, source: &str) -> String {
|
fn route_id(agent_id: &str, mac: Option<&MacAddr>, ip: Option<&IpAddr>, source: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}|{}|{}|{}",
|
"{}|{}|{}|{}",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use wakey_core::Presence;
|
use wakey_core::{DeviceEndpoint, EndpointKey, EndpointSource, Presence};
|
||||||
|
|
||||||
use super::build::{
|
use super::build::{
|
||||||
AgentRuntimeStatus, FleetBuildContext, build_fleet_devices, filter_fleet_devices,
|
AgentRuntimeStatus, FleetBuildContext, build_fleet_devices, filter_fleet_devices,
|
||||||
@@ -54,6 +54,7 @@ fn agent_device(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
hostnames: vec!["lda".to_string()],
|
hostnames: vec!["lda".to_string()],
|
||||||
|
endpoints: vec![],
|
||||||
facts: vec![],
|
facts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,7 @@ fn offline_agent_device(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
hostnames: vec!["lda".to_string()],
|
hostnames: vec!["lda".to_string()],
|
||||||
|
endpoints: vec![],
|
||||||
facts: vec![],
|
facts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,10 +107,32 @@ fn offline_ip_only_unknown(
|
|||||||
macs: vec![],
|
macs: vec![],
|
||||||
ips: vec![ip.parse().unwrap()],
|
ips: vec![ip.parse().unwrap()],
|
||||||
hostnames: vec![],
|
hostnames: vec![],
|
||||||
|
endpoints: vec![],
|
||||||
facts: vec![],
|
facts: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint(
|
||||||
|
source: EndpointSource,
|
||||||
|
mac: Option<&str>,
|
||||||
|
ip: Option<&str>,
|
||||||
|
last_seen_unix: u64,
|
||||||
|
) -> DeviceEndpoint {
|
||||||
|
DeviceEndpoint {
|
||||||
|
key: EndpointKey::new(
|
||||||
|
source,
|
||||||
|
mac.map(|m| m.parse::<MacAddr>().unwrap()),
|
||||||
|
ip.map(|i| i.parse::<IpAddr>().unwrap()),
|
||||||
|
)
|
||||||
|
.expect("endpoint key"),
|
||||||
|
hostname: Some("lda".into()),
|
||||||
|
interface: Some("br-lan".into()),
|
||||||
|
presence: Presence::LikelyOnline,
|
||||||
|
first_seen_unix: Some(1),
|
||||||
|
last_seen_unix: Some(last_seen_unix),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fleet_grouping_combines_same_mac_across_agents() {
|
fn fleet_grouping_combines_same_mac_across_agents() {
|
||||||
let devices = build_fleet_devices(
|
let devices = build_fleet_devices(
|
||||||
@@ -145,6 +169,40 @@ fn fleet_grouping_combines_same_mac_across_agents() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fleet_routes_use_endpoint_mac_ip_pairs() {
|
||||||
|
let mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
let mut row = agent_device(
|
||||||
|
"agent-a",
|
||||||
|
"mac:aa:bb:cc:dd:ee:ff",
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.2"),
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
row.endpoints = vec![
|
||||||
|
endpoint(
|
||||||
|
EndpointSource::HookNeighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.2"),
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
endpoint(EndpointSource::Neighbor, Some(mac), Some("192.168.1.3"), 20),
|
||||||
|
];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
assert_eq!(devices[0].endpoints.len(), 2);
|
||||||
|
assert_eq!(devices[0].route_candidates.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
devices[0]
|
||||||
|
.recommended_route
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|route| route.ip),
|
||||||
|
Some("192.168.1.3".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn known_device_with_two_macs_absorbs_both_device_groups() {
|
fn known_device_with_two_macs_absorbs_both_device_groups() {
|
||||||
let known = KnownDevice {
|
let known = KnownDevice {
|
||||||
@@ -242,7 +300,7 @@ fn offline_device_has_offline_presence() {
|
|||||||
assert_eq!(devices.len(), 1);
|
assert_eq!(devices.len(), 1);
|
||||||
assert!(devices[0].ips.is_empty());
|
assert!(devices[0].ips.is_empty());
|
||||||
assert_eq!(devices[0].presence, Presence::Offline);
|
assert_eq!(devices[0].presence, Presence::Offline);
|
||||||
assert!(devices[0].recommended_route.is_none());
|
assert!(devices[0].recommended_route.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub struct FleetDevice {
|
|||||||
pub hostnames: Vec<String>,
|
pub hostnames: Vec<String>,
|
||||||
pub agents: Vec<FleetDeviceAgent>,
|
pub agents: Vec<FleetDeviceAgent>,
|
||||||
pub sources: Vec<String>,
|
pub sources: Vec<String>,
|
||||||
|
pub endpoints: Vec<FleetDeviceEndpoint>,
|
||||||
pub first_seen_unix: Option<u64>,
|
pub first_seen_unix: Option<u64>,
|
||||||
pub last_seen_unix: Option<u64>,
|
pub last_seen_unix: Option<u64>,
|
||||||
pub presence: Presence,
|
pub presence: Presence,
|
||||||
@@ -83,6 +84,22 @@ pub struct FleetDeviceAgent {
|
|||||||
pub last_seen_unix: u64,
|
pub last_seen_unix: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FleetDeviceEndpoint {
|
||||||
|
pub agent_id: String,
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
pub connected: bool,
|
||||||
|
pub source: String,
|
||||||
|
#[serde(with = "mac::option_mac")]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub interface: Option<String>,
|
||||||
|
pub presence: Presence,
|
||||||
|
pub first_seen_unix: Option<u64>,
|
||||||
|
pub last_seen_unix: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FleetWakeRoute {
|
pub struct FleetWakeRoute {
|
||||||
pub route_id: String,
|
pub route_id: String,
|
||||||
@@ -93,7 +110,9 @@ pub struct FleetWakeRoute {
|
|||||||
pub mac: Option<MacAddr>,
|
pub mac: Option<MacAddr>,
|
||||||
pub ip: Option<IpAddr>,
|
pub ip: Option<IpAddr>,
|
||||||
pub hostname: Option<String>,
|
pub hostname: Option<String>,
|
||||||
|
pub interface: Option<String>,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
|
pub presence: Presence,
|
||||||
pub last_seen_unix: u64,
|
pub last_seen_unix: u64,
|
||||||
pub wakeable: bool,
|
pub wakeable: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use wakey_core::{Device, DeviceId};
|
use wakey_core::{Device, DeviceEndpoint, DeviceId, EndpointSource};
|
||||||
|
|
||||||
use super::Store;
|
use super::Store;
|
||||||
use super::helpers::core::*;
|
use super::helpers::core::*;
|
||||||
@@ -108,6 +108,48 @@ impl Store {
|
|||||||
.context("failed inserting device ips")?;
|
.context("failed inserting device ips")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"DELETE FROM agent_device_endpoints WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
|
)
|
||||||
|
.bind(agent_id)
|
||||||
|
.bind(&device_key)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed deleting device endpoints")?;
|
||||||
|
|
||||||
|
let endpoints = endpoints_for_storage(device);
|
||||||
|
if !endpoints.is_empty() {
|
||||||
|
let mut builder = sqlx::QueryBuilder::new(
|
||||||
|
"INSERT INTO agent_device_endpoints \
|
||||||
|
(agent_id, device_key, endpoint_key, source, mac, ip, hostname, interface, presence, first_seen_unix, last_seen_unix) ",
|
||||||
|
);
|
||||||
|
builder.push_values(endpoints, |mut b, endpoint| {
|
||||||
|
let first_seen = endpoint.first_seen_unix.unwrap_or(snapshot_time);
|
||||||
|
let last_seen = endpoint.last_seen_unix.unwrap_or(snapshot_time);
|
||||||
|
b.push_bind(agent_id)
|
||||||
|
.push_bind(device_key.clone())
|
||||||
|
.push_bind(endpoint_storage_key(&endpoint))
|
||||||
|
.push_bind(endpoint_source_to_str(endpoint.key.source))
|
||||||
|
.push_bind(
|
||||||
|
endpoint
|
||||||
|
.key
|
||||||
|
.mac
|
||||||
|
.map(|mac| mac.to_string().to_ascii_lowercase()),
|
||||||
|
)
|
||||||
|
.push_bind(endpoint.key.ip.map(|ip| ip.to_string()))
|
||||||
|
.push_bind(endpoint.hostname.clone())
|
||||||
|
.push_bind(endpoint.interface.clone())
|
||||||
|
.push_bind(presence_to_str(endpoint.presence))
|
||||||
|
.push_bind(i64::try_from(first_seen).unwrap_or(i64::MAX))
|
||||||
|
.push_bind(i64::try_from(last_seen).unwrap_or(i64::MAX));
|
||||||
|
});
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.context("failed inserting device endpoints")?;
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"DELETE FROM agent_device_hostnames WHERE agent_id = ?1 AND device_key = ?2",
|
"DELETE FROM agent_device_hostnames WHERE agent_id = ?1 AND device_key = ?2",
|
||||||
agent_id,
|
agent_id,
|
||||||
@@ -223,6 +265,15 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed listing agent device hostnames")?;
|
.context("failed listing agent device hostnames")?;
|
||||||
|
|
||||||
|
let endpoints = sqlx::query_as::<_, AgentDeviceEndpointRow>(
|
||||||
|
r#"SELECT agent_id, device_key, endpoint_key, source, mac, ip,
|
||||||
|
hostname, interface, presence, first_seen_unix, last_seen_unix
|
||||||
|
FROM agent_device_endpoints"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.context("failed listing agent device endpoints")?;
|
||||||
|
|
||||||
let facts = sqlx::query_as!(
|
let facts = sqlx::query_as!(
|
||||||
AgentDeviceFactRow,
|
AgentDeviceFactRow,
|
||||||
r#"SELECT agent_id as "agent_id!", device_key as "device_key!", fact_json as "fact_json!"
|
r#"SELECT agent_id as "agent_id!", device_key as "device_key!", fact_json as "fact_json!"
|
||||||
@@ -232,7 +283,9 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed listing agent device facts")?;
|
.context("failed listing agent device facts")?;
|
||||||
|
|
||||||
Ok(assemble_device_rows(devices, macs, ips, hostnames, facts))
|
Ok(assemble_device_rows(
|
||||||
|
devices, macs, ips, hostnames, endpoints, facts,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List agent device rows for a single agent.
|
/// List agent device rows for a single agent.
|
||||||
@@ -287,6 +340,17 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed listing agent device hostnames for agent")?;
|
.context("failed listing agent device hostnames for agent")?;
|
||||||
|
|
||||||
|
let endpoints = sqlx::query_as::<_, AgentDeviceEndpointRow>(
|
||||||
|
r#"SELECT agent_id, device_key, endpoint_key, source, mac, ip,
|
||||||
|
hostname, interface, presence, first_seen_unix, last_seen_unix
|
||||||
|
FROM agent_device_endpoints
|
||||||
|
WHERE agent_id = ?1"#,
|
||||||
|
)
|
||||||
|
.bind(agent_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.context("failed listing agent device endpoints for agent")?;
|
||||||
|
|
||||||
let facts = sqlx::query_as!(
|
let facts = sqlx::query_as!(
|
||||||
AgentDeviceFactRow,
|
AgentDeviceFactRow,
|
||||||
r#"SELECT agent_id as "agent_id!", device_key as "device_key!", fact_json as "fact_json!"
|
r#"SELECT agent_id as "agent_id!", device_key as "device_key!", fact_json as "fact_json!"
|
||||||
@@ -298,7 +362,9 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
.context("failed listing agent device facts for agent")?;
|
.context("failed listing agent device facts for agent")?;
|
||||||
|
|
||||||
Ok(assemble_device_rows(devices, macs, ips, hostnames, facts))
|
Ok(assemble_device_rows(
|
||||||
|
devices, macs, ips, hostnames, endpoints, facts,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +373,7 @@ fn assemble_device_rows(
|
|||||||
macs: Vec<AgentDeviceMacRow>,
|
macs: Vec<AgentDeviceMacRow>,
|
||||||
ips: Vec<AgentDeviceIpRow>,
|
ips: Vec<AgentDeviceIpRow>,
|
||||||
hostnames: Vec<AgentDeviceHostnameRow>,
|
hostnames: Vec<AgentDeviceHostnameRow>,
|
||||||
|
endpoints: Vec<AgentDeviceEndpointRow>,
|
||||||
facts: Vec<AgentDeviceFactRow>,
|
facts: Vec<AgentDeviceFactRow>,
|
||||||
) -> Vec<AgentDeviceWithChildren> {
|
) -> Vec<AgentDeviceWithChildren> {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -332,6 +399,15 @@ fn assemble_device_rows(
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(row.hostname.clone());
|
.push(row.hostname.clone());
|
||||||
}
|
}
|
||||||
|
let mut endpoint_map: BTreeMap<(&str, &str), Vec<DeviceEndpoint>> = BTreeMap::new();
|
||||||
|
for row in &endpoints {
|
||||||
|
if let Some(endpoint) = row.to_endpoint() {
|
||||||
|
endpoint_map
|
||||||
|
.entry((row.agent_id.as_str(), row.device_key.as_str()))
|
||||||
|
.or_default()
|
||||||
|
.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut fact_map: BTreeMap<(&str, &str), Vec<String>> = BTreeMap::new();
|
let mut fact_map: BTreeMap<(&str, &str), Vec<String>> = BTreeMap::new();
|
||||||
for row in &facts {
|
for row in &facts {
|
||||||
fact_map
|
fact_map
|
||||||
@@ -369,11 +445,13 @@ fn assemble_device_rows(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let hostnames = hostname_map.get(&key).cloned().unwrap_or_default();
|
let hostnames = hostname_map.get(&key).cloned().unwrap_or_default();
|
||||||
|
let endpoints = endpoint_map.get(&key).cloned().unwrap_or_default();
|
||||||
let facts = fact_map.get(&key).cloned().unwrap_or_default();
|
let facts = fact_map.get(&key).cloned().unwrap_or_default();
|
||||||
AgentDeviceWithChildren {
|
AgentDeviceWithChildren {
|
||||||
macs,
|
macs,
|
||||||
ips,
|
ips,
|
||||||
hostnames,
|
hostnames,
|
||||||
|
endpoints,
|
||||||
facts,
|
facts,
|
||||||
device,
|
device,
|
||||||
}
|
}
|
||||||
@@ -397,6 +475,65 @@ fn presence_to_str(presence: wakey_core::Presence) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint_source_to_str(source: EndpointSource) -> &'static str {
|
||||||
|
match source {
|
||||||
|
EndpointSource::Neighbor => "neighbor",
|
||||||
|
EndpointSource::DhcpLease => "dhcp_lease",
|
||||||
|
EndpointSource::HookNeighbor => "hook_neighbor",
|
||||||
|
EndpointSource::HookDhcp => "hook_dhcp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_storage_key(endpoint: &DeviceEndpoint) -> String {
|
||||||
|
format!(
|
||||||
|
"{}|{}|{}",
|
||||||
|
endpoint_source_to_str(endpoint.key.source),
|
||||||
|
endpoint
|
||||||
|
.key
|
||||||
|
.mac
|
||||||
|
.map(|mac| mac.to_string().to_ascii_lowercase())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
endpoint.key.ip.map(|ip| ip.to_string()).unwrap_or_default()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoints_for_storage(device: &Device) -> Vec<DeviceEndpoint> {
|
||||||
|
if !device.endpoints.is_empty() {
|
||||||
|
return device.endpoints.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for neighbor in &device.neighbors {
|
||||||
|
endpoints.push(DeviceEndpoint {
|
||||||
|
key: wakey_core::EndpointKey {
|
||||||
|
source: EndpointSource::Neighbor,
|
||||||
|
mac: neighbor.mac,
|
||||||
|
ip: Some(neighbor.ip),
|
||||||
|
},
|
||||||
|
hostname: None,
|
||||||
|
interface: neighbor.dev.clone(),
|
||||||
|
presence: wakey_core::Presence::from(neighbor.state),
|
||||||
|
first_seen_unix: None,
|
||||||
|
last_seen_unix: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for lease in &device.leases {
|
||||||
|
endpoints.push(DeviceEndpoint {
|
||||||
|
key: wakey_core::EndpointKey {
|
||||||
|
source: EndpointSource::DhcpLease,
|
||||||
|
mac: Some(lease.mac),
|
||||||
|
ip: Some(lease.ip),
|
||||||
|
},
|
||||||
|
hostname: lease.name.clone(),
|
||||||
|
interface: None,
|
||||||
|
presence: wakey_core::Presence::Unknown,
|
||||||
|
first_seen_unix: None,
|
||||||
|
last_seen_unix: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
endpoints
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::helpers::test_helpers::TestStore;
|
use super::super::helpers::test_helpers::TestStore;
|
||||||
@@ -412,6 +549,7 @@ mod tests {
|
|||||||
ips: vec![ip.parse().expect("ip")],
|
ips: vec![ip.parse().expect("ip")],
|
||||||
macs: vec![mac.parse().expect("mac")],
|
macs: vec![mac.parse().expect("mac")],
|
||||||
interfaces: vec!["br-lan".to_string()],
|
interfaces: vec!["br-lan".to_string()],
|
||||||
|
endpoints: vec![],
|
||||||
neighbors: vec![NeighborEntry {
|
neighbors: vec![NeighborEntry {
|
||||||
ip: ip.parse().expect("ip"),
|
ip: ip.parse().expect("ip"),
|
||||||
dev: Some("br-lan".to_string()),
|
dev: Some("br-lan".to_string()),
|
||||||
@@ -457,6 +595,15 @@ mod tests {
|
|||||||
vec!["192.168.1.10".parse::<std::net::IpAddr>().unwrap()]
|
vec!["192.168.1.10".parse::<std::net::IpAddr>().unwrap()]
|
||||||
);
|
);
|
||||||
assert_eq!(rows[0].hostnames, vec!["first-pc"]);
|
assert_eq!(rows[0].hostnames, vec!["first-pc"]);
|
||||||
|
assert_eq!(rows[0].endpoints.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
rows[0].endpoints[0].key.source,
|
||||||
|
wakey_core::EndpointSource::Neighbor
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rows[0].endpoints[0].key.ip,
|
||||||
|
Some("192.168.1.10".parse().expect("ip"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -582,6 +729,7 @@ mod tests {
|
|||||||
ips: vec![],
|
ips: vec![],
|
||||||
macs: vec![],
|
macs: vec![],
|
||||||
interfaces: vec![],
|
interfaces: vec![],
|
||||||
|
endpoints: vec![],
|
||||||
neighbors: vec![],
|
neighbors: vec![],
|
||||||
leases: vec![],
|
leases: vec![],
|
||||||
observations: vec![],
|
observations: vec![],
|
||||||
@@ -612,6 +760,7 @@ mod tests {
|
|||||||
ips: vec!["192.168.1.10".parse().expect("ip")],
|
ips: vec!["192.168.1.10".parse().expect("ip")],
|
||||||
macs: vec!["aa:bb:cc:dd:ee:01".parse().expect("mac")],
|
macs: vec!["aa:bb:cc:dd:ee:01".parse().expect("mac")],
|
||||||
interfaces: vec![],
|
interfaces: vec![],
|
||||||
|
endpoints: vec![],
|
||||||
neighbors: vec![],
|
neighbors: vec![],
|
||||||
leases: vec![],
|
leases: vec![],
|
||||||
observations: vec![DeviceObservationFact {
|
observations: vec![DeviceObservationFact {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wakey_core::Presence;
|
use wakey_core::{DeviceEndpoint, EndpointKey, EndpointSource, Presence};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct IssuedAgent {
|
pub struct IssuedAgent {
|
||||||
@@ -134,6 +134,66 @@ pub struct AgentDeviceHostnameRow {
|
|||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||||
|
pub struct AgentDeviceEndpointRow {
|
||||||
|
pub agent_id: String,
|
||||||
|
pub device_key: String,
|
||||||
|
pub endpoint_key: String,
|
||||||
|
pub source: String,
|
||||||
|
pub mac: Option<String>,
|
||||||
|
pub ip: Option<String>,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub interface: Option<String>,
|
||||||
|
pub presence: String,
|
||||||
|
pub first_seen_unix: i64,
|
||||||
|
pub last_seen_unix: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentDeviceEndpointRow {
|
||||||
|
pub fn to_endpoint(&self) -> Option<DeviceEndpoint> {
|
||||||
|
let source = endpoint_source_from_str(&self.source)?;
|
||||||
|
let mac = match self.mac.as_deref() {
|
||||||
|
Some(raw) => match raw.parse() {
|
||||||
|
Ok(mac) => Some(mac),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %err,
|
||||||
|
endpoint_key = %self.endpoint_key,
|
||||||
|
raw_mac = raw,
|
||||||
|
"failed to parse endpoint mac"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let ip = match self.ip.as_deref() {
|
||||||
|
Some(raw) => match raw.parse() {
|
||||||
|
Ok(ip) => Some(ip),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %err,
|
||||||
|
endpoint_key = %self.endpoint_key,
|
||||||
|
raw_ip = raw,
|
||||||
|
"failed to parse endpoint ip"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let key = EndpointKey::new(source, mac, ip)?;
|
||||||
|
Some(DeviceEndpoint {
|
||||||
|
key,
|
||||||
|
hostname: self.hostname.clone(),
|
||||||
|
interface: self.interface.clone(),
|
||||||
|
presence: Presence::from(self.presence.as_str()),
|
||||||
|
first_seen_unix: Some(self.first_seen_unix.max(0) as u64),
|
||||||
|
last_seen_unix: Some(self.last_seen_unix.max(0) as u64),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AgentDeviceFactRow {
|
pub struct AgentDeviceFactRow {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
@@ -147,10 +207,24 @@ pub struct AgentDeviceWithChildren {
|
|||||||
pub macs: Vec<macaddr::MacAddr>,
|
pub macs: Vec<macaddr::MacAddr>,
|
||||||
pub ips: Vec<std::net::IpAddr>,
|
pub ips: Vec<std::net::IpAddr>,
|
||||||
pub hostnames: Vec<String>,
|
pub hostnames: Vec<String>,
|
||||||
|
pub endpoints: Vec<DeviceEndpoint>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub facts: Vec<String>,
|
pub facts: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint_source_from_str(raw: &str) -> Option<EndpointSource> {
|
||||||
|
match raw {
|
||||||
|
"neighbor" => Some(EndpointSource::Neighbor),
|
||||||
|
"dhcp_lease" => Some(EndpointSource::DhcpLease),
|
||||||
|
"hook_neighbor" => Some(EndpointSource::HookNeighbor),
|
||||||
|
"hook_dhcp" => Some(EndpointSource::HookDhcp),
|
||||||
|
_ => {
|
||||||
|
tracing::warn!(source = raw, "unknown endpoint source");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AuditEvent {
|
pub struct AuditEvent {
|
||||||
pub event_id: String,
|
pub event_id: String,
|
||||||
|
|||||||
+128
-23
@@ -71,6 +71,75 @@ pub enum DeviceId {
|
|||||||
Ip(IpAddr),
|
Ip(IpAddr),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typed origin of an endpoint.
|
||||||
|
///
|
||||||
|
/// Raw source strings stay on `DeviceObservationFact` for debugging. Endpoint
|
||||||
|
/// source is the domain value used for summaries and wake-route ranking.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Ord, PartialOrd, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum EndpointSource {
|
||||||
|
Neighbor,
|
||||||
|
DhcpLease,
|
||||||
|
HookNeighbor,
|
||||||
|
HookDhcp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndpointSource {
|
||||||
|
/// Whether this source contributes an IP to the device summary.
|
||||||
|
pub const fn summary_ip_eligible(self) -> bool {
|
||||||
|
matches!(self, Self::Neighbor | Self::DhcpLease)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source quality used after reachability and recency when ranking routes.
|
||||||
|
pub const fn quality_rank(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Neighbor => 4,
|
||||||
|
Self::DhcpLease => 3,
|
||||||
|
Self::HookNeighbor => 2,
|
||||||
|
Self::HookDhcp => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source-scoped identity for one observed network contact point.
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct EndpointKey {
|
||||||
|
pub source: EndpointSource,
|
||||||
|
#[serde(with = "mac::option_mac", default)]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EndpointKey {
|
||||||
|
pub fn new(source: EndpointSource, mac: Option<MacAddr>, ip: Option<IpAddr>) -> Option<Self> {
|
||||||
|
if mac.is_none() && ip.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self { source, mac, ip })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agent-scoped endpoint key used by control-plane storage and APIs.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct AgentEndpointKey {
|
||||||
|
pub agent_id: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub endpoint: EndpointKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One interpreted network contact point for a device aggregate.
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct DeviceEndpoint {
|
||||||
|
pub key: EndpointKey,
|
||||||
|
pub hostname: Option<String>,
|
||||||
|
pub interface: Option<String>,
|
||||||
|
pub presence: Presence,
|
||||||
|
pub first_seen_unix: Option<u64>,
|
||||||
|
pub last_seen_unix: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
/// One raw source fact used while building a device aggregate.
|
/// One raw source fact used while building a device aggregate.
|
||||||
///
|
///
|
||||||
/// These are intentionally source-shaped and non-durable. They preserve details
|
/// These are intentionally source-shaped and non-durable. They preserve details
|
||||||
@@ -102,6 +171,8 @@ pub struct Device {
|
|||||||
#[serde(with = "mac::vec_mac")]
|
#[serde(with = "mac::vec_mac")]
|
||||||
pub macs: Vec<MacAddr>,
|
pub macs: Vec<MacAddr>,
|
||||||
pub interfaces: Vec<String>,
|
pub interfaces: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub endpoints: Vec<DeviceEndpoint>,
|
||||||
pub neighbors: Vec<NeighborEntry>,
|
pub neighbors: Vec<NeighborEntry>,
|
||||||
pub leases: Vec<DhcpLease>,
|
pub leases: Vec<DhcpLease>,
|
||||||
pub observations: Vec<DeviceObservationFact>,
|
pub observations: Vec<DeviceObservationFact>,
|
||||||
@@ -126,6 +197,7 @@ impl Device {
|
|||||||
let mut ips = BTreeSet::new();
|
let mut ips = BTreeSet::new();
|
||||||
let mut macs = BTreeSet::new();
|
let mut macs = BTreeSet::new();
|
||||||
let mut interfaces = BTreeSet::new();
|
let mut interfaces = BTreeSet::new();
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
let mut presence = Presence::Unknown;
|
let mut presence = Presence::Unknown;
|
||||||
|
|
||||||
for lease in &leases {
|
for lease in &leases {
|
||||||
@@ -134,6 +206,18 @@ impl Device {
|
|||||||
if let Some(name) = lease.name.as_deref() {
|
if let Some(name) = lease.name.as_deref() {
|
||||||
names.insert(name);
|
names.insert(name);
|
||||||
}
|
}
|
||||||
|
endpoints.push(DeviceEndpoint {
|
||||||
|
key: EndpointKey {
|
||||||
|
source: EndpointSource::DhcpLease,
|
||||||
|
mac: Some(lease.mac),
|
||||||
|
ip: Some(lease.ip),
|
||||||
|
},
|
||||||
|
hostname: lease.name.clone(),
|
||||||
|
interface: None,
|
||||||
|
presence: Presence::Unknown,
|
||||||
|
first_seen_unix: None,
|
||||||
|
last_seen_unix: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
for neighbor in &neighbors {
|
for neighbor in &neighbors {
|
||||||
ips.insert(neighbor.ip);
|
ips.insert(neighbor.ip);
|
||||||
@@ -143,47 +227,61 @@ impl Device {
|
|||||||
if let Some(dev) = neighbor.dev.as_deref() {
|
if let Some(dev) = neighbor.dev.as_deref() {
|
||||||
interfaces.insert(dev);
|
interfaces.insert(dev);
|
||||||
}
|
}
|
||||||
presence = std::cmp::max(presence, Presence::from(neighbor.state));
|
let endpoint_presence = Presence::from(neighbor.state);
|
||||||
|
endpoints.push(DeviceEndpoint {
|
||||||
|
key: EndpointKey {
|
||||||
|
source: EndpointSource::Neighbor,
|
||||||
|
mac: neighbor.mac,
|
||||||
|
ip: Some(neighbor.ip),
|
||||||
|
},
|
||||||
|
hostname: None,
|
||||||
|
interface: neighbor.dev.clone(),
|
||||||
|
presence: endpoint_presence,
|
||||||
|
first_seen_unix: None,
|
||||||
|
last_seen_unix: None,
|
||||||
|
});
|
||||||
|
presence = std::cmp::max(presence, endpoint_presence);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut observed_non_remove = false;
|
|
||||||
for observation in &observations {
|
for observation in &observations {
|
||||||
if let Some(name) = observation.hostname.as_deref() {
|
if let Some(name) = observation.hostname.as_deref() {
|
||||||
names.insert(name);
|
names.insert(name);
|
||||||
}
|
}
|
||||||
if let Some(ip) = observation.ip {
|
|
||||||
ips.insert(ip); // is adding removed IPs right? Or should we have a separate map of presence -> IP sets?
|
|
||||||
}
|
|
||||||
if let Some(mac) = observation.mac {
|
if let Some(mac) = observation.mac {
|
||||||
macs.insert(mac);
|
macs.insert(mac);
|
||||||
}
|
}
|
||||||
if observation.action != "remove" {
|
if let Some(source) = observation_endpoint_source(observation)
|
||||||
observed_non_remove = true;
|
&& let Some(key) = EndpointKey::new(source, observation.mac, observation.ip)
|
||||||
|
{
|
||||||
|
let endpoint_presence = observation_presence(observation);
|
||||||
|
endpoints.push(DeviceEndpoint {
|
||||||
|
key,
|
||||||
|
hostname: observation.hostname.clone(),
|
||||||
|
interface: None,
|
||||||
|
presence: endpoint_presence,
|
||||||
|
first_seen_unix: observation.first_seen_unix,
|
||||||
|
last_seen_unix: observation.last_seen_unix,
|
||||||
|
});
|
||||||
|
presence = std::cmp::max(presence, endpoint_presence);
|
||||||
}
|
}
|
||||||
presence = std::cmp::max(presence, observation_presence(observation));
|
|
||||||
}
|
|
||||||
|
|
||||||
if neighbors.is_empty()
|
|
||||||
&& leases.is_empty()
|
|
||||||
&& !observed_non_remove
|
|
||||||
&& !observations.is_empty()
|
|
||||||
{
|
|
||||||
presence = Presence::Offline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let macs: Vec<MacAddr> = macs.into_iter().collect();
|
let macs: Vec<MacAddr> = macs.into_iter().collect();
|
||||||
let ips: Vec<IpAddr> = ips.into_iter().collect();
|
let ips: Vec<IpAddr> = ips.into_iter().collect();
|
||||||
let id = macs
|
let id = macs.first().copied().map(DeviceId::Mac).or_else(|| {
|
||||||
.first()
|
endpoints
|
||||||
.copied()
|
.iter()
|
||||||
.map(DeviceId::Mac)
|
.filter_map(|endpoint| endpoint.key.ip)
|
||||||
.or_else(|| ips.first().copied().map(DeviceId::Ip));
|
.min()
|
||||||
|
.map(DeviceId::Ip)
|
||||||
|
});
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
names: names.into_iter().map(|n| n.to_owned()).collect(),
|
names: names.into_iter().map(|n| n.to_owned()).collect(),
|
||||||
ips,
|
ips,
|
||||||
macs,
|
macs,
|
||||||
interfaces: interfaces.into_iter().map(|d| d.to_owned()).collect(),
|
interfaces: interfaces.into_iter().map(|d| d.to_owned()).collect(),
|
||||||
|
endpoints,
|
||||||
neighbors,
|
neighbors,
|
||||||
leases,
|
leases,
|
||||||
observations,
|
observations,
|
||||||
@@ -194,10 +292,17 @@ impl Device {
|
|||||||
|
|
||||||
fn observation_presence(observation: &DeviceObservationFact) -> Presence {
|
fn observation_presence(observation: &DeviceObservationFact) -> Presence {
|
||||||
match (observation.kind.as_str(), observation.action.as_str()) {
|
match (observation.kind.as_str(), observation.action.as_str()) {
|
||||||
(_, "remove") => Presence::Offline,
|
("neigh", "remove") => Presence::Offline,
|
||||||
("neigh", "add" | "update" | "old") => Presence::LikelyOnline,
|
("neigh", "add" | "update" | "old") => Presence::LikelyOnline,
|
||||||
_ => Presence::Unknown,
|
_ => Presence::Unknown,
|
||||||
// dhcp events does not say anything about offline status, but this may shadow neighbor entries. This may be undesirable.
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observation_endpoint_source(observation: &DeviceObservationFact) -> Option<EndpointSource> {
|
||||||
|
match observation.kind.as_str() {
|
||||||
|
"neigh" => Some(EndpointSource::HookNeighbor),
|
||||||
|
"dhcp" => Some(EndpointSource::HookDhcp),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ mod neighbor;
|
|||||||
mod query;
|
mod query;
|
||||||
mod wake;
|
mod wake;
|
||||||
|
|
||||||
pub use device::{Device, DeviceId, DeviceInventory, DeviceObservationFact, Presence};
|
pub use device::{
|
||||||
|
AgentEndpointKey, Device, DeviceEndpoint, DeviceId, DeviceInventory, DeviceObservationFact,
|
||||||
|
EndpointKey, EndpointSource, Presence,
|
||||||
|
};
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub use dhcp::LeaseQuery;
|
pub use dhcp::LeaseQuery;
|
||||||
pub use dhcp::{DhcpLease, DhcpLeaseWithState};
|
pub use dhcp::{DhcpLease, DhcpLeaseWithState};
|
||||||
|
|||||||
Reference in New Issue
Block a user