module Beaker class CLI VERSION_STRING = " wWWWw |o o| | O | %s! |(\")| / \\X/ \\ | V | | | | " def initialize @timestamp = Time.now @options_parser = Beaker::Options::Parser.new @options = @options_parser.parse_args @attribution = @options_parser.attribution @logger = Beaker::Logger.new(@options) InParallel::InParallelExecutor.logger = @logger @options_parser.update_option(:logger, @logger, 'runtime') @options_parser.update_option(:timestamp, @timestamp, 'runtime') @options_parser.update_option(:beaker_version, Beaker::Version::STRING, 'runtime') beaker_version_string = VERSION_STRING % @options[:beaker_version] @execute = true if @options[:help] @logger.notify(@options_parser.usage) @execute = false return end if @options[:beaker_version_print] @logger.notify(beaker_version_string) @execute = false return end @logger.info("Beaker!") @logger.info(beaker_version_string) @logger.info(@options.dump) if @options[:parse_only] @execute = false return end #add additional paths to the LOAD_PATH if not @options[:load_path].empty? @options[:load_path].each do |path| $LOAD_PATH << File.expand_path(path) end end @options[:helper].each do |helper| require File.expand_path(helper) end end #Provision, validate and configure all hosts as defined in the hosts file def provision begin @hosts = [] @network_manager = Beaker::NetworkManager.new(@options, @logger) @hosts = @network_manager.provision @network_manager.proxy_package_manager @network_manager.validate @network_manager.configure rescue => e report_and_raise(@logger, e, "CLI.provision") end end #Run Beaker tests. # # - provision hosts (includes validation and configuration) # - run pre-suite # - run tests # - run post-suite # - cleanup hosts def execute! if !@execute return end begin trap(:INT) do @logger.warn "Interrupt received; exiting..." exit(1) end provision # Setup perf monitoring if needed if @options[:collect_perf_data].to_s =~ /(aggressive)|(normal)/ @perf = Beaker::Perf.new( @hosts, @options ) end errored = false #pre acceptance phase run_suite(:pre_suite, :fast) #testing phase begin run_suite(:tests, @options[:fail_mode]) #post acceptance phase rescue => e #post acceptance on failure #run post-suite if we are in fail-slow mode if @options[:fail_mode].to_s =~ /slow/ run_suite(:post_suite) @perf.print_perf_info if defined? @perf end raise e else #post acceptance on success run_suite(:post_suite) @perf.print_perf_info if defined? @perf end #cleanup phase rescue => e begin run_suite(:pre_cleanup) rescue => e # pre-cleanup failed @logger.error "Failed running the pre-cleanup suite." end #cleanup on error if @options[:preserve_hosts].to_s =~ /(never)|(onpass)/ @logger.notify "Cleanup: cleaning up after failed run" if @network_manager @network_manager.cleanup end else preserve_hosts_file end print_reproduction_info( :error ) @logger.error "Failed running the test suite." puts '' exit 1 else begin run_suite(:pre_cleanup) rescue => e # pre-cleanup failed @logger.error "Failed running the pre-cleanup suite." end #cleanup on success if @options[:preserve_hosts].to_s =~ /(never)|(onfail)/ @logger.notify "Cleanup: cleaning up after successful run" if @network_manager @network_manager.cleanup end else preserve_hosts_file end if @logger.is_debug? print_reproduction_info( :debug ) end end end #Run the provided test suite #@param [Symbol] suite_name The test suite to execute #@param [String] failure_strategy How to proceed after a test failure, 'fast' = stop running tests immediately, 'slow' = # continue to execute tests. def run_suite(suite_name, failure_strategy = nil) if (@options[suite_name].empty?) @logger.notify("No tests to run for suite '#{suite_name.to_s}'") return end Beaker::TestSuite.new( suite_name, @hosts, @options, @timestamp, failure_strategy ).run_and_raise_on_failure end # Sets aside the current hosts file for re-use with the --no-provision flag. # This is originally intended for use on a successful tests where the hosts # are preserved (the --preserve-hosts option is set accordingly). # It copies the current hosts file to the log directory, and rewrites the SUT # names to match their names during the finishing run. # # @return nil def preserve_hosts_file # things that don't belong in the preserved host file dontpreserve = /HOSTS|logger|timestamp|log_prefix|_dated_dir|logger_sut/ # set the pre/post/tests to be none @options[:pre_suite] = [] @options[:post_suite] = [] @options[:tests] = [] @options[:pre_cleanup] = [] preserved_hosts_filename = File.join(@options[:log_dated_dir], 'hosts_preserved.yml') hosts_yaml = @options newly_keyed_hosts_entries = {} hosts_yaml['HOSTS'].each do |host_name, file_host_hash| h = Beaker::Options::OptionsHash.new file_host_hash = h.merge(file_host_hash) @hosts.each do |host| if host_name.to_s == host.name.to_s newly_keyed_hosts_entries[host.reachable_name] = file_host_hash.merge(host.host_hash) break end end end hosts_yaml['HOSTS'] = newly_keyed_hosts_entries hosts_yaml['CONFIG'] = Beaker::Options::OptionsHash.new.merge(hosts_yaml['CONFIG'] || {}) # save the rest of the options, excepting the HOSTS that we have already processed hosts_yaml['CONFIG'] = hosts_yaml['CONFIG'].merge(@options.reject{ |k,v| k =~ dontpreserve }) # remove copy of HOSTS information hosts_yaml['CONFIG']['provision'] = false File.open(preserved_hosts_filename, 'w') do |file| YAML.dump(hosts_yaml, file) end @options[:hosts_preserved_yaml_file] = preserved_hosts_filename end # Prints all information required to reproduce the current run & results to the log # @see #print_env_vars_affecting_beaker # @see #print_command_line # # @return nil def print_reproduction_info( log_level = :debug ) print_command_line( log_level ) print_env_vars_affecting_beaker( log_level ) end # Prints Environment variables affecting the beaker run (those that # beaker introspects + the ruby env that beaker runs within) # @param [Symbol] log_level The log level (coloring) to print the message at # @example Print pertinent env vars using error leve reporting (red) # print_env_vars_affecting_beaker :error # # @return nil def print_env_vars_affecting_beaker( log_level ) non_beaker_env_vars = [ 'BUNDLE_PATH', 'BUNDLE_BIN', 'GEM_HOME', 'GEM_PATH', 'RUBYLIB', 'PATH'] env_var_map = non_beaker_env_vars.inject({}) do |memo, possibly_set_vars| set_var = Array(possibly_set_vars).detect {|possible_var| ENV[possible_var] } memo[set_var] = ENV[set_var] if set_var memo end env_var_map = env_var_map.merge(Beaker::Options::Presets.new.env_vars) @logger.send( log_level, "\nImportant ENV variables that may have affected your run:" ) env_var_map.each_pair do |var, value| if value.is_a?(Hash) value.each_pair do | subvar, subvalue | @logger.send( log_level, " #{subvar}\t\t#{subvalue}" ) end else @logger.send( log_level, " #{var}\t\t#{value}" ) end end end # Prints the command line that can be called to reproduce this run # (assuming the environment is the same) # @param [Symbol] log_level The log level (coloring) to print the message at # @example Print pertinent env vars using error level reporting (red) # print_command_line :error # # @note Re-use of already provisioned SUTs has been tested against the vmpooler & vagrant boxes. # Fusion doesn't need this, as it has no cleanup steps. Docker is untested at this time. # Please contact @electrical or the Puppet QE Team for more info, or for requests to support this. # # @return nil def print_command_line( log_level = :debug ) @logger.send(log_level, "\nYou can reproduce this run with:\n") @logger.send(log_level, @options[:command_line]) if @options[:hosts_preserved_yaml_file] set_docker_warning = false has_supported_hypervisor = false @hosts.each do |host| case host[:hypervisor] when /vagrant|fusion|vmpooler|vcloud/ has_supported_hypervisor = true when /docker/ set_docker_warning = true end end if has_supported_hypervisor reproducing_command = build_hosts_preserved_reproducing_command(@options[:command_line], @options[:hosts_preserved_yaml_file]) @logger.send(log_level, "\nYou can re-run commands against the already provisioned SUT(s) with:\n") @logger.send(log_level, '(docker support is untested for this feature. please reference the docs for more info)') if set_docker_warning @logger.send(log_level, reproducing_command) end end end # provides a new version of the command given, edited for re-use with a # preserved host. It does this by swapping the hosts file out for the # new_hostsfile argument and removing any previously set provisioning # flags that it finds # (we add +:provision => false+ in the new_hostsfile itself). # # @param [String] command Command line parameters to edit. # @param [String] new_hostsfile Path to the new hosts file to use. # # @return [String] The command line parameters edited for re-use def build_hosts_preserved_reproducing_command(command, new_hostsfile) command_parts = command.split(' ') replace_hosts_file_next = false reproducing_command = [] command_parts.each do |part| if replace_hosts_file_next reproducing_command << new_hostsfile replace_hosts_file_next = false next elsif part == '--provision' || part == '--no-provision' next # skip any provisioning flag. This is handled in the new_hostsfile itself elsif part == '--hosts' replace_hosts_file_next = true end reproducing_command << part end reproducing_command.join(' ') end end end