RunsOnByIpTests.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using RackPeek.Domain.Discovery;
  2. using RackPeek.Domain.Resources;
  3. using RackPeek.Domain.Resources.Services;
  4. using RackPeek.Domain.Resources.SystemResources;
  5. namespace Tests.Discovery;
  6. /// <summary>
  7. /// Giving a service the host it is plainly running on, when the collector could not.
  8. /// A docker collector talking to a remote engine has to guess its host's name and the
  9. /// link then dangles; the service's own address is evidence the guess is not. The
  10. /// rules that keep this from doing harm are what these tests own.
  11. /// </summary>
  12. public class RunsOnByIpTests {
  13. private static SystemResource System(string name, string? ip, string? discoveryId = null) =>
  14. new() {
  15. Kind = SystemResource.KindLabel,
  16. Name = name,
  17. Ip = ip,
  18. DiscoveryId = discoveryId
  19. };
  20. private static Service Service(string name, string? ip, string? runsOn = null, string? discoveryId = "rpk1:docker:1") =>
  21. new() {
  22. Kind = RackPeek.Domain.Resources.Services.Service.KindLabel,
  23. Name = name,
  24. DiscoveryId = discoveryId,
  25. Network = ip == null ? null : new Network { Ip = ip },
  26. RunsOn = runsOn == null ? [] : [runsOn]
  27. };
  28. [Fact]
  29. public void A_service_whose_host_name_matches_nothing_is_anchored_to_the_system_at_its_address() {
  30. // The remote-docker case: the engine called itself NAS01.lan, the inventory
  31. // calls that machine nas01, and nothing linked the two.
  32. List<Resource> existing = [System("nas01", "192.0.2.50")];
  33. List<Resource> incoming = [Service("immich", "192.0.2.50", "NAS01.lan")];
  34. DiscoveryIdResolver.ResolveNames(existing, incoming);
  35. Assert.Equal(["nas01"], incoming.OfType<Service>().Single().RunsOn);
  36. }
  37. [Fact]
  38. public void A_service_with_no_host_at_all_is_anchored_too() {
  39. List<Resource> existing = [System("nas01", "192.0.2.50")];
  40. List<Resource> incoming = [Service("immich", "192.0.2.50")];
  41. DiscoveryIdResolver.ResolveNames(existing, incoming);
  42. Assert.Equal(["nas01"], incoming.OfType<Service>().Single().RunsOn);
  43. }
  44. [Fact]
  45. public void A_host_arriving_in_the_same_payload_counts() {
  46. // discover docker emits the host alongside its services.
  47. List<Resource> incoming = [
  48. System("nas01", "192.0.2.50", "rpk1:sys:a"),
  49. Service("immich", "192.0.2.50", "somewhere-else")
  50. ];
  51. DiscoveryIdResolver.ResolveNames([], incoming);
  52. Assert.Equal(["nas01"], incoming.OfType<Service>().Single().RunsOn);
  53. }
  54. [Fact]
  55. public void A_working_host_link_is_never_overwritten() {
  56. // The service says it runs on a resource that exists. That is the collector's
  57. // own answer and it beats an inference drawn from an address.
  58. List<Resource> existing = [
  59. System("nas01", "192.0.2.50"),
  60. System("some-vm", "192.0.2.99")
  61. ];
  62. List<Resource> incoming = [Service("immich", "192.0.2.50", "some-vm")];
  63. DiscoveryIdResolver.ResolveNames(existing, incoming);
  64. Assert.Equal(["some-vm"], incoming.OfType<Service>().Single().RunsOn);
  65. }
  66. [Fact]
  67. public void A_stored_host_beats_a_scanned_stand_in_at_the_same_address() {
  68. // Once a hypervisor reports its guests' addresses, a sweep of that subnet finds
  69. // the same machines again and contributes a sparse card per address. The service
  70. // belongs on the guest the hypervisor described, not on the sweep's stand-in.
  71. List<Resource> existing = [System("app-vm", "192.0.2.50")];
  72. List<Resource> incoming = [
  73. System("host-1a2b3c4d", "192.0.2.50", "rpk1:net:c"),
  74. Service("immich", "192.0.2.50", "SOMEWHERE.lan")
  75. ];
  76. DiscoveryIdResolver.ResolveNames(existing, incoming);
  77. Assert.Equal(["app-vm"], incoming.OfType<Service>().Single().RunsOn);
  78. }
  79. [Fact]
  80. public void An_address_two_systems_claim_anchors_nothing() {
  81. // Overlapping subnets across sites, or a stale card nobody cleaned up. An
  82. // ambiguous address is no evidence, and a wrong parent is worse than none.
  83. List<Resource> existing = [
  84. System("site-a-host", "192.0.2.50"),
  85. System("site-b-host", "192.0.2.50")
  86. ];
  87. List<Resource> incoming = [Service("immich", "192.0.2.50", "NAS01.lan")];
  88. DiscoveryIdResolver.ResolveNames(existing, incoming);
  89. Assert.Equal(["NAS01.lan"], incoming.OfType<Service>().Single().RunsOn);
  90. }
  91. [Fact]
  92. public void A_service_with_no_address_is_left_alone() {
  93. List<Resource> existing = [System("nas01", "192.0.2.50")];
  94. List<Resource> incoming = [Service("immich", null, "NAS01.lan")];
  95. DiscoveryIdResolver.ResolveNames(existing, incoming);
  96. Assert.Equal(["NAS01.lan"], incoming.OfType<Service>().Single().RunsOn);
  97. }
  98. [Fact]
  99. public void An_address_no_system_claims_anchors_nothing() {
  100. List<Resource> existing = [System("nas01", "192.0.2.50")];
  101. List<Resource> incoming = [Service("immich", "198.51.100.99", "NAS01.lan")];
  102. DiscoveryIdResolver.ResolveNames(existing, incoming);
  103. Assert.Equal(["NAS01.lan"], incoming.OfType<Service>().Single().RunsOn);
  104. }
  105. [Fact]
  106. public void Systems_are_not_anchored_by_address() {
  107. // Two systems sharing an address are the same machine or a conflict — never a
  108. // parent and a child. Only services are hosted.
  109. List<Resource> existing = [System("nas01", "192.0.2.50")];
  110. List<Resource> incoming = [System("scanned-host", "192.0.2.50", "rpk1:net:b")];
  111. DiscoveryIdResolver.ResolveNames(existing, incoming);
  112. Assert.Empty(incoming.OfType<SystemResource>().Single().RunsOn);
  113. }
  114. // ---------------------------------------------------------------
  115. // Unifying a sweep's find with the guest a hypervisor already described
  116. // ---------------------------------------------------------------
  117. private static SystemResource Scanned(string name, string ip, string? mac = null) {
  118. var card = new SystemResource {
  119. Kind = SystemResource.KindLabel,
  120. Name = name,
  121. Ip = ip,
  122. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, mac ?? $"ip:{ip}")
  123. };
  124. if (mac != null)
  125. card.Labels["mac"] = mac;
  126. return card;
  127. }
  128. private static SystemResource Guest(string name, string ip, string id = "abc123") =>
  129. new() {
  130. Kind = SystemResource.KindLabel,
  131. Name = name,
  132. Ip = ip,
  133. DiscoveryId = $"rpk1:pve:{id}"
  134. };
  135. [Fact]
  136. public void A_sweep_find_becomes_the_guest_the_hypervisor_already_described() {
  137. // ARP is link-local, so a guest on another subnet gives the sweep no MAC and its
  138. // identity falls back to the address. Now that the hypervisor reports that same
  139. // address, it is the only thing tying the two records together.
  140. List<Resource> existing = [Guest("app-vm", "192.0.2.105")];
  141. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
  142. DiscoveryIdResolver.ResolveNames(existing, incoming);
  143. SystemResource card = incoming.OfType<SystemResource>().Single();
  144. Assert.Equal("app-vm", card.Name);
  145. // The sweep's weaker identity is dropped so the merge cannot downgrade the
  146. // hypervisor's.
  147. Assert.Null(card.DiscoveryId);
  148. }
  149. [Fact]
  150. public void A_sweep_find_that_saw_a_mac_is_left_to_the_mac_rule() {
  151. // A MAC is better evidence than an address. If it did not unify above, the two
  152. // records disagree, and an address must not override that.
  153. List<Resource> existing = [Guest("app-vm", "192.0.2.105")];
  154. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105", "bc:24:11:00:1a:01")];
  155. DiscoveryIdResolver.ResolveNames(existing, incoming);
  156. Assert.Equal("host-1a2b3c4d", incoming.OfType<SystemResource>().Single().Name);
  157. }
  158. [Fact]
  159. public void Two_stored_systems_at_one_address_unify_nothing() {
  160. // Overlapping subnets across sites, or a stale card. Ambiguity is not evidence.
  161. List<Resource> existing = [
  162. Guest("site-a-vm", "192.0.2.105", "aaa111"),
  163. Guest("site-b-vm", "192.0.2.105", "bbb222")
  164. ];
  165. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
  166. DiscoveryIdResolver.ResolveNames(existing, incoming);
  167. Assert.Equal("host-1a2b3c4d", incoming.OfType<SystemResource>().Single().Name);
  168. }
  169. [Fact]
  170. public void Two_sweep_finds_with_nothing_between_them_are_the_same_card_already() {
  171. // Neither card is more than "something replied at this address", and an address
  172. // is exactly what seeds their identity when no MAC was seen — so they carry the
  173. // same id and the address rule never gets a say. The stored name wins, as it
  174. // does for any re-run.
  175. List<Resource> existing = [Scanned("host-99999999", "192.0.2.105")];
  176. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
  177. Assert.Equal(existing[0].DiscoveryId, incoming[0].DiscoveryId);
  178. DiscoveryIdResolver.ResolveNames(existing, incoming);
  179. Assert.Equal("host-99999999", incoming.OfType<SystemResource>().Single().Name);
  180. }
  181. [Fact]
  182. public void A_sweep_find_folds_into_one_that_saw_the_mac() {
  183. // Not two stand-ins: the stored card names the NIC answering at that address —
  184. // a firewall's neighbour table does this for every subnet it routes — where the
  185. // incoming one only knows something replied. The MAC is the better identity, so
  186. // the address-only card folds into it rather than becoming a second machine.
  187. List<Resource> existing = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
  188. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105")];
  189. DiscoveryIdResolver.ResolveNames(existing, incoming);
  190. SystemResource card = incoming.OfType<SystemResource>().Single();
  191. Assert.Equal("host-99999999", card.Name);
  192. // The weaker address-seeded identity is dropped so the merge cannot downgrade
  193. // the MAC-seeded one it is landing on.
  194. Assert.Null(card.DiscoveryId);
  195. }
  196. [Fact]
  197. public void The_mac_wins_arriving_second_too() {
  198. // Sweep the routed subnet first, ask the firewall after: same two facts, same
  199. // one card. Here the MAC-seeded identity is the one that survives.
  200. List<Resource> existing = [Scanned("host-1a2b3c4d", "192.0.2.105")];
  201. List<Resource> incoming = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
  202. DiscoveryIdResolver.ResolveNames(existing, incoming);
  203. SystemResource card = incoming.OfType<SystemResource>().Single();
  204. Assert.Equal("host-1a2b3c4d", card.Name);
  205. Assert.NotNull(card.DiscoveryId);
  206. }
  207. [Fact]
  208. public void Two_sweep_finds_that_each_saw_a_mac_never_unify() {
  209. // Both name a NIC, and they name different ones. The MAC bridge has already had
  210. // its say; sharing an address now is a conflict or an overlapping subnet.
  211. List<Resource> existing = [Scanned("host-99999999", "192.0.2.105", "bc:24:11:00:1a:09")];
  212. List<Resource> incoming = [Scanned("host-1a2b3c4d", "192.0.2.105", "bc:24:11:00:1a:0a")];
  213. DiscoveryIdResolver.ResolveNames(existing, incoming);
  214. Assert.Equal("host-1a2b3c4d", incoming.OfType<SystemResource>().Single().Name);
  215. }
  216. }