confctl 1.0.0 → 2.1.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.
- checksums.yaml +4 -4
- data/.editorconfig +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +30 -1
- data/README.md +4 -9
- data/confctl.gemspec +14 -14
- data/docs/carrier.md +150 -0
- data/lib/confctl/cli/app.rb +19 -0
- data/lib/confctl/cli/cluster.rb +214 -49
- data/lib/confctl/cli/configuration.rb +7 -2
- data/lib/confctl/cli/gen_data.rb +19 -1
- data/lib/confctl/cli/generation.rb +47 -16
- data/lib/confctl/generation/build.rb +42 -1
- data/lib/confctl/generation/build_list.rb +10 -0
- data/lib/confctl/generation/host.rb +9 -5
- data/lib/confctl/generation/host_list.rb +22 -7
- data/lib/confctl/generation/unified.rb +5 -0
- data/lib/confctl/generation/unified_list.rb +10 -0
- data/lib/confctl/git_repo_mirror.rb +2 -2
- data/lib/confctl/machine.rb +105 -11
- data/lib/confctl/machine_control.rb +10 -2
- data/lib/confctl/machine_list.rb +18 -1
- data/lib/confctl/machine_status.rb +51 -4
- data/lib/confctl/nix.rb +90 -22
- data/lib/confctl/nix_copy.rb +5 -5
- data/lib/confctl/null_logger.rb +7 -0
- data/lib/confctl/swpins/specs/git.rb +1 -1
- data/lib/confctl/swpins/specs/git_rev.rb +1 -1
- data/lib/confctl/system_command.rb +3 -2
- data/lib/confctl/version.rb +1 -1
- data/libexec/auto-rollback.rb +106 -0
- data/man/man8/confctl-options.nix.8 +165 -1
- data/man/man8/confctl-options.nix.8.md +165 -1
- data/man/man8/confctl.8 +109 -73
- data/man/man8/confctl.8.md +86 -55
- data/nix/evaluator.nix +26 -7
- data/nix/lib/default.nix +64 -17
- data/nix/lib/machine/default.nix +14 -11
- data/nix/lib/machine/info.nix +3 -3
- data/nix/modules/cluster/default.nix +162 -3
- data/nix/modules/confctl/carrier/base.nix +35 -0
- data/nix/modules/confctl/carrier/carrier-env.rb +81 -0
- data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +962 -0
- data/nix/modules/confctl/carrier/netboot/nixos.nix +185 -0
- data/nix/modules/confctl/kexec-netboot/default.nix +36 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +62 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +455 -0
- data/nix/modules/system-list.nix +10 -0
- metadata +17 -7
- data/.ruby-version +0 -1
@@ -0,0 +1,962 @@
|
|
1
|
+
#!@ruby@/bin/ruby
|
2
|
+
require 'erb'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'json'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
class Config
|
8
|
+
class Memtest
|
9
|
+
# @return [Boolean]
|
10
|
+
attr_reader :enable
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
attr_reader :package
|
14
|
+
|
15
|
+
# @return [String]
|
16
|
+
attr_reader :params
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :params_line
|
20
|
+
|
21
|
+
def initialize(cfg)
|
22
|
+
@enable = !cfg.nil?
|
23
|
+
return unless @enable
|
24
|
+
|
25
|
+
@package = cfg.fetch('package')
|
26
|
+
@params = cfg.fetch('params')
|
27
|
+
@params_line = @params.join(' ')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class IsoImage
|
32
|
+
# @return [String]
|
33
|
+
attr_reader :file
|
34
|
+
|
35
|
+
# @return [String]
|
36
|
+
attr_reader :label
|
37
|
+
|
38
|
+
# @return [String]
|
39
|
+
attr_reader :name
|
40
|
+
|
41
|
+
# @param cfg [Hash]
|
42
|
+
# @param index [Integer]
|
43
|
+
def initialize(cfg, index)
|
44
|
+
@file = cfg.fetch('file')
|
45
|
+
@label = cfg.fetch('label')
|
46
|
+
|
47
|
+
basename = File.basename(@file)
|
48
|
+
|
49
|
+
@label = basename if @label.empty?
|
50
|
+
@name = format('%02d-%s', index, basename)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param file [String]
|
55
|
+
def self.parse(file)
|
56
|
+
new(JSON.parse(File.read(file)))
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [String]
|
60
|
+
attr_reader :ruby
|
61
|
+
|
62
|
+
# @return [String]
|
63
|
+
attr_reader :coreutils
|
64
|
+
|
65
|
+
# @return [String]
|
66
|
+
attr_reader :syslinux
|
67
|
+
|
68
|
+
# @return [String]
|
69
|
+
attr_reader :tftp_root
|
70
|
+
|
71
|
+
# @return [String]
|
72
|
+
attr_reader :http_root
|
73
|
+
|
74
|
+
# @return [String]
|
75
|
+
attr_reader :host_name
|
76
|
+
|
77
|
+
# @return [String]
|
78
|
+
attr_reader :http_url
|
79
|
+
|
80
|
+
# @return [Memtest]
|
81
|
+
attr_reader :memtest
|
82
|
+
|
83
|
+
# @return [Array<IsoImage>]
|
84
|
+
attr_reader :iso_images
|
85
|
+
|
86
|
+
# @param cfg [Hash]
|
87
|
+
def initialize(cfg)
|
88
|
+
@ruby = cfg.fetch('ruby')
|
89
|
+
@coreutils = cfg.fetch('coreutils')
|
90
|
+
@syslinux = cfg.fetch('syslinux')
|
91
|
+
@tftp_root = cfg.fetch('tftpRoot')
|
92
|
+
@http_root = cfg.fetch('httpRoot')
|
93
|
+
@host_name = cfg.fetch('hostName')
|
94
|
+
@http_url = cfg.fetch('httpUrl')
|
95
|
+
@memtest = Memtest.new(cfg.fetch('memtest'))
|
96
|
+
@iso_images = cfg.fetch('isoImages').each_with_index.map { |v, i| IsoImage.new(v, i) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class NetbootBuilder
|
101
|
+
CONFIG_FILE = '@jsonConfig@'.freeze
|
102
|
+
|
103
|
+
LINK_DIR = '/nix/var/nix/profiles'.freeze
|
104
|
+
|
105
|
+
CARRIED_PREFIX = 'confctl'.freeze
|
106
|
+
|
107
|
+
LOCK_FILE = '/run/confctl/build-netboot-server.lock'.freeze
|
108
|
+
|
109
|
+
def self.run
|
110
|
+
builder = new(Config.parse(CONFIG_FILE))
|
111
|
+
builder.run
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(config)
|
115
|
+
@config = config
|
116
|
+
end
|
117
|
+
|
118
|
+
def run
|
119
|
+
lock { safe_run }
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
|
124
|
+
def safe_run
|
125
|
+
machines = load_machines
|
126
|
+
|
127
|
+
machines.each do |m|
|
128
|
+
puts m.fqdn
|
129
|
+
|
130
|
+
m.generations.each do |g|
|
131
|
+
puts " - #{g.time_s} - #{g.version}#{g.current ? ' (current)' : ''}"
|
132
|
+
end
|
133
|
+
|
134
|
+
puts
|
135
|
+
end
|
136
|
+
|
137
|
+
random = SecureRandom.hex(3)
|
138
|
+
|
139
|
+
builders = [
|
140
|
+
TftpBuilder.new(@config, random, machines, @config.tftp_root),
|
141
|
+
HttpBuilder.new(@config, random, machines, @config.http_root)
|
142
|
+
]
|
143
|
+
|
144
|
+
builders.each(&:run)
|
145
|
+
builders.each(&:install)
|
146
|
+
builders.each(&:cleanup)
|
147
|
+
end
|
148
|
+
|
149
|
+
def lock
|
150
|
+
FileUtils.mkdir_p(File.dirname(LOCK_FILE))
|
151
|
+
|
152
|
+
File.open(LOCK_FILE, 'w') do |f|
|
153
|
+
f.flock(File::LOCK_EX)
|
154
|
+
yield
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def load_machines
|
159
|
+
machines = {}
|
160
|
+
|
161
|
+
Dir.entries(LINK_DIR).each do |v|
|
162
|
+
next if /\A#{Regexp.escape(CARRIED_PREFIX)}-(.+)-(\d+)-link\z/ !~ v
|
163
|
+
|
164
|
+
name = ::Regexp.last_match(1)
|
165
|
+
generation = ::Regexp.last_match(2).to_i
|
166
|
+
|
167
|
+
link_path = File.join(LINK_DIR, v)
|
168
|
+
|
169
|
+
machines[name] ||= Machine.new(@config, name)
|
170
|
+
machines[name].add_generation(link_path, generation)
|
171
|
+
end
|
172
|
+
|
173
|
+
machines.each_value do |m|
|
174
|
+
current_path = File.realpath(File.join(LINK_DIR, "#{CARRIED_PREFIX}-#{m.name}"))
|
175
|
+
|
176
|
+
m.generations.each do |g|
|
177
|
+
next if g.store_path != current_path
|
178
|
+
|
179
|
+
g.current = true
|
180
|
+
break
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
machines.each_value(&:resolve)
|
185
|
+
machines.values
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class RootBuilder
|
190
|
+
# @return [Array<Machine>]
|
191
|
+
attr_reader :machines
|
192
|
+
|
193
|
+
# @return [String]
|
194
|
+
attr_reader :root
|
195
|
+
|
196
|
+
# @param config [Config]
|
197
|
+
# @param random [String]
|
198
|
+
# @param machines [Array<Machine>]
|
199
|
+
# @param root [String]
|
200
|
+
def initialize(config, random, machines, root)
|
201
|
+
@config = config
|
202
|
+
@machines = machines
|
203
|
+
@target_root = root
|
204
|
+
@root = @new_root = "#{root}.#{random}"
|
205
|
+
|
206
|
+
begin
|
207
|
+
@current_root = File.readlink(root)
|
208
|
+
|
209
|
+
unless @current_root.start_with?('/')
|
210
|
+
@current_root = File.join(File.dirname(root), @current_root)
|
211
|
+
end
|
212
|
+
rescue Errno::ENOENT
|
213
|
+
@current_root = nil
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def run
|
218
|
+
mkdir_p('')
|
219
|
+
build
|
220
|
+
end
|
221
|
+
|
222
|
+
def build
|
223
|
+
raise NotImplementedError
|
224
|
+
end
|
225
|
+
|
226
|
+
def install
|
227
|
+
# ln -sfn is called instead of using ruby methods, because ln
|
228
|
+
# can replace the symlink atomically.
|
229
|
+
return if Kernel.system(File.join(@config.coreutils, 'bin/ln'), '-sfn', File.basename(@new_root), @target_root)
|
230
|
+
|
231
|
+
raise "Failed to install new root to #{@target_root}"
|
232
|
+
end
|
233
|
+
|
234
|
+
def cleanup
|
235
|
+
FileUtils.rm_r(@current_root) if @current_root
|
236
|
+
end
|
237
|
+
|
238
|
+
protected
|
239
|
+
|
240
|
+
# @param src [String] link target
|
241
|
+
# @param dst [String] link path relative to root
|
242
|
+
def ln_s(src, dst)
|
243
|
+
FileUtils.ln_s(src, File.join(root, dst))
|
244
|
+
end
|
245
|
+
|
246
|
+
# @param src [String] link target
|
247
|
+
# @param dst [String] link path relative to root
|
248
|
+
def cp(src, dst)
|
249
|
+
FileUtils.cp(src, File.join(root, dst))
|
250
|
+
end
|
251
|
+
|
252
|
+
# Create directories within root
|
253
|
+
def mkdir_p(*paths)
|
254
|
+
paths.each do |v|
|
255
|
+
FileUtils.mkdir_p(File.join(root, v))
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Create a file within root
|
260
|
+
def write_to(path, content)
|
261
|
+
mkdir_p(File.dirname(path))
|
262
|
+
File.write(File.join(root, path), content)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class TftpBuilder < RootBuilder
|
267
|
+
PROGRAMS = %w[pxelinux.0 ldlinux.c32 libcom32.c32 libutil.c32 memdisk menu.c32 reboot.c32].freeze
|
268
|
+
|
269
|
+
SPIN_LABELS = {
|
270
|
+
'nixos' => 'NixOS',
|
271
|
+
'vpsadminos' => 'vpsAdminOS'
|
272
|
+
}.freeze
|
273
|
+
|
274
|
+
def build
|
275
|
+
install_syslinux
|
276
|
+
|
277
|
+
mkdir_p('pxelinux.cfg', 'pxeserver')
|
278
|
+
|
279
|
+
install_boot_files
|
280
|
+
|
281
|
+
@spins =
|
282
|
+
machines.each_with_object([]) do |m, acc|
|
283
|
+
acc << m.spin unless acc.include?(m.spin)
|
284
|
+
end.to_h do |spin|
|
285
|
+
[spin, SPIN_LABELS.fetch(spin, spin)]
|
286
|
+
end
|
287
|
+
|
288
|
+
render_default_config
|
289
|
+
render_spin_configs
|
290
|
+
render_machine_configs
|
291
|
+
render_iso_image_configs
|
292
|
+
end
|
293
|
+
|
294
|
+
protected
|
295
|
+
|
296
|
+
def install_syslinux
|
297
|
+
PROGRAMS.each do |prog|
|
298
|
+
cp(File.join(@config.syslinux, 'share/syslinux', prog), prog)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def install_boot_files
|
303
|
+
# rubocop:disable Style/GuardClause
|
304
|
+
|
305
|
+
machines.each do |m|
|
306
|
+
m.generations.each do |g|
|
307
|
+
path = File.join('boot', m.fqdn, g.generation.to_s)
|
308
|
+
mkdir_p(path)
|
309
|
+
|
310
|
+
g.boot_files.each_value do |boot_file|
|
311
|
+
next unless %w[bzImage initrd].include?(boot_file.name)
|
312
|
+
|
313
|
+
ln_s(boot_file.path, File.join(path, boot_file.name))
|
314
|
+
end
|
315
|
+
|
316
|
+
ln_s(g.generation.to_s, File.join('boot', m.fqdn, 'current')) if g.current
|
317
|
+
end
|
318
|
+
|
319
|
+
m.current.macs.each do |mac|
|
320
|
+
# See https://wiki.syslinux.org/wiki/index.php?title=PXELINUX#Configuration
|
321
|
+
ln_s("../pxeserver/machines/#{m.fqdn}/auto.cfg", "pxelinux.cfg/01-#{mac.gsub(':', '-')}")
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
if @config.memtest.enable
|
326
|
+
mkdir_p('boot/memtest86')
|
327
|
+
ln_s(File.join(@config.memtest.package, 'memtest.bin'), 'boot/memtest86/memtest.bin')
|
328
|
+
end
|
329
|
+
|
330
|
+
if @config.iso_images.any?
|
331
|
+
mkdir_p('boot/iso-images')
|
332
|
+
|
333
|
+
@config.iso_images.each do |img|
|
334
|
+
ln_s(img.file, File.join('boot/iso-images', img.name))
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# rubocop:enable Style/GuardClause
|
339
|
+
end
|
340
|
+
|
341
|
+
def render_default_config
|
342
|
+
tpl = <<~ERB
|
343
|
+
DEFAULT menu.c32
|
344
|
+
PROMPT 0
|
345
|
+
TIMEOUT 0
|
346
|
+
MENU TITLE <%= hostname %>
|
347
|
+
|
348
|
+
<% spins.each do |spin, label| -%>
|
349
|
+
LABEL <%= spin %>
|
350
|
+
MENU LABEL <%= label %> >
|
351
|
+
KERNEL menu.c32
|
352
|
+
APPEND pxeserver/<%= spin %>.cfg
|
353
|
+
|
354
|
+
<% end -%>
|
355
|
+
<% if iso_images.any? -%>
|
356
|
+
LABEL isoimages
|
357
|
+
MENU LABEL ISO images >
|
358
|
+
KERNEL menu.c32
|
359
|
+
APPEND pxeserver/iso-images.cfg
|
360
|
+
|
361
|
+
<% end -%>
|
362
|
+
<% if enable_memtest -%>
|
363
|
+
LABEL memtest
|
364
|
+
MENU LABEL Memtest86
|
365
|
+
LINUX boot/memtest86/memtest.bin
|
366
|
+
APPEND <%= memtest_params %>
|
367
|
+
|
368
|
+
<% end -%>
|
369
|
+
LABEL local_boot
|
370
|
+
MENU LABEL Local Boot
|
371
|
+
LOCALBOOT 0
|
372
|
+
|
373
|
+
LABEL warm_reboot
|
374
|
+
MENU LABEL Warm Reboot
|
375
|
+
KERNEL reboot.c32
|
376
|
+
APPEND --warm
|
377
|
+
|
378
|
+
LABEL cold_reboot
|
379
|
+
MENU LABEL Cold Reboot
|
380
|
+
KERNEL reboot.c32
|
381
|
+
ERB
|
382
|
+
|
383
|
+
render_to(
|
384
|
+
tpl,
|
385
|
+
{
|
386
|
+
hostname: @config.host_name,
|
387
|
+
spins: @spins,
|
388
|
+
enable_memtest: @config.memtest.enable,
|
389
|
+
memtest_params: @config.memtest.params_line,
|
390
|
+
iso_images: @config.iso_images
|
391
|
+
},
|
392
|
+
'pxelinux.cfg/default'
|
393
|
+
)
|
394
|
+
end
|
395
|
+
|
396
|
+
def render_spin_configs
|
397
|
+
tpl = <<~ERB
|
398
|
+
MENU TITLE <%= label %>
|
399
|
+
|
400
|
+
<% spin_machines.each do |m| -%>
|
401
|
+
LABEL <%= m.fqdn %>
|
402
|
+
MENU LABEL <%= m.label %> >
|
403
|
+
KERNEL menu.c32
|
404
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/menu.cfg
|
405
|
+
<% end -%>
|
406
|
+
|
407
|
+
LABEL mainmenu
|
408
|
+
MENU LABEL < Back to Main Menu
|
409
|
+
KERNEL menu.c32
|
410
|
+
APPEND pxelinux.cfg/default
|
411
|
+
ERB
|
412
|
+
|
413
|
+
@spins.each do |spin, label|
|
414
|
+
spin_machines = machines.select { |m| m.spin == spin }.sort { |a, b| a.name <=> b.name }
|
415
|
+
|
416
|
+
render_to(tpl, { spin:, label:, spin_machines: }, "pxeserver/#{spin}.cfg")
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def render_machine_configs
|
421
|
+
mkdir_p('pxeserver/machines')
|
422
|
+
|
423
|
+
machines.each do |m|
|
424
|
+
mkdir_p("pxeserver/machines/#{m.fqdn}")
|
425
|
+
send(:"render_machine_#{m.spin}", m)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
def render_machine_nixos(machine)
|
430
|
+
tpl = <<~ERB
|
431
|
+
MENU TITLE <%= m.label %>
|
432
|
+
|
433
|
+
LABEL <%= m.fqdn %>
|
434
|
+
MENU LABEL <%= m.label %>
|
435
|
+
LINUX boot/<%= m.fqdn %>/<%= m.current.generation %>/bzImage
|
436
|
+
INITRD boot/<%= m.fqdn %>/<%= m.current.generation %>/initrd
|
437
|
+
APPEND init=<%= m.current.toplevel %>/init loglevel=7
|
438
|
+
|
439
|
+
<% m.generations[1..].each do |g| -%>
|
440
|
+
LABEL <%= m.fqdn %>-<%= g.generation %>
|
441
|
+
MENU LABEL Gen <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
|
442
|
+
LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
|
443
|
+
INITRD boot/<%= m.fqdn %>/<%= g.generation %>/initrd
|
444
|
+
APPEND init=<%= g.toplevel %>/init loglevel=7
|
445
|
+
|
446
|
+
<% end -%>
|
447
|
+
<% if enable_memtest -%>
|
448
|
+
LABEL memtest
|
449
|
+
MENU LABEL Memtest86
|
450
|
+
LINUX boot/memtest86/memtest.bin
|
451
|
+
APPEND <%= memtest_params %>
|
452
|
+
|
453
|
+
<% end -%>
|
454
|
+
LABEL mainmenu
|
455
|
+
MENU LABEL < Back to Main Menu
|
456
|
+
KERNEL menu.c32
|
457
|
+
APPEND pxelinux.cfg/default
|
458
|
+
ERB
|
459
|
+
|
460
|
+
render_to(
|
461
|
+
tpl,
|
462
|
+
{
|
463
|
+
m: machine,
|
464
|
+
enable_memtest: @config.memtest.enable,
|
465
|
+
memtest_params: @config.memtest.params_line
|
466
|
+
},
|
467
|
+
"pxeserver/machines/#{machine.fqdn}/menu.cfg"
|
468
|
+
)
|
469
|
+
end
|
470
|
+
|
471
|
+
def render_machine_vpsadminos(machine)
|
472
|
+
render_machine_vpsadminos_config(
|
473
|
+
machine,
|
474
|
+
generation: machine.current,
|
475
|
+
file: 'menu',
|
476
|
+
root: true
|
477
|
+
)
|
478
|
+
|
479
|
+
render_machine_vpsadminos_config(
|
480
|
+
machine,
|
481
|
+
generation: machine.current,
|
482
|
+
file: 'auto',
|
483
|
+
timeout: 5,
|
484
|
+
root: true
|
485
|
+
)
|
486
|
+
|
487
|
+
render_generations(machine)
|
488
|
+
|
489
|
+
machine.generations.each do |g|
|
490
|
+
render_machine_vpsadminos_config(
|
491
|
+
machine,
|
492
|
+
generation: g,
|
493
|
+
file: "generation-#{g.generation}",
|
494
|
+
root: false
|
495
|
+
)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# @param machine [Machine]
|
500
|
+
# @param generation [Generation]
|
501
|
+
# @param file [String] config base name
|
502
|
+
# @param root [Boolean] true if this is machine menu page, not generation menu
|
503
|
+
# @param timeout [Integer, nil] timeout in seconds until the default action is taken
|
504
|
+
def render_machine_vpsadminos_config(machine, generation:, file:, root:, timeout: nil)
|
505
|
+
tpl = <<~ERB
|
506
|
+
<% if timeout -%>
|
507
|
+
DEFAULT menu.c32
|
508
|
+
TIMEOUT 50
|
509
|
+
<% end -%>
|
510
|
+
MENU TITLE <%= m.short_label %> (<%= g.generation %> - <%= g.current ? 'current' : g.time_s %> - <%= g.shortrev %> - <%= g.kernel_version %>)
|
511
|
+
|
512
|
+
<% g.variants.each do |variant| -%>
|
513
|
+
LABEL <%= variant.name %>
|
514
|
+
MENU LABEL <%= variant.label %>
|
515
|
+
LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
|
516
|
+
INITRD boot/<%= m.fqdn %>/<%= g.generation %>/initrd
|
517
|
+
APPEND <%= g.kernel_params.join(' ') %> <%= variant.kernel_params.join(' ') %>
|
518
|
+
|
519
|
+
<% end -%>
|
520
|
+
LABEL <%= m.fqdn %>-generations
|
521
|
+
MENU LABEL <%= root ? 'Generations >' : '< Back to Generations' %>
|
522
|
+
KERNEL menu.c32
|
523
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/generations.cfg
|
524
|
+
|
525
|
+
<% if enable_memtest -%>
|
526
|
+
LABEL memtest
|
527
|
+
MENU LABEL Memtest86
|
528
|
+
LINUX boot/memtest86/memtest.bin
|
529
|
+
APPEND <%= memtest_params %>
|
530
|
+
|
531
|
+
<% end -%>
|
532
|
+
LABEL mainmenu
|
533
|
+
MENU LABEL < Back to Main Menu
|
534
|
+
KERNEL menu.c32
|
535
|
+
APPEND pxelinux.cfg/default
|
536
|
+
ERB
|
537
|
+
|
538
|
+
render_to(
|
539
|
+
tpl,
|
540
|
+
{
|
541
|
+
m: machine,
|
542
|
+
g: generation,
|
543
|
+
http_url: @config.http_url,
|
544
|
+
root:,
|
545
|
+
timeout:,
|
546
|
+
enable_memtest: @config.memtest.enable,
|
547
|
+
memtest_params: @config.memtest.params_line
|
548
|
+
},
|
549
|
+
"pxeserver/machines/#{machine.fqdn}/#{file}.cfg"
|
550
|
+
)
|
551
|
+
end
|
552
|
+
|
553
|
+
def render_generations(machine)
|
554
|
+
tpl = <<~ERB
|
555
|
+
MENU TITLE <%= m.label %> - generations
|
556
|
+
|
557
|
+
<% m.generations.each do |g| -%>
|
558
|
+
LABEL generations
|
559
|
+
MENU LABEL Gen <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %> - <%= g.kernel_version %>
|
560
|
+
KERNEL menu.c32
|
561
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/generation-<%= g.generation %>.cfg
|
562
|
+
|
563
|
+
<% end -%>
|
564
|
+
|
565
|
+
LABEL machine
|
566
|
+
MENU LABEL < Back to <%= m.label %>
|
567
|
+
KERNEL menu.c32
|
568
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/menu.cfg
|
569
|
+
|
570
|
+
LABEL mainmenu
|
571
|
+
MENU LABEL < Back to Main Menu
|
572
|
+
KERNEL menu.c32
|
573
|
+
APPEND pxelinux.cfg/default
|
574
|
+
ERB
|
575
|
+
|
576
|
+
render_to(tpl, { m: machine }, "pxeserver/machines/#{machine.fqdn}/generations.cfg")
|
577
|
+
end
|
578
|
+
|
579
|
+
def render_iso_image_configs
|
580
|
+
return if @config.iso_images.empty?
|
581
|
+
|
582
|
+
tpl = <<~ERB
|
583
|
+
MENU TITLE ISO images
|
584
|
+
|
585
|
+
<% iso_images.each_with_index do |img, i| -%>
|
586
|
+
LABEL image<%= i %>
|
587
|
+
MENU LABEL <%= img.label %>
|
588
|
+
KERNEL memdisk
|
589
|
+
APPEND iso initrd=boot/iso-images/<%= img.name %> raw
|
590
|
+
<% end -%>
|
591
|
+
|
592
|
+
LABEL mainmenu
|
593
|
+
MENU LABEL < Back to Main Menu
|
594
|
+
KERNEL menu.c32
|
595
|
+
APPEND pxelinux.cfg/default
|
596
|
+
ERB
|
597
|
+
|
598
|
+
render_to(tpl, { iso_images: @config.iso_images }, 'pxeserver/iso-images.cfg')
|
599
|
+
end
|
600
|
+
|
601
|
+
# @param template [String]
|
602
|
+
# @param vars [Hash]
|
603
|
+
def render(template, vars)
|
604
|
+
erb = ERB.new(template, trim_mode: '-')
|
605
|
+
erb.result_with_hash(vars)
|
606
|
+
end
|
607
|
+
|
608
|
+
# @param template [String]
|
609
|
+
# @param vars [Hash]
|
610
|
+
# @param path [String]
|
611
|
+
def render_to(template, vars, path)
|
612
|
+
File.write(File.join(root, path), render(template, vars))
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
class HttpBuilder < RootBuilder
|
617
|
+
def build
|
618
|
+
machines.each do |m|
|
619
|
+
m.generations.each do |g|
|
620
|
+
gen_path = File.join(m.fqdn, g.generation.to_s)
|
621
|
+
mkdir_p(gen_path)
|
622
|
+
|
623
|
+
g.boot_files.each_value do |boot_file|
|
624
|
+
ln_s(boot_file.path, File.join(gen_path, boot_file.name))
|
625
|
+
end
|
626
|
+
|
627
|
+
write_to(File.join(gen_path, 'generation.json'), JSON.pretty_generate(g))
|
628
|
+
write_to(File.join(gen_path, 'kernel-params'), g.kernel_params.join(' '))
|
629
|
+
|
630
|
+
ln_s(g.generation.to_s, File.join(m.fqdn, 'current')) if g.current
|
631
|
+
end
|
632
|
+
|
633
|
+
write_to(File.join(m.fqdn, 'machine.json'), JSON.pretty_generate(m))
|
634
|
+
end
|
635
|
+
|
636
|
+
write_to('machines.json', JSON.pretty_generate({ machines: }))
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
class Machine
|
641
|
+
# @return [String]
|
642
|
+
attr_reader :name
|
643
|
+
|
644
|
+
# @return [String]
|
645
|
+
attr_reader :spin
|
646
|
+
|
647
|
+
# @return [String]
|
648
|
+
attr_reader :fqdn
|
649
|
+
|
650
|
+
# @return [String]
|
651
|
+
attr_reader :label
|
652
|
+
|
653
|
+
# @return [String]
|
654
|
+
attr_reader :short_label
|
655
|
+
|
656
|
+
# @return [String]
|
657
|
+
attr_reader :url
|
658
|
+
|
659
|
+
# @return [Array<Generation>]
|
660
|
+
attr_reader :generations
|
661
|
+
|
662
|
+
# @return [Generation]
|
663
|
+
attr_reader :current
|
664
|
+
|
665
|
+
# @param config [Config]
|
666
|
+
# @param name [String] machine name
|
667
|
+
def initialize(config, name)
|
668
|
+
@config = config
|
669
|
+
@name = name
|
670
|
+
@spin = 'nixos'
|
671
|
+
@fqdn = name
|
672
|
+
@label = name
|
673
|
+
@short_label = name[0..14]
|
674
|
+
@toplevel = nil
|
675
|
+
@macs = []
|
676
|
+
@generations = []
|
677
|
+
@current = nil
|
678
|
+
end
|
679
|
+
|
680
|
+
# @param link_path [String] Nix store path
|
681
|
+
# @param generation [Integer]
|
682
|
+
def add_generation(link_path, generation)
|
683
|
+
@generations << Generation.new(self, link_path, generation)
|
684
|
+
nil
|
685
|
+
end
|
686
|
+
|
687
|
+
def resolve
|
688
|
+
sort_generations
|
689
|
+
load_json
|
690
|
+
|
691
|
+
@url = File.join(@config.http_url, fqdn)
|
692
|
+
generations.each(&:resolve)
|
693
|
+
|
694
|
+
nil
|
695
|
+
end
|
696
|
+
|
697
|
+
def to_json(*args)
|
698
|
+
{
|
699
|
+
url:,
|
700
|
+
name:,
|
701
|
+
spin:,
|
702
|
+
fqdn:,
|
703
|
+
label:,
|
704
|
+
generations:
|
705
|
+
}.to_json(*args)
|
706
|
+
end
|
707
|
+
|
708
|
+
protected
|
709
|
+
|
710
|
+
def sort_generations
|
711
|
+
@generations.sort! do |a, b|
|
712
|
+
b.generation <=> a.generation
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
def load_json
|
717
|
+
@current = @generations.detect(&:current)
|
718
|
+
|
719
|
+
if @current.nil?
|
720
|
+
raise "Unable to find current generation of machine #{name}"
|
721
|
+
end
|
722
|
+
|
723
|
+
@spin = @current.json.fetch('spin', spin)
|
724
|
+
@fqdn = @current.json.fetch('fqdn', name)
|
725
|
+
|
726
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
727
|
+
@label = @current.json.fetch('label', nil)
|
728
|
+
@label ||= @current.json.fetch('fqdn', nil)
|
729
|
+
@label ||= name
|
730
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
731
|
+
|
732
|
+
@short_label = @label.split('.')[0..1].join('.')
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
class Generation
|
737
|
+
# @return [Machine]
|
738
|
+
attr_reader :machine
|
739
|
+
|
740
|
+
# @return [String]
|
741
|
+
attr_reader :link_path
|
742
|
+
|
743
|
+
# @return [String]
|
744
|
+
attr_reader :store_path
|
745
|
+
|
746
|
+
# @return [Integer]
|
747
|
+
attr_reader :generation
|
748
|
+
|
749
|
+
# @return [Time]
|
750
|
+
attr_reader :time
|
751
|
+
|
752
|
+
# @return [String]
|
753
|
+
attr_reader :time_s
|
754
|
+
|
755
|
+
# @return [String]
|
756
|
+
attr_reader :kernel_version
|
757
|
+
|
758
|
+
# @return [Array<String>]
|
759
|
+
attr_reader :kernel_params
|
760
|
+
|
761
|
+
# @return [Hash<String, BootFile>]
|
762
|
+
attr_reader :boot_files
|
763
|
+
|
764
|
+
# @return [Array<Variant>]
|
765
|
+
attr_reader :variants
|
766
|
+
|
767
|
+
# @return [String] Nix store path to `config.system.build.toplevel`
|
768
|
+
attr_reader :toplevel
|
769
|
+
|
770
|
+
# @return [String, nil] system.nixos.version
|
771
|
+
attr_reader :version
|
772
|
+
|
773
|
+
# @return [String, nil] system.nixos.revision
|
774
|
+
attr_reader :revision
|
775
|
+
|
776
|
+
# @return [String, nil]
|
777
|
+
attr_reader :shortrev
|
778
|
+
|
779
|
+
# @return [Array<String>]
|
780
|
+
attr_reader :macs
|
781
|
+
|
782
|
+
# @return [Boolean]
|
783
|
+
attr_accessor :current
|
784
|
+
|
785
|
+
# @return [Hash] contents of `machine.json`
|
786
|
+
attr_reader :json
|
787
|
+
|
788
|
+
# @return [String]
|
789
|
+
attr_reader :url
|
790
|
+
|
791
|
+
# @param machine [Machine]
|
792
|
+
# @param link_path [String] Nix store path
|
793
|
+
# @param generation [Integer]
|
794
|
+
def initialize(machine, link_path, generation)
|
795
|
+
@machine = machine
|
796
|
+
@link_path = link_path
|
797
|
+
@store_path = File.realpath(link_path)
|
798
|
+
@generation = generation
|
799
|
+
@time = File.lstat(link_path).mtime
|
800
|
+
@time_s = @time.strftime('%Y-%m-%d %H:%M:%S')
|
801
|
+
@current = false
|
802
|
+
|
803
|
+
@json = JSON.parse(File.read(File.join(link_path, 'machine.json')))
|
804
|
+
@toplevel = json.fetch('toplevel')
|
805
|
+
@version = json.fetch('version', nil)
|
806
|
+
@revision = json.fetch('revision', nil)
|
807
|
+
|
808
|
+
@shortrev =
|
809
|
+
if @revision
|
810
|
+
@revision[0..8]
|
811
|
+
elsif @version
|
812
|
+
@version.split('.').last
|
813
|
+
end
|
814
|
+
|
815
|
+
@macs = json.fetch('macs', [])
|
816
|
+
|
817
|
+
@kernel_version = extract_kernel_version
|
818
|
+
|
819
|
+
kernel_params_file = File.join(store_path, 'kernel-params')
|
820
|
+
|
821
|
+
@kernel_params =
|
822
|
+
if File.exist?(kernel_params_file)
|
823
|
+
File.read(kernel_params_file).strip.split
|
824
|
+
else
|
825
|
+
json.fetch('kernelParams', [])
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
def resolve
|
830
|
+
@url = File.join(machine.url, generation.to_s)
|
831
|
+
@boot_files = find_boot_files
|
832
|
+
@variants = Variant.for_machine(machine)
|
833
|
+
|
834
|
+
return if machine.spin != 'vpsadminos'
|
835
|
+
|
836
|
+
@kernel_params.insert(0, "httproot=#{boot_files['root.squashfs'].url}")
|
837
|
+
end
|
838
|
+
|
839
|
+
def to_json(*args)
|
840
|
+
{
|
841
|
+
url:,
|
842
|
+
store_path:,
|
843
|
+
generation:,
|
844
|
+
time: time.to_i,
|
845
|
+
time_s:,
|
846
|
+
current:,
|
847
|
+
toplevel:,
|
848
|
+
version:,
|
849
|
+
revision:,
|
850
|
+
shortrev:,
|
851
|
+
macs:,
|
852
|
+
kernel_version:,
|
853
|
+
kernel_params:,
|
854
|
+
boot_files:,
|
855
|
+
variants:,
|
856
|
+
swpins_info: json['swpins-info']
|
857
|
+
}.to_json(*args)
|
858
|
+
end
|
859
|
+
|
860
|
+
protected
|
861
|
+
|
862
|
+
def extract_kernel_version
|
863
|
+
link = File.readlink(File.join(toplevel, 'kernel'))
|
864
|
+
return unless %r{\A/nix/store/[^-]+-linux-([^/]+)} =~ link
|
865
|
+
|
866
|
+
::Regexp.last_match(1)
|
867
|
+
rescue Errno::ENOENT
|
868
|
+
nil
|
869
|
+
end
|
870
|
+
|
871
|
+
def find_boot_files
|
872
|
+
%w[bzImage initrd root.squashfs].to_h do |name|
|
873
|
+
[name, BootFile.new(name, File.realpath(File.join(link_path, name)), File.join(url, name))]
|
874
|
+
rescue Errno::ENOENT
|
875
|
+
[name, nil]
|
876
|
+
end.compact
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
class BootFile
|
881
|
+
# @return [String]
|
882
|
+
attr_reader :name
|
883
|
+
|
884
|
+
# @return [String]
|
885
|
+
attr_reader :path
|
886
|
+
|
887
|
+
# @return [String]
|
888
|
+
attr_reader :url
|
889
|
+
|
890
|
+
def initialize(name, path, url)
|
891
|
+
@name = name
|
892
|
+
@path = path
|
893
|
+
@url = url
|
894
|
+
end
|
895
|
+
|
896
|
+
def to_json(*args)
|
897
|
+
url.to_json(*args)
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
class Variant
|
902
|
+
# @param machine [Machine]
|
903
|
+
# @return [Array<Variant>]
|
904
|
+
def self.for_machine(machine)
|
905
|
+
if machine.spin == 'vpsadminos'
|
906
|
+
[
|
907
|
+
new(
|
908
|
+
name: 'default',
|
909
|
+
label: 'Default runlevel',
|
910
|
+
kernel_params: ['runlevel=default']
|
911
|
+
),
|
912
|
+
new(
|
913
|
+
name: 'nopools',
|
914
|
+
label: 'Default runlevel without container imports',
|
915
|
+
kernel_params: ['runlevel=default', 'osctl.pools=0']
|
916
|
+
),
|
917
|
+
new(
|
918
|
+
name: 'nostart',
|
919
|
+
label: 'Default runlevel without container autostart',
|
920
|
+
kernel_params: ['runlevel=default', 'osctl.autostart=0']
|
921
|
+
),
|
922
|
+
new(
|
923
|
+
name: 'rescue',
|
924
|
+
label: 'Rescue runlevel (network and sshd)',
|
925
|
+
kernel_params: ['runlevel=rescue']
|
926
|
+
),
|
927
|
+
new(
|
928
|
+
name: 'single',
|
929
|
+
label: 'Single-user runlevel (console only)',
|
930
|
+
kernel_params: ['runlevel=single']
|
931
|
+
)
|
932
|
+
]
|
933
|
+
else
|
934
|
+
[]
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
# @return [String]
|
939
|
+
attr_reader :name
|
940
|
+
|
941
|
+
# @return [String]
|
942
|
+
attr_reader :label
|
943
|
+
|
944
|
+
# @return [String]
|
945
|
+
attr_reader :kernel_params
|
946
|
+
|
947
|
+
def initialize(name:, label:, kernel_params:)
|
948
|
+
@name = name
|
949
|
+
@label = label
|
950
|
+
@kernel_params = kernel_params
|
951
|
+
end
|
952
|
+
|
953
|
+
def to_json(*args)
|
954
|
+
{
|
955
|
+
name:,
|
956
|
+
label:,
|
957
|
+
kernel_params:
|
958
|
+
}.to_json(*args)
|
959
|
+
end
|
960
|
+
end
|
961
|
+
|
962
|
+
NetbootBuilder.run
|