vmth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -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()
@@ -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
+