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
61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
|
|
class PendingDeviceResponse(BaseModel):
|
|
id: str
|
|
ip: str | None
|
|
mac: str | None
|
|
hostname: str | None
|
|
os: str | None
|
|
services: list[Any]
|
|
suggested_type: str | None
|
|
status: str
|
|
discovery_source: str | None
|
|
# All sources that have observed this device (e.g. ["arp", "proxmox"]). Drives
|
|
# the inventory source filter + badges; falls back to [discovery_source].
|
|
discovery_sources: list[str] = []
|
|
ieee_address: str | None = None
|
|
friendly_name: str | None = None
|
|
device_subtype: str | None = None
|
|
model: str | None = None
|
|
vendor: str | None = None
|
|
lqi: int | None = None
|
|
# Display properties carried from discovery (e.g. Proxmox specs). Merged into
|
|
# the node on approve; empty for scan/mesh sources that don't set them.
|
|
properties: list[Any] = []
|
|
discovered_at: datetime
|
|
# Number of distinct canvases (designs) this device already appears on,
|
|
# correlated by ip / ieee_address against existing nodes. Computed per-request.
|
|
canvas_count: int = 0
|
|
# Timestamps from the linked canvas node(s), correlated by ip / ieee_address.
|
|
# Null when the device is not on any canvas yet. Aggregated across matches:
|
|
# created_at = oldest; last_scan / last_modified / last_seen = newest.
|
|
node_created_at: datetime | None = None
|
|
node_last_scan: datetime | None = None
|
|
node_last_modified: datetime | None = None
|
|
node_last_seen: datetime | None = None
|
|
|
|
@field_validator("properties", "discovery_sources", mode="before")
|
|
@classmethod
|
|
def _coerce_list(cls, v: Any) -> list[Any]:
|
|
# Legacy rows (columns added by migration) have these = NULL.
|
|
return v if isinstance(v, list) else []
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class ScanRunResponse(BaseModel):
|
|
id: str
|
|
status: str
|
|
kind: str = "ip"
|
|
ranges: list[str]
|
|
devices_found: int
|
|
started_at: datetime
|
|
finished_at: datetime | None
|
|
error: str | None
|
|
|
|
model_config = {"from_attributes": True}
|