# Runs Mu Studio scenario msl files in Studio Scale, requiring that all interfaces and # hosts used in the scenario are specified on the command-line through the –i option. # Runs either a single msl file or a directory of msl files, and has command-line options # to specify the Mu parameters, the interfaces to use, and the pattern in which to run require 'mu/api/scale' class Mu class Command class Cmd_runscale < Command attr_accessor :api, :params, :hosts, :addr_indexes, :offset_indexes, :peak_concurrency, :peak_throughput # displays command-line help # * argv = command-line arguments def cmd_help argv help end # sets up, executes, and closes a Studio Scale test for one scenario or each scenario (.msl file) found in the specified directory # * argv = command-line arguments # * optional -s scenario file # * optional -d directory containing one or more scenario files # * optional -r argument for recursive directory search (default is a flat directory) def cmd_run_files argv setup argv msg "Clean up existing stats files: app_id_status.json, app_id_stats.csv", Logger::INFO File.delete("app_id_status.json") if File.exists?("app_id_status.json") File.delete("app_id_stats.csv") if File.exists?("app_id_stats.csv") @api = Scale.new(@@mu_ip, @@mu_admin_user, @@mu_admin_pass) @api.configure("pattern", @cmd_line_pattern) @params = {} @params["hosts"] = @cmd_line_hosts if @hash["dir"].nil? files = [@hash["scenario"]] else recursive = (@hash['recursive'].nil?) ? "": "**" files = Dir.glob(File.join(@hash["dir"],recursive,"*.msl")) end if files.nil? or files.empty? msg "no msl files found", Logger::ERROR else files.each do | scenario | begin run scenario rescue Exception => e msg "Run failed: #{e}", Logger::ERROR end if @verify_only output_verify_results scenario else output_csv scenario end sleep 2 end end @api.release end private def setup argv parse_cli argv @params = {} @peak_concurrency = 0 @peak_throughput = 0.0 if @hash['test'] @verify_only = true else @verify_only = false end if not @hash['testset'] @testset = "" else @testset = @hash['testset'] end if not @hash['pattern'] @cmd_line_pattern = "{ \"iterations\": 1, \"intervals\": [ {\"iterations\":1, \"end\":100, \"start\":1, \"duration\":20 } ] }" else @cmd_line_pattern = @hash['pattern'] end if not @hash['interfaces'] @cmd_line_hosts = "b1,b2" else @cmd_line_hosts = @hash['interfaces'] end if not @hash['delay'] @delay = 0 else @delay = @hash['delay'].to_i end if not @hash['no_verify'] @no_verify = false else @no_verify = true end if not @hash['vector_address_pairing'] @vector_address_pairing = false @limit_concurrency = false else @vector_address_pairing = true @limit_concurrency = true end end def run(scenario) begin @api.configure("musl", File.read(scenario)) rescue raise "Failed to read in scenario #{scenario}" end if @testset.empty? @api.configure("csv", "") else msg "using testset [#{@testset}]", Logger::INFO @api.configure("csv", File.read(@testset)) end configure_hosts @api.configure("delay", @delay) @api.configure("vectorAddressPairing", @vector_address_pairing) @api.configure("limitConcurrency", @limit_concurrency) if @no_verify == false # don't do verify if no_verify==true msg "verifying #{scenario} ...", Logger::INFO response = @api.verify msg response, Logger::DEBUG # sleep 3 v = parse_verify_response(response) msg "#{v}", Logger::DEBUG if v.nil? or v.include? "Error" msg "error in verify", Logger::ERROR return end if @verify_only msg v, Logger::DEBUG return v end end msg "starting #{scenario} ...", Logger::INFO @api.start start_time = Time.now.to_i failed_status = 0 while true sleep 5 status = @api.status if !status.nil? if !status["status"].nil? if status["status"]["running"] == false msg "running = #{status["status"]["running"]}", Logger::DEBUG r = parse_status(status) dump_status(status, scenario) return else unless parse_status(status) msg("Exit run loop on status parse error", Logger::ERROR) return end end else # status['status'].nil? ... no bonafide status was returned time_now = Time.now.to_i if time_now - start_time > 60 msg "Error: timing out after 60 seconds. Test had failed to start or verify", Logger::ERROR break end end else failed_status += 1 break if failed_status == 10 end end ensure msg "stopping #{scenario} ...", Logger::DEBUG @api.stop end def cmd_running? if @api.nil? msg "false" return end status = @api.status if !status.nil? if !status["status"].nil? msg status["status"]["running"] end else msg "false" end end def configure_hosts @hosts = Array.new @addr_indexes = Array.new @offset_indexes = Array.new hosts = @params["hosts"] if !hosts.nil? p = hosts.split(",") p.each do | h | if h.include?("-") # b1-1000,b2-1 to indicate addr_count q = h.split("-") @hosts << q[0] if q[1].include?(":") # -1000:20 to indicate offset within range r = q[1].split(":") @addr_indexes << r[0] @offset_indexes << r[1] else @addr_indexes << q[1] @offset_indexes << 1 end else # default to the 1st addr index @hosts << h @addr_indexes << 1 @offset_indexes << 1 end end else @hosts = ['b1','b2'] @addr_indexes = [1,1] @offset_indexes = [1,1] end set_hosts_byname(@hosts, @addr_indexes, @offset_indexes) end def set_hosts_byname(hosts=@hosts, count=[1,1], offset=[1,1], v4=true) new_hosts = Array.new str = "" hosts.each_with_index do |n, i| if n.match(/^[ab][1-4]$/) or n.include?(".") # possible vlan if count[i] == 1 or count[i].nil? str = "#{n}/*" else str = "#{n}/*,#{count[i]},#{offset[i]}" end msg "using host #{str}", Logger::DEBUG else @net = Netconfig.new @hosts, @username, @password if v4 addr = @net.get("hosts/#{n}")['v4_addr'] else addr = @net.get("hosts/#{n}")['v6_addr'] end str = "#{addr}" msg "using host #{str}", Logger::DEBUG end new_hosts << str end set_hosts(new_hosts) end # expects full strings: e.g. b1/12.89.0.1 ... def set_hosts(hosts=["b1","b2"]) host_params = {} # assign hosts to consecutive string keys, host_0, host_1, etc ... hosts.each_with_index do | h, i | host_params["host_#{i}"] = hosts[i] end # convert keys to symbols # host_params.each_key { |k| host_params[k.to_sym] = host_params[k] } new_host_params = {} host_params.each_key { |k| new_host_params[k.to_sym] = host_params[k] } @api.configure("hosts", new_host_params) end def dump_status(status, msl) filename = "app_id_status.json" msg "Update status to: #{File.expand_path(filename)}", Logger::INFO f = File.open(filename, "a") status["filename"] = msl str = JSON.pretty_generate(status) File.zero?(filename) or f.puts(",") f.write(str) f.close end def output_csv(msl_file) filename = "app_id_stats.csv" msg "Update stats to: #{File.expand_path(filename)}", Logger::INFO unless File.exists?(filename) heading = "scenario,executed,errors,timeouts,client_tx_bytes,client_tx_msgs,client_tx_bitrate,client_rx_bytes,client_rx_msgs,client_rx_bitrate,server_tx_bytes,server_tx_msgs,server_tx_bitrate,server_rx_bytes,server_rx_msgs,server_rx_bitrate" File.open(filename, 'a'){|f| f.puts(heading)} end @row = [msl_file,@executed,@errors,@timeouts, @client_tx_bytes,@client_tx_msgs,@client_tx_bitrate, @client_rx_bytes,@client_rx_msgs,@client_rx_bitrate, @server_tx_bytes,@server_tx_msgs,@server_tx_bitrate, @server_rx_bytes,@server_rx_msgs,@server_rx_bitrate] File.open(filename, 'a'){|f| f.puts(@row.join(','))} end def output_verify_results(msl_file) filename = "app_id_stats.csv" msg "Update verify results to: #{File.expand_path(filename)}", Logger::DEBUG unless File.exists?(filename) msg "Writting verify results to: #{File.expand_path(filename)}", Logger::INFO heading = "scenario, status" File.open(filename, 'a'){|f| f.puts(heading)} end File.open(filename, 'a'){|f| f.puts("#{msl_file},#{@verify_response}")} end # TO DO - We need to fix the logic of our response verification, it isn't elegant right now def parse_verify_response(response) if response.nil? # || response.empty? msg "*** error = no response received from /verify ***", Logger::ERROR @verify_response = "Error = No response from verify" return @verify_response end begin msg JSON.pretty_generate(response), Logger::DEBUG if response.has_key?("error") @error = "Player Error" @reason = response["error"] msg "*** Error = #{@error}, reason = #{@reason} ***", Logger::ERROR @verify_response = "Error = #{@error}, reason = #{@reason}" return @verify_response end if !response["status"].nil? if response["status"]["error"] == true @error = response["status"]["error"] @reason = response["status"]["reason"] msg "*** Error = #{@error}, reason = #{@reason} ***", Logger::ERROR @verify_response = "Error = #{@error}, reason = #{@reason}" return @verify_response end end msg "*** verify: okay ***", Logger::INFO @verify_response = "okay" return @verify_response rescue => e puts "Broken Response: \n#{response}\n" msg e, Logger::ERROR raise return nil end end def parse_status(status) return nil if status.nil? @reported_volume = 0 if !status["status"]["error"].nil? if status["status"]["error"] == true @error = status["status"]["error"] @reason = status["status"]["reason"] msg "*** Error = #{@error}, reason = #{@reason} ***" return false end end @stats_summary = status["status"]["statistics"]["summary"] @duration = @stats_summary["duration"] @instances = @stats_summary["instances"] @total_instances = @instances["total"] @executed = @instances["executed"] @timeouts = @instances["timeouts"] @errors = @instances["errors"] @asserts_failed = @stats_summary["asserts"]["failed"] @server = @stats_summary["server"] @server_tx_bytes = @server["tx"]["bytes"] @server_tx_msgs = @server["tx"]["msgs"] @server_rx_bytes = @server["rx"]["bytes"] @server_rx_msgs = @server["rx"]["msgs"] @client = @stats_summary["client"] @client_tx_bytes = @client["tx"]["bytes"] @client_tx_msgs = @client["tx"]["msgs"] @client_rx_bytes = @client["rx"]["bytes"] @client_rx_msgs = @client["rx"]["msgs"] # Compute bitrates @client_tx_bitrate = @client_rx_bitrate = 0 @server_tx_bitrate = @server_rx_bitrate = 0 if @duration.to_f > 0 @client_tx_bitrate = format_float 2, (@client["tx"]["bytes"] * 8).to_f/@duration.to_f @client_rx_bitrate = format_float 2, (@client["rx"]["bytes"] * 8).to_f/@duration.to_f @server_tx_bitrate = format_float 2, (@server["tx"]["bytes"] * 8).to_f/@duration.to_f @server_tx_bitrate = format_float 2, (@server["rx"]["bytes"] * 8).to_f/@duration.to_f end # @scenarios = status["status"]["statistics"]["scenarios"] @scenarios.each do | scenario | @reported_volume = @reported_volume + scenario["volume"] end if @reported_volume.to_i > @peak_concurrency @peak_concurrency = @reported_volume.to_i end bits1 = (@client_tx_bytes.to_i + @client_rx_bytes.to_i) * 8 dur1 = @duration.to_f thruput = format_float(2, bits1.to_f / dur1) if thruput.to_f > @peak_throughput @peak_throughput = thruput.to_f end msg "" msg "duration: #{format_float(2, @duration)}" msg "concurrency: #{@reported_volume}" msg "tests/sec: #{format_float(2, @executed.to_f / @duration)}" if @duration.to_i > 0 msg "bits/sec: #{thruput}" if @duration.to_i > 0 msg "passed: #{@executed}" msg "errors: #{@errors}" msg "timeouts: #{@timeouts}" msg "client tx bits/sec #{@client_tx_bitrate}" msg "client tx msgs/sec #{format_float(2, @client_tx_msgs.to_f / @duration)}" if @duration.to_i > 0 msg "client rx bytes/sec #{format_float(2, @client_rx_bytes.to_f / @duration)}" if @duration.to_i > 0 msg "client rx msgs/sec #{format_float(2, @client_rx_msgs.to_f / @duration)}" if @duration.to_i > 0 msg "server tx bytes/sec #{format_float(2, @server_tx_bytes.to_f / @duration)}" if @duration.to_i > 0 msg "server tx msgs/sec #{format_float(2, @server_tx_msgs.to_f / @duration)}" if @duration.to_i > 0 msg "server rx bytes/sec #{format_float(2, @server_rx_bytes.to_f / @duration)}" if @duration.to_i > 0 msg "server rx msgs/sec #{format_float(2, @server_rx_msgs.to_f / @duration)}" if @duration.to_i > 0 msg "" end def parse_cli argv @hash = Hash.new while not argv.empty? break if argv.first[0,1] != '-' k = argv.shift if [ '-a', '--vector_address_pairing' ].member? k @hash['vector_address_pairing'] = true next end if [ '-c', '--csv' ].member? k @hash['testset'] = shift(k, argv) next end if [ '-d', '--dir' ].member? k @hash['dir'] = shift(k, argv) next end if [ '-f', '--default_host' ].member? k @hash['default_host'] = shift(k, argv) next end if [ '-i', '--interfaces' ].member? k @hash['interfaces'] = shift(k, argv) next end if [ '-h', '--help' ].member? k help exit end =begin if [ '-hold', '--hold_concurrency' ].member? k @hash['hold_concurrency'] = true next end =end if [ '-l', '--delay' ].member? k @hash['delay'] = shift(k, argv) next end if [ '-m', '--mu_string' ].member? k mu_string = shift(k, argv) if mu_string =~ /(.+?):(.+?)@(.*)/ @@mu_admin_user = $1 @@mu_admin_pass = $2 @@mu_ip = $3 end next end if [ '-n', '--no_verify' ].member? k @hash['no_verify'] = true next end if [ '-o', '--output' ].member? k $stdout.reopen(shift(k, argv), "w") $stdout.sync = true next end if [ '-p', '--pattern' ].member? k patterns = Array.new pattern_string = shift(k, argv) pstrings = pattern_string.split(",") pstrings.each do | p | if p =~ /(.+?)-(.+?):(.*)/ # e.g. 1-10000:60 start_vol = $1 end_vol = $2 duration = $3 patterns << "{\"iterations\":1, \"end\":#{end_vol}, \"start\":#{start_vol}, \"duration\":#{duration} }" end end ps = "{ \"iterations\": 1, \"intervals\": [" patterns.each do | p | ps = ps + p + "," end ps = ps[0..ps.length-2] # remove final comma ps = ps + "] }" @hash['pattern'] = ps next end if [ '-r', '--recursive'].member? k @hash['recursive'] = true next end if [ '-s', '--scenario' ].member? k @hash['scenario'] = shift(k, argv) next end if [ '-t', '--test' ].member? k @hash['test'] = true next end if [ '-v', '--verbose' ].member? k $log.level = Logger::DEBUG next end raise ArgumentError, "Unknown option #{k}" end @hash end def help helps = [ { :short => '-a', :long => '--vector_address_pairing', :value => '', :help => 'sets vectorAddressPairing to true (limits and maps concurrency to the rows in the testset)' }, { :short => '-c', :long => '--csv', :value => '', :help => 'name of the csv testset to run' }, { :short => '-d', :long => '--dir', :value => '', :help => 'directory containing the scenario file' }, { :short => '-f', :long => '--default_host', :value => '', :help => 'default_host setting' }, { :short => '-h', :long => '--help', :value => '', :help => 'help on command line options' }, # { :short => '-hold', :long => '--hold_concurrency', :value => '', :help => 'sets holdConcurrency to tru' }, { :short => '-i', :long => '--interfaces', :value => '', :help => 'comma-separated list of interfaces, e.g. b1,b2 or b1-1000,b2 for ip range' }, { :short => '-l', :long => '--delay', :value => '', :help => 'intra-scenario delay value' }, { :short => '-m', :long => '--mu_string', :value => '', :help => 'user, password, mu_ip in the form of admin:admin@10.9.8.7' }, { :short => '-n', :long => '--no_verify', :value => '', :help => 'do not do verify before start' }, { :short => '-o', :long => '--output', :value => '', :help => 'output logging to this file' }, { :short => '-p', :long => '--pattern', :value => '', :help => 'pattern in the form of comma-separated concurrency_start-end:duration patterns, e.g. 1-100:60,100-100:60,100-1:60' }, { :short => '-r', :long => '--recursive', :value => '', :help => 'for run_dir, recurse through sub-directories' }, { :short => '-s', :long => '--scenario', :value => '', :help => 'scenario file to run' }, { :short => '-t', :long => '--test', :value => '', :help => 'do verify only' }, { :short => '-v', :long => '--verbose', :value => '', :help => 'set Logger::DEBUG level' } ] cmds = [ "mu cmd_runscale:help", "mu cmd_runscale:run_files -s |-d --recursive -i -p ", "mu cmd_runscale:running?" ] max_long_size = helps.inject(0) { |memo, obj| [ obj[:long].size, memo ].max } max_value_size = helps.inject(0) { |memo, obj| [ obj[:value].size, memo ].max } helps.each do |h| puts "%-*s %*s %-*s %s" % [max_long_size, h[:long], 2, h[:short], max_value_size, h[:value], h[:help]] end puts puts "Available Commands" puts cmds.each do | c | puts c end puts end end end # Command end # Mu