confctl 2.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.
@@ -166,7 +166,7 @@ class NetbootBuilder
166
166
 
167
167
  link_path = File.join(LINK_DIR, v)
168
168
 
169
- machines[name] ||= Machine.new(name)
169
+ machines[name] ||= Machine.new(@config, name)
170
170
  machines[name].add_generation(link_path, generation)
171
171
  end
172
172
 
@@ -255,6 +255,12 @@ class RootBuilder
255
255
  FileUtils.mkdir_p(File.join(root, v))
256
256
  end
257
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
258
264
  end
259
265
 
260
266
  class TftpBuilder < RootBuilder
@@ -301,10 +307,10 @@ class TftpBuilder < RootBuilder
301
307
  path = File.join('boot', m.fqdn, g.generation.to_s)
302
308
  mkdir_p(path)
303
309
 
304
- Dir.entries(g.store_path).each do |v|
305
- next unless %w[bzImage initrd].include?(v)
310
+ g.boot_files.each_value do |boot_file|
311
+ next unless %w[bzImage initrd].include?(boot_file.name)
306
312
 
307
- ln_s(File.join(g.store_path, v), File.join(path, v))
313
+ ln_s(boot_file.path, File.join(path, boot_file.name))
308
314
  end
309
315
 
310
316
  ln_s(g.generation.to_s, File.join('boot', m.fqdn, 'current')) if g.current
@@ -432,7 +438,7 @@ class TftpBuilder < RootBuilder
432
438
 
433
439
  <% m.generations[1..].each do |g| -%>
434
440
  LABEL <%= m.fqdn %>-<%= g.generation %>
435
- MENU LABEL Configuration <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
441
+ MENU LABEL Gen <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
436
442
  LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
437
443
  INITRD boot/<%= m.fqdn %>/<%= g.generation %>/initrd
438
444
  APPEND init=<%= g.toplevel %>/init loglevel=7
@@ -496,47 +502,19 @@ class TftpBuilder < RootBuilder
496
502
  # @param root [Boolean] true if this is machine menu page, not generation menu
497
503
  # @param timeout [Integer, nil] timeout in seconds until the default action is taken
498
504
  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
505
  tpl = <<~ERB
528
506
  <% if timeout -%>
529
507
  DEFAULT menu.c32
530
508
  TIMEOUT 50
531
509
  <% end -%>
532
- MENU TITLE <%= m.label %> (<%= g.generation %> - <%= g.current ? 'current' : g.time_s %> - <%= g.shortrev %>)
510
+ MENU TITLE <%= m.short_label %> (<%= g.generation %> - <%= g.current ? 'current' : g.time_s %> - <%= g.shortrev %> - <%= g.kernel_version %>)
533
511
 
534
- <% variants.each do |variant, vopts| -%>
535
- LABEL <%= variant %>
536
- MENU LABEL <%= vopts[:label] %>
512
+ <% g.variants.each do |variant| -%>
513
+ LABEL <%= variant.name %>
514
+ MENU LABEL <%= variant.label %>
537
515
  LINUX boot/<%= m.fqdn %>/<%= g.generation %>/bzImage
538
516
  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(' ') %>
517
+ APPEND <%= g.kernel_params.join(' ') %> <%= variant.kernel_params.join(' ') %>
540
518
 
541
519
  <% end -%>
542
520
  LABEL <%= m.fqdn %>-generations
@@ -562,7 +540,6 @@ class TftpBuilder < RootBuilder
562
540
  {
563
541
  m: machine,
564
542
  g: generation,
565
- variants:,
566
543
  http_url: @config.http_url,
567
544
  root:,
568
545
  timeout:,
@@ -579,7 +556,7 @@ class TftpBuilder < RootBuilder
579
556
 
580
557
  <% m.generations.each do |g| -%>
581
558
  LABEL generations
582
- MENU LABEL Configuration <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %>
559
+ MENU LABEL Gen <%= g.generation %> - <%= g.time_s %> - <%= g.shortrev %> - <%= g.kernel_version %>
583
560
  KERNEL menu.c32
584
561
  APPEND pxeserver/machines/<%= m.fqdn %>/generation-<%= g.generation %>.cfg
585
562
 
@@ -640,19 +617,23 @@ class HttpBuilder < RootBuilder
640
617
  def build
641
618
  machines.each do |m|
642
619
  m.generations.each do |g|
643
- begin
644
- rootfs = File.realpath(File.join(g.link_path, 'root.squashfs'))
645
- rescue Errno::ENOENT
646
- next
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))
647
625
  end
