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