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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +2 -1
- data/docs/carrier.md +12 -0
- data/lib/confctl/cli/app.rb +16 -0
- data/lib/confctl/cli/cluster.rb +183 -47
- data/lib/confctl/cli/generation.rb +44 -15
- data/lib/confctl/generation/build.rb +42 -1
- data/lib/confctl/generation/build_list.rb +10 -0
- data/lib/confctl/generation/host.rb +9 -5
- data/lib/confctl/generation/host_list.rb +20 -5
- data/lib/confctl/generation/unified.rb +5 -0
- data/lib/confctl/generation/unified_list.rb +10 -0
- data/lib/confctl/machine.rb +4 -0
- data/lib/confctl/machine_control.rb +3 -2
- data/lib/confctl/machine_list.rb +4 -0
- data/lib/confctl/machine_status.rb +1 -1
- data/lib/confctl/nix.rb +63 -18
- data/lib/confctl/nix_copy.rb +5 -5
- data/lib/confctl/null_logger.rb +7 -0
- data/lib/confctl/swpins/specs/git_rev.rb +1 -1
- data/lib/confctl/system_command.rb +3 -2
- data/lib/confctl/version.rb +1 -1
- data/libexec/auto-rollback.rb +106 -0
- data/man/man8/confctl.8 +88 -72
- data/man/man8/confctl.8.md +70 -54
- data/nix/evaluator.nix +9 -1
- data/nix/modules/cluster/default.nix +20 -0
- data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +209 -50
- data/nix/modules/confctl/kexec-netboot/default.nix +36 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +62 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +455 -0
- data/nix/modules/system-list.nix +3 -1
- metadata +7 -2
@@ -0,0 +1,455 @@
|
|
1
|
+
#!@ruby@/bin/ruby
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
require 'optparse'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
class KexecNetboot
|
10
|
+
MACHINE_FQDN = '@machineFqdn@'.freeze
|
11
|
+
|
12
|
+
KEXEC = '@kexecTools@/bin/kexec'.freeze
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@server_url = nil
|
16
|
+
@machine_fqdn = nil
|
17
|
+
@machine_gen = nil
|
18
|
+
@variant_name = nil
|
19
|
+
@interactive = false
|
20
|
+
@append_params = ''
|
21
|
+
@exec = false
|
22
|
+
@unload = false
|
23
|
+
@machines_json = nil
|
24
|
+
@tmp_files = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
parse_arguments
|
29
|
+
|
30
|
+
if @unload && @exec
|
31
|
+
warn 'ERROR: use either --unload or --exec, not both'
|
32
|
+
exit 1
|
33
|
+
elsif @unload
|
34
|
+
return unload_kexec
|
35
|
+
elsif @exec
|
36
|
+
return exec_kexec
|
37
|
+
end
|
38
|
+
|
39
|
+
httproot = parse_httproot_from_cmdline
|
40
|
+
unless httproot
|
41
|
+
warn "ERROR: Could not find 'httproot=' parameter in /proc/cmdline"
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
machines_url =
|
46
|
+
if @server_url
|
47
|
+
File.join(@server_url, 'machines.json')
|
48
|
+
else
|
49
|
+
derive_machines_json_url(httproot)
|
50
|
+
end
|
51
|
+
|
52
|
+
@machines_json = fetch_machines_json(machines_url)
|
53
|
+
|
54
|
+
machine_data = pick_machine(@machine_fqdn)
|
55
|
+
|
56
|
+
if machine_data.nil?
|
57
|
+
warn 'ERROR: No suitable machine found.'
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
|
61
|
+
generation = pick_generation(machine_data, @machine_gen)
|
62
|
+
|
63
|
+
if generation.nil?
|
64
|
+
warn 'ERROR: No generation found/selected.'
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
variant = pick_variant(generation, @variant_name)
|
69
|
+
|
70
|
+
combined_params = generation['kernel_params'].dup
|
71
|
+
|
72
|
+
if variant && variant['kernel_params']
|
73
|
+
combined_params.concat(variant['kernel_params'])
|
74
|
+
end
|
75
|
+
|
76
|
+
combined_params << @append_params
|
77
|
+
final_params = combined_params.join(' ')
|
78
|
+
|
79
|
+
if @interactive
|
80
|
+
puts
|
81
|
+
puts 'Selected configuration:'
|
82
|
+
puts " Machine: #{machine_data['fqdn']} (spin=#{machine_data['spin']})"
|
83
|
+
puts " Generation: #{generation['generation']} (#{generation['time_s']}, rev=#{generation['shortrev']}, kernel=#{generation['kernel_version']})"
|
84
|
+
if variant
|
85
|
+
puts " Variant: #{variant['name']} (#{variant['label']})"
|
86
|
+
else
|
87
|
+
puts ' Variant: none'
|
88
|
+
end
|
89
|
+
puts " Kernel params: #{final_params}"
|
90
|
+
puts
|
91
|
+
|
92
|
+
loop do
|
93
|
+
print 'Continue? [y/N]: '
|
94
|
+
|
95
|
+
case $stdin.readline.strip.downcase
|
96
|
+
when 'y'
|
97
|
+
puts
|
98
|
+
break
|
99
|
+
when 'n'
|
100
|
+
warn 'Aborting'
|
101
|
+
exit(false)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Download kernel + initrd
|
107
|
+
kernel_path = download_file(generation['boot_files']['bzImage'])
|
108
|
+
initrd_path = download_file(generation['boot_files']['initrd'])
|
109
|
+
|
110
|
+
# kexec -l
|
111
|
+
load_kexec(kernel_path, initrd_path, final_params)
|
112
|
+
|
113
|
+
# Cleanup
|
114
|
+
cleanup_downloads
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def parse_arguments
|
120
|
+
opt_parser = OptionParser.new do |opts|
|
121
|
+
opts.banner = "Usage: #{$0} [options]"
|
122
|
+
|
123
|
+
opts.on('-s', '--server-url URL', 'Specify URL of the netboot server') do |val|
|
124
|
+
@server_url = val
|
125
|
+
end
|
126
|
+
|
127
|
+
opts.on('-m', '--machine FQDN', 'Select machine by FQDN') do |val|
|
128
|
+
@machine_fqdn = val
|
129
|
+
end
|
130
|
+
|
131
|
+
opts.on('-g', '--generation N', 'Select generation by number or negative offset') do |val|
|
132
|
+
@machine_gen = val
|
133
|
+
end
|
134
|
+
|
135
|
+
opts.on('-v', '--variant NAME', 'Select a specific variant by name') do |val|
|
136
|
+
@variant_name = val
|
137
|
+
end
|
138
|
+
|
139
|
+
opts.on('-i', '--interactive', 'Enable interactive mode') do
|
140
|
+
@interactive = true
|
141
|
+
end
|
142
|
+
|
143
|
+
opts.on('-a', '--append PARAMS', 'Append parameters to kernel command line') do |val|
|
144
|
+
@append_params = val
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.on('-e', '--exec', 'Run the currently loaded kernel') do
|
148
|
+
@exec = true
|
149
|
+
end
|
150
|
+
|
151
|
+
opts.on('-u', '--unload', 'Unload the current kexec target kernel and exit') do
|
152
|
+
@unload = true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
opt_parser.parse!(ARGV)
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_httproot_from_cmdline
|
160
|
+
cmdline = File.read('/proc/cmdline').strip
|
161
|
+
cmdline[/\bhttproot=([^\s]+)/, 1]
|
162
|
+
end
|
163
|
+
|
164
|
+
def derive_machines_json_url(httproot)
|
165
|
+
uri = URI.parse(httproot)
|
166
|
+
path_parts = uri.path.split('/').reject(&:empty?)
|
167
|
+
|
168
|
+
if path_parts.size >= 3
|
169
|
+
3.times { path_parts.pop }
|
170
|
+
end
|
171
|
+
|
172
|
+
new_path = "/#{path_parts.join('/')}"
|
173
|
+
new_path << '/' unless new_path.end_with?('/')
|
174
|
+
new_path << 'machines.json'
|
175
|
+
|
176
|
+
URI::HTTP.build(host: uri.host, port: uri.port, path: new_path).to_s
|
177
|
+
end
|
178
|
+
|
179
|
+
def fetch_machines_json(machines_url)
|
180
|
+
uri = URI.parse(machines_url)
|
181
|
+
response = Net::HTTP.get_response(uri)
|
182
|
+
|
183
|
+
unless response.is_a?(Net::HTTPSuccess)
|
184
|
+
warn "ERROR: Could not download #{machines_url}, HTTP #{response.code}"
|
185
|
+
exit 1
|
186
|
+
end
|
187
|
+
|
188
|
+
begin
|
189
|
+
JSON.parse(response.body)
|
190
|
+
rescue JSON::ParserError => e
|
191
|
+
warn "ERROR: Could not parse JSON: #{e}"
|
192
|
+
exit 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def pick_machine(requested_fqdn)
|
197
|
+
machines = @machines_json['machines']
|
198
|
+
return if machines.nil? || machines.empty?
|
199
|
+
|
200
|
+
if requested_fqdn.nil? && @interactive
|
201
|
+
return interactive_pick_machine(machines)
|
202
|
+
end
|
203
|
+
|
204
|
+
requested_fqdn ||= MACHINE_FQDN
|
205
|
+
|
206
|
+
found = machines.detect { |m| m['fqdn'] == requested_fqdn }
|
207
|
+
|
208
|
+
if found.nil?
|
209
|
+
warn "Machine '#{requested_fqdn}' not found."
|
210
|
+
return
|
211
|
+
end
|
212
|
+
|
213
|
+
found
|
214
|
+
end
|
215
|
+
|
216
|
+
def interactive_pick_machine(machines)
|
217
|
+
format_str = "%5s %-30s %s\n"
|
218
|
+
default_machine = machines.detect { |m| m['fqdn'] == MACHINE_FQDN }
|
219
|
+
|
220
|
+
loop do
|
221
|
+
puts format(format_str, '', 'FQDN', 'SPIN')
|
222
|
+
|
223
|
+
machines.each_with_index do |m, idx|
|
224
|
+
current_mark = m['fqdn'] == default_machine['fqdn'] ? '*' : ''
|
225
|
+
puts format(format_str, "[#{idx + 1}]" + current_mark, m['fqdn'], m['spin'])
|
226
|
+
end
|
227
|
+
|
228
|
+
print 'Select a machine by number: '
|
229
|
+
input = $stdin.gets
|
230
|
+
return if input.nil?
|
231
|
+
|
232
|
+
idx = input.strip.to_i
|
233
|
+
|
234
|
+
if idx == 0 && default_machine
|
235
|
+
return default_machine
|
236
|
+
elsif idx.between?(1, machines.size)
|
237
|
+
return machines[idx - 1]
|
238
|
+
end
|
239
|
+
|
240
|
+
puts 'Invalid selection. Please try again.'
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def pick_generation(machine_data, generation_input)
|
245
|
+
gens = machine_data['generations'] || []
|
246
|
+
return if gens.empty?
|
247
|
+
|
248
|
+
unless generation_input.nil?
|
249
|
+
parsed = parse_generation_input(generation_input, gens)
|
250
|
+
|
251
|
+
if parsed.nil?
|
252
|
+
warn "Requested generation '#{generation_input}' not found (or invalid)."
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
return parsed
|
257
|
+
end
|
258
|
+
|
259
|
+
if @interactive
|
260
|
+
loop do
|
261
|
+
current = gens.find { |x| x['current'] == true }
|
262
|
+
default_label = if current
|
263
|
+
"(default is #{current['generation']} - current)"
|
264
|
+
else
|
265
|
+
"(no current labeled, default is newest: #{gens[0]['generation']})"
|
266
|
+
end
|
267
|
+
|
268
|
+
puts "Available generations for #{machine_data['fqdn']}: #{default_label}"
|
269
|
+
list_generations(gens)
|
270
|
+
print 'Enter generation number (or negative offset) [ENTER for default]: '
|
271
|
+
input = $stdin.gets
|
272
|
+
return if input.nil?
|
273
|
+
|
274
|
+
line = input.strip
|
275
|
+
return current || gens[0] if line == ''
|
276
|
+
|
277
|
+
parsed = parse_generation_input(line, gens)
|
278
|
+
return parsed if parsed
|
279
|
+
|
280
|
+
puts "Invalid generation '#{line}'. Please try again."
|
281
|
+
end
|
282
|
+
else
|
283
|
+
current = gens.find { |x| x['current'] == true }
|
284
|
+
current || gens[0]
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def list_generations(gens)
|
289
|
+
format_str = "%5s %-19s %-10s %s\n"
|
290
|
+
|
291
|
+
puts format(format_str, '', 'TIME', 'REVISION', 'KERNEL')
|
292
|
+
|
293
|
+
gens.each do |g|
|
294
|
+
current_mark = g['current'] ? '*' : ''
|
295
|
+
|
296
|
+
line = format(
|
297
|
+
format_str,
|
298
|
+
"[#{g['generation']}]" + current_mark,
|
299
|
+
g['time_s'],
|
300
|
+
g['shortrev'],
|
301
|
+
g['kernel_version']
|
302
|
+
)
|
303
|
+
puts line
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def parse_generation_input(input_str, gens)
|
308
|
+
begin
|
309
|
+
val = Integer(input_str)
|
310
|
+
rescue ArgumentError
|
311
|
+
return
|
312
|
+
end
|
313
|
+
|
314
|
+
if val >= 0
|
315
|
+
gens.find { |g| g['generation'] == val }
|
316
|
+
else
|
317
|
+
offset = -val
|
318
|
+
return if offset >= gens.size
|
319
|
+
|
320
|
+
gens[offset]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def pick_variant(generation_data, desired_variant_name)
|
325
|
+
variants = generation_data['variants'] || []
|
326
|
+
return if variants.empty?
|
327
|
+
|
328
|
+
if desired_variant_name.nil? && @interactive
|
329
|
+
interactive_pick_variant(generation_data)
|
330
|
+
elsif desired_variant_name
|
331
|
+
v = variants.find { |x| x['name'] == desired_variant_name }
|
332
|
+
|
333
|
+
if v.nil?
|
334
|
+
warn "Requested variant '#{desired_variant_name}' not found in generation #{generation_data['generation']}."
|
335
|
+
return
|
336
|
+
end
|
337
|
+
|
338
|
+
v
|
339
|
+
else
|
340
|
+
variants.first
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def interactive_pick_variant(generation_data)
|
345
|
+
variants = generation_data['variants']
|
346
|
+
|
347
|
+
loop do
|
348
|
+
puts "Variants available for generation #{generation_data['generation']}:"
|
349
|
+
format_str = "%5s %s\n"
|
350
|
+
|
351
|
+
puts format(format_str, '', 'LABEL')
|
352
|
+
|
353
|
+
variants.each_with_index do |v, idx|
|
354
|
+
line = format(format_str, "[#{idx + 1}]", v['label'])
|
355
|
+
puts line
|
356
|
+
end
|
357
|
+
|
358
|
+
print "Choose variant by number [ENTER for '#{variants[0]['name']}']: "
|
359
|
+
|
360
|
+
input = $stdin.gets
|
361
|
+
return if input.nil?
|
362
|
+
|
363
|
+
line = input.strip
|
364
|
+
return variants[0] if line == ''
|
365
|
+
|
366
|
+
idx = line.to_i
|
367
|
+
return variants[idx - 1] if idx.between?(1, variants.size)
|
368
|
+
|
369
|
+
puts 'Invalid selection. Please try again.'
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def download_file(url)
|
374
|
+
uri = URI.parse(url)
|
375
|
+
basename = File.basename(uri.path)
|
376
|
+
tmp_file = Tempfile.new(basename)
|
377
|
+
|
378
|
+
puts "Downloading #{url} -> #{tmp_file.path}"
|
379
|
+
|
380
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
381
|
+
resp = http.get(uri.request_uri)
|
382
|
+
|
383
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
384
|
+
warn "ERROR: Could not download #{url}, HTTP #{resp.code}"
|
385
|
+
exit 1
|
386
|
+
end
|
387
|
+
|
388
|
+
tmp_file.write(resp.body)
|
389
|
+
end
|
390
|
+
|
391
|
+
tmp_file.close
|
392
|
+
|
393
|
+
@tmp_files << tmp_file
|
394
|
+
tmp_file.path
|
395
|
+
end
|
396
|
+
|
397
|
+
def load_kexec(kernel_path, initrd_path, kernel_params_string)
|
398
|
+
cmd = [
|
399
|
+
KEXEC,
|
400
|
+
'-l',
|
401
|
+
kernel_path,
|
402
|
+
"--initrd=#{initrd_path}",
|
403
|
+
"--append=\"#{kernel_params_string}\""
|
404
|
+
].join(' ')
|
405
|
+
|
406
|
+
puts "Executing: #{cmd}"
|
407
|
+
system(cmd)
|
408
|
+
|
409
|
+
if $?.exitstatus == 0
|
410
|
+
puts 'kexec -l completed successfully.'
|
411
|
+
else
|
412
|
+
warn 'ERROR: kexec -l failed!'
|
413
|
+
exit 1
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def exec_kexec
|
418
|
+
cmd = [KEXEC, '-e'].join(' ')
|
419
|
+
|
420
|
+
puts "Executing: #{cmd}"
|
421
|
+
system(cmd)
|
422
|
+
|
423
|
+
if $?.exitstatus == 0
|
424
|
+
puts 'kexec -e completed successfully.'
|
425
|
+
else
|
426
|
+
warn 'ERROR: kexec -u failed!'
|
427
|
+
exit 1
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def unload_kexec
|
432
|
+
cmd = [KEXEC, '-u'].join(' ')
|
433
|
+
|
434
|
+
puts "Executing: #{cmd}"
|
435
|
+
system(cmd)
|
436
|
+
|
437
|
+
if $?.exitstatus == 0
|
438
|
+
puts 'kexec -u completed successfully.'
|
439
|
+
else
|
440
|
+
warn 'ERROR: kexec -u failed!'
|
441
|
+
exit 1
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def cleanup_downloads
|
446
|
+
@tmp_files.each do |f|
|
447
|
+
f.unlink
|
448
|
+
rescue StandardError => e
|
449
|
+
warn "Warning: Could not remove tmp file #{f.path}: #{e}"
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
loader = KexecNetboot.new
|
455
|
+
loader.run
|
data/nix/modules/system-list.nix
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: confctl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jakub Skokan
|
@@ -285,6 +285,7 @@ files:
|
|
285
285
|
- lib/confctl/nix_copy.rb
|
286
286
|
- lib/confctl/nix_format.rb
|
287
287
|
- lib/confctl/nix_literal_expression.rb
|
288
|
+
- lib/confctl/null_logger.rb
|
288
289
|
- lib/confctl/parallel_executor.rb
|
289
290
|
- lib/confctl/pattern.rb
|
290
291
|
- lib/confctl/settings.rb
|
@@ -307,6 +308,7 @@ files:
|
|
307
308
|
- lib/confctl/user_scripts.rb
|
308
309
|
- lib/confctl/utils/file.rb
|
309
310
|
- lib/confctl/version.rb
|
311
|
+
- libexec/auto-rollback.rb
|
310
312
|
- man/man8/confctl-options.nix.8
|
311
313
|
- man/man8/confctl-options.nix.8.md
|
312
314
|
- man/man8/confctl.8
|
@@ -325,6 +327,9 @@ files:
|
|
325
327
|
- nix/modules/confctl/carrier/netboot/nixos.nix
|
326
328
|
- nix/modules/confctl/cli.nix
|
327
329
|
- nix/modules/confctl/generations.nix
|
330
|
+
- nix/modules/confctl/kexec-netboot/default.nix
|
331
|
+
- nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc
|
332
|
+
- nix/modules/confctl/kexec-netboot/kexec-netboot.rb
|
328
333
|
- nix/modules/confctl/nix.nix
|
329
334
|
- nix/modules/confctl/swpins.nix
|
330
335
|
- nix/modules/module-list.nix
|
@@ -351,7 +356,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
351
356
|
- !ruby/object:Gem::Version
|
352
357
|
version: '0'
|
353
358
|
requirements: []
|
354
|
-
rubygems_version: 3.5.
|
359
|
+
rubygems_version: 3.5.22
|
355
360
|
signing_key:
|
356
361
|
specification_version: 4
|
357
362
|
summary: Nix deployment management tool
|