confctl 2.2.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/.git-hooks/pre_commit/nixfmt.rb +13 -0
  3. data/.github/workflows/rspec.yml +64 -0
  4. data/.github/workflows/rubocop.yml +27 -0
  5. data/.github/workflows/tests.yml +139 -0
  6. data/.gitignore +12 -7
  7. data/.overcommit.yml +2 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +8 -1
  10. data/AGENTS.md +42 -0
  11. data/CHANGELOG.md +57 -0
  12. data/Gemfile +3 -2
  13. data/Gemfile.lock +198 -0
  14. data/README.md +166 -58
  15. data/Rakefile +5 -0
  16. data/confctl.gemspec +20 -2
  17. data/docs/carrier.md +3 -3
  18. data/docs/flake-inputs.md +159 -0
  19. data/docs/swpins-to-flakes.md +325 -0
  20. data/example/cluster/module-list.nix +2 -1
  21. data/example/cluster/nixos-machine/config.nix +12 -2
  22. data/example/cluster/nixos-machine/hardware.nix +6 -1
  23. data/example/cluster/vpsadminos-container/config.nix +6 -1
  24. data/example/cluster/vpsadminos-container/module.nix +4 -1
  25. data/example/cluster/vpsadminos-machine/config.nix +6 -1
  26. data/example/cluster/vpsadminos-machine/hardware.nix +6 -1
  27. data/example/cluster/vpsadminos-machine/module.nix +5 -2
  28. data/example/cluster/vpsfreecz-vps/config.nix +6 -1
  29. data/example/cluster/vpsfreecz-vps/module.nix +4 -1
  30. data/example/configs/swpins.nix +8 -3
  31. data/example/environments/base.nix +6 -1
  32. data/example/swpins/core.json +35 -0
  33. data/example-flake/.gitignore +2 -0
  34. data/example-flake/README.md +38 -0
  35. data/example-flake/cluster/cluster.nix +5 -0
  36. data/example-flake/cluster/module-list.nix +4 -0
  37. data/example-flake/cluster/nested/nixos-machine/config.nix +25 -0
  38. data/example-flake/cluster/nested/nixos-machine/hardware.nix +9 -0
  39. data/example-flake/cluster/nested/nixos-machine/module.nix +8 -0
  40. data/example-flake/cluster/nixos-machine/config.nix +25 -0
  41. data/example-flake/cluster/nixos-machine/hardware.nix +9 -0
  42. data/example-flake/cluster/nixos-machine/module.nix +8 -0
  43. data/example-flake/cluster/vpsadminos-container/config.nix +28 -0
  44. data/example-flake/cluster/vpsadminos-container/module.nix +8 -0
  45. data/example-flake/cluster/vpsadminos-machine/config.nix +27 -0
  46. data/example-flake/cluster/vpsadminos-machine/hardware.nix +9 -0
  47. data/example-flake/cluster/vpsadminos-machine/module.nix +8 -0
  48. data/example-flake/cluster/vpsfreecz-vps/config.nix +31 -0
  49. data/example-flake/cluster/vpsfreecz-vps/module.nix +8 -0
  50. data/example-flake/configs/confctl.nix +10 -0
  51. data/example-flake/data/default.nix +5 -0
  52. data/example-flake/data/ssh-keys.nix +7 -0
  53. data/example-flake/environments/base.nix +18 -0
  54. data/example-flake/flake.lock +75 -0
  55. data/example-flake/flake.nix +36 -0
  56. data/example-flake/modules/module-list.nix +13 -0
  57. data/example-flake/shell.nix +11 -0
  58. data/flake.lock +159 -0
  59. data/flake.nix +189 -0
  60. data/gemset.nix +1022 -0
  61. data/lib/confctl/cli/app.rb +145 -0
  62. data/lib/confctl/cli/attr_filters.rb +1 -1
  63. data/lib/confctl/cli/cluster.rb +681 -108
  64. data/lib/confctl/cli/command.rb +24 -2
  65. data/lib/confctl/cli/configuration.rb +171 -106
  66. data/lib/confctl/cli/generation.rb +65 -1
  67. data/lib/confctl/cli/inputs/channels.rb +190 -0
  68. data/lib/confctl/cli/inputs/machines.rb +83 -0
  69. data/lib/confctl/cli/inputs/root.rb +101 -0
  70. data/lib/confctl/cli/inputs.rb +5 -0
  71. data/lib/confctl/cli/log_view.rb +21 -10
  72. data/lib/confctl/cli/migrate/swpins_to_flakes.rb +866 -0
  73. data/lib/confctl/cli/migrate.rb +5 -0
  74. data/lib/confctl/cli/output_formatter.rb +5 -7
  75. data/lib/confctl/cli/swpins/base.rb +9 -0
  76. data/lib/confctl/cli/swpins/channel.rb +2 -5
  77. data/lib/confctl/cli/swpins/cluster.rb +2 -5
  78. data/lib/confctl/cli/swpins/core.rb +2 -5
  79. data/lib/confctl/config_type.rb +7 -0
  80. data/lib/confctl/flake_lock.rb +78 -0
  81. data/lib/confctl/flake_lock_diff.rb +36 -0
  82. data/lib/confctl/generation/build.rb +131 -24
  83. data/lib/confctl/generation/build_list.rb +4 -3
  84. data/lib/confctl/generation/unified.rb +14 -1
  85. data/lib/confctl/git_repo_mirror.rb +2 -2
  86. data/lib/confctl/health_checks/run_command.rb +3 -2
  87. data/lib/confctl/health_checks/systemd/properties.rb +1 -1
  88. data/lib/confctl/health_checks/systemd/property_list.rb +2 -2
  89. data/lib/confctl/inputs/commit_message.rb +125 -0
  90. data/lib/confctl/inputs/git_commit.rb +17 -0
  91. data/lib/confctl/inputs/nix_output_guard.rb +37 -0
  92. data/lib/confctl/inputs/setter.rb +179 -0
  93. data/lib/confctl/inputs/updater.rb +76 -0
  94. data/lib/confctl/inputs.rb +5 -0
  95. data/lib/confctl/inputs_info.rb +50 -0
  96. data/lib/confctl/line_buffer.rb +1 -1
  97. data/lib/confctl/machine.rb +18 -3
  98. data/lib/confctl/machine_control.rb +8 -2
  99. data/lib/confctl/machine_list.rb +2 -2
  100. data/lib/confctl/machine_status.rb +63 -20
  101. data/lib/confctl/nix/args.rb +48 -0
  102. data/lib/confctl/nix.rb +30 -437
  103. data/lib/confctl/nix_build_flake.rb +95 -0
  104. data/lib/confctl/nix_copy.rb +14 -2
  105. data/lib/confctl/nix_flake.rb +449 -0
  106. data/lib/confctl/nix_format.rb +3 -3
  107. data/lib/confctl/nix_legacy.rb +467 -0
  108. data/lib/confctl/swpins/change_set.rb +29 -3
  109. data/lib/confctl/swpins/specs/base.rb +2 -2
  110. data/lib/confctl/ui.rb +19 -0
  111. data/lib/confctl/version.rb +1 -1
  112. data/man/index.html +11 -0
  113. data/man/man8/confctl-options.nix.8.html +113 -0
  114. data/man/man8/confctl.8 +152 -21
  115. data/man/man8/confctl.8.html +355 -0
  116. data/man/man8/confctl.8.md +148 -17
  117. data/man/style.css +301 -0
  118. data/nix/evaluator.nix +94 -69
  119. data/nix/flake/mk-confctl-devshell.nix +85 -0
  120. data/nix/flake/mk-confctl-outputs.nix +522 -0
  121. data/nix/flake/mk-config-devshell.nix +168 -0
  122. data/nix/lib/default.nix +118 -65
  123. data/nix/lib/machine/default.nix +65 -45
  124. data/nix/lib/machine/info.nix +16 -5
  125. data/nix/lib/swpins/eval.nix +42 -29
  126. data/nix/lib/swpins/options.nix +6 -2
  127. data/nix/machines.nix +23 -15
  128. data/nix/modules/cluster/default.nix +104 -41
  129. data/nix/modules/confctl/carrier/base.nix +11 -4
  130. data/nix/modules/confctl/carrier/carrier-env.rb +2 -2
  131. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +50 -16
  132. data/nix/modules/confctl/carrier/netboot/nixos.nix +56 -24
  133. data/nix/modules/confctl/configuration-info.nix +17 -0
  134. data/nix/modules/confctl/generations.nix +2 -2
  135. data/nix/modules/confctl/host.nix +13 -0
  136. data/nix/modules/confctl/inputs-info.nix +21 -0
  137. data/nix/modules/confctl/kexec-netboot/default.nix +13 -6
  138. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +3 -0
  139. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +25 -16
  140. data/nix/modules/confctl/nix.nix +31 -1
  141. data/nix/modules/confctl/swpins.nix +13 -6
  142. data/nix/modules/module-list.nix +4 -2
  143. data/nix/modules/system-list.nix +4 -1
  144. data/nix/package.nix +37 -0
  145. data/shell.nix +39 -8
  146. data/skills/confctl-configuration-update/SKILL.md +228 -0
  147. data/skills/confctl-configuration-update/agents/openai.yaml +4 -0
  148. data/skills/confctl-release/SKILL.md +102 -0
  149. data/skills/confctl-release/agents/openai.yaml +4 -0
  150. data/spec/confctl/cli/cluster_health_check_spec.rb +43 -0
  151. data/spec/confctl/cli/cluster_skip_current_deploy_spec.rb +157 -0
  152. data/spec/confctl/cli/cluster_status_flake_spec.rb +83 -0
  153. data/spec/confctl/cli/inputs_set_output_spec.rb +70 -0
  154. data/spec/confctl/configuration_spec.rb +45 -0
  155. data/spec/confctl/inputs_spec.rb +104 -0
  156. data/spec/confctl/machine_control_spec.rb +112 -0
  157. data/spec/confctl/machine_list_spec.rb +47 -0
  158. data/spec/confctl/nix_flake_spec.rb +41 -0
  159. data/spec/confctl/swpins_spec.rb +106 -0
  160. data/spec/generation/build_modes_spec.rb +132 -0
  161. data/spec/inputs/commit_message_spec.rb +205 -0
  162. data/spec/inputs/nix_cached_fallback_spec.rb +50 -0
  163. data/spec/inputs/nix_output_guard_spec.rb +45 -0
  164. data/spec/spec_helper.rb +12 -0
  165. data/spec/support/cli_helper.rb +57 -0
  166. data/test-runner.sh +7 -0
  167. data/tests/all-tests.nix +30 -0
  168. data/tests/make-test.nix +15 -0
  169. data/tests/runner/extensions/confctl_helpers.rb +238 -0
  170. data/tests/runner/extensions/hostfwd_ports.rb +41 -0
  171. data/tests/suite/auto_rollback.nix +344 -0
  172. data/tests/suite/carrier/deploy.nix +751 -0
  173. data/tests/suite/carrier/netboot.nix +849 -0
  174. data/tests/suite/deploy/base.nix +806 -0
  175. data/tests/suite/deploy/flakes.nix +1 -0
  176. data/tests/suite/deploy/swpins.nix +1 -0
  177. metadata +104 -4
  178. data/nix/modules/confctl/overlays.nix +0 -15
