make fleet routes endpoint authoritative
This commit is contained in:
@@ -59,29 +59,6 @@ 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,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use wakey_core::Presence;
|
use wakey_core::{EndpointSource, Presence};
|
||||||
|
|
||||||
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
use crate::state::{AgentDeviceWithChildren, DeviceIdentifier, KnownDevice, KnownDeviceSummary};
|
||||||
|
|
||||||
@@ -203,12 +203,25 @@ fn add_agent_device_to_entry(
|
|||||||
entry.pinned = summary.pinned;
|
entry.pinned = summary.pinned;
|
||||||
entry.known_device = Some(summary);
|
entry.known_device = Some(summary);
|
||||||
}
|
}
|
||||||
for mac in &agent_device.macs {
|
if agent_device.endpoints.is_empty() {
|
||||||
entry.macs.insert(*mac);
|
for mac in &agent_device.macs {
|
||||||
}
|
entry.macs.insert(*mac);
|
||||||
if !device_offline {
|
}
|
||||||
for ip in &agent_device.ips {
|
if !device_offline {
|
||||||
entry.ips.insert(*ip);
|
for ip in &agent_device.ips {
|
||||||
|
entry.ips.insert(*ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for endpoint in &agent_device.endpoints {
|
||||||
|
if let Some(mac) = endpoint.key.mac {
|
||||||
|
entry.macs.insert(mac);
|
||||||
|
}
|
||||||
|
if endpoint_ip_is_summary_eligible(endpoint.key.source, endpoint.presence)
|
||||||
|
&& let Some(ip) = endpoint.key.ip
|
||||||
|
{
|
||||||
|
entry.ips.insert(ip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for hostname in &agent_device.hostnames {
|
for hostname in &agent_device.hostnames {
|
||||||
@@ -285,11 +298,10 @@ fn add_agent_device_to_entry(
|
|||||||
&agent_id,
|
&agent_id,
|
||||||
endpoint.key.mac.as_ref(),
|
endpoint.key.mac.as_ref(),
|
||||||
endpoint.key.ip.as_ref(),
|
endpoint.key.ip.as_ref(),
|
||||||
&source,
|
|
||||||
);
|
);
|
||||||
let wakeable = status.connected && endpoint.key.mac.is_some();
|
let wakeable = status.connected && endpoint.key.mac.is_some();
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -311,10 +323,10 @@ fn add_agent_device_to_entry(
|
|||||||
for mac in &agent_device.macs {
|
for mac in &agent_device.macs {
|
||||||
let ip_for_mac = agent_device.ips.first().copied();
|
let ip_for_mac = agent_device.ips.first().copied();
|
||||||
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
let hostname_for_mac = agent_device.hostnames.first().cloned();
|
||||||
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref(), "device");
|
let rid = route_id(&agent_id, Some(mac), ip_for_mac.as_ref());
|
||||||
let wakeable = status.connected;
|
let wakeable = status.connected;
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -338,9 +350,9 @@ fn add_agent_device_to_entry(
|
|||||||
&& 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();
|
||||||
let rid = route_id(&agent_id, None, Some(ip), "device");
|
let rid = route_id(&agent_id, None, Some(ip));
|
||||||
entry.routes.insert(
|
insert_route(
|
||||||
rid.clone(),
|
entry,
|
||||||
FleetWakeRoute {
|
FleetWakeRoute {
|
||||||
route_id: rid,
|
route_id: rid,
|
||||||
agent_id: agent_id.clone(),
|
agent_id: agent_id.clone(),
|
||||||
@@ -380,19 +392,66 @@ fn device_known_device(
|
|||||||
context: &FleetBuildContext,
|
context: &FleetBuildContext,
|
||||||
) -> Option<KnownDeviceSummary> {
|
) -> Option<KnownDeviceSummary> {
|
||||||
agent_device
|
agent_device
|
||||||
.macs
|
.endpoints
|
||||||
.first()
|
.iter()
|
||||||
|
.filter_map(|endpoint| endpoint.key.mac)
|
||||||
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
||||||
.and_then(|key| context.identifier_map.get(&key).cloned())
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
.or_else(|| {
|
||||||
|
agent_device
|
||||||
|
.macs
|
||||||
|
.iter()
|
||||||
|
.map(|mac| format!("mac:{}", mac.to_string().to_ascii_lowercase()))
|
||||||
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
agent_device
|
||||||
|
.endpoints
|
||||||
|
.iter()
|
||||||
|
.filter_map(|endpoint| endpoint.key.ip)
|
||||||
|
.map(|ip| format!("ip:{ip}"))
|
||||||
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
agent_device
|
agent_device
|
||||||
.ips
|
.ips
|
||||||
.first()
|
.iter()
|
||||||
.map(|ip| format!("ip:{ip}"))
|
.map(|ip| format!("ip:{ip}"))
|
||||||
.and_then(|key| context.identifier_map.get(&key).cloned())
|
.find_map(|key| context.identifier_map.get(&key).cloned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn endpoint_ip_is_summary_eligible(source: EndpointSource, presence: Presence) -> bool {
|
||||||
|
match source {
|
||||||
|
EndpointSource::DhcpLease => true,
|
||||||
|
EndpointSource::Neighbor => presence != Presence::Offline,
|
||||||
|
EndpointSource::HookNeighbor | EndpointSource::HookDhcp => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_route(entry: &mut FleetAccumulator, candidate: FleetWakeRoute) {
|
||||||
|
match entry.routes.entry(candidate.route_id.clone()) {
|
||||||
|
std::collections::btree_map::Entry::Vacant(slot) => {
|
||||||
|
slot.insert(candidate);
|
||||||
|
}
|
||||||
|
std::collections::btree_map::Entry::Occupied(mut slot) => {
|
||||||
|
if compare_route_quality(&candidate, slot.get()).is_gt() {
|
||||||
|
slot.insert(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_route_quality(a: &FleetWakeRoute, b: &FleetWakeRoute) -> std::cmp::Ordering {
|
||||||
|
a.connected
|
||||||
|
.cmp(&b.connected)
|
||||||
|
.then_with(|| a.wakeable.cmp(&b.wakeable))
|
||||||
|
.then_with(|| a.ip.is_some().cmp(&b.ip.is_some()))
|
||||||
|
.then_with(|| a.presence.cmp(&b.presence))
|
||||||
|
.then_with(|| a.last_seen_unix.cmp(&b.last_seen_unix))
|
||||||
|
.then_with(|| source_quality_rank(&a.source).cmp(&source_quality_rank(&b.source)))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
||||||
KnownDeviceSummary {
|
KnownDeviceSummary {
|
||||||
device_id: device.device_id.clone(),
|
device_id: device.device_id.clone(),
|
||||||
@@ -404,16 +463,8 @@ pub(crate) fn known_device_summary(device: &KnownDevice) -> KnownDeviceSummary {
|
|||||||
impl FleetAccumulator {
|
impl FleetAccumulator {
|
||||||
fn into_response(self) -> FleetDevice {
|
fn into_response(self) -> FleetDevice {
|
||||||
let mut route_candidates = self.routes.into_values().collect::<Vec<_>>();
|
let mut route_candidates = self.routes.into_values().collect::<Vec<_>>();
|
||||||
route_candidates.sort_by(|a, b| {
|
route_candidates
|
||||||
b.connected
|
.sort_by(|a, b| compare_route_quality(b, a).then_with(|| a.agent_id.cmp(&b.agent_id)));
|
||||||
.cmp(&a.connected)
|
|
||||||
.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(|| source_quality_rank(&b.source).cmp(&source_quality_rank(&a.source)))
|
|
||||||
.then_with(|| a.agent_id.cmp(&b.agent_id))
|
|
||||||
});
|
|
||||||
let recommended_route = route_candidates
|
let recommended_route = route_candidates
|
||||||
.iter()
|
.iter()
|
||||||
.find(|route| route.wakeable)
|
.find(|route| route.wakeable)
|
||||||
@@ -445,12 +496,12 @@ impl FleetAccumulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn endpoint_source_label(source: wakey_core::EndpointSource) -> &'static str {
|
fn endpoint_source_label(source: EndpointSource) -> &'static str {
|
||||||
match source {
|
match source {
|
||||||
wakey_core::EndpointSource::Neighbor => "neighbor",
|
EndpointSource::Neighbor => "neighbor",
|
||||||
wakey_core::EndpointSource::DhcpLease => "dhcp_lease",
|
EndpointSource::DhcpLease => "dhcp_lease",
|
||||||
wakey_core::EndpointSource::HookNeighbor => "hook_neighbor",
|
EndpointSource::HookNeighbor => "hook_neighbor",
|
||||||
wakey_core::EndpointSource::HookDhcp => "hook_dhcp",
|
EndpointSource::HookDhcp => "hook_dhcp",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,11 +515,10 @@ fn source_quality_rank(source: &str) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}|{}|{}|{}",
|
"{}|{}|{}",
|
||||||
agent_id,
|
agent_id,
|
||||||
source,
|
|
||||||
mac.map(|m| m.to_string()).unwrap_or_default(),
|
mac.map(|m| m.to_string()).unwrap_or_default(),
|
||||||
ip.map(|i| i.to_string()).unwrap_or_default()
|
ip.map(|i| i.to_string()).unwrap_or_default()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -117,6 +117,16 @@ fn endpoint(
|
|||||||
mac: Option<&str>,
|
mac: Option<&str>,
|
||||||
ip: Option<&str>,
|
ip: Option<&str>,
|
||||||
last_seen_unix: u64,
|
last_seen_unix: u64,
|
||||||
|
) -> DeviceEndpoint {
|
||||||
|
endpoint_with_presence(source, mac, ip, Presence::LikelyOnline, last_seen_unix)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn endpoint_with_presence(
|
||||||
|
source: EndpointSource,
|
||||||
|
mac: Option<&str>,
|
||||||
|
ip: Option<&str>,
|
||||||
|
presence: Presence,
|
||||||
|
last_seen_unix: u64,
|
||||||
) -> DeviceEndpoint {
|
) -> DeviceEndpoint {
|
||||||
DeviceEndpoint {
|
DeviceEndpoint {
|
||||||
key: EndpointKey::new(
|
key: EndpointKey::new(
|
||||||
@@ -127,7 +137,7 @@ fn endpoint(
|
|||||||
.expect("endpoint key"),
|
.expect("endpoint key"),
|
||||||
hostname: Some("lda".into()),
|
hostname: Some("lda".into()),
|
||||||
interface: Some("br-lan".into()),
|
interface: Some("br-lan".into()),
|
||||||
presence: Presence::LikelyOnline,
|
presence,
|
||||||
first_seen_unix: Some(1),
|
first_seen_unix: Some(1),
|
||||||
last_seen_unix: Some(last_seen_unix),
|
last_seen_unix: Some(last_seen_unix),
|
||||||
}
|
}
|
||||||
@@ -203,6 +213,112 @@ fn fleet_routes_use_endpoint_mac_ip_pairs() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fleet_summary_keeps_current_dhcp_ip_when_neighbor_endpoint_is_offline() {
|
||||||
|
let mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
let mut row = offline_agent_device(
|
||||||
|
"agent-a",
|
||||||
|
"mac:aa:bb:cc:dd:ee:ff",
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
row.ips = vec![
|
||||||
|
"192.168.1.10".parse().unwrap(),
|
||||||
|
"192.168.1.99".parse().unwrap(),
|
||||||
|
];
|
||||||
|
row.endpoints = vec![
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::Neighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
Presence::Offline,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::DhcpLease,
|
||||||
|
Some(mac),
|
||||||
|
Some("192.168.1.10"),
|
||||||
|
Presence::Unknown,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
devices[0].ips,
|
||||||
|
vec!["192.168.1.10".parse::<IpAddr>().unwrap()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_ip_identifier_matches_endpoint_that_is_not_in_summary_ips() {
|
||||||
|
let known = KnownDevice {
|
||||||
|
device_id: "dev-1".into(),
|
||||||
|
display_name: "lda remembered".into(),
|
||||||
|
pinned: true,
|
||||||
|
created_at_unix: 1,
|
||||||
|
updated_at_unix: 1,
|
||||||
|
notes: None,
|
||||||
|
identifiers: vec![DeviceIdentifier {
|
||||||
|
identifier_key: "ip:192.168.1.99".into(),
|
||||||
|
device_id: "dev-1".into(),
|
||||||
|
kind: "ip".into(),
|
||||||
|
value: "192.168.1.99".into(),
|
||||||
|
created_at_unix: 1,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let mut ctx = context(&["agent-a"]);
|
||||||
|
ctx.identifier_map
|
||||||
|
.insert("ip:192.168.1.99".into(), known_device_summary(&known));
|
||||||
|
let mut row = agent_device("agent-a", "ip:192.168.1.99", None, None, 20);
|
||||||
|
row.endpoints = vec![endpoint_with_presence(
|
||||||
|
EndpointSource::HookNeighbor,
|
||||||
|
None,
|
||||||
|
Some("192.168.1.99"),
|
||||||
|
Presence::Offline,
|
||||||
|
20,
|
||||||
|
)];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(vec![known], vec![row], &ctx);
|
||||||
|
|
||||||
|
assert_eq!(devices.len(), 1);
|
||||||
|
assert_eq!(devices[0].device_key, "known:dev-1");
|
||||||
|
assert_eq!(devices[0].endpoints.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equivalent_endpoint_routes_collapse_to_best_source() {
|
||||||
|
let mac = "aa:bb:cc:dd:ee:ff";
|
||||||
|
let ip = "192.168.1.2";
|
||||||
|
let mut row = agent_device("agent-a", "mac:aa:bb:cc:dd:ee:ff", Some(mac), Some(ip), 20);
|
||||||
|
row.endpoints = vec![
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::HookNeighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some(ip),
|
||||||
|
Presence::LikelyOnline,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
endpoint_with_presence(
|
||||||
|
EndpointSource::Neighbor,
|
||||||
|
Some(mac),
|
||||||
|
Some(ip),
|
||||||
|
Presence::Online,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let devices = build_fleet_devices(Vec::new(), vec![row], &context(&["agent-a"]));
|
||||||
|
|
||||||
|
assert_eq!(devices[0].endpoints.len(), 2);
|
||||||
|
assert_eq!(devices[0].route_candidates.len(), 1);
|
||||||
|
assert_eq!(devices[0].route_candidates[0].source, "neighbor");
|
||||||
|
assert_eq!(devices[0].route_candidates[0].presence, Presence::Online);
|
||||||
|
}
|
||||||
|
|
||||||
#[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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user