confctl 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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