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