i trust that ts works

This commit is contained in:
lda
2026-04-30 14:15:14 +07:00 Verified
parent b20f53c6ba
commit f16c076d96
10 changed files with 414 additions and 239 deletions
@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT events.event_id as \"event_id!\",\n ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) as \"observation_key!\",\n events.agent_id as \"agent_id!\",\n events.kind as \"kind!\",\n events.action as \"action!\",\n events.mac,\n events.ip,\n events.hostname,\n events.ts_unix,\n known_devices.device_id,\n known_devices.display_name,\n known_devices.pinned\n FROM agent_device_observation_events events\n LEFT JOIN device_identifiers identifiers\n ON identifiers.identifier_key =\n CASE\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n END\n LEFT JOIN known_devices ON known_devices.device_id = identifiers.device_id\n WHERE (?1 IS NULL OR events.agent_id = ?1)\n AND (?2 IS NULL OR events.kind = ?2)\n AND (?3 IS NULL OR events.mac = ?3)\n AND (?4 IS NULL OR events.ip = ?4)\n AND (?5 IS NULL OR ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) = ?5)\n ORDER BY events.ts_unix DESC\n LIMIT ?6",
"describe": {
"columns": [
{
"name": "event_id!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "observation_key!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "agent_id!",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "kind!",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "action!",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mac",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "ip",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "ts_unix",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "device_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "pinned",
"ordinal": 11,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
true,
true,
false,
false,
false,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "3bc141cecd0ba7c9ca937036b310728dff57c69ed7b74529fd81fa6a3c0e40cf"
}
@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT events.event_id as \"event_id!\",\n ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) as \"observation_key!\",\n events.agent_id as \"agent_id!\",\n events.kind as \"kind!\",\n events.action as \"action!\",\n events.mac,\n events.ip,\n events.hostname,\n events.ts_unix,\n known_devices.device_id,\n known_devices.display_name,\n known_devices.pinned\n FROM agent_device_observation_events events\n LEFT JOIN device_identifiers identifiers\n ON identifiers.identifier_key =\n CASE\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n END\n LEFT JOIN known_devices ON known_devices.device_id = identifiers.device_id\n WHERE (?1 IS NULL OR events.agent_id = ?1)\n AND (?2 IS NULL OR events.kind = ?2)\n AND (?3 IS NULL OR events.mac = ?3)\n AND (?4 IS NULL OR events.ip = ?4)\n AND (?5 IS NULL OR ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) = ?5)\n ORDER BY events.ts_unix DESC\n LIMIT ?6",
"describe": {
"columns": [
{
"name": "event_id!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "observation_key!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "agent_id!",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "kind!",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "action!",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mac",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "ip",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "ts_unix",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "device_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "pinned",
"ordinal": 11,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
true,
true,
false,
false,
false,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "52a39dd83e65374a5c9e5f55c8492d9927273712ec75814d08674784771fbad2"
}
@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "SELECT events.event_id as \"event_id!\",\n ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) as \"observation_key!\",\n events.agent_id as \"agent_id!\",\n events.kind as \"kind!\",\n events.action as \"action!\",\n events.mac,\n events.ip,\n events.hostname,\n events.ts_unix,\n known_devices.device_id,\n known_devices.display_name,\n known_devices.pinned\n FROM agent_device_observation_events events\n LEFT JOIN device_identifiers identifiers\n ON identifiers.identifier_key =\n CASE\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n END\n LEFT JOIN known_devices ON known_devices.device_id = identifiers.device_id\n WHERE (?1 IS NULL OR events.agent_id = ?1)\n AND (?2 IS NULL OR events.kind = ?2)\n AND (?3 IS NULL OR events.mac = ?3)\n AND (?4 IS NULL OR events.ip = ?4)\n AND (?5 IS NULL OR ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) = ?5)\n ORDER BY events.ts_unix DESC\n LIMIT ?6",
"describe": {
"columns": [
{
"name": "event_id!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "observation_key!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "agent_id!",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "kind!",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "action!",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mac",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "ip",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "ts_unix",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "device_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "pinned",
"ordinal": 11,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
true,
true,
false,
false,
false,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "3bc141cecd0ba7c9ca937036b310728dff57c69ed7b74529fd81fa6a3c0e40cf"
}
@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT events.event_id as \"event_id!\",\n ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) as \"observation_key!\",\n events.agent_id as \"agent_id!\",\n events.kind as \"kind!\",\n events.action as \"action!\",\n events.mac,\n events.ip,\n events.hostname,\n events.ts_unix,\n known_devices.device_id,\n known_devices.display_name,\n known_devices.pinned\n FROM agent_device_observation_events events\n LEFT JOIN device_identifiers identifiers\n ON identifiers.identifier_key =\n CASE\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n END\n LEFT JOIN known_devices ON known_devices.device_id = identifiers.device_id\n WHERE (?1 IS NULL OR events.agent_id = ?1)\n AND (?2 IS NULL OR events.kind = ?2)\n AND (?3 IS NULL OR events.mac = ?3)\n AND (?4 IS NULL OR events.ip = ?4)\n AND (?5 IS NULL OR ('agent:' || events.agent_id || ':' || events.kind || ':' ||\n CASE\n WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL\n THEN 'mac:' || events.mac || ':ip:' || events.ip\n WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac\n WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip\n ELSE ''\n END) = ?5)\n ORDER BY events.ts_unix DESC\n LIMIT ?6",
"describe": {
"columns": [
{
"name": "event_id!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "observation_key!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "agent_id!",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "kind!",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "action!",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "mac",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "ip",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "ts_unix",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "device_id",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "display_name",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "pinned",
"ordinal": 11,
"type_info": "Integer"
}
],
"parameters": {
"Right": 6
},
"nullable": [
true,
true,
false,
false,
false,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "52a39dd83e65374a5c9e5f55c8492d9927273712ec75814d08674784771fbad2"
}
+228 -22
View File
@@ -144,9 +144,44 @@ struct InventoryDevice {
#[serde(default)]
macs: Vec<String>,
#[serde(default)]
neighbors: Vec<InventoryNeighbor>,
#[serde(default)]
leases: Vec<InventoryLease>,
#[serde(default)]
observations: Vec<InventoryObservationFact>,
#[serde(default)]
presence: String,
}
#[derive(Debug, Deserialize)]
struct InventoryNeighbor {
ip: IpAddr,
#[serde(default)]
mac: Option<String>,
#[serde(default)]
state: Option<String>,
}
#[derive(Debug, Deserialize)]
struct InventoryLease {
ip: IpAddr,
mac: String,
#[serde(default)]
name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct InventoryObservationFact {
kind: String,
action: String,
#[serde(default)]
mac: Option<String>,
#[serde(default)]
ip: Option<IpAddr>,
#[serde(default)]
hostname: Option<String>,
}
pub async fn list_fleet_devices(
State(state): State<AppState>,
Query(query): Query<ListFleetDevicesQuery>,
@@ -716,6 +751,63 @@ fn inventory_result_to_observations(
.iter()
.find(|name| !name.trim().is_empty())
.cloned();
let mut wrote_source_observation = false;
for neighbor in &device.neighbors {
if neighbor.mac.is_none() && device.macs.is_empty() {
out.push(inventory_observation(
inventory_action_for_neighbor(neighbor),
None,
Some(neighbor.ip.to_string()),
hostname.clone(),
now,
));
wrote_source_observation = true;
continue;
}
let macs = neighbor
.mac
.iter()
.chain(device.macs.iter())
.collect::<BTreeSet<_>>();
for mac in macs {
out.push(inventory_observation(
inventory_action_for_neighbor(neighbor),
Some(mac.clone()),
Some(neighbor.ip.to_string()),
hostname.clone(),
now,
));
wrote_source_observation = true;
}
}
for observation in &device.observations {
if observation.mac.is_none() && observation.ip.is_none() {
continue;
}
out.push(inventory_observation(
inventory_action_for_observation(observation),
observation.mac.clone(),
observation.ip.map(|ip| ip.to_string()),
observation.hostname.clone().or_else(|| hostname.clone()),
now,
));
wrote_source_observation = true;
}
for lease in &device.leases {
out.push(inventory_observation(
"update",
Some(lease.mac.clone()),
Some(lease.ip.to_string()),
lease.name.clone().or_else(|| hostname.clone()),
now,
));
wrote_source_observation = true;
}
if wrote_source_observation {
continue;
}
let action = if device.presence == "offline" {
"remove"
} else {
@@ -723,44 +815,78 @@ fn inventory_result_to_observations(
};
if !device.macs.is_empty() {
for mac in device.macs {
out.push(AgentDeviceObservationInput {
kind: "inventory".into(),
action: action.into(),
mac: Some(mac),
ip: device.ips.first().map(ToString::to_string),
hostname: hostname.clone(),
first_seen_unix: now,
last_seen_unix: now,
});
out.push(inventory_observation(
action,
Some(mac),
device.ips.first().map(ToString::to_string),
hostname.clone(),
now,
));
}
} else {
for ip in &device.ips {
out.push(AgentDeviceObservationInput {
kind: "inventory".into(),
action: action.into(),
mac: None,
ip: Some(ip.to_string()),
hostname: hostname.clone(),
first_seen_unix: now,
last_seen_unix: now,
});
out.push(inventory_observation(
action,
None,
Some(ip.to_string()),
hostname.clone(),
now,
));
}
}
}
Ok(out)
}
fn inventory_observation(
action: &str,
mac: Option<String>,
ip: Option<String>,
hostname: Option<String>,
now: u64,
) -> AgentDeviceObservationInput {
AgentDeviceObservationInput {
kind: "inventory".into(),
action: action.into(),
mac,
ip,
hostname,
first_seen_unix: now,
last_seen_unix: now,
}
}
fn inventory_action_for_neighbor(neighbor: &InventoryNeighbor) -> &'static str {
if neighbor
.state
.as_deref()
.is_some_and(|state| state.eq_ignore_ascii_case("FAILED"))
{
"remove"
} else {
"update"
}
}
fn inventory_action_for_observation(observation: &InventoryObservationFact) -> &str {
match observation.action.as_str() {
"add" | "old" | "update" => "update",
"remove" | "del" => "remove",
_ if observation.kind == "neigh" => "update",
_ => "update",
}
}
fn observation_presence_rank(observation: &AgentDeviceObservation) -> u8 {
match observation.last_action.as_str() {
"remove" | "failed" => 0,
"permanent" | "reachable" => 3,
"stale" | "add" | "old" | "update" => 2,
"remove" => 0,
"add" | "old" | "update" => 2,
_ => 1,
}
}
fn observation_is_offline(observation: &AgentDeviceObservation) -> bool {
matches!(observation.last_action.as_str(), "remove" | "failed")
observation.last_action == "remove"
}
fn rank_presence(rank: u8) -> &'static str {
@@ -983,4 +1109,84 @@ mod tests {
assert_eq!(observations[0].action, "update");
assert_eq!(observations[0].mac.as_deref(), Some("aa:bb:cc:dd:ee:ff"));
}
#[test]
fn inventory_result_preserves_neighbor_failed_ip_as_remove() {
let observations = inventory_result_to_observations(serde_json::json!({
"kind": "inventory",
"devices": [{
"names": ["lda"],
"ips": ["192.168.1.2", "192.168.1.3"],
"macs": ["aa:bb:cc:dd:ee:ff"],
"neighbors": [
{
"ip": "192.168.1.2",
"mac": "aa:bb:cc:dd:ee:ff",
"state": "FAILED"
},
{
"ip": "192.168.1.3",
"mac": "aa:bb:cc:dd:ee:ff",
"state": "REACHABLE"
}
],
"presence": "online"
}]
}))
.expect("inventory should map");
assert_eq!(observations.len(), 2);
let removed = observations
.iter()
.find(|observation| observation.ip.as_deref() == Some("192.168.1.2"))
.expect("failed neighbor observation should exist");
assert_eq!(removed.action, "remove");
assert_eq!(removed.mac.as_deref(), Some("aa:bb:cc:dd:ee:ff"));
let current = observations
.iter()
.find(|observation| observation.ip.as_deref() == Some("192.168.1.3"))
.expect("reachable neighbor observation should exist");
assert_eq!(current.action, "update");
assert_eq!(current.mac.as_deref(), Some("aa:bb:cc:dd:ee:ff"));
}
#[test]
fn inventory_result_preserves_hook_observations_and_leases() {
let observations = inventory_result_to_observations(serde_json::json!({
"kind": "inventory",
"devices": [{
"names": ["lda"],
"ips": ["192.168.1.2", "192.168.1.3"],
"macs": ["aa:bb:cc:dd:ee:ff"],
"observations": [{
"kind": "neigh",
"action": "remove",
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.1.2"
}],
"leases": [{
"expires_epoch": 1893456000_u64,
"ip": "192.168.1.3",
"mac": "aa:bb:cc:dd:ee:ff",
"name": "lda"
}],
"presence": "likely_online"
}]
}))
.expect("inventory should map");
assert_eq!(observations.len(), 2);
assert!(observations.iter().any(|observation| {
observation.action == "remove"
&& observation.mac.as_deref() == Some("aa:bb:cc:dd:ee:ff")
&& observation.ip.as_deref() == Some("192.168.1.2")
}));
assert!(observations.iter().any(|observation| {
observation.action == "update"
&& observation.mac.as_deref() == Some("aa:bb:cc:dd:ee:ff")
&& observation.ip.as_deref() == Some("192.168.1.3")
&& observation.hostname.as_deref() == Some("lda")
}));
}
}
@@ -277,7 +277,7 @@ pub(in crate::state::store) fn normalize_agent_observation(
fn observation_identifier(kind: &str, mac: Option<&str>, ip: Option<&str>) -> Option<String> {
match (kind, mac, ip) {
("neigh", Some(mac), Some(ip)) => Some(format!("mac:{mac}:ip:{ip}")),
("neigh" | "inventory", Some(mac), Some(ip)) => Some(format!("mac:{mac}:ip:{ip}")),
(_, Some(mac), _) => Some(format!("mac:{mac}")),
(_, None, Some(ip)) => Some(format!("ip:{ip}")),
(_, None, None) => None,
@@ -211,7 +211,7 @@ impl Store {
r#"SELECT events.event_id as "event_id!",
('agent:' || events.agent_id || ':' || events.kind || ':' ||
CASE
WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL
WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL
THEN 'mac:' || events.mac || ':ip:' || events.ip
WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac
WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip
@@ -241,7 +241,7 @@ impl Store {
AND (?4 IS NULL OR events.ip = ?4)
AND (?5 IS NULL OR ('agent:' || events.agent_id || ':' || events.kind || ':' ||
CASE
WHEN events.kind = 'neigh' AND events.mac IS NOT NULL AND events.ip IS NOT NULL
WHEN events.kind IN ('neigh', 'inventory') AND events.mac IS NOT NULL AND events.ip IS NOT NULL
THEN 'mac:' || events.mac || ':ip:' || events.ip
WHEN events.mac IS NOT NULL THEN 'mac:' || events.mac
WHEN events.ip IS NOT NULL THEN 'ip:' || events.ip
+1 -3
View File
@@ -174,9 +174,7 @@ impl Device {
fn observation_presence(observation: &DeviceObservationFact) -> Presence {
match (observation.kind.as_str(), observation.action.as_str()) {
(_, "remove" | "failed") => Presence::Offline,
(_, "permanent" | "reachable") => Presence::Online,
(_, "stale") => Presence::LikelyOnline,
(_, "remove") => Presence::Offline,
("neigh", "add" | "update" | "old") => Presence::LikelyOnline,
_ => Presence::Unknown,
}
+2 -2
View File
@@ -145,14 +145,14 @@ mod tests {
let mac = "aa:bb:cc:dd:ee:ff".parse().expect("mac should parse");
observe_neighbor_event(
"stale",
"add",
Some(mac),
Some("192.168.1.2".parse().expect("ip should parse")),
)
.await
.expect("first observation should write");
observe_neighbor_event(
"reachable",
"update",
Some(mac),
Some("192.168.1.3".parse().expect("ip should parse")),
)
+8 -37
View File
@@ -1,10 +1,8 @@
use std::io::{self, ErrorKind};
use std::net::IpAddr;
use std::str::FromStr;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use wakey_core::NeighborState;
use super::{mac_name_cache_path, now_unix, observation_store_path};
@@ -220,7 +218,7 @@ pub async fn observe_neighbor_event(
mac: Option<MacAddr>,
ip: Option<IpAddr>,
) -> io::Result<bool> {
let action = normalize_neighbor_action(action, mac, ip).await;
let action = normalize_neighbor_action(action);
if !is_neighbor_observation_action(&action) {
return Ok(false);
}
@@ -270,47 +268,20 @@ pub async fn observe_neighbor_event(
Ok(changed)
}
async fn normalize_neighbor_action(
action: &str,
mac: Option<MacAddr>,
ip: Option<IpAddr>,
) -> String {
let action = action.trim().to_ascii_lowercase();
if action == "remove" || action == "del" {
return "remove".into();
fn normalize_neighbor_action(action: &str) -> String {
match action.trim().to_ascii_lowercase().as_str() {
"del" => "remove".into(),
"old" => "update".into(),
other => other.to_string(),
}
if NeighborState::from_str(&action).is_ok() {
return action;
}
if matches!(action.as_str(), "add" | "update" | "old") {
if let Some(state) = current_neighbor_state(mac, ip).await {
return state.as_ip_neigh_arg().into();
}
return action;
}
action
}
async fn current_neighbor_state(mac: Option<MacAddr>, ip: Option<IpAddr>) -> Option<NeighborState> {
let ips = ip.into_iter().collect::<Vec<_>>();
let macs = mac.into_iter().collect::<Vec<_>>();
let rows = crate::devices::get_neighbors(&[] as &[&str], &ips, &[] as &[&str], &[], &macs)
.await
.ok()?;
rows.into_iter()
.filter(|row| ip.is_none_or(|expected| row.ip == expected))
.filter(|row| mac.is_none_or(|expected| row.mac == Some(expected)))
.map(|row| row.state)
.max()
}
fn is_neighbor_observation_action(action: &str) -> bool {
matches!(action, "add" | "update" | "old" | "remove" | "del")
|| NeighborState::from_str(action).is_ok()
matches!(action, "add" | "update" | "remove")
}
fn is_offline_neighbor_action(action: &str) -> bool {
matches!(action, "remove" | "failed")
action == "remove"
}
fn neighbor_observation_key(mac: Option<MacAddr>, ip: Option<IpAddr>) -> Option<String> {