trex_stl_sim.py revision f6d11f9e
1# -*- coding: utf-8 -*-
2
3"""
4Itay Marom
5Cisco Systems, Inc.
6
7Copyright (c) 2015-2015 Cisco Systems, Inc.
8Licensed under the Apache License, Version 2.0 (the "License");
9you may not use this file except in compliance with the License.
10You may obtain a copy of the License at
11    http://www.apache.org/licenses/LICENSE-2.0
12Unless required by applicable law or agreed to in writing, software
13distributed under the License is distributed on an "AS IS" BASIS,
14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15See the License for the specific language governing permissions and
16limitations under the License.
17"""
18# simulator can be run as a standalone
19from . import trex_stl_ext
20from .trex_stl_exceptions import *
21from .trex_stl_streams import *
22from .utils import parsing_opts
23from .trex_stl_client import STLClient
24from .utils import pcap
25from trex_stl_lib.trex_stl_packet_builder_scapy import RawPcapReader, RawPcapWriter, hexdump
26
27from random import randint
28from random import choice as rand_choice
29
30from yaml import YAMLError
31
32import re
33import json
34import argparse
35import tempfile
36import subprocess
37import os
38from operator import itemgetter
39
40class BpSimException(Exception):
41    pass
42
43
44# stateless simulation
45class STLSim(object):
46    MASK_ALL = ((1 << 64) - 1)
47
48    def __init__ (self, bp_sim_path, handler = 0, port_id = 0, api_h = "dummy"):
49
50        self.bp_sim_path = os.path.abspath(bp_sim_path)
51        if not os.path.exists(self.bp_sim_path):
52            raise STLError('BP sim path %s does not exist' % self.bp_sim_path)
53
54        # dummies
55        self.handler = handler
56        self.api_h   = api_h
57        self.port_id = port_id
58
59
60    def generate_start_cmd (self, mult = "1", force = True, duration = -1):
61        return  {"id":1,
62                 "jsonrpc": "2.0",
63                 "method": "start_traffic",
64                 "params": {"handler": self.handler,
65                            "api_h" : self.api_h,
66                            "force":  force,
67                            "port_id": self.port_id,
68                            "mul": parsing_opts.decode_multiplier(mult),
69                            "duration": duration,
70                            "core_mask": self.MASK_ALL}
71                 }
72
73
74
75    # run command
76    # input_list - a list of streams or YAML files
77    # outfile - pcap file to save output, if None its a dry run
78    # dp_core_count - how many DP cores to use
79    # dp_core_index - simulate only specific dp core without merging
80    # is_debug - debug or release image
81    # pkt_limit - how many packets to simulate
82    # mult - multiplier
83    # mode - can be 'valgrind, 'gdb', 'json' or 'none'
84    def run (self,
85             input_list,
86             outfile = None,
87             dp_core_count = 1,
88             dp_core_index = None,
89             is_debug = True,
90             pkt_limit = 5000,
91             mult = "1",
92             duration = -1,
93             mode = 'none',
94             silent = False,
95             tunables = None):
96
97        if not mode in ['none', 'gdb', 'valgrind', 'json', 'yaml','pkt','native']:
98            raise STLArgumentError('mode', mode)
99
100        # listify
101        input_list = input_list if isinstance(input_list, list) else [input_list]
102
103        # check streams arguments
104        if not all([isinstance(i, (STLStream, str)) for i in input_list]):
105            raise STLArgumentError('input_list', input_list)
106
107        # split to two type
108        input_files  = [x for x in input_list if isinstance(x, str)]
109        stream_list = [x for x in input_list if isinstance(x, STLStream)]
110
111        # handle YAMLs
112        if tunables == None:
113            tunables = {}
114
115        for input_file in input_files:
116            try:
117                if not 'direction' in tunables:
118                    tunables['direction'] = self.port_id % 2
119
120                profile = STLProfile.load(input_file, **tunables)
121
122            except STLError as e:
123                s = format_text("\nError while loading profile '{0}'\n".format(input_file), 'bold')
124                s += "\n" + e.brief()
125                raise STLError(s)
126
127            stream_list += profile.get_streams()
128
129
130        # load streams
131        cmds_json = []
132
133        id_counter = 1
134
135        lookup = {}
136
137        # allocate IDs
138        for stream in stream_list:
139            if stream.get_id() is not None:
140                stream_id = stream.get_id()
141            else:
142                stream_id = id_counter
143                id_counter += 1
144
145            name = stream.get_name() if stream.get_name() is not None else id(stream)
146            if name in lookup:
147                raise STLError("multiple streams with name: '{0}'".format(name))
148            lookup[name] = stream_id
149
150        # resolve names
151        for stream in stream_list:
152
153            name = stream.get_name() if stream.get_name() is not None else id(stream)
154            stream_id = lookup[name]
155
156            next_id = -1
157            next = stream.get_next()
158            if next:
159                if not next in lookup:
160                    raise STLError("stream dependency error - unable to find '{0}'".format(next))
161                next_id = lookup[next]
162
163
164            stream_json = stream.to_json()
165            stream_json['next_stream_id'] = next_id
166
167            cmd = {"id":1,
168                   "jsonrpc": "2.0",
169                   "method": "add_stream",
170                   "params": {"handler": self.handler,
171                              "api_h": self.api_h,
172                              "port_id": self.port_id,
173                              "stream_id": stream_id,
174                              "stream": stream_json}
175                   }
176
177            cmds_json.append(cmd)
178
179        # generate start command
180        cmds_json.append(self.generate_start_cmd(mult = mult,
181                                                 force = True,
182                                                 duration = duration))
183
184        if mode == 'json':
185            print(json.dumps(cmds_json, indent = 4, separators=(',', ': '), sort_keys = True))
186            return
187        elif mode == 'yaml':
188            print(STLProfile(stream_list).dump_to_yaml())
189            return
190        elif mode == 'pkt':
191            print(STLProfile(stream_list).dump_as_pkt())
192            return
193        elif mode == 'native':
194            print(STLProfile(stream_list).dump_to_code())
195            return
196
197
198        # start simulation
199        self.outfile = outfile
200        self.dp_core_count = dp_core_count
201        self.dp_core_index = dp_core_index
202        self.is_debug = is_debug
203        self.pkt_limit = pkt_limit
204        self.mult = mult
205        self.duration = duration,
206        self.mode = mode
207        self.silent = silent
208
209        self.__run(cmds_json)
210
211
212    # internal run
213    def __run (self, cmds_json):
214
215        # write to temp file
216        f = tempfile.NamedTemporaryFile(delete = False)
217
218        msg = json.dumps(cmds_json).encode()
219
220        f.write(msg)
221        f.close()
222
223        # launch bp-sim
224        try:
225            self.execute_bp_sim(f.name)
226        finally:
227            os.unlink(f.name)
228
229
230
231    def execute_bp_sim (self, json_filename):
232        if self.is_debug:
233            exe = os.path.join(self.bp_sim_path, 'bp-sim-64-debug')
234        else:
235            exe = os.path.join(self.bp_sim_path, 'bp-sim-64')
236
237        if not os.path.exists(exe):
238            raise STLError("'{0}' does not exists, please build it before calling the simulation".format(exe))
239
240
241        cmd = [exe,
242               '--pcap',
243               '--sl',
244               '--cores',
245               str(self.dp_core_count),
246               '--limit',
247               str(self.pkt_limit),
248               '-f',
249               json_filename]
250
251        # out or dry
252        if not self.outfile:
253            cmd += ['--dry']
254            cmd += ['-o', '/dev/null']
255        else:
256            cmd += ['-o', self.outfile]
257
258        if self.dp_core_index != None:
259            cmd += ['--core_index', str(self.dp_core_index)]
260
261        if self.mode == 'valgrind':
262            cmd = ['valgrind', '--leak-check=full', '--error-exitcode=1'] + cmd
263
264        elif self.mode == 'gdb':
265            cmd = ['/usr/bin/gdb', '--args'] + cmd
266
267        print("executing command: '{0}'".format(" ".join(cmd)))
268
269        if self.silent:
270            FNULL = open(os.devnull, 'wb')
271            rc = subprocess.call(cmd, stdout=FNULL)
272        else:
273            rc = subprocess.call(cmd)
274
275        if rc != 0:
276            raise STLError('simulation has failed with error code {0}'.format(rc))
277
278        self.merge_results()
279
280
281    def merge_results (self):
282        if not self.outfile:
283            return
284
285        if self.dp_core_count == 1:
286            return
287
288        if self.dp_core_index != None:
289            return
290
291
292        if not self.silent:
293            print("Mering cores output to a single pcap file...\n")
294        inputs = ["{0}-{1}".format(self.outfile, index) for index in range(0, self.dp_core_count)]
295        pcap.merge_cap_files(inputs, self.outfile, delete_src = True)
296
297
298
299def is_valid_file(filename):
300    if not os.path.isfile(filename):
301        raise argparse.ArgumentTypeError("The file '%s' does not exist" % filename)
302
303    return filename
304
305
306def unsigned_int (x):
307    x = int(x)
308    if x < 0:
309        raise argparse.ArgumentTypeError("argument must be >= 0")
310
311    return x
312
313def setParserOptions():
314    parser = argparse.ArgumentParser(prog="stl_sim.py")
315
316    parser.add_argument("-f",
317                        dest ="input_file",
318                        help = "input file in YAML or Python format",
319                        type = is_valid_file,
320                        required=True)
321
322    parser.add_argument("-o",
323                        dest = "output_file",
324                        default = None,
325                        help = "output file in ERF format")
326
327
328    parser.add_argument("-c", "--cores",
329                        help = "DP core count [default is 1]",
330                        dest = "dp_core_count",
331                        default = 1,
332                        type = int,
333                        choices = list(range(1, 9)))
334
335    parser.add_argument("-n", "--core_index",
336                        help = "Record only a specific core",
337                        dest = "dp_core_index",
338                        default = None,
339                        type = int)
340
341    parser.add_argument("-i", "--port",
342                        help = "Simulate a specific port ID [default is 0]",
343                        dest = "port_id",
344                        default = 0,
345                        type = int)
346
347
348    parser.add_argument("-r", "--release",
349                        help = "runs on release image instead of debug [default is False]",
350                        action = "store_true",
351                        default = False)
352
353
354    parser.add_argument("-s", "--silent",
355                        help = "runs on silent mode (no stdout) [default is False]",
356                        action = "store_true",
357                        default = False)
358
359    parser.add_argument("-l", "--limit",
360                        help = "limit test total packet count [default is 5000]",
361                        default = 5000,
362                        type = unsigned_int)
363
364    parser.add_argument('-m', '--multiplier',
365                        help = parsing_opts.match_multiplier_help,
366                        dest = 'mult',
367                        default = "1",
368                        type = parsing_opts.match_multiplier_strict)
369
370    parser.add_argument('-d', '--duration',
371                        help = "run duration",
372                        dest = 'duration',
373                        default = -1,
374                        type = float)
375
376
377    parser.add_argument('-t',
378                        help = 'sets tunable for a profile',
379                        dest = 'tunables',
380                        default = None,
381                        type = parsing_opts.decode_tunables)
382
383    parser.add_argument('-p', '--path',
384                        help = "BP sim path",
385                        dest = 'bp_sim_path',
386                        default = None,
387                        type = str)
388
389
390    group = parser.add_mutually_exclusive_group()
391
392    group.add_argument("-x", "--valgrind",
393                       help = "run under valgrind [default is False]",
394                       action = "store_true",
395                       default = False)
396
397    group.add_argument("-g", "--gdb",
398                       help = "run under GDB [default is False]",
399                       action = "store_true",
400                       default = False)
401
402    group.add_argument("--json",
403                       help = "generate JSON output only to stdout [default is False]",
404                       action = "store_true",
405                       default = False)
406
407    group.add_argument("--pkt",
408                       help = "Parse the packet and show it as hex",
409                       action = "store_true",
410                       default = False)
411
412    group.add_argument("--yaml",
413                       help = "generate YAML from input file [default is False]",
414                       action = "store_true",
415                       default = False)
416
417    group.add_argument("--native",
418                       help = "generate Python code with stateless profile from input file [default is False]",
419                       action = "store_true",
420                       default = False)
421
422    group.add_argument("--test_multi_core",
423                       help = "runs the profile with c=1-8",
424                       action = "store_true",
425                       default = False)
426
427    return parser
428
429
430def validate_args (parser, options):
431
432    if options.dp_core_index:
433        if not options.dp_core_index in range(0, options.dp_core_count):
434            parser.error("DP core index valid range is 0 to {0}".format(options.dp_core_count - 1))
435
436    # zero is ok - no limit, but other values must be at least as the number of cores
437    if (options.limit != 0) and options.limit < options.dp_core_count:
438        parser.error("limit cannot be lower than number of DP cores")
439
440
441# a more flexible check
442def compare_caps (cap1, cap2, max_diff_sec = (5 * 1e-6)):
443    pkts1 = list(RawPcapReader(cap1))
444    pkts2 = list(RawPcapReader(cap2))
445
446    if len(pkts1) != len(pkts2):
447        print('{0} contains {1} packets vs. {2} contains {3} packets'.format(cap1, len(pkts1), cap2, len(pkts2)))
448        return False
449
450    # to be less strict we define equality if all packets from cap1 exists and in cap2
451    # and vice versa
452    # 'exists' means the same packet with abs(TS1-TS2) < 5nsec
453    # its O(n^2) but who cares, right ?
454    for i, pkt1 in enumerate(pkts1):
455        ts1 = float(pkt1[1][0]) + (float(pkt1[1][1]) / 1e6)
456        found = None
457        for j, pkt2 in enumerate(pkts2):
458            ts2 = float(pkt2[1][0]) + (float(pkt2[1][1]) / 1e6)
459
460            if abs(ts1-ts2) > max_diff_sec:
461                break
462
463            if pkt1[0] == pkt2[0]:
464                found = j
465                break
466
467
468        if found is None:
469            print(format_text("cannot find packet #{0} from {1} in {2}\n".format(i, cap1, cap2), 'bold'))
470            return False
471        else:
472            del pkts2[found]
473
474    return True
475
476
477def hexdiff (d1, d2):
478    rc = []
479
480    if len(d1) != len(d2):
481        return rc
482
483    for i in range(len(d1)):
484        if d1[i] != d2[i]:
485            rc.append(i)
486    return rc
487
488def prettyhex (h, diff_list):
489    if type(h[0]) == str:
490        h = [ord(x) for x in h]
491
492    for i in range(len(h)):
493
494        if i in diff_list:
495            sys.stdout.write("->'0x%02x'<-" % h[i])
496        else:
497            sys.stdout.write("  '0x%02x'  " % h[i])
498        if ((i % 9) == 8):
499            print("")
500
501    print("")
502
503# a more strict comparsion 1 <--> 1
504def compare_caps_strict (cap1, cap2, max_diff_sec = (5 * 1e-6)):
505    pkts1 = list(RawPcapReader(cap1))
506    pkts2 = list(RawPcapReader(cap2))
507
508    if len(pkts1) != len(pkts2):
509        print('{0} contains {1} packets vs. {1} contains {2} packets'.format(cap1, len(pkts1), cap2, len(pkts2)))
510        return False
511
512    # a strict check
513    for pkt1, pkt2, i in zip(pkts1, pkts2, range(1, len(pkts1))):
514        ts1 = float(pkt1[1][0]) + (float(pkt1[1][1]) / 1e6)
515        ts2 = float(pkt2[1][0]) + (float(pkt2[1][1]) / 1e6)
516
517        if abs(ts1-ts2) > 0.000005: # 5 nsec
518            print(format_text("TS error: cap files '{0}', '{1}' differ in cap #{2} - '{3}' vs. '{4}'\n".format(cap1, cap2, i, ts1, ts2), 'bold'))
519            return False
520
521        if pkt1[0] != pkt2[0]:
522            print(format_text("RAW error: cap files '{0}', '{1}' differ in cap #{2}\n".format(cap1, cap2, i), 'bold'))
523
524            diff_list = hexdiff(pkt1[0], pkt2[0])
525
526            print("{0} - packet #{1}:\n".format(cap1, i))
527            prettyhex(pkt1[0], diff_list)
528
529            print("\n{0} - packet #{1}:\n".format(cap2, i))
530            prettyhex(pkt2[0], diff_list)
531
532            print("")
533            return False
534
535    return True
536
537
538def test_multi_core (r, options):
539
540    for core_count in range(1, 9):
541        r.run(input_list = options.input_file,
542              outfile = '{0}.cap'.format(core_count),
543              dp_core_count = core_count,
544              is_debug = (not options.release),
545              pkt_limit = options.limit,
546              mult = options.mult,
547              duration = options.duration,
548              mode = 'none',
549              silent = True,
550              tunables = options.tunables)
551
552    print("")
553
554    for core_count in range(1, 9):
555        print(format_text("comparing {0} cores to 1 core:\n".format(core_count), 'underline'))
556        rc = compare_caps_strict('1.cap', '{0}.cap'.format(core_count))
557        if rc:
558            print("[Passed]\n")
559
560    return
561
562
563def main (args = None):
564    parser = setParserOptions()
565    options = parser.parse_args(args = args)
566
567    validate_args(parser, options)
568
569
570
571    if options.valgrind:
572        mode = 'valgrind'
573    elif options.gdb:
574        mode = 'gdb'
575    elif options.json:
576        mode = 'json'
577    elif options.yaml:
578        mode = 'yaml'
579    elif options.native:
580        mode = 'native'
581    elif options.pkt:
582        mode = 'pkt'
583    elif options.test_multi_core:
584        mode = 'test_multi_core'
585    else:
586        mode = 'none'
587
588    try:
589        r = STLSim(bp_sim_path = options.bp_sim_path, port_id = options.port_id)
590
591        if mode == 'test_multi_core':
592            test_multi_core(r, options)
593        else:
594            r.run(input_list = options.input_file,
595                  outfile = options.output_file,
596                  dp_core_count = options.dp_core_count,
597                  dp_core_index = options.dp_core_index,
598                  is_debug = (not options.release),
599                  pkt_limit = options.limit,
600                  mult = options.mult,
601                  duration = options.duration,
602                  mode = mode,
603                  silent = options.silent,
604                  tunables = options.tunables)
605
606    except KeyboardInterrupt as e:
607        print("\n\n*** Caught Ctrl + C... Exiting...\n\n")
608        return (-1)
609
610    except STLError as e:
611        print(e)
612        return (-1)
613
614    return (0)
615
616
617if __name__ == '__main__':
618    main()
619
620
621