feat(gaiia): MAC-based auto-matching (phase 1, highest confidence)
Adds MAC address matching as the primary matching phase: - Normalizes MACs (strips colons/dots/dashes) from SNMP interface data - Matches against gaiia_inventory_items.mac_address (synced from Gaiia) - Phase 1: MAC → Phase 2: IP → Phase 3: Site Flash now shows breakdown: "Auto-matched 47 devices (12 by MAC, 34 by IP, 1 by site). 17 remaining."
This commit is contained in:
parent
40dadededd
commit
3483d9d5f0
2 changed files with 63 additions and 8 deletions
|
|
@ -15,6 +15,7 @@ defmodule Towerops.Gaiia do
|
|||
alias Towerops.Repo
|
||||
alias Towerops.Sites.Site
|
||||
alias Towerops.Snmp.Device, as: SnmpDevice
|
||||
alias Towerops.Snmp.Interface, as: SnmpInterface
|
||||
# --- Accounts ---
|
||||
alias Towerops.Snmp.WirelessClientReading
|
||||
|
||||
|
|
@ -136,7 +137,22 @@ defmodule Towerops.Gaiia do
|
|||
|> where([i], is_nil(i.device_id))
|
||||
|> Repo.all()
|
||||
|
||||
# Phase 1: IP-based matching
|
||||
# Phase 1: MAC-based matching (highest confidence — exact identifier).
|
||||
# Gaiia equipment is assigned by MAC (usually br0), so this catches
|
||||
# devices that have no IP in Gaiia but are already inventoried.
|
||||
unmapped_by_mac =
|
||||
unmapped_items
|
||||
|> Enum.reject(fn i -> is_nil(i.mac_address) or i.mac_address == "" end)
|
||||
|> Enum.group_by(&normalize_mac(&1.mac_address))
|
||||
|
||||
device_macs = build_device_mac_index(organization_id)
|
||||
|
||||
{mac_matched, _after_mac} =
|
||||
match_phase(organization_id, unmapped_by_mac, fn device, items ->
|
||||
mac_matcher(device, items, device_macs)
|
||||
end)
|
||||
|
||||
# Phase 2: IP-based matching
|
||||
unmapped_by_ip =
|
||||
unmapped_items
|
||||
|> Enum.reject(fn i -> is_nil(i.ip_address) or i.ip_address == "" end)
|
||||
|
|
@ -144,7 +160,7 @@ defmodule Towerops.Gaiia do
|
|||
|
||||
{ip_matched, _after_ip} = match_phase(organization_id, unmapped_by_ip, &ip_matcher/2)
|
||||
|
||||
# Phase 2: Site-based matching — devices at a Towerops site linked to a
|
||||
# Phase 3: Site-based matching — devices at a Towerops site linked to a
|
||||
# Gaiia network site get matched to inventory items assigned to that site
|
||||
site_network_map = build_site_network_map(organization_id)
|
||||
|
||||
|
|
@ -174,7 +190,7 @@ defmodule Towerops.Gaiia do
|
|||
|
||||
remaining = total_devices - MapSet.size(mapped_device_ids)
|
||||
|
||||
{:ok, ip_matched, site_matched, remaining}
|
||||
{:ok, ip_matched, site_matched, mac_matched, remaining}
|
||||
end
|
||||
|
||||
defp match_phase(organization_id, unmapped_index, matcher_fn) do
|
||||
|
|
@ -241,6 +257,44 @@ defmodule Towerops.Gaiia do
|
|||
end)
|
||||
end
|
||||
|
||||
# Builds a map of device_id → normalized MAC address from SNMP interfaces.
|
||||
# Uses the first non-null MAC found — typically the br0/bridge interface.
|
||||
defp build_device_mac_index(organization_id) do
|
||||
SnmpInterface
|
||||
|> join(:inner, [si], sd in SnmpDevice, on: si.snmp_device_id == sd.id)
|
||||
|> join(:inner, [_si, sd], d in Device, on: sd.device_id == d.id)
|
||||
|> where([_si, _sd, d], d.organization_id == ^organization_id)
|
||||
|> where([si], not is_nil(si.mac_address) and si.mac_address != "")
|
||||
|> select([si, sd], {sd.device_id, si.mac_address})
|
||||
|> Repo.all()
|
||||
|> Enum.group_by(fn {device_id, _mac} -> device_id end, fn {_device_id, mac} -> normalize_mac(mac) end)
|
||||
end
|
||||
|
||||
defp mac_matcher(device, unmapped_by_mac, device_macs) do
|
||||
case Map.get(device_macs, device.id) do
|
||||
macs when is_list(macs) and macs != [] ->
|
||||
find_mac_match(macs, unmapped_by_mac)
|
||||
|
||||
_ ->
|
||||
:no_match
|
||||
end
|
||||
end
|
||||
|
||||
defp find_mac_match(macs, unmapped_by_mac) do
|
||||
Enum.find_value(macs, :no_match, fn mac ->
|
||||
case Map.get(unmapped_by_mac, mac) do
|
||||
[item | _rest] -> {:match, item}
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_mac(mac) when is_binary(mac) do
|
||||
mac
|
||||
|> String.downcase()
|
||||
|> String.replace(~r/[:.\-\s]/, "")
|
||||
end
|
||||
|
||||
# Builds a map of Towerops site_id → list of Gaiia network site gaiia_ids
|
||||
defp build_site_network_map(organization_id) do
|
||||
NetworkSite
|
||||
|
|
|
|||
|
|
@ -129,18 +129,19 @@ defmodule ToweropsWeb.Org.GaiiaReconciliationLive do
|
|||
)}
|
||||
end
|
||||
|
||||
defp match_flash({:ok, 0, 0, _remaining}) do
|
||||
defp match_flash({:ok, 0, 0, 0, _remaining}) do
|
||||
t("No matches found. Link devices manually on the mapping page.")
|
||||
end
|
||||
|
||||
defp match_flash({:ok, ip_matched, site_matched, remaining}) do
|
||||
total = ip_matched + site_matched
|
||||
defp match_flash({:ok, mac_matched, ip_matched, site_matched, remaining}) do
|
||||
total = mac_matched + ip_matched + site_matched
|
||||
|
||||
ngettext(
|
||||
"Auto-matched %{total} device (%{ip} by IP, %{site} by site). %{remaining} remaining.",
|
||||
"Auto-matched %{total} devices (%{ip} by IP, %{site} by site). %{remaining} remaining.",
|
||||
"Auto-matched %{total} device (%{mac} by MAC, %{ip} by IP, %{site} by site). %{remaining} remaining.",
|
||||
"Auto-matched %{total} devices (%{mac} by MAC, %{ip} by IP, %{site} by site). %{remaining} remaining.",
|
||||
total,
|
||||
total: total,
|
||||
mac: mac_matched,
|
||||
ip: ip_matched,
|
||||
site: site_matched,
|
||||
remaining: remaining
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue