IpBridgeSymmetryTests.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. using RackPeek.Domain.Discovery;
  2. using RackPeek.Domain.Resources;
  3. using RackPeek.Domain.Resources.Servers;
  4. using RackPeek.Domain.Resources.SystemResources;
  5. namespace Tests.Discovery;
  6. /// <summary>
  7. /// The address bridge has to read the same whichever collector ran first.
  8. /// <para>
  9. /// ARP is link-local, so a sweep of a routed subnet gets an address and no MAC,
  10. /// and the card it writes can only be called <c>host-&lt;hash&gt;</c>. A hypervisor
  11. /// knows that guest by name, by specification, and by the address it holds — so
  12. /// the address is the one thing tying the two together. Whether the sweep or the
  13. /// hypervisor got there first is an accident of what the person typed, and must
  14. /// not decide whether the inventory ends up with one card or two.
  15. /// </para>
  16. /// <para>
  17. /// A live run of nine subnets found this the hard way: sweeping before running
  18. /// the Proxmox and firewall collectors produced fourteen machines twice over,
  19. /// where the same commands in the other order produced one card each.
  20. /// </para>
  21. /// </summary>
  22. public class IpBridgeSymmetryTests {
  23. private const string _ip = "10.0.50.105";
  24. private const string _mac = "bc:24:11:00:4a:01";
  25. /// <summary>What a sweep of a routed subnet can write: an address, and nothing else.</summary>
  26. private static SystemResource ScanCard(string? ip = _ip) => new() {
  27. Kind = SystemResource.KindLabel,
  28. Name = "host-2ed3bfd7",
  29. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"ip:{ip}"),
  30. Ip = ip
  31. };
  32. /// <summary>What the hypervisor knows about the same box.</summary>
  33. private static SystemResource GuestCard(string name = "forgejo", string? ip = _ip) => new() {
  34. Kind = SystemResource.KindLabel,
  35. Name = name,
  36. DiscoveryId = DiscoveryId.Create(ProxmoxResourceMapper.Scheme, "vmid-105"),
  37. Type = "vm",
  38. Os = "Linux",
  39. Ip = ip,
  40. Labels = { ["macs"] = _mac }
  41. };
  42. private static void Resolve(List<Resource> existing, List<Resource> incoming) =>
  43. DiscoveryIdResolver.ResolveNames(existing, incoming, null, null, true);
  44. [Fact]
  45. public void The_hypervisor_claims_the_card_a_sweep_left_behind() {
  46. // Sweep ran first. This is the direction that was silently broken.
  47. List<Resource> existing = [ScanCard()];
  48. List<Resource> incoming = [GuestCard()];
  49. Resolve(existing, incoming);
  50. // One card, not two: the guest lands on the stored one.
  51. Assert.Equal(existing[0].Name, incoming[0].Name);
  52. }
  53. [Fact]
  54. public void The_sweep_lands_on_the_card_the_hypervisor_left_behind() {
  55. // The direction that already worked, kept honest.
  56. List<Resource> existing = [GuestCard()];
  57. List<Resource> incoming = [ScanCard()];
  58. Resolve(existing, incoming);
  59. Assert.Equal("forgejo", incoming[0].Name);
  60. }
  61. [Fact]
  62. public void The_agent_identity_wins_whichever_way_round_it_arrives() {
  63. // Arriving second, the hypervisor's id survives so the merge can upgrade the
  64. // stored card from a stand-in to a real identity.
  65. List<Resource> incoming = [GuestCard()];
  66. Resolve([ScanCard()], incoming);
  67. Assert.StartsWith("rpk1:pve:", incoming[0].DiscoveryId);
  68. // Arriving second, the sweep's id is dropped so the merge cannot downgrade it.
  69. incoming = [ScanCard()];
  70. Resolve([GuestCard()], incoming);
  71. Assert.Null(incoming[0].DiscoveryId);
  72. }
  73. [Fact]
  74. public void The_placeholder_name_gives_way_to_the_real_one() {
  75. // The point of unifying: the machine stops being a hash. This is the upgrade
  76. // that could never fire while the cards stayed separate.
  77. List<Resource> existing = [ScanCard()];
  78. Resolve(existing, [GuestCard()]);
  79. Assert.Equal("forgejo", existing[0].Name);
  80. }
  81. [Fact]
  82. public void Two_stand_ins_at_one_address_still_unify_nothing() {
  83. // Neither side knows anything the other does not, so an address proves nothing.
  84. List<Resource> existing = [ScanCard()];
  85. SystemResource other = ScanCard();
  86. other.Name = "host-9999aaaa";
  87. other.DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "ip:other");
  88. Resolve(existing, [other]);
  89. Assert.Equal("host-9999aaaa", other.Name);
  90. }
  91. [Fact]
  92. public void Two_agent_grade_cards_at_one_address_still_unify_nothing() {
  93. // A guest and the machine-id of the OS inside it are both agent-grade. They are
  94. // two cards on purpose; an address must not collapse them.
  95. var fromAgent = new SystemResource {
  96. Kind = SystemResource.KindLabel,
  97. Name = "forgejo-inside",
  98. DiscoveryId = DiscoveryId.Create(DiscoveryId.SystemScheme, "machine-a"),
  99. Ip = _ip
  100. };
  101. Resolve([GuestCard()], [fromAgent]);
  102. Assert.Equal("forgejo-inside", fromAgent.Name);
  103. }
  104. [Fact]
  105. public void A_sweep_that_saw_a_mac_is_left_to_the_mac_bridge() {
  106. // A MAC is better evidence than an address, whichever side is holding it. A scan
  107. // card with one either unified on the MAC already or genuinely disagrees.
  108. SystemResource scan = ScanCard();
  109. scan.Labels["mac"] = "bc:24:11:00:4a:99";
  110. Resolve([scan], [GuestCard()]);
  111. Assert.Equal("host-2ed3bfd7", scan.Name);
  112. }
  113. [Fact]
  114. public void A_different_kind_of_thing_at_the_same_address_is_not_the_same_thing() {
  115. // The box documented as hardware and the OS a sweep saw on it are separate cards
  116. // on purpose — the merge replaces on a type change, so unifying would delete one.
  117. var server = new Server {
  118. Kind = Server.KindLabel,
  119. Name = "kepler",
  120. DiscoveryId = DiscoveryId.Create(ProxmoxResourceMapper.Scheme, "node-kepler")
  121. };
  122. List<Resource> existing = [ScanCard()];
  123. Resolve(existing, [server]);
  124. Assert.Equal("kepler", server.Name);
  125. Assert.Equal("host-2ed3bfd7", existing[0].Name);
  126. }
  127. [Fact]
  128. public void An_address_two_stored_cards_claim_unifies_nothing() {
  129. // Overlapping subnets, or a stale card. Ambiguity is not evidence.
  130. SystemResource first = ScanCard();
  131. SystemResource second = ScanCard();
  132. second.Name = "host-bbbbcccc";
  133. second.DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "ip:dup");
  134. List<Resource> incoming = [GuestCard()];
  135. Resolve([first, second], incoming);
  136. Assert.Equal("forgejo", incoming[0].Name);
  137. }
  138. }