648
626
 
649
- gen_path = File.join(m.fqdn, g.generation.to_s)
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(' '))
650
629
 
651
- mkdir_p(gen_path)
652
- ln_s(rootfs, File.join(gen_path, 'root.squashfs'))
653
630
  ln_s(g.generation.to_s, File.join(m.fqdn, 'current')) if g.current
654
631
  end
632
+
633
+ write_to(File.join(m.fqdn, 'machine.json'), JSON.pretty_generate(m))
655
634
  end
635
+
636
+ write_to('machines.json', JSON.pretty_generate({ machines: }))
656
637
  end
657
638
  end
658
639
 
@@ -669,18 +650,27 @@ class Machine
669
650
  # @return [String]
670
651
  attr_reader :label
671
652
 
653
+ # @return [String]
654
+ attr_reader :short_label
655
+
656
+ # @return [String]
657
+ attr_reader :url
658
+
672
659
  # @return [Array<Generation>]
673
660
  attr_reader :generations
674
661
 
675
662
  # @return [Generation]
676
663
  attr_reader :current
677
664
 
665
+ # @param config [Config]
678
666
  # @param name [String] machine name
679
- def initialize(name)
667
+ def initialize(config, name)
668
+ @config = config
680
669
  @name = name
681
670
  @spin = 'nixos'
682
671
  @fqdn = name
683
672
  @label = name
673
+ @short_label = name[0..14]
684
674
  @toplevel = nil
685
675
  @macs = []
686
676
  @generations = []
@@ -690,13 +680,29 @@ class Machine
690
680
  # @param link_path [String] Nix store path
691
681
  # @param generation [Integer]
692
682
  def add_generation(link_path, generation)
693
- @generations << Generation.new(link_path, generation)
683
+ @generations << Generation.new(self, link_path, generation)
694
684
  nil
695
685
  end
696
686
 
697
687
  def resolve
698
688
  sort_generations
699
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)
700
706
  end
701
707
 
702
708
  protected
@@ -722,10 +728,15 @@ class Machine
722
728
  @label ||= @current.json.fetch('fqdn', nil)
723
729
  @label ||= name
724
730
  # rubocop:enable Naming/MemoizedInstanceVariableName
731
+
732
+ @short_label = @label.split('.')[0..1].join('.')
725
733
  end
726
734
  end
727
735
 
728
736
  class Generation
737
+ # @return [Machine]
738
+ attr_reader :machine
739
+
729
740
  # @return [String]
730
741
  attr_reader :link_path
731
742
 
@@ -741,9 +752,18 @@ class Generation
741
752
  # @return [String]
742
753
  attr_reader :time_s
743
754
 
755
+ # @return [String]
756
+ attr_reader :kernel_version
757
+
744
758
  # @return [Array<String>]
745
759
  attr_reader :kernel_params
746
760
 
761
+ # @return [Hash<String, BootFile>]
762
+ attr_reader :boot_files
763
+
764
+ # @return [Array<Variant>]
765
+ attr_reader :variants
766
+
747
767
  # @return [String] Nix store path to `config.system.build.toplevel`
748
768
  attr_reader :toplevel
749
769
 
@@ -765,9 +785,14 @@ class Generation
765
785
  # @return [Hash] contents of `machine.json`
766
786
  attr_reader :json
767
787
 
788
+ # @return [String]
789
+ attr_reader :url
790
+
791
+ # @param machine [Machine]
768
792
  # @param link_path [String] Nix store path
769
793
  # @param generation [Integer]
770
- def initialize(link_path, generation)
794
+ def initialize(machine, link_path, generation)
795
+ @machine = machine
771
796
  @link_path = link_path
772
797
  @store_path = File.realpath(link_path)
773
798
  @generation = generation
@@ -789,6 +814,8 @@ class Generation
789
814
 
