confctl 1.0.0 → 2.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.
@@ -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