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