790
815
  @macs = json.fetch('macs', [])
791
816
 
817
+ @kernel_version = extract_kernel_version
818
+
792
819
  kernel_params_file = File.join(store_path, 'kernel-params')
793
820
 
794
821
  @kernel_params =
@@ -798,6 +825,138 @@ class Generation
798
825
  json.fetch('kernelParams', [])
799
826
  end
800
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
801
960
  end
802
961
 
803
962
  NetbootBuilder.run
@@ -0,0 +1,36 @@
1
+ { config, lib, pkgs, confMachine, ... }:
2
+ let
3
+ inherit (lib) mkEnableOption mkIf;
4
+
5
+ cfg = config.confctl.programs.kexec-netboot;
6
+
7
+ kexecNetboot = pkgs.substituteAll {
8
+ name = "kexec-netboot";
9
+ src = ./kexec-netboot.rb;
10
+ isExecutable = true;
11
+ ruby = pkgs.ruby;
12
+ kexecTools = pkgs.kexec-tools;
13
+ machineFqdn = confMachine.host.fqdn;
14
+ };
15
+
16
+ kexecNetbootBin = pkgs.runCommand "kexec-netboot-bin" {} ''
17
+ mkdir -p $out/bin
18
+ ln -s ${kexecNetboot} $out/bin/kexec-netboot
19
+
20
+ mkdir -p $out/share/man/man8
21
+ ${pkgs.asciidoctor}/bin/asciidoctor \
22
+ -b manpage \
23
+ -D $out/share/man/man8 \
24
+ ${./kexec-netboot.8.adoc}
25
+ '';
26
+ in {
27
+ options = {
28
+ confctl.programs.kexec-netboot = {
29
+ enable = mkEnableOption "Enable kexec-netboot utility";
30
+ };
31
+ };
32
+
33
+ config = mkIf cfg.enable {
34
+ environment.systemPackages = [ kexecNetbootBin ];
35
+ };
36
+ }
@@ -0,0 +1,62 @@
1
+ = kexec-netboot(8)
2
+ :doctype: manpage
3
+ :docdate: 2025-03-03
4
+ :manmanual: kexec-netboot
5
+ :mansource: kexec-netboot
6
+ :man-linkstyle: pass:[blue R < >]
7
+
8
+ == Name
9
+
10
+ kexec-netboot - Prepare machine for kexec using kernel/initrd from netboot server
11
+
12
+ == Synopsis
13
+
14
+ *kexec-netboot* [_options_]
15
+
16
+ == Description
17
+
18
+ *kexec-netboot* may be used to download kernel and initrd from the netboot server
19
+ the machine was booted from and load it using *kexec*.
20
+
21
+ It is possible to boot into any machine available on the netboot server
22
+ and to select generation and variant (default, single-user mode).
23
+
24
+ == Options
25
+
26
+ The following options are understood:
27
+
28
+ *-h*, *--help*::
29
+ Print a short help text and exit.
30
+
31
+ *-s*, *--server-url* _URL_::
32
+ Specify URL to the netboot server. By default, the URL is auto-detected
33
+ by reading *httproot* from */proc/cmdline*.
34
+
35
+ *-m*, *--machine* _FQDN_::
36
+ Select machine from the netboot server.
37
+
38
+ *-g*, *--generation* _GENERATION_::
39
+ Select machine generation identified by its number.
40
+
41
+ *-v*, *--variant* _VARIANT_::
42
+ Select generation variant identified by its name.
43
+
44
+ *-i*, *--interactive*::
45
+ Ask the user to select machine, generation and variant interactively.
46
+
47
+ *-a*, *--append* _PARAMS_::
48
+ Append parameters to the kernel command line.
49
+
50
+ *-u*, *--unload*::
51
+ Unload the current kexec target kernel and exit.
52
+
53
+ *-e*, *--exec*::
54
+ Run the currently loaded kernel.
55
+
56
+ == Bugs
57
+
58
+ Report bugs to https://github.com/vpsfreecz/confctl/issues.
59
+
60
+ == About
61
+
62
+ *kexec-netboot* is a part of https://github.com/vpsfreecz/confctl[confctl].