8d23014cd0f8c4ebc13c38d97e2f39f327e6fdbd
[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', TRAFFIC_DEFAULTS['bidir'])
56         bidirectional = get_test_param('bidirectional', bidirectional)
57
58         traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
59         traffic_type = get_test_param('traffic_type', traffic_type)
60
61         framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
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', TRAFFIC_DEFAULTS['multistream'])
95         multistream = get_test_param('multistream', multistream)
96         stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
97         stream_type = get_test_param('stream_type', stream_type)
98         pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
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', TRAFFIC_DEFAULTS['flow_type']),
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                     if S.getValue('mode') == 'trafficgen-pause':
203                         time.sleep(2)
204                         true_vals = ('yes', 'y', 'ye', None)
205                         while True:
206                             choice = input(os.linesep + 'Transmission paused, should'
207                                            ' transmission be resumed? ' + os.linesep).lower()
208                             if not choice or choice not in true_vals:
209                                 print('Please respond with \'yes\' or \'y\' ', end='')
210                             else:
211                                 break
212                     with traffic_ctl:
213                         traffic_ctl.send_traffic(self._traffic)
214
215                     # dump vswitch flows before they are affected by VNF termination
216                     if not self._vswitch_none:
217                         vswitch_ctl.dump_vswitch_flows()
218
219         # umount hugepages if mounted
220         self._umount_hugepages()
221
222         self._logger.debug("Collector Results:")
223         collector.print_results()
224
225         if S.getValue('mode') != 'trafficgen-off':
226             self._logger.debug("Traffic Results:")
227             traffic_ctl.print_results()
228
229             output_file = os.path.join(self._results_dir, "result_" + self.name +
230                                        "_" + self.deployment + ".csv")
231
232             tc_results = self._append_results(traffic_ctl.get_results())
233             TestCase._write_result_to_file(tc_results, output_file)
234
235             report.generate(output_file, tc_results, collector.get_results(), self._performance_test)
236
237     def _append_results(self, results):
238         """
239         Method appends mandatory Test Case results to list of dictionaries.
240
241         :param results: list of dictionaries which contains results from
242                 traffic generator.
243
244         :returns: modified list of dictionaries.
245         """
246         for item in results:
247             item[ResultsConstants.ID] = self.name
248             item[ResultsConstants.DEPLOYMENT] = self.deployment
249             item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
250             if self._traffic['multistream']:
251                 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
252                 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
253                 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
254             if len(self.guest_loopback):
255                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
256
257         return results
258
259     def _copy_fwd_tools_for_guest(self):
260         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
261         """
262         counter = 0
263         # method is executed only for pvp and pvvp, so let's count number of 'v'
264         while counter < self.deployment.count('v'):
265             guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
266
267             # remove shared dir if it exists to avoid issues with file consistency
268             if os.path.exists(guest_dir):
269                 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
270                                'Removing content of shared directory...', True)
271
272             # directory to share files between host and guest
273             os.makedirs(guest_dir)
274
275             # copy sources into shared dir only if neccessary
276             if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
277                 try:
278                     tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
279                                     os.path.join(S.getValue('RTE_SDK'), ''),
280                                     os.path.join(guest_dir, 'DPDK')],
281                                    self._logger,
282                                    'Copying DPDK to shared directory...',
283                                    True)
284                     tasks.run_task(['rsync', '-a', '-r', '-l',
285                                     os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
286                                     os.path.join(guest_dir, 'l2fwd')],
287                                    self._logger,
288                                    'Copying l2fwd to shared directory...',
289                                    True)
290                 except subprocess.CalledProcessError:
291                     self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
292
293             counter += 1
294
295     def _mount_hugepages(self):
296         """Mount hugepages if usage of DPDK or Qemu is detected
297         """
298         # hugepages are needed by DPDK and Qemu
299         if not self._hugepages_mounted and \
300             (self.deployment.count('v') or \
301              S.getValue('VSWITCH').lower().count('dpdk') or \
302              self._vswitch_none):
303             hugepages.mount_hugepages()
304             self._hugepages_mounted = True
305
306     def _umount_hugepages(self):
307         """Umount hugepages if they were mounted before
308         """
309         if self._hugepages_mounted:
310             hugepages.umount_hugepages()
311             self._hugepages_mounted = False
312
313     @staticmethod
314     def _write_result_to_file(results, output):
315         """Write list of dictionaries to a CSV file.
316
317         Each element on list will create separate row in output file.
318         If output file already exists, data will be appended at the end,
319         otherwise it will be created.
320
321         :param results: list of dictionaries.
322         :param output: path to output file.
323         """
324         with open(output, 'a') as csvfile:
325
326             logging.info("Write results to file: " + output)
327             fieldnames = TestCase._get_unique_keys(results)
328
329             writer = csv.DictWriter(csvfile, fieldnames)
330
331             if not csvfile.tell():  # file is now empty
332                 writer.writeheader()
333
334             for result in results:
335                 writer.writerow(result)
336
337
338     @staticmethod
339     def _get_unique_keys(list_of_dicts):
340         """Gets unique key values as ordered list of strings in given dicts
341
342         :param list_of_dicts: list of dictionaries.
343
344         :returns: list of unique keys(strings).
345         """
346         result = OrderedDict()
347         for item in list_of_dicts:
348             for key in item.keys():
349                 result[key] = ''
350
351         return list(result.keys())
352
353
354     def _add_flows(self, vswitch_ctl):
355         """Add flows to the vswitch
356
357         :param vswitch_ctl vswitch controller
358         """
359         vswitch = vswitch_ctl.get_vswitch()
360         # TODO BOM 15-08-07 the frame mod code assumes that the
361         # physical ports are ports 1 & 2. The actual numbers
362         # need to be retrived from the vSwitch and the metadata value
363         # updated accordingly.
364         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
365         if self._frame_mod == "vlan":
366             # 0x8100 => VLAN ethertype
367             self._logger.debug(" ****   VLAN   ***** ")
368             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
369                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
370             vswitch.add_flow(bridge, flow)
371             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
372                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
373             vswitch.add_flow(bridge, flow)
374         elif self._frame_mod == "mpls":
375             # 0x8847 => MPLS unicast ethertype
376             self._logger.debug(" ****   MPLS  ***** ")
377             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
378                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
379             vswitch.add_flow(bridge, flow)
380             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
381                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
382             vswitch.add_flow(bridge, flow)
383         elif self._frame_mod == "mac":
384             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
385                     'actions': ['mod_dl_src:22:22:22:22:22:22',
386                                 'goto_table:3']}
387             vswitch.add_flow(bridge, flow)
388             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
389                     'actions': ['mod_dl_src:11:11:11:11:11:11',
390                                 'goto_table:3']}
391             vswitch.add_flow(bridge, flow)
392         elif self._frame_mod == "dscp":
393             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
394             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
395                     'dl_type':'0x0800',
396                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
397             vswitch.add_flow(bridge, flow)
398             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
399                     'dl_type':'0x0800',
400                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
401             vswitch.add_flow(bridge, flow)
402         elif self._frame_mod == "ttl":
403             # 251 and 241 are the highest prime numbers < 255
404             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
405                     'dl_type':'0x0800',
406                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
407             vswitch.add_flow(bridge, flow)
408             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
409                     'dl_type':'0x0800',
410                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
411             vswitch.add_flow(bridge, flow)
412         elif self._frame_mod == "ip_addr":
413             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
414                     'dl_type':'0x0800',
415                     'actions': ['mod_nw_src:10.10.10.10',
416                                 'mod_nw_dst:20.20.20.20',
417                                 'goto_table:3']}
418             vswitch.add_flow(bridge, flow)
419             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
420                     'dl_type':'0x0800',
421                     'actions': ['mod_nw_src:20.20.20.20',
422                                 'mod_nw_dst:10.10.10.10',
423                                 'goto_table:3']}
424             vswitch.add_flow(bridge, flow)
425         elif self._frame_mod == "ip_port":
426             # TODO BOM 15-08-27 The traffic generated is assumed
427             # to be UDP (nw_proto 17d) which is the default case but
428             # we will need to pick up the actual traffic params in use.
429             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
430                     'dl_type':'0x0800', 'nw_proto':'17',
431                     'actions': ['mod_tp_src:44444',
432                                 'mod_tp_dst:44444', 'goto_table:3']}
433             vswitch.add_flow(bridge, flow)
434             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
435                     'dl_type':'0x0800', 'nw_proto':'17',
436                     'actions': ['mod_tp_src:44444',
437                                 'mod_tp_dst:44444', 'goto_table:3']}
438             vswitch.add_flow(bridge, flow)
439         else:
440             pass