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