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