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