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)
86 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
87 traffic_type = get_test_param('traffic_type', traffic_type)
89 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
90 framerate = get_test_param('iload', framerate)
92 self.deployment = cfg['Deployment']
93 self._frame_mod = cfg.get('Frame Modification', None)
95 self._tunnel_type = None
96 self._tunnel_operation = None
98 if self.deployment == 'op2p':
99 self._tunnel_operation = cfg['Tunnel Operation']
101 if 'Tunnel Type' in cfg:
102 self._tunnel_type = cfg['Tunnel Type']
103 self._tunnel_type = get_test_param('tunnel_type',
106 # identify guest loopback method, so it can be added into reports
107 if self.deployment == 'pvp':
108 self.guest_loopback.append(S.getValue('GUEST_LOOPBACK')[0])
110 self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
112 # read configuration of streams; CLI parameter takes precedence to
113 # testcase definition
114 multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
115 multistream = get_test_param('multistream', multistream)
116 stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
117 stream_type = get_test_param('stream_type', stream_type)
118 pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
119 pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
121 # check if test requires background load and which generator it uses
122 self._load_cfg = cfg.get('Load', None)
123 if self._load_cfg and 'tool' in self._load_cfg:
124 self._loadgen = self._load_cfg['tool']
126 # background load is not requested, so use dummy implementation
127 self._loadgen = "Dummy"
130 self._frame_mod = self._frame_mod.lower()
131 self._results_dir = S.getValue('RESULTS_PATH')
133 # set traffic details, so they can be passed to vswitch and traffic ctls
134 self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
135 self._traffic.update({'traffic_type': traffic_type,
136 'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
137 'bidir': bidirectional,
138 'tunnel_type': self._tunnel_type,
139 'multistream': int(multistream),
140 'stream_type': stream_type,
141 'pre_installed_flows' : pre_installed_flows,
142 'frame_rate': int(framerate)})
144 # Packet Forwarding mode
145 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
147 # OVS Vanilla requires guest VM MAC address and IPs to work
148 if 'linux_bridge' in self.guest_loopback:
149 self._traffic['l2'].update({'srcmac': S.getValue('GUEST_NET2_MAC')[0],
150 'dstmac': S.getValue('GUEST_NET1_MAC')[0]})
151 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
152 'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
154 # trafficgen configuration required for tests of tunneling protocols
155 if self.deployment == "op2p":
156 self._traffic['l2'].update({'srcmac':
157 S.getValue('TRAFFICGEN_PORT1_MAC'),
159 S.getValue('TRAFFICGEN_PORT2_MAC')})
161 self._traffic['l3'].update({'srcip':
162 S.getValue('TRAFFICGEN_PORT1_IP'),
164 S.getValue('TRAFFICGEN_PORT2_IP')})
166 if self._tunnel_operation == "decapsulation":
167 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
168 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
169 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
170 elif S.getValue('NICS')[0]['type'] == 'vf' or S.getValue('NICS')[1]['type'] == 'vf':
171 mac1 = S.getValue('NICS')[0]['mac']
172 mac2 = S.getValue('NICS')[1]['mac']
174 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
176 self._logger.debug("MAC addresses can not be read")
178 def run_initialize(self):
179 """ Prepare test execution environment
181 self._logger.debug(self.name)
183 # mount hugepages if needed
184 self._mount_hugepages()
186 # copy sources of l2 forwarding tools into VM shared dir if needed
187 self._copy_fwd_tools_for_all_guests()
189 self._logger.debug("Controllers:")
191 self._traffic_ctl = component_factory.create_traffic(
192 self._traffic['traffic_type'],
193 loader.get_trafficgen_class())
195 self._vnf_ctl = component_factory.create_vnf(
197 loader.get_vnf_class())
199 if self._vswitch_none:
200 self._vswitch_ctl = component_factory.create_pktfwd(
202 loader.get_pktfwd_class())
204 self._vswitch_ctl = component_factory.create_vswitch(
206 loader.get_vswitch_class(),
208 self._tunnel_operation)
210 self._collector = component_factory.create_collector(
211 loader.get_collector_class(),
212 self._results_dir, self.name)
213 self._loadgen = component_factory.create_loadgen(
217 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
218 "_" + self.deployment + ".csv")
220 self._logger.debug("Setup:")
222 def run_finalize(self):
223 """ Tear down test execution environment and record test results
225 # umount hugepages if mounted
226 self._umount_hugepages()
228 # restore original settings
229 S.load_from_dict(self._settings_original)
231 def run_report(self):
232 """ Report test results
234 self._logger.debug("self._collector Results:")
235 self._collector.print_results()
237 if S.getValue('mode') != 'trafficgen-off':
238 self._logger.debug("Traffic Results:")
239 self._traffic_ctl.print_results()
241 self._tc_results = self._append_results(self._traffic_ctl.get_results())
242 TestCase._write_result_to_file(self._tc_results, self._output_file)
247 All setup and teardown through controllers is included.
249 # prepare test execution environment
250 self.run_initialize()
252 with self._vswitch_ctl, self._loadgen:
253 with self._vnf_ctl, self._collector:
254 if not self._vswitch_none:
257 # run traffic generator if requested, otherwise wait for manual termination
258 if S.getValue('mode') == 'trafficgen-off':
260 self._logger.debug("All is set. Please run traffic generator manually.")
261 input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
263 if S.getValue('mode') == 'trafficgen-pause':
265 true_vals = ('yes', 'y', 'ye', None)
267 choice = input(os.linesep + 'Transmission paused, should'
268 ' transmission be resumed? ' + os.linesep).lower()
269 if not choice or choice not in true_vals:
270 print('Please respond with \'yes\' or \'y\' ', end='')
273 with self._traffic_ctl:
274 self._traffic_ctl.send_traffic(self._traffic)
276 # dump vswitch flows before they are affected by VNF termination
277 if not self._vswitch_none:
278 self._vswitch_ctl.dump_vswitch_flows()
280 # tear down test execution environment and log results
283 # report test results
286 def _update_settings(self, param, value):
287 """ Check value of given configuration parameter
288 In case that new value is different, then testcase
289 specific settings is updated and original value stored
291 :param param: Name of parameter inside settings
292 :param value: Disired parameter value
294 orig_value = S.getValue(param)
295 if orig_value != value:
296 self._settings_original[param] = orig_value
297 S.setValue(param, value)
299 def _append_results(self, results):
301 Method appends mandatory Test Case results to list of dictionaries.
303 :param results: list of dictionaries which contains results from
306 :returns: modified list of dictionaries.
309 item[ResultsConstants.ID] = self.name
310 item[ResultsConstants.DEPLOYMENT] = self.deployment
311 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
312 if self._traffic['multistream']:
313 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
314 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
315 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
316 if self.deployment in ['pvp', 'pvvp'] and len(self.guest_loopback):
317 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
318 if self._tunnel_type:
319 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
322 def _copy_fwd_tools_for_all_guests(self):
323 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
325 # data are copied only for pvp and pvvp, so let's count number of 'v'
327 while counter <= self.deployment.count('v'):
328 self._copy_fwd_tools_for_guest(counter)
331 def _copy_fwd_tools_for_guest(self, index):
332 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
334 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
336 guest_dir = S.getValue('GUEST_SHARE_DIR')[index-1]
338 # remove shared dir if it exists to avoid issues with file consistency
339 if os.path.exists(guest_dir):
340 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
341 'Removing content of shared directory...', True)
343 # directory to share files between host and guest
344 os.makedirs(guest_dir)
346 # copy sources into shared dir only if neccessary
347 if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
349 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
350 os.path.join(S.getValue('RTE_SDK'), ''),
351 os.path.join(guest_dir, 'DPDK')],
353 'Copying DPDK to shared directory...',
355 tasks.run_task(['rsync', '-a', '-r', '-l',
356 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
357 os.path.join(guest_dir, 'l2fwd')],
359 'Copying l2fwd to shared directory...',
361 except subprocess.CalledProcessError:
362 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
365 def _mount_hugepages(self):
366 """Mount hugepages if usage of DPDK or Qemu is detected
368 # hugepages are needed by DPDK and Qemu
369 if not self._hugepages_mounted and \
370 (self.deployment.count('v') or \
371 S.getValue('VSWITCH').lower().count('dpdk') or \
372 self._vswitch_none or \
373 self.test and 'vnf' in [step[0][0:3] for step in self.test]):
374 hugepages.mount_hugepages()
375 self._hugepages_mounted = True
377 def _umount_hugepages(self):
378 """Umount hugepages if they were mounted before
380 if self._hugepages_mounted:
381 hugepages.umount_hugepages()
382 self._hugepages_mounted = False
385 def _write_result_to_file(results, output):
386 """Write list of dictionaries to a CSV file.
388 Each element on list will create separate row in output file.
389 If output file already exists, data will be appended at the end,
390 otherwise it will be created.
392 :param results: list of dictionaries.
393 :param output: path to output file.
395 with open(output, 'a') as csvfile:
397 logging.info("Write results to file: " + output)
398 fieldnames = TestCase._get_unique_keys(results)
400 writer = csv.DictWriter(csvfile, fieldnames)
402 if not csvfile.tell(): # file is now empty
405 for result in results:
406 writer.writerow(result)
409 def _get_unique_keys(list_of_dicts):
410 """Gets unique key values as ordered list of strings in given dicts
412 :param list_of_dicts: list of dictionaries.
414 :returns: list of unique keys(strings).
416 result = OrderedDict()
417 for item in list_of_dicts:
418 for key in item.keys():
421 return list(result.keys())
423 def _add_flows(self):
424 """Add flows to the vswitch
426 vswitch = self._vswitch_ctl.get_vswitch()
427 # TODO BOM 15-08-07 the frame mod code assumes that the
428 # physical ports are ports 1 & 2. The actual numbers
429 # need to be retrived from the vSwitch and the metadata value
430 # updated accordingly.
431 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
432 if self._frame_mod == "vlan":
433 # 0x8100 => VLAN ethertype
434 self._logger.debug(" **** VLAN ***** ")
435 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
436 'actions': ['push_vlan:0x8100', 'goto_table:3']}
437 vswitch.add_flow(bridge, flow)
438 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
439 'actions': ['push_vlan:0x8100', 'goto_table:3']}
440 vswitch.add_flow(bridge, flow)
441 elif self._frame_mod == "mpls":
442 # 0x8847 => MPLS unicast ethertype
443 self._logger.debug(" **** MPLS ***** ")
444 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
445 'actions': ['push_mpls:0x8847', 'goto_table:3']}
446 vswitch.add_flow(bridge, flow)
447 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
448 'actions': ['push_mpls:0x8847', 'goto_table:3']}
449 vswitch.add_flow(bridge, flow)
450 elif self._frame_mod == "mac":
451 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
452 'actions': ['mod_dl_src:22:22:22:22:22:22',
454 vswitch.add_flow(bridge, flow)
455 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
456 'actions': ['mod_dl_src:11:11:11:11:11:11',
458 vswitch.add_flow(bridge, flow)
459 elif self._frame_mod == "dscp":
460 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
461 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
463 'actions': ['mod_nw_tos:184', 'goto_table:3']}
464 vswitch.add_flow(bridge, flow)
465 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
467 'actions': ['mod_nw_tos:184', 'goto_table:3']}
468 vswitch.add_flow(bridge, flow)
469 elif self._frame_mod == "ttl":
470 # 251 and 241 are the highest prime numbers < 255
471 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
473 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
474 vswitch.add_flow(bridge, flow)
475 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
477 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
478 vswitch.add_flow(bridge, flow)
479 elif self._frame_mod == "ip_addr":
480 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
482 'actions': ['mod_nw_src:10.10.10.10',
483 'mod_nw_dst:20.20.20.20',
485 vswitch.add_flow(bridge, flow)
486 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
488 'actions': ['mod_nw_src:20.20.20.20',
489 'mod_nw_dst:10.10.10.10',
491 vswitch.add_flow(bridge, flow)
492 elif self._frame_mod == "ip_port":
493 # TODO BOM 15-08-27 The traffic generated is assumed
494 # to be UDP (nw_proto 17d) which is the default case but
495 # we will need to pick up the actual traffic params in use.
496 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
497 'dl_type':'0x0800', 'nw_proto':'17',
498 'actions': ['mod_tp_src:44444',
499 'mod_tp_dst:44444', 'goto_table:3']}
500 vswitch.add_flow(bridge, flow)
501 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
502 'dl_type':'0x0800', 'nw_proto':'17',
503 'actions': ['mod_tp_src:44444',
504 'mod_tp_dst:44444', 'goto_table:3']}
505 vswitch.add_flow(bridge, flow)