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.
@@ -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
+