confctl 1.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +1 -1
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +30 -1
  6. data/README.md +4 -9
  7. data/confctl.gemspec +14 -14
  8. data/docs/carrier.md +150 -0
  9. data/lib/confctl/cli/app.rb +19 -0
  10. data/lib/confctl/cli/cluster.rb +214 -49
  11. data/lib/confctl/cli/configuration.rb +7 -2
  12. data/lib/confctl/cli/gen_data.rb +19 -1
  13. data/lib/confctl/cli/generation.rb +47 -16
  14. data/lib/confctl/generation/build.rb +42 -1
  15. data/lib/confctl/generation/build_list.rb +10 -0
  16. data/lib/confctl/generation/host.rb +9 -5
  17. data/lib/confctl/generation/host_list.rb +22 -7
  18. data/lib/confctl/generation/unified.rb +5 -0
  19. data/lib/confctl/generation/unified_list.rb +10 -0
  20. data/lib/confctl/git_repo_mirror.rb +2 -2
  21. data/lib/confctl/machine.rb +105 -11
  22. data/lib/confctl/machine_control.rb +10 -2
  23. data/lib/confctl/machine_list.rb +18 -1
  24. data/lib/confctl/machine_status.rb +51 -4
  25. data/lib/confctl/nix.rb +90 -22
  26. data/lib/confctl/nix_copy.rb +5 -5
  27. data/lib/confctl/null_logger.rb +7 -0
  28. data/lib/confctl/swpins/specs/git.rb +1 -1
  29. data/lib/confctl/swpins/specs/git_rev.rb +1 -1
  30. data/lib/confctl/system_command.rb +3 -2
  31. data/lib/confctl/version.rb +1 -1
  32. data/libexec/auto-rollback.rb +106 -0
  33. data/man/man8/confctl-options.nix.8 +165 -1
  34. data/man/man8/confctl-options.nix.8.md +165 -1
  35. data/man/man8/confctl.8 +109 -73
  36. data/man/man8/confctl.8.md +86 -55
  37. data/nix/evaluator.nix +26 -7
  38. data/nix/lib/default.nix +64 -17
  39. data/nix/lib/machine/default.nix +14 -11
  40. data/nix/lib/machine/info.nix +3 -3
  41. data/nix/modules/cluster/default.nix +162 -3
  42. data/nix/modules/confctl/carrier/base.nix +35 -0
  43. data/nix/modules/confctl/carrier/carrier-env.rb +81 -0
  44. data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +962 -0
  45. data/nix/modules/confctl/carrier/netboot/nixos.nix +185 -0
  46. data/nix/modules/confctl/kexec-netboot/default.nix +36 -0
  47. data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +62 -0
  48. data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +455 -0
  49. data/nix/modules/system-list.nix +10 -0
  50. metadata +17 -7
  51. data/.ruby-version +0 -1
@@ -110,7 +110,10 @@ module ConfCtl::Cli
110
110
  end
111
111
 
112
112
  def status
113
- machines = select_machines(args[0]).managed
113
+ machines = select_machines(args[0]).managed.select do |_host, machine|
114
+ machine.target_host || machine.carried?
115
+ end
116
+
114
117
  raise 'No machines to check' if machines.empty?
115
118
 
116
119
  ask_confirmation! do
@@ -244,7 +247,7 @@ module ConfCtl::Cli
244
247
  end
245
248
 
246
249
  def test_connection
247
- machines = select_machines_with_managed(args[0])
250
+ machines = select_machines_with_managed(args[0]).runnable
248
251
  raise 'No machines to test' if machines.empty?
249
252
 
250
253
  ask_confirmation! do
@@ -276,7 +279,7 @@ module ConfCtl::Cli
276
279
  end
277
280
 
278
281
  def ssh
279
- machines = select_machines_with_managed(args[0])
282
+ machines = select_machines_with_managed(args[0]).runnable
280
283
  raise 'No machines to ssh to' if machines.empty?
281
284
 
282
285
  if opts['input-string'] && opts['input-file']
@@ -295,7 +298,7 @@ module ConfCtl::Cli
295
298
  end
