adf82f8f01
Reconcile the same physical device discovered by both the nmap IP scan and the Proxmox importer into a single inventory row, keyed on MAC. Previously each path only deduped by IP, and the importer captured no MAC (and no IP for stopped guests), so most guests double-listed. Backend: - mac_utils.normalize_mac: canonical MAC (lowercase, ':'-separated), the cross-source join key. Normalized on write and on compare. - proxmox_service: capture the guest NIC MAC agent-free from the net0 config (qemu virtio=<MAC>, lxc hwaddr=<MAC>); works for stopped guests. Resolver now returns (ip, mac). - proxmox persist: match existing Node/PendingDevice by ieee OR ip OR MAC; fill mac, keep the vm/lxc type, union sources. - scanner persist: match PendingDevice by ip OR MAC; fill the IP a Proxmox import lacked, keep a pve row's type, union the scan source. Stamp query matches raw + normalized MAC (legacy-safe). - Multi-source tags: new PendingDevice.discovery_sources JSON column so a merged device shows under both the IP and Proxmox filters. Idempotent migration backfills from discovery_source (legacy NULL-scalar rows with an IP become ["arp"]). _sources_after_merge preserves a scanned row's IP origin through the merge without tagging a pure Proxmox guest. - Import now broadcasts a scan update on completion so an open inventory reloads without a manual refresh. Frontend: - pendingSources: sourceBuckets/orderedSources map discovery_sources to filter buckets; a device with ["arp","proxmox"] matches both filters and renders both badges. PendingDevicesModal filter + badges use them. Tests: MAC normalization, config MAC capture, cross-source merge both directions, legacy-row IP-tag preservation, no-false-IP-tag guard, refresh broadcast, and the frontend bucket mapping. ha-relevant: maybe
28 lines
1.0 KiB
Python
28 lines
1.0 KiB
Python
"""Unit tests for MAC normalization (the cross-source dedup key)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.services.discovery_sources import add_source
|
|
from app.services.mac_utils import normalize_mac
|
|
|
|
|
|
def test_normalize_mac_lowercases_and_unifies_separators() -> None:
|
|
assert normalize_mac("BC:24:11:AA:BB:CC") == "bc:24:11:aa:bb:cc"
|
|
assert normalize_mac("bc-24-11-aa-bb-cc") == "bc:24:11:aa:bb:cc"
|
|
assert normalize_mac(" BC:24:11:AA:BB:CC ") == "bc:24:11:aa:bb:cc"
|
|
|
|
|
|
def test_normalize_mac_blank_is_none() -> None:
|
|
assert normalize_mac(None) is None
|
|
assert normalize_mac("") is None
|
|
assert normalize_mac(" ") is None
|
|
|
|
|
|
def test_add_source_unions_without_duplicates() -> None:
|
|
assert add_source(None, "arp") == ["arp"]
|
|
assert add_source(["arp"], "proxmox") == ["arp", "proxmox"]
|
|
assert add_source(["arp", "proxmox"], "proxmox") == ["arp", "proxmox"]
|
|
assert add_source(["arp"], None) == ["arp"]
|
|
# Drops falsy members already present.
|
|
assert add_source(["arp", ""], "proxmox") == ["arp", "proxmox"]
|