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