confctl 2.0.0 → 2.2.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -1
  3. data/Gemfile +6 -0
  4. data/README.md +2 -1
  5. data/docs/carrier.md +12 -0
  6. data/lib/confctl/cli/app.rb +19 -0
  7. data/lib/confctl/cli/cluster.rb +183 -47
  8. data/lib/confctl/cli/generation.rb +44 -15
  9. data/lib/confctl/cli/swpins/channel.rb +6 -4
  10. data/lib/confctl/cli/swpins/cluster.rb +6 -4
  11. data/lib/confctl/cli/swpins/core.rb +6 -4
  12. data/lib/confctl/generation/build.rb +42 -1
  13. data/lib/confctl/generation/build_list.rb +10 -0
  14. data/lib/confctl/generation/host.rb +9 -5
  15. data/lib/confctl/generation/host_list.rb +20 -5
  16. data/lib/confctl/generation/unified.rb +5 -0
  17. data/lib/confctl/generation/unified_list.rb +10 -0
  18. data/lib/confctl/machine.rb +4 -0
  19. data/lib/confctl/machine_control.rb +3 -2
  20. data/lib/confctl/machine_list.rb +4 -0
  21. data/lib/confctl/machine_status.rb +1 -1
  22. data/lib/confctl/nix.rb +63 -18
  23. data/lib/confctl/nix_copy.rb +5 -5
  24. data/lib/confctl/null_logger.rb +7 -0
  25. data/lib/confctl/swpins/change_set.rb +11 -4
  26. data/lib/confctl/swpins/specs/git.rb +23 -16
  27. data/lib/confctl/swpins/specs/git_rev.rb +1 -1
  28. data/lib/confctl/system_command.rb +3 -2
  29. data/lib/confctl/version.rb +1 -1
  30. data/libexec/auto-rollback.rb +106 -0
  31. data/man/man8/confctl.8 +109 -72
  32. data/man/man8/confctl.8.md +91 -54
  33. data/nix/evaluator.nix +26 -1
  34. data/nix/modules/cluster/default.nix +20 -0
  35. data/nix/modules/confctl/carrier/base.nix +8 -6
  36. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +209 -50
  37. data/nix/modules/confctl/carrier/netboot/nixos.nix +5 -3
  38. data/nix/modules/confctl/kexec-netboot/default.nix +38 -0
  39. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +62 -0
  40. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +455 -0
  41. data/nix/modules/confctl/overlays.nix +15 -0
  42. data/nix/modules/module-list.nix +1 -0
  43. data/nix/modules/system-list.nix +3 -1
  44. data/shell.nix +9 -2
  45. metadata +8 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 602260a8486eef31801f7fcf2221f09164e4443e66ea94d6fe534f8f68c56fc2
4
- data.tar.gz: 24b4b5dabe240f2e93e0a9784f9eda90bbf52cec2a82fab8e5c5644e323be8da
3
+ metadata.gz: bd99990731daa837f1243feb4a3b098320180badbbb9cc3846c105e7b747b794
4
+ data.tar.gz: 2ab400c25ea0bfc35f53713826b0e69aeff4667314048aaf6f8f56c942dda4e3
5
5
  SHA512:
