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._testcase_start_time = time.time()
49 self._hugepages_mounted = False
50 self._traffic_ctl = None
52 self._vswitch_ctl = None
53 self._collector = 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
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')))
67 # update global settings
68 guest_loopback = get_test_param('guest_loopback', None)
70 self._update_settings('GUEST_LOOPBACK', [guest_loopback for dummy in S.getValue('GUEST_LOOPBACK')])
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'),
77 functions.settings_update_paths()
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)
85 bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
86 bidirectional = get_test_param('bidirectional', bidirectional)
87 if not isinstance(bidirectional, str):
89 'Bi-dir value must be of type string in testcase configuration')
90 bidirectional = bidirectional.title() # Keep things consistent
92 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
93 traffic_type = get_test_param('traffic_type', traffic_type)
95 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
96 framerate = get_test_param('iload', framerate)
98 self.deployment = cfg['Deployment']
99 self._frame_mod = cfg.get('Frame Modification', None)
101 self._tunnel_type = None
102 self._tunnel_operation = None
104 if self.deployment == 'op2p':
105 self._tunnel_operation = cfg['Tunnel Operation']
107 if 'Tunnel Type' in cfg:
108 self._tunnel_type = cfg['Tunnel Type']
109 self._tunnel_type = get_test_param('tunnel_type',
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])
116 self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
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)
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']
132 # background load is not requested, so use dummy implementation
133 self._loadgen = "Dummy"
136 self._frame_mod = self._frame_mod.lower()
137 self._results_dir = S.getValue('RESULTS_PATH')
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)})
150 # Packet Forwarding mode
151 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
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')})
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'),
165 S.getValue('TRAFFICGEN_PORT2_MAC')})
167 self._traffic['l3'].update({'srcip':
168 S.getValue('TRAFFICGEN_PORT1_IP'),
170 S.getValue('TRAFFICGEN_PORT2_IP')})
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']
180 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
182 self._logger.debug("MAC addresses can not be read")
184 def run_initialize(self):
185 """ Prepare test execution environment
187 self._logger.debug(self.name)
189 # mount hugepages if needed
190 self._mount_hugepages()
192 # copy sources of l2 forwarding tools into VM shared dir if needed
193 self._copy_fwd_tools_for_all_guests()
195 self._logger.debug("Controllers:")
197 self._traffic_ctl = component_factory.create_traffic(
198 self._traffic['traffic_type'],
199 loader.get_trafficgen_class())
201 self._vnf_ctl = component_factory.create_vnf(
203 loader.get_vnf_class())
205 if self._vswitch_none:
206 self._vswitch_ctl = component_factory.create_pktfwd(
208 loader.get_pktfwd_class())
210 self._vswitch_ctl = component_factory.create_vswitch(
212 loader.get_vswitch_class(),
214 self._tunnel_operation)
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(
223 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
224 "_" + self.deployment + ".csv")
226 self._logger.debug("Setup:")
228 def run_finalize(self):
229 """ Tear down test execution environment and record test results
231 # umount hugepages if mounted
232 self._umount_hugepages()
234 # restore original settings
235 S.load_from_dict(self._settings_original)
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'):
249 veth_list = os.listdir('/tmp/veth')
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')
257 def run_report(self):
258 """ Report test results
260 self._logger.debug("self._collector Results:")
261 self._collector.print_results()
263 if S.getValue('mode') != 'trafficgen-off':
264 self._logger.debug("Traffic Results:")
265 self._traffic_ctl.print_results()
267 self._tc_results = self._append_results(self._traffic_ctl.get_results())
268 TestCase.write_result_to_file(self._tc_results, self._output_file)
273 All setup and teardown through controllers is included.
275 # prepare test execution environment
276 self.run_initialize()
278 with self._vswitch_ctl, self._loadgen:
279 with self._vnf_ctl, self._collector:
280 if not self._vswitch_none:
283 # run traffic generator if requested, otherwise wait for manual termination
284 if S.getValue('mode') == 'trafficgen-off':
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)
289 if S.getValue('mode') == 'trafficgen-pause':
291 true_vals = ('yes', 'y', 'ye', None)
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='')
299 with self._traffic_ctl:
300 self._traffic_ctl.send_traffic(self._traffic)
302 # dump vswitch flows before they are affected by VNF termination
303 if not self._vswitch_none:
304 self._vswitch_ctl.dump_vswitch_flows()
306 # tear down test execution environment and log results
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
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
320 :param param: Name of parameter inside settings
321 :param value: Disired parameter value
323 orig_value = S.getValue(param)
324 if orig_value != value:
325 self._settings_original[param] = orig_value
326 S.setValue(param, value)
328 def _append_results(self, results):
330 Method appends mandatory Test Case results to list of dictionaries.
332 :param results: list of dictionaries which contains results from
335 :returns: modified list of dictionaries.
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
352 def _copy_fwd_tools_for_all_guests(self):
353 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
355 # data are copied only for pvp and pvvp, so let's count number of 'v'
357 while counter <= self.deployment.count('v'):
358 self._copy_fwd_tools_for_guest(counter)
361 def _copy_fwd_tools_for_guest(self, index):
362 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
364 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
366 guest_dir = S.getValue('GUEST_SHARE_DIR')[index-1]
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)
373 # directory to share files between host and guest
374 os.makedirs(guest_dir)
376 # copy sources into shared dir only if neccessary
377 if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
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')],
383 'Copying DPDK to shared directory...',
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')],
389 'Copying l2fwd to shared directory...',
391 except subprocess.CalledProcessError:
392 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
395 def _mount_hugepages(self):
396 """Mount hugepages if usage of DPDK or Qemu is detected
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
407 def _umount_hugepages(self):
408 """Umount hugepages if they were mounted before
410 if self._hugepages_mounted:
411 hugepages.umount_hugepages()
412 self._hugepages_mounted = False
415 def write_result_to_file(results, output):
416 """Write list of dictionaries to a CSV file.
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.
422 :param results: list of dictionaries.
423 :param output: path to output file.
425 with open(output, 'a') as csvfile:
427 logging.info("Write results to file: " + output)
428 fieldnames = TestCase._get_unique_keys(results)
430 writer = csv.DictWriter(csvfile, fieldnames)
432 if not csvfile.tell(): # file is now empty
435 for result in results:
436 writer.writerow(result)
439 def _get_unique_keys(list_of_dicts):
440 """Gets unique key values as ordered list of strings in given dicts
442 :param list_of_dicts: list of dictionaries.
444 :returns: list of unique keys(strings).
446 result = OrderedDict()
447 for item in list_of_dicts:
448 for key in item.keys():
451 return list(result.keys())
453 def _add_flows(self):
454 """Add flows to the vswitch
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',
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',
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',
493 'actions': ['mod_nw_tos:184', 'goto_table:3']}
494 vswitch.add_flow(bridge, flow)
495 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
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',
503 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
504 vswitch.add_flow(bridge, flow)
505 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
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',
512 'actions': ['mod_nw_src:10.10.10.10',
513 'mod_nw_dst:20.20.20.20',
515 vswitch.add_flow(bridge, flow)
516 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
518 'actions': ['mod_nw_src:20.20.20.20',
519 'mod_nw_dst:10.10.10.10',
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)