confctl 1.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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +11 -0
  3. data/.gitignore +8 -0
  4. data/.overcommit.yml +6 -0
  5. data/.rubocop.yml +67 -0
  6. data/.rubocop_todo.yml +5 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +2 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE.txt +674 -0
  11. data/README.md +522 -0
  12. data/Rakefile +40 -0
  13. data/bin/confctl +4 -0
  14. data/confctl.gemspec +33 -0
  15. data/example/.gitignore +2 -0
  16. data/example/README.md +38 -0
  17. data/example/cluster/cluster.nix +7 -0
  18. data/example/cluster/module-list.nix +3 -0
  19. data/example/cluster/nixos-machine/config.nix +15 -0
  20. data/example/cluster/nixos-machine/hardware.nix +4 -0
  21. data/example/cluster/nixos-machine/module.nix +8 -0
  22. data/example/cluster/vpsadminos-container/config.nix +22 -0
  23. data/example/cluster/vpsadminos-container/module.nix +8 -0
  24. data/example/cluster/vpsadminos-machine/config.nix +22 -0
  25. data/example/cluster/vpsadminos-machine/hardware.nix +4 -0
  26. data/example/cluster/vpsadminos-machine/module.nix +8 -0
  27. data/example/cluster/vpsfreecz-vps/config.nix +25 -0
  28. data/example/cluster/vpsfreecz-vps/module.nix +8 -0
  29. data/example/configs/confctl.nix +10 -0
  30. data/example/configs/swpins.nix +28 -0
  31. data/example/data/default.nix +5 -0
  32. data/example/data/ssh-keys.nix +7 -0
  33. data/example/environments/base.nix +13 -0
  34. data/example/modules/module-list.nix +13 -0
  35. data/example/shell.nix +11 -0
  36. data/example/swpins/channels/nixos-unstable.json +35 -0
  37. data/example/swpins/channels/vpsadminos-staging.json +35 -0
  38. data/lib/confctl/cli/app.rb +551 -0
  39. data/lib/confctl/cli/attr_filters.rb +51 -0
  40. data/lib/confctl/cli/cluster.rb +1248 -0
  41. data/lib/confctl/cli/command.rb +206 -0
  42. data/lib/confctl/cli/configuration.rb +296 -0
  43. data/lib/confctl/cli/gen_data.rb +97 -0
  44. data/lib/confctl/cli/generation.rb +335 -0
  45. data/lib/confctl/cli/log_view.rb +267 -0
  46. data/lib/confctl/cli/output_formatter.rb +288 -0
  47. data/lib/confctl/cli/swpins/base.rb +40 -0
  48. data/lib/confctl/cli/swpins/channel.rb +73 -0
  49. data/lib/confctl/cli/swpins/cluster.rb +80 -0
  50. data/lib/confctl/cli/swpins/core.rb +86 -0
  51. data/lib/confctl/cli/swpins/utils.rb +55 -0
  52. data/lib/confctl/cli/swpins.rb +5 -0
  53. data/lib/confctl/cli/tag_filters.rb +30 -0
  54. data/lib/confctl/cli.rb +5 -0
  55. data/lib/confctl/conf_cache.rb +105 -0
  56. data/lib/confctl/conf_dir.rb +88 -0
  57. data/lib/confctl/erb_template.rb +37 -0
  58. data/lib/confctl/exceptions.rb +3 -0
  59. data/lib/confctl/gcroot.rb +30 -0
  60. data/lib/confctl/generation/build.rb +145 -0
  61. data/lib/confctl/generation/build_list.rb +106 -0
  62. data/lib/confctl/generation/host.rb +35 -0
  63. data/lib/confctl/generation/host_list.rb +81 -0
  64. data/lib/confctl/generation/unified.rb +117 -0
  65. data/lib/confctl/generation/unified_list.rb +63 -0
  66. data/lib/confctl/git_repo_mirror.rb +79 -0
  67. data/lib/confctl/health_checks/base.rb +66 -0
  68. data/lib/confctl/health_checks/run_command.rb +179 -0
  69. data/lib/confctl/health_checks/systemd/properties.rb +84 -0
  70. data/lib/confctl/health_checks/systemd/property_check.rb +31 -0
  71. data/lib/confctl/health_checks/systemd/property_list.rb +20 -0
  72. data/lib/confctl/health_checks.rb +5 -0
  73. data/lib/confctl/hook.rb +35 -0
  74. data/lib/confctl/line_buffer.rb +53 -0
  75. data/lib/confctl/logger.rb +151 -0
  76. data/lib/confctl/machine.rb +107 -0
  77. data/lib/confctl/machine_control.rb +172 -0
  78. data/lib/confctl/machine_list.rb +108 -0
  79. data/lib/confctl/machine_status.rb +135 -0
  80. data/lib/confctl/module_options.rb +95 -0
  81. data/lib/confctl/nix.rb +382 -0
  82. data/lib/confctl/nix_build.rb +108 -0
  83. data/lib/confctl/nix_collect_garbage.rb +64 -0
  84. data/lib/confctl/nix_copy.rb +49 -0
  85. data/lib/confctl/nix_format.rb +124 -0
  86. data/lib/confctl/nix_literal_expression.rb +15 -0
  87. data/lib/confctl/parallel_executor.rb +43 -0
  88. data/lib/confctl/pattern.rb +9 -0
  89. data/lib/confctl/settings.rb +50 -0
  90. data/lib/confctl/std_line_buffer.rb +40 -0
  91. data/lib/confctl/swpins/change_set.rb +151 -0
  92. data/lib/confctl/swpins/channel.rb +62 -0
  93. data/lib/confctl/swpins/channel_list.rb +47 -0
  94. data/lib/confctl/swpins/cluster_name.rb +94 -0
  95. data/lib/confctl/swpins/cluster_name_list.rb +15 -0
  96. data/lib/confctl/swpins/core.rb +137 -0
  97. data/lib/confctl/swpins/deployed_info.rb +23 -0
  98. data/lib/confctl/swpins/spec.rb +20 -0
  99. data/lib/confctl/swpins/specs/base.rb +184 -0
  100. data/lib/confctl/swpins/specs/directory.rb +51 -0
  101. data/lib/confctl/swpins/specs/git.rb +135 -0
  102. data/lib/confctl/swpins/specs/git_rev.rb +24 -0
  103. data/lib/confctl/swpins.rb +17 -0
  104. data/lib/confctl/system_command.rb +10 -0
  105. data/lib/confctl/user_script.rb +13 -0
  106. data/lib/confctl/user_scripts.rb +41 -0
  107. data/lib/confctl/utils/file.rb +21 -0
  108. data/lib/confctl/version.rb +3 -0
  109. data/lib/confctl.rb +43 -0
  110. data/man/man8/confctl-options.nix.8 +1334 -0
  111. data/man/man8/confctl-options.nix.8.md +1340 -0
  112. data/man/man8/confctl.8 +660 -0
  113. data/man/man8/confctl.8.md +654 -0
  114. data/nix/evaluator.nix +160 -0
  115. data/nix/lib/default.nix +83 -0
  116. data/nix/lib/machine/default.nix +74 -0
  117. data/nix/lib/machine/info.nix +5 -0
  118. data/nix/lib/swpins/eval.nix +71 -0
  119. data/nix/lib/swpins/options.nix +94 -0
  120. data/nix/machines.nix +31 -0
  121. data/nix/modules/cluster/default.nix +459 -0
  122. data/nix/modules/confctl/cli.nix +21 -0
  123. data/nix/modules/confctl/generations.nix +84 -0
  124. data/nix/modules/confctl/nix.nix +28 -0
  125. data/nix/modules/confctl/swpins.nix +55 -0
  126. data/nix/modules/module-list.nix +19 -0
  127. data/shell.nix +42 -0
  128. data/template/confctl-options.nix/main.erb +45 -0
  129. data/template/confctl-options.nix/options.erb +15 -0
  130. metadata +353 -0
