1 # Copyright 2015-2016 Intel Corporation.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
23 from collections import OrderedDict
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
35 class TestCase(object):
36 """TestCase base class
38 In this basic form runs RFC2544 throughput test
40 def __init__(self, cfg):
41 """Pull out fields from test config
43 :param cfg: A dictionary of string-value pairs describing the test
44 configuration. Both the key and values strings use well-known
46 :param results_dir: Where the csv formatted results are written.
48 self._hugepages_mounted = False
49 self._traffic_ctl = None
51 self._vswitch_ctl = None
52 self._collector = 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
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')))
65 # update global settings
66 guest_loopback = get_test_param('guest_loopback', None)
68 self._update_settings('GUEST_LOOPBACK', [guest_loopback for dummy in S.getValue('GUEST_LOOPBACK')])
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'),
75 functions.settings_update_paths()
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)
83 bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
84 bidirectional = get_test_param('bidirectional', bidirectional)
85 if not isinstance(bidirectional, str):
87 'Bi-dir value must be of type string in testcase configuration')
88 bidirectional = bidirectional.title() # Keep things consistent
90 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
91 traffic_type = get_test_param('traffic_type', traffic_type)
93 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
94 framerate = get_test_param('iload', framerate)
96 self.deployment = cfg['Deployment']
97 self._frame_mod = cfg.get('Frame Modification', None)
99 self._tunnel_type = None
100 self._tunnel_operation = None
102 if self.deployment == 'op2p':
103 self._tunnel_operation = cfg['Tunnel Operation']
105 if 'Tunnel Type' in cfg:
106 self._tunnel_type = cfg['Tunnel Type']
107 self._tunnel_type = get_test_param('tunnel_type',
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])
114 self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
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)
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']
130 # background load is not requested, so use dummy implementation
131 self._loadgen = "Dummy"
134 self._frame_mod = self._frame_mod.lower()
135 self._results_dir = S.getValue('RESULTS_PATH')
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)})
148 # Packet Forwarding mode
149 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
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')})
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'),
163 S.getValue('TRAFFICGEN_PORT2_MAC')})
165 self._traffic['l3'].update({'srcip':
166 S.getValue('TRAFFICGEN_PORT1_IP'),
168 S.getValue('TRAFFICGEN_PORT2_IP')})
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']
178 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
180 self._logger.debug("MAC addresses can not be read")
182 def run_initialize(self):
183 """ Prepare test execution environment
185 self._logger.debug(self.name)
187 # mount hugepages if needed
188 self._mount_hugepages()
190 # copy sources of l2 forwarding tools into VM shared dir if needed
191 self._copy_fwd_tools_for_all_guests()
193 self._logger.debug("Controllers:")
195 self._traffic_ctl = component_factory.create_traffic(
196 self._traffic['traffic_type'],
197 loader.get_trafficgen_class())
199 self._vnf_ctl = component_factory.create_vnf(
201 loader.get_vnf_class())
203 if self._vswitch_none:
204 self._vswitch_ctl = component_factory.create_pktfwd(
206 loader.get_pktfwd_class())
208 self._vswitch_ctl = component_factory.create_vswitch(
210 loader.get_vswitch_class(),
212 self._tunnel_operation)
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(
221 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
222 "_" + self.deployment + ".csv")
224 self._logger.debug("Setup:")
226 def run_finalize(self):
227 """ Tear down test execution environment and record test results
229 # umount hugepages if mounted
230 self._umount_hugepages()
232 # restore original settings
233 S.load_from_dict(self._settings_original)
235 def run_report(self):
236 """ Report test results
238 self._logger.debug("self._collector Results:")
239 self._collector.print_results()
241 if S.getValue('mode') != 'trafficgen-off':
242 self._logger.debug("Traffic Results:")
243 self._traffic_ctl.print_results()
245 self._tc_results = self._append_results(self._traffic_ctl.get_results())
246 TestCase._write_result_to_file(self._tc_results, self._output_file)
251 All setup and teardown through controllers is included.
253 # prepare test execution environment
254 self.run_initialize()
256 with self._vswitch_ctl, self._loadgen:
257 with self._vnf_ctl, self._collector:
258 if not self._vswitch_none:
261 # run traffic generator if requested, otherwise wait for manual termination
262 if S.getValue('mode') == 'trafficgen-off':
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)
267 if S.getValue('mode') == 'trafficgen-pause':
269 true_vals = ('yes', 'y', 'ye', None)
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='')
277 with self._traffic_ctl:
278 self._traffic_ctl.send_traffic(self._traffic)
280 # dump vswitch flows before they are affected by VNF termination
281 if not self._vswitch_none:
282 self._vswitch_ctl.dump_vswitch_flows()
284 # tear down test execution environment and log results
287 # report test results
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
295 :param param: Name of parameter inside settings
296 :param value: Disired parameter value
298 orig_value = S.getValue(param)
299 if orig_value != value:
300 self._settings_original[param] = orig_value
301 S.setValue(param, value)
303 def _append_results(self, results):
305 Method appends mandatory Test Case results to list of dictionaries.
307 :param results: list of dictionaries which contains results from
310 :returns: modified list of dictionaries.
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
326 def _copy_fwd_tools_for_all_guests(self):
327 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
329 # data are copied only for pvp and pvvp, so let's count number of 'v'
331 while counter <= self.deployment.count('v'):
332 self._copy_fwd_tools_for_guest(counter)
335 def _copy_fwd_tools_for_guest(self, index):
336 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
338 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
340 guest_dir = S.getValue('GUEST_SHARE_DIR')[index-1]
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)
347 # directory to share files between host and guest
348 os.makedirs(guest_dir)
350 # copy sources into shared dir only if neccessary
351 if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
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')],
357 'Copying DPDK to shared directory...',
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')],
363 'Copying l2fwd to shared directory...',
365 except subprocess.CalledProcessError:
366 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
369 def _mount_hugepages(self):
370 """Mount hugepages if usage of DPDK or Qemu is detected
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
381 def _umount_hugepages(self):
382 """Umount hugepages if they were mounted before
384 if self._hugepages_mounted:
385 hugepages.umount_hugepages()
386 self._hugepages_mounted = False
389 def _write_result_to_file(results, output):
390 """Write list of dictionaries to a CSV file.
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.
396 :param results: list of dictionaries.
397 :param output: path to output file.
399 with open(output, 'a') as csvfile:
401 logging.info("Write results to file: " + output)
402 fieldnames = TestCase._get_unique_keys(results)
404 writer = csv.DictWriter(csvfile, fieldnames)
406 if not csvfile.tell(): # file is now empty
409 for result in results:
410 writer.writerow(result)
413 def _get_unique_keys(list_of_dicts):
414 """Gets unique key values as ordered list of strings in given dicts
416 :param list_of_dicts: list of dictionaries.
418 :returns: list of unique keys(strings).
420 result = OrderedDict()
421 for item in list_of_dicts:
422 for key in item.keys():
425 return list(result.keys())
427 def _add_flows(self):
428 """Add flows to the vswitch
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',
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',
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',
467 'actions': ['mod_nw_tos:184', 'goto_table:3']}
468 vswitch.add_flow(bridge, flow)
469 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
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',
477 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
478 vswitch.add_flow(bridge, flow)
479 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
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',
486 'actions': ['mod_nw_src:10.10.10.10',
487 'mod_nw_dst:20.20.20.20',
489 vswitch.add_flow(bridge, flow)
490 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
492 'actions': ['mod_nw_src:20.20.20.20',
493 'mod_nw_dst:10.10.10.10',
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)