6
- metadata.gz: 87f22fe62a9354b20e711fff429f0a34f6961f9c08489bd42f97ff1d8f9f350bc281e7eca069aecc25081ae71f2ea73f803aa06a0dc4c3d244adbfe10dbbaa86
7
- data.tar.gz: 8f44626b3b0e9a97b37171cbc58e94a15b04d87f448110ff211bfa5bc34477d1ce9a0d9f344b19cab93ed8d23f657e44fab3d14c8bad71e62b5d65ea2d3d3d26
6
+ metadata.gz: 7b2f85762c2c9c8173ffc38fd1fd6c37e2866ae8a6d5a4d1cbfc6d4cbbb8e8037dc9e9d40d0d8f9f12f7a0eae7140c4c52f435ea6e952327803024a6cd08720b
7
+ data.tar.gz: a3f87cfe12b26ef6c0a520030256a798418b85cee9dbedc2cd63e35b7625aa7e763c4f3d07f9e194826c6d3cffd5f12bc1a55d04201c6963067412a5010c4bad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ # Fri Jun 06 2025 -- version 2.2.0
2
+ - Handle `pkgs.substituteAll` / `pkgs.replaceVarsWith` compatibility on NixOS unstable
3
+ and 25.05
4
+ - Change swpin files only when revisions are updated, skip commit when no changes were made
5
+ - Add option `--[no-]editor` to `confctl swpins core/cluster/channel set/update` commands
6
+
7
+ # Sun May 11 2025 -- version 2.1.0
8
+ - Support for referring to generations by their offset
9
+ - Resolved generations are printed on build/deploy/etc.
10
+ - Automatically rollback faulty configurations
11
+ - Interleave copying of carried machine generations
12
+ - Add `confctl.programs.kexec-netboot`
13
+ - Let the user retry dry activation in interactive mode
14
+ - Fix listing, deletion and garbage collection of carried machines' generations
15
+ - Read and display kernel version for each generation
16
+ - Option to disable the garbage collection in `confctl generation rotate`
17
+ - Shorten titles and entries in netboot menus
18
+
1
19
  # Sun Nov 17 2024 -- version 2.0.0
2
20
  - Distinguish machine `config` and `metaConfig` (breaking change)
3
21
  - Support for machine carriers and netboot servers
@@ -13,7 +31,7 @@ or it could mean machine metadata from `module.nix`. Machine metadata
13
31
  is now accessible as `metaConfig`.
14
32
 
15
33
  - `confLib.findConfig` has been renamed to `confLib.findMetaConfig`
16
- - `confLib.confLib.getClusterMachines` returns a list of machines with `metaConfig` attribute
34
+ - `confLib.getClusterMachines` returns a list of machines with `metaConfig` attribute
17
35
 
18
36
  # Sat Feb 17 2024 -- version 1.0.0
19
37
  - Initial release
data/Gemfile CHANGED
@@ -1,2 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
+
4
+ group :development do
5
+ gem 'overcommit'
6
+ gem 'rubocop', '~> 1.75.0'
7
+ gem 'rubocop-rake'
8
+ end
data/README.md CHANGED
@@ -13,7 +13,8 @@ machines.
13
13
  configurations)
14
14
  * Query machine state, view changelogs and diffs
15
15
  * Run health checks
16
- * Support for creating netboot servers, see [docs/carrier.md](docs/carrier.md)
16
+ * Automatically roll back faulty configurations
17
+ * Support for creating netboot servers with option to kexec, see [docs/carrier.md](docs/carrier.md)
17
18
 
18
19
  ## Requirements
19
20
 
data/docs/carrier.md CHANGED
@@ -136,3 +136,15 @@ images are deployed to it:
136
136
  '';
137
137
  }