@@ -0,0 +1,95 @@
1
+ require 'cgi'
2
+
3
+ module ConfCtl
4
+ class ModuleOptions
5
+ class Option
6
+ attr_reader :name, :description, :type, :default, :example, :declarations
7
+
8
+ def initialize(nixos_opt)
9
+ @name = nixos_opt['name']
10
+ @description = nixos_opt['description'] || 'This option has no description.'
11
+ @type = nixos_opt['type']
12
+ @default = extract_expression(nixos_opt['default'])
13
+ @example = extract_expression(nixos_opt['example'])
14
+ @declarations = nixos_opt['declarations'].map do |v|
15
+ raise "unable to place module '#{v}'" unless %r{/confctl/([^$]+)} =~ v
16
+
17
+ "<confctl/#{::Regexp.last_match(1)}>"
18
+ end
19
+ end
20
+
21
+ def md_description
22
+ tagless = description
23
+ .gsub(%r{<literal>([^<]+)</literal>}, '`\1`')
24
+ .gsub(%r{<option>([^<]+)</option>}, '`\1`')
25
+
26
+ CGI.unescapeHTML(tagless)
27
+ end
28
+
29
+ def nix_default
30
+ nixify(default)
31
+ end
32
+
33
+ def nix_example
34
+ example && nixify(example)
35
+ end
36
+
37
+ protected
38
+
39
+ def extract_expression(v)
40
+ if v.is_a?(Hash)
41
+ case v['_type']
42
+ when 'literalExpression'
43
+ NixLiteralExpression.new(v['text'])
44
+ else
45
+ raise "Unsupported expression type #{v['_type'].inspect}"
46
+ end
47
+ else
48
+ v
49
+ end
50
+ end
51
+
52
+ def nixify(v)
53
+ ConfCtl::NixFormat.to_nix(v)
54
+ end
55
+ end
56
+
57
+ # @return [Array<ModuleOptions::Option>]
58
+ attr_reader :options
59
+
60
+ # @param nix [Nix, nil]
61
+ def initialize(nix: nil)
62
+ @nix = nix || Nix.new
63
+ @options = []
64
+ end
65
+
66
+ def read
67
+ @options = nix.module_options.map do |opt|
68
+ Option.new(opt)
69
+ end
70
+ end
71
+
72
+ def confctl_settings
73
+ options.select do |opt|
74
+ opt.name.start_with?('confctl.') \
75
+ && !opt.name.start_with?('confctl.swpins.')
76
+ end
77
+ end
78
+
79
+ def swpin_settings
80
+ options.select { |opt| opt.name.start_with?('confctl.swpins.') }
81
+ end
82
+
83
+ def machine_settings
84
+ options.select { |opt| opt.name.start_with?('cluster.') }
85
+ end
86
+
87
+ def service_settings
88
+ options.select { |opt| opt.name.start_with?('services.') }
89
+ end
90
+
91
+ protected
92
+
93
+ attr_reader :nix
94
+ end
95
+ end
@@ -0,0 +1,382 @@
1
+ require 'confctl/utils/file'
2
+ require 'etc'
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'tempfile'
6
+
7
+ module ConfCtl
8
+ class Nix
9
+ # Create a new instance without access to {ConfCtl::Settings}, i.e. when
10
+ # called outside of cluster configuration directory.
11
+ # @return [Nix]
12
+ def self.stateless(show_trace: false, max_jobs: 'auto')
13
+ new(show_trace:, max_jobs:)
14
+ end
15
+
16
+ include Utils::File
17
+
18
+ def initialize(conf_dir: nil, show_trace: false, max_jobs: nil)
19
+ @conf_dir = conf_dir || ConfDir.path
20
+ @show_trace = show_trace
21
+ @max_jobs = max_jobs || Settings.instance.max_jobs
22
+ @cmd = SystemCommand.new
23
+ end
24
+
25
+ def confctl_settings
26
+ out_link = File.join(
27
+ cache_dir,
28
+ 'build',
29
+ 'settings.json'
30
+ )
31
+
32
+ with_cache(out_link) do
33
+ with_argument({
34
+ confDir: conf_dir,
35
+ build: :confctl
36
+ }) do |arg|
37
+ cmd_args = [
38
+ 'nix-build',
39
+ '--arg', 'jsonArg', arg,
40
+ '--out-link', out_link,
41
+ (show_trace ? '--show-trace' : nil),
42
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
43
+ ConfCtl.nix_asset('evaluator.nix')
44
+ ].flatten.compact
45
+
46
+ cmd.run(*cmd_args)
47
+ end
48
+ end
49
+
50
+ demodulify(JSON.parse(File.read(out_link))['confctl'])
51
+ end
52
+
53
+ # Returns an array with options from all confctl modules
54
+ # @return [Array]
55
+ def module_options
56
+ options = nix_instantiate({
57
+ confDir: conf_dir,
58
+ build: :moduleOptions
59
+ })
60
+ end
61
+
62
+ # Returns an array with machine fqdns
63
+ # @return [Array<String>]
64
+ def list_machine_fqdns
65
+ nix_instantiate({
66
+ confDir: conf_dir,
67
+ build: :list
68
+ })['machines']
69
+ end
70
+
71
+ # Return machines and their config in a hash
72
+ # @return [Hash]
73
+ def list_machines
74
+ out_link = File.join(
75
+ cache_dir,
76
+ 'build',
77
+ 'machine-list.json'
78
+ )
79
+
80
+ with_cache(out_link) do
81
+ with_argument({
82
+ confDir: conf_dir,
83
+ build: :info
84
+ }, core_swpins: true) do |arg|
85
+ cmd_args = [
86
+ 'nix-build',
87
+ '--arg', 'jsonArg', arg,
88
+ '--out-link', out_link,
89
+ (show_trace ? '--show-trace' : nil),
90
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
91
+ ConfCtl.nix_asset('evaluator.nix')
92
+ ].flatten.compact
93
+
94
+ cmd.run(*cmd_args)
95
+ end
96
+ end
97
+
98
+ demodulify(JSON.parse(File.read(out_link)))
99
+ end
100
+
101
+ def list_swpins_channels
102
+ nix_instantiate({
103
+ confDir: conf_dir,
104
+ build: :listSwpinsChannels
105
+ })
106
+ end
107
+
108
+ # Evaluate swpins for host
109
+ # @return [Hash]
110
+ def eval_core_swpins
111
+ with_argument({
112
+ confDir: conf_dir,
113
+ build: :evalCoreSwpins
114
+ }) do |arg|
115
+ out_link = File.join(
116
+ cache_dir,
117
+ 'build',
118
+ 'core.swpins'
119
+ )
120
+
121
+ cmd_args = [
122
+ 'nix-build',
123
+ '--arg', 'jsonArg', arg,
124
+ '--out-link', out_link,
125
+ (show_trace ? '--show-trace' : nil),
126
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
127
+ ConfCtl.nix_asset('evaluator.nix')
128
+ ].flatten.compact
129
+
130
+ out, = cmd.run(*cmd_args).stdout
131
+
132
+ JSON.parse(File.read(out.strip))
133
+ end
134
+ end
135
+
136
+ # Evaluate swpins for host
137
+ # @param host [String]
138
+ # @return [Hash]
139
+ def eval_host_swpins(host)
140
+ with_argument({
141
+ confDir: conf_dir,
142
+ build: :evalHostSwpins,
143
+ machines: [host]
144
+ }, core_swpins: true) do |arg|
145
+ out_link = File.join(
146
+ cache_dir,
147
+ 'build',
148
+ "#{ConfCtl.safe_host_name(host)}.swpins"
149
+ )
150
+
151
+ cmd_args = [
152
+ 'nix-build',
153
+ '--arg', 'jsonArg', arg,
154
+ '--out-link', out_link,
155
+ (show_trace ? '--show-trace' : nil),
156
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
157
+ ConfCtl.nix_asset('evaluator.nix')
158
+ ].flatten.compact
159
+
160
+ out, = cmd.run(*cmd_args)
161
+
162
+ JSON.parse(File.read(out.strip))[host]
163
+ end
164
+ end
165
+
166
+ # Build config.system.build.toplevel for selected hosts
167
+ #
168
+ # @param hosts [Array<String>]
169
+ # @param swpin_paths [Hash]
170
+ # @param host_swpin_specs [Hash]
171
+ # @param time [Time]
172
+ #
173
+ # @yieldparam type [:build, :fetch]
174
+ # @yieldparam progress [Integer]
175
+ # @yieldparam total [Integer]
176
+ # @yieldparam path [String]
177
+ #
178
+ # @return [Hash<String, Generation::Build>]
179
+ def build_toplevels(hosts: [], swpin_paths: {}, host_swpin_specs: {}, time: nil, &block)
180
+ with_argument({
181
+ confDir: conf_dir,
182
+ build: :toplevel,
183
+ machines: hosts
184
+ }) do |arg|
185
+ time ||= Time.now
186
+ ret_generations = {}
187
+ out_link = File.join(cache_dir, 'build', "#{SecureRandom.hex(4)}.build")
188
+
189
+ cmd_args = [
190
+ '--arg', 'jsonArg', arg,
191
+ '--out-link', out_link,
192
+ (show_trace ? '--show-trace' : nil),
193
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
194
+ ConfCtl.nix_asset('evaluator.nix')
195
+ ].flatten.compact
196
+
197
+ nb = NixBuild.new(cmd_args, swpin_paths)
198
+ nb.run(&block)
199
+
200
+ begin
201
+ host_toplevels = JSON.parse(File.read(out_link))
202
+ host_toplevels.each do |host, toplevel|
203
+ host_generations = Generation::BuildList.new(host)
204
+ generation = host_generations.find(toplevel, swpin_paths)
205
+
206
+ if generation.nil?
207
+ generation = Generation::Build.new(host)
208
+ generation.create(
209
+ toplevel,
210
+ swpin_paths,
211
+ host_swpin_specs[host],
212
+ date: time
213
+ )
214
+ generation.save
215
+ end
216
+
217
+ host_generations.current = generation
218
+ ret_generations[host] = generation
219
+ end
220
+ ensure
221
+ unlink_if_exists(out_link)
222
+ end
223
+
224
+ ret_generations
225
+ end
226
+ end
227
+
228
+ # @param machine [Machine]
229
+ # @param toplevel [String]
230
+ #
231
+ # @yieldparam progress [Integer]
232
+ # @yieldparam total [Integer]
233
+ # @yieldparam path [String]
234
+ #
235
+ # @return [Boolean]
236
+ def copy(machine, toplevel, &)
237
+ if machine.localhost?
238
+ true
239
+ else
240
+ cp = NixCopy.new(machine.target_host, toplevel)
241
+ cp.run!(&).success?
242
+ end
243
+ end
244
+
245
+ # @param machine [Machine]
246
+ # @param toplevel [String]
247
+ # @param action [String]
248
+ # @return [Boolean]
249
+ def activate(machine, toplevel, action)
250
+ args = [File.join(toplevel, 'bin/switch-to-configuration'), action]
251
+
252
+ MachineControl.new(machine).execute!(*args).success?
253
+ end
254
+
255
+ # @param machine [Machine]
256
+ # @param toplevel [String]
257
+ # @return [Boolean]
258
+ def set_profile(machine, toplevel)
259
+ args = [
260
+ 'nix-env',
261
+ '-p', '/nix/var/nix/profiles/system',
262
+ '--set', toplevel
263
+ ]
264
+
265
+ MachineControl.new(machine).execute!(*args).success?
266
+ end
267
+
268
+ # @param packages [Array<String>]
269
+ # @param command [String]
270
+ # @return [Boolean]
271
+ def run_command_in_shell(packages: [], command: nil)
272
+ args = ['nix-shell']
273
+
274
+ if packages.any?
275
+ args << '-p'
276
+ args.concat(packages)
277
+ end
278
+
279
+ args << '--command'
280
+ args << command
281
+
282
+ cmd.run!(*args, env: { 'shellHook' => nil }).success?
283
+ end
284
+
285
+ # @param machine [Machine]
286
+ # @yieldparam progress [NixCollectGarbage::Progress]
287
+ # @return [Boolean]
288
+ def collect_garbage(machine, &)
289
+ gc = NixCollectGarbage.new(machine)
290
+ gc.run!(&).success?
291
+ end
292
+
293
+ protected
294
+
295
+ attr_reader :conf_dir, :show_trace, :max_jobs, :cmd
296
+
297
+ # Execute block only if `out_link` does not exist or conf dir has changed
298
+ # @param out_link [String] out link path
299
+ def with_cache(out_link)
300
+ unchanged = false
301
+
302
+ if File.exist?(out_link)
303
+ unchanged = ConfDir.unchanged?
304
+
305
+ if unchanged
306
+ cache_mtime = ConfDir.state_mtime
307
+
308
+ if cache_mtime
309
+ begin
310
+ st = File.lstat(out_link)
311
+ rescue Errno::ENOENT
312
+ # pass
313
+ else
314
+ if st.mtime > cache_mtime
315
+ Logger.instance << "Using #{out_link}\n"
316
+ return
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ Logger.instance << "Building #{out_link}\n"
324
+
325
+ unless unchanged
326
+ Logger.instance << "Updating configuration cache\n"
327
+ ConfDir.update_state
328
+ end
329
+ yield
330
+ end
331
+
332
+ # @param hash [Hash]
333
+ # @param core_swpins [Boolean]
334
+ # @yieldparam file [String]
335
+ def with_argument(hash, core_swpins: false)
336
+ if core_swpins
337
+ paths = Swpins::Core.get.pre_evaluated_store_paths
338
+ hash[:coreSwpins] = paths if paths
339
+ end
340
+
341
+ f = Tempfile.new('confctl')
342
+ f.puts(hash.to_json)
343
+ f.close
344
+ yield(f.path)
345
+ ensure
346
+ f.unlink
347
+ end
348
+
349
+ def nix_instantiate(hash, **opts)
350
+ with_argument(hash, **opts) do |arg|
351
+ cmd_args = [
352
+ 'nix-instantiate',
353
+ '--eval',
354
+ '--json',
355
+ '--strict',
356
+ '--read-write-mode',
357
+ '--arg', 'jsonArg', arg,
358
+ (show_trace ? '--show-trace' : nil),
359
+ (max_jobs ? ['--max-jobs', max_jobs.to_s] : nil),
360
+ ConfCtl.nix_asset('evaluator.nix')
361
+ ].flatten.compact
362
+
363
+ out, = cmd.run(*cmd_args).stdout
364
+
365
+ demodulify(JSON.parse(out))
366
+ end
367
+ end
368
+
369
+ def demodulify(value)
370
+ if value.is_a?(Array)
371
+ value.each { |item| demodulify(item) }
372
+ elsif value.is_a?(Hash)
373
+ value.delete('_module')
374
+ value.each_value { |v| demodulify(v) }
375
+ end
376
+ end
377
+
378
+ def cache_dir
379
+ ConfDir.cache_dir
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,108 @@
1
+ module ConfCtl
2
+ class NixBuild
3
+ # @param args [Array<String>]
4
+ # @param swpin_paths [Hash<String>]
5
+ def initialize(args, swpin_paths)
6
+ @args = args
7
+ @swpin_paths = swpin_paths
8
+ @build_progress = 0
9
+ @build_total = 0
10
+ @fetch_progress = 0
11
+ @fetch_total = 0
12
+ end
13
+
14
+ # @yieldparam type [:build, :fetch]
15
+ # @yieldparam progress [Integer]
16
+ # @yieldparam total [Integer]
17
+ # @yieldparam path [String]
18
+ def run(&block)
19
+ cmd = SystemCommand.new
20
+
21
+ line_buf = StdLineBuffer.new do |_out, err|
22
+ parse_line(err, &block) if err && block
23
+ end
24
+
25
+ ret = cmd.run(
26
+ 'nix-build',
27
+ *args,
28
+ env: { 'NIX_PATH' => build_nix_path(swpin_paths) },
29
+ &line_buf.feed_block
30
+ )
31
+
32
+ line_buf.flush
33
+ ret
34
+ end
35
+
36
+ protected
37
+
38
+ attr_reader :args, :swpin_paths
39
+
40
+ def parse_line(line)
41
+ # Beware that nix-build can fetch/build stuff even before those two
42
+ # summary lines are printed. Therefore we report progress with total=0
43
+ # (indeterminate) until the total becomes known.
44
+
45
+ # Nix >= around 2.11
46
+ case line
47
+ when /^this derivation will be built:/
48
+ @build_total = 1
49
+ return
50
+ when /^these (\d+) derivations will be built:/
51
+ @build_total = ::Regexp.last_match(1).to_i
52
+ return
53
+ when /^this path will be fetched /
54
+ @fetch_total = 1
55
+ return
56
+ when /^these (\d+) paths will be fetched /
57
+ @fetch_total = ::Regexp.last_match(1).to_i
58
+ return
59
+ end
60
+
61
+ # Nix < around 2.11, we can drop this in the future
62
+ if /^these derivations will be built:/ =~ line
63
+ @in_derivation_list = true
64
+ @in_fetch_list = false
65
+ return
66
+ elsif /^these paths will be fetched / =~ line
67
+ @in_derivation_list = false
68
+ @in_fetch_list = true
69
+ return
70
+ end
71
+
72
+ if @in_derivation_list
73
+ if %r{^\s+/nix/store/} =~ line
74
+ @build_total += 1
75
+ return
76
+ else
77
+ @in_derivation_list = false
78
+ @build_total += @build_progress
79
+ end
80
+
81
+ elsif @in_fetch_list
82
+ if %r{^\s+/nix/store/} =~ line
83
+ @fetch_total += 1
84
+ return
85
+ else
86
+ @in_fetch_list = false
87
+ @fetch_total += @fetch_progress
88
+ end
89
+ end
90
+
91
+ if /^building '([^']+)/ =~ line
92
+ @build_progress += 1
93
+ yield(:build, @build_progress, @build_total, ::Regexp.last_match(1))
94
+ elsif /^copying path '([^']+)/ =~ line
95
+ @fetch_progress += 1
96
+ yield(:fetch, @fetch_progress, @fetch_total, ::Regexp.last_match(1))
97
+ end
98
+ end
99
+
100
+ def build_nix_path(swpins)
101
+ paths = []
102
+ paths << "confctl=#{ConfCtl.root}"
103
+ paths.concat(swpins.map { |k, v| "#{k}=#{v}" })
104
+ paths.concat(Settings.instance.nix_paths)
105
+ paths.join(':')
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,64 @@
1
+ module ConfCtl
2
+ # Run nix-collect-garbage on {Machine}
3
+ class NixCollectGarbage
4
+ class Progress
5
+ def initialize(line)
6
+ @line = line
7
+ @path = false
8
+ parse
9
+ end
10
+
11
+ def path?
12
+ !@path.nil?
13
+ end
14
+
15
+ # @return [String, nil]
16
+ attr_reader :path
17
+
18
+ def to_s
19
+ @line
20
+ end
21
+
22
+ protected
23
+
24
+ def parse
25
+ return unless %r{^deleting '(/nix/store/[^']+)'$} =~ @line
26
+
27
+ @path = ::Regexp.last_match(1)
28
+ end
29
+ end
30
+
31
+ # @param machine [String]
32
+ def initialize(machine)
33
+ @machine = machine
34
+ end
35
+
36
+ # @yieldparam progress [Progress]
37
+ # @return [TTY::Command::Result]
38
+ def run!
39
+ mc = MachineControl.new(machine)
40
+
41
+ line_buf = StdLineBuffer.new do |out, err|
42
+ next unless block_given?
43
+
44
+ # %d store paths deleted, %f MiB freed
45
+ yield(Progress.new(out)) if out
46
+
47
+ # finding garbage collector roots...
48
+ # removing stale link from '...'
49
+ # deleting garbage...
50
+ # deleting '/nix/store/...'
51
+ # ...
52
+ yield(Progress.new(err)) if err
53
+ end
54
+
55
+ ret = mc.execute!('nix-collect-garbage', &line_buf.feed_block)
56
+ line_buf.flush
57
+ ret
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :machine
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ module ConfCtl
2
+ class NixCopy
3
+ # @param target [String]
4
+ # @param store_path [String]
5
+ def initialize(target, store_path)
6
+ @target = target
7
+ @store_path = store_path
8
+ @total = nil
9
+ @progress = 0
10
+ end
11
+
12
+ # @yieldparam progress [Integer]
13
+ # @yieldparam total [Integer]
14
+ # @yieldparam path [String]
15
+ def run!(&block)
16
+ cmd = SystemCommand.new
17
+
18
+ line_buf = StdLineBuffer.new do |_out, err|
19
+ parse_line(err, &block) if err && block
20
+ end
21
+
22
+ ret = cmd.run!(
23
+ 'nix-copy-closure',
24
+ '--to', "root@#{target}",
25
+ store_path,
26
+ &line_buf.feed_block
27
+ )
28
+
29
+ line_buf.flush
30
+ ret
31
+ end
32
+
33
+ protected
34
+
35
+ attr_reader :target, :store_path
36
+
37
+ def parse_line(line)
38
+ if @total.nil? && /^copying (\d+) paths/ =~ line
39
+ @total = ::Regexp.last_match(1).to_i
40
+ return
41
+ end
42
+
43
+ return unless /^copying path '([^']+)/ =~ line
44
+
45
+ @progress += 1
46
+ yield(@progress, @total, ::Regexp.last_match(1))
47
+ end
48
+ end
49
+ end