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.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
31 from conf import settings as S
32 from conf import get_test_param
34 class TestCase(object):
35 """TestCase base class
37 In this basic form runs RFC2544 throughput test
39 def __init__(self, cfg):
40 """Pull out fields from test config
42 :param cfg: A dictionary of string-value pairs describing the test
43 configuration. Both the key and values strings use well-known
45 :param results_dir: Where the csv formatted results are written.
47 self._hugepages_mounted = False
48 self._traffic_ctl = None
50 self._vswitch_ctl = None
51 self._collector = None
53 self._output_file = None
54 self._tc_results = None
56 # set test parameters; CLI options take precedence to testcase settings
57 self._logger = logging.getLogger(__name__)
58 self.name = cfg['Name']
59 self.desc = cfg.get('Description', 'No description given.')
60 self.test = cfg.get('TestSteps', None)
62 bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
63 bidirectional = get_test_param('bidirectional', bidirectional)
65 traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
66 traffic_type = get_test_param('traffic_type', traffic_type)
68 framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
69 framerate = get_test_param('iload', framerate)
71 self.deployment = cfg['Deployment']
72 self._frame_mod = cfg.get('Frame Modification', None)
74 self._tunnel_type = None
75 self._tunnel_operation = None
77 if self.deployment == 'op2p':
78 self._tunnel_operation = cfg['Tunnel Operation']
80 if 'Tunnel Type' in cfg:
81 self._tunnel_type = cfg['Tunnel Type']
82 self._tunnel_type = get_test_param('tunnel_type',
86 # identify guest loopback method, so it can be added into reports
87 self.guest_loopback = []
88 if self.deployment in ['pvp', 'pvvp']:
89 guest_loopback = get_test_param('guest_loopback', None)
91 self.guest_loopback.append(guest_loopback)
93 if self.deployment == 'pvp':
94 self.guest_loopback.append(S.getValue('GUEST_LOOPBACK')[0])
96 self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
98 # read configuration of streams; CLI parameter takes precedence to
100 multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
101 multistream = get_test_param('multistream', multistream)
102 stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
103 stream_type = get_test_param('stream_type', stream_type)
104 pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
105 pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
107 # check if test requires background load and which generator it uses
108 self._load_cfg = cfg.get('Load', None)
109 if self._load_cfg and 'tool' in self._load_cfg:
110 self._loadgen = self._load_cfg['tool']
112 # background load is not requested, so use dummy implementation
113 self._loadgen = "Dummy"
116 self._frame_mod = self._frame_mod.lower()
117 self._results_dir = S.getValue('RESULTS_PATH')
119 # set traffic details, so they can be passed to vswitch and traffic ctls
120 self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
121 self._traffic.update({'traffic_type': traffic_type,
122 'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
123 'bidir': bidirectional,
124 'tunnel_type': self._tunnel_type,
125 'multistream': int(multistream),
126 'stream_type': stream_type,
127 'pre_installed_flows' : pre_installed_flows,
128 'frame_rate': int(framerate)})
130 # OVS Vanilla requires guest VM MAC address and IPs to work
131 if 'linux_bridge' in self.guest_loopback:
132 self._traffic['l2'].update({'srcmac': S.getValue('GUEST_NET2_MAC')[0],
133 'dstmac': S.getValue('GUEST_NET1_MAC')[0]})
134 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
135 'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
137 # Packet Forwarding mode
138 self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
140 def run_initialize(self):
141 """ Prepare test execution environment
143 self._logger.debug(self.name)
145 # mount hugepages if needed
146 self._mount_hugepages()
148 # copy sources of l2 forwarding tools into VM shared dir if needed
149 self._copy_fwd_tools_for_guest()
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")
176 self._logger.debug("Controllers:")
178 self._traffic_ctl = component_factory.create_traffic(
179 self._traffic['traffic_type'],
180 loader.get_trafficgen_class())
182 self._vnf_ctl = component_factory.create_vnf(
184 loader.get_vnf_class())
186 if self._vswitch_none:
187 self._vswitch_ctl = component_factory.create_pktfwd(
189 loader.get_pktfwd_class())
191 self._vswitch_ctl = component_factory.create_vswitch(
193 loader.get_vswitch_class(),
195 self._tunnel_operation)
197 self._collector = component_factory.create_collector(
198 loader.get_collector_class(),
199 self._results_dir, self.name)
200 self._loadgen = component_factory.create_loadgen(
204 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
205 "_" + self.deployment + ".csv")
207 self._logger.debug("Setup:")
209 def run_finalize(self):
210 """ Tear down test execution environment and record test results
212 # umount hugepages if mounted
213 self._umount_hugepages()
215 def run_report(self):
216 """ Report test results
218 self._logger.debug("self._collector Results:")
219 self._collector.print_results()
221 if S.getValue('mode') != 'trafficgen-off':
222 self._logger.debug("Traffic Results:")
223 self._traffic_ctl.print_results()
225 self._tc_results = self._append_results(self._traffic_ctl.get_results())
226 TestCase._write_result_to_file(self._tc_results, self._output_file)
231 All setup and teardown through controllers is included.
233 # prepare test execution environment
234 self.run_initialize()
236 with self._vswitch_ctl, self._loadgen:
237 with self._vnf_ctl, self._collector:
238 if not self._vswitch_none:
241 # run traffic generator if requested, otherwise wait for manual termination
242 if S.getValue('mode') == 'trafficgen-off':
244 self._logger.debug("All is set. Please run traffic generator manually.")
245 input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
247 if S.getValue('mode') == 'trafficgen-pause':
249 true_vals = ('yes', 'y', 'ye', None)
251 choice = input(os.linesep + 'Transmission paused, should'
252 ' transmission be resumed? ' + os.linesep).lower()
253 if not choice or choice not in true_vals:
254 print('Please respond with \'yes\' or \'y\' ', end='')
257 with self._traffic_ctl:
258 self._traffic_ctl.send_traffic(self._traffic)
260 # dump vswitch flows before they are affected by VNF termination
261 if not self._vswitch_none:
262 self._vswitch_ctl.dump_vswitch_flows()
264 # tear down test execution environment and log results
267 # report test results
270 def _append_results(self, results):
272 Method appends mandatory Test Case results to list of dictionaries.
274 :param results: list of dictionaries which contains results from
277 :returns: modified list of dictionaries.
280 item[ResultsConstants.ID] = self.name
281 item[ResultsConstants.DEPLOYMENT] = self.deployment
282 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
283 if self._traffic['multistream']:
284 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
285 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
286 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
287 if len(self.guest_loopback):
288 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
289 if self._tunnel_type:
290 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
293 def _copy_fwd_tools_for_guest(self):
294 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
297 # method is executed only for pvp and pvvp, so let's count number of 'v'
298 while counter < self.deployment.count('v'):
299 guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
301 # remove shared dir if it exists to avoid issues with file consistency
302 if os.path.exists(guest_dir):
303 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
304 'Removing content of shared directory...', True)
306 # directory to share files between host and guest
307 os.makedirs(guest_dir)
309 # copy sources into shared dir only if neccessary
310 if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
312 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
313 os.path.join(S.getValue('RTE_SDK'), ''),
314 os.path.join(guest_dir, 'DPDK')],
316 'Copying DPDK to shared directory...',
318 tasks.run_task(['rsync', '-a', '-r', '-l',
319 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
320 os.path.join(guest_dir, 'l2fwd')],
322 'Copying l2fwd to shared directory...',
324 except subprocess.CalledProcessError:
325 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
329 def _mount_hugepages(self):
330 """Mount hugepages if usage of DPDK or Qemu is detected
332 # hugepages are needed by DPDK and Qemu
333 if not self._hugepages_mounted and \
334 (self.deployment.count('v') or \
335 S.getValue('VSWITCH').lower().count('dpdk') or \
337 hugepages.mount_hugepages()
338 self._hugepages_mounted = True
340 def _umount_hugepages(self):
341 """Umount hugepages if they were mounted before
343 if self._hugepages_mounted:
344 hugepages.umount_hugepages()
345 self._hugepages_mounted = False
348 def _write_result_to_file(results, output):
349 """Write list of dictionaries to a CSV file.
351 Each element on list will create separate row in output file.
352 If output file already exists, data will be appended at the end,
353 otherwise it will be created.
355 :param results: list of dictionaries.
356 :param output: path to output file.
358 with open(output, 'a') as csvfile:
360 logging.info("Write results to file: " + output)
361 fieldnames = TestCase._get_unique_keys(results)
363 writer = csv.DictWriter(csvfile, fieldnames)
365 if not csvfile.tell(): # file is now empty
368 for result in results:
369 writer.writerow(result)
372 def _get_unique_keys(list_of_dicts):
373 """Gets unique key values as ordered list of strings in given dicts
375 :param list_of_dicts: list of dictionaries.
377 :returns: list of unique keys(strings).
379 result = OrderedDict()
380 for item in list_of_dicts:
381 for key in item.keys():
384 return list(result.keys())
386 def _add_flows(self):
387 """Add flows to the vswitch
389 vswitch = self._vswitch_ctl.get_vswitch()
390 # TODO BOM 15-08-07 the frame mod code assumes that the
391 # physical ports are ports 1 & 2. The actual numbers
392 # need to be retrived from the vSwitch and the metadata value
393 # updated accordingly.
394 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
395 if self._frame_mod == "vlan":
396 # 0x8100 => VLAN ethertype
397 self._logger.debug(" **** VLAN ***** ")
398 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
399 'actions': ['push_vlan:0x8100', 'goto_table:3']}
400 vswitch.add_flow(bridge, flow)
401 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
402 'actions': ['push_vlan:0x8100', 'goto_table:3']}
403 vswitch.add_flow(bridge, flow)
404 elif self._frame_mod == "mpls":
405 # 0x8847 => MPLS unicast ethertype
406 self._logger.debug(" **** MPLS ***** ")
407 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
408 'actions': ['push_mpls:0x8847', 'goto_table:3']}
409 vswitch.add_flow(bridge, flow)
410 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
411 'actions': ['push_mpls:0x8847', 'goto_table:3']}
412 vswitch.add_flow(bridge, flow)
413 elif self._frame_mod == "mac":
414 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
415 'actions': ['mod_dl_src:22:22:22:22:22:22',
417 vswitch.add_flow(bridge, flow)
418 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
419 'actions': ['mod_dl_src:11:11:11:11:11:11',
421 vswitch.add_flow(bridge, flow)
422 elif self._frame_mod == "dscp":
423 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
424 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
426 'actions': ['mod_nw_tos:184', 'goto_table:3']}
427 vswitch.add_flow(bridge, flow)
428 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
430 'actions': ['mod_nw_tos:184', 'goto_table:3']}
431 vswitch.add_flow(bridge, flow)
432 elif self._frame_mod == "ttl":
433 # 251 and 241 are the highest prime numbers < 255
434 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
436 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
437 vswitch.add_flow(bridge, flow)
438 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
440 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
441 vswitch.add_flow(bridge, flow)
442 elif self._frame_mod == "ip_addr":
443 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
445 'actions': ['mod_nw_src:10.10.10.10',
446 'mod_nw_dst:20.20.20.20',
448 vswitch.add_flow(bridge, flow)
449 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
451 'actions': ['mod_nw_src:20.20.20.20',
452 'mod_nw_dst:10.10.10.10',
454 vswitch.add_flow(bridge, flow)
455 elif self._frame_mod == "ip_port":
456 # TODO BOM 15-08-27 The traffic generated is assumed
457 # to be UDP (nw_proto 17d) which is the default case but
458 # we will need to pick up the actual traffic params in use.
459 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
460 'dl_type':'0x0800', 'nw_proto':'17',
461 'actions': ['mod_tp_src:44444',
462 'mod_tp_dst:44444', 'goto_table:3']}
463 vswitch.add_flow(bridge, flow)
464 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
465 'dl_type':'0x0800', 'nw_proto':'17',
466 'actions': ['mod_tp_src:44444',
467 'mod_tp_dst:44444', 'goto_table:3']}
468 vswitch.add_flow(bridge, flow)