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