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