diff options
Diffstat (limited to 'scripts/automation/regression/trex.py')
-rw-r--r-- | scripts/automation/regression/trex.py | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/scripts/automation/regression/trex.py b/scripts/automation/regression/trex.py new file mode 100644 index 00000000..7b96f2f8 --- /dev/null +++ b/scripts/automation/regression/trex.py @@ -0,0 +1,457 @@ +#!/router/bin/python + +import os +import sys +import subprocess +import misc_methods +import re +import signal +import time +from CProgressDisp import TimedProgressBar +from stateful_tests.tests_exceptions import TRexInUseError +import datetime + +class CTRexScenario: + modes = set() # list of modes of this setup: loopback, virtual etc. + server_logs = False + is_test_list = False + is_init = False + is_stl_init = False + trex_crashed = False + configuration = None + trex = None + stl_trex = None + stl_ports_map = None + stl_init_error = None + router = None + router_cfg = None + daemon_log_lines = 0 + setup_name = None + setup_dir = None + router_image = None + trex_version = None + scripts_path = None + benchmark = None + report_dir = 'reports' + # logger = None + test_types = {'functional_tests': [], 'stateful_tests': [], 'stateless_tests': []} + is_copied = False + GAManager = None + no_daemon = False + debug_image = False + test = None + json_verbose = False + +class CTRexRunner: + """This is an instance for generating a CTRexRunner""" + + def __init__ (self, config_dict, yaml): + self.trex_config = config_dict#misc_methods.load_config_file(config_file) + self.yaml = yaml + + + def get_config (self): + """ get_config() -> dict + + Returns the stored configuration of the TRex server of the CTRexRunner instance as a dictionary + """ + return self.trex_config + + def set_yaml_file (self, yaml_path): + """ update_yaml_file (self, yaml_path) -> None + + Defines the yaml file to be used by the TRex. + """ + self.yaml = yaml_path + + + def generate_run_cmd (self, multiplier, cores, duration, nc = True, export_path="/tmp/trex.txt", **kwargs): + """ generate_run_cmd(self, multiplier, duration, export_path) -> str + + Generates a custom running command for the kick-off of the TRex traffic generator. + Returns a command (string) to be issued on the trex server + + Parameters + ---------- + multiplier : float + Defines the TRex multiplier factor (platform dependant) + duration : int + Defines the duration of the test + export_path : str + a full system path to which the results of the trex-run will be logged. + + """ + fileName, fileExtension = os.path.splitext(self.yaml) + if self.yaml == None: + raise ValueError('TRex yaml file is not defined') + elif fileExtension != '.yaml': + raise TypeError('yaml path is not referencing a .yaml file') + + if 'results_file_path' in kwargs: + export_path = kwargs['results_file_path'] + + trex_cmd_str = './t-rex-64 -c %d -m %f -d %d -f %s ' + + if nc: + trex_cmd_str = trex_cmd_str + ' --nc ' + + trex_cmd = trex_cmd_str % (cores, + multiplier, + duration, + self.yaml) + # self.trex_config['trex_latency']) + + for key, value in kwargs.items(): + tmp_key = key.replace('_','-') + dash = ' -' if (len(key)==1) else ' --' + if value == True: + trex_cmd += (dash + tmp_key) + else: + trex_cmd += (dash + '{k} {val}'.format( k = tmp_key, val = value )) + + print("\nTRex COMMAND: ", trex_cmd) + + cmd = 'sshpass.exp %s %s root "cd %s; %s > %s"' % (self.trex_config['trex_password'], + self.trex_config['trex_name'], + self.trex_config['trex_version_path'], + trex_cmd, + export_path) + + return cmd; + + def generate_fetch_cmd (self, result_file_full_path="/tmp/trex.txt"): + """ generate_fetch_cmd(self, result_file_full_path) -> str + + Generates a custom command for which will enable to fetch the resutls of the TRex run. + Returns a command (string) to be issued on the trex server. + + Example use: fetch_trex_results() - command that will fetch the content from the default log file- /tmp/trex.txt + fetch_trex_results("/tmp/trex_secondary_file.txt") - command that will fetch the content from a custom log file- /tmp/trex_secondary_file.txt + """ + #dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + script_running_dir = os.path.dirname(os.path.realpath(__file__)) # get the current script working directory so that the sshpass could be accessed. + cmd = script_running_dir + '/sshpass.exp %s %s root "cat %s"' % (self.trex_config['trex_password'], + self.trex_config['trex_name'], + result_file_full_path); + return cmd; + + + + def run (self, multiplier, cores, duration, **kwargs): + """ run(self, multiplier, duration, results_file_path) -> CTRexResults + + Running the TRex server based on the config file. + Returns a CTRexResults object containing the results of the run. + + Parameters + ---------- + multiplier : float + Defines the TRex multiplier factor (platform dependant) + duration : int + Defines the duration of the test + results_file_path : str + a full system path to which the results of the trex-run will be logged and fetched from. + + """ + tmp_path = None + # print kwargs + if 'export_path' in kwargs: + tmp_path = kwargs['export_path'] + del kwargs['export_path'] + cmd = self.generate_run_cmd(multiplier, cores, duration, tmp_path, **kwargs) + else: + cmd = self.generate_run_cmd(multiplier, cores, duration, **kwargs) + +# print 'TRex complete command to be used:' +# print cmd + # print kwargs + + progress_thread = TimedProgressBar(duration) + progress_thread.start() + interrupted = False + try: + start_time = time.time() + start = datetime.datetime.now() + results = subprocess.call(cmd, shell = True, stdout = open(os.devnull, 'wb')) + end_time = time.time() + fin = datetime.datetime.now() + # print "Time difference : ", fin-start + runtime_deviation = abs(( (end_time - start_time)/ (duration+15) ) - 1) + print("runtime_deviation: %2.0f %%" % ( runtime_deviation*100.0)) + if ( runtime_deviation > 0.6 ) : + # If the run stopped immediately - classify as Trex in use or reachability issue + interrupted = True + if ((end_time - start_time) < 2): + raise TRexInUseError ('TRex run failed since TRex is used by another process, or due to reachability issues') + else: + CTRexScenario.trex_crashed = True + # results = subprocess.Popen(cmd, stdout = open(os.devnull, 'wb'), + # shell=True, preexec_fn=os.setsid) + except KeyboardInterrupt: + print("\nTRex test interrupted by user during traffic generation!!") + results.killpg(results.pid, signal.SIGTERM) # Send the kill signal to all the process groups + interrupted = True + raise RuntimeError + finally: + progress_thread.join(isPlannedStop = (not interrupted) ) + + if results!=0: + sys.stderr.write("TRex run failed. Please Contact trex-dev mailer for further details") + sys.stderr.flush() + return None + elif interrupted: + sys.stderr.write("TRex run failed due user-interruption.") + sys.stderr.flush() + return None + else: + + if tmp_path: + cmd = self.generate_fetch_cmd( tmp_path )#**kwargs)#results_file_path) + else: + cmd = self.generate_fetch_cmd() + + try: + run_log = subprocess.check_output(cmd, shell = True) + trex_result = CTRexResult(None, run_log) + trex_result.load_file_lines() + trex_result.parse() + + return trex_result + + except subprocess.CalledProcessError: + sys.stderr.write("TRex result fetching failed. Please Contact trex-dev mailer for further details") + sys.stderr.flush() + return None + +class CTRexResult(): + """This is an instance for generating a CTRexResult""" + def __init__ (self, file, buffer = None): + self.file = file + self.buffer = buffer + self.result = {} + + + def load_file_lines (self): + """ load_file_lines(self) -> None + + Loads into the self.lines the content of self.file + """ + if self.buffer: + self.lines = self.buffer.split("\n") + else: + f = open(self.file,'r') + self.lines = f.readlines() + f.close() + + + def dump (self): + """ dump(self) -> None + + Prints nicely the content of self.result dictionary into the screen + """ + for key, value in self.result.items(): + print("{0:20} : \t{1}".format(key, float(value))) + + def update (self, key, val, _str): + """ update (self, key, val, _str) -> None + + Updates the self.result[key] with a possibly new value representation of val + Example: 15K might be updated into 15000.0 + + Parameters + ---------- + key : + Key of the self.result dictionary of the TRexResult instance + val : float + Key of the self.result dictionary of the TRexResult instance + _str : str + a represntation of the BW (. + + """ + + s = _str.strip() + + if s[0]=="G": + val = val*1E9 + elif s[0]=="M": + val = val*1E6 + elif s[0]=="K": + val = val*1E3 + + if key in self.result: + if self.result[key] > 0: + if (val/self.result[key] > 0.97 ): + self.result[key]= val + else: + self.result[key] = val + else: + self.result[key] = val + + + + def parse (self): + """ parse(self) -> None + + Parse the content of the result file from the TRex test and upload the data into + """ + stop_read = False + d = { + 'total-tx' : 0, + 'total-rx' : 0, + 'total-pps' : 0, + 'total-cps' : 0, + + 'expected-pps' : 0, + 'expected-cps' : 0, + 'expected-bps' : 0, + 'active-flows' : 0, + 'open-flows' : 0 + } + + self.error = "" + + # Parse the output of the test, line by line (each line matches another RegEx and as such + # different rules apply + for line in self.lines: + match = re.match(".*/var/run/.rte_config.*", line) + if match: + stop_read = True + continue + + #Total-Tx : 462.42 Mbps Nat_time_out : 0 ==> we try to parse the next decimal in this case Nat_time_out +# match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)\W*\w+\W+(\w+)\W*([:]|[=])\W*(\d+)(.*)", line); +# if match: +# key = misc_methods.mix_string(match.group(5)) +# val = float(match.group(7)) +# # continue to parse !! we try the second +# self.result[key] = val #update latest + + # check if we need to stop reading + match = re.match(".*latency daemon has stopped.*", line) + if match: + stop_read = True + continue + + match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)(.*ps)\s+(\w+)\W*([:]|[=])\W*(\d+)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + val = float(match.group(4)) + if key in d: + if stop_read == False: + self.update (key, val, match.group(5)) + else: + self.result[key] = val # update latest + key2 = misc_methods.mix_string(match.group(6)) + val2 = int(match.group(8)) + self.result[key2] = val2 # always take latest + + + match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)(.*)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + val = float(match.group(4)) + if key in d: + if stop_read == False: + self.update (key, val, match.group(5)) + else: + self.result[key] = val # update latest + continue + + match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+)(.*)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + val = float(match.group(4)) + self.result[key] = val #update latest + continue + + match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(OK)(.*)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + val = 0 # valid + self.result[key] = val #update latest + continue + + match = re.match("\W*(Cpu Utilization)\W*([:]|[=])\W*(\d+[.]\d+) %(.*)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + val = float(match.group(3)) + if key in self.result: + if (self.result[key] < val): # update only if larger than previous value + self.result[key] = val + else: + self.result[key] = val + continue + + match = re.match(".*(rx_check\s.*)\s+:\s+(\w+)", line) + if match: + key = misc_methods.mix_string(match.group(1)) + try: + val = int(match.group(2)) + except ValueError: # corresponds with rx_check validation case + val = match.group(2) + finally: + self.result[key] = val + continue + + + def get_status (self, drop_expected = False): + if (self.error != ""): + print(self.error) + return (self.STATUS_ERR_FATAL) + + d = self.result + + # test for latency + latency_limit = 5000 + if ( d['maximum-latency'] > latency_limit ): + self.reason="Abnormal latency measured (higher than %s" % latency_limit + return self.STATUS_ERR_LATENCY + + # test for drops + if drop_expected == False: + if ( d['total-pkt-drop'] > 0 ): + self.reason=" At least one packet dropped " + return self.STATUS_ERR_DROP + + # test for rx/tx distance + rcv_vs_tx = d['total-tx']/d['total-rx'] + if ( (rcv_vs_tx >1.2) or (rcv_vs_tx <0.9) ): + self.reason="rx and tx should be close" + return self.STATUS_ERR_RX_TX_DISTANCE + + # expected measurement + expect_vs_measued=d['total-tx']/d['expected-bps'] + if ( (expect_vs_measued >1.1) or (expect_vs_measued < 0.9) ) : + print(expect_vs_measued) + print(d['total-tx']) + print(d['expected-bps']) + self.reason="measure is not as expected" + return self.STATUS_ERR_BAD_EXPECTED_MEASUREMENT + + if ( d['latency-any-error'] !=0 ): + self.reason=" latency-any-error has error" + return self.STATUS_ERR_LATENCY_ANY_ERROR + + return self.STATUS_OK + + # return types + STATUS_OK = 0 + STATUS_ERR_FATAL = 1 + STATUS_ERR_LATENCY = 2 + STATUS_ERR_DROP = 3 + STATUS_ERR_RX_TX_DISTANCE = 4 + STATUS_ERR_BAD_EXPECTED_MEASUREMENT = 5, + STATUS_ERR_LATENCY_ANY_ERROR = 6 + +def test_TRex_result_parser(): + t=CTRexResult('trex.txt'); + t.load_file_lines() + t.parse() + print(t.result) + + + + +if __name__ == "__main__": + #test_TRex_result_parser(); + pass |