ff1247fc4cf3d9d7851111144a427c6ad0c9f89d
[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):
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 = S.getValue('RESULTS_PATH')
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         elif S.getValue('NICS')[0]['type'] == 'vf' or S.getValue('NICS')[1]['type'] == 'vf':
167             mac1 = S.getValue('NICS')[0]['mac']
168             mac2 = S.getValue('NICS')[1]['mac']
169             if mac1 and mac2:
170                 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
171             else:
172                 self._logger.debug("MAC addresses can not be read")
173
174
175
176         self._logger.debug("Controllers:")
177         loader = Loader()
178         self._traffic_ctl = component_factory.create_traffic(
179             self._traffic['traffic_type'],
180             loader.get_trafficgen_class())
181
182         self._vnf_ctl = component_factory.create_vnf(
183             self.deployment,
184             loader.get_vnf_class())
185
186         if self._vswitch_none:
187             self._vswitch_ctl = component_factory.create_pktfwd(
188                 self.deployment,
189                 loader.get_pktfwd_class())
190         else:
191             self._vswitch_ctl = component_factory.create_vswitch(
192                 self.deployment,
193                 loader.get_vswitch_class(),
194                 self._traffic,
195                 self._tunnel_operation)
196
197         self._collector = component_factory.create_collector(
198             loader.get_collector_class(),
199             self._results_dir, self.name)
200         self._loadgen = component_factory.create_loadgen(
201             self._loadgen,
202             self._load_cfg)
203
204         self._output_file = os.path.join(self._results_dir, "result_" + self.name +
205                                          "_" + self.deployment + ".csv")
206
207         self._logger.debug("Setup:")
208
209     def run_finalize(self):
210         """ Tear down test execution environment and record test results
211         """
212         # umount hugepages if mounted
213         self._umount_hugepages()
214
215     def run_report(self):
216         """ Report test results
217         """
218         self._logger.debug("self._collector Results:")
219         self._collector.print_results()
220
221         if S.getValue('mode') != 'trafficgen-off':
222             self._logger.debug("Traffic Results:")
223             self._traffic_ctl.print_results()
224
225             self._tc_results = self._append_results(self._traffic_ctl.get_results())
226             TestCase._write_result_to_file(self._tc_results, self._output_file)
227
228     def run(self):
229         """Run the test
230
231         All setup and teardown through controllers is included.
232         """
233         # prepare test execution environment
234         self.run_initialize()
235
236         with self._vswitch_ctl, self._loadgen:
237             with self._vnf_ctl, self._collector:
238                 if not self._vswitch_none:
239                     self._add_flows()
240
241                 # run traffic generator if requested, otherwise wait for manual termination
242                 if S.getValue('mode') == 'trafficgen-off':
243                     time.sleep(2)
244                     self._logger.debug("All is set. Please run traffic generator manually.")
245                     input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
246                 else:
247                     if S.getValue('mode') == 'trafficgen-pause':
248                         time.sleep(2)
249                         true_vals = ('yes', 'y', 'ye', None)
250                         while True:
251                             choice = input(os.linesep + 'Transmission paused, should'
252                                            ' transmission be resumed? ' + os.linesep).lower()
253                             if not choice or choice not in true_vals:
254                                 print('Please respond with \'yes\' or \'y\' ', end='')
255                             else:
256                                 break
257                     with self._traffic_ctl:
258                         self._traffic_ctl.send_traffic(self._traffic)
259
260                     # dump vswitch flows before they are affected by VNF termination
261                     if not self._vswitch_none:
262                         self._vswitch_ctl.dump_vswitch_flows()
263
264         # tear down test execution environment and log results
265         self.run_finalize()
266
267         # report test results
268         self.run_report()
269
270     def _append_results(self, results):
271         """
272         Method appends mandatory Test Case results to list of dictionaries.
273
274         :param results: list of dictionaries which contains results from
275                 traffic generator.
276
277         :returns: modified list of dictionaries.
278         """
279         for item in results:
280             item[ResultsConstants.ID] = self.name
281             item[ResultsConstants.DEPLOYMENT] = self.deployment
282             item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
283             if self._traffic['multistream']:
284                 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
285                 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
286                 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
287             if len(self.guest_loopback):
288                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
289             if self._tunnel_type:
290                 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
291         return results
292
293     def _copy_fwd_tools_for_guest(self):
294         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
295         """
296         counter = 0
297         # method is executed only for pvp and pvvp, so let's count number of 'v'
298         while counter < self.deployment.count('v'):
299             guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
300
301             # remove shared dir if it exists to avoid issues with file consistency
302             if os.path.exists(guest_dir):
303                 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
304                                'Removing content of shared directory...', True)
305
306             # directory to share files between host and guest
307             os.makedirs(guest_dir)
308
309             # copy sources into shared dir only if neccessary
310             if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
311                 try:
312                     tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
313                                     os.path.join(S.getValue('RTE_SDK'), ''),
314                                     os.path.join(guest_dir, 'DPDK')],
315                                    self._logger,
316                                    'Copying DPDK to shared directory...',
317                                    True)
318                     tasks.run_task(['rsync', '-a', '-r', '-l',
319                                     os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
320                                     os.path.join(guest_dir, 'l2fwd')],
321                                    self._logger,
322                                    'Copying l2fwd to shared directory...',
323                                    True)
324                 except subprocess.CalledProcessError:
325                     self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
326
327             counter += 1
328
329     def _mount_hugepages(self):
330         """Mount hugepages if usage of DPDK or Qemu is detected
331         """
332         # hugepages are needed by DPDK and Qemu
333         if not self._hugepages_mounted and \
334             (self.deployment.count('v') or \
335              S.getValue('VSWITCH').lower().count('dpdk') or \
336              self._vswitch_none):
337             hugepages.mount_hugepages()
338             self._hugepages_mounted = True
339
340     def _umount_hugepages(self):
341         """Umount hugepages if they were mounted before
342         """
343         if self._hugepages_mounted:
344             hugepages.umount_hugepages()
345             self._hugepages_mounted = False
346
347     @staticmethod
348     def _write_result_to_file(results, output):
349         """Write list of dictionaries to a CSV file.
350
351         Each element on list will create separate row in output file.
352         If output file already exists, data will be appended at the end,
353         otherwise it will be created.
354
355         :param results: list of dictionaries.
356         :param output: path to output file.
357         """
358         with open(output, 'a') as csvfile:
359
360             logging.info("Write results to file: " + output)
361             fieldnames = TestCase._get_unique_keys(results)
362
363             writer = csv.DictWriter(csvfile, fieldnames)
364
365             if not csvfile.tell():  # file is now empty
366                 writer.writeheader()
367
368             for result in results:
369                 writer.writerow(result)
370
371     @staticmethod
372     def _get_unique_keys(list_of_dicts):
373         """Gets unique key values as ordered list of strings in given dicts
374
375         :param list_of_dicts: list of dictionaries.
376
377         :returns: list of unique keys(strings).
378         """
379         result = OrderedDict()
380         for item in list_of_dicts:
381             for key in item.keys():
382                 result[key] = ''
383
384         return list(result.keys())
385
386     def _add_flows(self):
387         """Add flows to the vswitch
388         """
389         vswitch = self._vswitch_ctl.get_vswitch()
390         # TODO BOM 15-08-07 the frame mod code assumes that the
391         # physical ports are ports 1 & 2. The actual numbers
392         # need to be retrived from the vSwitch and the metadata value
393         # updated accordingly.
394         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
395         if self._frame_mod == "vlan":
396             # 0x8100 => VLAN ethertype
397             self._logger.debug(" ****   VLAN   ***** ")
398             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
399                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
400             vswitch.add_flow(bridge, flow)
401             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
402                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
403             vswitch.add_flow(bridge, flow)
404         elif self._frame_mod == "mpls":
405             # 0x8847 => MPLS unicast ethertype
406             self._logger.debug(" ****   MPLS  ***** ")
407             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
408                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
409             vswitch.add_flow(bridge, flow)
410             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
411                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
412             vswitch.add_flow(bridge, flow)
413         elif self._frame_mod == "mac":
414             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
415                     'actions': ['mod_dl_src:22:22:22:22:22:22',
416                                 'goto_table:3']}
417             vswitch.add_flow(bridge, flow)
418             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
419                     'actions': ['mod_dl_src:11:11:11:11:11:11',
420                                 'goto_table:3']}
421             vswitch.add_flow(bridge, flow)
422         elif self._frame_mod == "dscp":
423             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
424             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
425                     'dl_type':'0x0800',
426                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
427             vswitch.add_flow(bridge, flow)
428             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
429                     'dl_type':'0x0800',
430                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
431             vswitch.add_flow(bridge, flow)
432         elif self._frame_mod == "ttl":
433             # 251 and 241 are the highest prime numbers < 255
434             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
435                     'dl_type':'0x0800',
436                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
437             vswitch.add_flow(bridge, flow)
438             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
439                     'dl_type':'0x0800',
440                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
441             vswitch.add_flow(bridge, flow)
442         elif self._frame_mod == "ip_addr":
443             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
444                     'dl_type':'0x0800',
445                     'actions': ['mod_nw_src:10.10.10.10',
446                                 'mod_nw_dst:20.20.20.20',
447                                 'goto_table:3']}
448             vswitch.add_flow(bridge, flow)
449             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
450                     'dl_type':'0x0800',
451                     'actions': ['mod_nw_src:20.20.20.20',
452                                 'mod_nw_dst:10.10.10.10',
453                                 'goto_table:3']}
454             vswitch.add_flow(bridge, flow)
455         elif self._frame_mod == "ip_port":
456             # TODO BOM 15-08-27 The traffic generated is assumed
457             # to be UDP (nw_proto 17d) which is the default case but
458             # we will need to pick up the actual traffic params in use.
459             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
460                     'dl_type':'0x0800', 'nw_proto':'17',
461                     'actions': ['mod_tp_src:44444',
462                                 'mod_tp_dst:44444', 'goto_table:3']}
463             vswitch.add_flow(bridge, flow)
464             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
465                     'dl_type':'0x0800', 'nw_proto':'17',
466                     'actions': ['mod_tp_src:44444',
467                                 'mod_tp_dst:44444', 'goto_table:3']}
468             vswitch.add_flow(bridge, flow)
469         else:
470             pass