parsing_opts.py revision 75585e21
1import argparse
2from collections import namedtuple, OrderedDict
3from .common import list_intersect, list_difference, is_valid_ipv4, is_valid_mac, list_remove_dup
4from .text_opts import format_text
5from ..trex_stl_types import *
6from .constants import ON_OFF_DICT, UP_DOWN_DICT, FLOW_CTRL_DICT
7
8import sys
9import re
10import os
11
12ArgumentPack = namedtuple('ArgumentPack', ['name_or_flags', 'options'])
13ArgumentGroup = namedtuple('ArgumentGroup', ['type', 'args', 'options'])
14
15
16# list of available parsing options
17MULTIPLIER = 1
18MULTIPLIER_STRICT = 2
19PORT_LIST = 3
20ALL_PORTS = 4
21PORT_LIST_WITH_ALL = 5
22FILE_PATH = 6
23FILE_FROM_DB = 7
24SERVER_IP = 8
25STREAM_FROM_PATH_OR_FILE = 9
26DURATION = 10
27FORCE = 11
28DRY_RUN = 12
29XTERM = 13
30TOTAL = 14
31FULL_OUTPUT = 15
32IPG = 16
33SPEEDUP = 17
34COUNT = 18
35PROMISCUOUS = 19
36LINK_STATUS = 20
37LED_STATUS = 21
38TUNABLES = 22
39REMOTE_FILE = 23
40LOCKED = 24
41PIN_CORES = 25
42CORE_MASK = 26
43DUAL = 27
44FLOW_CTRL = 28
45SUPPORTED = 29
46FILE_PATH_NO_CHECK = 30
47
48OUTPUT_FILENAME = 31
49ALL_FILES = 32
50LIMIT = 33
51PORT_RESTART   = 34
52
53IPV4 = 35
54DEST = 36
55RETRIES = 37
56
57RX_FILTER_MODE = 38
58SOURCE_PORT = 39
59PING_IPV4 = 40
60PKT_SIZE = 41
61
62GLOBAL_STATS = 50
63PORT_STATS = 51
64PORT_STATUS = 52
65STREAMS_STATS = 53
66STATS_MASK = 54
67CPU_STATS = 55
68MBUF_STATS = 56
69EXTENDED_STATS = 57
70EXTENDED_INC_ZERO_STATS = 58
71
72STREAMS_MASK = 60
73CORE_MASK_GROUP = 61
74
75# ALL_STREAMS = 61
76# STREAM_LIST_WITH_ALL = 62
77
78
79
80# list of ArgumentGroup types
81MUTEX = 1
82
83def check_negative(value):
84    ivalue = int(value)
85    if ivalue < 0:
86        raise argparse.ArgumentTypeError("non positive value provided: '{0}'".format(value))
87    return ivalue
88
89def match_time_unit(val):
90    '''match some val against time shortcut inputs '''
91    match = re.match("^(\d+(\.\d+)?)([m|h]?)$", val)
92    if match:
93        digit = float(match.group(1))
94        unit = match.group(3)
95        if not unit:
96            return digit
97        elif unit == 'm':
98            return digit*60
99        else:
100            return digit*60*60
101    else:
102        raise argparse.ArgumentTypeError("Duration should be passed in the following format: \n"
103                                         "-d 100 : in sec \n"
104                                         "-d 10m : in min \n"
105                                         "-d 1h  : in hours")
106
107
108match_multiplier_help = """Multiplier should be passed in the following format:
109                          [number][<empty> | bps | kbps | mbps |  gbps | pps | kpps | mpps | %% ].
110
111                          no suffix will provide an absoulute factor and percentage
112                          will provide a percentage of the line rate. examples
113
114                          '-m 10',
115                          '-m 10kbps',
116                          '-m 10kbpsl1',
117                          '-m 10mpps',
118                          '-m 23%% '
119
120                          '-m 23%%' : is 23%% L1 bandwidth
121                          '-m 23mbps': is 23mbps in L2 bandwidth (including FCS+4)
122                          '-m 23mbpsl1': is 23mbps in L1 bandwidth
123
124                          """
125
126
127# decodes multiplier
128# if allow_update - no +/- is allowed
129# divide states between how many entities the
130# value should be divided
131def decode_multiplier(val, allow_update = False, divide_count = 1):
132
133    factor_table = {None: 1, 'k': 1e3, 'm': 1e6, 'g': 1e9}
134    pattern = "^(\d+(\.\d+)?)(((k|m|g)?(bpsl1|pps|bps))|%)?"
135
136    # do we allow updates ?  +/-
137    if not allow_update:
138        pattern += "$"
139        match = re.match(pattern, val)
140        op = None
141    else:
142        pattern += "([\+\-])?$"
143        match = re.match(pattern, val)
144        if match:
145            op  = match.group(7)
146        else:
147            op = None
148
149    result = {}
150
151    if not match:
152        return None
153
154    # value in group 1
155    value = float(match.group(1))
156
157    # decode unit as whole
158    unit = match.group(3)
159
160    # k,m,g
161    factor = match.group(5)
162
163    # type of multiplier
164    m_type = match.group(6)
165
166    # raw type (factor)
167    if not unit:
168        result['type'] = 'raw'
169        result['value'] = value
170
171    # percentage
172    elif unit == '%':
173        result['type'] = 'percentage'
174        result['value']  = value
175
176    elif m_type == 'bps':
177        result['type'] = 'bps'
178        result['value'] = value * factor_table[factor]
179
180    elif m_type == 'pps':
181        result['type'] = 'pps'
182        result['value'] = value * factor_table[factor]
183
184    elif m_type == 'bpsl1':
185        result['type'] = 'bpsl1'
186        result['value'] = value * factor_table[factor]
187
188
189    if op == "+":
190        result['op'] = "add"
191    elif op == "-":
192        result['op'] = "sub"
193    else:
194        result['op'] = "abs"
195
196    if result['op'] != 'percentage':
197        result['value'] = result['value'] / divide_count
198
199    return result
200
201
202
203def match_multiplier(val):
204    '''match some val against multiplier  shortcut inputs '''
205    result = decode_multiplier(val, allow_update = True)
206    if not result:
207        raise argparse.ArgumentTypeError(match_multiplier_help)
208
209    return val
210
211
212def match_multiplier_strict(val):
213    '''match some val against multiplier  shortcut inputs '''
214    result = decode_multiplier(val, allow_update = False)
215    if not result:
216        raise argparse.ArgumentTypeError(match_multiplier_help)
217
218    return val
219
220def hex_int (val):
221    pattern = r"0x[1-9a-fA-F][0-9a-fA-F]*"
222
223    if not re.match(pattern, val):
224        raise argparse.ArgumentTypeError("{0} is not a valid positive HEX formatted number".format(val))
225
226    return int(val, 16)
227
228
229def is_valid_file(filename):
230    if not os.path.isfile(filename):
231        raise argparse.ArgumentTypeError("The file '%s' does not exist" % filename)
232
233    return filename
234
235def check_ipv4_addr (ipv4_str):
236    if not is_valid_ipv4(ipv4_str):
237        raise argparse.ArgumentTypeError("invalid IPv4 address: '{0}'".format(ipv4_str))
238
239    return ipv4_str
240
241def check_pkt_size (pkt_size):
242    try:
243        pkt_size = int(pkt_size)
244    except ValueError:
245        raise argparse.ArgumentTypeError("invalid packet size type: '{0}'".format(pkt_size))
246
247    if (pkt_size < 64) or (pkt_size > 9000):
248        raise argparse.ArgumentTypeError("invalid packet size: '{0}' - valid range is 64 to 9000".format(pkt_size))
249
250    return pkt_size
251
252def check_dest_addr (addr):
253    if not (is_valid_ipv4(addr) or is_valid_mac(addr)):
254        raise argparse.ArgumentTypeError("not a valid IPv4 or MAC address: '{0}'".format(addr))
255
256    return addr
257
258
259def decode_tunables (tunable_str):
260    tunables = {}
261
262    # split by comma to tokens
263    tokens = tunable_str.split(',')
264
265    # each token is of form X=Y
266    for token in tokens:
267        m = re.search('(\S+)=(.+)', token)
268        if not m:
269            raise argparse.ArgumentTypeError("bad syntax for tunables: {0}".format(token))
270        val = m.group(2)           # string
271        if val.startswith(("'", '"')) and val.endswith(("'", '"')) and len(val) > 1: # need to remove the quotes from value
272            val = val[1:-1]
273        elif val.startswith('0x'): # hex
274            val = int(val, 16)
275        else:
276            try:
277                if '.' in val:     # float
278                    val = float(val)
279                else:              # int
280                    val = int(val)
281            except:
282                pass
283        tunables[m.group(1)] = val
284
285    return tunables
286
287
288
289OPTIONS_DB = {MULTIPLIER: ArgumentPack(['-m', '--multiplier'],
290                                 {'help': match_multiplier_help,
291                                  'dest': "mult",
292                                  'default': "1",
293                                  'type': match_multiplier}),
294
295              MULTIPLIER_STRICT: ArgumentPack(['-m', '--multiplier'],
296                               {'help': match_multiplier_help,
297                                  'dest': "mult",
298                                  'default': "1",
299                                  'type': match_multiplier_strict}),
300
301              TOTAL: ArgumentPack(['-t', '--total'],
302                                 {'help': "traffic will be divided between all ports specified",
303                                  'dest': "total",
304                                  'default': False,
305                                  'action': "store_true"}),
306
307              IPG: ArgumentPack(['-i', '--ipg'],
308                                {'help': "IPG value in usec between packets. default will be from the pcap",
309                                 'dest': "ipg_usec",
310                                 'default':  None,
311                                 'type': float}),
312
313
314              SPEEDUP: ArgumentPack(['-s', '--speedup'],
315                                   {'help': "Factor to accelerate the injection. effectively means IPG = IPG / SPEEDUP",
316                                    'dest': "speedup",
317                                    'default':  1.0,
318                                    'type': float}),
319
320              COUNT: ArgumentPack(['-n', '--count'],
321                                  {'help': "How many times to perform action [default is 1, 0 means forever]",
322                                   'dest': "count",
323                                   'default':  1,
324                                   'type': int}),
325
326              PROMISCUOUS: ArgumentPack(['--prom'],
327                                        {'help': "Set port promiscuous on/off",
328                                         'choices': ON_OFF_DICT}),
329
330              LINK_STATUS: ArgumentPack(['--link'],
331                                     {'help': 'Set link status up/down',
332                                      'choices': UP_DOWN_DICT}),
333
334              LED_STATUS: ArgumentPack(['--led'],
335                                   {'help': 'Set LED status on/off',
336                                    'choices': ON_OFF_DICT}),
337
338              FLOW_CTRL: ArgumentPack(['--fc'],
339                                   {'help': 'Set Flow Control type',
340                                    'dest': 'flow_ctrl',
341                                    'choices': FLOW_CTRL_DICT}),
342
343              RX_FILTER_MODE: ArgumentPack(['--rxf'],
344                                           {'help': 'Set RX filtering mode',
345                                            'dest': 'rx_filter_mode',
346                                            'choices': ['hw', 'all']}),
347
348
349              IPV4: ArgumentPack(['--ipv4'],
350                                 {'help': 'IPv4 address(s) for the port(s)',
351                                  'dest': 'ipv4',
352                                  'nargs': '+',
353                                  'default': None,
354                                  'type': check_ipv4_addr}),
355
356              DEST: ArgumentPack(['--dest'],
357                                 {'help': 'Destination address(s) for the port(s) in either IPv4 or MAC format',
358                                  'dest': 'dest',
359                                  'nargs': '+',
360                                  'default': None,
361                                  'type': check_dest_addr}),
362
363              RETRIES: ArgumentPack(['-r', '--retries'],
364                                    {'help': 'retries count [default is zero]',
365                                     'dest': 'retries',
366                                     'default':  0,
367                                     'type': int}),
368
369
370              OUTPUT_FILENAME: ArgumentPack(['-o', '--output'],
371                                            {'help': 'Output PCAP filename',
372                                             'dest': 'output_filename',
373                                             'default': None,
374                                             'required': True,
375                                             'type': str}),
376
377
378              PORT_RESTART: ArgumentPack(['-r', '--restart'],
379                                         {'help': 'hard restart port(s)',
380                                          'dest': 'restart',
381                                          'default': False,
382                                          'action': 'store_true'}),
383
384
385              ALL_FILES: ArgumentPack(['--all'],
386                                      {'help': 'change RX port filter to fetch all packets',
387                                       'dest': 'all',
388                                       'default': False,
389                                       'action': "store_true"}),
390
391
392              LIMIT: ArgumentPack(['-l', '--limit'],
393                                  {'help': 'Limit the packet count to be written to the file',
394                                   'dest': 'limit',
395                                   'default':  1000,
396                                   'type': int}),
397
398
399              SUPPORTED: ArgumentPack(['--supp'],
400                                   {'help': 'Show which attributes are supported by current NICs',
401                                    'default': None,
402                                    'action': 'store_true'}),
403
404              TUNABLES: ArgumentPack(['-t'],
405                                     {'help': "Sets tunables for a profile. Example: '-t fsize=100,pg_id=7'",
406                                      'metavar': 'T1=VAL[,T2=VAL ...]',
407                                      'dest': "tunables",
408                                      'default': None,
409                                      'action': 'merge',
410                                      'type': decode_tunables}),
411
412              PORT_LIST: ArgumentPack(['--port', '-p'],
413                                        {"nargs": '+',
414                                         'dest':'ports',
415                                         'metavar': 'PORTS',
416                                         'action': 'merge',
417                                         'type': int,
418                                         'help': "A list of ports on which to apply the command",
419                                         'default': []}),
420
421
422              SOURCE_PORT: ArgumentPack(['--port', '-p'],
423                                        {'dest':'source_port',
424                                         'type': int,
425                                         'help': 'source port for the action',
426                                         'required': True}),
427
428              PING_IPV4: ArgumentPack(['-d'],
429                                      {'help': 'which IPv4 to ping',
430                                      'dest': 'ping_ipv4',
431                                      'required': True,
432                                      'type': check_ipv4_addr}),
433
434              PKT_SIZE: ArgumentPack(['-s'],
435                                     {'dest':'pkt_size',
436                                      'help': 'packet size to use',
437                                      'default': 64,
438                                      'type': check_pkt_size}),
439
440
441              ALL_PORTS: ArgumentPack(['-a'],
442                                        {"action": "store_true",
443                                         "dest": "all_ports",
444                                         'help': "Set this flag to apply the command on all available ports",
445                                         'default': False},),
446
447              DURATION: ArgumentPack(['-d'],
448                                        {'action': "store",
449                                         'metavar': 'TIME',
450                                         'dest': 'duration',
451                                         'type': match_time_unit,
452                                         'default': -1.0,
453                                         'help': "Set duration time for job."}),
454
455              FORCE: ArgumentPack(['--force'],
456                                        {"action": "store_true",
457                                         'default': False,
458                                         'help': "Set if you want to stop active ports before appyling command."}),
459
460              REMOTE_FILE: ArgumentPack(['-r', '--remote'],
461                                        {"action": "store_true",
462                                         'default': False,
463                                         'help': "file path should be interpeted by the server (remote file)"}),
464
465              DUAL: ArgumentPack(['--dual'],
466                                 {"action": "store_true",
467                                  'default': False,
468                                  'help': "Transmit in a dual mode - requires ownership on the adjacent port"}),
469
470              FILE_PATH: ArgumentPack(['-f'],
471                                      {'metavar': 'FILE',
472                                       'dest': 'file',
473                                       'nargs': 1,
474                                       'required': True,
475                                       'type': is_valid_file,
476                                       'help': "File path to load"}),
477
478              FILE_PATH_NO_CHECK: ArgumentPack(['-f'],
479                                      {'metavar': 'FILE',
480                                       'dest': 'file',
481                                       'nargs': 1,
482                                       'required': True,
483                                       'type': str,
484                                       'help': "File path to load"}),
485
486              FILE_FROM_DB: ArgumentPack(['--db'],
487                                         {'metavar': 'LOADED_STREAM_PACK',
488                                          'help': "A stream pack which already loaded into console cache."}),
489
490              SERVER_IP: ArgumentPack(['--server'],
491                                      {'metavar': 'SERVER',
492                                       'help': "server IP"}),
493
494              DRY_RUN: ArgumentPack(['-n', '--dry'],
495                                    {'action': 'store_true',
496                                     'dest': 'dry',
497                                     'default': False,
498                                     'help': "Dry run - no traffic will be injected"}),
499
500              XTERM: ArgumentPack(['-x', '--xterm'],
501                                  {'action': 'store_true',
502                                   'dest': 'xterm',
503                                   'default': False,
504                                   'help': "Starts TUI in xterm window"}),
505
506              LOCKED: ArgumentPack(['-l', '--locked'],
507                                   {'action': 'store_true',
508                                    'dest': 'locked',
509                                    'default': False,
510                                    'help': "Locks TUI on legend mode"}),
511
512              FULL_OUTPUT: ArgumentPack(['--full'],
513                                         {'action': 'store_true',
514                                          'help': "Prompt full info in a JSON format"}),
515
516              GLOBAL_STATS: ArgumentPack(['-g'],
517                                         {'action': 'store_true',
518                                          'help': "Fetch only global statistics"}),
519
520              PORT_STATS: ArgumentPack(['-p'],
521                                       {'action': 'store_true',
522                                        'help': "Fetch only port statistics"}),
523
524              PORT_STATUS: ArgumentPack(['--ps'],
525                                        {'action': 'store_true',
526                                         'help': "Fetch only port status data"}),
527
528              STREAMS_STATS: ArgumentPack(['-s'],
529                                          {'action': 'store_true',
530                                           'help': "Fetch only streams stats"}),
531
532              CPU_STATS: ArgumentPack(['-c'],
533                                      {'action': 'store_true',
534                                       'help': "Fetch only CPU utilization stats"}),
535
536              MBUF_STATS: ArgumentPack(['-m'],
537                                       {'action': 'store_true',
538                                        'help': "Fetch only MBUF utilization stats"}),
539
540              EXTENDED_STATS: ArgumentPack(['-x'],
541                                       {'action': 'store_true',
542                                        'help': "Fetch xstats of port, excluding lines with zero values"}),
543
544              EXTENDED_INC_ZERO_STATS: ArgumentPack(['--xz'],
545                                       {'action': 'store_true',
546                                        'help': "Fetch xstats of port, including lines with zero values"}),
547
548              STREAMS_MASK: ArgumentPack(['--streams'],
549                                         {"nargs": '+',
550                                          'dest':'streams',
551                                          'metavar': 'STREAMS',
552                                          'type': int,
553                                          'help': "A list of stream IDs to query about. Default: analyze all streams",
554                                          'default': []}),
555
556
557              PIN_CORES: ArgumentPack(['--pin'],
558                                      {'action': 'store_true',
559                                       'dest': 'pin_cores',
560                                       'default': False,
561                                       'help': "Pin cores to interfaces - cores will be divided between interfaces (performance boot for symetric profiles)"}),
562
563              CORE_MASK: ArgumentPack(['--core_mask'],
564                                      {'action': 'store',
565                                       'nargs': '+',
566                                       'type': hex_int,
567                                       'dest': 'core_mask',
568                                       'default': None,
569                                       'help': "Core mask - only cores responding to the bit mask will be active"}),
570
571              # advanced options
572              PORT_LIST_WITH_ALL: ArgumentGroup(MUTEX, [PORT_LIST,
573                                                        ALL_PORTS],
574                                                {'required': False}),
575
576
577              STREAM_FROM_PATH_OR_FILE: ArgumentGroup(MUTEX, [FILE_PATH,
578                                                              FILE_FROM_DB],
579                                                      {'required': True}),
580              STATS_MASK: ArgumentGroup(MUTEX, [GLOBAL_STATS,
581                                                PORT_STATS,
582                                                PORT_STATUS,
583                                                STREAMS_STATS,
584                                                CPU_STATS,
585                                                MBUF_STATS,
586                                                EXTENDED_STATS,
587                                                EXTENDED_INC_ZERO_STATS,],
588                                        {}),
589
590
591              CORE_MASK_GROUP:  ArgumentGroup(MUTEX, [PIN_CORES,
592                                                      CORE_MASK],
593                                              {'required': False}),
594
595              }
596
597class _MergeAction(argparse._AppendAction):
598    def __call__(self, parser, namespace, values, option_string=None):
599        items = getattr(namespace, self.dest)
600        if not items:
601            items = values
602        elif type(items) is list and type(values) is list:
603            items.extend(values)
604        elif type(items) is dict and type(values) is dict: # tunables are dict
605            items.update(values)
606        else:
607            raise Exception("Argparser 'merge' option should be used on dict or list.")
608
609        setattr(namespace, self.dest, items)
610
611class CCmdArgParser(argparse.ArgumentParser):
612
613    def __init__(self, stateless_client, *args, **kwargs):
614        super(CCmdArgParser, self).__init__(*args, **kwargs)
615        self.stateless_client = stateless_client
616        self.cmd_name = kwargs.get('prog')
617        self.register('action', 'merge', _MergeAction)
618
619    # hook this to the logger
620    def _print_message(self, message, file=None):
621        self.stateless_client.logger.log(message)
622
623    def error(self, message):
624        self.print_usage()
625        self._print_message(('%s: error: %s\n') % (self.prog, message))
626        raise ValueError(message)
627
628    def has_ports_cfg (self, opts):
629        return hasattr(opts, "all_ports") or hasattr(opts, "ports")
630
631    def parse_args(self, args=None, namespace=None, default_ports=None, verify_acquired=False):
632        try:
633            opts = super(CCmdArgParser, self).parse_args(args, namespace)
634            if opts is None:
635                return RC_ERR("'{0}' - invalid arguments".format(self.cmd_name))
636
637            if not self.has_ports_cfg(opts):
638                return opts
639
640            # if all ports are marked or
641            if (getattr(opts, "all_ports", None) == True) or (getattr(opts, "ports", None) == []):
642                if default_ports is None:
643                    opts.ports = self.stateless_client.get_acquired_ports()
644                else:
645                    opts.ports = default_ports
646
647            opts.ports = list_remove_dup(opts.ports)
648
649            # so maybe we have ports configured
650            invalid_ports = list_difference(opts.ports, self.stateless_client.get_all_ports())
651            if invalid_ports:
652                msg = "{0}: port(s) {1} are not valid port IDs".format(self.cmd_name, invalid_ports)
653                self.stateless_client.logger.log(format_text(msg, 'bold'))
654                return RC_ERR(msg)
655
656            # verify acquired ports
657            if verify_acquired:
658                acquired_ports = self.stateless_client.get_acquired_ports()
659
660                diff = list_difference(opts.ports, acquired_ports)
661                if diff:
662                    msg = "{0} - port(s) {1} are not acquired".format(self.cmd_name, diff)
663                    self.stateless_client.logger.log(format_text(msg, 'bold'))
664                    return RC_ERR(msg)
665
666                # no acquire ports at all
667                if not acquired_ports:
668                    msg = "{0} - no acquired ports".format(self.cmd_name)
669                    self.stateless_client.logger.log(format_text(msg, 'bold'))
670                    return RC_ERR(msg)
671
672
673            return opts
674
675        except ValueError as e:
676            return RC_ERR("'{0}' - {1}".format(self.cmd_name, str(e)))
677
678        except SystemExit:
679            # recover from system exit scenarios, such as "help", or bad arguments.
680            return RC_ERR("'{0}' - {1}".format(self.cmd_name, "no action"))
681
682
683def get_flags (opt):
684    return OPTIONS_DB[opt].name_or_flags
685
686def gen_parser(stateless_client, op_name, description, *args):
687    parser = CCmdArgParser(stateless_client, prog=op_name, conflict_handler='resolve',
688                           description=description)
689    for param in args:
690        try:
691
692            if isinstance(param, int):
693                argument = OPTIONS_DB[param]
694            else:
695                argument = param
696
697            if isinstance(argument, ArgumentGroup):
698                if argument.type == MUTEX:
699                    # handle as mutually exclusive group
700                    group = parser.add_mutually_exclusive_group(**argument.options)
701                    for sub_argument in argument.args:
702                        group.add_argument(*OPTIONS_DB[sub_argument].name_or_flags,
703                                           **OPTIONS_DB[sub_argument].options)
704                else:
705                    # ignore invalid objects
706                    continue
707            elif isinstance(argument, ArgumentPack):
708                parser.add_argument(*argument.name_or_flags,
709                                    **argument.options)
710            else:
711                # ignore invalid objects
712                continue
713        except KeyError as e:
714            cause = e.args[0]
715            raise KeyError("The attribute '{0}' is missing as a field of the {1} option.\n".format(cause, param))
716    return parser
717
718
719if __name__ == "__main__":
720    pass
721