test_spec: LTD: MatchAction Performance testing
[vswitchperf.git] / tools / pkt_gen / ixnet / ixnet.py
1 # Copyright 2015-2016 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
82 import tkinter
83 import logging
84 import os
85 import re
86 import csv
87
88 from collections import OrderedDict
89 from tools.pkt_gen import trafficgen
90 from conf import settings
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         # Not allowing derived dictionary types for now
125         # pylint: disable=unidiomatic-typecheck
126         if type(value) == dict:
127             _prefix = ' '.join([prefix, key]).strip()
128             for subkey in _build_set_cmds(value, _prefix):
129                 yield subkey
130             continue
131
132         # pylint: disable=unidiomatic-typecheck
133         # tcl doesn't recognise the strings "True" or "False", only "1"
134         # or "0". Special case to convert them
135         if type(value) == bool:
136             value = str(int(value))
137         else:
138             value = str(value)
139
140         if prefix:
141             yield ' '.join([prefix, key, value]).strip()
142         else:
143             yield ' '.join([key, value]).strip()
144
145
146 class IxNet(trafficgen.ITrafficGenerator):
147     """A wrapper around IXIA IxNetwork applications.
148
149     Runs different traffic generator tests through an Ixia traffic
150     generator chassis by generating TCL scripts from templates.
151
152     Currently only the RFC2544 tests are implemented.
153     """
154     if settings.getValue('TRAFFICGEN_IXNET_TCL_SCRIPT') == '':
155         _script = os.path.join(os.path.dirname(__file__), 'ixnetrfc2544.tcl')
156     else:
157         _script = os.path.join(os.path.dirname(__file__),
158                                settings.getValue('TRAFFICGEN_IXNET_TCL_SCRIPT'))
159     _tclsh = tkinter.Tcl()
160     _cfg = None
161     _logger = logging.getLogger(__name__)
162     _params = None
163     _bidir = None
164
165     def run_tcl(self, cmd):
166         """Run a TCL script using the TCL interpreter found in ``tkinter``.
167
168         :param cmd: Command to execute
169
170         :returns: Output of command, where applicable.
171         """
172         self._logger.debug('%s%s', trafficgen.CMD_PREFIX, cmd)
173
174         output = self._tclsh.eval(cmd)
175
176         return output.split()
177
178     def connect(self):
179         """Configure system for IxNetwork.
180         """
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         return self
199
200     def disconnect(self):
201         """Disconnect from Ixia chassis.
202         """
203         pass
204
205     def send_cont_traffic(self, traffic=None, duration=30):
206         """See ITrafficGenerator for description
207         """
208         self.start_cont_traffic(traffic, duration)
209
210         return self.stop_cont_traffic()
211
212     def start_cont_traffic(self, traffic=None, duration=30):
213         """Start transmission.
214         """
215         self._bidir = traffic['bidir']
216         self._params = {}
217
218         self._params['config'] = {
219             'binary': False,  # don't do binary search and send one stream
220             'duration': duration,
221             'framerate': traffic['frame_rate'],
222             'multipleStreams': traffic['multistream'],
223             'streamType': traffic['stream_type'],
224             'rfc2544TestType': 'throughput',
225         }
226         self._params['traffic'] = self.traffic_defaults.copy()
227
228         if traffic:
229             self._params['traffic'] = trafficgen.merge_spec(
230                 self._params['traffic'], traffic)
231         self._cfg['bidir'] = self._bidir
232
233         for cmd in _build_set_cmds(self._cfg, prefix='set'):
234             self.run_tcl(cmd)
235
236         for cmd in _build_set_cmds(self._params):
237             self.run_tcl(cmd)
238
239         output = self.run_tcl('source {%s}' % self._script)
240         if output:
241             self._logger.critical(
242                 'An error occured when connecting to IxNetwork machine...')
243             raise RuntimeError('Ixia failed to initialise.')
244
245         self.run_tcl('startRfc2544Test $config $traffic')
246         if output:
247             self._logger.critical(
248                 'Failed to start continuous traffic test')
249             raise RuntimeError('Continuous traffic test failed to start.')
250
251     def stop_cont_traffic(self):
252         """See ITrafficGenerator for description
253         """
254         return self._wait_result()
255
256     def send_rfc2544_throughput(self, traffic=None, trials=3, duration=20,
257                                 lossrate=0.0):
258         """See ITrafficGenerator for description
259         """
260         self.start_rfc2544_throughput(traffic, trials, duration, lossrate)
261
262         return self.wait_rfc2544_throughput()
263
264     def start_rfc2544_throughput(self, traffic=None, trials=3, duration=20,
265                                  lossrate=0.0):
266         """Start transmission.
267         """
268         self._bidir = traffic['bidir']
269         self._params = {}
270
271         self._params['config'] = {
272             'binary': True,
273             'trials': trials,
274             'duration': duration,
275             'lossrate': lossrate,
276             'multipleStreams': traffic['multistream'],
277             'streamType': traffic['stream_type'],
278             'rfc2544TestType': 'throughput',
279         }
280         self._params['traffic'] = self.traffic_defaults.copy()
281
282         if traffic:
283             self._params['traffic'] = trafficgen.merge_spec(
284                 self._params['traffic'], traffic)
285         self._cfg['bidir'] = self._bidir
286
287         for cmd in _build_set_cmds(self._cfg, prefix='set'):
288             self.run_tcl(cmd)
289
290         for cmd in _build_set_cmds(self._params):
291             self.run_tcl(cmd)
292
293         output = self.run_tcl('source {%s}' % self._script)
294         if output:
295             self._logger.critical(
296                 'An error occured when connecting to IxNetwork machine...')
297             raise RuntimeError('Ixia failed to initialise.')
298
299         self.run_tcl('startRfc2544Test $config $traffic')
300         if output:
301             self._logger.critical(
302                 'Failed to start RFC2544 test')
303             raise RuntimeError('RFC2544 test failed to start.')
304
305     def wait_rfc2544_throughput(self):
306         """See ITrafficGenerator for description
307         """
308         return self._wait_result()
309
310     def _wait_result(self):
311         """Wait for results.
312         """
313         def parse_result_string(results):
314             """Get path to results file from output
315
316             Check for related errors
317
318             :param results: Text stream from test.
319
320             :returns: Path to results file.
321             """
322             result_status = re.search(_RESULT_RE, results)
323             result_path = re.search(_RESULTPATH_RE, results)
324
325             if not result_status or not result_path:
326                 self._logger.critical(
327                     'Could not parse results from IxNetwork machine...')
328                 raise ValueError('Failed to parse output.')
329
330             if result_status.group(1) != 'pass':
331                 self._logger.critical(
332                     'An error occured when running tests...')
333                 raise RuntimeError('Ixia failed to initialise.')
334
335             # transform path into someting useful
336
337             path = result_path.group(1).replace('\\', '/')
338             path = os.path.join(path, 'AggregateResults.csv')
339             path = path.replace(
340                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
341                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
342             return path
343
344         def parse_ixnet_rfc_results(path):
345             """Parse CSV output of IxNet RFC2544 test run.
346
347             :param path: Input file path
348             """
349             results = OrderedDict()
350
351             with open(path, 'r') as in_file:
352                 reader = csv.reader(in_file, delimiter=',')
353                 next(reader)
354                 for row in reader:
355                     #Replace null entries added by Ixia with 0s.
356                     row = [entry if len(entry) > 0 else '0' for entry in row]
357
358                     # tx_fps and tx_mps cannot be reliably calculated
359                     # as the DUT may be modifying the frame size
360                     tx_fps = 'Unknown'
361                     tx_mbps = 'Unknown'
362
363                     if bool(results.get(ResultsConstants.THROUGHPUT_RX_FPS)) \
364                                                                 is False:
365                         prev_percent_rx = 0.0
366                     else:
367                         prev_percent_rx = \
368                         float(results.get(ResultsConstants.THROUGHPUT_RX_FPS))
369                     if float(row[5]) >= prev_percent_rx:
370                         results[ResultsConstants.TX_RATE_FPS] = tx_fps
371                         results[ResultsConstants.THROUGHPUT_RX_FPS] = row[5]
372                         results[ResultsConstants.TX_RATE_MBPS] = tx_mbps
373                         results[ResultsConstants.THROUGHPUT_RX_MBPS] = row[6]
374                         results[ResultsConstants.TX_RATE_PERCENT] = row[3]
375                         results[ResultsConstants.THROUGHPUT_RX_PERCENT] = row[4]
376                         results[ResultsConstants.FRAME_LOSS_PERCENT] = row[10]
377                         results[ResultsConstants.MIN_LATENCY_NS] = row[11]
378                         results[ResultsConstants.MAX_LATENCY_NS] = row[12]
379                         results[ResultsConstants.AVG_LATENCY_NS] = row[13]
380             return results
381
382         output = self.run_tcl('waitForRfc2544Test')
383
384         # the run_tcl function will return a list with one element. We extract
385         # that one element (a string representation of an IXIA-specific Tcl
386         # datatype), parse it to find the path of the results file then parse
387         # the results file
388         return parse_ixnet_rfc_results(parse_result_string(output[0]))
389
390     def send_rfc2544_back2back(self, traffic=None, trials=50, duration=2,
391                                lossrate=0.0):
392         """See ITrafficGenerator for description
393         """
394         # NOTE 2 seconds is the recommended duration for a back 2 back
395         # test in RFC2544. 50 trials is the recommended number from the
396         # RFC also.
397         self.start_rfc2544_back2back(traffic, trials, duration, lossrate)
398
399         return self.wait_rfc2544_back2back()
400
401     def start_rfc2544_back2back(self, traffic=None, trials=50, duration=2,
402                                 lossrate=0.0):
403         """Start transmission.
404         """
405         self._bidir = traffic['bidir']
406         self._params = {}
407
408         self._params['config'] = {
409             'binary': True,
410             'trials': trials,
411             'duration': duration,
412             'lossrate': lossrate,
413             'multipleStreams': traffic['multistream'],
414             'streamType': traffic['stream_type'],
415             'rfc2544TestType': 'back2back',
416         }
417         self._params['traffic'] = self.traffic_defaults.copy()
418
419         if traffic:
420             self._params['traffic'] = trafficgen.merge_spec(
421                 self._params['traffic'], traffic)
422         self._cfg['bidir'] = self._bidir
423
424         for cmd in _build_set_cmds(self._cfg, prefix='set'):
425             self.run_tcl(cmd)
426
427         for cmd in _build_set_cmds(self._params):
428             self.run_tcl(cmd)
429
430         output = self.run_tcl('source {%s}' % self._script)
431         if output:
432             self._logger.critical(
433                 'An error occured when connecting to IxNetwork machine...')
434             raise RuntimeError('Ixia failed to initialise.')
435
436         self.run_tcl('startRfc2544Test $config $traffic')
437         if output:
438             self._logger.critical(
439                 'Failed to start RFC2544 test')
440             raise RuntimeError('RFC2544 test failed to start.')
441
442     def wait_rfc2544_back2back(self):
443         """Wait for results.
444         """
445         def parse_result_string(results):
446             """Get path to results file from output
447
448             Check for related errors
449
450             :param results: Text stream from test.
451
452             :returns: Path to results file.
453             """
454             result_status = re.search(_RESULT_RE, results)
455             result_path = re.search(_RESULTPATH_RE, results)
456
457             if not result_status or not result_path:
458                 self._logger.critical(
459                     'Could not parse results from IxNetwork machine...')
460                 raise ValueError('Failed to parse output.')
461
462             if result_status.group(1) != 'pass':
463                 self._logger.critical(
464                     'An error occured when running tests...')
465                 raise RuntimeError('Ixia failed to initialise.')
466
467             # transform path into something useful
468
469             path = result_path.group(1).replace('\\', '/')
470             path = os.path.join(path, 'iteration.csv')
471             path = path.replace(
472                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
473                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
474
475             return path
476
477         def parse_ixnet_rfc_results(path):
478             """Parse CSV output of IxNet RFC2544 Back2Back test run.
479
480             :param path: Input file path
481
482             :returns: Best parsed result from CSV file.
483             """
484             results = OrderedDict()
485             results[ResultsConstants.B2B_FRAMES] = 0
486             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = 100
487
488             with open(path, 'r') as in_file:
489                 reader = csv.reader(in_file, delimiter=',')
490                 next(reader)
491                 for row in reader:
492                     # if back2back count higher than previously found, store it
493                     # Note: row[N] here refers to the Nth column of a row
494                     if float(row[14]) <= self._params['config']['lossrate']:
495                         if int(row[12]) > \
496                          int(results[ResultsConstants.B2B_FRAMES]):
497                             results[ResultsConstants.B2B_FRAMES] = int(row[12])
498                             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = float(row[14])
499
500             return results
501
502         output = self.run_tcl('waitForRfc2544Test')
503
504         # the run_tcl function will return a list with one element. We extract
505         # that one element (a string representation of an IXIA-specific Tcl
506         # datatype), parse it to find the path of the results file then parse
507         # the results file
508
509         return parse_ixnet_rfc_results(parse_result_string(output[0]))
510
511
512 if __name__ == '__main__':
513     TRAFFIC = {
514         'l3': {
515             'proto': 'udp',
516             'srcip': '10.1.1.1',
517             'dstip': '10.1.1.254',
518         },
519     }
520
521     with IxNet() as dev:
522         print(dev.send_cont_traffic())
523         print(dev.send_rfc2544_throughput())