296
299
 
297
300
  def cssh
298
- machines = select_machines_with_managed(args[0])
301
+ machines = select_machines_with_managed(args[0]).runnable
299
302
  raise 'No machines to open cssh to' if machines.empty?
300
303
 
301
304
  ask_confirmation! do
@@ -330,7 +333,7 @@ module ConfCtl::Cli
330
333
 
331
334
  if opts[:interactive]
332
335
  host_generations.each do |host, gen|
333
- if copy_to_host(nix, host, machines[host], gen.toplevel) == :skip
336
+ if copy_to_host(nix, host, machines[host], gen) == :skip
334
337
  puts Rainbow("Skipping #{host}").yellow
335
338
  skipped_copy << host
336
339
  end
@@ -348,7 +351,7 @@ module ConfCtl::Cli
348
351
  next
349
352
  end
350
353
 
351
- if deploy_to_host(nix, host, machines[host], gen.toplevel, action) == :skip
354
+ if deploy_to_host(nix, host, machines[host], gen, action) == :skip
352
355
  puts Rainbow("Skipping #{host}").yellow
353
356
  skipped_activation << host
354
357
  next
@@ -393,14 +396,14 @@ module ConfCtl::Cli
393
396
  host_generations.each do |host, gen|
394
397
  machine = machines[host]
395
398
 
396
- if copy_to_host(nix, host, machine, gen.toplevel) == :skip
399
+ if copy_to_host(nix, host, machine, gen) == :skip
397
400
  puts Rainbow("Skipping #{host}").yellow
398
401
  next
399
402
  end
400
403
 
401
404
  next if opts['copy-only']
402
405
 
403
- if deploy_to_host(nix, host, machine, gen.toplevel, action) == :skip
406
+ if deploy_to_host(nix, host, machine, gen, action) == :skip
404
407
  puts Rainbow("Skipping #{host}").yellow
405
408
  next
406
409
  end
@@ -418,7 +421,7 @@ module ConfCtl::Cli
418
421
  end
419
422
  end
420
423
 
421
- def copy_to_host(nix, host, machine, toplevel)
424
+ def copy_to_host(nix, host, machine, build_generation)
422
425
  puts Rainbow("Copying configuration to #{host} (#{machine.target_host})").yellow
423
426
 
424
427
  return :skip if opts[:interactive] && !ask_confirmation(always: true)
@@ -432,7 +435,7 @@ module ConfCtl::Cli
432
435
  width: 80
433
436
  )
434
437
 
435
- ret = nix.copy(machine, toplevel) do |i, n, path|
438
+ ret = nix_copy(nix, machine, build_generation) do |i, n, path|
436
439
  lw << "[#{i}/#{n}] #{path}"
437
440
 
438
441
  lw.sync_console do
@@ -448,8 +451,10 @@ module ConfCtl::Cli
448
451
  end
449
452
 
450
453
  def concurrent_copy(machines, host_generations, nix)
454
+ sorted_generations = sort_generations_for_copy(machines, host_generations)
455
+
451
456
  LogView.open(
452
- header: "#{Rainbow("Copying to #{host_generations.length} machines").bright}\n",
457
+ header: "#{Rainbow("Copying to #{sorted_generations.length} machines").bright}\n",
453
458
  title: Rainbow('Live view').bright
454
459
  ) do |lw|
455
460
  multibar = TTY::ProgressBar::Multi.new(
@@ -458,13 +463,13 @@ module ConfCtl::Cli
458
463
  )
459
464
  executor = ConfCtl::ParallelExecutor.new(opts['max-concurrent-copy'])
460
465
 
461
- host_generations.each do |host, gen|
466
+ sorted_generations.each do |host, gen|
462
467
  pb = multibar.register(
463
468
  "#{host} [:bar] :current/:total (:percent)"
464
469
  )
465
470
 
466
471
  executor.add do
467
- ret = nix.copy(machines[host], gen.toplevel) do |i, n, path|
472
+ ret = nix_copy(nix, machines[host], gen) do |i, n, path|
468
473
  lw << "#{host}> [#{i}/#{n}] #{path}"
