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