Class: Vmth

Inherits:
Object
  • Object
show all
Defined in:
lib/vmth.rb

Overview

This class provides a VM test harness to allow testing of operational code (puppet policies, chef configs, etc..) against a environment similar to your production environment.

The VM test harness uses features of the VM monitor (qemu) to freeze and re-use system memory/disk state so that a series of test scenarios can be rapidly tested.

This class provides all the logic to implement the VM test harness. It manages the VM, loads and runs tests for each scenario, and produces a ‘results’ hash with the results of the test.

Constant Summary collapse

DEFAULT_OPTIONS =

new takes no arguments.

{
  :source_path => ".",
  :vmm_enabled => true,
  :config_file => nil,
  :scenarios_file => nil,
  :image_file => nil,
  :debug => false,
  :action => 'all',
  :outfile => self.class.to_s.downcase+"_out.yaml",
  :out_format => 'text',
  :services => []
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Vmth

Returns a new instance of Vmth.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/vmth.rb', line 76

def initialize(options={})
  @options=DEFAULT_OPTIONS.merge(options)
  @config = YAML.load_file(File.dirname(__FILE__)+'/defaults.yaml')
  if @options[:config_file]
    @config.merge!(YAML.load_file(@options[:config_file]))
  end
  @log = Logger.new(STDERR)
  if @options[:debug]
    @log.level = Logger::DEBUG
  else
    @log.level = Logger::WARN
  end
  @tmp_state = Tempfile.new("pth").path
  @results = {
    'tests' => {}
  }
  ssh_port_range=@config['vmm']['ssh_port_start']..@config['vmm']['ssh_port_end']
  @vm_ssh_port = Vmth.allocate_tcp_port(ssh_port_range)
  vnc_port_range=@config['vmm']['vnc_port_start']..@config['vmm']['vnc_port_end']
  @vm_vnc_port = Vmth.allocate_tcp_port(vnc_port_range) - 5900
  @vm_mac_addr = @config['vmm']['mca_start'] + "%02x" % (rand()*256).round
  @vmm_prompt = eb(@config['vmm']['prompt'])
  @vmm_timeout = @config['vmm']['timeout']
  @image_file=@options[:image_file]
  @source_path=@options[:source_path]
  # Try to cleanly shutdown the vmm.
  trap("INT") do
    if @vm_running
      stop_vm()
    end
    raise
  end
end

Instance Attribute Details

#image_fileObject

The system/disk image file booted by QEMU



53
54
55
# File 'lib/vmth.rb', line 53

def image_file
  @image_file
end

#optionsObject (readonly)

Returns the value of attribute options.



59
60
61
# File 'lib/vmth.rb', line 59

def options
  @options
end

#resultsObject (readonly)

Contains the hash of all test output and results. Read this after you’ve completed your test run.



56
57
58
# File 'lib/vmth.rb', line 56

def results
  @results
end

#scenarios_fileObject

The machdb services.yaml location. Describes which services should be tested.



51
52
53
# File 'lib/vmth.rb', line 51

def scenarios_file
  @scenarios_file
end

#source_dirObject

Set the directory where your puppet code directory is.



47
48
49
# File 'lib/vmth.rb', line 47

def source_dir
  @source_dir
end

#vmm_enabledObject

A boolean, should we use QEMU or not? Should almost always be true.



49
50
51
# File 'lib/vmth.rb', line 49

def vmm_enabled
  @vmm_enabled
end

#vmm_rObject

So we can flush this if there’s an error.



58
59
60
# File 'lib/vmth.rb', line 58

def vmm_r
  @vmm_r
end

Instance Method Details

#cleanupObject

Cleanup state file, but only if everything is done!



199
200
201
# File 'lib/vmth.rb', line 199

def cleanup
  File.delete(@tmp_state) rescue nil
end

#cleanup_private_diskObject



191
192
193
194
195
196
# File 'lib/vmth.rb', line 191

def cleanup_private_disk
  @log.debug "Removing tmp imagefile #{@image_file}"
  if defined?(@orig_image_file) and @orig_image_file != @image_file
    File.delete(@image_file)
  end
end

#consoleObject

Set up a vm, and drop it off for a developer to use.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/vmth.rb', line 158

def console
  create_private_disk
  @results['test_start'] = Time.now()
  passed = []
  boot_vm() if @options[:vmm_enabled]
  prep
  freeze_vm() if @options[:vmm_enabled]
  # Print out ssh & vnc port, and freeze name.
  @log.info "Handing off VM to you.. Type #{@config['vmm']['quitvmm']} to end session."
  @log.info "Ports - SSH: #{@vm_ssh_port}  VNC: #{@vm_vnc_port}"

  # hand off console.
  print @config['vmm']['prompt']
  begin
    system('stty raw -echo')
    Thread.new{ loop { @vmm_w.print $stdin.getc.chr } }
    loop { $stdout.print @vmm_r.readpartial(512); STDOUT.flush }
  rescue
    nil # User probably caused the VMM to exit.
  ensure
    system "stty -raw echo"
  end
  # Done via the user?
  # stop_vm()
  cleanup_private_disk
  return
end

#create_private_diskObject



185
186
187
188
189
190
# File 'lib/vmth.rb', line 185

def create_private_disk
  @orig_image_file = @image_file
  @image_file = "#{@orig_image_file}.#{$$}"
  @log.debug "Copying #{@orig_image_file} to #{@image_file}"
  FileUtils.cp(@orig_image_file,@image_file)
end

#eb(string) ⇒ Object

Expand a string with ERB.



110
111
112
113
# File 'lib/vmth.rb', line 110

def eb(string)
  renderer = ERB.new(string)
  return renderer.result(binding)
end

#loglevel=(level) ⇒ Object

Change the loglevel of the logger. Argument should be a loglevel constant, i.e. Logger::INFO



116
117
118
# File 'lib/vmth.rb', line 116

def loglevel=(level)
  @log.level=level
end

#test_allObject Also known as: test

Test all testable services - this is indicated by if a service in machdb has the ‘testable’ field set to true. It takes no arguments and returns an array of booleans, indicating the success or failure of tests. You should query results() for your results.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/vmth.rb', line 123

def test_all
  @results['test_start'] = Time.now()
  passed = []
  boot_vm() if @options[:vmm_enabled]
  prep
  freeze_vm() if @options[:vmm_enabled]
  @log.info "RUNNING NO-SERVICE TEST"
  passed << one_test(@config['init_scenario'])
  # Stop testing if our initial test fails.
  unless passed.first == true
    @log.error "Initial setup failed.. sleeping 60 seconds for debugging."
    sleep 60
    stop_vm() if @options[:vmm_enabled]
    return passed
  end
  freeze_vm() if @options[:vmm_enabled]
  @log.info "RUNNING TESTS"
  scenarios = get_scenarios
  test_counter = 0
  scenarios.each do |scenario|
    test_counter += 1
    @log.info "Running test for #{scenario} - #{test_counter} of #{scenarios.size}"
    passed << one_test(scenario)
  end
  stop_vm() if @config[:vmm_enabled]
  all_passed = passed.select{|p| p == false}.size == 0
  @log.info "Number of tests run : #{passed.size}"
  @log.info "Result of ALL tests: Passed? #{all_passed}"
  @results['test_stop'] = Time.now()
  @results['elapsed_time'] = @results['test_stop'] - @results['test_start']
  return all_passed
end

#test_services(services) ⇒ Object

Test a bunch of services. Pass in an array containing the names of services to test. Returns an array of booleans, indicating the success or failure of the tests. You should read detailed results from results()



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/vmth.rb', line 213

def test_services(services)
  @results['test_start'] = Time.now()
  boot_vm() if @options[:vmm_enabled]
  prep
  freeze_vm() if @options[:vmm_enabled]
  passed = []
  @log.info "RUNNING NO-SERVICE TEST"
  passed << one_test(eb(@config["init_scenario"]))
  # Stop testing if our initial test fails.
  unless passed.first == true
    stop_vm() if @options[:vmm_enabled]
    return passed
  end
  freeze_vm() if @options[:vmm_enabled]
  @log.info "RUNNING TESTS"
  test_counter = 0
  services.each do |service|
    test_counter += 1
    @log.info "Running test for #{service} - #{test_counter} of #{services.size}"
    passed << one_test(service)
  end
  stop_vm() if @options[:vmm_enabled]
  @results['test_stop'] = Time.now()
  @results['elapsed_time']= @results['test_stop'] - @results['test_start']
  return passed
end

#test_without_vmObject

Really only for development/testing of this class. Will run tests against an already running VM (presumably the developer is running it in another window)



205
206
207
208
# File 'lib/vmth.rb', line 205

def test_without_vm
  prep
  test_services
end

#vmclObject

Return the command-line that would have been used to start QEMU. This can be used for developing this library, or to get a new disk image prepped to be used with the test harness.



242
243
244
# File 'lib/vmth.rb', line 242

def vmcl
  return vmm_command_line
end