i trust that ts works
This commit is contained in:
+86
@@ -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"
|
||||
}
|
||||
-86
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user