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