469
474
 
470
475
  lw.sync_console do
@@ -501,13 +506,93 @@ module ConfCtl::Cli
501
506
  end
502
507
  end
503
508
 
504
- 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
+
505
544
  LogView.open_with_logger(
506
545
  header: "#{Rainbow('Deploying to').bright} #{Rainbow(host).yellow}\n",
507
546
  title: Rainbow('Live view').bright,
508
547
  size: :auto,
509
548
  reserved_lines: 10
510
549
  ) do |lw|
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
556
+
557
+ lw.flush
558
+ end
559
+
560
+ ret
561
+ end
562
+
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
565
+
566
+ # rubocop:disable Style/GuardClause
567
+
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
576
+
577
+ if %w[boot switch].include?(action) && !nix.set_profile(machine, generation.toplevel)
578
+ raise "Error while setting profile on #{host}"
579
+ end
580
+
581
+ # rubocop:enable Style/GuardClause
582
+ end
583
+
584
+ def deploy_carried_to_host(lw, nix, host, machine, generation, action)
585
+ return if action != 'switch'
586
+
587
+ # rubocop:disable Style/GuardClause
588
+ unless nix.set_carried_profile(machine, generation.toplevel)
589
+ raise "Error while setting carried profile for #{host} on #{machine.carrier_machine}"
590
+ end
591
+ # rubocop:enable Style/GuardClause
592
+ end
593
+
594
+ def pre_host_activate(lw, nix, host, machine, generation, action)
595
+ loop do
511
596
  if opts['dry-activate-first']
512
597
  lw.sync_console do
513
598
  puts Rainbow(
@@ -516,7 +601,9 @@ module ConfCtl::Cli
516
601
  ).yellow
517
602
  end
518
603
 
519
- raise "Error while activating configuration on #{host}" unless nix.activate(machine, toplevel, 'dry-activate')
604
+ unless nix.activate(machine, generation, 'dry-activate')
605
+ raise "Error while activating configuration on #{host}"
606
+ end
520
607
  end
521
608
 
522
609
  lw.sync_console do
@@ -526,12 +613,25 @@ module ConfCtl::Cli
526
613
  ).yellow
527
614
  end
528
615
 
529
- return :skip if opts[:interactive] && !ask_confirmation(always: true)
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'
530
623
 
531
- raise "Error while activating configuration on #{host}" unless nix.activate(machine, toplevel, action)
624
+ answer = ask_action(options:, default: nil)
532
625
 
533
- if %w[boot switch].include?(action) && !nix.set_profile(machine, toplevel)
534
- raise "Error while setting profile on #{host}"
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'
535
635
  end
536
636
  end
537
637
  end
@@ -540,6 +640,9 @@ module ConfCtl::Cli
540
640
  if machine.localhost?
541
641
  puts Rainbow("Skipping reboot of #{host} as it is localhost").yellow
542
642
  return :skip
643
+ elsif machine.carried?
644
+ puts Rainbow("Skipping reboot of carried machine #{host}").yellow
645
+ return :skip
543
646
  end
544
647
 
545
648
  puts Rainbow("Rebooting #{host} (#{machine.target_host})").yellow
@@ -761,22 +864,16 @@ module ConfCtl::Cli
761
864
  machines.each do |host, machine|
762
865
  mc = ConfCtl::MachineControl.new(machine)
763
866
 
764
- begin
765
- puts "#{host}:" unless aggregate
867
+ puts "#{host}:" unless aggregate
766
868
 
767
- result = run_ssh_command_on_machine(mc, cmd)
869
+ result = run_ssh_command_on_machine(mc, cmd)
768
870
 
769
- if aggregate
770
- results[host] = result
771
- else
772
- puts result.out
773
- end
774
- rescue TTY::Command::ExitError => e
775
- if aggregate
776
- results[host] = e
777
- else
778
- puts e.message
779
- end
871
+ if aggregate
872
+ results[host] = result
873
+ elsif result.success?
874
+ puts result.out
875
+ else
876
+ puts result.err
780
877
  end
781
878
 
782
879
  puts unless aggregate
