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
17 from collections import OrderedDict
27 from conf import settings as S
28 from conf import get_test_param
29 import core.component_factory as component_factory
30 from core.loader import Loader
31 from core.results.results_constants import ResultsConstants
32 from tools import tasks
33 from tools import hugepages
34 from tools import functions
35 from tools.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
38 class TestCase(object):
39 """TestCase base class
41 In this basic form runs RFC2544 throughput test
43 def __init__(self, cfg):
44 """Pull out fields from test config
46 :param cfg: A dictionary of string-value pairs describing the test
47 configuration. Both the key and values strings use well-known
49 :param results_dir: Where the csv formatted results are written.
51 self._testcase_start_time = time.time()
52 self._hugepages_mounted = False
53 self._traffic_ctl = None
55 self._vswitch_ctl = None
56 self._collector = None
58 self._output_file = None
59 self._tc_results = None
60 self._settings_original = {}
61 self._settings_paths_modified = False
62 self._testcast_run_time = None
64 self._update_settings('VSWITCH', cfg.get('vSwitch', S.getValue('VSWITCH')))
65 self._update_settings('VNF', cfg.get('VNF', S.getValue('VNF')))
66 self._update_settings('TRAFFICGEN', cfg.get('Trafficgen', S.getValue('TRAFFICGEN')))
67 self._update_settings('TEST_PARAMS', cfg.get('Parameters', S.getValue('TEST_PARAMS')))
69 # update global settings
70 guest_loopback = get_test_param('guest_loopback', None)
72 # we can put just one item, it'll be expanded automatically for all VMs
73 self._update_settings('GUEST_LOOPBACK', [guest_loopback])
75 if 'VSWITCH' in self._settings_original or 'VNF' in self._settings_original:
76 self._settings_original.update({
77 'RTE_SDK' : S.getValue('RTE_SDK'),
78 'OVS_DIR' : S.getValue('OVS_DIR'),
80 functions.settings_update_paths()
82 # set test parameters; CLI options take precedence to testcase settings
83 self._logger = logging.getLogger(__name__)
84 self.name = cfg['Name']
85 self.desc = cfg.get('Description', 'No description given.')
86 self.test = cfg.get('TestSteps', None)
88 bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
89 bidirectional = get_test_param('bidirectional', bidirectional)
90 if not isinstance(bidirectional, str):
92 'Bi-dir value must be of type string in testcase configuration')
93 bidirectional = bidirectional.title() # Keep things consistent
95 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
96 traffic_type = get_test_param('traffic_type', traffic_type)
98 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
99 framerate = get_test_param('iload', framerate)
101 self.deployment = cfg['Deployment']
102 self._frame_mod = cfg.get('Frame Modification', None)
104 self._tunnel_type = None
105 self._tunnel_operation = None
107 if self.deployment == 'op2p':
108 self._tunnel_operation = cfg['Tunnel Operation']
110 if 'Tunnel Type' in cfg:
111 self._tunnel_type = cfg['Tunnel Type']
112 self._tunnel_type = get_test_param('tunnel_type',
115 # read configuration of streams; CLI parameter takes precedence to
116 # testcase definition
117 multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
118 multistream = get_test_param('multistream', multistream)
119 stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
120 stream_type = get_test_param('stream_type', stream_type)
121 pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
122 pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
124 # check if test requires background load and which generator it uses
125 self._load_cfg = cfg.get('Load', None)
126 if self._load_cfg and 'tool' in self._load_cfg:
127 self._loadgen = self._load_cfg['tool']
129 # background load is not requested, so use dummy implementation
130 self._loadgen = "Dummy"
133 self._frame_mod = self._frame_mod.lower()
134 self._results_dir = S.getValue('RESULTS_PATH')
136 # set traffic details, so they can be passed to vswitch and traffic ctls
137 self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
138 self._traffic.update({'traffic_type': traffic_type,
139 'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
140 'bidir': bidirectional,
141 'tunnel_type': self._tunnel_type,
142 'multistream': int(multistream),
143 'stream_type': stream_type,
144 'pre_installed_flows' : pre_installed_flows,
145 'frame_rate': int(framerate)})
147 # Packet Forwarding mode
148 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
150 # trafficgen configuration required for tests of tunneling protocols
151 if self.deployment == "op2p":
152 self._traffic['l2'].update({'srcmac':
153 S.getValue('TRAFFICGEN_PORT1_MAC'),
155 S.getValue('TRAFFICGEN_PORT2_MAC')})
157 self._traffic['l3'].update({'srcip':
158 S.getValue('TRAFFICGEN_PORT1_IP'),
160 S.getValue('TRAFFICGEN_PORT2_IP')})
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']
170 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
172 self._logger.debug("MAC addresses can not be read")
174 def run_initialize(self):
175 """ Prepare test execution environment
177 self._logger.debug(self.name)
179 # mount hugepages if needed
180 self._mount_hugepages()
182 self._logger.debug("Controllers:")
184 self._traffic_ctl = component_factory.create_traffic(
185 self._traffic['traffic_type'],
186 loader.get_trafficgen_class())
188 self._vnf_ctl = component_factory.create_vnf(
190 loader.get_vnf_class())
192 # verify enough hugepages are free to run the testcase
193 if not self._check_for_enough_hugepages():
194 raise RuntimeError('Not enough hugepages free to run test.')
196 # perform guest related handling
197 if self._vnf_ctl.get_vnfs_number():
198 # copy sources of l2 forwarding tools into VM shared dir if needed
199 self._copy_fwd_tools_for_all_guests()
201 # in case of multi VM in parallel, set the number of streams to the number of VMs
202 if self.deployment.startswith('pvpv'):
203 # for each VM NIC pair we need an unique stream
205 for vm_nic in S.getValue('GUEST_NICS_NR')[:self._vnf_ctl.get_vnfs_number()]:
206 streams += int(vm_nic / 2) if vm_nic > 1 else 1
207 self._logger.debug("VMs with parallel connection were detected. "
208 "Thus Number of streams was set to %s", streams)
209 self._traffic.update({'multistream': streams})
211 # OVS Vanilla requires guest VM MAC address and IPs to work
212 if 'linux_bridge' in S.getValue('GUEST_LOOPBACK'):
213 self._traffic['l2'].update({'srcmac': S.getValue('VANILLA_TGEN_PORT1_MAC'),
214 'dstmac': S.getValue('VANILLA_TGEN_PORT2_MAC')})
215 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
216 'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
218 if self._vswitch_none:
219 self._vswitch_ctl = component_factory.create_pktfwd(
221 loader.get_pktfwd_class())
223 self._vswitch_ctl = component_factory.create_vswitch(
225 loader.get_vswitch_class(),
227 self._tunnel_operation)
229 self._collector = component_factory.create_collector(
230 loader.get_collector_class(),
231 self._results_dir, self.name)
232 self._loadgen = component_factory.create_loadgen(
236 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
237 "_" + self.deployment + ".csv")
239 self._logger.debug("Setup:")
241 def run_finalize(self):
242 """ Tear down test execution environment and record test results
244 # umount hugepages if mounted
245 self._umount_hugepages()
247 # restore original settings
248 S.load_from_dict(self._settings_original)
250 # cleanup any namespaces created
251 if os.path.isdir('/tmp/namespaces'):
252 import tools.namespace
253 namespace_list = os.listdir('/tmp/namespaces')
254 if len(namespace_list):
255 self._logger.info('Cleaning up namespaces')
256 for name in namespace_list:
257 tools.namespace.delete_namespace(name)
258 os.rmdir('/tmp/namespaces')
259 # cleanup any veth ports created
260 if os.path.isdir('/tmp/veth'):
262 veth_list = os.listdir('/tmp/veth')
264 self._logger.info('Cleaning up veth ports')
265 for eth in veth_list:
266 port1, port2 = eth.split('-')
267 tools.veth.del_veth_port(port1, port2)
268 os.rmdir('/tmp/veth')
270 def run_report(self):
271 """ Report test results
273 self._logger.debug("self._collector Results:")
274 self._collector.print_results()
276 if S.getValue('mode') != 'trafficgen-off':
277 self._logger.debug("Traffic Results:")
278 self._traffic_ctl.print_results()
280 self._tc_results = self._append_results(self._traffic_ctl.get_results())
281 TestCase.write_result_to_file(self._tc_results, self._output_file)
286 All setup and teardown through controllers is included.
288 # prepare test execution environment
289 self.run_initialize()
291 with self._vswitch_ctl, self._loadgen:
292 with self._vnf_ctl, self._collector:
293 if not self._vswitch_none:
296 # run traffic generator if requested, otherwise wait for manual termination
297 if S.getValue('mode') == 'trafficgen-off':
299 self._logger.debug("All is set. Please run traffic generator manually.")
300 input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
302 if S.getValue('mode') == 'trafficgen-pause':
304 true_vals = ('yes', 'y', 'ye', None)
306 choice = input(os.linesep + 'Transmission paused, should'
307 ' transmission be resumed? ' + os.linesep).lower()
308 if not choice or choice not in true_vals:
309 print('Please respond with \'yes\' or \'y\' ', end='')
312 with self._traffic_ctl:
313 self._traffic_ctl.send_traffic(self._traffic)
315 # dump vswitch flows before they are affected by VNF termination
316 if not self._vswitch_none:
317 self._vswitch_ctl.dump_vswitch_flows()
319 # tear down test execution environment and log results
322 self._testcase_run_time = time.strftime("%H:%M:%S",
323 time.gmtime(time.time() - self._testcase_start_time))
324 logging.info("Testcase execution time: " + self._testcase_run_time)
325 # report test results
328 def _update_settings(self, param, value):
329 """ Check value of given configuration parameter
330 In case that new value is different, then testcase
331 specific settings is updated and original value stored
333 :param param: Name of parameter inside settings
334 :param value: Disired parameter value
336 orig_value = S.getValue(param)
337 if orig_value != value:
338 self._settings_original[param] = orig_value
339 S.setValue(param, value)
341 def _append_results(self, results):
343 Method appends mandatory Test Case results to list of dictionaries.
345 :param results: list of dictionaries which contains results from
348 :returns: modified list of dictionaries.
351 item[ResultsConstants.ID] = self.name
352 item[ResultsConstants.DEPLOYMENT] = self.deployment
353 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
354 item[ResultsConstants.TEST_RUN_TIME] = self._testcase_run_time
355 if self._traffic['multistream']:
356 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
357 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
358 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
359 if self._vnf_ctl.get_vnfs_number():
360 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(S.getValue('GUEST_LOOPBACK'))
361 if self._tunnel_type:
362 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
365 def _copy_fwd_tools_for_all_guests(self):
366 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
368 # consider only VNFs involved in the test
369 for guest_dir in set(S.getValue('GUEST_SHARE_DIR')[:self._vnf_ctl.get_vnfs_number()]):
370 self._copy_fwd_tools_for_guest(guest_dir)
372 def _copy_fwd_tools_for_guest(self, guest_dir):
373 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
375 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
377 # remove shared dir if it exists to avoid issues with file consistency
378 if os.path.exists(guest_dir):
379 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
380 'Removing content of shared directory...', True)
382 # directory to share files between host and guest
383 os.makedirs(guest_dir)
385 # copy sources into shared dir only if neccessary
386 guest_loopback = set(S.getValue('GUEST_LOOPBACK'))
387 if 'testpmd' in guest_loopback or 'l2fwd' in guest_loopback:
389 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
390 os.path.join(S.getValue('RTE_SDK_USER'), ''),
391 os.path.join(guest_dir, 'DPDK')],
393 'Copying DPDK to shared directory...',
395 tasks.run_task(['rsync', '-a', '-r', '-l',
396 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
397 os.path.join(guest_dir, 'l2fwd')],
399 'Copying l2fwd to shared directory...',
401 except subprocess.CalledProcessError:
402 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
404 def _mount_hugepages(self):
405 """Mount hugepages if usage of DPDK or Qemu is detected
407 # hugepages are needed by DPDK and Qemu
408 if not self._hugepages_mounted and \
409 (self.deployment.count('v') or \
410 S.getValue('VSWITCH').lower().count('dpdk') or \
411 self._vswitch_none or \
412 self.test and 'vnf' in [step[0][0:3] for step in self.test]):
413 hugepages.mount_hugepages()
414 self._hugepages_mounted = True
416 def _umount_hugepages(self):
417 """Umount hugepages if they were mounted before
419 if self._hugepages_mounted:
420 hugepages.umount_hugepages()
421 self._hugepages_mounted = False
423 def _check_for_enough_hugepages(self):
424 """Check to make sure enough hugepages are free to satisfy the
428 hugepage_size = hugepages.get_hugepage_size()
429 # get hugepage amounts per guest involved in the test
430 for guest in range(self._vnf_ctl.get_vnfs_number()):
431 hugepages_needed += math.ceil((int(S.getValue(
432 'GUEST_MEMORY')[guest]) * 1000) / hugepage_size)
434 # get hugepage amounts for each socket on dpdk
435 sock0_mem, sock1_mem = 0, 0
436 if S.getValue('VSWITCH').lower().count('dpdk'):
437 # the import below needs to remain here and not put into the module
438 # imports because of an exception due to settings not yet loaded
439 from vswitches import ovs_dpdk_vhost
440 if ovs_dpdk_vhost.OvsDpdkVhost.old_dpdk_config():
442 r'-socket-mem\s+(\d+),(\d+)',
443 ''.join(S.getValue('VSWITCHD_DPDK_ARGS')))
445 sock0_mem, sock1_mem = (int(match.group(1)) * 1024 / hugepage_size,
446 int(match.group(2)) * 1024 / hugepage_size)
449 'Could not parse socket memory config in dpdk params.')
451 sock0_mem, sock1_mem = (
453 'VSWITCHD_DPDK_CONFIG')['dpdk-socket-mem'].split(','))
454 sock0_mem, sock1_mem = (int(sock0_mem) * 1024 / hugepage_size,
455 int(sock1_mem) * 1024 / hugepage_size)
457 # If hugepages needed, verify the amounts are free
458 if any([hugepages_needed, sock0_mem, sock1_mem]):
459 free_hugepages = hugepages.get_free_hugepages()
461 logging.info('Need %s hugepages free for guests',
463 result1 = free_hugepages >= hugepages_needed
464 free_hugepages -= hugepages_needed
469 logging.info('Need %s hugepages free for dpdk socket 0',
471 result2 = hugepages.get_free_hugepages('0') >= sock0_mem
472 free_hugepages -= sock0_mem
477 logging.info('Need %s hugepages free for dpdk socket 1',
479 result3 = hugepages.get_free_hugepages('1') >= sock1_mem
480 free_hugepages -= sock1_mem
484 logging.info('Need a total of {} total hugepages'.format(
485 hugepages_needed + sock1_mem + sock0_mem))
487 # The only drawback here is sometimes dpdk doesn't release
488 # its hugepages on a test failure. This could cause a test
489 # to fail when dpdk would be OK to start because it will just
490 # use the previously allocated hugepages.
491 result4 = True if free_hugepages >= 0 else False
493 return all([result1, result2, result3, result4])
498 def write_result_to_file(results, output):
499 """Write list of dictionaries to a CSV file.
501 Each element on list will create separate row in output file.
502 If output file already exists, data will be appended at the end,
503 otherwise it will be created.
505 :param results: list of dictionaries.
506 :param output: path to output file.
508 with open(output, 'a') as csvfile:
510 logging.info("Write results to file: " + output)
511 fieldnames = TestCase._get_unique_keys(results)
513 writer = csv.DictWriter(csvfile, fieldnames)
515 if not csvfile.tell(): # file is now empty
518 for result in results:
519 writer.writerow(result)
522 def _get_unique_keys(list_of_dicts):
523 """Gets unique key values as ordered list of strings in given dicts
525 :param list_of_dicts: list of dictionaries.
527 :returns: list of unique keys(strings).
529 result = OrderedDict()
530 for item in list_of_dicts:
531 for key in item.keys():
534 return list(result.keys())
536 def _add_flows(self):
537 """Add flows to the vswitch
539 vswitch = self._vswitch_ctl.get_vswitch()
540 # TODO BOM 15-08-07 the frame mod code assumes that the
541 # physical ports are ports 1 & 2. The actual numbers
542 # need to be retrived from the vSwitch and the metadata value
543 # updated accordingly.
544 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
545 if self._frame_mod == "vlan":
546 # 0x8100 => VLAN ethertype
547 self._logger.debug(" **** VLAN ***** ")
548 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
549 'actions': ['push_vlan:0x8100', 'goto_table:3']}
550 vswitch.add_flow(bridge, flow)
551 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
552 'actions': ['push_vlan:0x8100', 'goto_table:3']}
553 vswitch.add_flow(bridge, flow)
554 elif self._frame_mod == "mpls":
555 # 0x8847 => MPLS unicast ethertype
556 self._logger.debug(" **** MPLS ***** ")
557 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
558 'actions': ['push_mpls:0x8847', 'goto_table:3']}
559 vswitch.add_flow(bridge, flow)
560 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
561 'actions': ['push_mpls:0x8847', 'goto_table:3']}
562 vswitch.add_flow(bridge, flow)
563 elif self._frame_mod == "mac":
564 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
565 'actions': ['mod_dl_src:22:22:22:22:22:22',
567 vswitch.add_flow(bridge, flow)
568 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
569 'actions': ['mod_dl_src:11:11:11:11:11:11',
571 vswitch.add_flow(bridge, flow)
572 elif self._frame_mod == "dscp":
573 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
574 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
576 'actions': ['mod_nw_tos:184', 'goto_table:3']}
577 vswitch.add_flow(bridge, flow)
578 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
580 'actions': ['mod_nw_tos:184', 'goto_table:3']}
581 vswitch.add_flow(bridge, flow)
582 elif self._frame_mod == "ttl":
583 # 251 and 241 are the highest prime numbers < 255
584 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
586 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
587 vswitch.add_flow(bridge, flow)
588 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
590 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
591 vswitch.add_flow(bridge, flow)
592 elif self._frame_mod == "ip_addr":
593 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
595 'actions': ['mod_nw_src:10.10.10.10',
596 'mod_nw_dst:20.20.20.20',
598 vswitch.add_flow(bridge, flow)
599 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
601 'actions': ['mod_nw_src:20.20.20.20',
602 'mod_nw_dst:10.10.10.10',
604 vswitch.add_flow(bridge, flow)
605 elif self._frame_mod == "ip_port":
606 # TODO BOM 15-08-27 The traffic generated is assumed
607 # to be UDP (nw_proto 17d) which is the default case but
608 # we will need to pick up the actual traffic params in use.
609 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
610 'dl_type':'0x0800', 'nw_proto':'17',
611 'actions': ['mod_tp_src:44444',
612 'mod_tp_dst:44444', 'goto_table:3']}
613 vswitch.add_flow(bridge, flow)
614 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
615 'dl_type':'0x0800', 'nw_proto':'17',
616 'actions': ['mod_tp_src:44444',
617 'mod_tp_dst:44444', 'goto_table:3']}
618 vswitch.add_flow(bridge, flow)