138
138
  ```
139
+
140
+ ## kexec from the netboot server
141
+ Set `confctl.programs.kexec-netboot.enable = true;` within your netbooted machine
142
+ configurations. This will add program `kexec-netboot` to your system path.
143
+
144
+ This program can be used to load kernel and initrd from the netboot server for kexec.
145
+ It automatically discovers the netboot server it was booted from, by default it uses
146
+ the latest generation found on the netboot server at the moment of execution. Use
147
+ `kexec-netboot --interactive` to select which machine/generation/variant should be
148
+ loaded. See `kexec-netboot --help` for all available options.
149
+
150
+ The loaded kernel can be run either by `kexec-netboot -e` or `kexec -e` directly.
@@ -11,6 +11,13 @@ module ConfCtl::Cli
11
11
  end
12
12
 
13
13
  def self.run
14
+ # Workaround for nix-build error:
15
+ # error: creating directory '/tmp/nix-shell-5100-0/nix-build-1986594-0': No such file or directory
16
+ if ENV['IN_NIX_SHELL']
17
+ ENV['TMP'] = '/tmp'
18
+ ENV['TMPDIR'] = '/tmp'
19
+ end
20
+
14
21
  cli = get
15
22
  exit(cli.run(ARGV))
16
23
  end
@@ -213,6 +220,12 @@ module ConfCtl::Cli
213
220
  c.desc 'Do not activate copied closures'
214
221
  c.switch 'copy-only', negatable: false
215
222
 
223
+ c.desc 'Enable auto-rollback'
224
+ c.switch 'enable-auto-rollback', default_value: false, negatable: false
225
+
226
+ c.desc 'Disable auto-rollback'
227
+ c.switch 'disable-auto-rollback', default_value: false, negatable: false
228
+
216
229
  c.desc 'Reboot target systems after deployment'
217
230
  c.switch :reboot
218
231
 
@@ -447,6 +460,9 @@ module ConfCtl::Cli
447
460
  c.desc 'List remote machine generations'
448
461
  c.switch %i[r remote]
449
462
 
463
+ c.desc 'Do not run the garbage collector if enabled in configuration'
464
+ c.switch %i[gc collect-garbage], default_value: true
465
+
450
466
  c.desc 'Max number of concurrent nix-collect-garbage processes'
451
467
  c.flag 'max-concurrent-gc', arg_name: 'n', type: Integer,
452
468
  default_value: 5
@@ -521,6 +537,9 @@ module ConfCtl::Cli
521
537
  c.desc 'Include changelog in the commit message'
522
538
  c.switch :changelog, default_value: true
523
539
 
540
+ c.desc 'Open $EDITOR with commit message'
541
+ c.switch :editor, default_value: true
542
+
524
543
  c.desc 'Generate changelog for downgrade'
525
544
  c.switch %i[d downgrade], default_value: false
526
545
 
@@ -333,7 +333,7 @@ module ConfCtl::Cli
333
333
 
334
334
  if opts[:interactive]
335
335
  host_generations.each do |host, gen|
336
- if copy_to_host(nix, host, machines[host], gen.toplevel) == :skip
336
+ if copy_to_host(nix, host, machines[host], gen) == :skip
337
337
  puts Rainbow("Skipping #{host}").yellow
338
338
  skipped_copy << host
339
339
  end
@@ -351,7 +351,7 @@ module ConfCtl::Cli
351
351
  next
352
352
  end
353
353
 
354
- if deploy_to_host(nix, host, machines[host], gen.toplevel, action) == :skip
354
+ if deploy_to_host(nix, host, machines[host], gen, action) == :skip
355
355
  puts Rainbow("Skipping #{host}").yellow
356
356
  skipped_activation << host
357
357
  next
@@ -396,14 +396,14 @@ module ConfCtl::Cli
396
396
  host_generations.each do |host, gen|
397
397
  machine = machines[host]
398
398
 
399
- if copy_to_host(nix, host, machine, gen.toplevel) == :skip
399
+ if copy_to_host(nix, host, machine, gen) == :skip
400
400
  puts Rainbow("Skipping #{host}").yellow
401
401
  next
402
402
  end
403
403
 
404
404
  next if opts['copy-only']
405
405
 
406
- if deploy_to_host(nix, host, machine, gen.toplevel, action) == :skip
406
+ if deploy_to_host(nix, host, machine, gen, action) == :skip
407
407
  puts Rainbow("Skipping #{host}").yellow
408
408
  next
409
409
  end
@@ -421,7 +421,7 @@ module ConfCtl::Cli
421
421
  end
422
422
  end
423
423
 
424
- def copy_to_host(nix, host, machine, toplevel)
424
+ def copy_to_host(nix, host, machine, build_generation)
425
425
  puts Rainbow("Copying configuration to #{host} (#{machine.target_host})").yellow
426
426
 
427
427
  return :skip if opts[:interactive] && !ask_confirmation(always: true)
@@ -435,7 +435,7 @@ module ConfCtl::Cli
435
435
  width: 80
436
436
  )
437
437
 
438
- ret = nix.copy(machine, toplevel) do |i, n, path|
438
+ ret = nix_copy(nix, machine, build_generation) do |i, n, path|
439
439
  lw << "[#{i}/#{n}] #{path}"
440
440
 
441
441
  lw.sync_console do
@@ -451,8 +451,10 @@ module ConfCtl::Cli
451
451
  end
452
452
 
453
453
  def concurrent_copy(machines, host_generations, nix)
454
+ sorted_generations = sort_generations_for_copy(machines, host_generations)
455
+
454
456
  LogView.open(
455
- header: "#{Rainbow("Copying to #{host_generations.length} machines").bright}\n",
457
+ header: "#{Rainbow("Copying to #{sorted_generations.length} machines").bright}\n",
456
458
  title: Rainbow('Live view').bright
457
459
  ) do |lw|
458
460
  multibar = TTY::ProgressBar::Multi.new(
@@ -461,13 +463,13 @@ module ConfCtl::Cli
461
463
  )
462
464
  executor = ConfCtl::ParallelExecutor.new(opts['max-concurrent-copy'])
463
465
 
464
- host_generations.each do |host, gen|
466
+ sorted_generations.each do |host, gen|
465
467
  pb = multibar.register(
466
468
  "#{host} [:bar] :current/:total (:percent)"
467
469
  )
468
470
 
469
471
  executor.add do
470
- ret = nix.copy(machines[host], gen.toplevel) do |i, n, path|
472
+ ret = nix_copy(nix, machines[host], gen) do |i, n, path|
471
473
  lw << "#{host}> [#{i}/#{n}] #{path}"
472
474
 
473
475
  lw.sync_console do
@@ -504,67 +506,136 @@ module ConfCtl::Cli
504
506
  end
505
507
  end
506
508
 
507
- def deploy_to_host(nix, host, machine, toplevel, action)
509
+ # Sort generations for copy to target machines
510
+ #
511
+ # This algorithm interleaves carried machines from different carriers,
512
+ # so that we copy to multiple carriers at the same time, and not many carried
513
+ # machines to one carrier.
514
+ def sort_generations_for_copy(machines, host_generations)
515
+ carried, standalone = machines.to_a.partition(&:carried?)
516
+
517
+ carried_groups = carried.group_by { |m| m.carrier_machine.name }
518
+
519
+ sorted_generations = standalone.map do |m|
520
+ [m.name, host_generations[m.name]]
521
+ end
522
+
523
+ until carried_groups.empty?
524
+ carried_groups.delete_if do |_, machines|
525
+ m = machines.shift
526
+ next(true) if m.nil?
527
+
528
+ sorted_generations << [m.name, host_generations[m.name]]
529
+
530
+ machines.empty?
531
+ end
532
+ end
533
+
534
+ if sorted_generations.length != host_generations.length
535
+ raise 'programming error: sorted generations length != original generations'
536
+ end
537
+
538
+ sorted_generations
539
+ end
540
+
541
+ def deploy_to_host(nix, host, machine, generation, action)
542
+ ret = nil
543
+
508
544
  LogView.open_with_logger(
509
545
  header: "#{Rainbow('Deploying to').bright} #{Rainbow(host).yellow}\n",
510
546
  title: Rainbow('Live view').bright,
511
547
  size: :auto,
512
548
  reserved_lines: 10
513
549
  ) do |lw|
514
- if machine.carried?
515
- deploy_carried_to_host(lw, nix, host, machine, toplevel, action)
516
- else
517
- deploy_standalone_to_host(lw, nix, host, machine, toplevel, action)
518
- end
550
+ ret =
551
+ if machine.carried?
552
+ deploy_carried_to_host(lw, nix, host, machine, generation, action)
553
+ else
554
+ deploy_standalone_to_host(lw, nix, host, machine, generation, action)
555
+ end
519
556
 
520
557
  lw.flush
521
558
  end
522
- end
523
559
 
524
- def deploy_standalone_to_host(lw, nix, host, machine, toplevel, action)
525
- if opts['dry-activate-first']
526
- lw.sync_console do
527
- puts Rainbow(
528
- "Trying to activate configuration on #{host} " \
529
- "(#{machine.target_host})"
530
- ).yellow
531
- end
532
-
533
- raise "Error while activating configuration on #{host}" unless nix.activate(machine, toplevel, 'dry-activate')
534
- end
535
-
536
- lw.sync_console do
537
- puts Rainbow(
538
- "Activating configuration on #{host} (#{machine.target_host}): " \
539
- "#{action}"
540
- ).yellow
541
- end
560
+ ret
561
+ end
542
562
 
543
- return :skip if opts[:interactive] && !ask_confirmation(always: true)
563
+ def deploy_standalone_to_host(lw, nix, host, machine, generation, action)
564
+ return :skip if pre_host_activate(lw, nix, host, machine, generation, action) == :skip
544
565
 
545
566
  # rubocop:disable Style/GuardClause
546
567
 
547
- unless nix.activate(machine, toplevel, action)
548
- raise "Error while activating configuration on #{host}"
549
- end
568
+ result =
569
+ if %w[test switch].include?(action) && enable_auto_rollback?(machine, generation)
570
+ nix.activate_with_rollback(machine, generation, action)
571
+ else
572
+ nix.activate(machine, generation, action)
573
+ end
574
+
575
+ raise "Error while activating configuration on #{host}" unless result
550
576
 
551
- if %w[boot switch].include?(action) && !nix.set_profile(machine, toplevel)
577
+ if %w[boot switch].include?(action) && !nix.set_profile(machine, generation.toplevel)
552
578
  raise "Error while setting profile on #{host}"
553
579
  end
554
580
 
555
581
  # rubocop:enable Style/GuardClause
556
582
  end
557
583
 
558
- def deploy_carried_to_host(lw, nix, host, machine, toplevel, action)
584
+ def deploy_carried_to_host(lw, nix, host, machine, generation, action)
559
585
  return if action != 'switch'
560
586
 
561
587
  # rubocop:disable Style/GuardClause
562
- unless nix.set_carried_profile(machine, toplevel)
588
+ unless nix.set_carried_profile(machine, generation.toplevel)
563
589
  raise "Error while setting carried profile for #{host} on #{machine.carrier_machine}"
564
590
  end
565
591
  # rubocop:enable Style/GuardClause
566
592
  end
567
593
 
594
+ def pre_host_activate(lw, nix, host, machine, generation, action)
595
+ loop do
596
+ if opts['dry-activate-first']
597
+ lw.sync_console do
598
+ puts Rainbow(
599
+ "Trying to activate configuration on #{host} " \
600
+ "(#{machine.target_host})"
601
+ ).yellow
602
+ end
603
+
604
+ unless nix.activate(machine, generation, 'dry-activate')
605
+ raise "Error while activating configuration on #{host}"
606
+ end
607
+ end
608
+
609
+ lw.sync_console do
610
+ puts Rainbow(
611
+ "Activating configuration on #{host} (#{machine.target_host}): " \
612
+ "#{action}"
613
+ ).yellow
614
+ end
615
+
616
+ return unless opts[:interactive]
617
+
618
+ options = {}
619
+ options['y'] = 'Continue'
620
+ options['r'] = 'Retry' if opts['dry-activate-first']
621
+ options['s'] = 'Skip'
622
+ options['a'] = 'Abort'
623
+
624
+ answer = ask_action(options:, default: nil)
625
+
626
+ case answer
627
+ when 'y'
628
+ return
629
+ when 'r'
630
+ next
631
+ when 's'
632
+ return :skip
633
+ when 'a'
634
+ raise 'Aborting'
635
+ end
636
+ end
637
+ end
638
+
568
639
  def reboot_host(host, machine)
569
640
  if machine.localhost?
570
641
  puts Rainbow("Skipping reboot of #{host} as it is localhost").yellow
@@ -896,12 +967,15 @@ module ConfCtl::Cli
896
967
  def find_generations(machines, generation_name)
897
968
  host_generations = {}
898
969
  missing_hosts = []
970
+ generation_offset = generation_name.to_i if /\A-?\d+\z/ =~ generation_name
899
971
 
900
972
  machines.each_key do |host|
901
973
  list = ConfCtl::Generation::BuildList.new(host)
902
974
 
903
975
  gen =
904
- if generation_name == 'current'
976
+ if generation_offset
977
+ list.at_offset(generation_offset)
978
+ elsif generation_name == 'current'
905
979
  list.current
906
980
  else
907
981
  list[generation_name]
@@ -916,8 +990,17 @@ module ConfCtl::Cli
916
990
 
917
991
  raise 'No generation found' if host_generations.empty?
918
992
 
993
+ puts 'Resolved host generations:'
994
+ list_generations(host_generations, missing_hosts:)
995
+
996
+ if opts[:interactive]
997
+ ask_confirmation!
998
+ else
999
+ puts
1000
+ end
1001
+
919
1002
  if missing_hosts.any?
920
- ask_confirmation! do
1003
+ ask_confirmation!(always: opts[:interactive]) do
921
1004
  puts "Generation '#{generation_name}' was not found on the following hosts:"
922
1005
  missing_hosts.each { |host| puts " #{host}" }
923
1006
  puts
@@ -928,22 +1011,59 @@ module ConfCtl::Cli
928
1011
  host_generations
929
1012
  end
930
1013
 
1014
+ def list_generations(host_generations, missing_hosts:)
1015
+ swpin_names = []
1016
+
1017
+ host_generations.each_value do |gen|
1018
+ gen.swpin_names.each do |name|
1019
+ swpin_names << name unless swpin_names.include?(name)
1020
+ end
1021
+ end
1022
+
1023
+ rows = host_generations.map do |host, gen|
1024
+ row = {
1025
+ 'name' => host,
1026
+ 'generation' => gen.name,
1027
+ 'kernel' => gen.kernel_version
1028
+ }
1029
+
1030
+ gen.swpin_specs.each do |name, spec|
1031
+ row[name] = spec.version
1032
+ end
1033
+
1034
+ row
1035
+ end
1036
+
1037
+ missing_hosts.each do |host|
1038
+ rows << { 'name' => host, 'generation' => 'not found' }
1039
+ end
1040
+
1041
+ OutputFormatter.print(
1042
+ rows,
1043
+ %w[name generation kernel] + swpin_names,
1044
+ layout: :columns,
1045
+ sort: %w[name generation]
1046
+ )
1047
+ end
1048
+
931
1049
  def do_build(machines)
932
1050
  nix = ConfCtl::Nix.new(
933
1051
  show_trace: opts['show-trace'],
934
1052
  max_jobs: opts['max-jobs'],
935
1053
  cores: opts['cores']
936
1054
  )
937
- hosts_swpin_paths = {}
938
1055
 
939
1056
  autoupdate_swpins(machines)
940
1057
  host_swpin_specs = check_swpins(machines)
941
1058
 
942
1059
  raise 'one or more swpins need to be updated' unless host_swpin_specs
943
1060
 
944
- machines.each do |host, d|
945
- puts Rainbow("Evaluating swpins for #{host}...").bright
946
- hosts_swpin_paths[host] = nix.eval_host_swpins(host).update(d.nix_paths)
1061
+ puts Rainbow("Evaluating swpins for #{machines.length} machines...").bright
1062
+
1063
+ hosts_swpin_paths = nix.eval_host_swpins(machines.map { |host, _| host })
1064
+
1065
+ machines.each do |host, m|
1066
+ hosts_swpin_paths[host].update(m.nix_paths)
947
1067
  end
948
1068
 
949
1069
  grps = swpin_build_groups(hosts_swpin_paths)
@@ -1273,5 +1393,21 @@ module ConfCtl::Cli
1273
1393
 
1274
1394
  raise ArgumentError, "invalid time duration '#{interval}'"
1275
1395
  end
1396
+
1397
+ # @param nix [Nix]
1398
+ # @param machine [Machine]
1399
+ # @param generation [Generation::Build]
1400
+ def nix_copy(nix, machine, generation, &)
1401
+ paths = [generation.toplevel]
1402
+ paths << generation.auto_rollback if enable_auto_rollback?(machine, generation)
1403
+
1404
+ nix.copy(machine, paths, &)
1405
+ end
1406
+
1407
+ def enable_auto_rollback?(machine, generation)
1408
+ !opts['disable-auto-rollback'] \
1409
+ && (opts['enable-auto-rollback'] || machine.auto_rollback?) \
1410
+ && generation.auto_rollback
1411
+ end
1276
1412
  end
1277
1413
  end
@@ -11,6 +11,7 @@ module ConfCtl::Cli
11
11
  def remove
12
12
  machines = select_machines(args[0])
13
13
  gens = select_generations(machines, args[1])
14
+ changed_hosts = []
14
15
 
15
16
  if gens.empty?
16
17
  puts 'No generations found'
@@ -27,21 +28,36 @@ module ConfCtl::Cli
27
28
  gens.each do |gen|
28
29
  puts "Removing #{gen.presence_str} generation #{gen.host}@#{gen.name}"
29
30
  gen.destroy
31
+
32
+ changed_hosts << gen.host unless changed_hosts.include?(gen.host)
30
33
  end
31
34
 
32
35
  return unless opts[:remote] && opts[:gc]
33
36
 
34
- machines_gc = machines.runnable.select do |host, _machine|
35
- gens.detect { |gen| gen.host == host }
37
+ machines_gc = {}
38
+
39
+ machines.each do |host, machine|
40
+ next unless changed_hosts.include?(host)
41
+
42
+ m =
43
+ if machine.carried?
44
+ machine.carrier_machine
45
+ else
46
+ machine
47
+ end
48
+
49
+ machines_gc[m.name] = m if m.target_host
36
50
  end
37
51
 
38
- run_gc(machines_gc)
52
+ run_gc(ConfCtl::MachineList.new(machines: machines_gc))
39
53
  end
40
54
 
41
55
  def rotate
42
56
  machines = select_machines(args[0])
43
57
 
44
58
  to_delete = []
59
+ changed_hosts = []
60
+ enable_gc = opts[:remote] && opts[:gc]
45
61
 
46
62
  to_delete.concat(host_generations_rotate(machines)) if opts[:remote]
47
63
 
@@ -56,29 +72,37 @@ module ConfCtl::Cli
56
72
  puts 'The following generations will be removed:'
57
73
  OutputFormatter.print(to_delete, %i[host name type id], layout: :columns)
58
74
  puts
59
- puts "Garbage collection: #{opts[:remote] ? 'when enabled in configuration' : 'no'}"
75
+ puts "Garbage collection: #{enable_gc ? 'when enabled in configuration' : 'no'}"
60
76
  end
61
77
 
62
78
  to_delete.each do |gen|
63
79
  puts "Removing #{gen[:type]} generation #{gen[:host]}@#{gen[:name]}"
64
80
  gen[:generation].destroy
81
+
82
+ changed_hosts << gen[:host] unless changed_hosts.include?(gen[:host])
65
83
  end
66
84
 
67
- return unless opts[:remote]
85
+ return unless enable_gc
68
86
 
69
87
  global = ConfCtl::Settings.instance.host_generations
88
+ machines_gc = {}
70
89
 
71
- machines_gc = machines.runnable.select do |_host, machine|
72
- gc = machine['buildGenerations']['collectGarbage']
90
+ machines.each do |host, machine|
91
+ next unless changed_hosts.include?(host)
73
92
 
74
- if gc.nil?
75
- global['collectGarbage']
76
- else
77
- gc
78
- end
93
+ m =
94
+ if machine.carried?
95
+ machine.carrier_machine
96
+ else
97
+ machine
98
+ end
99
+
100
+ next if !m.target_host || (!m['buildGenerations']['collectGarbage'] && !global['collectGarbage'])
101
+
102
+ machines_gc[m.name] = m
79
103
  end
80
104
 
81
- run_gc(machines_gc) if machines_gc.any?
105
+ run_gc(ConfCtl::MachineList.new(machines: machines_gc)) if machines_gc.any?
82
106
  end
83
107
 
84
108
  def collect_garbage
@@ -131,12 +155,16 @@ module ConfCtl::Cli
131
155
  select_older_than =
132
156
  (Time.now - (::Regexp.last_match(1).to_i * 24 * 60 * 60) if !select_old && /\A(\d+)d\Z/ =~ pattern)
133
157
 
158
+ gen_at = gens.at_offset(pattern.to_i) if /\A-?\d+\z/ =~ pattern
159
+
134
160
  if pattern
135
161
  gens.delete_if do |gen|
136
162
  if select_old
137
163
  gen.current
138
164
  elsif select_older_than
139
165
  gen.date >= select_older_than
166
+ elsif gen_at
167
+ gen != gen_at
140
168
  else
141
169
  !ConfCtl::Pattern.match?(pattern, gen.name)
142
170
  end
@@ -316,7 +344,8 @@ module ConfCtl::Cli
316
344
  'name' => gen.name,
317
345
  'id' => gen.id,
318
346
  'presence' => gen.presence_str,
319
- 'current' => gen.current_str
347
+ 'current' => gen.current_str,
348
+ 'kernel' => gen.kernel_version
320
349
  }
321
350
 
322
351
  gen.swpin_specs.each do |name, spec|
@@ -328,7 +357,7 @@ module ConfCtl::Cli
328
357
 
329
358
  OutputFormatter.print(
330
359
  rows,
331
- %w[host name id presence current] + swpin_names,
360
+ %w[host name id presence current kernel] + swpin_names,
332
361
  layout: :columns,
333
362
  sort: %w[name host]
334
363
  )
@@ -37,11 +37,12 @@ module ConfCtl::Cli
37
37
 
38
38
  channels.each(&:save)
39
39
 
40
- return unless opts[:commit]
40
+ return if !opts[:commit] || !change_set.any_changes?
41
41
 
42
42
  change_set.commit(
43
43
  type: opts[:downgrade] ? :downgrade : :upgrade,
44
- changelog: opts[:changelog]
44
+ changelog: opts[:changelog],
45
+ editor: opts[:editor]
45
46
  )
46
47
  end
47
48
 
@@ -62,11 +63,12 @@ module ConfCtl::Cli
62
63
 
63
64
  channels.each(&:save)
64
65
 
65
- return unless opts[:commit]
66
+ return if !opts[:commit] || !change_set.any_changes?
66
67
 
67
68
  change_set.commit(
68
69
  type: opts[:downgrade] ? :downgrade : :upgrade,
69
- changelog: opts[:changelog]
70
+ changelog: opts[:changelog],
71
+ editor: opts[:editor]
70
72
  )
71
73
  end
72
74
  end
@@ -42,11 +42,12 @@ module ConfCtl::Cli
42
42
 
43
43
  cluster_names.each(&:save)
44
44
 
45
- return unless opts[:commit]
45
+ return if !opts[:commit] || !change_set.any_changes?
46
46
 
47
47
  change_set.commit(
48
48
  type: opts[:downgrade] ? :downgrade : :upgrade,
49
- changelog: opts[:changelog]
49
+ changelog: opts[:changelog],
50
+ editor: opts[:editor]
50
51
  )
51
52
  end
52
53
 
@@ -69,11 +70,12 @@ module ConfCtl::Cli
69
70
 
70
71
  cluster_names.each(&:save)
71
72
 
72
- return unless opts[:commit]
73
+ return if !opts[:commit] || !change_set.any_changes?
73
74
 
74
75
  change_set.commit(
75
76
  type: opts[:downgrade] ? :downgrade : :upgrade,
76
- changelog: opts[:changelog]
77
+ changelog: opts[:changelog],
78
+ editor: opts[:editor]
77
79
  )
78
80
  end
79
81
  end