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