c79e4d0ff3b6f3a71276829a646d5d1832167bd7
[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                     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             # create shared dir if it doesn't exist
268             if not os.path.exists(guest_dir):
269                 os.makedirs(guest_dir)
270
271             # copy sources into shared dir only if neccessary
272             if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
273                 try:
274                     tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
275                                     os.path.join(S.getValue('RTE_SDK'), ''),
276                                     os.path.join(guest_dir, 'DPDK')],
277                                    self._logger,
278                                    'Copying DPDK to shared directory...',
279                                    True)
280                     tasks.run_task(['rsync', '-a', '-r', '-l',
281                                     os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
282                                     os.path.join(guest_dir, 'l2fwd')],
283                                    self._logger,
284                                    'Copying l2fwd to shared directory...',
285                                    True)
286                 except subprocess.CalledProcessError:
287                     self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
288
289             counter += 1
290
291     def _mount_hugepages(self):
292         """Mount hugepages if usage of DPDK or Qemu is detected
293         """
294         # hugepages are needed by DPDK and Qemu
295         if not self._hugepages_mounted and \
296             (self.deployment.count('v') or \
297              S.getValue('VSWITCH').lower().count('dpdk') or \
298              self._vswitch_none):
299             hugepages.mount_hugepages()
300             self._hugepages_mounted = True
301
302     def _umount_hugepages(self):
303         """Umount hugepages if they were mounted before
304         """
305         if self._hugepages_mounted:
306             hugepages.umount_hugepages()
307             self._hugepages_mounted = False
308
309     @staticmethod
310     def _write_result_to_file(results, output):
311         """Write list of dictionaries to a CSV file.
312
313         Each element on list will create separate row in output file.
314         If output file already exists, data will be appended at the end,
315         otherwise it will be created.
316
317         :param results: list of dictionaries.
318         :param output: path to output file.
319         """
320         with open(output, 'a') as csvfile:
321
322             logging.info("Write results to file: " + output)
323             fieldnames = TestCase._get_unique_keys(results)
324
325             writer = csv.DictWriter(csvfile, fieldnames)
326
327             if not csvfile.tell():  # file is now empty
328                 writer.writeheader()
329
330             for result in results:
331                 writer.writerow(result)
332
333
334     @staticmethod
335     def _get_unique_keys(list_of_dicts):
336         """Gets unique key values as ordered list of strings in given dicts
337
338         :param list_of_dicts: list of dictionaries.
339
340         :returns: list of unique keys(strings).
341         """
342         result = OrderedDict()
343         for item in list_of_dicts:
344             for key in item.keys():
345                 result[key] = ''
346
347         return list(result.keys())
348
349
350     def _add_flows(self, vswitch_ctl):
351         """Add flows to the vswitch
352
353         :param vswitch_ctl vswitch controller
354         """
355         vswitch = vswitch_ctl.get_vswitch()
356         # TODO BOM 15-08-07 the frame mod code assumes that the
357         # physical ports are ports 1 & 2. The actual numbers
358         # need to be retrived from the vSwitch and the metadata value
359         # updated accordingly.
360         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
361         if self._frame_mod == "vlan":
362             # 0x8100 => VLAN ethertype
363             self._logger.debug(" ****   VLAN   ***** ")
364             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
365                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
366             vswitch.add_flow(bridge, flow)
367             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
368                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
369             vswitch.add_flow(bridge, flow)
370         elif self._frame_mod == "mpls":
371             # 0x8847 => MPLS unicast ethertype
372             self._logger.debug(" ****   MPLS  ***** ")
373             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
374                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
375             vswitch.add_flow(bridge, flow)
376             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
377                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
378             vswitch.add_flow(bridge, flow)
379         elif self._frame_mod == "mac":
380             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
381                     'actions': ['mod_dl_src:22:22:22:22:22:22',
382                                 'goto_table:3']}
383             vswitch.add_flow(bridge, flow)
384             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
385                     'actions': ['mod_dl_src:11:11:11:11:11:11',
386                                 'goto_table:3']}
387             vswitch.add_flow(bridge, flow)
388         elif self._frame_mod == "dscp":
389             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
390             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
391                     'dl_type':'0x0800',
392                     'actions': ['mod_nw_tos:184', '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_tos:184', 'goto_table:3']}
397             vswitch.add_flow(bridge, flow)
398         elif self._frame_mod == "ttl":
399             # 251 and 241 are the highest prime numbers < 255
400             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
401                     'dl_type':'0x0800',
402                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
403             vswitch.add_flow(bridge, flow)
404             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
405                     'dl_type':'0x0800',
406                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
407             vswitch.add_flow(bridge, flow)
408         elif self._frame_mod == "ip_addr":
409             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
410                     'dl_type':'0x0800',
411                     'actions': ['mod_nw_src:10.10.10.10',
412                                 'mod_nw_dst:20.20.20.20',
413                                 'goto_table:3']}
414             vswitch.add_flow(bridge, flow)
415             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
416                     'dl_type':'0x0800',
417                     'actions': ['mod_nw_src:20.20.20.20',
418                                 'mod_nw_dst:10.10.10.10',
419                                 'goto_table:3']}
420             vswitch.add_flow(bridge, flow)
421         elif self._frame_mod == "ip_port":
422             # TODO BOM 15-08-27 The traffic generated is assumed
423             # to be UDP (nw_proto 17d) which is the default case but
424             # we will need to pick up the actual traffic params in use.
425             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
426                     'dl_type':'0x0800', 'nw_proto':'17',
427                     'actions': ['mod_tp_src:44444',
428                                 'mod_tp_dst:44444', 'goto_table:3']}
429             vswitch.add_flow(bridge, flow)
430             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
431                     'dl_type':'0x0800', 'nw_proto':'17',
432                     'actions': ['mod_tp_src:44444',
433                                 'mod_tp_dst:44444', 'goto_table:3']}
434             vswitch.add_flow(bridge, flow)
435         else:
436             pass