ixia: Support of 1 NIC connection to trafficgen
[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 configure(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     def connect(self):
198         """Connect to IxNetwork - nothing to be done here
199         """
200         pass
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.configure()
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.configure()
272         self._bidir = traffic['bidir']
273         self._params = {}
274
275         self._params['config'] = {
276             'binary': True,
277             'tests': tests,
278             'duration': duration,
279             'lossrate': lossrate,
280             'multipleStreams': traffic['multistream'],
281             'streamType': traffic['stream_type'],
282             'rfc2544TestType': 'throughput',
283         }
284         self._params['traffic'] = self.traffic_defaults.copy()
285
286         if traffic:
287             self._params['traffic'] = merge_spec(
288                 self._params['traffic'], traffic)
289         self._cfg['bidir'] = self._bidir
290
291         for cmd in _build_set_cmds(self._cfg, prefix='set'):
292             self.run_tcl(cmd)
293
294         for cmd in _build_set_cmds(self._params):
295             self.run_tcl(cmd)
296
297         output = self.run_tcl('source {%s}' % self._script)
298         if output:
299             self._logger.critical(
300                 'An error occured when connecting to IxNetwork machine...')
301             raise RuntimeError('Ixia failed to initialise.')
302
303         self.run_tcl('startRfc2544Test $config $traffic')
304         if output:
305             self._logger.critical(
306                 'Failed to start RFC2544 test')
307             raise RuntimeError('RFC2544 test failed to start.')
308
309     def wait_rfc2544_throughput(self):
310         """See ITrafficGenerator for description
311         """
312         return self._wait_result()
313
314     def _wait_result(self):
315         """Wait for results.
316         """
317         def parse_result_string(results):
318             """Get path to results file from output
319
320             Check for related errors
321
322             :param results: Text stream from test.
323
324             :returns: Path to results file.
325             """
326             result_status = re.search(_RESULT_RE, results)
327             result_path = re.search(_RESULTPATH_RE, results)
328
329             if not result_status or not result_path:
330                 self._logger.critical(
331                     'Could not parse results from IxNetwork machine...')
332                 raise ValueError('Failed to parse output.')
333
334             if result_status.group(1) != 'pass':
335                 self._logger.critical(
336                     'An error occured when running tests...')
337                 raise RuntimeError('Ixia failed to initialise.')
338
339             # transform path into someting useful
340
341             path = result_path.group(1).replace('\\', '/')
342             path = os.path.join(path, 'AggregateResults.csv')
343             path = path.replace(
344                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
345                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
346             return path
347
348         def parse_ixnet_rfc_results(path):
349             """Parse CSV output of IxNet RFC2544 test run.
350
351             :param path: Input file path
352             """
353             results = OrderedDict()
354
355             with open(path, 'r') as in_file:
356                 reader = csv.reader(in_file, delimiter=',')
357                 next(reader)
358                 for row in reader:
359                     #Replace null entries added by Ixia with 0s.
360                     row = [entry if len(entry) > 0 else '0' for entry in row]
361
362                     # tx_fps and tx_mps cannot be reliably calculated
363                     # as the DUT may be modifying the frame size
364                     tx_fps = 'Unknown'
365                     tx_mbps = 'Unknown'
366
367                     if bool(results.get(ResultsConstants.THROUGHPUT_RX_FPS)) \
368                                                                 is False:
369                         prev_percent_rx = 0.0
370                     else:
371                         prev_percent_rx = \
372                         float(results.get(ResultsConstants.THROUGHPUT_RX_FPS))
373                     if float(row[5]) >= prev_percent_rx:
374                         results[ResultsConstants.TX_RATE_FPS] = tx_fps
375                         results[ResultsConstants.THROUGHPUT_RX_FPS] = row[5]
376                         results[ResultsConstants.TX_RATE_MBPS] = tx_mbps
377                         results[ResultsConstants.THROUGHPUT_RX_MBPS] = row[6]
378                         results[ResultsConstants.TX_RATE_PERCENT] = row[3]
379                         results[ResultsConstants.THROUGHPUT_RX_PERCENT] = row[4]
380                         results[ResultsConstants.FRAME_LOSS_PERCENT] = row[10]
381                         results[ResultsConstants.MIN_LATENCY_NS] = row[11]
382                         results[ResultsConstants.MAX_LATENCY_NS] = row[12]
383                         results[ResultsConstants.AVG_LATENCY_NS] = row[13]
384             return results
385
386         output = self.run_tcl('waitForRfc2544Test')
387
388         # the run_tcl function will return a list with one element. We extract
389         # that one element (a string representation of an IXIA-specific Tcl
390         # datatype), parse it to find the path of the results file then parse
391         # the results file
392         return parse_ixnet_rfc_results(parse_result_string(output[0]))
393
394     def send_rfc2544_back2back(self, traffic=None, tests=1, duration=2,
395                                lossrate=0.0):
396         """See ITrafficGenerator for description
397         """
398         # NOTE 2 seconds is the recommended duration for a back 2 back
399         # test in RFC2544. 50 trials is the recommended number from the
400         # RFC also.
401         self.start_rfc2544_back2back(traffic, tests, duration, lossrate)
402
403         return self.wait_rfc2544_back2back()
404
405     def start_rfc2544_back2back(self, traffic=None, tests=1, duration=2,
406                                 lossrate=0.0):
407         """Start transmission.
408         """
409         self.configure()
410         self._bidir = traffic['bidir']
411         self._params = {}
412
413         self._params['config'] = {
414             'binary': True,
415             'tests': tests,
416             'duration': duration,
417             'lossrate': lossrate,
418             'multipleStreams': traffic['multistream'],
419             'streamType': traffic['stream_type'],
420             'rfc2544TestType': 'back2back',
421         }
422         self._params['traffic'] = self.traffic_defaults.copy()
423
424         if traffic:
425             self._params['traffic'] = merge_spec(
426                 self._params['traffic'], traffic)
427         self._cfg['bidir'] = self._bidir
428
429         for cmd in _build_set_cmds(self._cfg, prefix='set'):
430             self.run_tcl(cmd)
431
432         for cmd in _build_set_cmds(self._params):
433             self.run_tcl(cmd)
434
435         output = self.run_tcl('source {%s}' % self._script)
436         if output:
437             self._logger.critical(
438                 'An error occured when connecting to IxNetwork machine...')
439             raise RuntimeError('Ixia failed to initialise.')
440
441         self.run_tcl('startRfc2544Test $config $traffic')
442         if output:
443             self._logger.critical(
444                 'Failed to start RFC2544 test')
445             raise RuntimeError('RFC2544 test failed to start.')
446
447     def wait_rfc2544_back2back(self):
448         """Wait for results.
449         """
450         def parse_result_string(results):
451             """Get path to results file from output
452
453             Check for related errors
454
455             :param results: Text stream from test.
456
457             :returns: Path to results file.
458             """
459             result_status = re.search(_RESULT_RE, results)
460             result_path = re.search(_RESULTPATH_RE, results)
461
462             if not result_status or not result_path:
463                 self._logger.critical(
464                     'Could not parse results from IxNetwork machine...')
465                 raise ValueError('Failed to parse output.')
466
467             if result_status.group(1) != 'pass':
468                 self._logger.critical(
469                     'An error occured when running tests...')
470                 raise RuntimeError('Ixia failed to initialise.')
471
472             # transform path into something useful
473
474             path = result_path.group(1).replace('\\', '/')
475             path = os.path.join(path, 'iteration.csv')
476             path = path.replace(
477                 settings.getValue('TRAFFICGEN_IXNET_TESTER_RESULT_DIR'),
478                 settings.getValue('TRAFFICGEN_IXNET_DUT_RESULT_DIR'))
479
480             return path
481
482         def parse_ixnet_rfc_results(path):
483             """Parse CSV output of IxNet RFC2544 Back2Back test run.
484
485             :param path: Input file path
486
487             :returns: Best parsed result from CSV file.
488             """
489             results = OrderedDict()
490             results[ResultsConstants.B2B_FRAMES] = 0
491             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = 100
492
493             with open(path, 'r') as in_file:
494                 reader = csv.reader(in_file, delimiter=',')
495                 next(reader)
496                 for row in reader:
497                     # if back2back count higher than previously found, store it
498                     # Note: row[N] here refers to the Nth column of a row
499                     if float(row[14]) <= self._params['config']['lossrate']:
500                         if int(row[12]) > \
501                          int(results[ResultsConstants.B2B_FRAMES]):
502                             results[ResultsConstants.B2B_FRAMES] = int(row[12])
503                             results[ResultsConstants.B2B_FRAME_LOSS_PERCENT] = float(row[14])
504
505             return results
506
507         output = self.run_tcl('waitForRfc2544Test')
508
509         # the run_tcl function will return a list with one element. We extract
510         # that one element (a string representation of an IXIA-specific Tcl
511         # datatype), parse it to find the path of the results file then parse
512         # the results file
513
514         return parse_ixnet_rfc_results(parse_result_string(output[0]))
515
516     def send_burst_traffic(self, traffic=None, numpkts=100, duration=20):
517         return NotImplementedError('IxNet does not implement send_burst_traffic')
518
519 if __name__ == '__main__':
520     TRAFFIC = {
521         'l3': {
522             'proto': 'udp',
523             'srcip': '10.1.1.1',
524             'dstip': '10.1.1.254',
525         },
526     }
527
528     with IxNet() as dev:
529         print(dev.send_cont_traffic())
530         print(dev.send_rfc2544_throughput())