teststeps: Improvements and bugfixing of teststeps
[vswitchperf.git] / tools / pkt_gen / ixnet / ixnet.py
1 # Copyright 2015-2017 Intel Corporation.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #   http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 """IxNetwork traffic generator model.
15
16 Provides a model for an IxNetwork machine and appropriate applications.
17
18 This requires the following settings in your config file:
19
20 * TRAFFICGEN_IXNET_LIB_PATH
21     IxNetwork libraries path
22 * TRAFFICGEN_IXNET_PORT
23     IxNetwork host port number
24 * TRAFFICGEN_IXNET_USER
25     IxNetwork host user name
26 * TRAFFICGEN_IXNET_TESTER_RESULT_DIR
27     The result directory on the IxNetwork computer
28 * TRAFFICGEN_IXNET_DUT_RESULT_DIR
29     The result directory on DUT. This needs to map to the same directory
30     as the previous one
31
32 The following settings are also required. These can likely be shared
33 with an 'Ixia' traffic generator instance:
34
35 * TRAFFICGEN_IXIA_HOST
36     IXIA chassis IP address
37 * TRAFFICGEN_IXIA_CARD
38     IXIA card
39 * TRAFFICGEN_IXIA_PORT1
40     IXIA Tx port
41 * TRAFFICGEN_IXIA_PORT2
42     IXIA Rx port
43
44 If any of these don't exist, the application will raise an exception
45 (EAFP).
46
47 Additional Configuration:
48 -------------------------
49
50 You will also need to configure the IxNetwork machine to start the IXIA
51 IxNetworkTclServer. This can be started like so:
52
53 1. Connect to the IxNetwork machine using RDP
54 2. Go to:
55
56     Start->
57       Programs ->
58         Ixia ->
59           IxNetwork ->
60             IxNetwork 7.21.893.14 GA ->
61               IxNetworkTclServer
62
63    Pin a shortcut to this application to the taskbar.
64 3. Before running it right click the pinned shortcut  and go to
65    "Properties". Here change the port number to your own port number.
66    This will be the same value as "TRAFFICGEN_IXNET_PORT" above.
67 4. You will find this on the shortcut tab under the heading "Target"
68 5. Finally run it. If you see the following error check that you
69    followed the above steps exactly:
70
71        ERROR: couldn't open socket : connection refused
72
73 Debugging:
74 ----------
75
76 This method of automation is quite error prone as the IxNetwork API
77 does not give any feedback as to the status of tests. As such, it can
78 be expected that the user have access to the IxNetwork machine should
79 this trafficgen need to be debugged.
80 """
81 import tkinter
82 import logging
83 import os
84 import re
85 import csv
86
87 from collections import OrderedDict
88 from tools.pkt_gen import trafficgen
89 from conf import settings
90 from conf import merge_spec
91 from core.results.results_constants import ResultsConstants
92
93 _ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
94
95 _RESULT_RE = r'(?:\{kString,result\},\{kString,)(\w+)(?:\})'
96 _RESULTPATH_RE = r'(?:\{kString,resultPath\},\{kString,)([\\\w\.\-\:]+)(?:\})'
97
98
99 def _build_set_cmds(values, prefix='dict set'):
100     """Generate a list of 'dict set' args for Tcl.
101
102     Parse a dictionary and recursively build the arguments for the
103     'dict set' Tcl command, given that this is of the format:
104
105         dict set [name...] [key] [value]
106
107     For example, for a non-nested dict (i.e. a non-dict element):
108
109         dict set mydict mykey myvalue
110
111     For a nested dict (i.e. a dict element):
112
113         dict set mydict mysubdict mykey myvalue
114
115     :param values: Dictionary to yield values for
116     :param prefix: Prefix to append to output string. Generally the
117         already generated part of the command.
118
119     :yields: Output strings to be passed to a `Tcl` instance.
120     """
121     for key in values:
122         value = values[key]
123
124         if isinstance(value, dict):
125             _prefix = ' '.join([prefix, key]).strip()
126             for subkey in _build_set_cmds(value, _prefix):
127                 yield subkey
128             continue
129
130         # tcl doesn't recognise the strings "True" or "False", only "1"
131         # or "0". Special case to convert them
132         if isinstance(value, bool):
133             value = str(int(value))
134         else:
135             value = str(value)
136
137         if prefix:
138             yield ' '.join([prefix, key, value]).strip()
139         else:
140             yield ' '.join([key, value]).strip()
141
142
143 class IxNet(trafficgen.ITrafficGenerator):
144     """A wrapper around IXIA IxNetwork applications.
145
146     Runs different traffic generator tests through an Ixia traffic
147     generator chassis by generating TCL scripts from templates.
148
149     Currently only the RFC2544 tests are implemented.
150     """
151
152     def __init__(self):
153         """Initialize IXNET members
154         """
155         super().__init__()
156         self._tclsh = tkinter.Tcl()
157         self._script = None
158         self._cfg = None
159         self._logger = logging.getLogger(__name__)
160         self._params = None
161         self._bidir = None
162
163     def run_tcl(self, cmd):
164         """Run a TCL script using the TCL interpreter found in ``tkinter``.
165
166         :param cmd: Command to execute
167
168         :returns: Output of command, where applicable.
169         """
170         self._logger.debug('%s%s', trafficgen.CMD_PREFIX, cmd)
171
172         output = self._tclsh.eval(cmd)
173
174         return output.split()
175
176     def configure(self):
177         """Configure system for IxNetwork.
178         """
179         self._script = os.path.join(settings.getValue('TRAFFICGEN_IXIA_3RD_PARTY'),
180                                     settings.getValue('TRAFFICGEN_IXNET_TCL_SCRIPT'))
181         self._cfg = {
182             'lib_path': settings.getValue('TRAFFICGEN_IXNET_LIB_PATH'),
183             # IxNetwork machine configuration
184             'machine': settings.getValue('TRAFFICGEN_IXNET_MACHINE'),
185             'port': settings.getValue('TRAFFICGEN_IXNET_PORT'),
186             'user': settings.getValue('TRAFFICGEN_IXNET_USER'),
187             # IXIA chassis configuration
188             'chassis': settings.getValue('TRAFFICGEN_IXIA_HOST'),
189             'card': settings.getValue('TRAFFICGEN_IXIA_CARD'),
190             'port1': settings.getValue('TRAFFICGEN_IXIA_PORT1'),
191             'port2': settings.getValue('TRAFFICGEN_IXIA_PORT2'),
192             'output_dir':
193                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
194         }
195
196         self._logger.debug('IXIA configuration configuration : %s', self._cfg)
197
198     def connect(self):
199         """Connect to IxNetwork - nothing to be done here
200         """
201         pass
202
203     def disconnect(self):
204         """Disconnect from Ixia chassis.
205         """
206         pass
207
208     def send_cont_traffic(self, traffic=None, duration=30):
209         """See ITrafficGenerator for description
210         """
211         self.start_cont_traffic(traffic, duration)
212
213         return self.stop_cont_traffic()
214
215     def start_cont_traffic(self, traffic=None, duration=30):
216         """Start transmission.
217         """
218         self.configure()
219         self._bidir = traffic['bidir']
220         self._params = {}
221
222         self._params['config'] = {
223             'binary': False,  # don't do binary search and send one stream
224             'duration': duration,
225             'framerate': traffic['frame_rate'],
226             'multipleStreams': traffic['multistream'],
227             'streamType': traffic['stream_type'],
228             'rfc2544TestType': 'throughput',
229             'flowControl': "True" if traffic['flow_control'] else "False",
230             'learningFrames': "True" if traffic['learning_frames'] else "False",
231         }
232         self._params['traffic'] = self.traffic_defaults.copy()
233
234         if traffic:
235             self._params['traffic'] = merge_spec(
236                 self._params['traffic'], traffic)
237         self._cfg['bidir'] = self._bidir
238
239         for cmd in _build_set_cmds(self._cfg, prefix='set'):
240             self.run_tcl(cmd)
241
242         for cmd in _build_set_cmds(self._params):
243             self.run_tcl(cmd)
244
245         output = self.run_tcl('source {%s}' % self._script)
246         if output:
247             self._logger.critical(
248                 'An error occured when connecting to IxNetwork machine...')
249             raise RuntimeError('Ixia failed to initialise.')
250
251         self.run_tcl('startRfc2544Test $config $traffic')
252         if output:
253             self._logger.critical(
254                 'Failed to start continuous traffic test')
255             raise RuntimeError('Continuous traffic test failed to start.')
256
257     def stop_cont_traffic(self):
258         """See ITrafficGenerator for description
259         """
260         return self._wait_result()
261
262     def send_rfc2544_throughput(self, traffic=None, tests=1, duration=20,
263                                 lossrate=0.0):
264         """See ITrafficGenerator for description
265         """
266         self.start_rfc2544_throughput(traffic, tests, duration, lossrate)
267
268         return self.wait_rfc2544_throughput()
269
270     def start_rfc2544_throughput(self, traffic=None, tests=1, duration=20,
271                                  lossrate=0.0):
272         """Start transmission.
273         """
274         self.configure()
275         self._bidir = traffic['bidir']
276         self._params = {}
277
278         self._params['config'] = {
279             'binary': True,
280             'tests': tests,
281             'duration': duration,
282             'lossrate': lossrate,
283             'multipleStreams': traffic['multistream'],
284             'streamType': traffic['stream_type'],
285             'rfc2544TestType': 'throughput',
286             'flowControl': "True" if traffic['flow_control'] else "False",
287             'learningFrames': "True" if traffic['learning_frames'] else "False",
288         }
289         self._params['traffic'] = self.traffic_defaults.copy()
290
291         if traffic:
292             self._params['traffic'] = merge_spec(
293                 self._params['traffic'], traffic)
294         self._cfg['bidir'] = self._bidir
295
296         for cmd in _build_set_cmds(self._cfg, prefix='set'):
297             self.run_tcl(cmd)
298
299         for cmd in _build_set_cmds(self._params):
300             self.run_tcl(cmd)
301
302         output = self.run_tcl('source {%s}' % self._script)
303         if output:
304             self._logger.critical(
305                 'An error occured when connecting to IxNetwork machine...')
306             raise RuntimeError('Ixia failed to initialise.')
307
308         self.run_tcl('startRfc2544Test $config $traffic')
309         if output:
310             self._logger.critical(
311                 'Failed to start RFC2544 test')
312             raise RuntimeError('RFC2544 test failed to start.')
313
314     def wait_rfc2544_throughput(self):
315         """See ITrafficGenerator for description
316         """
317         return self._wait_result()
318
319     def _wait_result(self):
320         """Wait for results.
321         """
322         def parse_result_string(results):
323             """Get path to results file from output
324
325             Check for related errors
326
327             :param results: Text stream from test.
328
329             :returns: Path to results file.
330             """
331             result_status = re.search(_RESULT_RE, results)
332             result_path = re.search(_RESULTPATH_RE, results)
333
334             if not result_status or not result_path:
335                 self._logger.critical(
336                     'Could not parse results from IxNetwork machine...')
337                 raise ValueError('Failed to parse output.')
338
339             if result_status.group(1) != 'pass':
340                 self._logger.critical(
341                     'An error occured when running tests...')
342                 raise RuntimeError('Ixia failed to initialise.')
343
344             # transform path into someting useful
345
346             path = result_path.group(1).replace('\\', '/')
347             path = os.path.join(path, 'AggregateResults.csv')
348             path = path.replace(
349                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
350                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
351             return path
352
353         def parse_ixnet_rfc_results(path):
354             """Parse CSV output of IxNet RFC2544 test run.
355
356             :param path: Input file path
357             """
358             results = OrderedDict()
359
360             with open(path, 'r') as in_file:
361                 reader = csv.reader(in_file, delimiter=',')
362                 next(reader)
363                 for row in reader:
364                     #Replace null entries added by Ixia with 0s.
365                     row = [entry if len(entry) > 0 else '0' for entry in row]
366
367                     # tx_fps and tx_mps cannot be reliably calculated
368                     # as the DUT may be modifying the frame size
369                     tx_fps = 'Unknown'
370                     tx_mbps = 'Unknown'
371
372                     if bool(results.get(ResultsConstants.THROUGHPUT_RX_FPS)) \
373                                                                 is False:
374                         prev_percent_rx = 0.0
375                     else:
376                         prev_percent_rx = \
377                         float(results.get(ResultsConstants.THROUGHPUT_RX_FPS))
378                     if float(row[5]) >= prev_percent_rx:
379                         results[ResultsConstants.TX_RATE_FPS] = tx_fps
380                         results[ResultsConstants.THROUGHPUT_RX_FPS] = row[5]
381                         results[ResultsConstants.TX_RATE_MBPS] = tx_mbps
382                         results[ResultsConstants.THROUGHPUT_RX_MBPS] = row[6]
383                         results[ResultsConstants.TX_RATE_PERCENT] = row[3]
384                         results[ResultsConstants.THROUGHPUT_RX_PERCENT] = row[4]
385                         results[ResultsConstants.FRAME_LOSS_PERCENT] = row[10]
386                         results[ResultsConstants.MIN_LATENCY_NS] = row[11]
387                         results[ResultsConstants.MAX_LATENCY_NS] = row[12]
388                         results[ResultsConstants.AVG_LATENCY_NS] = row[13]
389             return results
390
391         output = self.run_tcl('waitForRfc2544Test')
392
393         # the run_tcl function will return a list with one element. We extract
394         # that one element (a string representation of an IXIA-specific Tcl
395         # datatype), parse it to find the path of the results file then parse
396         # the results file
397         return parse_ixnet_rfc_results(parse_result_string(output[0]))
398
399     def send_rfc2544_back2back(self, traffic=None, tests=1, duration=2,
400                                lossrate=0.0):
401         """See ITrafficGenerator for description
402         """
403         # NOTE 2 seconds is the recommended duration for a back 2 back
404         # test in RFC2544. 50 trials is the recommended number from the
405         # RFC also.
406         self.start_rfc2544_back2back(traffic, tests, duration, lossrate)
407
408         return self.wait_rfc2544_back2back()
409
410     def start_rfc2544_back2back(self, traffic=None, tests=1, duration=2,
411                                 lossrate=0.0):
412         """Start transmission.
413         """
414         self.configure()
415         self._bidir = traffic['bidir']
416         self._params = {}
417
418         self._params['config'] = {
419             'binary': True,
420             'tests': tests,
421             'duration': duration,
422             'lossrate': lossrate,
423             'multipleStreams': traffic['multistream'],
424             'streamType': traffic['stream_type'],
425             'rfc2544TestType': 'back2back',
426             'flowControl': "True" if traffic['flow_control'] else "False",
427             'learningFrames': "True" if traffic['learning_frames'] else "False",
428         }
429         self._params['traffic'] = self.traffic_defaults.copy()
430
431         if traffic:
432             self._params['traffic'] = merge_spec(
433                 self._params['traffic'], traffic)
434         self._cfg['bidir'] = self._bidir
435
436         for cmd in _build_set_cmds(self._cfg, prefix='set'):
437             self.run_tcl(cmd)
438
439         for cmd in _build_set_cmds(self._params):
440             self.run_tcl(cmd)
441
442         output = self.run_tcl('source {%s}' % self._script)
443         if output:
444             self._logger.critical(
445                 'An error occured when connecting to IxNetwork machine...')
446             raise RuntimeError('Ixia failed to initialise.')
447
448         self.run_tcl('startRfc2544Test $config $traffic')
449         if output:
450             self._logger.critical(
451                 'Failed to start RFC2544 test')
452             raise RuntimeError('RFC2544 test failed to start.')
453
454     def wait_rfc2544_back2back(self):
455         """Wait for results.
456         """
457         def parse_result_string(results):
458             """Get path to results file from output
459
460             Check for related errors
461
462             :param results: Text stream from test.
463
464             :returns: Path to results file.
465             """
466             result_status = re.search(_RESULT_RE, results)
467             result_path = re.search(_RESULTPATH_RE, results)
468
469             if not result_status or not result_path:
470                 self._logger.critical(
471                     'Could not parse results from IxNetwork machine...')
472                 raise ValueError('Failed to parse output.')
473
474             if result_status.group(1) != 'pass':
475                 self._logger.critical(
476                     'An error occured when running tests...')
477                 raise RuntimeError('Ixia failed to initialise.')
478
479             # transform path into something useful
480
481             path = result_path.group(1).replace('\\', '/')
482             path = os.path.join(path, 'iteration.csv')
483             path = path.replace(
484                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
485                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
486
487             return path
488
489         def parse_ixnet_rfc_results(path):
490             """Parse CSV output of IxNet RFC2544 Back2Back test run.
491
492             :param path: Input file path
493
494             :returns: Best parsed result from CSV file.
495             """
496             results = OrderedDict()
497             results[ResultsConstants.B2B_FRAMES] = 0
498             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = 100
499
500             with open(path, 'r') as in_file:
501                 reader = csv.reader(in_file, delimiter=',')
502                 next(reader)
503                 for row in reader:
504                     # if back2back count higher than previously found, store it
505                     # Note: row[N] here refers to the Nth column of a row
506                     if float(row[14]) <= self._params['config']['lossrate']:
507                         if int(row[12]) > \
508                          int(results[ResultsConstants.B2B_FRAMES]):
509                             results[ResultsConstants.B2B_FRAMES] = int(row[12])
510                             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = float(row[14])
511
512             return results
513
514         output = self.run_tcl('waitForRfc2544Test')
515
516         # the run_tcl function will return a list with one element. We extract
517         # that one element (a string representation of an IXIA-specific Tcl
518         # datatype), parse it to find the path of the results file then parse
519         # the results file
520
521         return parse_ixnet_rfc_results(parse_result_string(output[0]))
522
523     def send_burst_traffic(self, traffic=None, numpkts=100, duration=20):
524         return NotImplementedError('IxNet does not implement send_burst_traffic')
525
526 if __name__ == '__main__':
527     TRAFFIC = {
528         'l3': {
529             'proto': 'udp',
530             'srcip': '10.1.1.1',
531             'dstip': '10.1.1.254',
532         },
533     }
534
535     with IxNet() as dev:
536         print(dev.send_cont_traffic())
537         print(dev.send_rfc2544_throughput())