pkt_gen: Add IxNet support for GRE frames
[vswitchperf.git] / testcases / testcase.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 """TestCase base class
15 """
16
17 import csv
18 import os
19 import logging
20 import subprocess
21 import copy
22 import time
23 from collections import OrderedDict
24
25 from core.results.results_constants import ResultsConstants
26 import core.component_factory as component_factory
27 from core.loader import Loader
28 from tools import tasks
29 from tools import hugepages
30 from tools.report import report
31 from tools.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
32 from conf import settings as S
33 from conf import get_test_param
34
35 class TestCase(object):
36     """TestCase base class
37
38     In this basic form runs RFC2544 throughput test
39     """
40     def __init__(self, cfg, results_dir, performance_test=True):
41         """Pull out fields from test config
42
43         :param cfg: A dictionary of string-value pairs describing the test
44             configuration. Both the key and values strings use well-known
45             values.
46         :param results_dir: Where the csv formatted results are written.
47         """
48         self._hugepages_mounted = False
49
50         # set test parameters; CLI options take precedence to testcase settings
51         self._logger = logging.getLogger(__name__)
52         self.name = cfg['Name']
53         self.desc = cfg.get('Description', 'No description given.')
54
55         bidirectional = cfg.get('biDirectional', False)
56         bidirectional = get_test_param('bidirectional', bidirectional)
57
58         traffic_type = cfg.get('Traffic Type', 'rfc2544')
59         traffic_type = get_test_param('traffic_type', traffic_type)
60
61         framerate = cfg.get('iLoad', 100)
62         framerate = get_test_param('iload', framerate)
63
64         self.deployment = cfg['Deployment']
65         self._frame_mod = cfg.get('Frame Modification', None)
66         self._performance_test = performance_test
67
68         self._tunnel_type = None
69         self._tunnel_operation = None
70
71         if self.deployment == 'op2p':
72             self._tunnel_operation = cfg['Tunnel Operation']
73
74             if 'Tunnel Type' in cfg:
75                 self._tunnel_type = cfg['Tunnel Type']
76                 self._tunnel_type = get_test_param('tunnel_type',
77                                                    self._tunnel_type)
78
79
80         # identify guest loopback method, so it can be added into reports
81         self.guest_loopback = []
82         if self.deployment in ['pvp', 'pvvp']:
83             guest_loopback = get_test_param('guest_loopback', None)
84             if guest_loopback:
85                 self.guest_loopback.append(guest_loopback)
86             else:
87                 if self.deployment == 'pvp':
88                     self.guest_loopback.append(S.getValue('GUEST_LOOPBACK')[0])
89                 else:
90                     self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
91
92         # read configuration of streams; CLI parameter takes precedence to
93         # testcase definition
94         multistream = cfg.get('MultiStream', 0)
95         multistream = get_test_param('multistream', multistream)
96         stream_type = cfg.get('Stream Type', 'L4')
97         stream_type = get_test_param('stream_type', stream_type)
98         pre_installed_flows = cfg.get('Pre-installed Flows', 'No')
99         pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
100
101         # check if test requires background load and which generator it uses
102         self._load_cfg = cfg.get('Load', None)
103         if self._load_cfg and 'tool' in self._load_cfg:
104             self._loadgen = self._load_cfg['tool']
105         else:
106             # background load is not requested, so use dummy implementation
107             self._loadgen = "Dummy"
108
109         if self._frame_mod:
110             self._frame_mod = self._frame_mod.lower()
111         self._results_dir = results_dir
112
113         # set traffic details, so they can be passed to vswitch and traffic ctls
114         self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
115         self._traffic.update({'traffic_type': traffic_type,
116                               'flow_type': cfg.get('Flow Type', 'port'),
117                               'bidir': bidirectional,
118                               'tunnel_type': self._tunnel_type,
119                               'multistream': int(multistream),
120                               'stream_type': stream_type,
121                               'pre_installed_flows' : pre_installed_flows,
122                               'frame_rate': int(framerate)})
123
124         # OVS Vanilla requires guest VM MAC address and IPs to work
125         if 'linux_bridge' in self.guest_loopback:
126             self._traffic['l2'].update({'srcmac': S.getValue('GUEST_NET2_MAC')[0],
127                                         'dstmac': S.getValue('GUEST_NET1_MAC')[0]})
128             self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
129                                         'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
130
131         # Packet Forwarding mode
132         self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
133
134     def run(self):
135         """Run the test
136
137         All setup and teardown through controllers is included.
138         """
139         self._logger.debug(self.name)
140
141         # mount hugepages if needed
142         self._mount_hugepages()
143
144         # copy sources of l2 forwarding tools into VM shared dir if needed
145         self._copy_fwd_tools_for_guest()
146
147         if self.deployment == "op2p":
148             self._traffic['l2'].update({'srcmac':
149                                         S.getValue('TRAFFICGEN_PORT1_MAC'),
150                                         'dstmac':
151                                         S.getValue('TRAFFICGEN_PORT2_MAC')})
152
153             self._traffic['l3'].update({'srcip':
154                                         S.getValue('TRAFFICGEN_PORT1_IP'),
155                                         'dstip':
156                                         S.getValue('TRAFFICGEN_PORT2_IP')})
157
158             if self._tunnel_operation == "decapsulation":
159                 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
160                 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
161                 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
162
163
164         self._logger.debug("Controllers:")
165         loader = Loader()
166         traffic_ctl = component_factory.create_traffic(
167             self._traffic['traffic_type'],
168             loader.get_trafficgen_class())
169         vnf_ctl = component_factory.create_vnf(
170             self.deployment,
171             loader.get_vnf_class())
172
173         if self._vswitch_none:
174             vswitch_ctl = component_factory.create_pktfwd(
175                 loader.get_pktfwd_class())
176         else:
177             vswitch_ctl = component_factory.create_vswitch(
178                 self.deployment,
179                 loader.get_vswitch_class(),
180                 self._traffic,
181                 self._tunnel_operation)
182
183         collector = component_factory.create_collector(
184             loader.get_collector_class(),
185             self._results_dir, self.name)
186         loadgen = component_factory.create_loadgen(
187             self._loadgen,
188             self._load_cfg)
189
190         self._logger.debug("Setup:")
191         with vswitch_ctl, loadgen:
192             with vnf_ctl, collector:
193                 if not self._vswitch_none:
194                     self._add_flows(vswitch_ctl)
195
196                 # run traffic generator if requested, otherwise wait for manual termination
197                 if S.getValue('mode') == 'trafficgen-off':
198                     time.sleep(2)
199                     self._logger.debug("All is set. Please run traffic generator manually.")
200                     input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
201                 else:
202                     with traffic_ctl:
203                         traffic_ctl.send_traffic(self._traffic)
204
205                     # dump vswitch flows before they are affected by VNF termination
206                     if not self._vswitch_none:
207                         vswitch_ctl.dump_vswitch_flows()
208
209         # umount hugepages if mounted
210         self._umount_hugepages()
211
212         self._logger.debug("Collector Results:")
213         collector.print_results()
214
215         if S.getValue('mode') != 'trafficgen-off':
216             self._logger.debug("Traffic Results:")
217             traffic_ctl.print_results()
218
219             output_file = os.path.join(self._results_dir, "result_" + self.name +
220                                        "_" + self.deployment + ".csv")
221
222             tc_results = self._append_results(traffic_ctl.get_results())
223             TestCase._write_result_to_file(tc_results, output_file)
224
225             report.generate(output_file, tc_results, collector.get_results(), self._performance_test)
226
227     def _append_results(self, results):
228         """
229         Method appends mandatory Test Case results to list of dictionaries.
230
231         :param results: list of dictionaries which contains results from
232                 traffic generator.
233
234         :returns: modified list of dictionaries.
235         """
236         for item in results:
237             item[ResultsConstants.ID] = self.name
238             item[ResultsConstants.DEPLOYMENT] = self.deployment
239             item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
240             if self._traffic['multistream']:
241                 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
242                 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
243                 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
244             if len(self.guest_loopback):
245                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
246
247         return results
248
249     def _copy_fwd_tools_for_guest(self):
250         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
251         """
252         counter = 0
253         # method is executed only for pvp and pvvp, so let's count number of 'v'
254         while counter < self.deployment.count('v'):
255             guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
256
257             # create shared dir if it doesn't exist
258             if not os.path.exists(guest_dir):
259                 os.makedirs(guest_dir)
260
261             # copy sources into shared dir only if neccessary
262             if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
263                 try:
264                     tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
265                                     os.path.join(S.getValue('RTE_SDK'), ''),
266                                     os.path.join(guest_dir, 'DPDK')],
267                                    self._logger,
268                                    'Copying DPDK to shared directory...',
269                                    True)
270                     tasks.run_task(['rsync', '-a', '-r', '-l',
271                                     os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
272                                     os.path.join(guest_dir, 'l2fwd')],
273                                    self._logger,
274                                    'Copying l2fwd to shared directory...',
275                                    True)
276                 except subprocess.CalledProcessError:
277                     self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
278
279             counter += 1
280
281     def _mount_hugepages(self):
282         """Mount hugepages if usage of DPDK or Qemu is detected
283         """
284         # hugepages are needed by DPDK and Qemu
285         if not self._hugepages_mounted and \
286             (self.deployment.count('v') or \
287              S.getValue('VSWITCH').lower().count('dpdk') or \
288              self._vswitch_none):
289             hugepages.mount_hugepages()
290             self._hugepages_mounted = True
291
292     def _umount_hugepages(self):
293         """Umount hugepages if they were mounted before
294         """
295         if self._hugepages_mounted:
296             hugepages.umount_hugepages()
297             self._hugepages_mounted = False
298
299     @staticmethod
300     def _write_result_to_file(results, output):
301         """Write list of dictionaries to a CSV file.
302
303         Each element on list will create separate row in output file.
304         If output file already exists, data will be appended at the end,
305         otherwise it will be created.
306
307         :param results: list of dictionaries.
308         :param output: path to output file.
309         """
310         with open(output, 'a') as csvfile:
311
312             logging.info("Write results to file: " + output)
313             fieldnames = TestCase._get_unique_keys(results)
314
315             writer = csv.DictWriter(csvfile, fieldnames)
316
317             if not csvfile.tell():  # file is now empty
318                 writer.writeheader()
319
320             for result in results:
321                 writer.writerow(result)
322
323
324     @staticmethod
325     def _get_unique_keys(list_of_dicts):
326         """Gets unique key values as ordered list of strings in given dicts
327
328         :param list_of_dicts: list of dictionaries.
329
330         :returns: list of unique keys(strings).
331         """
332         result = OrderedDict()
333         for item in list_of_dicts:
334             for key in item.keys():
335                 result[key] = ''
336
337         return list(result.keys())
338
339
340     def _add_flows(self, vswitch_ctl):
341         """Add flows to the vswitch
342
343         :param vswitch_ctl vswitch controller
344         """
345         vswitch = vswitch_ctl.get_vswitch()
346         # TODO BOM 15-08-07 the frame mod code assumes that the
347         # physical ports are ports 1 & 2. The actual numbers
348         # need to be retrived from the vSwitch and the metadata value
349         # updated accordingly.
350         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
351         if self._frame_mod == "vlan":
352             # 0x8100 => VLAN ethertype
353             self._logger.debug(" ****   VLAN   ***** ")
354             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
355                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
356             vswitch.add_flow(bridge, flow)
357             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
358                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
359             vswitch.add_flow(bridge, flow)
360         elif self._frame_mod == "mpls":
361             # 0x8847 => MPLS unicast ethertype
362             self._logger.debug(" ****   MPLS  ***** ")
363             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
364                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
365             vswitch.add_flow(bridge, flow)
366             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
367                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
368             vswitch.add_flow(bridge, flow)
369         elif self._frame_mod == "mac":
370             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
371                     'actions': ['mod_dl_src:22:22:22:22:22:22',
372                                 'goto_table:3']}
373             vswitch.add_flow(bridge, flow)
374             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
375                     'actions': ['mod_dl_src:11:11:11:11:11:11',
376                                 'goto_table:3']}
377             vswitch.add_flow(bridge, flow)
378         elif self._frame_mod == "dscp":
379             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
380             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
381                     'dl_type':'0x0800',
382                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
383             vswitch.add_flow(bridge, flow)
384             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
385                     'dl_type':'0x0800',
386                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
387             vswitch.add_flow(bridge, flow)
388         elif self._frame_mod == "ttl":
389             # 251 and 241 are the highest prime numbers < 255
390             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
391                     'dl_type':'0x0800',
392                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
393             vswitch.add_flow(bridge, flow)
394             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
395                     'dl_type':'0x0800',
396                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
397             vswitch.add_flow(bridge, flow)
398         elif self._frame_mod == "ip_addr":
399             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
400                     'dl_type':'0x0800',
401                     'actions': ['mod_nw_src:10.10.10.10',
402                                 'mod_nw_dst:20.20.20.20',
403                                 'goto_table:3']}
404             vswitch.add_flow(bridge, flow)
405             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
406                     'dl_type':'0x0800',
407                     'actions': ['mod_nw_src:20.20.20.20',
408                                 'mod_nw_dst:10.10.10.10',
409                                 'goto_table:3']}
410             vswitch.add_flow(bridge, flow)
411         elif self._frame_mod == "ip_port":
412             # TODO BOM 15-08-27 The traffic generated is assumed
413             # to be UDP (nw_proto 17d) which is the default case but
414             # we will need to pick up the actual traffic params in use.
415             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
416                     'dl_type':'0x0800', 'nw_proto':'17',
417                     'actions': ['mod_tp_src:44444',
418                                 'mod_tp_dst:44444', 'goto_table:3']}
419             vswitch.add_flow(bridge, flow)
420             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
421                     'dl_type':'0x0800', 'nw_proto':'17',
422                     'actions': ['mod_tp_src:44444',
423                                 'mod_tp_dst:44444', 'goto_table:3']}
424             vswitch.add_flow(bridge, flow)
425         else:
426             pass