trex.py revision 420216e5
1#!/router/bin/python
2
3import os
4import sys
5import subprocess
6import misc_methods
7import re
8import signal
9import time
10from CProgressDisp import TimedProgressBar
11from stateful_tests.tests_exceptions import TRexInUseError
12import datetime
13import copy
14
15class CTRexScenario:
16    modes            = set() # list of modes of this setup: loopback, virtual etc.
17    server_logs      = False
18    is_test_list     = False
19    is_init          = False
20    is_stl_init      = False
21    trex_crashed     = False
22    configuration    = None
23    trex             = None
24    stl_trex         = None
25    stl_ports_map    = None
26    stl_init_error   = None
27    router           = None
28    router_cfg       = None
29    daemon_log_lines = 0
30    setup_name       = None
31    setup_dir        = None
32    router_image     = None
33    trex_version     = None
34    scripts_path     = None
35    benchmark        = None
36    report_dir       = 'reports'
37    # logger         = None
38    test_types       = {'functional_tests': [], 'stateful_tests': [], 'stateless_tests': []}
39    pkg_updated      = False
40    GAManager        = None
41    no_daemon        = False
42    debug_image      = False
43    test             = None
44    json_verbose     = False
45    elk              = None
46    elk_info         = None
47
48def copy_elk_info ():
49   assert(CTRexScenario.elk_info)
50   d = copy.deepcopy(CTRexScenario.elk_info);
51
52   timestamp = datetime.datetime.now(); # need to update this
53   d['timestamp']=timestamp.strftime("%Y-%m-%d %H:%M:%S")
54   return(d)
55
56
57
58
59
60class CTRexRunner:
61    """This is an instance for generating a CTRexRunner"""
62
63    def __init__ (self, config_dict, yaml):
64        self.trex_config = config_dict#misc_methods.load_config_file(config_file)
65        self.yaml = yaml
66
67
68    def get_config (self):
69        """ get_config() -> dict
70
71        Returns the stored configuration of the TRex server of the CTRexRunner instance as a dictionary
72        """
73        return self.trex_config
74
75    def set_yaml_file (self, yaml_path):
76        """ update_yaml_file (self, yaml_path) -> None
77
78        Defines the yaml file to be used by the TRex.
79        """
80        self.yaml = yaml_path
81
82
83    def generate_run_cmd (self, multiplier, cores, duration, nc = True, export_path="/tmp/trex.txt", **kwargs):
84        """ generate_run_cmd(self, multiplier, duration, export_path) -> str
85
86        Generates a custom running command for the kick-off of the TRex traffic generator.
87        Returns a command (string) to be issued on the trex server
88
89        Parameters
90        ----------
91        multiplier : float
92            Defines the TRex multiplier factor (platform dependant)
93        duration : int
94            Defines the duration of the test
95        export_path : str
96            a full system path to which the results of the trex-run will be logged.
97
98        """
99        fileName, fileExtension = os.path.splitext(self.yaml)
100        if self.yaml == None:
101            raise ValueError('TRex yaml file is not defined')
102        elif fileExtension != '.yaml':
103            raise TypeError('yaml path is not referencing a .yaml file')
104
105        if 'results_file_path' in kwargs:
106            export_path = kwargs['results_file_path']
107
108        trex_cmd_str = './t-rex-64 -c %d -m %f -d %d -f %s '
109
110        if nc:
111            trex_cmd_str = trex_cmd_str + ' --nc '
112
113        trex_cmd = trex_cmd_str % (cores,
114            multiplier,
115            duration,
116            self.yaml)
117            # self.trex_config['trex_latency'])
118
119        for key, value in kwargs.items():
120            tmp_key = key.replace('_','-')
121            dash = ' -' if (len(key)==1) else ' --'
122            if value == True:
123                trex_cmd += (dash + tmp_key)
124            else:
125                trex_cmd += (dash + '{k} {val}'.format( k = tmp_key, val =  value ))
126
127        print("\nTRex COMMAND: ", trex_cmd)
128
129        cmd = 'sshpass.exp %s %s root "cd %s; %s > %s"' % (self.trex_config['trex_password'],
130            self.trex_config['trex_name'],
131            self.trex_config['trex_version_path'],
132            trex_cmd,
133            export_path)
134
135        return cmd;
136
137    def generate_fetch_cmd (self, result_file_full_path="/tmp/trex.txt"):
138        """ generate_fetch_cmd(self, result_file_full_path) -> str
139
140        Generates a custom command for which will enable to fetch the resutls of the TRex run.
141        Returns a command (string) to be issued on the trex server.
142
143        Example use: fetch_trex_results()                                   -   command that will fetch the content from the default log file- /tmp/trex.txt
144                     fetch_trex_results("/tmp/trex_secondary_file.txt")     -   command that will fetch the content from a custom log file- /tmp/trex_secondary_file.txt
145        """
146        #dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
147        script_running_dir = os.path.dirname(os.path.realpath(__file__))    # get the current script working directory so that the sshpass could be accessed.
148        cmd = script_running_dir + '/sshpass.exp %s %s root "cat %s"' % (self.trex_config['trex_password'],
149            self.trex_config['trex_name'],
150            result_file_full_path);
151        return cmd;
152
153
154
155    def run (self, multiplier, cores, duration, **kwargs):
156        """ run(self, multiplier, duration, results_file_path) -> CTRexResults
157
158        Running the TRex server based on the config file.
159        Returns a CTRexResults object containing the results of the run.
160
161        Parameters
162        ----------
163        multiplier : float
164            Defines the TRex multiplier factor (platform dependant)
165        duration : int
166            Defines the duration of the test
167        results_file_path : str
168            a full system path to which the results of the trex-run will be logged and fetched from.
169
170        """
171        tmp_path = None
172        # print kwargs
173        if 'export_path' in kwargs:
174            tmp_path = kwargs['export_path']
175            del kwargs['export_path']
176            cmd = self.generate_run_cmd(multiplier, cores, duration, tmp_path, **kwargs)
177        else:
178            cmd = self.generate_run_cmd(multiplier, cores, duration, **kwargs)
179
180#       print 'TRex complete command to be used:'
181#       print cmd
182        # print kwargs
183
184        progress_thread = TimedProgressBar(duration)
185        progress_thread.start()
186        interrupted = False
187        try:
188            start_time = time.time()
189            start = datetime.datetime.now()
190            results = subprocess.call(cmd, shell = True, stdout = open(os.devnull, 'wb'))
191            end_time = time.time()
192            fin = datetime.datetime.now()
193            # print "Time difference : ", fin-start
194            runtime_deviation =  abs(( (end_time - start_time)/ (duration+15) ) - 1)
195            print("runtime_deviation: %2.0f %%" % ( runtime_deviation*100.0))
196            if (   runtime_deviation  > 0.6 )  :
197                # If the run stopped immediately - classify as Trex in use or reachability issue
198                interrupted = True
199                if ((end_time - start_time) < 2):
200                    raise TRexInUseError ('TRex run failed since TRex is used by another process, or due to reachability issues')
201                else:
202                    CTRexScenario.trex_crashed = True
203            # results = subprocess.Popen(cmd, stdout = open(os.devnull, 'wb'),
204            #            shell=True, preexec_fn=os.setsid)
205        except KeyboardInterrupt:
206            print("\nTRex test interrupted by user during traffic generation!!")
207            results.killpg(results.pid, signal.SIGTERM)  # Send the kill signal to all the process groups
208            interrupted = True
209            raise RuntimeError
210        finally:
211            progress_thread.join(isPlannedStop = (not interrupted) )
212
213        if results!=0:
214            sys.stderr.write("TRex run failed. Please Contact trex-dev mailer for further details")
215            sys.stderr.flush()
216            return None
217        elif interrupted:
218            sys.stderr.write("TRex run failed due user-interruption.")
219            sys.stderr.flush()
220            return None
221        else:
222
223            if tmp_path:
224                cmd = self.generate_fetch_cmd( tmp_path )#**kwargs)#results_file_path)
225            else:
226                cmd = self.generate_fetch_cmd()
227
228            try:
229                run_log = subprocess.check_output(cmd, shell = True)
230                trex_result = CTRexResult(None, run_log)
231                trex_result.load_file_lines()
232                trex_result.parse()
233
234                return trex_result
235
236            except subprocess.CalledProcessError:
237                sys.stderr.write("TRex result fetching failed. Please Contact trex-dev mailer for further details")
238                sys.stderr.flush()
239                return None
240
241class CTRexResult():
242    """This is an instance for generating a CTRexResult"""
243    def __init__ (self, file, buffer = None):
244        self.file = file
245        self.buffer = buffer
246        self.result = {}
247
248
249    def load_file_lines (self):
250        """ load_file_lines(self) -> None
251
252        Loads into the self.lines the content of self.file
253        """
254        if self.buffer:
255            self.lines = self.buffer.split("\n")
256        else:
257            f = open(self.file,'r')
258            self.lines = f.readlines()
259            f.close()
260
261
262    def dump (self):
263        """ dump(self) -> None
264
265        Prints nicely the content of self.result dictionary into the screen
266        """
267        for key, value in self.result.items():
268            print("{0:20} : \t{1}".format(key, float(value)))
269
270    def update (self, key, val, _str):
271        """ update (self, key, val, _str) -> None
272
273        Updates the self.result[key] with a possibly new value representation of val
274        Example: 15K might be updated into 15000.0
275
276        Parameters
277        ----------
278        key :
279            Key of the self.result dictionary of the TRexResult instance
280        val : float
281            Key of the self.result dictionary of the TRexResult instance
282        _str : str
283            a represntation of the BW (.
284
285        """
286
287        s = _str.strip()
288
289        if s[0]=="G":
290            val = val*1E9
291        elif s[0]=="M":
292            val = val*1E6
293        elif s[0]=="K":
294            val = val*1E3
295
296        if key in self.result:
297            if self.result[key] > 0:
298                if (val/self.result[key] > 0.97 ):
299                    self.result[key]= val
300            else:
301                self.result[key] = val
302        else:
303            self.result[key] = val
304
305
306
307    def parse (self):
308        """ parse(self) -> None
309
310        Parse the content of the result file from the TRex test and upload the data into
311        """
312        stop_read = False
313        d = {
314            'total-tx'      : 0,
315            'total-rx'      : 0,
316            'total-pps'     : 0,
317            'total-cps'     : 0,
318
319            'expected-pps'  : 0,
320            'expected-cps'  : 0,
321            'expected-bps'  : 0,
322            'active-flows'  : 0,
323            'open-flows'    : 0
324        }
325
326        self.error = ""
327
328        # Parse the output of the test, line by line (each line matches another RegEx and as such
329        # different rules apply
330        for line in self.lines:
331            match = re.match(".*/var/run/.rte_config.*", line)
332            if match:
333                stop_read = True
334                continue
335
336            #Total-Tx        :     462.42 Mbps   Nat_time_out    :        0   ==> we try to parse the next decimal in this case Nat_time_out
337#           match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)\W*\w+\W+(\w+)\W*([:]|[=])\W*(\d+)(.*)", line);
338#           if match:
339#               key = misc_methods.mix_string(match.group(5))
340#               val = float(match.group(7))
341#               # continue to parse !! we try the second
342#               self.result[key] = val #update latest
343
344            # check if we need to stop reading
345            match = re.match(".*latency daemon has stopped.*", line)
346            if match:
347                stop_read = True
348                continue
349
350            match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)(.*ps)\s+(\w+)\W*([:]|[=])\W*(\d+)", line)
351            if match:
352                key = misc_methods.mix_string(match.group(1))
353                val = float(match.group(4))
354                if key in d:
355                   if stop_read == False:
356                       self.update (key, val, match.group(5))
357                else:
358                    self.result[key] = val # update latest
359                key2 = misc_methods.mix_string(match.group(6))
360                val2 = int(match.group(8))
361                self.result[key2] = val2 # always take latest
362
363
364            match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+[.]\d+)(.*)", line)
365            if match:
366               key = misc_methods.mix_string(match.group(1))
367               val = float(match.group(4))
368               if key in d:
369                   if stop_read == False:
370                       self.update (key, val, match.group(5))
371               else:
372                    self.result[key] = val # update latest
373               continue
374
375            match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(\d+)(.*)", line)
376            if match:
377                key = misc_methods.mix_string(match.group(1))
378                val = float(match.group(4))
379                self.result[key] = val #update latest
380                continue
381
382            match = re.match("\W*(\w(\w|[-])+)\W*([:]|[=])\W*(OK)(.*)", line)
383            if match:
384                key = misc_methods.mix_string(match.group(1))
385                val = 0 # valid
386                self.result[key] = val #update latest
387                continue
388
389            match = re.match("\W*(Cpu Utilization)\W*([:]|[=])\W*(\d+[.]\d+)  %(.*)", line)
390            if match:
391                key = misc_methods.mix_string(match.group(1))
392                val = float(match.group(3))
393                if key in self.result:
394                    if (self.result[key] < val): # update only if larger than previous value
395                        self.result[key] = val
396                else:
397                    self.result[key] = val
398                continue
399
400            match = re.match(".*(rx_check\s.*)\s+:\s+(\w+)", line)
401            if match:
402                key = misc_methods.mix_string(match.group(1))
403                try:
404                    val = int(match.group(2))
405                except ValueError: # corresponds with rx_check validation case
406                    val = match.group(2)
407                finally:
408                    self.result[key] = val
409                continue
410
411
412    def get_status (self, drop_expected = False):
413        if (self.error != ""):
414            print(self.error)
415            return (self.STATUS_ERR_FATAL)
416
417        d = self.result
418
419        # test for latency
420        latency_limit = 5000
421        if ( d['maximum-latency'] > latency_limit ):
422            self.reason="Abnormal latency measured (higher than %s" % latency_limit
423            return self.STATUS_ERR_LATENCY
424
425        # test for drops
426        if drop_expected == False:
427            if ( d['total-pkt-drop'] > 0 ):
428                self.reason=" At least one packet dropped "
429                return self.STATUS_ERR_DROP
430
431        # test for rx/tx distance
432        rcv_vs_tx = d['total-tx']/d['total-rx']
433        if ( (rcv_vs_tx >1.2) or (rcv_vs_tx <0.9) ):
434            self.reason="rx and tx should be close"
435            return self.STATUS_ERR_RX_TX_DISTANCE
436
437        # expected measurement
438        expect_vs_measued=d['total-tx']/d['expected-bps']
439        if ( (expect_vs_measued >1.1) or (expect_vs_measued < 0.9) ) :
440            print(expect_vs_measued)
441            print(d['total-tx'])
442            print(d['expected-bps'])
443            self.reason="measure is not as expected"
444            return self.STATUS_ERR_BAD_EXPECTED_MEASUREMENT
445
446        if ( d['latency-any-error'] !=0 ):
447            self.reason=" latency-any-error has error"
448            return self.STATUS_ERR_LATENCY_ANY_ERROR
449
450        return self.STATUS_OK
451
452        # return types
453        STATUS_OK = 0
454        STATUS_ERR_FATAL = 1
455        STATUS_ERR_LATENCY = 2
456        STATUS_ERR_DROP = 3
457        STATUS_ERR_RX_TX_DISTANCE = 4
458        STATUS_ERR_BAD_EXPECTED_MEASUREMENT = 5,
459        STATUS_ERR_LATENCY_ANY_ERROR = 6
460
461def test_TRex_result_parser():
462    t=CTRexResult('trex.txt');
463    t.load_file_lines()
464    t.parse()
465    print(t.result)
466
467
468
469
470if __name__ == "__main__":
471    #test_TRex_result_parser();
472    pass
473