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, results_dir):
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 = results_dir
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')
168 self._logger.debug("Controllers:")
170 self._traffic_ctl = component_factory.create_traffic(
171 self._traffic['traffic_type'],
172 loader.get_trafficgen_class())
174 self._vnf_ctl = component_factory.create_vnf(
176 loader.get_vnf_class())
178 if self._vswitch_none:
179 self._vswitch_ctl = component_factory.create_pktfwd(
180 loader.get_pktfwd_class())
182 self._vswitch_ctl = component_factory.create_vswitch(
184 loader.get_vswitch_class(),
186 self._tunnel_operation)
188 self._collector = component_factory.create_collector(
189 loader.get_collector_class(),
190 self._results_dir, self.name)
191 self._loadgen = component_factory.create_loadgen(
195 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
196 "_" + self.deployment + ".csv")
198 self._logger.debug("Setup:")
200 def run_finalize(self):
201 """ Tear down test execution environment and record test results
203 # umount hugepages if mounted
204 self._umount_hugepages()
206 def run_report(self):
207 """ Report test results
209 self._logger.debug("self._collector Results:")
210 self._collector.print_results()
212 if S.getValue('mode') != 'trafficgen-off':
213 self._logger.debug("Traffic Results:")
214 self._traffic_ctl.print_results()
216 self._tc_results = self._append_results(self._traffic_ctl.get_results())
217 TestCase._write_result_to_file(self._tc_results, self._output_file)
222 All setup and teardown through controllers is included.
224 # prepare test execution environment
225 self.run_initialize()
227 with self._vswitch_ctl, self._loadgen:
228 with self._vnf_ctl, self._collector:
229 if not self._vswitch_none:
232 # run traffic generator if requested, otherwise wait for manual termination
233 if S.getValue('mode') == 'trafficgen-off':
235 self._logger.debug("All is set. Please run traffic generator manually.")
236 input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
238 if S.getValue('mode') == 'trafficgen-pause':
240 true_vals = ('yes', 'y', 'ye', None)
242 choice = input(os.linesep + 'Transmission paused, should'
243 ' transmission be resumed? ' + os.linesep).lower()
244 if not choice or choice not in true_vals:
245 print('Please respond with \'yes\' or \'y\' ', end='')
248 with self._traffic_ctl:
249 self._traffic_ctl.send_traffic(self._traffic)
251 # dump vswitch flows before they are affected by VNF termination
252 if not self._vswitch_none:
253 self._vswitch_ctl.dump_vswitch_flows()
255 # tear down test execution environment and log results
258 # report test results
261 def _append_results(self, results):
263 Method appends mandatory Test Case results to list of dictionaries.
265 :param results: list of dictionaries which contains results from
268 :returns: modified list of dictionaries.
271 item[ResultsConstants.ID] = self.name
272 item[ResultsConstants.DEPLOYMENT] = self.deployment
273 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
274 if self._traffic['multistream']:
275 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
276 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
277 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
278 if len(self.guest_loopback):
279 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
280 if self._tunnel_type:
281 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
284 def _copy_fwd_tools_for_guest(self):
285 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
288 # method is executed only for pvp and pvvp, so let's count number of 'v'
289 while counter < self.deployment.count('v'):
290 guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
292 # remove shared dir if it exists to avoid issues with file consistency
293 if os.path.exists(guest_dir):
294 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
295 'Removing content of shared directory...', True)
297 # directory to share files between host and guest
298 os.makedirs(guest_dir)
300 # copy sources into shared dir only if neccessary
301 if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
303 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
304 os.path.join(S.getValue('RTE_SDK'), ''),
305 os.path.join(guest_dir, 'DPDK')],
307 'Copying DPDK to shared directory...',
309 tasks.run_task(['rsync', '-a', '-r', '-l',
310 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
311 os.path.join(guest_dir, 'l2fwd')],
313 'Copying l2fwd to shared directory...',
315 except subprocess.CalledProcessError:
316 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
320 def _mount_hugepages(self):
321 """Mount hugepages if usage of DPDK or Qemu is detected
323 # hugepages are needed by DPDK and Qemu
324 if not self._hugepages_mounted and \
325 (self.deployment.count('v') or \
326 S.getValue('VSWITCH').lower().count('dpdk') or \
328 hugepages.mount_hugepages()
329 self._hugepages_mounted = True
331 def _umount_hugepages(self):
332 """Umount hugepages if they were mounted before
334 if self._hugepages_mounted:
335 hugepages.umount_hugepages()
336 self._hugepages_mounted = False
339 def _write_result_to_file(results, output):
340 """Write list of dictionaries to a CSV file.
342 Each element on list will create separate row in output file.
343 If output file already exists, data will be appended at the end,
344 otherwise it will be created.
346 :param results: list of dictionaries.
347 :param output: path to output file.
349 with open(output, 'a') as csvfile:
351 logging.info("Write results to file: " + output)
352 fieldnames = TestCase._get_unique_keys(results)
354 writer = csv.DictWriter(csvfile, fieldnames)
356 if not csvfile.tell(): # file is now empty
359 for result in results:
360 writer.writerow(result)
363 def _get_unique_keys(list_of_dicts):
364 """Gets unique key values as ordered list of strings in given dicts
366 :param list_of_dicts: list of dictionaries.
368 :returns: list of unique keys(strings).
370 result = OrderedDict()
371 for item in list_of_dicts:
372 for key in item.keys():
375 return list(result.keys())
377 def _add_flows(self):
378 """Add flows to the vswitch
380 vswitch = self._vswitch_ctl.get_vswitch()
381 # TODO BOM 15-08-07 the frame mod code assumes that the
382 # physical ports are ports 1 & 2. The actual numbers
383 # need to be retrived from the vSwitch and the metadata value
384 # updated accordingly.
385 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
386 if self._frame_mod == "vlan":
387 # 0x8100 => VLAN ethertype
388 self._logger.debug(" **** VLAN ***** ")
389 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
390 'actions': ['push_vlan:0x8100', 'goto_table:3']}
391 vswitch.add_flow(bridge, flow)
392 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
393 'actions': ['push_vlan:0x8100', 'goto_table:3']}
394 vswitch.add_flow(bridge, flow)
395 elif self._frame_mod == "mpls":
396 # 0x8847 => MPLS unicast ethertype
397 self._logger.debug(" **** MPLS ***** ")
398 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
399 'actions': ['push_mpls:0x8847', 'goto_table:3']}
400 vswitch.add_flow(bridge, flow)
401 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
402 'actions': ['push_mpls:0x8847', 'goto_table:3']}
403 vswitch.add_flow(bridge, flow)
404 elif self._frame_mod == "mac":
405 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
406 'actions': ['mod_dl_src:22:22:22:22:22:22',
408 vswitch.add_flow(bridge, flow)
409 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
410 'actions': ['mod_dl_src:11:11:11:11:11:11',
412 vswitch.add_flow(bridge, flow)
413 elif self._frame_mod == "dscp":
414 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
415 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
417 'actions': ['mod_nw_tos:184', 'goto_table:3']}
418 vswitch.add_flow(bridge, flow)
419 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
421 'actions': ['mod_nw_tos:184', 'goto_table:3']}
422 vswitch.add_flow(bridge, flow)
423 elif self._frame_mod == "ttl":
424 # 251 and 241 are the highest prime numbers < 255
425 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
427 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
428 vswitch.add_flow(bridge, flow)
429 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
431 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
432 vswitch.add_flow(bridge, flow)
433 elif self._frame_mod == "ip_addr":
434 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
436 'actions': ['mod_nw_src:10.10.10.10',
437 'mod_nw_dst:20.20.20.20',
439 vswitch.add_flow(bridge, flow)
440 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
442 'actions': ['mod_nw_src:20.20.20.20',
443 'mod_nw_dst:10.10.10.10',
445 vswitch.add_flow(bridge, flow)
446 elif self._frame_mod == "ip_port":
447 # TODO BOM 15-08-27 The traffic generated is assumed
448 # to be UDP (nw_proto 17d) which is the default case but
449 # we will need to pick up the actual traffic params in use.
450 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
451 'dl_type':'0x0800', 'nw_proto':'17',
452 'actions': ['mod_tp_src:44444',
453 'mod_tp_dst:44444', 'goto_table:3']}
454 vswitch.add_flow(bridge, flow)
455 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
456 'dl_type':'0x0800', 'nw_proto':'17',
457 'actions': ['mod_tp_src:44444',
458 'mod_tp_dst:44444', 'goto_table:3']}
459 vswitch.add_flow(bridge, flow)