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