@@ -0,0 +1,849 @@
1
+ { testFramework, ... }@args:
2
+ let
3
+ vpsadminosSource = testFramework.sourcePath;
4
+ in
5
+ import ../../make-test.nix (
6
+ {
7
+ pkgs,
8
+ confctlPackage,
9
+ confctlSrc,
10
+ ...
11
+ }:
12
+ let
13
+ confctlBin =
14
+ if confctlPackage == null then
15
+ throw "suiteArgs.confctlPackage is required"
16
+ else
17
+ "${confctlPackage}/bin/confctl";
18
+
19
+ confctlSource = if confctlSrc == null then throw "suiteArgs.confctlSrc is required" else confctlSrc;
20
+ carrierMgmtMac = "52:54:00:10:00:01";
21
+ carrierPxeMac = "52:54:00:10:00:02";
22
+ nixosPxeMac = "52:54:00:10:00:11";
23
+ vpsadminosPxeMac = "52:54:00:10:00:12";
24
+ pxeMachineMemory = 4096;
25
+ pxeNetworkTag = "carrier-pxe";
26
+ in
27
+ {
28
+ name = "carrier-netboot";
29
+
30
+ description = ''
31
+ Verify carried NixOS and vpsAdminOS machines can be served and booted over PXE from a NixOS carrier.
32
+ '';
33
+
34
+ tags = [
35
+ "ci"
36
+ ];
37
+
38
+ machines = {
39
+ carrier = {
40
+ spin = "nixos";
41
+ diskSize = 20480;
42
+ networks = [
43
+ {
44
+ type = "user";
45
+ macAddress = carrierMgmtMac;
46
+ opts = {
47
+ hostForward = "tcp::net1-:22";
48
+ network = "10.0.2.0/24";
49
+ host = "10.0.2.2";
50
+ dns = "10.0.2.3";
51
+ };
52
+ }
53
+ {
54
+ type = "socket";
55
+ macAddress = carrierPxeMac;
56
+ model = "e1000";
57
+ mcast.port = pxeNetworkTag;
58
+ }
59
+ ];
60
+ config = {
61
+ services.openssh = {
62
+ enable = true;
63
+ settings = {
64
+ PermitRootLogin = "yes";
65
+ PasswordAuthentication = false;
66
+ };
67
+ };
68
+ systemd.services.logrotate-checkconf.enable = false;
69
+ networking.firewall.enable = false;
70
+ environment.systemPackages = with pkgs; [
71
+ git
72
+ ];
73
+ };
74
+ };
75
+
76
+ nixos = {
77
+ spin = "nixos";
78
+ config = null;
79
+ memory = pxeMachineMemory;
80
+ cpus = 2;
81
+ cpu = {
82
+ cores = 2;
83
+ threads = 1;
84
+ sockets = 1;
85
+ };
86
+ networks = [
87
+ {
88
+ type = "socket";
89
+ macAddress = nixosPxeMac;
90
+ model = "e1000";
91
+ mcast.port = pxeNetworkTag;
92
+ }
93
+ ];
94
+ };
95
+
96
+ vpsadminos = {
97
+ spin = "vpsadminos";
98
+ config = null;
99
+ memory = pxeMachineMemory;
100
+ cpus = 2;
101
+ cpu = {
102
+ cores = 2;
103
+ threads = 1;
104
+ sockets = 1;
105
+ };
106
+ networks = [
107
+ {
108
+ type = "socket";
109
+ macAddress = vpsadminosPxeMac;
110
+ model = "e1000";
111
+ mcast.port = pxeNetworkTag;
112
+ }
113
+ ];
114
+ };
115
+ };
116
+
117
+ testScript = ''
118
+ require 'base64'
119
+ require 'fileutils'
120
+ require 'json'
121
+ require 'shellwords'
122
+
123
+ CARRIER_MACHINE = 'carrier'
124
+ NIXOS_MACHINE = 'nixos-machine'
125
+ VPSADMINOS_MACHINE = 'vpsadminos-machine'
126
+ VPSADMINOS_PATH = '${vpsadminosSource}'
127
+
128
+ NIXOS_FQDN = "#{NIXOS_MACHINE}.test.invalid"
129
+ VPSADMINOS_FQDN = "#{VPSADMINOS_MACHINE}.test.invalid"
130
+ NIXOS_BOOT_TIMEOUT = 600
131
+ VPSADMINOS_BOOT_TIMEOUT = 360
132
+ NETBOOT_HTTP_TIMEOUT = 180
133
+ PXE_ADDRESS = '192.168.100.1'
134
+ PXE_SUBNET = '192.168.100.0/24'
135
+
136
+ NIXOS_PXE_MAC = '${nixosPxeMac}'
137
+ VPSADMINOS_PXE_MAC = '${vpsadminosPxeMac}'
138
+
139
+ def nixos_virtualisation_options_module
140
+ <<~NIX
141
+ (
142
+ { lib, ... }:
143
+ {
144
+ options.virtualisation.cores = lib.mkOption {
145
+ type = lib.types.int;
146
+ default = 2;
147
+ };
148
+ options.virtualisation.memorySize = lib.mkOption {
149
+ type = lib.types.int;
150
+ default = 2048;
151
+ };
152
+ options.virtualisation.mountHostNixStore = lib.mkOption {
153
+ type = lib.types.bool;
154
+ default = false;
155
+ };
156
+ options.virtualisation.sharedDirectories = lib.mkOption {
157
+ type = lib.types.attrsOf lib.types.anything;
158
+ default = { };
159
+ };
160
+ }
161
+ )
162
+ NIX
163
+ end
164
+
165
+ def nixos_test_base_import
166
+ '(inputs.vpsadminos + "/tests/configs/nixos/base.nix")'
167
+ end
168
+
169
+ def vpsadminos_test_base_import
170
+ '(inputs.vpsadminos + "/tests/configs/vpsadminos/base.nix")'
171
+ end
172
+
173
+ def write_flake_root!(conf_dir)
174
+ File.write(File.join(conf_dir, 'flake.nix'), <<~NIX)
175
+ {
176
+ description = "confctl carrier netboot test fixture";
177
+
178
+ inputs = {
179
+ confctl.url = "path:#{confctl_src}";
180
+ nixpkgs.follows = "confctl/nixpkgs";
181
+ vpsadminos.url = "path:#{VPSADMINOS_PATH}";
182
+ };
183
+
184
+ outputs = inputs@{ self, confctl, ... }:
185
+ let
186
+ channels = {
187
+ carrier = {
188
+ nixpkgs = "nixpkgs";
189
+ vpsadminos = "vpsadminos";
190
+ };
191
+ };
192
+
193
+ confctlOutputs = confctl.lib.mkConfctlOutputs {
194
+ confDir = ./.;
195
+ inherit inputs channels;
196
+ };
197
+ in
198
+ {
199
+ confctl = confctlOutputs;
200
+
201
+ devShells.x86_64-linux.default = confctl.lib.mkConfigDevShell {
202
+ system = "x86_64-linux";
203
+ mode = "minimal";
204
+ };
205
+ };
206
+ }
207
+ NIX
208
+ end
209
+
210
+ def write_hardware_config!(conf_dir, machine_name)
211
+ dir = File.join(conf_dir, 'cluster', machine_name)
212
+ FileUtils.mkdir_p(dir)
213
+
214
+ File.write(File.join(dir, 'hardware.nix'), <<~NIX)
215
+ {
216
+ config,
217
+ pkgs,
218
+ lib,
219
+ ...
220
+ }:
221
+ {
222
+ # Test fixture hardware configuration.
223
+ }
224
+ NIX
225
+ end
226
+
227
+ def write_carrier_config!(conf_dir)
228
+ write_hardware_config!(conf_dir, 'carrier')
229
+
230
+ File.write(File.join(conf_dir, 'cluster/carrier/config.nix'), <<~NIX)
231
+ { inputs, lib, ... }:
232
+ {
233
+ imports = [
234
+ ../../environments/base.nix
235
+ ./hardware.nix
236
+ #{nixos_virtualisation_options_module}
237
+ #{nixos_test_base_import}
238
+ ];
239
+
240
+ networking.hostName = "carrier";
241
+ time.timeZone = lib.mkForce "UTC";
242
+ services.openssh = {
243
+ enable = true;
244
+ settings = {
245
+ PermitRootLogin = "yes";
246
+ PasswordAuthentication = false;
247
+ };
248
+ };
249
+
250
+ networking.firewall.enable = false;
251
+ networking.useNetworkd = true;
252
+ systemd.network.enable = true;
253
+
254
+ systemd.network.networks = {
255
+ "10-mgmt" = {
256
+ matchConfig.Name = "eth0";
257
+ networkConfig.DHCP = "ipv4";
258
+ linkConfig.RequiredForOnline = "routable";
259
+ };
260
+
261
+ "10-pxe" = {
262
+ matchConfig.Name = "eth1";
263
+ address = [ "#{PXE_ADDRESS}/24" ];
264
+ networkConfig.DHCP = "no";
265
+ linkConfig.RequiredForOnline = "no";
266
+ };
267
+ };
268
+
269
+ services.dnsmasq = {
270
+ enable = true;
271
+ settings = {
272
+ interface = "eth1";
273
+ bind-interfaces = true;
274
+ port = 0;
275
+ dhcp-authoritative = true;
276
+ log-dhcp = true;
277
+ dhcp-range = "192.168.100.50,192.168.100.150,255.255.255.0";
278
+ dhcp-option = [
279
+ "3,#{PXE_ADDRESS}"
280
+ "6,#{PXE_ADDRESS}"
281
+ ];
282
+ dhcp-boot = "pxelinux.0,carrier,#{PXE_ADDRESS}";
283
+ };
284
+ };
285
+ systemd.services.logrotate-checkconf.enable = false;
286
+
287
+ confctl.carrier.netboot = {
288
+ enable = true;
289
+ host = "#{PXE_ADDRESS}";
290
+ allowedIPv4Ranges = [ "#{PXE_SUBNET}" ];
291
+ };
292
+ }
293
+ NIX
294
+ end
295
+
296
+ def write_nixos_config!(conf_dir, marker:)
297
+ write_hardware_config!(conf_dir, NIXOS_MACHINE)
298
+
299
+ File.write(File.join(conf_dir, "cluster/#{NIXOS_MACHINE}/config.nix"), <<~NIX)
300
+ { config, lib, pkgs, confMachine, inputs, inputsInfo, ... }:
301
+ let
302
+ toplevel = builtins.unsafeDiscardStringContext config.system.build.toplevel;
303
+ kernelParams = [
304
+ "console=ttyS0"
305
+ "init=''${toplevel}/init"
306
+ "nohibernate"
307
+ "loglevel=4"
308
+ "lsm=landlock,yama,bpf"
309
+ ];
310
+
311
+ machineJson = pkgs.writeText "machine-#{NIXOS_MACHINE}.json" (builtins.toJSON {
312
+ spin = "nixos";
313
+ fqdn = confMachine.host.fqdn;
314
+ label = confMachine.host.fqdn;
315
+ inherit toplevel kernelParams;
316
+ version = config.system.nixos.version;
317
+ revision = config.system.nixos.revision;
318
+ macs = confMachine.netboot.macs;
319
+ inputs-info = inputsInfo;
320
+ });
321
+ in
322
+ {
323
+ imports = [
324
+ ../../environments/base.nix
325
+ ./hardware.nix
326
+ #{nixos_virtualisation_options_module}
327
+ #{nixos_test_base_import}
328
+ (inputs.nixpkgs + "/nixos/modules/installer/netboot/netboot-minimal.nix")
329
+ ];
330
+
331
+ networking.hostName = "#{NIXOS_MACHINE}";
332
+ time.timeZone = lib.mkForce "UTC";
333
+ networking.useDHCP = lib.mkForce true;
334
+ networking.nameservers = [ "#{PXE_ADDRESS}" ];
335
+ virtualisation.memorySize = lib.mkForce ${toString pxeMachineMemory};
336
+ boot.initrd.availableKernelModules = lib.mkAfter [
337
+ "e1000"
338
+ "e1000e"
339
+ ];
340
+
341
+ confctl.programs.kexec-netboot.enable = true;
342
+
343
+ environment.etc."confctl-marker".text = "#{marker}\n";
344
+
345
+ system.build.dist = pkgs.symlinkJoin {
346
+ name = "#{NIXOS_MACHINE}-netboot";
347
+ paths = [
348
+ config.system.build.netbootRamdisk
349
+ config.system.build.kernel
350
+ config.system.build.netbootIpxeScript
351
+ ];
352
+
353
+ postBuild = '''
354
+ ln -s ''${machineJson} $out/machine.json
355
+ ''';
356
+ };
357
+ }
358
+ NIX
359
+ end
360
+
361
+ def write_vpsadminos_config!(conf_dir, marker:)
362
+ write_hardware_config!(conf_dir, VPSADMINOS_MACHINE)
363
+
364
+ File.write(File.join(conf_dir, "cluster/#{VPSADMINOS_MACHINE}/config.nix"), <<~NIX)
365
+ { config, pkgs, lib, confMachine, inputsInfo, inputs, ... }:
366
+ let
367
+ machineJson = pkgs.writeText "machine-#{VPSADMINOS_MACHINE}.json" (builtins.toJSON {
368
+ spin = "vpsadminos";
369
+ fqdn = confMachine.host.fqdn;
370
+ label = confMachine.host.fqdn;
371
+ toplevel = builtins.unsafeDiscardStringContext config.system.build.toplevel;
372
+ version = config.system.vpsadminos.version;
373
+ revision = config.system.vpsadminos.revision;
374
+ macs = confMachine.netboot.macs;
375
+ inputs-info = inputsInfo;
376
+ });
377
+ in
378
+ {
379
+ imports = [
380
+ ../../environments/base.nix
381
+ ./hardware.nix
382
+ #{vpsadminos_test_base_import}
383
+ ];
384
+
385
+ networking.hostName = "#{VPSADMINOS_MACHINE}";
386
+ networking.static.enable = false;
387
+ networking.useDHCP = true;
388
+ networking.nameservers = [ "#{PXE_ADDRESS}" ];
389
+
390
+ osctld.waitForNetworkOnline = false;
391
+ osctld.waitForSetClock = false;
392
+
393
+ boot.kernelParams = [
394
+ "console=ttyS0"
395
+ ];
396
+ boot.initrd.network = {
397
+ enable = true;
398
+ useDHCP = true;
399
+ preferredDHCPInterfaceMacAddresses = [ "#{VPSADMINOS_PXE_MAC}" ];
400
+ };
401
+ boot.initrd.kernelModules = lib.mkAfter [
402
+ "e1000"
403
+ "e1000e"
404
+ ];
405
+
406
+ confctl.programs.kexec-netboot.enable = true;
407
+
408
+ environment.etc."confctl-marker".text = "#{marker}\n";
409
+
410
+ system.distBuilderCommands = '''
411
+ ln -s ''${machineJson} $out/machine.json
412
+ ''';
413
+
414
+ system.stateVersion = "20.09";
415
+ }
416
+ NIX
417
+ end
418
+
419
+ def write_machine_modules!(conf_dir, carrier_port:)
420
+ FileUtils.mkdir_p(File.join(conf_dir, 'cluster', 'carrier'))
421
+ FileUtils.mkdir_p(File.join(conf_dir, 'cluster', NIXOS_MACHINE))
422
+ FileUtils.mkdir_p(File.join(conf_dir, 'cluster', VPSADMINOS_MACHINE))
423
+
424
+ File.write(File.join(conf_dir, 'cluster/carrier/module.nix'), <<~NIX)
425
+ { config, ... }:
426
+ {
427
+ cluster."carrier" = {
428
+ spin = "nixos";
429
+ inputs.channels = [ "carrier" ];
430
+ host = {
431
+ name = "carrier";
432
+ domain = "test.invalid";
433
+ target = "127.0.0.1";
434
+ port = #{carrier_port};
435
+ };
436
+ healthChecks.machineCommands = [
437
+ {
438
+ description = "true";
439
+ command = [ "true" ];
440
+ }
441
+ ];
442
+
443
+ carrier = {
444
+ enable = true;
445
+ machines = [
446
+ {
447
+ machine = "#{NIXOS_MACHINE}";
448
+ alias = "#{NIXOS_MACHINE}";
449
+ buildAttribute = [ "system" "build" "dist" ];
450
+ }
451
+ {
452
+ machine = "#{VPSADMINOS_MACHINE}";
453
+ alias = "#{VPSADMINOS_MACHINE}";
454
+ buildAttribute = [ "system" "build" "dist" ];
455
+ }
456
+ ];
457
+ };
458
+ };
459
+ }
460
+ NIX
461
+
462
+ File.write(File.join(conf_dir, "cluster/#{NIXOS_MACHINE}/module.nix"), <<~NIX)
463
+ { config, ... }:
464
+ {
465
+ cluster."#{NIXOS_MACHINE}" = {
466
+ spin = "nixos";
467
+ inputs.channels = [ "carrier" ];
468
+ host = {
469
+ name = "#{NIXOS_MACHINE}";
470
+ domain = "test.invalid";
471
+ target = null;
472
+ };
473
+ netboot = {
474
+ enable = true;
475
+ macs = [ "#{NIXOS_PXE_MAC}" ];
476
+ };
477
+ };
478
+ }
479
+ NIX
480
+
481
+ File.write(File.join(conf_dir, "cluster/#{VPSADMINOS_MACHINE}/module.nix"), <<~NIX)
482
+ { config, ... }:
483
+ {
484
+ cluster."#{VPSADMINOS_MACHINE}" = {
485
+ spin = "vpsadminos";
486
+ inputs.channels = [ "carrier" ];
487
+ host = {
488
+ name = "#{VPSADMINOS_MACHINE}";
489
+ domain = "test.invalid";
490
+ target = null;
491
+ };
492
+ netboot = {
493
+ enable = true;
494
+ macs = [ "#{VPSADMINOS_PXE_MAC}" ];
495
+ };
496
+ };
497
+ }
498
+ NIX
499
+ end
500
+
501
+ def prepare_fixture!(conf_dir:, carrier_port:, pubkey:)
502
+ prepare_fixture_dir!(conf_dir)
503
+ write_flake_root!(conf_dir)
504
+ FileUtils.rm_f(File.join(conf_dir, 'flake.lock'))
505
+ run_local!(%w[nix flake lock], chdir: conf_dir)
506
+ write_machine_modules!(conf_dir, carrier_port:)
507
+ write_carrier_config!(conf_dir)
508
+ write_nixos_config!(conf_dir, marker: 'A')
509
+ write_vpsadminos_config!(conf_dir, marker: 'A')
510
+ write_admin_ssh_keys!(conf_dir, pubkey)
511
+ write_cluster_modules!(
512
+ conf_dir,
513
+ [
514
+ 'carrier/module.nix',
515
+ "#{NIXOS_MACHINE}/module.nix",
516
+ "#{VPSADMINOS_MACHINE}/module.nix"
517
+ ]
518
+ )
519
+ init_fixture_repo!(conf_dir)
520
+ end
521
+
522
+ def update_nixos_generation!(marker:)
523
+ write_nixos_config!(@conf_dir, marker:)
524
+ end
525
+
526
+ def update_vpsadminos_generation!(marker:)
527
+ write_vpsadminos_config!(@conf_dir, marker:)
528
+ end
529
+
530
+ def carrier_success?(cmd, timeout: 30)
531
+ status, = carrier.execute(cmd, timeout:)
532
+ status == 0
533
+ rescue OsVm::Error
534
+ false
535
+ end
536
+
537
+ def carrier_realpath(path)
538
+ _, out = carrier.succeeds("realpath #{Shellwords.escape(path)}")
539
+ store_path = extract_store_path(out)
540
+ return store_path unless store_path.nil?
541
+
542
+ out.lines.map(&:strip).reject(&:empty?).last.to_s
543
+ end
544
+
545
+ def carrier_file(path)
546
+ _, encoded = carrier.succeeds("base64 -w0 < #{Shellwords.escape(path)}")
547
+ Base64.decode64(encoded)
548
+ end
549
+
550
+ def carrier_json(path)
551
+ JSON.parse(carrier_file(path))
552
+ end
553
+
554
+ def carrier_profile_path(alias_name)
555
+ "/nix/var/nix/profiles/confctl-#{alias_name}"
556
+ end
557
+
558
+ def netboot_machine_path(fqdn)
559
+ "/var/lib/confctl/carrier/netboot/http/#{fqdn}/machine.json"
560
+ end
561
+
562
+ def netboot_current_path(fqdn)
563
+ "/var/lib/confctl/carrier/netboot/http/#{fqdn}/current"
564
+ end
565
+
566
+ def netboot_generation_path(fqdn, generation)
567
+ "/var/lib/confctl/carrier/netboot/http/#{fqdn}/#{generation}/generation.json"
568
+ end
569
+
570
+ def netboot_kernel_params_path(fqdn, generation)
571
+ "/var/lib/confctl/carrier/netboot/http/#{fqdn}/#{generation}/kernel-params"
572
+ end
573
+
574
+ def carrier_netboot_machine(fqdn)
575
+ carrier_json(netboot_machine_path(fqdn))
576
+ end
577
+
578
+ def build_generation!(host)
579
+ confctl!('build', '--yes', host)
580
+ confctl_generation_info(host)
581
+ end
582
+
583
+ def wait_for_carrier_netboot_services!
584
+ wait_for_block(name: 'carrier netboot services', timeout: 300) do
585
+ carrier_success?('systemctl is-active --quiet dnsmasq.service') &&
586
+ carrier_success?('systemctl is-active --quiet netboot-atftpd.service') &&
587
+ carrier_success?('systemctl is-active --quiet nginx.service')
588
+ end
589
+ end
590
+
591
+ def wait_for_nixos_netboot_artifacts!(generation:, previous_generations: [])
592
+ wait_for_block(name: 'nixos carried netboot artifacts', timeout: 300) do
593
+ next(false) unless carrier_success?("test -e #{Shellwords.escape(carrier_profile_path(NIXOS_MACHINE))}")
594
+ next(false) unless carrier_success?('test -e /var/lib/confctl/carrier/netboot/tftp/pxelinux.cfg/01-52-54-00-10-00-11')
595
+ next(false) unless carrier_success?("test -e #{Shellwords.escape("/var/lib/confctl/carrier/netboot/tftp/pxeserver/machines/#{NIXOS_FQDN}/auto.cfg")}")
596
+
597
+ begin
598
+ next(false) unless carrier_realpath(carrier_profile_path(NIXOS_MACHINE)) == generation['toplevel']
599
+
600
+ machines_json = carrier_json('/var/lib/confctl/carrier/netboot/http/machines.json')
601
+ next(false) unless machines_json.fetch('machines').any? { |m| m.fetch('name') == NIXOS_MACHINE }
602
+
603
+ machine_json = carrier_netboot_machine(NIXOS_FQDN)
604
+ current_generation = machine_json.fetch('generations').detect { |g| g.fetch('current') }
605
+ next(false) if current_generation.nil?
606
+ next(false) unless current_generation.fetch('store_path') == generation['toplevel']
607
+ next(false) unless carrier_success?("test -e #{Shellwords.escape(netboot_generation_path(NIXOS_FQDN, current_generation.fetch('generation')))}")
608
+ next(false) unless carrier_success?("test \"$(basename $(readlink -f #{Shellwords.escape(netboot_current_path(NIXOS_FQDN))}))\" = #{Shellwords.escape(current_generation.fetch('generation').to_s)}")
609
+
610
+ next(false) unless previous_generations.all? do |prev_generation|
611
+ prev = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == prev_generation.fetch('toplevel') }
612
+ !prev.nil? && carrier_success?("test -e #{Shellwords.escape(netboot_generation_path(NIXOS_FQDN, prev.fetch('generation')))}")
613
+ end
614
+
615
+ nixos_auto = carrier_file("/var/lib/confctl/carrier/netboot/tftp/pxeserver/machines/#{NIXOS_FQDN}/auto.cfg")
616
+ next(false) unless nixos_auto.include?("DEFAULT #{NIXOS_FQDN}")
617
+
618
+ nixos_kernel_params = carrier_file(netboot_kernel_params_path(NIXOS_FQDN, current_generation.fetch('generation')))
619
+ next(false) unless nixos_kernel_params.include?("init=#{current_generation.fetch('toplevel')}/init")
620
+ next(false) unless nixos_kernel_params.include?('httproot=http://192.168.100.1/')
621
+ rescue JSON::ParserError, OsVm::Error
622
+ next(false)
623
+ end
624
+
625
+ true
626
+ end
627
+ end
628
+
629
+ def wait_for_vpsadminos_netboot_artifacts!(generation:, previous_generations: [])
630
+ wait_for_block(name: 'vpsadminos carried netboot artifacts', timeout: 300) do
631
+ next(false) unless carrier_success?("test -e #{Shellwords.escape(carrier_profile_path(VPSADMINOS_MACHINE))}")
632
+ next(false) unless carrier_success?('test -e /var/lib/confctl/carrier/netboot/tftp/pxelinux.cfg/01-52-54-00-10-00-12')
633
+ next(false) unless carrier_success?("test -e #{Shellwords.escape("/var/lib/confctl/carrier/netboot/tftp/pxeserver/machines/#{VPSADMINOS_FQDN}/auto.cfg")}")
634
+
635
+ begin
636
+ next(false) unless carrier_realpath(carrier_profile_path(VPSADMINOS_MACHINE)) == generation['toplevel']
637
+
638
+ machines_json = carrier_json('/var/lib/confctl/carrier/netboot/http/machines.json')
639
+ next(false) unless machines_json.fetch('machines').any? { |m| m.fetch('name') == VPSADMINOS_MACHINE }
640
+
641
+ machine_json = carrier_netboot_machine(VPSADMINOS_FQDN)
642
+ current_generation = machine_json.fetch('generations').detect { |g| g.fetch('current') }
643
+ next(false) if current_generation.nil?
644
+ next(false) unless current_generation.fetch('store_path') == generation['toplevel']
645
+ next(false) unless carrier_success?("test -e #{Shellwords.escape(netboot_generation_path(VPSADMINOS_FQDN, current_generation.fetch('generation')))}")
646
+ next(false) unless carrier_success?("test \"$(basename $(readlink -f #{Shellwords.escape(netboot_current_path(VPSADMINOS_FQDN))}))\" = #{Shellwords.escape(current_generation.fetch('generation').to_s)}")
647
+
648
+ next(false) unless previous_generations.all? do |prev_generation|
649
+ prev = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == prev_generation.fetch('toplevel') }
650
+ !prev.nil? && carrier_success?("test -e #{Shellwords.escape(netboot_generation_path(VPSADMINOS_FQDN, prev.fetch('generation')))}")
651
+ end
652
+
653
+ vpsadminos_kernel_params = carrier_file(netboot_kernel_params_path(VPSADMINOS_FQDN, current_generation.fetch('generation')))
654
+ next(false) unless vpsadminos_kernel_params.include?('httproot=http://192.168.100.1/')
655
+ rescue JSON::ParserError, OsVm::Error
656
+ next(false)
657
+ end
658
+
659
+ true
660
+ end
661
+ end
662
+
663
+ def boot_pxe_client!(machine, timeout:)
664
+ machine.start(wait_for_boot: false) unless machine.running?
665
+ machine.wait_for_boot(timeout:)
666
+ end
667
+
668
+ def wait_for_netboot_http!(machine, timeout:)
669
+ machine.wait_until_succeeds("curl -sf http://#{PXE_ADDRESS}/machines.json >/dev/null", timeout:)
670
+ end
671
+
672
+ def boot_id(machine)
673
+ _, out = machine.succeeds('cat /proc/sys/kernel/random/boot_id')
674
+ out.strip
675
+ end
676
+
677
+ def wait_for_reboot!(machine, previous_boot_id:, timeout:)
678
+ wait_for_block(name: "#{machine.name} reboot after kexec", timeout:) do
679
+ begin
680
+ _, out = machine.execute('cat /proc/sys/kernel/random/boot_id', timeout: 10)
681
+ next(false) if out.strip == previous_boot_id
682
+
683
+ true
684
+ rescue OsVm::MachineShellClosed, OsVm::TimeoutError, OsVm::Error
685
+ next(false)
686
+ end
687
+ end
688
+
689
+ machine.wait_for_boot(timeout: 30)
690
+ end
691
+
692
+ def kexec_netboot!(machine, load_args: [], boot_timeout:)
693
+ previous_boot_id = boot_id(machine)
694
+ machine.succeeds(Shellwords.join(['kexec-netboot', *load_args]))
695
+
696
+ begin
697
+ machine.execute('kexec-netboot --exec', timeout: 120)
698
+ rescue OsVm::MachineShellClosed
699
+ # The old shell can disappear either during the exec call itself or
700
+ # slightly later as the new kernel takes over.
701
+ end
702
+
703
+ wait_for_reboot!(machine, previous_boot_id:, timeout: boot_timeout)
704
+ end
705
+
706
+ def assert_marker!(machine, marker)
707
+ _, out = machine.succeeds('cat /etc/confctl-marker')
708
+ expect(out).to eq("#{marker}\n")
709
+ end
710
+
711
+ before(:suite) do
712
+ @carrier_port = ConfctlHostfwdPorts.reserve('net1')
713
+
714
+ carrier.start
715
+
716
+ @state_dir = @opts[:state_dir]
717
+ @conf_dir = File.join(@state_dir, 'conf')
718
+ @home_dir = File.join(@state_dir, 'home')
719
+ confctl_setup!(
720
+ bin: "${confctlBin}",
721
+ src: "${confctlSource}",
722
+ conf_dir: @conf_dir,
723
+ home_dir: @home_dir
724
+ )
725
+
726
+ @pubkey = setup_ssh_home!(@home_dir)
727
+ install_pubkey!(carrier, @pubkey)
728
+
729
+ prepare_fixture!(
730
+ conf_dir: @conf_dir,
731
+ carrier_port: @carrier_port,
732
+ pubkey: @pubkey
733
+ )
734
+ end
735
+
736
+ describe 'confctl carrier netboot behavior', order: :defined do
737
+ before(:context) do
738
+ out = wait_for_confctl_connectivity!(expected_successes: 1, timeout: 180)
739
+ expect(out).to include('1 successful')
740
+ end
741
+
742
+ it 'deploys the carrier machine for netboot' do
743
+ carrier_generation = build_generation!(CARRIER_MACHINE)
744
+ confctl!('deploy', '--yes', CARRIER_MACHINE)
745
+ wait_for_carrier_netboot_services!
746
+ expect(carrier_realpath('/nix/var/nix/profiles/system')).to eq(carrier_generation['toplevel'])
747
+ end
748
+
749
+ it 'deploys the carried NixOS machine to the carrier' do
750
+ @carried_nixos_gen_a = build_generation!("carrier##{NIXOS_MACHINE}")
751
+ confctl!('deploy', '--yes', "carrier##{NIXOS_MACHINE}")
752
+ wait_for_carrier_netboot_services!
753
+ wait_for_nixos_netboot_artifacts!(generation: @carried_nixos_gen_a)
754
+ end
755
+
756
+ it 'deploys the carried vpsAdminOS machine to the carrier' do
757
+ @carried_vpsadminos_gen_a = build_generation!("carrier##{VPSADMINOS_MACHINE}")
758
+ confctl!('deploy', '--yes', "carrier##{VPSADMINOS_MACHINE}")
759
+ wait_for_carrier_netboot_services!
760
+ wait_for_vpsadminos_netboot_artifacts!(generation: @carried_vpsadminos_gen_a)
761
+ end
762
+
763
+ it 'boots the carried NixOS machine over PXE from the carrier' do
764
+ boot_pxe_client!(nixos, timeout: NIXOS_BOOT_TIMEOUT)
765
+ assert_marker!(nixos, 'A')
766
+ wait_for_netboot_http!(nixos, timeout: NETBOOT_HTTP_TIMEOUT)
767
+
768
+ _, cmdline = nixos.succeeds('cat /proc/cmdline')
769
+ expect(cmdline).to include('httproot=http://192.168.100.1/')
770
+ end
771
+
772
+ it 'boots the carried vpsAdminOS machine over PXE from the carrier' do
773
+ boot_pxe_client!(vpsadminos, timeout: VPSADMINOS_BOOT_TIMEOUT)
774
+ assert_marker!(vpsadminos, 'A')
775
+ wait_for_netboot_http!(vpsadminos, timeout: NETBOOT_HTTP_TIMEOUT)
776
+
777
+ _, cmdline = vpsadminos.succeeds('cat /proc/cmdline')
778
+ expect(cmdline).to include('httproot=http://192.168.100.1/')
779
+ end
780
+
781
+ it 'kexec-netboots the carried NixOS machine to the latest and selected generation' do
782
+ expect(@carried_nixos_gen_a).not_to be_nil
783
+
784
+ update_nixos_generation!(marker: 'B')
785
+ @carried_nixos_gen_b = build_generation!("carrier##{NIXOS_MACHINE}")
786
+ confctl!('deploy', '--yes', "carrier##{NIXOS_MACHINE}")
787
+ wait_for_carrier_netboot_services!
788
+ wait_for_nixos_netboot_artifacts!(
789
+ generation: @carried_nixos_gen_b,
790
+ previous_generations: [@carried_nixos_gen_a]
791
+ )
792
+
793
+ machine_json = carrier_netboot_machine(NIXOS_FQDN)
794
+ gen_a = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == @carried_nixos_gen_a['toplevel'] }
795
+ gen_b = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == @carried_nixos_gen_b['toplevel'] }
796
+
797
+ expect(gen_a).not_to be_nil
798
+ expect(gen_b).not_to be_nil
799
+ expect(gen_b.fetch('current')).to be(true)
800
+
801
+ kexec_netboot!(nixos, boot_timeout: NIXOS_BOOT_TIMEOUT)
802
+ assert_marker!(nixos, 'B')
803
+ wait_for_netboot_http!(nixos, timeout: NETBOOT_HTTP_TIMEOUT)
804
+
805
+ kexec_netboot!(
806
+ nixos,
807
+ load_args: ['--generation', gen_a.fetch('generation').to_s],
808
+ boot_timeout: NIXOS_BOOT_TIMEOUT
809
+ )
810
+ assert_marker!(nixos, 'A')
811
+ wait_for_netboot_http!(nixos, timeout: NETBOOT_HTTP_TIMEOUT)
812
+ end
813
+
814
+ it 'kexec-netboots the carried vpsAdminOS machine to the latest and selected generation' do
815
+ expect(@carried_vpsadminos_gen_a).not_to be_nil
816
+
817
+ update_vpsadminos_generation!(marker: 'B')
818
+ @carried_vpsadminos_gen_b = build_generation!("carrier##{VPSADMINOS_MACHINE}")
819
+ confctl!('deploy', '--yes', "carrier##{VPSADMINOS_MACHINE}")
820
+ wait_for_carrier_netboot_services!
821
+ wait_for_vpsadminos_netboot_artifacts!(
822
+ generation: @carried_vpsadminos_gen_b,
823
+ previous_generations: [@carried_vpsadminos_gen_a]
824
+ )
825
+
826
+ machine_json = carrier_netboot_machine(VPSADMINOS_FQDN)
827
+ gen_a = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == @carried_vpsadminos_gen_a['toplevel'] }
828
+ gen_b = machine_json.fetch('generations').detect { |g| g.fetch('store_path') == @carried_vpsadminos_gen_b['toplevel'] }
829
+
830
+ expect(gen_a).not_to be_nil
831
+ expect(gen_b).not_to be_nil
832
+ expect(gen_b.fetch('current')).to be(true)
833
+
834
+ kexec_netboot!(vpsadminos, boot_timeout: VPSADMINOS_BOOT_TIMEOUT)
835
+ assert_marker!(vpsadminos, 'B')
836
+ wait_for_netboot_http!(vpsadminos, timeout: NETBOOT_HTTP_TIMEOUT)
837
+
838
+ kexec_netboot!(
839
+ vpsadminos,
840
+ load_args: ['--generation', gen_a.fetch('generation').to_s],
841
+ boot_timeout: VPSADMINOS_BOOT_TIMEOUT
842
+ )
843
+ assert_marker!(vpsadminos, 'A')
844
+ wait_for_netboot_http!(vpsadminos, timeout: NETBOOT_HTTP_TIMEOUT)
845
+ end
846
+ end
847
+ '';
848
+ }
849
+ ) args