Add a simple performance test that sends a continuous stream
[vswitchperf.git] / tools / pkt_gen / ixnet / ixnet.py
1 # Copyright 2015 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     _script = os.path.join(os.path.dirname(__file__), 'ixnetrfc2544.tcl')
157     _tclsh = tkinter.Tcl()
158     _cfg = None
159     _logger = logging.getLogger(__name__)
160     _params = None
161     _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 connect(self):
177         """Configure system for IxNetwork.
178         """
179         self._cfg = {
180             'lib_path': settings.getValue('TRAFFICGEN_IXNET_LIB_PATH'),
181             # IxNetwork machine configuration
182             'machine': settings.getValue('TRAFFICGEN_IXNET_MACHINE'),
183             'port': settings.getValue('TRAFFICGEN_IXNET_PORT'),
184             'user': settings.getValue('TRAFFICGEN_IXNET_USER'),
185             # IXIA chassis configuration
186             'chassis': settings.getValue('TRAFFICGEN_IXIA_HOST'),
187             'card': settings.getValue('TRAFFICGEN_IXIA_CARD'),
188             'port1': settings.getValue('TRAFFICGEN_IXIA_PORT1'),
189             'port2': settings.getValue('TRAFFICGEN_IXIA_PORT2'),
190             'output_dir':
191                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
192         }
193
194         self._logger.debug('IXIA configuration configuration : %s', self._cfg)
195
196         return self
197
198     def disconnect(self):
199         """Disconnect from Ixia chassis.
200         """
201         pass
202
203     def send_cont_traffic(self, traffic=None, time=30, framerate=100,
204                           multistream=False):
205         """See ITrafficGenerator for description
206         """
207         self.start_cont_traffic(traffic, time, framerate, multistream)
208
209         return self.stop_cont_traffic()
210
211     def start_cont_traffic(self, traffic=None, time=30, framerate=100,
212                            multistream=False):
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             'time': time,
221             'framerate': framerate,
222             'multipleStreams': multistream,
223             'rfc2544TestType': 'throughput',
224         }
225         self._params['traffic'] = self.traffic_defaults.copy()
226
227         if traffic:
228             self._params['traffic'] = trafficgen.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, trials=3, duration=20,
256                                 lossrate=0.0, multistream=False):
257         """See ITrafficGenerator for description
258         """
259         self.start_rfc2544_throughput(traffic, trials, duration, lossrate,
260                                       multistream)
261
262         return self.wait_rfc2544_throughput()
263
264     def start_rfc2544_throughput(self, traffic=None, trials=3, duration=20,
265                                  lossrate=0.0, multistream=False):
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': multistream,
277             'rfc2544TestType': 'throughput',
278         }
279         self._params['traffic'] = self.traffic_defaults.copy()
280
281         if traffic:
282             self._params['traffic'] = trafficgen.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                     # calculate tx fps by (rx fps * (tx % / rx %))
357                     tx_fps = float(row[5]) * (float(row[4]) / float(row[3]))
358                     # calculate tx mbps by (rx mbps * (tx % / rx %))
359                     tx_mbps = float(row[6]) * (float(row[4]) / float(row[3]))
360
361                     if bool(results.get(ResultsConstants.THROUGHPUT_RX_FPS)) \
362                                                                 == False:
363                         prev_percent_rx = 0.0
364                     else:
365                         prev_percent_rx = \
366                         float(results.get(ResultsConstants.THROUGHPUT_RX_FPS))
367                     if float(row[5]) >= prev_percent_rx:
368                         results[ResultsConstants.THROUGHPUT_TX_FPS] = tx_fps
369                         results[ResultsConstants.THROUGHPUT_RX_FPS] = row[5]
370                         results[ResultsConstants.THROUGHPUT_TX_MBPS] = tx_mbps
371                         results[ResultsConstants.THROUGHPUT_RX_MBPS] = row[6]
372                         results[ResultsConstants.THROUGHPUT_TX_PERCENT] = row[3]
373                         results[ResultsConstants.THROUGHPUT_RX_PERCENT] = row[4]
374                         results[ResultsConstants.MIN_LATENCY_NS] = row[11]
375                         results[ResultsConstants.MAX_LATENCY_NS] = row[12]
376                         results[ResultsConstants.AVG_LATENCY_NS] = row[13]
377             return results
378
379         output = self.run_tcl('waitForRfc2544Test')
380
381         # the run_tcl function will return a list with one element. We extract
382         # that one element (a string representation of an IXIA-specific Tcl
383         # datatype), parse it to find the path of the results file then parse
384         # the results file
385         return parse_ixnet_rfc_results(parse_result_string(output[0]))
386
387     def send_rfc2544_back2back(self, traffic=None, trials=1, duration=20,
388                                lossrate=0.0, multistream=False):
389         """See ITrafficGenerator for description
390         """
391         self.start_rfc2544_back2back(traffic, trials, duration, lossrate,
392                                      multistream)
393
394         return self.wait_rfc2544_back2back()
395
396     def start_rfc2544_back2back(self, traffic=None, trials=1, duration=20,
397                                 lossrate=0.0, multistream=False):
398         """Start transmission.
399         """
400         self._bidir = traffic['bidir']
401         self._params = {}
402
403         self._params['config'] = {
404             'binary': True,
405             'trials': trials,
406             'duration': duration,
407             'lossrate': lossrate,
408             'multipleStreams': multistream,
409             'rfc2544TestType': 'back2back',
410         }
411         self._params['traffic'] = self.traffic_defaults.copy()
412
413         if traffic:
414             self._params['traffic'] = trafficgen.merge_spec(
415                 self._params['traffic'], traffic)
416         self._cfg['bidir'] = self._bidir
417
418         for cmd in _build_set_cmds(self._cfg, prefix='set'):
419             self.run_tcl(cmd)
420
421         for cmd in _build_set_cmds(self._params):
422             self.run_tcl(cmd)
423
424         output = self.run_tcl('source {%s}' % self._script)
425         if output:
426             self._logger.critical(
427                 'An error occured when connecting to IxNetwork machine...')
428             raise RuntimeError('Ixia failed to initialise.')
429
430         self.run_tcl('startRfc2544Test $config $traffic')
431         if output:
432             self._logger.critical(
433                 'Failed to start RFC2544 test')
434             raise RuntimeError('RFC2544 test failed to start.')
435
436     def wait_rfc2544_back2back(self):
437         """Wait for results.
438         """
439         def parse_result_string(results):
440             """Get path to results file from output
441
442             Check for related errors
443
444             :param results: Text stream from test.
445
446             :returns: Path to results file.
447             """
448             result_status = re.search(_RESULT_RE, results)
449             result_path = re.search(_RESULTPATH_RE, results)
450
451             if not result_status or not result_path:
452                 self._logger.critical(
453                     'Could not parse results from IxNetwork machine...')
454                 raise ValueError('Failed to parse output.')
455
456             if result_status.group(1) != 'pass':
457                 self._logger.critical(
458                     'An error occured when running tests...')
459                 raise RuntimeError('Ixia failed to initialise.')
460
461             # transform path into something useful
462
463             path = result_path.group(1).replace('\\', '/')
464             path = os.path.join(path, 'iteration.csv')
465             path = path.replace(
466                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
467                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
468
469             return path
470
471         def parse_ixnet_rfc_results(path):
472             """Parse CSV output of IxNet RFC2544 Back2Back test run.
473
474             :param path: Input file path
475
476             :returns: Best parsed result from CSV file.
477             """
478             results = OrderedDict()
479             results[ResultsConstants.B2B_FRAMES] = 0
480
481             with open(path, 'r') as in_file:
482                 reader = csv.reader(in_file, delimiter=',')
483                 next(reader)
484                 for row in reader:
485                     # if back2back count higher than previously found, store it
486                     # Note: row[N] here refers to the Nth column of a row
487                     if float(row[14]) <= self._params['config']['lossrate']:
488                         if int(row[12]) > \
489                          int(results[ResultsConstants.B2B_FRAMES]):
490                             results[ResultsConstants.B2B_FRAMES] = int(row[12])
491
492             return results
493
494         output = self.run_tcl('waitForRfc2544Test')
495
496         # the run_tcl function will return a list with one element. We extract
497         # that one element (a string representation of an IXIA-specific Tcl
498         # datatype), parse it to find the path of the results file then parse
499         # the results file
500
501         return parse_ixnet_rfc_results(parse_result_string(output[0]))
502
503
504 if __name__ == '__main__':
505     TRAFFIC = {
506         'l3': {
507             'proto': 'udp',
508             'srcip': '10.1.1.1',
509             'dstip': '10.1.1.254',
510         },
511     }
512
513     with IxNet() as dev:
514         print(dev.send_cont_traffic())
515         print(dev.send_rfc2544_throughput())