Merge "test_spec: Remove formatted text file verions of IETF Draft"
[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
38 class TestCase(object):
39     """TestCase base class
40
41     In this basic form runs RFC2544 throughput test
42     """
43     def __init__(self, cfg):
44         """Pull out fields from test config
45
46         :param cfg: A dictionary of string-value pairs describing the test
47             configuration. Both the key and values strings use well-known
48             values.
49         :param results_dir: Where the csv formatted results are written.
50         """
51         self._testcase_start_time = time.time()
52         self._hugepages_mounted = False
53         self._traffic_ctl = None
54         self._vnf_ctl = None
55         self._vswitch_ctl = None
56         self._collector = None
57         self._loadgen = None
58         self._output_file = None
59         self._tc_results = None
60         self._settings_original = {}
61         self._settings_paths_modified = False
62         self._testcast_run_time = None
63
64         self._update_settings('VSWITCH', cfg.get('vSwitch', S.getValue('VSWITCH')))
65         self._update_settings('VNF', cfg.get('VNF', S.getValue('VNF')))
66         self._update_settings('TRAFFICGEN', cfg.get('Trafficgen', S.getValue('TRAFFICGEN')))
67         self._update_settings('TEST_PARAMS', cfg.get('Parameters', S.getValue('TEST_PARAMS')))
68
69         # update global settings
70         guest_loopback = get_test_param('guest_loopback', None)
71         if guest_loopback:
72             # we can put just one item, it'll be expanded automatically for all VMs
73             self._update_settings('GUEST_LOOPBACK', [guest_loopback])
74
75         if 'VSWITCH' in self._settings_original or 'VNF' in self._settings_original:
76             self._settings_original.update({
77                 'RTE_SDK' : S.getValue('RTE_SDK'),
78                 'OVS_DIR' : S.getValue('OVS_DIR'),
79             })
80             functions.settings_update_paths()
81
82         # set test parameters; CLI options take precedence to testcase settings
83         self._logger = logging.getLogger(__name__)
84         self.name = cfg['Name']
85         self.desc = cfg.get('Description', 'No description given.')
86         self.test = cfg.get('TestSteps', None)
87
88         bidirectional = cfg.get('biDirectional', TRAFFIC_DEFAULTS['bidir'])
89         bidirectional = get_test_param('bidirectional', bidirectional)
90         if not isinstance(bidirectional, str):
91             raise TypeError(
92                 'Bi-dir value must be of type string in testcase configuration')
93         bidirectional = bidirectional.title()  # Keep things consistent
94
95         traffic_type = cfg.get('Traffic Type', TRAFFIC_DEFAULTS['traffic_type'])
96         traffic_type = get_test_param('traffic_type', traffic_type)
97
98         framerate = cfg.get('iLoad', TRAFFIC_DEFAULTS['frame_rate'])
99         framerate = get_test_param('iload', framerate)
100
101         self.deployment = cfg['Deployment']
102         self._frame_mod = cfg.get('Frame Modification', None)
103
104         self._tunnel_type = None
105         self._tunnel_operation = None
106
107         if self.deployment == 'op2p':
108             self._tunnel_operation = cfg['Tunnel Operation']
109
110             if 'Tunnel Type' in cfg:
111                 self._tunnel_type = cfg['Tunnel Type']
112                 self._tunnel_type = get_test_param('tunnel_type',
113                                                    self._tunnel_type)
114
115         # read configuration of streams; CLI parameter takes precedence to
116         # testcase definition
117         multistream = cfg.get('MultiStream', TRAFFIC_DEFAULTS['multistream'])
118         multistream = get_test_param('multistream', multistream)
119         stream_type = cfg.get('Stream Type', TRAFFIC_DEFAULTS['stream_type'])
120         stream_type = get_test_param('stream_type', stream_type)
121         pre_installed_flows = cfg.get('Pre-installed Flows', TRAFFIC_DEFAULTS['pre_installed_flows'])
122         pre_installed_flows = get_test_param('pre-installed_flows', pre_installed_flows)
123
124         # check if test requires background load and which generator it uses
125         self._load_cfg = cfg.get('Load', None)
126         if self._load_cfg and 'tool' in self._load_cfg:
127             self._loadgen = self._load_cfg['tool']
128         else:
129             # background load is not requested, so use dummy implementation
130             self._loadgen = "Dummy"
131
132         if self._frame_mod:
133             self._frame_mod = self._frame_mod.lower()
134         self._results_dir = S.getValue('RESULTS_PATH')
135
136         # set traffic details, so they can be passed to vswitch and traffic ctls
137         self._traffic = copy.deepcopy(TRAFFIC_DEFAULTS)
138         self._traffic.update({'traffic_type': traffic_type,
139                               'flow_type': cfg.get('Flow Type', TRAFFIC_DEFAULTS['flow_type']),
140                               'bidir': bidirectional,
141                               'tunnel_type': self._tunnel_type,
142                               'multistream': int(multistream),
143                               'stream_type': stream_type,
144                               'pre_installed_flows' : pre_installed_flows,
145                               'frame_rate': int(framerate)})
146
147         # Packet Forwarding mode
148         self._vswitch_none = 'none' == S.getValue('VSWITCH').strip().lower()
149
150         # trafficgen configuration required for tests of tunneling protocols
151         if self.deployment == "op2p":
152             self._traffic['l2'].update({'srcmac':
153                                         S.getValue('TRAFFICGEN_PORT1_MAC'),
154                                         'dstmac':
155                                         S.getValue('TRAFFICGEN_PORT2_MAC')})
156
157             self._traffic['l3'].update({'srcip':
158                                         S.getValue('TRAFFICGEN_PORT1_IP'),
159                                         'dstip':
160                                         S.getValue('TRAFFICGEN_PORT2_IP')})
161
162             if self._tunnel_operation == "decapsulation":
163                 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
164                 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
165                 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
166         elif S.getValue('NICS')[0]['type'] == 'vf' or S.getValue('NICS')[1]['type'] == 'vf':
167             mac1 = S.getValue('NICS')[0]['mac']
168             mac2 = S.getValue('NICS')[1]['mac']
169             if mac1 and mac2:
170                 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
171             else:
172                 self._logger.debug("MAC addresses can not be read")
173
174     def run_initialize(self):
175         """ Prepare test execution environment
176         """
177         self._logger.debug(self.name)
178
179         # mount hugepages if needed
180         self._mount_hugepages()
181
182         self._logger.debug("Controllers:")
183         loader = Loader()
184         self._traffic_ctl = component_factory.create_traffic(
185             self._traffic['traffic_type'],
186             loader.get_trafficgen_class())
187
188         self._vnf_ctl = component_factory.create_vnf(
189             self.deployment,
190             loader.get_vnf_class())
191
192         # verify enough hugepages are free to run the testcase
193         if not self._check_for_enough_hugepages():
194             raise RuntimeError('Not enough hugepages free to run test.')
195
196         # perform guest related handling
197         if self._vnf_ctl.get_vnfs_number():
198             # copy sources of l2 forwarding tools into VM shared dir if needed
199             self._copy_fwd_tools_for_all_guests()
200
201             # in case of multi VM in parallel, set the number of streams to the number of VMs
202             if self.deployment.startswith('pvpv'):
203                 # for each VM NIC pair we need an unique stream
204                 streams = 0
205                 for vm_nic in S.getValue('GUEST_NICS_NR')[:self._vnf_ctl.get_vnfs_number()]:
206                     streams += int(vm_nic / 2) if vm_nic > 1 else 1
207                 self._logger.debug("VMs with parallel connection were detected. "
208                                    "Thus Number of streams was set to %s", streams)
209                 self._traffic.update({'multistream': streams})
210
211             # OVS Vanilla requires guest VM MAC address and IPs to work
212             if 'linux_bridge' in S.getValue('GUEST_LOOPBACK'):
213                 self._traffic['l2'].update({'srcmac': S.getValue('VANILLA_TGEN_PORT1_MAC'),
214                                             'dstmac': S.getValue('VANILLA_TGEN_PORT2_MAC')})
215                 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
216                                             'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
217
218         if self._vswitch_none:
219             self._vswitch_ctl = component_factory.create_pktfwd(
220                 self.deployment,
221                 loader.get_pktfwd_class())
222         else:
223             self._vswitch_ctl = component_factory.create_vswitch(
224                 self.deployment,
225                 loader.get_vswitch_class(),
226                 self._traffic,
227                 self._tunnel_operation)
228
229         self._collector = component_factory.create_collector(
230             loader.get_collector_class(),
231             self._results_dir, self.name)
232         self._loadgen = component_factory.create_loadgen(
233             self._loadgen,
234             self._load_cfg)
235
236         self._output_file = os.path.join(self._results_dir, "result_" + self.name +
237                                          "_" + self.deployment + ".csv")
238
239         self._logger.debug("Setup:")
240
241     def run_finalize(self):
242         """ Tear down test execution environment and record test results
243         """
244         # umount hugepages if mounted
245         self._umount_hugepages()
246
247         # restore original settings
248         S.load_from_dict(self._settings_original)
249
250         # cleanup any namespaces created
251         if os.path.isdir('/tmp/namespaces'):
252             import tools.namespace
253             namespace_list = os.listdir('/tmp/namespaces')
254             if len(namespace_list):
255                 self._logger.info('Cleaning up namespaces')
256             for name in namespace_list:
257                 tools.namespace.delete_namespace(name)
258             os.rmdir('/tmp/namespaces')
259         # cleanup any veth ports created
260         if os.path.isdir('/tmp/veth'):
261             import tools.veth
262             veth_list = os.listdir('/tmp/veth')
263             if len(veth_list):
264                 self._logger.info('Cleaning up veth ports')
265             for eth in veth_list:
266                 port1, port2 = eth.split('-')
267                 tools.veth.del_veth_port(port1, port2)
268             os.rmdir('/tmp/veth')
269
270     def run_report(self):
271         """ Report test results
272         """
273         self._logger.debug("self._collector Results:")
274         self._collector.print_results()
275
276         if S.getValue('mode') != 'trafficgen-off':
277             self._logger.debug("Traffic Results:")
278             self._traffic_ctl.print_results()
279
280             self._tc_results = self._append_results(self._traffic_ctl.get_results())
281             TestCase.write_result_to_file(self._tc_results, self._output_file)
282
283     def run(self):
284         """Run the test
285
286         All setup and teardown through controllers is included.
287         """
288         # prepare test execution environment
289         self.run_initialize()
290
291         with self._vswitch_ctl, self._loadgen:
292             with self._vnf_ctl, self._collector:
293                 if not self._vswitch_none:
294                     self._add_flows()
295
296                 # run traffic generator if requested, otherwise wait for manual termination
297                 if S.getValue('mode') == 'trafficgen-off':
298                     time.sleep(2)
299                     self._logger.debug("All is set. Please run traffic generator manually.")
300                     input(os.linesep + "Press Enter to terminate vswitchperf..." + os.linesep + os.linesep)
301                 else:
302                     if S.getValue('mode') == 'trafficgen-pause':
303                         time.sleep(2)
304                         true_vals = ('yes', 'y', 'ye', None)
305                         while True:
306                             choice = input(os.linesep + 'Transmission paused, should'
307                                            ' transmission be resumed? ' + os.linesep).lower()
308                             if not choice or choice not in true_vals:
309                                 print('Please respond with \'yes\' or \'y\' ', end='')
310                             else:
311                                 break
312                     with self._traffic_ctl:
313                         self._traffic_ctl.send_traffic(self._traffic)
314
315                     # dump vswitch flows before they are affected by VNF termination
316                     if not self._vswitch_none:
317                         self._vswitch_ctl.dump_vswitch_flows()
318
319         # tear down test execution environment and log results
320         self.run_finalize()
321
322         self._testcase_run_time = time.strftime("%H:%M:%S",
323                                   time.gmtime(time.time() - self._testcase_start_time))
324         logging.info("Testcase execution time: " + self._testcase_run_time)
325         # report test results
326         self.run_report()
327
328     def _update_settings(self, param, value):
329         """ Check value of given configuration parameter
330         In case that new value is different, then testcase
331         specific settings is updated and original value stored
332
333         :param param: Name of parameter inside settings
334         :param value: Disired parameter value
335         """
336         orig_value = S.getValue(param)
337         if orig_value != value:
338             self._settings_original[param] = orig_value
339             S.setValue(param, value)
340
341     def _append_results(self, results):
342         """
343         Method appends mandatory Test Case results to list of dictionaries.
344
345         :param results: list of dictionaries which contains results from
346                 traffic generator.
347
348         :returns: modified list of dictionaries.
349         """
350         for item in results:
351             item[ResultsConstants.ID] = self.name
352             item[ResultsConstants.DEPLOYMENT] = self.deployment
353             item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
354             item[ResultsConstants.TEST_RUN_TIME] = self._testcase_run_time
355             if self._traffic['multistream']:
356                 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
357                 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
358                 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
359             if self._vnf_ctl.get_vnfs_number():
360                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(S.getValue('GUEST_LOOPBACK'))
361             if self._tunnel_type:
362                 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
363         return results
364
365     def _copy_fwd_tools_for_all_guests(self):
366         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
367         """
368         # consider only VNFs involved in the test
369         for guest_dir in set(S.getValue('GUEST_SHARE_DIR')[:self._vnf_ctl.get_vnfs_number()]):
370             self._copy_fwd_tools_for_guest(guest_dir)
371
372     def _copy_fwd_tools_for_guest(self, guest_dir):
373         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
374
375         :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
376         """
377         # remove shared dir if it exists to avoid issues with file consistency
378         if os.path.exists(guest_dir):
379             tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
380                            'Removing content of shared directory...', True)
381
382         # directory to share files between host and guest
383         os.makedirs(guest_dir)
384
385         # copy sources into shared dir only if neccessary
386         guest_loopback = set(S.getValue('GUEST_LOOPBACK'))
387         if 'testpmd' in guest_loopback or 'l2fwd' in guest_loopback:
388             try:
389                 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
390                                 os.path.join(S.getValue('RTE_SDK_USER'), ''),
391                                 os.path.join(guest_dir, 'DPDK')],
392                                self._logger,
393                                'Copying DPDK to shared directory...',
394                                True)
395                 tasks.run_task(['rsync', '-a', '-r', '-l',
396                                 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
397                                 os.path.join(guest_dir, 'l2fwd')],
398                                self._logger,
399                                'Copying l2fwd to shared directory...',
400                                True)
401             except subprocess.CalledProcessError:
402                 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
403
404     def _mount_hugepages(self):
405         """Mount hugepages if usage of DPDK or Qemu is detected
406         """
407         # hugepages are needed by DPDK and Qemu
408         if not self._hugepages_mounted and \
409             (self.deployment.count('v') or \
410              S.getValue('VSWITCH').lower().count('dpdk') or \
411              self._vswitch_none or \
412              self.test and 'vnf' in [step[0][0:3] for step in self.test]):
413             hugepages.mount_hugepages()
414             self._hugepages_mounted = True
415
416     def _umount_hugepages(self):
417         """Umount hugepages if they were mounted before
418         """
419         if self._hugepages_mounted:
420             hugepages.umount_hugepages()
421             self._hugepages_mounted = False
422
423     def _check_for_enough_hugepages(self):
424         """Check to make sure enough hugepages are free to satisfy the
425         test environment.
426         """
427         hugepages_needed = 0
428         hugepage_size = hugepages.get_hugepage_size()
429         # get hugepage amounts per guest involved in the test
430         for guest in range(self._vnf_ctl.get_vnfs_number()):
431             hugepages_needed += math.ceil((int(S.getValue(
432                 'GUEST_MEMORY')[guest]) * 1000) / hugepage_size)
433
434         # get hugepage amounts for each socket on dpdk
435         sock0_mem, sock1_mem = 0, 0
436         if S.getValue('VSWITCH').lower().count('dpdk'):
437             # the import below needs to remain here and not put into the module
438             # imports because of an exception due to settings not yet loaded
439             from vswitches import ovs_dpdk_vhost
440             if ovs_dpdk_vhost.OvsDpdkVhost.old_dpdk_config():
441                 match = re.search(
442                     r'-socket-mem\s+(\d+),(\d+)',
443                     ''.join(S.getValue('VSWITCHD_DPDK_ARGS')))
444                 if match:
445                     sock0_mem, sock1_mem = (int(match.group(1)) * 1024 / hugepage_size,
446                                             int(match.group(2)) * 1024 / hugepage_size)
447                 else:
448                     logging.info(
449                         'Could not parse socket memory config in dpdk params.')
450             else:
451                 sock0_mem, sock1_mem = (
452                     S.getValue(
453                         'VSWITCHD_DPDK_CONFIG')['dpdk-socket-mem'].split(','))
454                 sock0_mem, sock1_mem = (int(sock0_mem) * 1024 / hugepage_size,
455                                         int(sock1_mem) * 1024 / hugepage_size)
456
457         # If hugepages needed, verify the amounts are free
458         if any([hugepages_needed, sock0_mem, sock1_mem]):
459             free_hugepages = hugepages.get_free_hugepages()
460             if hugepages_needed:
461                 logging.info('Need %s hugepages free for guests',
462                              hugepages_needed)
463                 result1 = free_hugepages >= hugepages_needed
464                 free_hugepages -= hugepages_needed
465             else:
466                 result1 = True
467
468             if sock0_mem:
469                 logging.info('Need %s hugepages free for dpdk socket 0',
470                              sock0_mem)
471                 result2 = hugepages.get_free_hugepages('0') >= sock0_mem
472                 free_hugepages -= sock0_mem
473             else:
474                 result2 = True
475
476             if sock1_mem:
477                 logging.info('Need %s hugepages free for dpdk socket 1',
478                              sock1_mem)
479                 result3 = hugepages.get_free_hugepages('1') >= sock1_mem
480                 free_hugepages -= sock1_mem
481             else:
482                 result3 = True
483
484             logging.info('Need a total of {} total hugepages'.format(
485                 hugepages_needed + sock1_mem + sock0_mem))
486
487             # The only drawback here is sometimes dpdk doesn't release
488             # its hugepages on a test failure. This could cause a test
489             # to fail when dpdk would be OK to start because it will just
490             # use the previously allocated hugepages.
491             result4 = True if free_hugepages >= 0 else False
492
493             return all([result1, result2, result3, result4])
494         else:
495             return True
496
497     @staticmethod
498     def write_result_to_file(results, output):
499         """Write list of dictionaries to a CSV file.
500
501         Each element on list will create separate row in output file.
502         If output file already exists, data will be appended at the end,
503         otherwise it will be created.
504
505         :param results: list of dictionaries.
506         :param output: path to output file.
507         """
508         with open(output, 'a') as csvfile:
509
510             logging.info("Write results to file: " + output)
511             fieldnames = TestCase._get_unique_keys(results)
512
513             writer = csv.DictWriter(csvfile, fieldnames)
514
515             if not csvfile.tell():  # file is now empty
516                 writer.writeheader()
517
518             for result in results:
519                 writer.writerow(result)
520
521     @staticmethod
522     def _get_unique_keys(list_of_dicts):
523         """Gets unique key values as ordered list of strings in given dicts
524
525         :param list_of_dicts: list of dictionaries.
526
527         :returns: list of unique keys(strings).
528         """
529         result = OrderedDict()
530         for item in list_of_dicts:
531             for key in item.keys():
532                 result[key] = ''
533
534         return list(result.keys())
535
536     def _add_flows(self):
537         """Add flows to the vswitch
538         """
539         vswitch = self._vswitch_ctl.get_vswitch()
540         # TODO BOM 15-08-07 the frame mod code assumes that the
541         # physical ports are ports 1 & 2. The actual numbers
542         # need to be retrived from the vSwitch and the metadata value
543         # updated accordingly.
544         bridge = S.getValue('VSWITCH_BRIDGE_NAME')
545         if self._frame_mod == "vlan":
546             # 0x8100 => VLAN ethertype
547             self._logger.debug(" ****   VLAN   ***** ")
548             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
549                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
550             vswitch.add_flow(bridge, flow)
551             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
552                     'actions': ['push_vlan:0x8100', 'goto_table:3']}
553             vswitch.add_flow(bridge, flow)
554         elif self._frame_mod == "mpls":
555             # 0x8847 => MPLS unicast ethertype
556             self._logger.debug(" ****   MPLS  ***** ")
557             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
558                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
559             vswitch.add_flow(bridge, flow)
560             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
561                     'actions': ['push_mpls:0x8847', 'goto_table:3']}
562             vswitch.add_flow(bridge, flow)
563         elif self._frame_mod == "mac":
564             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
565                     'actions': ['mod_dl_src:22:22:22:22:22:22',
566                                 'goto_table:3']}
567             vswitch.add_flow(bridge, flow)
568             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
569                     'actions': ['mod_dl_src:11:11:11:11:11:11',
570                                 'goto_table:3']}
571             vswitch.add_flow(bridge, flow)
572         elif self._frame_mod == "dscp":
573             # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
574             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
575                     'dl_type':'0x0800',
576                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
577             vswitch.add_flow(bridge, flow)
578             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
579                     'dl_type':'0x0800',
580                     'actions': ['mod_nw_tos:184', 'goto_table:3']}
581             vswitch.add_flow(bridge, flow)
582         elif self._frame_mod == "ttl":
583             # 251 and 241 are the highest prime numbers < 255
584             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
585                     'dl_type':'0x0800',
586                     'actions': ['mod_nw_ttl:251', 'goto_table:3']}
587             vswitch.add_flow(bridge, flow)
588             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
589                     'dl_type':'0x0800',
590                     'actions': ['mod_nw_ttl:241', 'goto_table:3']}
591             vswitch.add_flow(bridge, flow)
592         elif self._frame_mod == "ip_addr":
593             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
594                     'dl_type':'0x0800',
595                     'actions': ['mod_nw_src:10.10.10.10',
596                                 'mod_nw_dst:20.20.20.20',
597                                 'goto_table:3']}
598             vswitch.add_flow(bridge, flow)
599             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
600                     'dl_type':'0x0800',
601                     'actions': ['mod_nw_src:20.20.20.20',
602                                 'mod_nw_dst:10.10.10.10',
603                                 'goto_table:3']}
604             vswitch.add_flow(bridge, flow)
605         elif self._frame_mod == "ip_port":
606             # TODO BOM 15-08-27 The traffic generated is assumed
607             # to be UDP (nw_proto 17d) which is the default case but
608             # we will need to pick up the actual traffic params in use.
609             flow = {'table':'2', 'priority':'1000', 'metadata':'2',
610                     'dl_type':'0x0800', 'nw_proto':'17',
611                     'actions': ['mod_tp_src:44444',
612                                 'mod_tp_dst:44444', 'goto_table:3']}
613             vswitch.add_flow(bridge, flow)
614             flow = {'table':'2', 'priority':'1000', 'metadata':'1',
615                     'dl_type':'0x0800', 'nw_proto':'17',
616                     'actions': ['mod_tp_src:44444',
617                                 'mod_tp_dst:44444', 'goto_table:3']}
618             vswitch.add_flow(bridge, flow)
619         else:
620             pass