vmth 0.0.1
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.
- 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
|
+
|