PlaceholderNameUpgradeTests.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. using RackPeek.Domain.Discovery;
  2. using RackPeek.Domain.Resources;
  3. using RackPeek.Domain.Resources.Connections;
  4. using RackPeek.Domain.Resources.Services;
  5. using RackPeek.Domain.Resources.SystemResources;
  6. namespace Tests.Discovery;
  7. /// <summary>
  8. /// A placeholder name is a stand-in, and a later collector that knows the machine's
  9. /// real name should be allowed to replace it.
  10. /// <para>
  11. /// A sweep of a routed subnet can see that something answers and nothing else, so
  12. /// it writes host-1a2b3c4d. Run the firewall collector and that same machine has
  13. /// a DHCP name; ask the hypervisor and it has a guest name. Without this the
  14. /// inventory would keep the hash for ever and the better name would be discarded
  15. /// on every run.
  16. /// </para>
  17. /// <para>
  18. /// The line it must not cross is a name a person typed. That is what
  19. /// <see cref="Resource.UserNamed" /> records, and once set it is never unset.
  20. /// </para>
  21. /// </summary>
  22. public class PlaceholderNameUpgradeTests {
  23. private const string _mac = "bc:24:11:00:3a:01";
  24. private static string PlaceholderFor(string discoveryId) =>
  25. $"host-{DiscoveryId.ShortSuffix(discoveryId)}";
  26. /// <summary>A sweep's card: an address, a MAC, and a name that is just its own hash.</summary>
  27. private static SystemResource Stored(string? name = null, bool? userNamed = null) {
  28. var id = DiscoveryId.Create(DiscoveryId.NetworkScheme, _mac);
  29. return new SystemResource {
  30. Kind = SystemResource.KindLabel,
  31. Name = name ?? PlaceholderFor(id),
  32. DiscoveryId = id,
  33. UserNamed = userNamed,
  34. Ip = "192.0.2.50",
  35. Labels = { ["mac"] = _mac }
  36. };
  37. }
  38. /// <summary>The firewall's view of the same box: same identity, but it knows the name.</summary>
  39. private static SystemResource Incoming(string name = "forgejo") => new() {
  40. Kind = SystemResource.KindLabel,
  41. Name = name,
  42. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, _mac),
  43. Ip = "192.0.2.50",
  44. Labels = { ["mac"] = _mac }
  45. };
  46. private static List<Resource> Resolve(
  47. Resource stored,
  48. Resource incoming,
  49. IReadOnlyList<Connection>? storedConnections = null,
  50. params Resource[] alsoStored) {
  51. List<Resource> existing = [stored, .. alsoStored];
  52. DiscoveryIdResolver.ResolveNames(
  53. existing,
  54. [incoming],
  55. null,
  56. storedConnections,
  57. true);
  58. return existing;
  59. }
  60. [Fact]
  61. public void A_real_name_replaces_a_placeholder_nobody_chose() {
  62. SystemResource stored = Stored();
  63. Resolve(stored, Incoming());
  64. Assert.Equal("forgejo", stored.Name);
  65. }
  66. [Fact]
  67. public void A_name_a_person_typed_is_never_touched() {
  68. SystemResource stored = Stored("the-blue-one", true);
  69. Resolve(stored, Incoming());
  70. Assert.Equal("the-blue-one", stored.Name);
  71. }
  72. [Fact]
  73. public void A_placeholder_a_person_chose_to_keep_is_still_theirs() {
  74. // Renaming a card back to its hash is a strange thing to do, but it is a choice,
  75. // and the flag is what records that rather than the shape of the name.
  76. SystemResource stored = Stored(userNamed: true);
  77. var before = stored.Name;
  78. Resolve(stored, Incoming());
  79. Assert.Equal(before, stored.Name);
  80. }
  81. [Fact]
  82. public void A_placeholder_never_replaces_a_real_name() {
  83. // The reverse direction: the sweep runs after the firewall and knows less. The
  84. // upgrade is one-way or the card would flip names on alternate runs.
  85. SystemResource stored = Stored("forgejo");
  86. var placeholder = PlaceholderFor(stored.DiscoveryId!);
  87. Resolve(stored, Incoming(placeholder));
  88. Assert.Equal("forgejo", stored.Name);
  89. }
  90. [Fact]
  91. public void One_real_name_does_not_replace_another() {
  92. // Two collectors that each know a different name for one box would otherwise
  93. // rename it back and forth every run. First real name wins and stays.
  94. SystemResource stored = Stored("forgejo");
  95. Resolve(stored, Incoming("git-server"));
  96. Assert.Equal("forgejo", stored.Name);
  97. }
  98. [Fact]
  99. public void A_hand_written_resource_counts_as_user_named_without_the_flag() {
  100. // Nothing but a person could have written a resource with no discovery id, so
  101. // the absent flag must not be read as permission.
  102. var stored = new SystemResource {
  103. Kind = SystemResource.KindLabel,
  104. Name = "forgejo",
  105. Ip = "192.0.2.50"
  106. };
  107. Assert.True(stored.IsUserNamed());
  108. Assert.False(Stored().IsUserNamed());
  109. }
  110. [Fact]
  111. public void Everything_pointing_at_the_old_name_follows_it() {
  112. SystemResource stored = Stored();
  113. var placeholder = stored.Name;
  114. var service = new Service {
  115. Kind = Service.KindLabel,
  116. Name = "forgejo-https",
  117. RunsOn = [placeholder]
  118. };
  119. Resolve(stored, Incoming(), null, service);
  120. Assert.Equal(["forgejo"], service.RunsOn);
  121. }
  122. [Fact]
  123. public void Stored_connections_follow_it_too() {
  124. SystemResource stored = Stored();
  125. var placeholder = stored.Name;
  126. var connection = new Connection {
  127. A = new PortReference { Resource = placeholder },
  128. B = new PortReference { Resource = "core-switch", PortIndex = 12 }
  129. };
  130. Resolve(stored, Incoming(), [connection]);
  131. Assert.Equal("forgejo", connection.A.Resource);
  132. }
  133. [Fact]
  134. public void A_re_push_of_exported_yaml_follows_the_rename_too() {
  135. // The payload still calls the machine by the name it was stored under, because
  136. // that is what was exported. Its links have to land on the upgraded card.
  137. SystemResource stored = Stored();
  138. var placeholder = stored.Name;
  139. var arriving = new Service {
  140. Kind = Service.KindLabel,
  141. Name = "forgejo-https",
  142. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:443"),
  143. RunsOn = [placeholder]
  144. };
  145. DiscoveryIdResolver.ResolveNames([stored], [Incoming(), arriving], null, null, true);
  146. Assert.Equal("forgejo", stored.Name);
  147. Assert.Equal(["forgejo"], arriving.RunsOn);
  148. }
  149. [Fact]
  150. public void A_service_named_after_the_host_follows_it() {
  151. // The sweep names what it finds on a port after the host it found it on, so the
  152. // leftover reads host-1a2b3c4d-ssh running on forgejo until this carries it over.
  153. SystemResource stored = Stored();
  154. var placeholder = stored.Name;
  155. var ssh = new Service {
  156. Kind = Service.KindLabel,
  157. Name = $"{placeholder}-ssh",
  158. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
  159. RunsOn = [placeholder]
  160. };
  161. Resolve(stored, Incoming(), null, ssh);
  162. Assert.Equal("forgejo-ssh", ssh.Name);
  163. Assert.Equal(["forgejo"], ssh.RunsOn);
  164. }
  165. [Fact]
  166. public void A_service_a_person_named_keeps_its_name() {
  167. SystemResource stored = Stored();
  168. var placeholder = stored.Name;
  169. var ssh = new Service {
  170. Kind = Service.KindLabel,
  171. Name = $"{placeholder}-ssh",
  172. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
  173. UserNamed = true,
  174. RunsOn = [placeholder]
  175. };
  176. Resolve(stored, Incoming(), null, ssh);
  177. Assert.Equal($"{placeholder}-ssh", ssh.Name);
  178. }
  179. [Fact]
  180. public void A_service_that_only_looks_like_the_host_is_not_renamed() {
  181. // Named for the old host but running somewhere else entirely: a coincidence,
  182. // and coincidences are not evidence of anything.
  183. SystemResource stored = Stored();
  184. var placeholder = stored.Name;
  185. var stray = new Service {
  186. Kind = Service.KindLabel,
  187. Name = $"{placeholder}-ssh",
  188. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "elsewhere:22"),
  189. RunsOn = ["some-other-box"]
  190. };
  191. Resolve(stored, Incoming(), null, stray);
  192. Assert.Equal($"{placeholder}-ssh", stray.Name);
  193. }
  194. [Fact]
  195. public void A_service_whose_new_name_is_taken_keeps_the_old_one() {
  196. SystemResource stored = Stored();
  197. var placeholder = stored.Name;
  198. var ssh = new Service {
  199. Kind = Service.KindLabel,
  200. Name = $"{placeholder}-ssh",
  201. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
  202. RunsOn = [placeholder]
  203. };
  204. var occupier = new Service {
  205. Kind = Service.KindLabel,
  206. Name = "forgejo-ssh",
  207. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, "other:22"),
  208. RunsOn = ["some-other-box"]
  209. };
  210. Resolve(stored, Incoming(), null, ssh, occupier);
  211. Assert.Equal($"{placeholder}-ssh", ssh.Name);
  212. }
  213. [Fact]
  214. public void A_connection_to_a_renamed_service_follows_it() {
  215. SystemResource stored = Stored();
  216. var placeholder = stored.Name;
  217. var ssh = new Service {
  218. Kind = Service.KindLabel,
  219. Name = $"{placeholder}-ssh",
  220. DiscoveryId = DiscoveryId.Create(DiscoveryId.NetworkScheme, $"{_mac}:22"),
  221. RunsOn = [placeholder]
  222. };
  223. var connection = new Connection {
  224. A = new PortReference { Resource = $"{placeholder}-ssh" },
  225. B = new PortReference { Resource = "core-switch", PortIndex = 3 }
  226. };
  227. DiscoveryIdResolver.ResolveNames(
  228. [stored, ssh], [Incoming()], null, [connection], true);
  229. Assert.Equal("forgejo-ssh", connection.A.Resource);
  230. }
  231. [Fact]
  232. public void A_name_already_in_use_is_left_alone() {
  233. // Renaming onto an occupied name would collide two unrelated resources in the
  234. // merge, which keys on name. Keeping the placeholder is the safe outcome.
  235. SystemResource stored = Stored();
  236. var placeholder = stored.Name;
  237. var other = new SystemResource {
  238. Kind = SystemResource.KindLabel,
  239. Name = "forgejo",
  240. Ip = "192.0.2.99"
  241. };
  242. Resolve(stored, Incoming(), null, other);
  243. Assert.Equal(placeholder, stored.Name);
  244. }
  245. [Fact]
  246. public void A_dry_run_leaves_the_stored_name_where_it_was() {
  247. // The upgrade rewrites the inventory, so a caller that is only reporting what
  248. // would happen must not ask for it — the default is off for exactly this reason.
  249. SystemResource stored = Stored();
  250. var placeholder = stored.Name;
  251. DiscoveryIdResolver.ResolveNames([stored], [Incoming()]);
  252. Assert.Equal(placeholder, stored.Name);
  253. }
  254. }