@@ -808,12 +905,8 @@ module ConfCtl::Cli
808
905
  tw.add do
809
906
  mc = ConfCtl::MachineControl.new(machine)
810
907
 
811
- begin
812
- result = run_ssh_command_on_machine(mc, cmd)
813
- results[host] = result
814
- rescue TTY::Command::ExitError => e
815
- results[host] = e
816
- end
908
+ result = run_ssh_command_on_machine(mc, cmd)
909
+ results[host] = result
817
910
 
818
911
  lw.sync_console { pb.advance }
819
912
  end
@@ -830,7 +923,13 @@ module ConfCtl::Cli
830
923
 
831
924
  results.each do |host, result|
832
925
  puts "#{host}:"
833
- puts result.out
926
+
927
+ if result.success?
928
+ puts result.out
929
+ else
930
+ puts result.err
931
+ end
932
+
834
933
  puts
835
934
  end
836
935
  end
@@ -844,7 +943,7 @@ module ConfCtl::Cli
844
943
  cmd_opts[:in] = opts['input-file']
845
944
  end
846
945
 
847
- mc.execute(*cmd, **cmd_opts)
946
+ mc.execute!(*cmd, **cmd_opts)
848
947
  end
849
948
 
850
949
  def process_aggregated_results(results)
@@ -868,12 +967,15 @@ module ConfCtl::Cli
868
967
  def find_generations(machines, generation_name)
869
968
  host_generations = {}
870
969
  missing_hosts = []
970
+ generation_offset = generation_name.to_i if /\A-?\d+\z/ =~ generation_name
871
971
 
872
972
  machines.each_key do |host|
873
973
  list = ConfCtl::Generation::BuildList.new(host)
874
974
 
875
975
  gen =
876
- if generation_name == 'current'
976
+ if generation_offset
977
+ list.at_offset(generation_offset)
978
+ elsif generation_name == 'current'
877
979
  list.current
878
980
  else
879
981
  list[generation_name]
@@ -888,8 +990,17 @@ module ConfCtl::Cli
888
990
 
889
991
  raise 'No generation found' if host_generations.empty?
890
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
+
891
1002
  if missing_hosts.any?
892
- ask_confirmation! do
1003
+ ask_confirmation!(always: opts[:interactive]) do
893
1004
  puts "Generation '#{generation_name}' was not found on the following hosts:"
894
1005
  missing_hosts.each { |host| puts " #{host}" }
895
1006
  puts
@@ -900,21 +1011,59 @@ module ConfCtl::Cli
900
1011
  host_generations
901
1012
  end
902
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
+
903
1049
  def do_build(machines)
904
1050
  nix = ConfCtl::Nix.new(
905
1051
  show_trace: opts['show-trace'],
906
- max_jobs: opts['max-jobs']
1052
+ max_jobs: opts['max-jobs'],
1053
+ cores: opts['cores']
907
1054
  )
908
- hosts_swpin_paths = {}
909
1055
 
910
1056
  autoupdate_swpins(machines)
911
1057
  host_swpin_specs = check_swpins(machines)
912
1058
 
913
1059
  raise 'one or more swpins need to be updated' unless host_swpin_specs
914
1060
 
915
- machines.each do |host, d|
916
- puts Rainbow("Evaluating swpins for #{host}...").bright
917
- 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)
918
1067
  end
919
1068
 
920
1069
  grps = swpin_build_groups(hosts_swpin_paths)
@@ -996,7 +1145,7 @@ module ConfCtl::Cli
996
1145
  'Fetching [:bar] :current/:total (:percent)'
997
1146
  )
998
1147
 
