NetworkScanMapper.cs 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. using RackPeek.Domain.Resources;
  2. using RackPeek.Domain.Resources.Services.Networking;
  3. using RackPeek.Domain.Resources.SystemResources;
  4. namespace RackPeek.Domain.Discovery;
  5. /// <summary>Maps swept hosts onto the System resources RackPeek stores. Pure.</summary>
  6. public static class NetworkScanMapper {
  7. public static List<Resource> ToResources(IReadOnlyList<NetworkHostFact> hosts) {
  8. var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  9. var resources = new List<Resource>();
  10. foreach ((NetworkHostFact host, IReadOnlyList<string> allIps) in Collapse(hosts)) {
  11. // The MAC is the only identity a scan can see that survives a DHCP re-lease;
  12. // when ARP could not provide one (a routed subnet, say) the IP has to do,
  13. // and the id changes if the address does — documented in the guide.
  14. var discoveryId = DiscoveryId.Create(
  15. DiscoveryId.NetworkScheme,
  16. host.Mac ?? $"ip:{host.Ip}");
  17. var system = new SystemResource {
  18. Kind = SystemResource.KindLabel,
  19. Name = DiscoveryNaming.Unique(
  20. DiscoveryNaming.Suggest(
  21. DiscoveryNaming.HostLabel(host.Hostname),
  22. "host",
  23. discoveryId),
  24. discoveryId,
  25. taken),
  26. DiscoveryId = discoveryId,
  27. // Deliberately sparse: a scan sees an address, not an OS or a type, and
  28. // whatever it wrote here would overwrite the real values on every rescan
  29. // of a card the user (or an agent collector) has since filled in.
  30. Ip = host.Ip
  31. };
  32. if (host.Mac != null)
  33. system.Labels["mac"] = host.Mac;
  34. if (allIps.Count > 1)
  35. system.Labels["ips"] = string.Join(",", allIps);
  36. resources.Add(system);
  37. }
  38. return resources;
  39. }
  40. /// <summary>
  41. /// One MAC answering on several addresses — a gateway's VIPs and aliases — is
  42. /// still one machine, so it becomes one card: the lowest address as the card's
  43. /// ip (deterministic), every address in an "ips" label. Anything else would make
  44. /// the machine's identity depend on how many of its addresses happened to answer
  45. /// a particular scan, and identity must never move between scans.
  46. /// </summary>
  47. private static IEnumerable<(NetworkHostFact Host, IReadOnlyList<string> AllIps)> Collapse(
  48. IReadOnlyList<NetworkHostFact> hosts) {
  49. var byMac = new Dictionary<string, List<NetworkHostFact>>(StringComparer.OrdinalIgnoreCase);
  50. foreach (NetworkHostFact host in hosts)
  51. if (host.Mac != null) {
  52. if (!byMac.TryGetValue(host.Mac, out List<NetworkHostFact>? group))
  53. byMac[host.Mac] = group = [];
  54. group.Add(host);
  55. }
  56. var emitted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  57. foreach (NetworkHostFact host in hosts) {
  58. if (host.Mac == null) {
  59. yield return (host, [host.Ip]);
  60. continue;
  61. }
  62. if (!emitted.Add(host.Mac))
  63. continue;
  64. var group = byMac[host.Mac]
  65. .OrderBy(h => IpHelper.ToUInt32(h.Ip))
  66. .ToList();
  67. NetworkHostFact primary = group[0];
  68. // Any name in the group beats none: a VIP rarely has its own PTR record.
  69. var hostname = group.Select(h => h.Hostname).FirstOrDefault(n => n != null);
  70. yield return (
  71. primary with { Hostname = hostname },
  72. group.Select(h => h.Ip).ToList());
  73. }
  74. }
  75. }