ci: Decrease hugepages size for VERIFY&MERGE jobs
[vswitchperf.git] / testcases / testcase.py
1 # Copyright 2015-2016 Intel Corporation.
2 #
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
6 #
7 #   http://www.apache.org/licenses/LICENSE-2.0
8 #
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
15 """
16
17 from collections import OrderedDict
18 import copy
19 import csv
20 import logging
21 import math
22 import os
23 import re
24 import time
25 import subprocess
26
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 import namespace
36 from tools import veth
37 from tools.teststepstools import TestStepsTools
38 from tools.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
39
40 CHECK_PREFIX = 'validate_'
41
42 class TestCase(object):
43     """TestCase base class
44
45     In this basic form runs RFC2544 throughput test
46     """
47     def __init__(self, cfg):
48         """Pull out fields from test config
49
50         :param cfg: A dictionary of string-value pairs describing the test
51             configuration. Both the key and values strings use well-known
52             values.
53         :param results_dir: Where the csv formatted results are written.
54         """
55         self._testcase_start_time = time.time()
56         self._hugepages_mounted = False
57         self._traffic_ctl = None
58         self._vnf_ctl = None
59         self._vswitch_ctl = None
60         self._collector = None
61         self._loadgen = None
62         self._output_file = None
63         self._tc_results = None
64         self._settings_original = {}
65         self._settings_paths_modified = False
66         self._testcast_run_time = None
67         # initialization of step driven specific members
68         self._step_check = False    # by default don't check result for step driven testcases
69         self._step_vnf_list = {}
70         self._step_result = []
71         self._step_status = None
72         self._testcase_run_time = None
73
74         # store all GUEST_ specific settings to keep original values before their expansion
75         for key in S.__dict__:
76             if key.startswith('GUEST_'):
77                 self._settings_original[key] = S.getValue(key)
78
79         self._update_settings('VSWITCH', cfg.get('vSwitch', S.getValue('VSWITCH')))
80         self._update_settings('VNF', cfg.get('VNF', S.getValue('VNF')))
81         self._update_settings('TRAFFICGEN', cfg.get('Trafficgen', S.getValue('TRAFFICGEN')))
82         test_params = copy.deepcopy(S.getValue('TEST_PARAMS'))
83         tc_test_params = cfg.get('Parameters', S.getValue('TEST_PARAMS'))
84         test_params.update(tc_test_params)
85         self._update_settings('TEST_PARAMS', test_params)
86         S.check_test_params()
87
88         # override all redefined GUEST_ values to have them expanded correctly
89         tmp_test_params = copy.deepcopy(S.getValue('TEST_PARAMS'))
90         for key in tmp_test_params:
91             if key.startswith('GUEST_'):
92                 S.setValue(key, S.getValue(key))
93                 S.getValue('TEST_PARAMS').pop(key)
94
95         # update global settings
96         functions.settings_update_paths()
97
98         # set test parameters; CLI options take precedence to testcase settings
99         self._logger = logging.getLogger(__name__)
100         self.name = cfg['Name']
101         self.desc = cfg.get('Description', 'No description given.')
102         self.test = cfg.get('TestSteps', None)
103
104         bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
105         bidirectional = get_test_param('bidirectional', bidirectional)
106         if not isinstance(bidirectional, str):
107             raise TypeError(
108                 'Bi-dir value must be of type string in testcase configuration')
109         bidirectional = bidirectional.title()  # Keep things consistent
110
111         traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
112         traffic_type = get_test_param('traffic_type', traffic_type)
113
114         framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
115         framerate = get_test_param('iload', framerate)
116
117         self.deployment = cfg['Deployment']
118         self._frame_mod = cfg.get('Frame Modification', None)
119
120         self._tunnel_type = None
121         self._tunnel_operation = None
122
123         if self.deployment == 'op2p':
124             self._tunnel_operation = cfg['Tunnel Operation']
125
126             if 'Tunnel Type' in cfg:
127                 self._tunnel_type = cfg['Tunnel Type']
128                 self._tunnel_type = get_test_param('tunnel_type',
129                                                    self._tunnel_type)
130
131         # read configuration of streams; CLI parameter takes precedence to
132         # testcase definition
133         multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
134         multistream = get_test_param('multistream', multistream)
135         stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
136         stream_type = get_test_param('stream_type', stream_type)
137         pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
138         pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
139
140         # check if test requires background load and which generator it uses
141         self._load_cfg = cfg.get('Load', None)
142         if self._load_cfg and 'tool' in self._load_cfg:
143             self._loadgen = self._load_cfg['tool']
144         else:
145             # background load is not requested, so use dummy implementation
146             self._loadgen = "Dummy"
147
148         if self._frame_mod:
149             self._frame_mod = self._frame_mod.lower()
150         self._results_dir = S.getValue('RESULTS_PATH')
151
152         # set traffic details, so they can be passed to vswitch and traffic ctls
153         self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
154         self._traffic.update({'traffic_type': traffic_type,
155                               'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
156                               'bidir': bidirectional,
157                               'tunnel_type': self._tunnel_type,
158                               'multistream': int(multistream),
159                               'stream_type': stream_type,
160                               'pre_installed_flows' : pre_installed_flows,
161                               'frame_rate': int(framerate)})
162
163         # Packet Forwarding mode
164         self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
165
166         # trafficgen configuration required for tests of tunneling protocols
167         if self.deployment == "op2p":
168             self._traffic['l2'].update({'srcmac':
169                                         S.getValue('TRAFFICGEN_PORT1_MAC'),
170                                         'dstmac':
171                                         S.getValue('TRAFFICGEN_PORT2_MAC')})
172
173             self._traffic['l3'].update({'srcip':
174                                         S.getValue('TRAFFICGEN_PORT1_IP'),
175                                         'dstip':
176                                         S.getValue('TRAFFICGEN_PORT2_IP')})
177
178             if self._tunnel_operation == "decapsulation":
179                 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
180                 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
181                 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
182         elif len(S.getValue('NICS')) and \
183              (S.getValue('NICS')[0]['type'] == 'vf' or
184               S.getValue('NICS')[1]['type'] == 'vf'):
185             mac1 = S.getValue('NICS')[0]['mac']
186             mac2 = S.getValue('NICS')[1]['mac']
187             if mac1 and mac2:
188                 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
189             else:
190                 self._logger.debug("MAC addresses can not be read")
191
192         # count how many VNFs are involved in TestSteps
193         if self.test:
194             for step in self.test:
195                 if step[0].startswith('vnf'):
196                     self._step_vnf_list[step[0]] = None
197
198     def run_initialize(self):
199         """ Prepare test execution environment
200         """
201         self._logger.debug(self.name)
202
203         # mount hugepages if needed
204         self._mount_hugepages()
205
206         self._logger.debug("Controllers:")
207         loader = Loader()
208         self._traffic_ctl = component_factory.create_traffic(
209             self._traffic['traffic_type'],
210             loader.get_trafficgen_class())
211
212         self._vnf_ctl = component_factory.create_vnf(
213             self.deployment,
214             loader.get_vnf_class(),
215             len(self._step_vnf_list))
216
217         # verify enough hugepages are free to run the testcase
218         if not self._check_for_enough_hugepages():
219             raise RuntimeError('Not enough hugepages free to run test.')
220
221         # perform guest related handling
222         tmp_vm_count = self._vnf_ctl.get_vnfs_number() + len(self._step_vnf_list)
223         if tmp_vm_count:
224             # copy sources of l2 forwarding tools into VM shared dir if needed
225             self._copy_fwd_tools_for_all_guests(tmp_vm_count)
226
227             # in case of multi VM in parallel, set the number of streams to the number of VMs
228             if self.deployment.startswith('pvpv'):
229                 # for each VM NIC pair we need an unique stream
230                 streams = 0
231                 for vm_nic in S.getValue('GUEST_NICS_NR')[:tmp_vm_count]:
232                     streams += int(vm_nic / 2) if vm_nic > 1 else 1
233                 self._logger.debug("VMs with parallel connection were detected. "
234                                    "Thus Number of streams was set to %s", streams)
235                 # update streams if needed; In case of additional VNFs deployed by TestSteps
236                 # user can define a proper stream count manually
237                 if 'multistream' not in self._traffic or self._traffic['multistream'] < streams:
238                     self._traffic.update({'multistream': streams})
239
240             # OVS Vanilla requires guest VM MAC address and IPs to work
241             if 'linux_bridge' in S.getValue('GUEST_LOOPBACK'):
242                 self._traffic['l2'].update({'srcmac': S.getValue('VANILLA_TGEN_PORT1_MAC'),
243                                             'dstmac': S.getValue('VANILLA_TGEN_PORT2_MAC')})
244                 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
245                                             'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
246
247         if self._vswitch_none:
248             self._vswitch_ctl = component_factory.create_pktfwd(
249                 self.deployment,
250                 loader.get_pktfwd_class())
251         else:
252             self._vswitch_ctl = component_factory.create_vswitch(
253                 self.deployment,
254                 loader.get_vswitch_class(),
255                 self._traffic,
256                 self._tunnel_operation)
257
258         self._collector = component_factory.create_collector(
259             loader.get_collector_class(),
260             self._results_dir, self.name)
261         self._loadgen = component_factory.create_loadgen(
262             self._loadgen,
263             self._load_cfg)
264
265         self._output_file = os.path.join(self._results_dir, "result_" + self.name +
266                                          "_" + self.deployment + ".csv")
267
268         self._step_status = {'status' : True, 'details' : ''}
269
270         self._logger.debug("Setup:")
271
272     def run_finalize(self):
273         """ Tear down test execution environment and record test results
274         """
275         # Stop all VNFs started by TestSteps in case that something went wrong
276         self.step_stop_vnfs()
277
278         # umount hugepages if mounted
279         self._umount_hugepages()
280
281         # restore original settings
282         S.load_from_dict(self._settings_original)
283
284         # cleanup any namespaces created
285         if os.path.isdir('/tmp/namespaces'):
286             namespace_list = os.listdir('/tmp/namespaces')
287             if len(namespace_list):
288                 self._logger.info('Cleaning up namespaces')
289             for name in namespace_list:
290                 namespace.delete_namespace(name)
291             os.rmdir('/tmp/namespaces')
292         # cleanup any veth ports created
293         if os.path.isdir('/tmp/veth'):
294             veth_list = os.listdir('/tmp/veth')
295             if len(veth_list):
296                 self._logger.info('Cleaning up veth ports')
297             for eth in veth_list:
298                 port1, port2 = eth.split('-')
299                 veth.del_veth_port(port1, port2)
300             os.rmdir('/tmp/veth')
301
302     def run_report(self):
303         """ Report test results
304         """
305         self._logger.debug("self._collector Results:")
306         self._collector.print_results()
307
308         results = self._traffic_ctl.get_results()
309         if results:
310             self._logger.debug("Traffic Results:")
311             self._traffic_ctl.print_results()
312
313             self._tc_results = self._append_results(results)
314             TestCase.write_result_to_file(self._tc_results, self._output_file)
315
316     def run(self):
317         """Run the test
318
319         All setup and teardown through controllers is included.
320         """
321         # prepare test execution environment
322         self.run_initialize()
323
324         try:
325             with self._vswitch_ctl, self._loadgen:
326                 with self._vnf_ctl, self._collector:
327                     if not self._vswitch_none:
328                         self._add_flows()
329
330                     with self._traffic_ctl:
331                         # execute test based on TestSteps definition if needed...
332                         if self.step_run():
333                             # ...and continue with traffic generation, but keep
334                             # in mind, that clean deployment does not configure
335                             # OVS nor executes the traffic
336                             if self.deployment != 'clean':
337                                 self._traffic_ctl.send_traffic(self._traffic)
338
339                         # dump vswitch flows before they are affected by VNF termination
340                         if not self._vswitch_none:
341                             self._vswitch_ctl.dump_vswitch_flows()
342
343                     # garbage collection for case that TestSteps modify existing deployment
344                     self.step_stop_vnfs()
345
346         finally:
347             # tear down test execution environment and log results
348             self.run_finalize()
349
350         self._testcase_run_time = time.strftime("%H:%M:%S",
351                                                 time.gmtime(time.time() -
352                                                             self._testcase_start_time))
353         logging.info("Testcase execution time: " + self._testcase_run_time)
354         # report test results
355         self.run_report()
356
357     def _update_settings(self, param, value):
358         """ Check value of given configuration parameter
359         In case that new value is different, then testcase
360         specific settings is updated and original value stored
361
362         :param param: Name of parameter inside settings
363         :param value: Disired parameter value
364         """
365         orig_value = S.getValue(param)
366         if orig_value != value:
367             self._settings_original[param] = copy.deepcopy(orig_value)
368             S.setValue(param, value)
369
370     def _append_results(self, results):
371         """
372         Method appends mandatory Test Case results to list of dictionaries.
373
374         :param results: list of dictionaries which contains results from
375                 traffic generator.
376
377         :returns: modified list of dictionaries.
378         """
379         for item in results:
380             item[ResultsConstants.ID] = self.name
381             item[ResultsConstants.DEPLOYMENT] = self.deployment
382             item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
383             item[ResultsConstants.TEST_RUN_TIME] = self._testcase_run_time
384             if self._traffic['multistream']:
385                 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
386                 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
387                 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
388             if self._vnf_ctl.get_vnfs_number():
389                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(S.getValue('GUEST_LOOPBACK'))
390             if self._tunnel_type:
391                 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
392         return results
393
394     def _copy_fwd_tools_for_all_guests(self, vm_count):
395         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
396         """
397         # consider only VNFs involved in the test
398         for guest_dir in set(S.getValue('GUEST_SHARE_DIR')[:vm_count]):
399             self._copy_fwd_tools_for_guest(guest_dir)
400
401     def _copy_fwd_tools_for_guest(self, guest_dir):
402         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
403
404         :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
405         """
406         # remove shared dir if it exists to avoid issues with file consistency
407         if os.path.exists(guest_dir):
408             tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
409                            'Removing content of shared directory...', True)
410
411         # directory to share files between host and guest
412         os.makedirs(guest_dir)
413
414         # copy sources into shared dir only if neccessary
415         guest_loopback = set(S.getValue('GUEST_LOOPBACK'))
416         if 'testpmd' in guest_loopback:
417             try:
418                 # exclude whole .git/ subdirectory and all o-files;
419                 # It is assumed, that the same RTE_TARGET is used in both host
420                 # and VMs; This simplification significantly speeds up testpmd
421                 # build. If we will need a different RTE_TARGET in VM,
422                 # then we have to build whole DPDK from the scratch in VM.
423                 # In that case we can copy just DPDK sources (e.g. by excluding
424                 # all items obtained by git status -unormal --porcelain).
425                 # NOTE: Excluding RTE_TARGET directory won't help on systems,
426                 # where DPDK is built for multiple targets (e.g. for gcc & icc)
427                 exclude = []
428                 exclude.append(r'--exclude=.git/')
429                 exclude.append(r'--exclude=*.o')
430                 tasks.run_task(['rsync', '-a', '-r', '-l'] + exclude +
431                                [os.path.join(S.getValue('TOOLS')['dpdk_src'], ''),
432                                 os.path.join(guest_dir, 'DPDK')],
433                                self._logger,
434                                'Copying DPDK to shared directory...',
435                                True)
436             except subprocess.CalledProcessError:
437                 self._logger.error('Unable to copy DPDK to shared directory')
438                 raise
439         if 'l2fwd' in guest_loopback:
440             try:
441                 tasks.run_task(['rsync', '-a', '-r', '-l',
442                                 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
443                                 os.path.join(guest_dir, 'l2fwd')],
444                                self._logger,
445                                'Copying l2fwd to shared directory...',
446                                True)
447             except subprocess.CalledProcessError:
448                 self._logger.error('Unable to copy l2fwd to shared directory')
449                 raise
450
451     def _mount_hugepages(self):
452         """Mount hugepages if usage of DPDK or Qemu is detected
453         """
454         # hugepages are needed by DPDK and Qemu
455         if not self._hugepages_mounted and \
456             (self.deployment.count('v') or \
457              S.getValue('VSWITCH').lower().count('dpdk') or \
458              self._vswitch_none or \
459              self.test and 'vnf' in [step[0][0:3] for step in self.test]):
460             hugepages.mount_hugepages()
461             self._hugepages_mounted = True
462
463     def _umount_hugepages(self):
464         """Umount hugepages if they were mounted before
465         """
466         if self._hugepages_mounted:
467             hugepages.umount_hugepages()
468             self._hugepages_mounted = False
469
470     def _check_for_enough_hugepages(self):
471         """Check to make sure enough hugepages are free to satisfy the
472         test environment.
473         """
474         hugepages_needed = 0
475         hugepage_size = hugepages.get_hugepage_size()
476         # get hugepage amounts per guest involved in the test
477         for guest in range(self._vnf_ctl.get_vnfs_number()):
478             hugepages_needed += math.ceil((int(S.getValue(
479                 'GUEST_MEMORY')[guest]) * 1000) / hugepage_size)
480
481         # get hugepage amounts for each socket on dpdk
482         sock0_mem, sock1_mem = 0, 0
483         if S.getValue('VSWITCH').lower().count('dpdk'):
484             # the import below needs to remain here and not put into the module
485             # imports because of an exception due to settings not yet loaded
486             from vswitches import ovs_dpdk_vhost
487             if ovs_dpdk_vhost.OvsDpdkVhost.old_dpdk_config():
488                 match = re.search(
489                     r'-socket-mem\s+(\d+),(\d+)',
490                     ''.join(S.getValue('VSWITCHD_DPDK_ARGS')))
491                 if match:
492                     sock0_mem, sock1_mem = (int(match.group(1)) * 1024 / hugepage_size,
493                                             int(match.group(2)) * 1024 / hugepage_size)
494                 else:
495                     logging.info(
496                         'Could not parse socket memory config in dpdk params.')
497             else:
498                 sock0_mem, sock1_mem = (
499                     S.getValue(
500                         'VSWITCHD_DPDK_CONFIG')['dpdk-socket-mem'].split(','))
501                 sock0_mem, sock1_mem = (int(sock0_mem) * 1024 / hugepage_size,
502                                         int(sock1_mem) * 1024 / hugepage_size)
503
504         # If hugepages needed, verify the amounts are free
505         if any([hugepages_needed, sock0_mem, sock1_mem]):
506             free_hugepages = hugepages.get_free_hugepages()
507             if hugepages_needed:
508                 logging.info('Need %s hugepages free for guests',
509                              hugepages_needed)
510                 result1 = free_hugepages >= hugepages_needed
511                 free_hugepages -= hugepages_needed
512             else:
513                 result1 = True
514
515             if sock0_mem:
516                 logging.info('Need %s hugepages free for dpdk socket 0',
517                              sock0_mem)
518                 result2 = hugepages.get_free_hugepages('0') >= sock0_mem
519                 free_hugepages -= sock0_mem
520             else:
521                 result2 = True
522
523             if sock1_mem:
524                 logging.info('Need %s hugepages free for dpdk socket 1',
525                              sock1_mem)
526                 result3 = hugepages.get_free_hugepages('1') >= sock1_mem
527                 free_hugepages -= sock1_mem
528             else:
529                 result3 = True
530
531             logging.info('Need a total of {} total hugepages'.format(
532                 hugepages_needed + sock1_mem + sock0_mem))
533
534             # The only drawback here is sometimes dpdk doesn't release
535             # its hugepages on a test failure. This could cause a test
536             # to fail when dpdk would be OK to start because it will just
537             # use the previously allocated hugepages.
538             result4 = True if free_hugepages >= 0 else False
539
540             return all([result1, result2, result3, result4])
541         else:
542             return True
543
544     @staticmethod
545     def write_result_to_file(results, output):
546         """Write list of dictionaries to a CSV file.
547
548         Each element on list will create separate row in output file.
549         If output file already exists, data will be appended at the end,
550         otherwise it will be created.
551
552         :param results: list of dictionaries.
553         :param output: path to output file.
554         """
555         with open(output, 'a') as csvfile:
556
557             logging.info("Write results to file: " + output)
558             fieldnames = TestCase._get_unique_keys(results)
559
560             writer = csv.DictWriter(csvfile, fieldnames)
561
562             if not csvfile.tell():  # file is now empty
563                 writer.writeheader()
564
565             for result in results:
566                 writer.writerow(result)
567
568     @staticmethod
569     def _get_unique_keys(list_of_dicts):
570         """Gets unique key values as ordered list of strings in given dicts
571
572         :param list_of_dicts: list of dictionaries.
573
574         :returns: list of unique keys(strings).
575         """
576         result = OrderedDict()
577         for item in list_of_dicts:
578             for key in item.keys():
579                 result[key] = ''
580
581         return list(result.keys())
582
583     def _add_flows(self):
584         """Add flows to the vswitch
585         """
586         vswitch = self._vswitch_ctl.get_vswitch()
587         # TODO BOM 15-08-07 the frame mod code assumes that the
588         # physical ports are ports 1 & 2. The actual numbers
589         # need to be retrived from the vSwitch and the metadata value
590         # updated accordingly.
591         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
592         if self._frame_mod == "vlan":
593             # 0x8100 => VLAN ethertype
594             self._logger.debug(" ****   VLAN   ***** ")
595             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
596                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
597             vswitch.add_flow(bridge, flow)
598             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
599                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
600             vswitch.add_flow(bridge, flow)
601         elif self._frame_mod == "mpls":
602             # 0x8847 => MPLS unicast ethertype
603             self._logger.debug(" ****   MPLS  ***** ")
604             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
605                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
606             vswitch.add_flow(bridge, flow)
607             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
608                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
609             vswitch.add_flow(bridge, flow)
610         elif self._frame_mod == "mac":
611             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
612                     'actions': ['mod_dl_src:22:22:22:22:22:22',
613                                 'goto_table:3']}
614             vswitch.add_flow(bridge, flow)
615             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
616                     'actions': ['mod_dl_src:11:11:11:11:11:11',
617                                 'goto_table:3']}
618             vswitch.add_flow(bridge, flow)
619         elif self._frame_mod == "dscp":
620             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
621             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
622                     'dl_type':'0x0800',
623                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
624             vswitch.add_flow(bridge, flow)
625             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
626                     'dl_type':'0x0800',
627                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
628             vswitch.add_flow(bridge, flow)
629         elif self._frame_mod == "ttl":
630             # 251 and 241 are the highest prime numbers < 255
631             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
632                     'dl_type':'0x0800',
633                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
634             vswitch.add_flow(bridge, flow)
635             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
636                     'dl_type':'0x0800',
637                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
638             vswitch.add_flow(bridge, flow)
639         elif self._frame_mod == "ip_addr":
640             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
641                     'dl_type':'0x0800',
642                     'actions': ['mod_nw_src:10.10.10.10',
643                                 'mod_nw_dst:20.20.20.20',
644                                 'goto_table:3']}
645             vswitch.add_flow(bridge, flow)
646             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
647                     'dl_type':'0x0800',
648                     'actions': ['mod_nw_src:20.20.20.20',
649                                 'mod_nw_dst:10.10.10.10',
650                                 'goto_table:3']}
651             vswitch.add_flow(bridge, flow)
652         elif self._frame_mod == "ip_port":
653             # TODO BOM 15-08-27 The traffic generated is assumed
654             # to be UDP (nw_proto 17d) which is the default case but
655             # we will need to pick up the actual traffic params in use.
656             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
657                     'dl_type':'0x0800', 'nw_proto':'17',
658                     'actions': ['mod_tp_src:44444',
659                                 'mod_tp_dst:44444', 'goto_table:3']}
660             vswitch.add_flow(bridge, flow)
661             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
662                     'dl_type':'0x0800', 'nw_proto':'17',
663                     'actions': ['mod_tp_src:44444',
664                                 'mod_tp_dst:44444', 'goto_table:3']}
665             vswitch.add_flow(bridge, flow)
666         else:
667             pass
668
669
670     #
671     # TestSteps realted methods
672     #
673     def step_report_status(self, label, status):
674         """ Log status of test step
675         """
676         self._logger.info("%s ... %s", label, 'OK' if status else 'FAILED')
677
678     def step_stop_vnfs(self):
679         """ Stop all VNFs started by TestSteps
680         """
681         for vnf in self._step_vnf_list:
682             self._step_vnf_list[vnf].stop()
683
684     @staticmethod
685     def step_eval_param(param, STEP):
686         # pylint: disable=invalid-name
687         """ Helper function for #STEP macro evaluation
688         """
689         if isinstance(param, str):
690             # evaluate every #STEP reference inside parameter itself
691             macros = re.findall(r'#STEP\[[\w\[\]\-\'\"]+\]', param)
692             if macros:
693                 for macro in macros:
694                     # pylint: disable=eval-used
695                     tmp_val = str(eval(macro[1:]))
696                     param = param.replace(macro, tmp_val)
697             return param
698         elif isinstance(param, list) or isinstance(param, tuple):
699             tmp_list = []
700             for item in param:
701                 tmp_list.append(TestCase.step_eval_param(item, STEP))
702             return tmp_list
703         elif isinstance(param, dict):
704             tmp_dict = {}
705             for (key, value) in param.items():
706                 tmp_dict[key] = TestCase.step_eval_param(value, STEP)
707             return tmp_dict
708         else:
709             return param
710
711     @staticmethod
712     def step_eval_params(params, step_result):
713         """ Evaluates referrences to results from previous steps
714         """
715         eval_params = []
716         # evaluate all parameters if needed
717         for param in params:
718             eval_params.append(TestCase.step_eval_param(param, step_result))
719         return eval_params
720
721     def step_run(self):
722         """ Execute actions specified by TestSteps list
723
724         :return: False if any error was detected
725                  True otherwise
726         """
727         # anything to do?
728         if not self.test:
729             return True
730
731         # required for VNFs initialization
732         loader = Loader()
733         # initialize list with results
734         self._step_result = [None] * len(self.test)
735
736         # run test step by step...
737         for i, step in enumerate(self.test):
738             step_ok = not self._step_check
739             if step[0] == 'vswitch':
740                 test_object = self._vswitch_ctl.get_vswitch()
741             elif step[0] == 'namespace':
742                 test_object = namespace
743             elif step[0] == 'veth':
744                 test_object = veth
745             elif step[0] == 'settings':
746                 test_object = S
747             elif step[0] == 'tools':
748                 test_object = TestStepsTools()
749                 step[1] = step[1].title()
750             elif step[0] == 'trafficgen':
751                 test_object = self._traffic_ctl
752                 # in case of send_traffic or send_traffic_async methods, ensure
753                 # that specified traffic values are merged with existing self._traffic
754                 if step[1].startswith('send_traffic'):
755                     tmp_traffic = copy.deepcopy(self._traffic)
756                     tmp_traffic.update(step[2])
757                     step[2] = tmp_traffic
758             elif step[0].startswith('vnf'):
759                 if not self._step_vnf_list[step[0]]:
760                     # initialize new VM
761                     self._step_vnf_list[step[0]] = loader.get_vnf_class()()
762                 test_object = self._step_vnf_list[step[0]]
763             elif step[0] == 'wait':
764                 input(os.linesep + "Step {}: Press Enter to continue with "
765                       "the next step...".format(i) + os.linesep + os.linesep)
766                 continue
767             else:
768                 self._logger.error("Unsupported test object %s", step[0])
769                 self._step_status = {'status' : False, 'details' : ' '.join(step)}
770                 self.step_report_status("Step '{}'".format(' '.join(step)),
771                                         self._step_status['status'])
772                 return False
773
774             test_method = getattr(test_object, step[1])
775             if self._step_check:
776                 test_method_check = getattr(test_object, CHECK_PREFIX + step[1])
777             else:
778                 test_method_check = None
779
780             step_params = []
781             try:
782                 # eval parameters, but use only valid step_results
783                 # to support negative indexes
784                 step_params = TestCase.step_eval_params(step[2:], self._step_result[:i])
785                 step_log = '{} {}'.format(' '.join(step[:2]), step_params)
786                 self._logger.debug("Step %s '%s' start", i, step_log)
787                 self._step_result[i] = test_method(*step_params)
788                 self._logger.debug("Step %s '%s' results '%s'", i,
789                                    step_log, self._step_result[i])
790                 time.sleep(5)
791                 if self._step_check:
792                     step_ok = test_method_check(self._step_result[i], *step_params)
793             except (AssertionError, AttributeError, IndexError) as ex:
794                 step_ok = False
795                 self._logger.error("Step %s raised %s", i, type(ex).__name__)
796
797             if self._step_check:
798                 self.step_report_status("Step {} - '{}'".format(i, step_log), step_ok)
799
800             if not step_ok:
801                 self._step_status = {'status' : False, 'details' : step_log}
802                 # Stop all VNFs started by TestSteps
803                 self.step_stop_vnfs()
804                 return False
805
806         # all steps processed without any issue
807         return True