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