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