confctl 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +1 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +18 -1
- data/README.md +3 -9
- data/confctl.gemspec +14 -14
- data/docs/carrier.md +138 -0
- data/lib/confctl/cli/app.rb +3 -0
- data/lib/confctl/cli/cluster.rb +73 -44
- data/lib/confctl/cli/configuration.rb +7 -2
- data/lib/confctl/cli/gen_data.rb +19 -1
- data/lib/confctl/cli/generation.rb +5 -3
- data/lib/confctl/generation/host_list.rb +3 -3
- data/lib/confctl/git_repo_mirror.rb +2 -2
- data/lib/confctl/machine.rb +101 -11
- data/lib/confctl/machine_control.rb +7 -0
- data/lib/confctl/machine_list.rb +14 -1
- data/lib/confctl/machine_status.rb +51 -4
- data/lib/confctl/nix.rb +28 -5
- data/lib/confctl/swpins/specs/git.rb +1 -1
- data/lib/confctl/version.rb +1 -1
- data/man/man8/confctl-options.nix.8 +165 -1
- data/man/man8/confctl-options.nix.8.md +165 -1
- data/man/man8/confctl.8 +21 -1
- data/man/man8/confctl.8.md +16 -1
- data/nix/evaluator.nix +18 -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 +142 -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 +803 -0
- data/nix/modules/confctl/carrier/netboot/nixos.nix +185 -0
- data/nix/modules/system-list.nix +8 -0
- metadata +12 -7
- data/.ruby-version +0 -1
@@ -0,0 +1,803 @@
|
|
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(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
|
+
end
|
259
|
+
|
260
|
+
class TftpBuilder < RootBuilder
|
261
|
+
PROGRAMS = %w[pxelinux.0 ldlinux.c32 libcom32.c32 libutil.c32 memdisk menu.c32 reboot.c32].freeze
|
262
|
+
|
263
|
+
SPIN_LABELS = {
|
264
|
+
'nixos' => 'NixOS',
|
265
|
+
'vpsadminos' => 'vpsAdminOS'
|
266
|
+
}.freeze
|
267
|
+
|
268
|
+
def build
|
269
|
+
install_syslinux
|
270
|
+
|
271
|
+
mkdir_p('pxelinux.cfg', 'pxeserver')
|
272
|
+
|
273
|
+
install_boot_files
|
274
|
+
|
275
|
+
@spins =
|
276
|
+
machines.each_with_object([]) do |m, acc|
|
277
|
+
acc << m.spin unless acc.include?(m.spin)
|
278
|
+
end.to_h do |spin|
|
279
|
+
[spin, SPIN_LABELS.fetch(spin, spin)]
|
280
|
+
end
|
281
|
+
|
282
|
+
render_default_config
|
283
|
+
render_spin_configs
|
284
|
+
render_machine_configs
|
285
|
+
render_iso_image_configs
|
286
|
+
end
|
287
|
+
|
288
|
+
protected
|
289
|
+
|
290
|
+
def install_syslinux
|
291
|
+
PROGRAMS.each do |prog|
|
292
|
+
cp(File.join(@config.syslinux, 'share/syslinux', prog), prog)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def install_boot_files
|
297
|
+
# rubocop:disable Style/GuardClause
|
298
|
+
|
299
|
+
machines.each do |m|
|
300
|
+
m.generations.each do |g|
|
301
|
+
path = File.join('boot', m.fqdn, g.generation.to_s)
|
302
|
+
mkdir_p(path)
|
303
|
+
|
304
|
+
Dir.entries(g.store_path).each do |v|
|
305
|
+
next unless %w[bzImage initrd].include?(v)
|
306
|
+
|
307
|
+
ln_s(File.join(g.store_path, v), File.join(path, v))
|
308
|
+
end
|
309
|
+
|
310
|
+
ln_s(g.generation.to_s, File.join('boot', m.fqdn, 'current')) if g.current
|
311
|
+
end
|
312
|
+
|
313
|
+
m.current.macs.each do |mac|
|
314
|
+
# See https://wiki.syslinux.org/wiki/index.php?title=PXELINUX#Configuration
|
315
|
+
ln_s("../pxeserver/machines/#{m.fqdn}/auto.cfg", "pxelinux.cfg/01-#{mac.gsub(':', '-')}")
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
if @config.memtest.enable
|
320
|
+
mkdir_p('boot/memtest86')
|
321
|
+
ln_s(File.join(@config.memtest.package, 'memtest.bin'), 'boot/memtest86/memtest.bin')
|
322
|
+
end
|
323
|
+
|
324
|
+
if @config.iso_images.any?
|
325
|
+
mkdir_p('boot/iso-images')
|
326
|
+
|
327
|
+
@config.iso_images.each do |img|
|
328
|
+
ln_s(img.file, File.join('boot/iso-images', img.name))
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# rubocop:enable Style/GuardClause
|
333
|
+
end
|
334
|
+
|
335
|
+
def render_default_config
|
336
|
+
tpl = <<~ERB
|
337
|
+
DEFAULT menu.c32
|
338
|
+
PROMPT 0
|
339
|
+
TIMEOUT 0
|
340
|
+
MENU TITLE <%= hostname %>
|
341
|
+
|
342
|
+
<% spins.each do |spin, label| -%>
|
343
|
+
LABEL <%= spin %>
|
344
|
+
MENU LABEL <%= label %> >
|
345
|
+
KERNEL menu.c32
|
346
|
+
APPEND pxeserver/<%= spin %>.cfg
|
347
|
+
|
348
|
+
<% end -%>
|
349
|
+
<% if iso_images.any? -%>
|
350
|
+
LABEL isoimages
|
351
|
+
MENU LABEL ISO images >
|
352
|
+
KERNEL menu.c32
|
353
|
+
APPEND pxeserver/iso-images.cfg
|
354
|
+
|
355
|
+
<% end -%>
|
356
|
+
<% if enable_memtest -%>
|
357
|
+
LABEL memtest
|
358
|
+
MENU LABEL Memtest86
|
359
|
+
LINUX boot/memtest86/memtest.bin
|
360
|
+
APPEND <%= memtest_params %>
|
361
|
+
|
362
|
+
<% end -%>
|
363
|
+
LABEL local_boot
|
364
|
+
MENU LABEL Local Boot
|
365
|
+
LOCALBOOT 0
|
366
|
+
|
367
|
+
LABEL warm_reboot
|
368
|
+
MENU LABEL Warm Reboot
|
369
|
+
KERNEL reboot.c32
|
370
|
+
APPEND --warm
|
371
|
+
|
372
|
+
LABEL cold_reboot
|
373
|
+
MENU LABEL Cold Reboot
|
374
|
+
KERNEL reboot.c32
|
375
|
+
ERB
|
376
|
+
|
377
|
+
render_to(
|
378
|
+
tpl,
|
379
|
+
{
|
380
|
+
hostname: @config.host_name,
|
381
|
+
spins: @spins,
|
382
|
+
enable_memtest: @config.memtest.enable,
|
383
|
+
memtest_params: @config.memtest.params_line,
|
384
|
+
iso_images: @config.iso_images
|
385
|
+
},
|
386
|
+
'pxelinux.cfg/default'
|
387
|
+
)
|
388
|
+
end
|
389
|
+
|
390
|
+
def render_spin_configs
|
391
|
+
tpl = <<~ERB
|
392
|
+
MENU TITLE <%= label %>
|
393
|
+
|
394
|
+
<% spin_machines.each do |m| -%>
|
395
|
+
LABEL <%= m.fqdn %>
|
396
|
+
MENU LABEL <%= m.label %> >
|
397
|
+
KERNEL menu.c32
|
398
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/menu.cfg
|
399
|
+
<% end -%>
|
400
|
+
|
401
|
+
LABEL mainmenu
|
402
|
+
MENU LABEL < Back to Main Menu
|
403
|
+
KERNEL menu.c32
|
404
|
+
APPEND pxelinux.cfg/default
|
405
|
+
ERB
|
406
|
+
|
407
|
+
@spins.each do |spin, label|
|
408
|
+
spin_machines = machines.select { |m| m.spin == spin }.sort { |a, b| a.name <=> b.name }
|
409
|
+
|
410
|
+
render_to(tpl, { spin:, label:, spin_machines: }, "pxeserver/#{spin}.cfg")
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def render_machine_configs
|
415
|
+
mkdir_p('pxeserver/machines')
|
416
|
+
|
417
|
+
machines.each do |m|
|
418
|
+
mkdir_p("pxeserver/machines/#{m.fqdn}")
|
419
|
+
send(:"render_machine_#{m.spin}", m)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def render_machine_nixos(machine)
|
424
|
+
tpl = <<~ERB
|
425
|
+
MENU TITLE <%= m.label %>
|
426
|
+
|
427
|
+
LABEL <%= m.fqdn %>
|
428
|
+
MENU LABEL <%= m.label %>
|
429
|
+
LINUX boot/<%= m.fqdn %>/<%= m.current.generation %>/bzImage
|
430
|
+
INITRD boot/<%= m.fqdn %>/<%= m.current.generation %>/initrd
|
431
|
+
APPEND init=<%= m.current.toplevel %>/init loglevel=7
|
432
|
+
|
433
|
+
<% m.generations[1..].each do |g| -%>
|
434
|
+
LABEL <%= m.fqdn %>-<%= g.generation %>
|
435
|
+
MENU LABEL Configuration <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
|
436
|
+
LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
|
437
|
+
INITRD boot/<%= m.fqdn %>/<%= g.generation %>/initrd
|
438
|
+
APPEND init=<%= g.toplevel %>/init loglevel=7
|
439
|
+
|
440
|
+
<% end -%>
|
441
|
+
<% if enable_memtest -%>
|
442
|
+
LABEL memtest
|
443
|
+
MENU LABEL Memtest86
|
444
|
+
LINUX boot/memtest86/memtest.bin
|
445
|
+
APPEND <%= memtest_params %>
|
446
|
+
|
447
|
+
<% end -%>
|
448
|
+
LABEL mainmenu
|
449
|
+
MENU LABEL < Back to Main Menu
|
450
|
+
KERNEL menu.c32
|
451
|
+
APPEND pxelinux.cfg/default
|
452
|
+
ERB
|
453
|
+
|
454
|
+
render_to(
|
455
|
+
tpl,
|
456
|
+
{
|
457
|
+
m: machine,
|
458
|
+
enable_memtest: @config.memtest.enable,
|
459
|
+
memtest_params: @config.memtest.params_line
|
460
|
+
},
|
461
|
+
"pxeserver/machines/#{machine.fqdn}/menu.cfg"
|
462
|
+
)
|
463
|
+
end
|
464
|
+
|
465
|
+
def render_machine_vpsadminos(machine)
|
466
|
+
render_machine_vpsadminos_config(
|
467
|
+
machine,
|
468
|
+
generation: machine.current,
|
469
|
+
file: 'menu',
|
470
|
+
root: true
|
471
|
+
)
|
472
|
+
|
473
|
+
render_machine_vpsadminos_config(
|
474
|
+
machine,
|
475
|
+
generation: machine.current,
|
476
|
+
file: 'auto',
|
477
|
+
timeout: 5,
|
478
|
+
root: true
|
479
|
+
)
|
480
|
+
|
481
|
+
render_generations(machine)
|
482
|
+
|
483
|
+
machine.generations.each do |g|
|
484
|
+
render_machine_vpsadminos_config(
|
485
|
+
machine,
|
486
|
+
generation: g,
|
487
|
+
file: "generation-#{g.generation}",
|
488
|
+
root: false
|
489
|
+
)
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# @param machine [Machine]
|
494
|
+
# @param generation [Generation]
|
495
|
+
# @param file [String] config base name
|
496
|
+
# @param root [Boolean] true if this is machine menu page, not generation menu
|
497
|
+
# @param timeout [Integer, nil] timeout in seconds until the default action is taken
|
498
|
+
def render_machine_vpsadminos_config(machine, generation:, file:, root:, timeout: nil)
|
499
|
+
variants = {
|
500
|
+
default: {
|
501
|
+
label: 'Default runlevel',
|
502
|
+
kernel_params: [],
|
503
|
+
runlevel: 'default'
|
504
|
+
},
|
505
|
+
nopools: {
|
506
|
+
label: 'Default runlevel without container imports',
|
507
|
+
kernel_params: ['osctl.pools=0'],
|
508
|
+
runlevel: 'default'
|
509
|
+
},
|
510
|
+
nostart: {
|
511
|
+
label: 'Default runlevel without container autostart',
|
512
|
+
kernel_params: ['osctl.autostart=0'],
|
513
|
+
runlevel: 'default'
|
514
|
+
},
|
515
|
+
rescue: {
|
516
|
+
label: 'Rescue runlevel (network and sshd)',
|
517
|
+
kernel_params: [],
|
518
|
+
runlevel: 'rescue'
|
519
|
+
},
|
520
|
+
single: {
|
521
|
+
label: 'Single-user runlevel (console only)',
|
522
|
+
kernel_params: [],
|
523
|
+
runlevel: 'single'
|
524
|
+
}
|
525
|
+
}
|
526
|
+
|
527
|
+
tpl = <<~ERB
|
528
|
+
<% if timeout -%>
|
529
|
+
DEFAULT menu.c32
|
530
|
+
TIMEOUT 50
|
531
|
+
<% end -%>
|
532
|
+
MENU TITLE <%= m.label %> (<%= g.generation %> - <%= g.current ? 'current' : g.time_s %> - <%= g.shortrev %>)
|
533
|
+
|
534
|
+
<% variants.each do |variant, vopts| -%>
|
535
|
+
LABEL <%= variant %>
|
536
|
+
MENU LABEL <%= vopts[:label] %>
|
537
|
+
LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
|
538
|
+
INITRD boot/<%= m.fqdn %>/<%= g.generation %>/initrd
|
539
|
+
APPEND httproot=<%= File.join(http_url, m.fqdn, g.generation.to_s, 'root.squashfs') %> <%= g.kernel_params.join(' ') %> runlevel=<%= vopts[:runlevel] %> <%= vopts[:kernel_params].join(' ') %>
|
540
|
+
|
541
|
+
<% end -%>
|
542
|
+
LABEL <%= m.fqdn %>-generations
|
543
|
+
MENU LABEL <%= root ? 'Generations >' : '< Back to Generations' %>
|
544
|
+
KERNEL menu.c32
|
545
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/generations.cfg
|
546
|
+
|
547
|
+
<% if enable_memtest -%>
|
548
|
+
LABEL memtest
|
549
|
+
MENU LABEL Memtest86
|
550
|
+
LINUX boot/memtest86/memtest.bin
|
551
|
+
APPEND <%= memtest_params %>
|
552
|
+
|
553
|
+
<% end -%>
|
554
|
+
LABEL mainmenu
|
555
|
+
MENU LABEL < Back to Main Menu
|
556
|
+
KERNEL menu.c32
|
557
|
+
APPEND pxelinux.cfg/default
|
558
|
+
ERB
|
559
|
+
|
560
|
+
render_to(
|
561
|
+
tpl,
|
562
|
+
{
|
563
|
+
m: machine,
|
564
|
+
g: generation,
|
565
|
+
variants:,
|
566
|
+
http_url: @config.http_url,
|
567
|
+
root:,
|
568
|
+
timeout:,
|
569
|
+
enable_memtest: @config.memtest.enable,
|
570
|
+
memtest_params: @config.memtest.params_line
|
571
|
+
},
|
572
|
+
"pxeserver/machines/#{machine.fqdn}/#{file}.cfg"
|
573
|
+
)
|
574
|
+
end
|
575
|
+
|
576
|
+
def render_generations(machine)
|
577
|
+
tpl = <<~ERB
|
578
|
+
MENU TITLE <%= m.label %> - generations
|
579
|
+
|
580
|
+
<% m.generations.each do |g| -%>
|
581
|
+
LABEL generations
|
582
|
+
MENU LABEL Configuration <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
|
583
|
+
KERNEL menu.c32
|
584
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/generation-<%= g.generation %>.cfg
|
585
|
+
|
586
|
+
<% end -%>
|
587
|
+
|
588
|
+
LABEL machine
|
589
|
+
MENU LABEL < Back to <%= m.label %>
|
590
|
+
KERNEL menu.c32
|
591
|
+
APPEND pxeserver/machines/<%= m.fqdn %>/menu.cfg
|
592
|
+
|
593
|
+
LABEL mainmenu
|
594
|
+
MENU LABEL < Back to Main Menu
|
595
|
+
KERNEL menu.c32
|
596
|
+
APPEND pxelinux.cfg/default
|
597
|
+
ERB
|
598
|
+
|
599
|
+
render_to(tpl, { m: machine }, "pxeserver/machines/#{machine.fqdn}/generations.cfg")
|
600
|
+
end
|
601
|
+
|
602
|
+
def render_iso_image_configs
|
603
|
+
return if @config.iso_images.empty?
|
604
|
+
|
605
|
+
tpl = <<~ERB
|
606
|
+
MENU TITLE ISO images
|
607
|
+
|
608
|
+
<% iso_images.each_with_index do |img, i| -%>
|
609
|
+
LABEL image<%= i %>
|
610
|
+
MENU LABEL <%= img.label %>
|
611
|
+
KERNEL memdisk
|
612
|
+
APPEND iso initrd=boot/iso-images/<%= img.name %> raw
|
613
|
+
<% end -%>
|
614
|
+
|
615
|
+
LABEL mainmenu
|
616
|
+
MENU LABEL < Back to Main Menu
|
617
|
+
KERNEL menu.c32
|
618
|
+
APPEND pxelinux.cfg/default
|
619
|
+
ERB
|
620
|
+
|
621
|
+
render_to(tpl, { iso_images: @config.iso_images }, 'pxeserver/iso-images.cfg')
|
622
|
+
end
|
623
|
+
|
624
|
+
# @param template [String]
|
625
|
+
# @param vars [Hash]
|
626
|
+
def render(template, vars)
|
627
|
+
erb = ERB.new(template, trim_mode: '-')
|
628
|
+
erb.result_with_hash(vars)
|
629
|
+
end
|
630
|
+
|
631
|
+
# @param template [String]
|
632
|
+
# @param vars [Hash]
|
633
|
+
# @param path [String]
|
634
|
+
def render_to(template, vars, path)
|
635
|
+
File.write(File.join(root, path), render(template, vars))
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
class HttpBuilder < RootBuilder
|
640
|
+
def build
|
641
|
+
machines.each do |m|
|
642
|
+
m.generations.each do |g|
|
643
|
+
begin
|
644
|
+
rootfs = File.realpath(File.join(g.link_path, 'root.squashfs'))
|
645
|
+
rescue Errno::ENOENT
|
646
|
+
next
|
647
|
+
end
|
648
|
+
|
649
|
+
gen_path = File.join(m.fqdn, g.generation.to_s)
|
650
|
+
|
651
|
+
mkdir_p(gen_path)
|
652
|
+
ln_s(rootfs, File.join(gen_path, 'root.squashfs'))
|
653
|
+
ln_s(g.generation.to_s, File.join(m.fqdn, 'current')) if g.current
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
class Machine
|
660
|
+
# @return [String]
|
661
|
+
attr_reader :name
|
662
|
+
|
663
|
+
# @return [String]
|
664
|
+
attr_reader :spin
|
665
|
+
|
666
|
+
# @return [String]
|
667
|
+
attr_reader :fqdn
|
668
|
+
|
669
|
+
# @return [String]
|
670
|
+
attr_reader :label
|
671
|
+
|
672
|
+
# @return [Array<Generation>]
|
673
|
+
attr_reader :generations
|
674
|
+
|
675
|
+
# @return [Generation]
|
676
|
+
attr_reader :current
|
677
|
+
|
678
|
+
# @param name [String] machine name
|
679
|
+
def initialize(name)
|
680
|
+
@name = name
|
681
|
+
@spin = 'nixos'
|
682
|
+
@fqdn = name
|
683
|
+
@label = name
|
684
|
+
@toplevel = nil
|
685
|
+
@macs = []
|
686
|
+
@generations = []
|
687
|
+
@current = nil
|
688
|
+
end
|
689
|
+
|
690
|
+
# @param link_path [String] Nix store path
|
691
|
+
# @param generation [Integer]
|
692
|
+
def add_generation(link_path, generation)
|
693
|
+
@generations << Generation.new(link_path, generation)
|
694
|
+
nil
|
695
|
+
end
|
696
|
+
|
697
|
+
def resolve
|
698
|
+
sort_generations
|
699
|
+
load_json
|
700
|
+
end
|
701
|
+
|
702
|
+
protected
|
703
|
+
|
704
|
+
def sort_generations
|
705
|
+
@generations.sort! do |a, b|
|
706
|
+
b.generation <=> a.generation
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
def load_json
|
711
|
+
@current = @generations.detect(&:current)
|
712
|
+
|
713
|
+
if @current.nil?
|
714
|
+
raise "Unable to find current generation of machine #{name}"
|
715
|
+
end
|
716
|
+
|
717
|
+
@spin = @current.json.fetch('spin', spin)
|
718
|
+
@fqdn = @current.json.fetch('fqdn', name)
|
719
|
+
|
720
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
721
|
+
@label = @current.json.fetch('label', nil)
|
722
|
+
@label ||= @current.json.fetch('fqdn', nil)
|
723
|
+
@label ||= name
|
724
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
class Generation
|
729
|
+
# @return [String]
|
730
|
+
attr_reader :link_path
|
731
|
+
|
732
|
+
# @return [String]
|
733
|
+
attr_reader :store_path
|
734
|
+
|
735
|
+
# @return [Integer]
|
736
|
+
attr_reader :generation
|
737
|
+
|
738
|
+
# @return [Time]
|
739
|
+
attr_reader :time
|
740
|
+
|
741
|
+
# @return [String]
|
742
|
+
attr_reader :time_s
|
743
|
+
|
744
|
+
# @return [Array<String>]
|
745
|
+
attr_reader :kernel_params
|
746
|
+
|
747
|
+
# @return [String] Nix store path to `config.system.build.toplevel`
|
748
|
+
attr_reader :toplevel
|
749
|
+
|
750
|
+
# @return [String, nil] system.nixos.version
|
751
|
+
attr_reader :version
|
752
|
+
|
753
|
+
# @return [String, nil] system.nixos.revision
|
754
|
+
attr_reader :revision
|
755
|
+
|
756
|
+
# @return [String, nil]
|
757
|
+
attr_reader :shortrev
|
758
|
+
|
759
|
+
# @return [Array<String>]
|
760
|
+
attr_reader :macs
|
761
|
+
|
762
|
+
# @return [Boolean]
|
763
|
+
attr_accessor :current
|
764
|
+
|
765
|
+
# @return [Hash] contents of `machine.json`
|
766
|
+
attr_reader :json
|
767
|
+
|
768
|
+
# @param link_path [String] Nix store path
|
769
|
+
# @param generation [Integer]
|
770
|
+
def initialize(link_path, generation)
|
771
|
+
@link_path = link_path
|
772
|
+
@store_path = File.realpath(link_path)
|
773
|
+
@generation = generation
|
774
|
+
@time = File.lstat(link_path).mtime
|
775
|
+
@time_s = @time.strftime('%Y-%m-%d %H:%M:%S')
|
776
|
+
@current = false
|
777
|
+
|
778
|
+
@json = JSON.parse(File.read(File.join(link_path, 'machine.json')))
|
779
|
+
@toplevel = json.fetch('toplevel')
|
780
|
+
@version = json.fetch('version', nil)
|
781
|
+
@revision = json.fetch('revision', nil)
|
782
|
+
|
783
|
+
@shortrev =
|
784
|
+
if @revision
|
785
|
+
@revision[0..8]
|
786
|
+
elsif @version
|
787
|
+
@version.split('.').last
|
788
|
+
end
|
789
|
+
|
790
|
+
@macs = json.fetch('macs', [])
|
791
|
+
|
792
|
+
kernel_params_file = File.join(store_path, 'kernel-params')
|
793
|
+
|
794
|
+
@kernel_params =
|
795
|
+
if File.exist?(kernel_params_file)
|
796
|
+
File.read(kernel_params_file).strip.split
|
797
|
+
else
|
798
|
+
json.fetch('kernelParams', [])
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
NetbootBuilder.run
|