vmth 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +1 -0
- data/CONFIG.rdoc +7 -0
- data/DESCRIPTION +1 -0
- data/LICENSE +202 -0
- data/Manifest +18 -0
- data/QUICKSTART.rdoc +44 -0
- data/README.rdoc +37 -0
- data/Rakefile +22 -0
- data/bin/virb +41 -0
- data/bin/vmth +232 -0
- data/lib/defaults.yaml +28 -0
- data/lib/virb.rb +53 -0
- data/lib/vmth.rb +558 -0
- data/sample_config.yaml +54 -0
- data/test/helpers.rb +5 -0
- data/test/test_virb.rb +13 -0
- data/test/test_vmth.rb +25 -0
- data/vmth.gemspec +65 -0
- metadata +160 -0
- metadata.gz.sig +2 -0
data/lib/defaults.yaml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# This is the defaults file for the vmth. You can use this to set system-wide
|
2
|
+
# defaults for testing, for stuff that changes frequently you should specify
|
3
|
+
# a config.yaml on the command line.
|
4
|
+
vmm:
|
5
|
+
cmdline: qemu-kvm -usb -usbdevice tablet -m 1024 -smp 1 -hda <%=@image_file%> -vnc :<%=@vm_vnc_port%> -net nic,macaddr=<%=@vm_mac_addr%> -net user -redir tcp:<%=@vm_ssh_port%>::22 -no-reboot -monitor stdio
|
6
|
+
loadinit: loadvm init-test
|
7
|
+
saveteststate: savevm test-freeze
|
8
|
+
loadteststate: loadvm test-freeze
|
9
|
+
quitvmm: quit
|
10
|
+
ssh_port_start: 2224
|
11
|
+
ssh_port_end: 2233
|
12
|
+
vnc_port_start: 5903
|
13
|
+
vnc_port_end: 5912
|
14
|
+
mca_start: "00:50:56:36:b3:"
|
15
|
+
timeout: 30
|
16
|
+
prompt: "(qemu)"
|
17
|
+
ssh:
|
18
|
+
host: localhost
|
19
|
+
user: root
|
20
|
+
auth_methods: password
|
21
|
+
password: ""
|
22
|
+
paranoid: false
|
23
|
+
init_scenario: 0nulltest
|
24
|
+
prep:
|
25
|
+
applying:
|
26
|
+
testing:
|
27
|
+
teardown:
|
28
|
+
|
data/lib/virb.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright 2011 Greg Retkowski
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#++
|
18
|
+
|
19
|
+
require 'yaml'
|
20
|
+
|
21
|
+
|
22
|
+
def load_obj(filename)
|
23
|
+
$y = YAML.load_file(filename)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
def p_apply(service)
|
27
|
+
puts $y['tests'][service]['apply']
|
28
|
+
true
|
29
|
+
end
|
30
|
+
def p_test(service)
|
31
|
+
puts $y['tests'][service]['test']
|
32
|
+
true
|
33
|
+
|
34
|
+
end
|
35
|
+
# Write out the output of the 'apply' stage to a file.
|
36
|
+
def f_apply(service,file)
|
37
|
+
File.open(file,'w') do |f|
|
38
|
+
f.puts $y['tests'][service]['apply']
|
39
|
+
end
|
40
|
+
end
|
41
|
+
def helpme()
|
42
|
+
use = []
|
43
|
+
use << "Usage:"
|
44
|
+
use << ""
|
45
|
+
use << "load_obj 'filename' # Load the output of your vmth run"
|
46
|
+
use << "p_apply 'scenario' # Shows output of apply step"
|
47
|
+
use << "p_test 'scenario' # Shows output of test step"
|
48
|
+
use << "f_apply 'scenario','filename' # Write out a scenario's output to a file"
|
49
|
+
use << "helpme() # This help message"
|
50
|
+
return use.join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
puts helpme()
|
data/lib/vmth.rb
ADDED
@@ -0,0 +1,558 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright 2011 Greg Retkowski
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#++
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'net/ssh'
|
21
|
+
require 'net/scp'
|
22
|
+
require 'pty'
|
23
|
+
require 'expect'
|
24
|
+
require 'fileutils'
|
25
|
+
require 'tempfile'
|
26
|
+
require 'erb'
|
27
|
+
|
28
|
+
|
29
|
+
class VmthError < RuntimeError;end
|
30
|
+
|
31
|
+
=begin rdoc
|
32
|
+
This class provides a VM test harness to allow testing of operational
|
33
|
+
code (puppet policies, chef configs, etc..) against a environment
|
34
|
+
similar to your production environment.
|
35
|
+
|
36
|
+
The VM test harness uses features of the VM monitor (qemu) to freeze
|
37
|
+
and re-use system memory/disk state so that a series of test scenarios
|
38
|
+
can be rapidly tested.
|
39
|
+
|
40
|
+
This class provides all the logic to implement the VM test harness. It
|
41
|
+
manages the VM, loads and runs tests for each scenario, and produces
|
42
|
+
a 'results' hash with the results of the test.
|
43
|
+
=end
|
44
|
+
|
45
|
+
class Vmth
|
46
|
+
# Set the directory where your puppet code directory is.
|
47
|
+
attr_accessor :source_dir
|
48
|
+
# A boolean, should we use QEMU or not? Should almost always be true.
|
49
|
+
attr_accessor :vmm_enabled
|
50
|
+
# The machdb services.yaml location. Describes which services should be tested.
|
51
|
+
attr_accessor :scenarios_file
|
52
|
+
# The system/disk image file booted by QEMU
|
53
|
+
attr_accessor :image_file
|
54
|
+
# Contains the hash of all test output and results. Read this after
|
55
|
+
# you've completed your test run.
|
56
|
+
attr_reader :results
|
57
|
+
# So we can flush this if there's an error.
|
58
|
+
attr_accessor :vmm_r
|
59
|
+
attr_reader :options
|
60
|
+
#
|
61
|
+
# new takes no arguments.
|
62
|
+
#
|
63
|
+
DEFAULT_OPTIONS={
|
64
|
+
:source_path => ".",
|
65
|
+
:vmm_enabled => true,
|
66
|
+
:config_file => nil,
|
67
|
+
:scenarios_file => nil,
|
68
|
+
:image_file => nil,
|
69
|
+
:debug => false,
|
70
|
+
:action => 'all',
|
71
|
+
:outfile => self.class.to_s.downcase+"_out.yaml",
|
72
|
+
:out_format => 'text',
|
73
|
+
:services => []
|
74
|
+
}
|
75
|
+
|
76
|
+
def initialize(options={})
|
77
|
+
@options=DEFAULT_OPTIONS.merge(options)
|
78
|
+
@config = YAML.load_file(File.dirname(__FILE__)+'/defaults.yaml')
|
79
|
+
if @options[:config_file]
|
80
|
+
@config.merge!(YAML.load_file(@options[:config_file]))
|
81
|
+
end
|
82
|
+
@log = Logger.new(STDERR)
|
83
|
+
if @options[:debug]
|
84
|
+
@log.level = Logger::DEBUG
|
85
|
+
else
|
86
|
+
@log.level = Logger::WARN
|
87
|
+
end
|
88
|
+
@tmp_state = Tempfile.new("pth").path
|
89
|
+
@results = {
|
90
|
+
'tests' => {}
|
91
|
+
}
|
92
|
+
ssh_port_range=@config['vmm']['ssh_port_start']..@config['vmm']['ssh_port_end']
|
93
|
+
@vm_ssh_port = Vmth.allocate_tcp_port(ssh_port_range)
|
94
|
+
vnc_port_range=@config['vmm']['vnc_port_start']..@config['vmm']['vnc_port_end']
|
95
|
+
@vm_vnc_port = Vmth.allocate_tcp_port(vnc_port_range) - 5900
|
96
|
+
@vm_mac_addr = @config['vmm']['mca_start'] + "%02x" % (rand()*256).round
|
97
|
+
@vmm_prompt = eb(@config['vmm']['prompt'])
|
98
|
+
@vmm_timeout = @config['vmm']['timeout']
|
99
|
+
@image_file=@options[:image_file]
|
100
|
+
@source_path=@options[:source_path]
|
101
|
+
# Try to cleanly shutdown the vmm.
|
102
|
+
trap("INT") do
|
103
|
+
if @vm_running
|
104
|
+
stop_vm()
|
105
|
+
end
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
end
|
109
|
+
# Expand a string with ERB.
|
110
|
+
def eb(string)
|
111
|
+
renderer = ERB.new(string)
|
112
|
+
return renderer.result(binding)
|
113
|
+
end
|
114
|
+
# Change the loglevel of the logger. Argument should
|
115
|
+
# be a loglevel constant, i.e. Logger::INFO
|
116
|
+
def loglevel=(level)
|
117
|
+
@log.level=level
|
118
|
+
end
|
119
|
+
# Test all testable services - this is indicated by if a service in machdb
|
120
|
+
# has the 'testable' field set to true. It takes no arguments and returns
|
121
|
+
# an array of booleans, indicating the success or failure of tests. You
|
122
|
+
# should query results() for your results.
|
123
|
+
def test_all
|
124
|
+
@results['test_start'] = Time.now()
|
125
|
+
passed = []
|
126
|
+
boot_vm() if @options[:vmm_enabled]
|
127
|
+
prep
|
128
|
+
freeze_vm() if @options[:vmm_enabled]
|
129
|
+
@log.info "RUNNING NO-SERVICE TEST"
|
130
|
+
passed << one_test(@config['init_scenario'])
|
131
|
+
# Stop testing if our initial test fails.
|
132
|
+
unless passed.first == true
|
133
|
+
@log.error "Initial setup failed.. sleeping 60 seconds for debugging."
|
134
|
+
sleep 60
|
135
|
+
stop_vm() if @options[:vmm_enabled]
|
136
|
+
return passed
|
137
|
+
end
|
138
|
+
freeze_vm() if @options[:vmm_enabled]
|
139
|
+
@log.info "RUNNING TESTS"
|
140
|
+
scenarios = get_scenarios
|
141
|
+
test_counter = 0
|
142
|
+
scenarios.each do |scenario|
|
143
|
+
test_counter += 1
|
144
|
+
@log.info "Running test for #{scenario} - #{test_counter} of #{scenarios.size}"
|
145
|
+
passed << one_test(scenario)
|
146
|
+
end
|
147
|
+
stop_vm() if @config[:vmm_enabled]
|
148
|
+
all_passed = passed.select{|p| p == false}.size == 0
|
149
|
+
@log.info "Number of tests run : #{passed.size}"
|
150
|
+
@log.info "Result of ALL tests: Passed? #{all_passed}"
|
151
|
+
@results['test_stop'] = Time.now()
|
152
|
+
@results['elapsed_time'] = @results['test_stop'] - @results['test_start']
|
153
|
+
return all_passed
|
154
|
+
end
|
155
|
+
alias :test :test_all
|
156
|
+
|
157
|
+
# Set up a vm, and drop it off for a developer to use.
|
158
|
+
def console
|
159
|
+
create_private_disk
|
160
|
+
@results['test_start'] = Time.now()
|
161
|
+
passed = []
|
162
|
+
boot_vm() if @options[:vmm_enabled]
|
163
|
+
prep
|
164
|
+
freeze_vm() if @options[:vmm_enabled]
|
165
|
+
# Print out ssh & vnc port, and freeze name.
|
166
|
+
@log.info "Handing off VM to you.. Type #{@config['vmm']['quitvmm']} to end session."
|
167
|
+
@log.info "Ports - SSH: #{@vm_ssh_port} VNC: #{@vm_vnc_port}"
|
168
|
+
|
169
|
+
# hand off console.
|
170
|
+
print @config['vmm']['prompt']
|
171
|
+
begin
|
172
|
+
system('stty raw -echo')
|
173
|
+
Thread.new{ loop { @vmm_w.print $stdin.getc.chr } }
|
174
|
+
loop { $stdout.print @vmm_r.readpartial(512); STDOUT.flush }
|
175
|
+
rescue
|
176
|
+
nil # User probably caused the VMM to exit.
|
177
|
+
ensure
|
178
|
+
system "stty -raw echo"
|
179
|
+
end
|
180
|
+
# Done via the user?
|
181
|
+
# stop_vm()
|
182
|
+
cleanup_private_disk
|
183
|
+
return
|
184
|
+
end
|
185
|
+
def create_private_disk
|
186
|
+
@orig_image_file = @image_file
|
187
|
+
@image_file = "#{@orig_image_file}.#{$$}"
|
188
|
+
@log.debug "Copying #{@orig_image_file} to #{@image_file}"
|
189
|
+
FileUtils.cp(@orig_image_file,@image_file)
|
190
|
+
end
|
191
|
+
def cleanup_private_disk
|
192
|
+
@log.debug "Removing tmp imagefile #{@image_file}"
|
193
|
+
if defined?(@orig_image_file) and @orig_image_file != @image_file
|
194
|
+
File.delete(@image_file)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Cleanup state file, but only if everything is done!
|
199
|
+
def cleanup
|
200
|
+
File.delete(@tmp_state) rescue nil
|
201
|
+
end
|
202
|
+
# Really only for development/testing of this class.
|
203
|
+
# Will run tests against an already running VM (presumably the
|
204
|
+
# developer is running it in another window)
|
205
|
+
def test_without_vm
|
206
|
+
prep
|
207
|
+
test_services
|
208
|
+
end
|
209
|
+
# Test a bunch of services. Pass in an array containing the names
|
210
|
+
# of services to test. Returns an array of booleans, indicating
|
211
|
+
# the success or failure of the tests. You should read detailed
|
212
|
+
# results from results()
|
213
|
+
def test_services(services)
|
214
|
+
@results['test_start'] = Time.now()
|
215
|
+
boot_vm() if @options[:vmm_enabled]
|
216
|
+
prep
|
217
|
+
freeze_vm() if @options[:vmm_enabled]
|
218
|
+
passed = []
|
219
|
+
@log.info "RUNNING NO-SERVICE TEST"
|
220
|
+
passed << one_test(eb(@config["init_scenario"]))
|
221
|
+
# Stop testing if our initial test fails.
|
222
|
+
unless passed.first == true
|
223
|
+
stop_vm() if @options[:vmm_enabled]
|
224
|
+
return passed
|
225
|
+
end
|
226
|
+
freeze_vm() if @options[:vmm_enabled]
|
227
|
+
@log.info "RUNNING TESTS"
|
228
|
+
test_counter = 0
|
229
|
+
services.each do |service|
|
230
|
+
test_counter += 1
|
231
|
+
@log.info "Running test for #{service} - #{test_counter} of #{services.size}"
|
232
|
+
passed << one_test(service)
|
233
|
+
end
|
234
|
+
stop_vm() if @options[:vmm_enabled]
|
235
|
+
@results['test_stop'] = Time.now()
|
236
|
+
@results['elapsed_time']= @results['test_stop'] - @results['test_start']
|
237
|
+
return passed
|
238
|
+
end
|
239
|
+
# Return the command-line that would have been used to start QEMU.
|
240
|
+
# This can be used for developing this library, or to get a new
|
241
|
+
# disk image prepped to be used with the test harness.
|
242
|
+
def vmcl
|
243
|
+
return vmm_command_line
|
244
|
+
end
|
245
|
+
#
|
246
|
+
# START PRIVATE METHODS
|
247
|
+
#
|
248
|
+
private
|
249
|
+
# This starts the QEMU instance for the test VM. Spawns the VM
|
250
|
+
# and then sets @qemu_r (read socket for qemu), @qemu_w (write
|
251
|
+
# socket for qemu) and @qemu_pid (qemu process ID).
|
252
|
+
# These class variables are used to interact with the QEMU
|
253
|
+
# supervisor.
|
254
|
+
def start_vm
|
255
|
+
unless File.exists?(@image_file) and File.owned?(@image_file)
|
256
|
+
@log.error "Image file #{@image_file} doesn't exist or is not owned by you!"
|
257
|
+
exit 255
|
258
|
+
end
|
259
|
+
@log.info "VM Will use SSH Port #{@vm_ssh_port} and VNC Port #{@vm_vnc_port}"
|
260
|
+
@log.info "Starting vmm now..."
|
261
|
+
@log.debug "vmm command line is: " + vmm_command_line()
|
262
|
+
@vmm_r, @vmm_w, @vmm_pid = PTY.spawn vmm_command_line()
|
263
|
+
@vmm_r.expect(@vmm_prompt,@vmm_timeout) do |line|
|
264
|
+
true
|
265
|
+
end
|
266
|
+
@vm_running = true
|
267
|
+
@log.debug "vmm instance pid is #{@vmm_pid}"
|
268
|
+
end
|
269
|
+
# Read in the scenarios file and return it as an array.
|
270
|
+
def get_scenarios
|
271
|
+
scenarios = []
|
272
|
+
File.open(@options[:scenarios_file]) do |f|
|
273
|
+
f.each_line do |line|
|
274
|
+
scenarios << line.chomp
|
275
|
+
end
|
276
|
+
end
|
277
|
+
return scenarios.sort
|
278
|
+
end
|
279
|
+
# Returns a command-line for invoking QEMU. Used by
|
280
|
+
# start_qemu
|
281
|
+
def vmm_command_line
|
282
|
+
return eb(@config['vmm']['cmdline'])
|
283
|
+
end
|
284
|
+
# Stops the vmm process. First tries to issue the 'quit'
|
285
|
+
# command on the qemu console.
|
286
|
+
def stop_vm
|
287
|
+
exit_status = nil
|
288
|
+
@vm_running = false
|
289
|
+
begin
|
290
|
+
exit_status = vmm_command(eb(@config['vmm']['quitvmm']))
|
291
|
+
sleep 1
|
292
|
+
# Check to see if it is still running.
|
293
|
+
is_alive = (Process.kill(0, @vmm_pid) rescue 0)
|
294
|
+
if is_alive != 0
|
295
|
+
@log.warn "Warning, vmm didn't die.. killing manually"
|
296
|
+
Process.kill("TERM",@vmm_pid)
|
297
|
+
sleep 2
|
298
|
+
end
|
299
|
+
rescue PTY::ChildExited
|
300
|
+
true # expected
|
301
|
+
end
|
302
|
+
return exit_status
|
303
|
+
end
|
304
|
+
# Issue a command to the QEMU supervisor. Used
|
305
|
+
# for saving or restoring VM state between tests.
|
306
|
+
def vmm_command(command)
|
307
|
+
return nil unless @options[:vmm_enabled]
|
308
|
+
result = nil
|
309
|
+
@log.debug "Issuing '#{command}' to vmm"
|
310
|
+
return nil unless @vmm_w
|
311
|
+
@vmm_w.puts("#{command}\n")
|
312
|
+
begin
|
313
|
+
@vmm_r.expect(@vmm_prompt,@vmm_timeout) do |line|
|
314
|
+
@log.debug "Expect line was: #{line}"
|
315
|
+
result = line
|
316
|
+
end
|
317
|
+
# Handle quick exit on 'quit' commands.
|
318
|
+
rescue PTY::ChildExited, Errno::EIO => e
|
319
|
+
if command == eb(@config['vmm']['quitvmm'])
|
320
|
+
@log.debug "Command 'quit' exited before completion."
|
321
|
+
else
|
322
|
+
raise e
|
323
|
+
end
|
324
|
+
end
|
325
|
+
@log.debug "Command completed with result '#{result}'"
|
326
|
+
return result
|
327
|
+
end
|
328
|
+
def one_test(service)
|
329
|
+
reset_vm
|
330
|
+
passed = true
|
331
|
+
@log.info "Running test for #{service}"
|
332
|
+
passed = run_vmth_test(service)
|
333
|
+
@log.info "Did it pass?: #{passed}"
|
334
|
+
return passed
|
335
|
+
end
|
336
|
+
# Run a command on a ssh channel. Return false if we get a
|
337
|
+
# match on bad_match - otherwise return true. bad_match
|
338
|
+
# is used to pattern match text that indicates a bad exit
|
339
|
+
# state - used when running something that'll trip a test
|
340
|
+
def run_on_channel(session,command,bad_match)
|
341
|
+
if bad_match.class == Regexp
|
342
|
+
bad_match_regexp = bad_match
|
343
|
+
else
|
344
|
+
bad_match_regexp = /#{bad_match}/
|
345
|
+
end
|
346
|
+
output = []
|
347
|
+
test_passed = true
|
348
|
+
@log.debug "Running #{command}"
|
349
|
+
session.open_channel do |ch|
|
350
|
+
ch.exec command do |ch, success|
|
351
|
+
unless success
|
352
|
+
@log.info "could not execute #{command}"
|
353
|
+
test_passed = false
|
354
|
+
end
|
355
|
+
ch.on_data do |ch, data|
|
356
|
+
@log.debug data
|
357
|
+
output << data
|
358
|
+
if data =~ bad_match_regexp
|
359
|
+
test_passed = false
|
360
|
+
end
|
361
|
+
end
|
362
|
+
# Test failed if program/script exited nonzero
|
363
|
+
ch.on_request("exit-status") do |ch,data|
|
364
|
+
exit_code = data.read_long
|
365
|
+
@log.debug "Command exited with #{exit_code.to_s}"
|
366
|
+
if exit_code != 0
|
367
|
+
test_passed = false
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
# Causes this to block until the command completes.
|
373
|
+
session.loop
|
374
|
+
# So far if there's no output, the command failed..
|
375
|
+
if output.empty?
|
376
|
+
test_passed = false
|
377
|
+
end
|
378
|
+
return ({"passed" => test_passed, "output" => output.join("\n") })
|
379
|
+
end
|
380
|
+
|
381
|
+
# Run a test for a specific scenario on the guest VM. Will set 'service'
|
382
|
+
# class on the VM and then execute puppet - which will invoke all
|
383
|
+
# rules related to that class. It will then execute any unit
|
384
|
+
# tests associated with that service.
|
385
|
+
# Fills in the @results instance variable with information
|
386
|
+
# about the test then returns true|false indicating pass|fail
|
387
|
+
def run_vmth_test(scenario)
|
388
|
+
@scenario=scenario
|
389
|
+
service=scenario # legacy/lazy
|
390
|
+
start_timer = Time.now()
|
391
|
+
@results['tests'][service] = {}
|
392
|
+
test_passed = true
|
393
|
+
begin
|
394
|
+
ssh_session do |session|
|
395
|
+
@results['tests'][service]['apply'] =
|
396
|
+
_recursor(@config['applying'],session)
|
397
|
+
@results['tests'][service]['test'] =
|
398
|
+
_recursor(@config['testing'],session)
|
399
|
+
@results['tests'][service]['passed'] = @results['tests'][service]['apply']['passed'] \
|
400
|
+
and @results['tests'][service]['test']['passed']
|
401
|
+
_recursor(@config['teardown'],session)
|
402
|
+
@results['tests'][service]['teardown'] =
|
403
|
+
_recursor(@config['teardown'],session)
|
404
|
+
end
|
405
|
+
rescue => e
|
406
|
+
# If anything was raised here it is big problems yo.
|
407
|
+
@results['tests'][service]['apply'] ||= {}
|
408
|
+
@results['tests'][service]['apply']['passed'] = false
|
409
|
+
@results['tests'][service]['test'] ||= {}
|
410
|
+
@results['tests'][service]['test']['passed'] = false
|
411
|
+
@results['tests'][service]['passed'] = false
|
412
|
+
@results['tests'][service]['error'] = {
|
413
|
+
'class' => e.class.to_s, 'message' => e.message, 'backtrace' => e.backtrace
|
414
|
+
}
|
415
|
+
end
|
416
|
+
@results['tests'][service]['elapsed_time'] = (Time.now() - start_timer)
|
417
|
+
write_out_state()
|
418
|
+
return @results['tests'][service]['passed']
|
419
|
+
end
|
420
|
+
|
421
|
+
# Write out a state file, handy for debugging later.
|
422
|
+
def write_out_state
|
423
|
+
if @options[:out_file]
|
424
|
+
filename = @options[:out_file]
|
425
|
+
else
|
426
|
+
filename = @tmp_state
|
427
|
+
end
|
428
|
+
@log.debug "Writing out state into #{filename}"
|
429
|
+
File.open(filename,'w') do |f|
|
430
|
+
f.puts YAML.dump(@results)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
# Starts the QEMU instance and then immediately loads the saved
|
434
|
+
# VM via 'loadvm foo'
|
435
|
+
def boot_vm
|
436
|
+
start_vm()
|
437
|
+
@log.debug "Loading initial vm..."
|
438
|
+
vmm_command(eb(@config['vmm']['loadinit']))
|
439
|
+
end
|
440
|
+
# Freeze the current state of the VM - so we can use it later
|
441
|
+
# to reset the VM so that it is immediately ready for the next test.
|
442
|
+
def freeze_vm()
|
443
|
+
@log.debug "Freezing vm for test series"
|
444
|
+
vmm_command(eb(@config['vmm']['saveteststate']))
|
445
|
+
end
|
446
|
+
# Reset the VM for the next test - using the instance saved by 'freeze'
|
447
|
+
def reset_vm()
|
448
|
+
@log.debug "Reseting vm for next test"
|
449
|
+
vmm_command(eb(@config['vmm']['loadteststate']))
|
450
|
+
# Give it a half a tic to reset...
|
451
|
+
sleep 0.5
|
452
|
+
end
|
453
|
+
# Set up an ssh session.
|
454
|
+
def ssh_session
|
455
|
+
retry_flag=true
|
456
|
+
@log.debug "ssh is #{@config['ssh'].inspect}"
|
457
|
+
ssh_config = @config['ssh'].clone
|
458
|
+
host = ssh_config['host']
|
459
|
+
ssh_config.delete('host')
|
460
|
+
user = ssh_config['user']
|
461
|
+
ssh_config.delete('user')
|
462
|
+
# Convert strings to symbols..
|
463
|
+
ssh_config = ssh_config.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
464
|
+
ssh_config[:port] = @vm_ssh_port
|
465
|
+
begin
|
466
|
+
Net::SSH.start(host,user,ssh_config) do |session|
|
467
|
+
yield session
|
468
|
+
end
|
469
|
+
rescue EOFError => e
|
470
|
+
raise(e) unless retry_flag
|
471
|
+
retry_flag = false
|
472
|
+
@log.info "SSH session creation failed, retrying!"
|
473
|
+
retry
|
474
|
+
end
|
475
|
+
end
|
476
|
+
# This function executes all the commands on the just-started VM to
|
477
|
+
# sync over all files and state needed before testing can start.
|
478
|
+
def prep
|
479
|
+
ssh_session do |session|
|
480
|
+
@results['prep'] = _recursor(@config['prep'],session)
|
481
|
+
end
|
482
|
+
@log.info "FINISHED PREP STEPS...."
|
483
|
+
end
|
484
|
+
|
485
|
+
private
|
486
|
+
# Allocate a TCP port.
|
487
|
+
def self.allocate_tcp_port(valid_ports=[])
|
488
|
+
last_error = ArgumentError.new("Port range not given.")
|
489
|
+
# Try to bind to each port until we don't error out
|
490
|
+
# because of permission or it already being used.
|
491
|
+
valid_ports.each do |port|
|
492
|
+
begin
|
493
|
+
s = TCPServer.open('0.0.0.0',port)
|
494
|
+
s.close
|
495
|
+
return port
|
496
|
+
rescue => e
|
497
|
+
last_error = e
|
498
|
+
next
|
499
|
+
end
|
500
|
+
end
|
501
|
+
# If we can't allocate a port raise an error.
|
502
|
+
raise last_error.class, last_error.message
|
503
|
+
end
|
504
|
+
# Execute commands on vm, recurse if required.
|
505
|
+
def _recursor(cmds,session)
|
506
|
+
results = []
|
507
|
+
passed = true
|
508
|
+
@log.debug "Processing #{cmds.inspect}"
|
509
|
+
cmds.each do |myhash|
|
510
|
+
if myhash.size != 1
|
511
|
+
@log.error "Config format problem with #{myhash.inspect}"
|
512
|
+
raise VmthError
|
513
|
+
end
|
514
|
+
cmd = myhash.keys.first
|
515
|
+
values = myhash[cmd]
|
516
|
+
@log.debug "Values is #{values.inspect}"
|
517
|
+
if cmd=='foreach'
|
518
|
+
args = values.shift['args']
|
519
|
+
args.each do |arg|
|
520
|
+
@log.debug "Arg is #{arg.inspect}"
|
521
|
+
@arg = arg
|
522
|
+
res_hash = _recursor(values,session)
|
523
|
+
results << res_hash['output']
|
524
|
+
passed = res_hash['passed'] and passed
|
525
|
+
end
|
526
|
+
elsif cmd=='cmd'
|
527
|
+
command_string = eb(values)+" 2>&1"
|
528
|
+
@log.debug "Running on vm.. '#{command_string}"
|
529
|
+
result = session.exec!(command_string)
|
530
|
+
@log.debug "output is: #{result}"
|
531
|
+
results << result
|
532
|
+
elsif %{upload download upload_recurse download_recurse}.include?(cmd)
|
533
|
+
first=eb(values[0])
|
534
|
+
second=eb(values[1])
|
535
|
+
@log.debug "File transfer with #{first} => #{second}"
|
536
|
+
if cmd=='upload'
|
537
|
+
results << session.scp.upload!(first,second)
|
538
|
+
elsif cmd=='upload_recurse'
|
539
|
+
results << session.scp.upload!(first,second,{:recursive=>true})
|
540
|
+
elsif cmd=='download'
|
541
|
+
results << session.scp.download!(first,second )
|
542
|
+
elsif cmd=='download_recurse'
|
543
|
+
results << session.scp.download!(first,second,{:recursive=>true})
|
544
|
+
end
|
545
|
+
elsif cmd=='cmdtest'
|
546
|
+
res_hash = run_on_channel(session,eb(values[0]),values[1])
|
547
|
+
results << res_hash['output']
|
548
|
+
passed = res_hash['passed'] and passed
|
549
|
+
else
|
550
|
+
@log.error "unknown command #{cmd.inspect}"
|
551
|
+
raise VmthError
|
552
|
+
end
|
553
|
+
end
|
554
|
+
return {'output'=>results,'passed'=>passed}
|
555
|
+
end
|
556
|
+
end # Class
|
557
|
+
|
558
|
+
|