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 # store all GUEST_ specific settings to keep original values before their expansion
65 for key in S.__dict__:
66 if key.startswith('GUEST_'):
67 self._settings_original[key] = S.getValue(key)
69 self._update_settings('VSWITCH', cfg.get('vSwitch', S.getValue('VSWITCH')))
70 self._update_settings('VNF', cfg.get('VNF', S.getValue('VNF')))
71 self._update_settings('TRAFFICGEN', cfg.get('Trafficgen', S.getValue('TRAFFICGEN')))
72 self._update_settings('TEST_PARAMS', cfg.get('Parameters', S.getValue('TEST_PARAMS')))
74 # update global settings
75 functions.settings_update_paths()
76 guest_loopback = get_test_param('guest_loopback', None)
78 # we can put just one item, it'll be expanded automatically for all VMs
79 self._update_settings('GUEST_LOOPBACK', [guest_loopback])
81 # set test parameters; CLI options take precedence to testcase settings
82 self._logger = logging.getLogger(__name__)
83 self.name = cfg['Name']
84 self.desc = cfg.get('Description', 'No description given.')
85 self.test = cfg.get('TestSteps', None)
87 bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
88 bidirectional = get_test_param('bidirectional', bidirectional)
89 if not isinstance(bidirectional, str):
91 'Bi-dir value must be of type string in testcase configuration')
92 bidirectional = bidirectional.title() # Keep things consistent
94 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
95 traffic_type = get_test_param('traffic_type', traffic_type)
97 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
98 framerate = get_test_param('iload', framerate)
100 self.deployment = cfg['Deployment']
101 self._frame_mod = cfg.get('Frame Modification', None)
103 self._tunnel_type = None
104 self._tunnel_operation = None
106 if self.deployment == 'op2p':
107 self._tunnel_operation = cfg['Tunnel Operation']
109 if 'Tunnel Type' in cfg:
110 self._tunnel_type = cfg['Tunnel Type']
111 self._tunnel_type = get_test_param('tunnel_type',
114 # read configuration of streams; CLI parameter takes precedence to
115 # testcase definition
116 multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
117 multistream = get_test_param('multistream', multistream)
118 stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
119 stream_type = get_test_param('stream_type', stream_type)
120 pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
121 pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
123 # check if test requires background load and which generator it uses
124 self._load_cfg = cfg.get('Load', None)
125 if self._load_cfg and 'tool' in self._load_cfg:
126 self._loadgen = self._load_cfg['tool']
128 # background load is not requested, so use dummy implementation
129 self._loadgen = "Dummy"
132 self._frame_mod = self._frame_mod.lower()
133 self._results_dir = S.getValue('RESULTS_PATH')
135 # set traffic details, so they can be passed to vswitch and traffic ctls
136 self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
137 self._traffic.update({'traffic_type': traffic_type,
138 'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
139 'bidir': bidirectional,
140 'tunnel_type': self._tunnel_type,
141 'multistream': int(multistream),
142 'stream_type': stream_type,
143 'pre_installed_flows' : pre_installed_flows,
144 'frame_rate': int(framerate)})
146 # Packet Forwarding mode
147 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
149 # trafficgen configuration required for tests of tunneling protocols
150 if self.deployment == "op2p":
151 self._traffic['l2'].update({'srcmac':
152 S.getValue('TRAFFICGEN_PORT1_MAC'),
154 S.getValue('TRAFFICGEN_PORT2_MAC')})
156 self._traffic['l3'].update({'srcip':
157 S.getValue('TRAFFICGEN_PORT1_IP'),
159 S.getValue('TRAFFICGEN_PORT2_IP')})
161 if self._tunnel_operation == "decapsulation":
162 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
163 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
164 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
165 elif S.getValue('NICS')[0]['type'] == 'vf' or S.getValue('NICS')[1]['type'] == 'vf':
166 mac1 = S.getValue('NICS')[0]['mac']
167 mac2 = S.getValue('NICS')[1]['mac']
169 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
171 self._logger.debug("MAC addresses can not be read")
173 def run_initialize(self):
174 """ Prepare test execution environment
176 self._logger.debug(self.name)
178 # mount hugepages if needed
179 self._mount_hugepages()
181 self._logger.debug("Controllers:")
183 self._traffic_ctl = component_factory.create_traffic(
184 self._traffic['traffic_type'],
185 loader.get_trafficgen_class())
187 self._vnf_ctl = component_factory.create_vnf(
189 loader.get_vnf_class())
191 # verify enough hugepages are free to run the testcase
192 if not self._check_for_enough_hugepages():
193 raise RuntimeError('Not enough hugepages free to run test.')
195 # perform guest related handling
196 if self._vnf_ctl.get_vnfs_number():
197 # copy sources of l2 forwarding tools into VM shared dir if needed
198 self._copy_fwd_tools_for_all_guests(self._vnf_ctl.get_vnfs_number())
200 # in case of multi VM in parallel, set the number of streams to the number of VMs
201 if self.deployment.startswith('pvpv'):
202 # for each VM NIC pair we need an unique stream
204 for vm_nic in S.getValue('GUEST_NICS_NR')[:self._vnf_ctl.get_vnfs_number()]:
205 streams += int(vm_nic / 2) if vm_nic > 1 else 1
206 self._logger.debug("VMs with parallel connection were detected. "
207 "Thus Number of streams was set to %s", streams)
208 self._traffic.update({'multistream': streams})
210 # OVS Vanilla requires guest VM MAC address and IPs to work
211 if 'linux_bridge' in S.getValue('GUEST_LOOPBACK'):
212 self._traffic['l2'].update({'srcmac': S.getValue('VANILLA_TGEN_PORT1_MAC'),
213 'dstmac': S.getValue('VANILLA_TGEN_PORT2_MAC')})
214 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
215 'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
217 if self._vswitch_none:
218 self._vswitch_ctl = component_factory.create_pktfwd(
220 loader.get_pktfwd_class())
222 self._vswitch_ctl = component_factory.create_vswitch(
224 loader.get_vswitch_class(),
226 self._tunnel_operation)
228 self._collector = component_factory.create_collector(
229 loader.get_collector_class(),
230 self._results_dir, self.name)
231 self._loadgen = component_factory.create_loadgen(
235 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
236 "_" + self.deployment + ".csv")
238 self._logger.debug("Setup:")
240 def run_finalize(self):
241 """ Tear down test execution environment and record test results
243 # umount hugepages if mounted
244 self._umount_hugepages()
246 # restore original settings
247 S.load_from_dict(self._settings_original)
249 # cleanup any namespaces created
250 if os.path.isdir('/tmp/namespaces'):
251 import tools.namespace
252 namespace_list = os.listdir('/tmp/namespaces')
253 if len(namespace_list):
254 self._logger.info('Cleaning up namespaces')
255 for name in namespace_list:
256 tools.namespace.delete_namespace(name)
257 os.rmdir('/tmp/namespaces')
258 # cleanup any veth ports created
259 if os.path.isdir('/tmp/veth'):
261 veth_list = os.listdir('/tmp/veth')
263 self._logger.info('Cleaning up veth ports')
264 for eth in veth_list:
265 port1, port2 = eth.split('-')
266 tools.veth.del_veth_port(port1, port2)
267 os.rmdir('/tmp/veth')
269 def run_report(self):
270 """ Report test results
272 self._logger.debug("self._collector Results:")
273 self._collector.print_results()
275 if S.getValue('mode') != 'trafficgen-off':
276 self._logger.debug("Traffic Results:")
277 self._traffic_ctl.print_results()
279 self._tc_results = self._append_results(self._traffic_ctl.get_results())
280 TestCase.write_result_to_file(self._tc_results, self._output_file)
285 All setup and teardown through controllers is included.
287 # prepare test execution environment
288 self.run_initialize()
290 with self._vswitch_ctl, self._loadgen:
291 with self._vnf_ctl, self._collector:
292 if not self._vswitch_none:
295 # run traffic generator if requested, otherwise wait for manual termination
296 if S.getValue('mode') == 'trafficgen-off':
298 self._logger.debug("All is set. Please run traffic generator manually.")
299 input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
301 if S.getValue('mode') == 'trafficgen-pause':
303 true_vals = ('yes', 'y', 'ye', None)
305 choice = input(os.linesep + 'Transmission paused, should'
306 ' transmission be resumed? ' + os.linesep).lower()
307 if not choice or choice not in true_vals:
308 print('Please respond with \'yes\' or \'y\' ', end='')
311 with self._traffic_ctl:
312 self._traffic_ctl.send_traffic(self._traffic)
314 # dump vswitch flows before they are affected by VNF termination
315 if not self._vswitch_none:
316 self._vswitch_ctl.dump_vswitch_flows()
318 # tear down test execution environment and log results
321 self._testcase_run_time = time.strftime("%H:%M:%S",
322 time.gmtime(time.time() - self._testcase_start_time))
323 logging.info("Testcase execution time: " + self._testcase_run_time)
324 # report test results
327 def _update_settings(self, param, value):
328 """ Check value of given configuration parameter
329 In case that new value is different, then testcase
330 specific settings is updated and original value stored
332 :param param: Name of parameter inside settings
333 :param value: Disired parameter value
335 orig_value = S.getValue(param)
336 if orig_value != value:
337 self._settings_original[param] = orig_value
338 S.setValue(param, value)
340 def _append_results(self, results):
342 Method appends mandatory Test Case results to list of dictionaries.
344 :param results: list of dictionaries which contains results from
347 :returns: modified list of dictionaries.
350 item[ResultsConstants.ID] = self.name
351 item[ResultsConstants.DEPLOYMENT] = self.deployment
352 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
353 item[ResultsConstants.TEST_RUN_TIME] = self._testcase_run_time
354 if self._traffic['multistream']:
355 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
356 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
357 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
358 if self._vnf_ctl.get_vnfs_number():
359 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(S.getValue('GUEST_LOOPBACK'))
360 if self._tunnel_type:
361 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
364 def _copy_fwd_tools_for_all_guests(self, vm_count):
365 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
367 # consider only VNFs involved in the test
368 for guest_dir in set(S.getValue('GUEST_SHARE_DIR')[:vm_count]):
369 self._copy_fwd_tools_for_guest(guest_dir)
371 def _copy_fwd_tools_for_guest(self, guest_dir):
372 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
374 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
376 # remove shared dir if it exists to avoid issues with file consistency
377 if os.path.exists(guest_dir):
378 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
379 'Removing content of shared directory...', True)
381 # directory to share files between host and guest
382 os.makedirs(guest_dir)
384 # copy sources into shared dir only if neccessary
385 guest_loopback = set(S.getValue('GUEST_LOOPBACK'))
386 if 'testpmd' in guest_loopback:
388 # exclude whole .git/ subdirectory and all o-files;
389 # It is assumed, that the same RTE_TARGET is used in both host
390 # and VMs; This simplification significantly speeds up testpmd
391 # build. If we will need a different RTE_TARGET in VM,
392 # then we have to build whole DPDK from the scratch in VM.
393 # In that case we can copy just DPDK sources (e.g. by excluding
394 # all items obtained by git status -unormal --porcelain).
395 # NOTE: Excluding RTE_TARGET directory won't help on systems,
396 # where DPDK is built for multiple targets (e.g. for gcc & icc)
398 exclude.append(r'--exclude=.git/')
399 exclude.append(r'--exclude=*.o')
400 tasks.run_task(['rsync', '-a', '-r', '-l'] + exclude +
401 [os.path.join(S.getValue('TOOLS')['dpdk_src'], ''),
402 os.path.join(guest_dir, 'DPDK')],
404 'Copying DPDK to shared directory...',
406 except subprocess.CalledProcessError:
407 self._logger.error('Unable to copy DPDK to shared directory')
409 if 'l2fwd' in guest_loopback:
411 tasks.run_task(['rsync', '-a', '-r', '-l',
412 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
413 os.path.join(guest_dir, 'l2fwd')],
415 'Copying l2fwd to shared directory...',
417 except subprocess.CalledProcessError:
418 self._logger.error('Unable to copy l2fwd to shared directory')
421 def _mount_hugepages(self):
422 """Mount hugepages if usage of DPDK or Qemu is detected
424 # hugepages are needed by DPDK and Qemu
425 if not self._hugepages_mounted and \
426 (self.deployment.count('v') or \
427 S.getValue('VSWITCH').lower().count('dpdk') or \
428 self._vswitch_none or \
429 self.test and 'vnf' in [step[0][0:3] for step in self.test]):
430 hugepages.mount_hugepages()
431 self._hugepages_mounted = True
433 def _umount_hugepages(self):
434 """Umount hugepages if they were mounted before
436 if self._hugepages_mounted:
437 hugepages.umount_hugepages()
438 self._hugepages_mounted = False
440 def _check_for_enough_hugepages(self):
441 """Check to make sure enough hugepages are free to satisfy the
445 hugepage_size = hugepages.get_hugepage_size()
446 # get hugepage amounts per guest involved in the test
447 for guest in range(self._vnf_ctl.get_vnfs_number()):
448 hugepages_needed += math.ceil((int(S.getValue(
449 'GUEST_MEMORY')[guest]) * 1000) / hugepage_size)
451 # get hugepage amounts for each socket on dpdk
452 sock0_mem, sock1_mem = 0, 0
453 if S.getValue('VSWITCH').lower().count('dpdk'):
454 # the import below needs to remain here and not put into the module
455 # imports because of an exception due to settings not yet loaded
456 from vswitches import ovs_dpdk_vhost
457 if ovs_dpdk_vhost.OvsDpdkVhost.old_dpdk_config():
459 r'-socket-mem\s+(\d+),(\d+)',
460 ''.join(S.getValue('VSWITCHD_DPDK_ARGS')))
462 sock0_mem, sock1_mem = (int(match.group(1)) * 1024 / hugepage_size,
463 int(match.group(2)) * 1024 / hugepage_size)
466 'Could not parse socket memory config in dpdk params.')
468 sock0_mem, sock1_mem = (
470 'VSWITCHD_DPDK_CONFIG')['dpdk-socket-mem'].split(','))
471 sock0_mem, sock1_mem = (int(sock0_mem) * 1024 / hugepage_size,
472 int(sock1_mem) * 1024 / hugepage_size)
474 # If hugepages needed, verify the amounts are free
475 if any([hugepages_needed, sock0_mem, sock1_mem]):
476 free_hugepages = hugepages.get_free_hugepages()
478 logging.info('Need %s hugepages free for guests',
480 result1 = free_hugepages >= hugepages_needed
481 free_hugepages -= hugepages_needed
486 logging.info('Need %s hugepages free for dpdk socket 0',
488 result2 = hugepages.get_free_hugepages('0') >= sock0_mem
489 free_hugepages -= sock0_mem
494 logging.info('Need %s hugepages free for dpdk socket 1',
496 result3 = hugepages.get_free_hugepages('1') >= sock1_mem
497 free_hugepages -= sock1_mem
501 logging.info('Need a total of {} total hugepages'.format(
502 hugepages_needed + sock1_mem + sock0_mem))
504 # The only drawback here is sometimes dpdk doesn't release
505 # its hugepages on a test failure. This could cause a test
506 # to fail when dpdk would be OK to start because it will just
507 # use the previously allocated hugepages.
508 result4 = True if free_hugepages >= 0 else False
510 return all([result1, result2, result3, result4])
515 def write_result_to_file(results, output):
516 """Write list of dictionaries to a CSV file.
518 Each element on list will create separate row in output file.
519 If output file already exists, data will be appended at the end,
520 otherwise it will be created.
522 :param results: list of dictionaries.
523 :param output: path to output file.
525 with open(output, 'a') as csvfile:
527 logging.info("Write results to file: " + output)
528 fieldnames = TestCase._get_unique_keys(results)
530 writer = csv.DictWriter(csvfile, fieldnames)
532 if not csvfile.tell(): # file is now empty
535 for result in results:
536 writer.writerow(result)
539 def _get_unique_keys(list_of_dicts):
540 """Gets unique key values as ordered list of strings in given dicts
542 :param list_of_dicts: list of dictionaries.
544 :returns: list of unique keys(strings).
546 result = OrderedDict()
547 for item in list_of_dicts:
548 for key in item.keys():
551 return list(result.keys())
553 def _add_flows(self):
554 """Add flows to the vswitch
556 vswitch = self._vswitch_ctl.get_vswitch()
557 # TODO BOM 15-08-07 the frame mod code assumes that the
558 # physical ports are ports 1 & 2. The actual numbers
559 # need to be retrived from the vSwitch and the metadata value
560 # updated accordingly.
561 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
562 if self._frame_mod == "vlan":
563 # 0x8100 => VLAN ethertype
564 self._logger.debug(" **** VLAN ***** ")
565 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
566 'actions': ['push_vlan:0x8100', 'goto_table:3']}
567 vswitch.add_flow(bridge, flow)
568 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
569 'actions': ['push_vlan:0x8100', 'goto_table:3']}
570 vswitch.add_flow(bridge, flow)
571 elif self._frame_mod == "mpls":
572 # 0x8847 => MPLS unicast ethertype
573 self._logger.debug(" **** MPLS ***** ")
574 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
575 'actions': ['push_mpls:0x8847', 'goto_table:3']}
576 vswitch.add_flow(bridge, flow)
577 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
578 'actions': ['push_mpls:0x8847', 'goto_table:3']}
579 vswitch.add_flow(bridge, flow)
580 elif self._frame_mod == "mac":
581 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
582 'actions': ['mod_dl_src:22:22:22:22:22:22',
584 vswitch.add_flow(bridge, flow)
585 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
586 'actions': ['mod_dl_src:11:11:11:11:11:11',
588 vswitch.add_flow(bridge, flow)
589 elif self._frame_mod == "dscp":
590 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
591 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
593 'actions': ['mod_nw_tos:184', 'goto_table:3']}
594 vswitch.add_flow(bridge, flow)
595 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
597 'actions': ['mod_nw_tos:184', 'goto_table:3']}
598 vswitch.add_flow(bridge, flow)
599 elif self._frame_mod == "ttl":
600 # 251 and 241 are the highest prime numbers < 255
601 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
603 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
604 vswitch.add_flow(bridge, flow)
605 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
607 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
608 vswitch.add_flow(bridge, flow)
609 elif self._frame_mod == "ip_addr":
610 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
612 'actions': ['mod_nw_src:10.10.10.10',
613 'mod_nw_dst:20.20.20.20',
615 vswitch.add_flow(bridge, flow)
616 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
618 'actions': ['mod_nw_src:20.20.20.20',
619 'mod_nw_dst:10.10.10.10',
621 vswitch.add_flow(bridge, flow)
622 elif self._frame_mod == "ip_port":
623 # TODO BOM 15-08-27 The traffic generated is assumed
624 # to be UDP (nw_proto 17d) which is the default case but
625 # we will need to pick up the actual traffic params in use.
626 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
627 'dl_type':'0x0800', 'nw_proto':'17',
628 'actions': ['mod_tp_src:44444',
629 'mod_tp_dst:44444', 'goto_table:3']}
630 vswitch.add_flow(bridge, flow)
631 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
632 'dl_type':'0x0800', 'nw_proto':'17',
633 'actions': ['mod_tp_src:44444',
634 'mod_tp_dst:44444', 'goto_table:3']}
635 vswitch.add_flow(bridge, flow)