999
- built_generations = nix.build_toplevels(
1148
+ built_generations = nix.build_attributes(
1000
1149
  hosts:,
1001
1150
  swpin_paths:,
1002
1151
  time:,
@@ -1244,5 +1393,21 @@ module ConfCtl::Cli
1244
1393
 
1245
1394
  raise ArgumentError, "invalid time duration '#{interval}'"
1246
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
1247
1412
  end
1248
1413
  end
@@ -1,8 +1,11 @@
1
+ require_relative '../hook'
1
2
  require 'fileutils'
2
3
  require 'securerandom'
3
4
 
4
5
  module ConfCtl::Cli
5
6
  class Configuration < Command
7
+ ConfCtl::Hook.register :configuration_rediscover
8
+
6
9
  DIR_MODE = 0o755
7
10
  FILE_MODE = 0o644
8
11
 
@@ -238,6 +241,8 @@ module ConfCtl::Cli
238
241
 
239
242
  f.puts(']')
240
243
  end
244
+
245
+ ConfCtl::Hook.call(:configuration_rediscover)
241
246
  end
242
247
 
243
248
  protected
@@ -251,8 +256,8 @@ module ConfCtl::Cli
251
256
 
252
257
  entry_rel_path = File.join(*[rel_path, v].compact)
253
258
 
254
- if File.exist?(File.join(entry_abs_path, 'module.nix')) \
255
- && File.exist?(File.join(entry_abs_path, 'config.nix'))
259
+ if File.exist?(File.join(entry_abs_path, 'module.nix')) &&
260
+ File.exist?(File.join(entry_abs_path, 'config.nix'))
256
261
  ret << entry_rel_path
257
262
  end
258
263
 
@@ -78,10 +78,28 @@ module ConfCtl::Cli
78
78
  q.echo = false
79
79
  end.to_s
80
80
 
81
- @vpsadmin_client.authenticate(:basic, user:, password:)
81
+ @vpsadmin_client.authenticate(:token, user:, password:, lifetime: 'fixed', interval: 60) do |_action, params|
82
+ ret = {}
83
+
84
+ params.each do |name, desc|
85
+ ret[name] = read_auth_param(name, desc)
86
+ end
87
+
88
+ ret
89
+ end
90
+
82
91
  @vpsadmin_client
83
92
  end
84
93
 
94
+ def read_auth_param(name, p)
95
+ prompt = "#{p[:label] || name}: "
96
+
97
+ ask(prompt) do |q|
98
+ q.default = nil
99
+ q.echo = !p[:protected]
100
+ end
101
+ end
102
+
85
103
  def update_file(relpath, &)
86
104
  abs = File.join(data_dir, relpath)
87
105
  tmp = "#{abs}.new"
@@ -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.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,33 +72,41 @@ 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 = {}
89
+
90
+ machines.each do |host, machine|
91
+ next unless changed_hosts.include?(host)
70
92
 
71
- machines_gc = machines.select do |_host, machine|
72
- gc = machine['buildGenerations']['collectGarbage']
93
+ m =
94
+ if machine.carried?
95
+ machine.carrier_machine
96
+ else
97
+ machine
98
+ end
73
99
 
74
- if gc.nil?
75
- global['collectGarbage']
76
- else
77
- gc
78
- end
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
85
- machines = select_machines(args[0])
109
+ machines = select_machines(args[0]).runnable
86
110
 
87
111
  raise 'No machines to collect garbage on' if machines.empty?
88
112
 
@@ -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
@@ -295,6 +323,8 @@ module ConfCtl::Cli
295
323
  retvals = executor.run
296
324
  failed = retvals.compact
297
325
 
326
+ lw.flush
327
+
298
328
  raise "Gargabe collection failed on: #{failed.join(', ')}" if failed.any?
299
329
  end
300
330
  end
@@ -314,7 +344,8 @@ module ConfCtl::Cli
314
344
  'name' => gen.name,
315
345
  'id' => gen.id,
316
346
  'presence' => gen.presence_str,
317
- 'current' => gen.current_str
347
+ 'current' => gen.current_str,
348
+ 'kernel' => gen.kernel_version
318
349
  }
319
350
 
320
351
  gen.swpin_specs.each do |name, spec|
@@ -326,7 +357,7 @@ module ConfCtl::Cli
326
357
 
327
358
  OutputFormatter.print(
328
359
  rows,
329
- %w[host name id presence current] + swpin_names,
360
+ %w[host name id presence current kernel] + swpin_names,
330
361
  layout: :columns,
331
362
  sort: %w[name host]
332
363
  )