1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
|
require 'date'
require 'io/console'
require 'pry'
require 'timeout'
require 'test/unit'
# Test::Unit adds an at_exit hook which, among other things, consumes
# the command-line arguments that were intended for cucumber. If
# e.g. `--format` was passed it will throw an error since it's not a
# valid option for Test::Unit, and it throwing an error at this time
# (at_exit) will make Cucumber think it failed and consequently exit
# with an error. Fooling Test::Unit that this hook has already run
# works around this craziness.
Test::Unit.run = true
# Make all the assert_* methods easily accessible in any context.
include Test::Unit::Assertions
def assert_vmcommand_success(p, msg = nil)
assert(p.success?, msg.nil? ? "Command failed: #{p.cmd}\n" + \
"error code: #{p.returncode}\n" \
"stderr: #{p.stderr}" : \
msg)
end
# It's forbidden to throw this exception (or subclasses) in anything
# but try_for() below. Just don't use it anywhere else!
class UniqueTryForTimeoutError < Exception
end
# Call block (ignoring any exceptions it may throw) repeatedly with
# one second breaks until it returns true, or until `timeout` seconds have
# passed when we throw a Timeout::Error exception. If `timeout` is `nil`,
# then we just run the code block with no timeout.
def try_for(timeout, options = {})
if block_given? && timeout.nil?
return yield
end
options[:delay] ||= 1
last_exception = nil
# Create a unique exception used only for this particular try_for
# call's Timeout to allow nested try_for:s. If we used the same one,
# the innermost try_for would catch all outer ones', creating a
# really strange situation.
unique_timeout_exception = Class.new(UniqueTryForTimeoutError)
Timeout::timeout(timeout, unique_timeout_exception) do
loop do
begin
return if yield
rescue NameError, UniqueTryForTimeoutError => e
# NameError most likely means typos, and hiding that is rarely
# (never?) a good idea, so we rethrow them. See below why we
# also rethrow *all* the unique exceptions.
raise e
rescue Exception => e
# All other exceptions are ignored while trying the
# block. Well we save the last exception so we can print it in
# case of a timeout.
last_exception = e
end
sleep options[:delay]
end
end
# At this point the block above either succeeded and we'll return,
# or we are throwing an exception. If the latter, we either have a
# NameError that we'll not catch (and will any try_for below us in
# the stack), or we have a unique exception. That can mean one of
# two things:
# 1. it's the one unique to this try_for, and in that case we'll
# catch it, rethrowing it as something that will be ignored by
# inside the blocks of all try_for:s below us in the stack.
# 2. it's an exception unique to another try_for. Assuming that we
# do not throw the unique exceptions in any other place or way
# than we do it in this function, this means that there is a
# try_for below us in the stack to which this exception must be
# unique to.
# Let 1 be the base step, and 2 the inductive step, and we sort of
# an inductive proof for the correctness of try_for when it's
# nested. It shows that for an infinite stack of try_for:s, any of
# the unique exceptions will be caught only by the try_for instance
# it is unique to, and all try_for:s in between will ignore it so it
# ends up there immediately.
rescue unique_timeout_exception => e
msg = options[:msg] || 'try_for() timeout expired'
exc_class = options[:exception] || Timeout::Error
if last_exception
msg += "\nLast ignored exception was: " +
"#{last_exception.class}: #{last_exception}"
end
raise exc_class.new(msg)
end
class TorFailure < StandardError
end
class MaxRetriesFailure < StandardError
end
def force_new_tor_circuit()
debug_log("Forcing new Tor circuit...")
# Tor rate limits NEWNYM to at most one per 10 second period.
interval = 10
if $__last_newnym
elapsed = Time.now - $__last_newnym
# We sleep an extra second to avoid tight timings.
sleep interval - elapsed + 1 if 0 < elapsed && elapsed < interval
end
$vm.execute_successfully('tor_control_send "signal NEWNYM"', :libs => 'tor')
$__last_newnym = Time.now
end
# This will retry the block up to MAX_NEW_TOR_CIRCUIT_RETRIES
# times. The block must raise an exception for a run to be considered
# as a failure. After a failure recovery_proc will be called (if
# given) and the intention with it is to bring us back to the state
# expected by the block, so it can be retried.
def retry_tor(recovery_proc = nil, &block)
tor_recovery_proc = Proc.new do
force_new_tor_circuit
recovery_proc.call if recovery_proc
end
retry_action($config['MAX_NEW_TOR_CIRCUIT_RETRIES'],
:recovery_proc => tor_recovery_proc,
:operation_name => 'Tor operation', &block)
end
def retry_action(max_retries, options = {}, &block)
assert(max_retries.is_a?(Integer), "max_retries must be an integer")
options[:recovery_proc] ||= nil
options[:operation_name] ||= 'Operation'
retries = 1
loop do
begin
block.call
return
rescue NameError => e
# NameError most likely means typos, and hiding that is rarely
# (never?) a good idea, so we rethrow them.
raise e
rescue Exception => e
if retries <= max_retries
debug_log("#{options[:operation_name]} failed (Try #{retries} of " +
"#{max_retries}) with:\n" +
"#{e.class}: #{e.message}")
options[:recovery_proc].call if options[:recovery_proc]
retries += 1
else
raise MaxRetriesFailure.new("#{options[:operation_name]} failed (despite retrying " +
"#{max_retries} times) with\n" +
"#{e.class}: #{e.message}")
end
end
end
end
alias :retry_times :retry_action
class TorBootstrapFailure < StandardError
end
def wait_until_tor_is_working
try_for(270) { $vm.execute('/usr/local/sbin/tor-has-bootstrapped').success? }
rescue Timeout::Error
raise TorBootstrapFailure.new('Tor failed to bootstrap')
end
def convert_bytes_mod(unit)
case unit
when "bytes", "b" then mod = 1
when "KB" then mod = 10**3
when "k", "KiB" then mod = 2**10
when "MB" then mod = 10**6
when "M", "MiB" then mod = 2**20
when "GB" then mod = 10**9
when "G", "GiB" then mod = 2**30
when "TB" then mod = 10**12
when "T", "TiB" then mod = 2**40
else
raise "invalid memory unit '#{unit}'"
end
return mod
end
def convert_to_bytes(size, unit)
return (size*convert_bytes_mod(unit)).to_i
end
def convert_to_MiB(size, unit)
return (size*convert_bytes_mod(unit) / (2**20)).to_i
end
def convert_from_bytes(size, unit)
return size.to_f/convert_bytes_mod(unit).to_f
end
def cmd_helper(cmd, env = {})
if cmd.instance_of?(Array)
cmd << {:err => [:child, :out]}
elsif cmd.instance_of?(String)
cmd += " 2>&1"
end
env = ENV.to_h.merge(env)
IO.popen(env, cmd) do |p|
out = p.readlines.join("\n")
p.close
ret = $?
assert_equal(0, ret, "Command failed (returned #{ret}): #{cmd}:\n#{out}")
return out
end
end
def all_tor_hosts
nodes = Array.new
chutney_torrcs = Dir.glob(
"#{$config['TMPDIR']}/chutney-data/nodes/*/torrc"
)
chutney_torrcs.each do |torrc|
open(torrc) do |f|
nodes += f.grep(/^(Or|Dir)Port\b/).map do |line|
{ address: $vmnet.bridge_ip_addr, port: line.split.last.to_i }
end
end
end
return nodes
end
def allowed_hosts_under_tor_enforcement
all_tor_hosts + @lan_hosts
end
def get_free_space(machine, path)
case machine
when 'host'
assert(File.exists?(path), "Path '#{path}' not found on #{machine}.")
free = cmd_helper(["df", path])
when 'guest'
assert($vm.file_exist?(path), "Path '#{path}' not found on #{machine}.")
free = $vm.execute_successfully("df '#{path}'")
else
raise 'Unsupported machine type #{machine} passed.'
end
output = free.split("\n").last
return output.match(/[^\s]\s+[0-9]+\s+[0-9]+\s+([0-9]+)\s+.*/)[1].chomp.to_i
end
def random_string_from_set(set, min_len, max_len)
len = (min_len..max_len).to_a.sample
len ||= min_len
(0..len-1).map { |n| set.sample }.join
end
def random_alpha_string(min_len, max_len = 0)
alpha_set = ('A'..'Z').to_a + ('a'..'z').to_a
random_string_from_set(alpha_set, min_len, max_len)
end
def random_alnum_string(min_len, max_len = 0)
alnum_set = ('A'..'Z').to_a + ('a'..'z').to_a + (0..9).to_a.map { |n| n.to_s }
random_string_from_set(alnum_set, min_len, max_len)
end
# Sanitize the filename from unix-hostile filename characters
def sanitize_filename(filename, options = {})
options[:replacement] ||= '_'
bad_unix_filename_chars = Regexp.new("[^A-Za-z0-9_\\-.,+:]")
filename.gsub(bad_unix_filename_chars, options[:replacement])
end
def info_log_artifact_location(type, path)
if $config['ARTIFACTS_BASE_URI']
# Remove any trailing slashes, we'll add one ourselves
base_url = $config['ARTIFACTS_BASE_URI'].gsub(/\/*$/, "")
path = "#{base_url}/#{File.basename(path)}"
end
info_log("#{type.capitalize}: #{path}")
end
def notify_user(message)
alarm_script = $config['NOTIFY_USER_COMMAND']
return if alarm_script.nil? || alarm_script.empty?
cmd_helper(alarm_script.gsub('%m', message))
end
def pause(message = "Paused")
notify_user(message)
STDERR.puts
STDERR.puts message
# Ring the ASCII bell for a helpful notification in most terminal
# emulators.
STDOUT.write "\a"
STDERR.puts
loop do
STDERR.puts "Return: Continue; d: Debugging REPL"
c = STDIN.getch
case c
when "\r"
return
when "d"
binding.pry(quiet: true)
end
end
end
def dbus_send(service, object_path, method, *args, **opts)
opts ||= {}
ruby_type_to_dbus_type = {
String => 'string',
Fixnum => 'int32',
}
typed_args = args.map do |arg|
type = ruby_type_to_dbus_type[arg.class]
assert_not_nil(type, "No DBus type conversion for Ruby type '#{arg.class}'")
"#{type}:#{arg}"
end
ret = $vm.execute_successfully(
"dbus-send --print-reply --dest=#{service} #{object_path} " +
" #{method} #{typed_args.join(' ')}",
**opts
).stdout.lines
# The first line written is about timings and other stuff we don't
# care about; we only care about the return values.
ret.shift
ret.map! do |s|
type, val = /^\s*(\S+)\s+(\S+)$/.match(s)[1,2]
case type
when 'string'
# Unquote
val[1, val.length - 2]
when 'int32'
val.to_i
else
raise "No Ruby type conversion for DBus type '#{type}'"
end
end
if ret.size == 0
return nil
elsif ret.size == 1
return ret.first
else
return ret
end
end
|