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
@@ -16,6 +16,9 @@ module ConfCtl
|
|
16
16
|
# @return [String]
|
17
17
|
attr_reader :toplevel
|
18
18
|
|
19
|
+
# @return [String]
|
20
|
+
attr_reader :auto_rollback
|
21
|
+
|
19
22
|
# @return [Array<String>]
|
20
23
|
attr_reader :swpin_names
|
21
24
|
|
@@ -29,22 +32,28 @@ module ConfCtl
|
|
29
32
|
# @return [Boolean]
|
30
33
|
attr_accessor :current
|
31
34
|
|
35
|
+
# @return [String, nil]
|
36
|
+
attr_reader :kernel_version
|
37
|
+
|
32
38
|
# @param host [String]
|
33
39
|
def initialize(host)
|
34
40
|
@host = host
|
35
41
|
end
|
36
42
|
|
37
43
|
# @param toplevel [String]
|
44
|
+
# @param auto_rollback [String]
|
38
45
|
# @param swpin_paths [Hash]
|
39
46
|
# @param swpin_specs [Hash]
|
40
47
|
# @param date [Time]
|
41
|
-
def create(toplevel, swpin_paths, swpin_specs, date: nil)
|
48
|
+
def create(toplevel, auto_rollback, swpin_paths, swpin_specs, date: nil)
|
42
49
|
@toplevel = toplevel
|
50
|
+
@auto_rollback = auto_rollback
|
43
51
|
@swpin_names = swpin_paths.keys
|
44
52
|
@swpin_paths = swpin_paths
|
45
53
|
@swpin_specs = swpin_specs
|
46
54
|
@date = date || Time.now
|
47
55
|
@name = date.strftime('%Y-%m-%d--%H-%M-%S')
|
56
|
+
@kernel_version = extract_kernel_version
|
48
57
|
end
|
49
58
|
|
50
59
|
# @param name [String]
|
@@ -53,6 +62,7 @@ module ConfCtl
|
|
53
62
|
|
54
63
|
cfg = JSON.parse(File.read(config_path))
|
55
64
|
@toplevel = cfg['toplevel']
|
65
|
+
@auto_rollback = cfg['auto_rollback']
|
56
66
|
|
57
67
|
@swpin_names = []
|
58
68
|
@swpin_paths = {}
|
@@ -69,6 +79,7 @@ module ConfCtl
|
|
69
79
|
end
|
70
80
|
|
71
81
|
@date = Time.iso8601(cfg['date'])
|
82
|
+
@kernel_version = extract_kernel_version
|
72
83
|
rescue StandardError => e
|
73
84
|
raise Error, "invalid generation '#{name}': #{e.message}"
|
74
85
|
end
|
@@ -76,6 +87,7 @@ module ConfCtl
|
|
76
87
|
def save
|
77
88
|
FileUtils.mkdir_p(dir)
|
78
89
|
File.symlink(toplevel, toplevel_path)
|
90
|
+
File.symlink(auto_rollback, auto_rollback_path)
|
79
91
|
|
80
92
|
swpin_paths.each do |name, path|
|
81
93
|
File.symlink(path, swpin_path(name))
|
@@ -85,6 +97,7 @@ module ConfCtl
|
|
85
97
|
f.puts(JSON.pretty_generate({
|
86
98
|
date: date.iso8601,
|
87
99
|
toplevel:,
|
100
|
+
auto_rollback:,
|
88
101
|
swpins: swpin_paths.to_h do |name, path|
|
89
102
|
[name, { path:, spec: swpin_specs[name].as_json }]
|
90
103
|
end
|
@@ -97,6 +110,13 @@ module ConfCtl
|
|
97
110
|
def destroy
|
98
111
|
remove_gcroot
|
99
112
|
File.unlink(toplevel_path)
|
113
|
+
|
114
|
+
begin
|
115
|
+
File.unlink(auto_rollback_path)
|
116
|
+
rescue Errno::ENOENT
|
117
|
+
# Older generations might not have auto_rollback
|
118
|
+
end
|
119
|
+
|
100
120
|
swpin_paths.each_key { |name| File.unlink(swpin_path(name)) }
|
101
121
|
File.unlink(config_path)
|
102
122
|
Dir.rmdir(dir)
|
@@ -104,6 +124,7 @@ module ConfCtl
|
|
104
124
|
|
105
125
|
def add_gcroot
|
106
126
|
GCRoot.add(gcroot_name('toplevel'), toplevel_path)
|
127
|
+
GCRoot.add(gcroot_name('auto_rollback'), auto_rollback_path)
|
107
128
|
swpin_paths.each_key do |name|
|
108
129
|
GCRoot.add(gcroot_name("swpin.#{name}"), toplevel_path)
|
109
130
|
end
|
@@ -111,6 +132,7 @@ module ConfCtl
|
|
111
132
|
|
112
133
|
def remove_gcroot
|
113
134
|
GCRoot.remove(gcroot_name('toplevel'))
|
135
|
+
GCRoot.remove(gcroot_name('auto_rollback'))
|
114
136
|
swpin_paths.each_key do |name|
|
115
137
|
GCRoot.remove(gcroot_name("swpin.#{name}"))
|
116
138
|
end
|
@@ -130,6 +152,10 @@ module ConfCtl
|
|
130
152
|
@toplevel_path ||= File.join(dir, 'toplevel')
|
131
153
|
end
|
132
154
|
|
155
|
+
def auto_rollback_path
|
156
|
+
@auto_rollback_path ||= File.join(dir, 'auto_rollback')
|
157
|
+
end
|
158
|
+
|
133
159
|
def swpin_path(name)
|
134
160
|
File.join(dir, "#{name}.swpin")
|
135
161
|
end
|
@@ -141,5 +167,20 @@ module ConfCtl
|
|
141
167
|
def gcroot_name(file)
|
142
168
|
"#{escaped_host}-generation-#{name}-#{file}"
|
143
169
|
end
|
170
|
+
|
171
|
+
def extract_kernel_version
|
172
|
+
# `kernel` is for NixOS/vpsAdminOS and also carried NixOS machines (netboot)
|
173
|
+
# `bzImage` is for carried vpsAdminOS machines (netboot)
|
174
|
+
%w[kernel bzImage].each do |v|
|
175
|
+
link = File.readlink(File.join(toplevel, v))
|
176
|
+
next unless %r{\A/nix/store/[^-]+-linux-([^/]+)} =~ link
|
177
|
+
|
178
|
+
return ::Regexp.last_match(1)
|
179
|
+
rescue Errno::ENOENT, Errno::EINVAL
|
180
|
+
next
|
181
|
+
end
|
182
|
+
|
183
|
+
nil
|
184
|
+
end
|
144
185
|
end
|
145
186
|
end
|
@@ -55,6 +55,16 @@ module ConfCtl
|
|
55
55
|
index[name]
|
56
56
|
end
|
57
57
|
|
58
|
+
# @param offset [Integer] 0 = current/last, 1 = first (oldest), -1 = before last
|
59
|
+
# @return [Generation::Build]
|
60
|
+
def at_offset(offset)
|
61
|
+
if offset == 0
|
62
|
+
generations.last
|
63
|
+
else
|
64
|
+
generations[offset - 1]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
58
68
|
def each(&)
|
59
69
|
generations.each(&)
|
60
70
|
end
|
@@ -1,19 +1,22 @@
|
|
1
1
|
module ConfCtl
|
2
2
|
class Generation::Host
|
3
|
-
attr_reader :host, :profile, :id, :toplevel, :date, :current
|
3
|
+
attr_reader :host, :profile, :id, :toplevel, :date, :kernel_version, :current
|
4
4
|
|
5
|
-
# @param
|
5
|
+
# @param machine [Machine]
|
6
6
|
# @param profile [String]
|
7
7
|
# @param id [Integer]
|
8
8
|
# @param toplevel [String]
|
9
9
|
# @param date [Time]
|
10
|
+
# @param kernel_version [String, nil]
|
10
11
|
# @param mc [MachineControl]
|
11
|
-
def initialize(
|
12
|
-
@host =
|
12
|
+
def initialize(machine, profile, id, toplevel, date, kernel_version, current: false, mc: nil)
|
13
|
+
@host = machine.name
|
14
|
+
@machine = machine
|
13
15
|
@profile = profile
|
14
16
|
@id = id
|
15
17
|
@toplevel = toplevel
|
16
18
|
@date = date
|
19
|
+
@kernel_version = kernel_version
|
17
20
|
@current = current
|
18
21
|
@mc = mc
|
19
22
|
end
|
@@ -25,7 +28,8 @@ module ConfCtl
|
|
25
28
|
def destroy
|
26
29
|
raise 'machine control not available' if mc.nil?
|
27
30
|
|
28
|
-
|
31
|
+
env_cmd = @machine.carried? ? 'carrier-env' : 'nix-env'
|
32
|
+
mc.execute(env_cmd, '-p', profile, '--delete-generations', id.to_s)
|
29
33
|
end
|
30
34
|
|
31
35
|
protected
|
@@ -1,24 +1,33 @@
|
|
1
1
|
module ConfCtl
|
2
2
|
class Generation::HostList
|
3
|
+
# @parma machine [Machine]
|
3
4
|
# @param mc [MachineControl]
|
4
5
|
# @param profile [String]
|
5
6
|
# @return [Generation::HostList]
|
6
|
-
def self.fetch(mc, profile:)
|
7
|
+
def self.fetch(machine, mc, profile:)
|
7
8
|
out, = mc.bash_script(<<~END)
|
8
9
|
realpath #{profile}
|
9
10
|
|
10
11
|
for generation in `ls -d -1 #{profile}-*-link` ; do
|
11
|
-
echo "$generation
|
12
|
+
echo -n "$generation;"
|
13
|
+
echo -n "$(readlink $generation);"
|
14
|
+
echo -n "$(stat --format=%Y $generation);"
|
15
|
+
|
16
|
+
for kernel_file in kernel bzImage ; do
|
17
|
+
[ -h "$generation/$kernel_file" ] && echo -n $(readlink "$generation/$kernel_file")
|
18
|
+
done
|
19
|
+
|
20
|
+
echo
|
12
21
|
done
|
13
22
|
END
|
14
23
|
|
15
|
-
list = new(
|
24
|
+
list = new(machine.name)
|
16
25
|
lines = out.strip.split("\n")
|
17
26
|
current_path = lines.shift
|
18
27
|
id_rx = /^#{Regexp.escape(profile)}-(\d+)-link$/
|
19
28
|
|
20
29
|
lines.each do |line|
|
21
|
-
link, path, created_at = line.split(';')
|
30
|
+
link, path, created_at, kernel = line.split(';')
|
22
31
|
|
23
32
|
if id_rx =~ link
|
24
33
|
id = ::Regexp.last_match(1).to_i
|
@@ -27,12 +36,18 @@ module ConfCtl
|
|
27
36
|
next
|
28
37
|
end
|
29
38
|
|
39
|
+
kernel_version =
|
40
|
+
if kernel && %r{\A/nix/store/[^-]+-linux-([^/]+)} =~ kernel
|
41
|
+
::Regexp.last_match(1)
|
42
|
+
end
|
43
|
+
|
30
44
|
list << Generation::Host.new(
|
31
|
-
|
45
|
+
machine,
|
32
46
|
profile,
|
33
47
|
id,
|
34
48
|
path,
|
35
49
|
Time.at(created_at.to_i),
|
50
|
+
kernel_version,
|
36
51
|
current: path == current_path,
|
37
52
|
mc:
|
38
53
|
)
|
@@ -15,6 +15,9 @@ module ConfCtl
|
|
15
15
|
# @return [Time]
|
16
16
|
attr_reader :date
|
17
17
|
|
18
|
+
# @return [String, nil]
|
19
|
+
attr_reader :kernel_version
|
20
|
+
|
18
21
|
# @return [Boolean]
|
19
22
|
attr_reader :current
|
20
23
|
|
@@ -37,11 +40,13 @@ module ConfCtl
|
|
37
40
|
@name = build_generation.name
|
38
41
|
@toplevel = build_generation.toplevel
|
39
42
|
@date = build_generation.date
|
43
|
+
@kernel_version = build_generation.kernel_version
|
40
44
|
@current ||= build_generation.current
|
41
45
|
elsif host_generation
|
42
46
|
@name = host_generation.approx_name
|
43
47
|
@toplevel = host_generation.toplevel
|
44
48
|
@date = host_generation.date
|
49
|
+
@kernel_version = host_generation.kernel_version
|
45
50
|
@current ||= host_generation.current
|
46
51
|
else
|
47
52
|
raise ArgumentError, 'set build or host'
|
@@ -46,6 +46,16 @@ module ConfCtl
|
|
46
46
|
generations.each(&)
|
47
47
|
end
|
48
48
|
|
49
|
+
# @param offset [Integer] 0 = current/last, 1 = first (oldest), -1 = before last
|
50
|
+
# @return [Generation::Unified]
|
51
|
+
def at_offset(offset)
|
52
|
+
if offset == 0
|
53
|
+
generations.last
|
54
|
+
else
|
55
|
+
generations[offset - 1]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
49
59
|
def delete_if(&)
|
50
60
|
generations.delete_if(&)
|
51
61
|
end
|
data/lib/confctl/machine.rb
CHANGED
@@ -6,10 +6,11 @@ module ConfCtl
|
|
6
6
|
attr_reader :machine
|
7
7
|
|
8
8
|
# @param machine [Machine]
|
9
|
-
|
9
|
+
# @param logger [#<<]
|
10
|
+
def initialize(machine, logger: nil)
|
10
11
|
@machine = machine
|
11
12
|
@extra_ssh_opts = []
|
12
|
-
@cmd = SystemCommand.new
|
13
|
+
@cmd = SystemCommand.new(logger:)
|
13
14
|
end
|
14
15
|
|
15
16
|
# Try to open SSH connection
|
data/lib/confctl/machine_list.rb
CHANGED
@@ -103,7 +103,7 @@ module ConfCtl
|
|
103
103
|
|
104
104
|
if generations
|
105
105
|
begin
|
106
|
-
@generations = Generation::HostList.fetch(mc, profile: machine.profile)
|
106
|
+
@generations = Generation::HostList.fetch(machine, mc, profile: machine.profile)
|
107
107
|
rescue TTY::Command::ExitError
|
108
108
|
return
|
109
109
|
end
|
data/lib/confctl/nix.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'confctl/utils/file'
|
2
|
+
require 'digest'
|
2
3
|
require 'etc'
|
3
4
|
require 'json'
|
4
5
|
require 'securerandom'
|
@@ -137,19 +138,19 @@ module ConfCtl
|
|
137
138
|
end
|
138
139
|
end
|
139
140
|
|
140
|
-
# Evaluate swpins for
|
141
|
-
# @param
|
142
|
-
# @return [Hash]
|
143
|
-
def eval_host_swpins(
|
141
|
+
# Evaluate swpins for hosts
|
142
|
+
# @param hosts [Array<String>]
|
143
|
+
# @return [Hash] host => swpins
|
144
|
+
def eval_host_swpins(hosts)
|
144
145
|
with_argument({
|
145
146
|
confDir: conf_dir,
|
146
147
|
build: :evalHostSwpins,
|
147
|
-
machines:
|
148
|
+
machines: hosts
|
148
149
|
}, core_swpins: true) do |arg|
|
149
150
|
out_link = File.join(
|
150
151
|
cache_dir,
|
151
152
|
'build',
|
152
|
-
"#{
|
153
|
+
"#{Digest::SHA256.hexdigest(hosts.join(','))[0..11]}.swpins"
|
153
154
|
)
|
154
155
|
|
155
156
|
cmd_args = [
|
@@ -164,7 +165,7 @@ module ConfCtl
|
|
164
165
|
|
165
166
|
out, = cmd.run(*cmd_args)
|
166
167
|
|
167
|
-
JSON.parse(File.read(out.strip))
|
168
|
+
JSON.parse(File.read(out.strip))
|
168
169
|
end
|
169
170
|
end
|
170
171
|
|
@@ -204,15 +205,16 @@ module ConfCtl
|
|
204
205
|
nb.run(&block)
|
205
206
|
|
206
207
|
begin
|
207
|
-
|
208
|
-
|
208
|
+
host_results = JSON.parse(File.read(out_link))
|
209
|
+
host_results.each do |host, result|
|
209
210
|
host_generations = Generation::BuildList.new(host)
|
210
|
-
generation = host_generations.find(
|
211
|
+
generation = host_generations.find(result['attribute'], swpin_paths)
|
211
212
|
|
212
213
|
if generation.nil?
|
213
214
|
generation = Generation::Build.new(host)
|
214
215
|
generation.create(
|
215
|
-
|
216
|
+
result['attribute'],
|
217
|
+
result['autoRollback'],
|
216
218
|
swpin_paths,
|
217
219
|
host_swpin_specs[host],
|
218
220
|
date: time
|
@@ -232,35 +234,78 @@ module ConfCtl
|
|
232
234
|
end
|
233
235
|
|
234
236
|
# @param machine [Machine]
|
235
|
-
# @param
|
237
|
+
# @param paths [Array<String>]
|
236
238
|
#
|
237
239
|
# @yieldparam progress [Integer]
|
238
240
|
# @yieldparam total [Integer]
|
239
241
|
# @yieldparam path [String]
|
240
242
|
#
|
241
243
|
# @return [Boolean]
|
242
|
-
def copy(machine,
|
244
|
+
def copy(machine, paths, &)
|
243
245
|
if machine.localhost?
|
244
246
|
true
|
245
247
|
elsif machine.carried?
|
246
|
-
cp = NixCopy.new(machine.carrier_machine.target_host,
|
248
|
+
cp = NixCopy.new(machine.carrier_machine.target_host, paths)
|
247
249
|
cp.run!(&).success?
|
248
250
|
else
|
249
|
-
cp = NixCopy.new(machine.target_host,
|
251
|
+
cp = NixCopy.new(machine.target_host, paths)
|
250
252
|
cp.run!(&).success?
|
251
253
|
end
|
252
254
|
end
|
253
255
|
|
254
256
|
# @param machine [Machine]
|
255
|
-
# @param
|
257
|
+
# @param generation [Generation::Build]
|
256
258
|
# @param action [String]
|
257
259
|
# @return [Boolean]
|
258
|
-
def activate(machine,
|
259
|
-
args = [File.join(toplevel, 'bin/switch-to-configuration'), action]
|
260
|
+
def activate(machine, generation, action)
|
261
|
+
args = [File.join(generation.toplevel, 'bin/switch-to-configuration'), action]
|
260
262
|
|
261
263
|
MachineControl.new(machine).execute!(*args).success?
|
262
264
|
end
|
263
265
|
|
266
|
+
# @param machine [Machine]
|
267
|
+
# @param generation [Generation::Build]
|
268
|
+
# @param action [String]
|
269
|
+
# @return [Boolean]
|
270
|
+
def activate_with_rollback(machine, generation, action)
|
271
|
+
check_file = File.join('/run', "confctl-confirm-#{SecureRandom.hex(3)}")
|
272
|
+
timeout = machine['autoRollback']['timeout']
|
273
|
+
logger = NullLogger.new
|
274
|
+
|
275
|
+
args = [
|
276
|
+
generation.auto_rollback,
|
277
|
+
'-t', timeout,
|
278
|
+
generation.toplevel,
|
279
|
+
action,
|
280
|
+
check_file
|
281
|
+
]
|
282
|
+
|
283
|
+
activation_success = nil
|
284
|
+
|
285
|
+
activation_thread = Thread.new do
|
286
|
+
activation_success = MachineControl.new(machine).execute!(*args).success?
|
287
|
+
end
|
288
|
+
|
289
|
+
# Wait for the configuration to be switched
|
290
|
+
t = Time.now
|
291
|
+
|
292
|
+
loop do
|
293
|
+
out, = MachineControl.new(machine, logger:).execute!('cat', check_file, '2>/dev/null')
|
294
|
+
stripped = out.strip
|
295
|
+
break if stripped == 'switched' || ((t + timeout + 10) < Time.now && stripped != 'switching')
|
296
|
+
|
297
|
+
sleep(1)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Confirm it
|
301
|
+
10.times do
|
302
|
+
break if MachineControl.new(machine, logger:).execute!('sh', '-c', "'echo confirmed > #{check_file}'").success?
|
303
|
+
end
|
304
|
+
|
305
|
+
activation_thread.join
|
306
|
+
activation_success
|
307
|
+
end
|
308
|
+
|
264
309
|
# @param machine [Machine]
|
265
310
|
# @param toplevel [String]
|
266
311
|
# @return [Boolean]
|
data/lib/confctl/nix_copy.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
module ConfCtl
|
2
2
|
class NixCopy
|
3
3
|
# @param target [String]
|
4
|
-
# @param
|
5
|
-
def initialize(target,
|
4
|
+
# @param store_paths [Array<String>]
|
5
|
+
def initialize(target, store_paths)
|
6
6
|
@target = target
|
7
|
-
@
|
7
|
+
@store_paths = store_paths
|
8
8
|
@total = nil
|
9
9
|
@progress = 0
|
10
10
|
end
|
@@ -22,7 +22,7 @@ module ConfCtl
|
|
22
22
|
ret = cmd.run!(
|
23
23
|
'nix-copy-closure',
|
24
24
|
'--to', "root@#{target}",
|
25
|
-
|
25
|
+
*store_paths,
|
26
26
|
&line_buf.feed_block
|
27
27
|
)
|
28
28
|
|
@@ -32,7 +32,7 @@ module ConfCtl
|
|
32
32
|
|
33
33
|
protected
|
34
34
|
|
35
|
-
attr_reader :target, :
|
35
|
+
attr_reader :target, :store_paths
|
36
36
|
|
37
37
|
def parse_line(line)
|
38
38
|
if @total.nil? && /^copying (\d+) paths/ =~ line
|
@@ -2,9 +2,10 @@ require 'tty-command'
|
|
2
2
|
|
3
3
|
module ConfCtl
|
4
4
|
module SystemCommand
|
5
|
+
# @param logger [#<<]
|
5
6
|
# @return [TTY::Command]
|
6
|
-
def self.new
|
7
|
-
TTY::Command.new(output: Logger.instance, color: false)
|
7
|
+
def self.new(logger: nil)
|
8
|
+
TTY::Command.new(output: logger || Logger.instance, color: false)
|
8
9
|
end
|
9
10
|
end
|
10
11
|
end
|
data/lib/confctl/version.rb
CHANGED
@@ -0,0 +1,106 @@
|
|
1
|
+
#!@ruby@/bin/ruby
|
2
|
+
# Switch to a new system configuration and wait for confctl to confirm
|
3
|
+
# connectivity. If confctl is unable to reach the deployed machine, this
|
4
|
+
# script will roll back to the previous configuration.
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
class AutoRollback
|
9
|
+
def self.run
|
10
|
+
ar = new
|
11
|
+
ar.run
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
puts 'Deploying with auto-rollback'
|
16
|
+
|
17
|
+
options = parse_options
|
18
|
+
|
19
|
+
current_system = File.readlink('/run/current-system')
|
20
|
+
puts " current system = #{current_system}"
|
21
|
+
puts " new system = #{options[:toplevel]}"
|
22
|
+
puts " action = #{options[:action]}"
|
23
|
+
puts " check file = #{options[:check_file]}"
|
24
|
+
puts " timeout = #{options[:timeout]} seconds"
|
25
|
+
puts
|
26
|
+
|
27
|
+
puts 'Switching to new configuration'
|
28
|
+
File.write(options[:check_file], 'switching')
|
29
|
+
|
30
|
+
pid = Process.spawn(
|
31
|
+
File.join(options[:toplevel], 'bin/switch-to-configuration'),
|
32
|
+
options[:action]
|
33
|
+
)
|
34
|
+
|
35
|
+
Process.wait(pid)
|
36
|
+
|
37
|
+
File.write(options[:check_file], 'switched')
|
38
|
+
|
39
|
+
puts 'Switch complete, waiting for confirmation'
|
40
|
+
t = Time.now
|
41
|
+
|
42
|
+
loop do
|
43
|
+
sleep(0.5)
|
44
|
+
|
45
|
+
if File.read(options[:check_file]).strip == 'confirmed'
|
46
|
+
puts 'Configuration confirmed'
|
47
|
+
File.unlink(options[:check_file])
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
break if t + options[:timeout] < Time.now
|
52
|
+
end
|
53
|
+
|
54
|
+
puts 'Timeout occurred, rolling back'
|
55
|
+
|
56
|
+
pid = Process.spawn(
|
57
|
+
File.join(current_system, 'bin/switch-to-configuration'),
|
58
|
+
options[:action]
|
59
|
+
)
|
60
|
+
|
61
|
+
Process.wait(pid)
|
62
|
+
|
63
|
+
puts 'Rollback complete'
|
64
|
+
exit(false)
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def parse_options
|
70
|
+
options = {
|
71
|
+
timeout: 60,
|
72
|
+
toplevel: nil,
|
73
|
+
action: nil,
|
74
|
+
check_file: nil
|
75
|
+
}
|
76
|
+
|
77
|
+
opt_parser = OptionParser.new do |parser|
|
78
|
+
parser.banner = "Usage: #{$0} [options] <toplevel> <action> <check file>"
|
79
|
+
|
80
|
+
parser.on('-t', '--timeout TIMEOUT', Integer, 'Timeout in seconds') do |v|
|
81
|
+
options[:timeout] = v
|
82
|
+
end
|
83
|
+
|
84
|
+
parser.on('-h', '--help', 'Print help message and exit') do
|
85
|
+
puts parser
|
86
|
+
exit
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
opt_parser.parse!
|
91
|
+
|
92
|
+
if ARGV.length != 3
|
93
|
+
warn 'Invalid arguments'
|
94
|
+
warn opt_parser
|
95
|
+
exit(false)
|
96
|
+
end
|
97
|
+
|
98
|
+
options[:toplevel] = ARGV[0]
|
99
|
+
options[:action] = ARGV[1]
|
100
|
+
options[:check_file] = ARGV[2]
|
101
|
+
|
102
|
+
options
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
AutoRollback.run
|