bugfix: Trafficgen Ixia does not work
[vswitchperf.git] / tools / pkt_gen / ixia / ixia.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 """IXIA traffic generator model.
15
16 Provides a model for the IXIA traffic generator. In addition, provides
17 a number of generic "helper" functions that are used to do the "heavy
18 lifting".
19
20 This requires the following settings in your config file:
21
22 * TRAFFICGEN_IXIA_ROOT_DIR
23     IXIA libraries path
24 * TRAFFICGEN_IXIA_HOST
25     IXIA chassis IP address
26 * TRAFFICGEN_IXIA_CARD
27     IXIA card
28 * TRAFFICGEN_IXIA_PORT1
29     IXIA Tx port
30 * TRAFFICGEN_IXIA_PORT2
31     IXIA Rx port
32
33 If any of these don't exist, the application will raise an exception
34 (EAFP).
35 """
36
37 import tkinter
38 import logging
39 import os
40
41 from collections import OrderedDict
42 from tools import systeminfo
43 from tools.pkt_gen import trafficgen
44 from conf import settings
45 from core.results.results_constants import ResultsConstants
46
47 _ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
48 _IXIA_ROOT_DIR = settings.getValue('TRAFFICGEN_IXIA_ROOT_DIR')
49
50
51 def configure_env():
52     """Configure envionment for TCL.
53
54     """
55     os.environ['IXIA_HOME'] = _IXIA_ROOT_DIR
56
57     # USER MAY NEED TO CHANGE THESE IF USING OWN TCL LIBRARY
58     os.environ['TCL_HOME'] = _IXIA_ROOT_DIR
59     os.environ['TCLver'] = '8.5'
60
61     # USER NORMALLY DOES NOT CHANGE ANY LINES BELOW
62     os.environ['IxiaLibPath'] = os.path.expandvars('$IXIA_HOME/lib')
63     os.environ['IxiaBinPath'] = os.path.expandvars('$IXIA_HOME/bin')
64
65     os.environ['TCLLibPath'] = os.path.expandvars('$TCL_HOME/lib')
66     os.environ['TCLBinPath'] = os.path.expandvars('$TCL_HOME/bin')
67
68     os.environ['TCL_LIBRARY'] = os.path.expandvars('$TCLLibPath/tcl$TCLver')
69     os.environ['TK_LIBRARY'] = os.path.expandvars('$TCLLibPath/tk$TCLver')
70
71     os.environ['PATH'] = os.path.expandvars('$IxiaBinPath:.:$TCLBinPath:$PATH')
72     os.environ['TCLLIBPATH'] = os.path.expandvars('$IxiaLibPath')
73     os.environ['LD_LIBRARY_PATH'] = os.path.expandvars(
74         '$IxiaLibPath:$TCLLibPath:$LD_LIBRARY_PATH')
75
76     os.environ['IXIA_RESULTS_DIR'] = '/tmp/Ixia/Results'
77     os.environ['IXIA_LOGS_DIR'] = '/tmp/Ixia/Logs'
78     os.environ['IXIA_TCL_DIR'] = os.path.expandvars('$IxiaLibPath')
79     os.environ['IXIA_SAMPLES'] = os.path.expandvars('$IxiaLibPath/ixTcl1.0')
80     os.environ['IXIA_VERSION'] = systeminfo.get_version('Ixia')['version']
81
82
83 def _build_set_cmds(values, prefix='dict set'):
84     """Generate a list of 'dict set' args for Tcl.
85
86     Parse a dictionary and recursively build the arguments for the
87     'dict set' Tcl command, given that this is of the format:
88
89         dict set [name...] [key] [value]
90
91     For example, for a non-nested dict (i.e. a non-dict element):
92
93         dict set mydict mykey myvalue
94
95     For a nested dict (i.e. a dict element):
96
97         dict set mydict mysubdict mykey myvalue
98
99     :param values: Dictionary to yield values for
100     :param prefix: Prefix to append to output string. Generally the
101         already generated part of the command.
102
103     :yields: Output strings to be passed to a `Tcl` instance.
104     """
105     for key in values:
106         value = values[key]
107
108         # Not allowing derived dictionary types for now
109         # pylint: disable=unidiomatic-typecheck
110         if type(value) == dict:
111             _prefix = ' '.join([prefix, key]).strip()
112             for subkey in _build_set_cmds(value, _prefix):
113                 yield subkey
114             continue
115
116         # tcl doesn't recognise the strings "True" or "False", only "1"
117         # or "0". Special case to convert them
118         if type(value) == bool:
119             value = str(int(value))
120         else:
121             value = str(value)
122
123         if prefix:
124             yield ' '.join([prefix, key, value]).strip()
125         else:
126             yield ' '.join([key, value]).strip()
127
128
129 class Ixia(trafficgen.ITrafficGenerator):
130     """A wrapper around the IXIA traffic generator.
131
132     Runs different traffic generator tests through an Ixia traffic
133     generator chassis by generating TCL scripts from templates.
134     """
135     _script = os.path.join(settings.getValue('TRAFFICGEN_IXIA_3RD_PARTY'),
136                            'pass_fail.tcl')
137     _tclsh = tkinter.Tcl()
138     _logger = logging.getLogger(__name__)
139
140     def run_tcl(self, cmd):
141         """Run a TCL script using the TCL interpreter found in ``tkinter``.
142
143         :param cmd: Command to execute
144
145         :returns: Output of command, where applicable.
146         """
147         self._logger.debug('%s%s', trafficgen.CMD_PREFIX, cmd)
148
149         output = self._tclsh.eval(cmd)
150
151         return output.split()
152
153     def connect(self):
154         """Connect to Ixia chassis.
155         """
156         ixia_cfg = {
157             'lib_path': os.path.join(_IXIA_ROOT_DIR, 'lib', 'ixTcl1.0'),
158             'host': settings.getValue('TRAFFICGEN_IXIA_HOST'),
159             'card': settings.getValue('TRAFFICGEN_IXIA_CARD'),
160             'port1': settings.getValue('TRAFFICGEN_IXIA_PORT1'),
161             'port2': settings.getValue('TRAFFICGEN_IXIA_PORT2'),
162         }
163
164         self._logger.info('Connecting to IXIA...')
165
166         self._logger.debug('IXIA configuration configuration : %s', ixia_cfg)
167
168         configure_env()
169
170         for cmd in _build_set_cmds(ixia_cfg, prefix='set'):
171             self.run_tcl(cmd)
172
173         output = self.run_tcl('source {%s}' % self._script)
174         if output:
175             self._logger.critical(
176                 'An error occured when connecting to IXIA...')
177             raise RuntimeError('Ixia failed to initialise.')
178
179         self._logger.info('Connected to IXIA...')
180
181         return self
182
183     def disconnect(self):
184         """Disconnect from Ixia chassis.
185         """
186         self._logger.info('Disconnecting from IXIA...')
187
188         self.run_tcl('cleanUp')
189
190         self._logger.info('Disconnected from IXIA...')
191
192     def _send_traffic(self, flow, traffic):
193         """Send regular traffic.
194
195         :param flow: Flow specification
196         :param traffic: Traffic specification
197
198         :returns: Results from IXIA
199         """
200         params = {}
201
202         params['flow'] = flow
203         params['traffic'] = self.traffic_defaults.copy()
204
205         if traffic:
206             params['traffic'] = trafficgen.merge_spec(
207                 params['traffic'], traffic)
208
209         for cmd in _build_set_cmds(params):
210             self.run_tcl(cmd)
211
212         result = self.run_tcl('sendTraffic $flow $traffic')
213
214         return result
215
216     def send_burst_traffic(self, traffic=None, numpkts=100, duration=20):
217         """See ITrafficGenerator for description
218         """
219         flow = {
220             'numpkts': numpkts,
221             'duration': duration,
222             'type': 'stopStream',
223             'framerate': traffic['frame_rate'],
224         }
225
226         result = self._send_traffic(flow, traffic)
227
228         assert len(result) == 6  # fail-fast if underlying Tcl code changes
229
230         #TODO - implement Burst results setting via TrafficgenResults.
231
232     def send_cont_traffic(self, traffic=None, duration=30):
233         """See ITrafficGenerator for description
234         """
235         flow = {
236             'numpkts': 100,
237             'duration': duration,
238             'type': 'contPacket',
239             'framerate': traffic['frame_rate'],
240             'multipleStreams': traffic['multistream'],
241         }
242
243         result = self._send_traffic(flow, traffic)
244
245         return Ixia._create_result(result)
246
247     def start_cont_traffic(self, traffic=None, duration=30):
248         """See ITrafficGenerator for description
249         """
250         return self.send_cont_traffic(traffic, 0)
251
252     def stop_cont_traffic(self):
253         """See ITrafficGenerator for description
254         """
255         return self.run_tcl('stopTraffic')
256
257     def send_rfc2544_throughput(self, traffic=None, tests=1, duration=20, lossrate=0.0):
258         """See ITrafficGenerator for description
259         """
260         params = {}
261
262         params['config'] = {
263             'tests': tests,
264             'duration': duration,
265             'lossrate': lossrate,
266             'multipleStreams': traffic['multistream'],
267         }
268         params['traffic'] = self.traffic_defaults.copy()
269
270         if traffic:
271             params['traffic'] = trafficgen.merge_spec(
272                 params['traffic'], traffic)
273
274         for cmd in _build_set_cmds(params):
275             self.run_tcl(cmd)
276
277         # this will return a list with one result
278         result = self.run_tcl('rfcThroughputTest $config $traffic')
279
280         return Ixia._create_result(result)
281
282     @staticmethod
283     def _create_result(result):
284         """Create result based on list returned from tcl script.
285
286         :param result: list representing output from tcl script.
287
288         :returns: dictionary strings representing results from
289                     traffic generator.
290         """
291         assert len(result) == 8  # fail-fast if underlying Tcl code changes
292
293         if float(result[0]) == 0:
294             loss_rate = 100
295         else:
296             loss_rate = (float(result[0]) - float(result[1])) / float(result[0]) * 100
297         result_dict = OrderedDict()
298         # drop the first 4 elements as we don't use/need them. In
299         # addition, IxExplorer does not support latency or % line rate
300         # metrics so we have to return dummy values for these metrics
301         result_dict[ResultsConstants.THROUGHPUT_RX_FPS] = result[4]
302         result_dict[ResultsConstants.TX_RATE_FPS] = result[5]
303         result_dict[ResultsConstants.THROUGHPUT_RX_MBPS] = str(round(int(result[6]) / 1000000, 3))
304         result_dict[ResultsConstants.TX_RATE_MBPS] = str(round(int(result[7]) / 1000000, 3))
305         result_dict[ResultsConstants.FRAME_LOSS_PERCENT] = loss_rate
306         result_dict[ResultsConstants.TX_RATE_PERCENT] = \
307                                             ResultsConstants.UNKNOWN_VALUE
308         result_dict[ResultsConstants.THROUGHPUT_RX_PERCENT] = \
309                                             ResultsConstants.UNKNOWN_VALUE
310         result_dict[ResultsConstants.MIN_LATENCY_NS] = \
311                                             ResultsConstants.UNKNOWN_VALUE
312         result_dict[ResultsConstants.MAX_LATENCY_NS] = \
313                                             ResultsConstants.UNKNOWN_VALUE
314         result_dict[ResultsConstants.AVG_LATENCY_NS] = \
315                                             ResultsConstants.UNKNOWN_VALUE
316
317         return result_dict
318
319 if __name__ == '__main__':
320     TRAFFIC = {
321         'l3': {
322             'proto': 'udp',
323             'srcip': '10.1.1.1',
324             'dstip': '10.1.1.254',
325         },
326     }
327
328     with Ixia() as dev:
329         print(dev.send_burst_traffic(traffic=TRAFFIC))
330         print(dev.send_cont_traffic(traffic=TRAFFIC))
331         print(dev.send_rfc2544_throughput(traffic=TRAFFIC))