feat: add confidence merging and link upsert
merge_confidence/2 combines scores using 1-(1-a)(1-b). upsert_link/1 creates or updates links keyed on source device + interface + remote MAC. Merges confidence on update, appends evidence records.
This commit is contained in:
parent
00c366ac9a
commit
9327fd3f13
2 changed files with 186 additions and 0 deletions
|
|
@ -12,6 +12,8 @@ defmodule Towerops.Topology do
|
|||
alias Towerops.Snmp.Interface
|
||||
alias Towerops.Snmp.MacAddress
|
||||
alias Towerops.Snmp.Neighbor
|
||||
alias Towerops.Topology.DeviceLink
|
||||
alias Towerops.Topology.DeviceLinkEvidence
|
||||
|
||||
@doc """
|
||||
Builds lookup maps for matching evidence to managed devices in an organization.
|
||||
|
|
@ -219,4 +221,57 @@ defmodule Towerops.Topology do
|
|||
end)
|
||||
|> Enum.reject(fn ev -> ev.matched_device_id == device_id end)
|
||||
end
|
||||
|
||||
@doc "Merge two independent confidence scores: 1 - (1-a)(1-b)"
|
||||
def merge_confidence(a, b), do: 1.0 - (1.0 - a) * (1.0 - b)
|
||||
|
||||
@doc """
|
||||
Create or update a device link. Keyed on source_device_id + source_interface_id +
|
||||
discovered_remote_mac. If existing, merges confidence and appends evidence.
|
||||
Always updates last_confirmed_at.
|
||||
"""
|
||||
def upsert_link(attrs) do
|
||||
evidence_attrs = Map.get(attrs, :evidence, [])
|
||||
link_attrs = Map.delete(attrs, :evidence)
|
||||
|
||||
existing =
|
||||
Repo.one(
|
||||
from(l in DeviceLink,
|
||||
where:
|
||||
l.source_device_id == ^link_attrs.source_device_id and
|
||||
l.source_interface_id == ^link_attrs[:source_interface_id] and
|
||||
l.discovered_remote_mac == ^link_attrs[:discovered_remote_mac]
|
||||
)
|
||||
)
|
||||
|
||||
result =
|
||||
case existing do
|
||||
nil ->
|
||||
%DeviceLink{}
|
||||
|> DeviceLink.changeset(link_attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
link ->
|
||||
merged_confidence = merge_confidence(link.confidence, link_attrs.confidence)
|
||||
|
||||
link
|
||||
|> DeviceLink.changeset(%{
|
||||
confidence: merged_confidence,
|
||||
last_confirmed_at: link_attrs.last_confirmed_at,
|
||||
target_device_id: link_attrs[:target_device_id] || link.target_device_id,
|
||||
metadata: Map.merge(link.metadata || %{}, link_attrs[:metadata] || %{})
|
||||
})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
with {:ok, link} <- result do
|
||||
Enum.each(evidence_attrs, fn ev ->
|
||||
%DeviceLinkEvidence{}
|
||||
|> DeviceLinkEvidence.changeset(Map.put(ev, :device_link_id, link.id))
|
||||
|> Repo.insert!()
|
||||
end)
|
||||
|
||||
{:ok, Repo.reload!(link)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -283,6 +283,137 @@ defmodule Towerops.TopologyTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "merge_confidence/2" do
|
||||
test "combines two independent confidence scores" do
|
||||
assert_in_delta Topology.merge_confidence(0.95, 0.7), 0.985, 0.001
|
||||
end
|
||||
|
||||
test "combining with 0 returns the other score" do
|
||||
assert Topology.merge_confidence(0.8, 0.0) == 0.8
|
||||
end
|
||||
|
||||
test "combining two high scores approaches 1.0" do
|
||||
assert_in_delta Topology.merge_confidence(0.95, 0.95), 0.9975, 0.001
|
||||
end
|
||||
end
|
||||
|
||||
describe "upsert_link/1" do
|
||||
test "creates a new link from evidence", %{router: router, router_iface: iface} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, link} =
|
||||
Topology.upsert_link(%{
|
||||
source_device_id: router.id,
|
||||
target_device_id: nil,
|
||||
source_interface_id: iface.id,
|
||||
link_type: "lldp",
|
||||
confidence: 0.95,
|
||||
discovered_remote_mac: "dd:ee:ff:00:11:22",
|
||||
discovered_remote_ip: "10.0.0.5",
|
||||
discovered_remote_name: "remote-switch",
|
||||
last_confirmed_at: now,
|
||||
evidence: [
|
||||
%{
|
||||
evidence_type: "lldp_neighbor",
|
||||
evidence_data: %{"remote_chassis_id" => "dd:ee:ff:00:11:22"},
|
||||
observed_at: now
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert link.source_device_id == router.id
|
||||
assert link.confidence == 0.95
|
||||
assert link.link_type == "lldp"
|
||||
|
||||
link_with_evidence = Repo.preload(link, :evidence)
|
||||
assert length(link_with_evidence.evidence) == 1
|
||||
end
|
||||
|
||||
test "updates existing link and merges confidence", %{router: router, router_iface: iface} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Create initial LLDP link
|
||||
{:ok, _link1} =
|
||||
Topology.upsert_link(%{
|
||||
source_device_id: router.id,
|
||||
source_interface_id: iface.id,
|
||||
link_type: "lldp",
|
||||
confidence: 0.95,
|
||||
discovered_remote_mac: "dd:ee:ff:00:11:22",
|
||||
last_confirmed_at: now,
|
||||
evidence: [
|
||||
%{
|
||||
evidence_type: "lldp_neighbor",
|
||||
evidence_data: %{},
|
||||
observed_at: now
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Upsert again with MAC evidence — should merge confidence
|
||||
{:ok, updated} =
|
||||
Topology.upsert_link(%{
|
||||
source_device_id: router.id,
|
||||
source_interface_id: iface.id,
|
||||
link_type: "mac_match",
|
||||
confidence: 0.7,
|
||||
discovered_remote_mac: "dd:ee:ff:00:11:22",
|
||||
last_confirmed_at: now,
|
||||
evidence: [
|
||||
%{
|
||||
evidence_type: "mac_on_interface",
|
||||
evidence_data: %{},
|
||||
observed_at: now
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
# Confidence merged: 1 - (1-0.95)(1-0.7) = 0.985
|
||||
assert_in_delta updated.confidence, 0.985, 0.001
|
||||
# Evidence accumulated
|
||||
updated_with_evidence = Repo.preload(updated, :evidence)
|
||||
assert length(updated_with_evidence.evidence) == 2
|
||||
end
|
||||
|
||||
test "populates target_device_id when matched", %{
|
||||
router: router,
|
||||
switch: switch,
|
||||
router_iface: iface
|
||||
} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, link} =
|
||||
Topology.upsert_link(%{
|
||||
source_device_id: router.id,
|
||||
target_device_id: switch.id,
|
||||
source_interface_id: iface.id,
|
||||
link_type: "lldp",
|
||||
confidence: 0.95,
|
||||
discovered_remote_mac: "bb:cc:dd:00:11:22",
|
||||
last_confirmed_at: now,
|
||||
evidence: []
|
||||
})
|
||||
|
||||
assert link.target_device_id == switch.id
|
||||
end
|
||||
|
||||
test "creates link without evidence list", %{router: router, router_iface: iface} do
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, link} =
|
||||
Topology.upsert_link(%{
|
||||
source_device_id: router.id,
|
||||
source_interface_id: iface.id,
|
||||
link_type: "lldp",
|
||||
confidence: 0.95,
|
||||
discovered_remote_mac: "dd:ee:ff:00:11:22",
|
||||
last_confirmed_at: now
|
||||
})
|
||||
|
||||
assert link.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "collect_arp_evidence/2" do
|
||||
test "returns evidence for ARP entries matching known devices", %{
|
||||
organization: org,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue