using RackPeek.Domain.Resources; using RackPeek.Domain.Resources.Services.Networking; using RackPeek.Domain.Resources.SystemResources; namespace RackPeek.Domain.Discovery; /// Maps swept hosts onto the System resources RackPeek stores. Pure. public static class NetworkScanMapper { public static List ToResources(IReadOnlyList hosts) { var taken = new HashSet(StringComparer.OrdinalIgnoreCase); var resources = new List(); foreach ((NetworkHostFact host, IReadOnlyList allIps) in Collapse(hosts)) { // The MAC is the only identity a scan can see that survives a DHCP re-lease; // when ARP could not provide one (a routed subnet, say) the IP has to do, // and the id changes if the address does — documented in the guide. var discoveryId = DiscoveryId.Create( DiscoveryId.NetworkScheme, host.Mac ?? $"ip:{host.Ip}"); var system = new SystemResource { Kind = SystemResource.KindLabel, Name = DiscoveryNaming.Unique( DiscoveryNaming.Suggest( DiscoveryNaming.HostLabel(host.Hostname), "host", discoveryId), discoveryId, taken), DiscoveryId = discoveryId, // Deliberately sparse: a scan sees an address, not an OS or a type, and // whatever it wrote here would overwrite the real values on every rescan // of a card the user (or an agent collector) has since filled in. Ip = host.Ip }; if (host.Mac != null) system.Labels["mac"] = host.Mac; if (allIps.Count > 1) system.Labels["ips"] = string.Join(",", allIps); resources.Add(system); } return resources; } /// /// One MAC answering on several addresses — a gateway's VIPs and aliases — is /// still one machine, so it becomes one card: the lowest address as the card's /// ip (deterministic), every address in an "ips" label. Anything else would make /// the machine's identity depend on how many of its addresses happened to answer /// a particular scan, and identity must never move between scans. /// private static IEnumerable<(NetworkHostFact Host, IReadOnlyList AllIps)> Collapse( IReadOnlyList hosts) { var byMac = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (NetworkHostFact host in hosts) if (host.Mac != null) { if (!byMac.TryGetValue(host.Mac, out List? group)) byMac[host.Mac] = group = []; group.Add(host); } var emitted = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (NetworkHostFact host in hosts) { if (host.Mac == null) { yield return (host, [host.Ip]); continue; } if (!emitted.Add(host.Mac)) continue; var group = byMac[host.Mac] .OrderBy(h => IpHelper.ToUInt32(h.Ip)) .ToList(); NetworkHostFact primary = group[0]; // Any name in the group beats none: a VIP rarely has its own PTR record. var hostname = group.Select(h => h.Hostname).FirstOrDefault(n => n != null); yield return ( primary with { Hostname = hostname }, group.Select(h => h.Ip).ToList()); } } }