vnfs: configurable loopback application support inside VM
[vswitchperf.git] / testcases / testcase.py
1 # Copyright 2015 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 import csv
18 import os
19 import logging
20 import subprocess
21 from collections import OrderedDict
22
23 from core.results.results_constants import ResultsConstants
24 import core.component_factory as component_factory
25 from core.loader import Loader
26 from tools import tasks
27 from tools.report import report
28 from conf import settings as S
29 from tools.pkt_gen.trafficgen.trafficgenhelper import TRAFFIC_DEFAULTS
30 from conf import get_test_param
31
32 class TestCase(object):
33     """TestCase base class
34
35     In this basic form runs RFC2544 throughput test
36     """
37     def __init__(self, cfg, results_dir):
38         """Pull out fields from test config
39
40         :param cfg: A dictionary of string-value pairs describing the test
41             configuration. Both the key and values strings use well-known
42             values.
43         :param results_dir: Where the csv formatted results are written.
44         """
45         self._logger = logging.getLogger(__name__)
46         self.name = cfg['Name']
47         self.desc = cfg.get('Description', 'No description given.')
48         self.deployment = cfg['Deployment']
49         self._frame_mod = cfg.get('Frame Modification', None)
50         framerate = get_test_param('iload', None)
51         if framerate == None:
52             framerate = cfg.get('iLoad', 100)
53
54         # identify guest loopback method, so it can be added into reports
55         self.guest_loopback = []
56         if self.deployment in ['pvp', 'pvvp']:
57             guest_loopback = get_test_param('guest_loopback', None)
58             if guest_loopback:
59                 self.guest_loopback.append(guest_loopback)
60             else:
61                 if self.deployment == 'pvp':
62                     self.guest_loopback.append(S.getValue('GUEST_LOOPBACK')[0])
63                 else:
64                     self.guest_loopback = S.getValue('GUEST_LOOPBACK').copy()
65
66         # check if test requires background load and which generator it uses
67         self._load_cfg = cfg.get('Load', None)
68         if self._load_cfg and 'tool' in self._load_cfg:
69             self._loadgen = self._load_cfg['tool']
70         else:
71             # background load is not requested, so use dummy implementation
72             self._loadgen = "Dummy"
73
74         if self._frame_mod:
75             self._frame_mod = self._frame_mod.lower()
76         self._results_dir = results_dir
77
78         # set traffic details, so they can be passed to vswitch and traffic ctls
79         self._traffic = TRAFFIC_DEFAULTS.copy()
80         self._traffic.update({'traffic_type': cfg['Traffic Type'],
81                               'flow_type': cfg.get('Flow Type', 'port'),
82                               'bidir': cfg['biDirectional'],
83                               'multistream': cfg.get('MultiStream', 0),
84                               'frame_rate': int(framerate)})
85
86         # OVS Vanilla requires guest VM MAC address and IPs to work
87         if 'linux_bridge' in self.guest_loopback:
88             self._traffic['l2'] = {'srcmac': S.getValue('GUEST_NET2_MAC')[0],
89                                    'dstmac': S.getValue('GUEST_NET1_MAC')[0]}
90             self._traffic['l3'] = {'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
91                                    'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')}
92
93     def run(self):
94         """Run the test
95
96         All setup and teardown through controllers is included.
97         """
98         self._logger.debug(self.name)
99
100         # copy sources of l2 forwarding tools into VM shared dir if needed
101         if 'testpmd' in self.guest_loopback or 'l2fwd' in self.guest_loopback:
102             self._copy_fwd_tools_for_guest()
103
104         self._logger.debug("Controllers:")
105         loader = Loader()
106         traffic_ctl = component_factory.create_traffic(
107             self._traffic['traffic_type'],
108             loader.get_trafficgen_class())
109         vnf_ctl = component_factory.create_vnf(
110             self.deployment,
111             loader.get_vnf_class())
112         vswitch_ctl = component_factory.create_vswitch(
113             self.deployment,
114             loader.get_vswitch_class(),
115             self._traffic)
116         collector = component_factory.create_collector(
117             loader.get_collector_class(),
118             self._results_dir, self.name)
119         loadgen = component_factory.create_loadgen(
120             self._loadgen,
121             self._load_cfg)
122
123         self._logger.debug("Setup:")
124         with vswitch_ctl, loadgen:
125             with vnf_ctl, collector:
126                 vswitch = vswitch_ctl.get_vswitch()
127                 # TODO BOM 15-08-07 the frame mod code assumes that the
128                 # physical ports are ports 1 & 2. The actual numbers
129                 # need to be retrived from the vSwitch and the metadata value
130                 # updated accordingly.
131                 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
132                 if self._frame_mod == "vlan":
133                     # 0x8100 => VLAN ethertype
134                     self._logger.debug(" ****   VLAN   ***** ")
135                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
136                             'actions': ['push_vlan:0x8100', 'goto_table:3']}
137                     vswitch.add_flow(bridge, flow)
138                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
139                             'actions': ['push_vlan:0x8100', 'goto_table:3']}
140                     vswitch.add_flow(bridge, flow)
141                 elif self._frame_mod == "mpls":
142                     # 0x8847 => MPLS unicast ethertype
143                     self._logger.debug(" ****   MPLS  ***** ")
144                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
145                             'actions': ['push_mpls:0x8847', 'goto_table:3']}
146                     vswitch.add_flow(bridge, flow)
147                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
148                             'actions': ['push_mpls:0x8847', 'goto_table:3']}
149                     vswitch.add_flow(bridge, flow)
150                 elif self._frame_mod == "mac":
151                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
152                             'actions': ['mod_dl_src:22:22:22:22:22:22',
153                                         'goto_table:3']}
154                     vswitch.add_flow(bridge, flow)
155                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
156                             'actions': ['mod_dl_src:11:11:11:11:11:11',
157                                         'goto_table:3']}
158                     vswitch.add_flow(bridge, flow)
159                 elif self._frame_mod == "dscp":
160                     # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
161                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
162                             'dl_type':'0x0800',
163                             'actions': ['mod_nw_tos:184', 'goto_table:3']}
164                     vswitch.add_flow(bridge, flow)
165                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
166                             'dl_type':'0x0800',
167                             'actions': ['mod_nw_tos:184', 'goto_table:3']}
168                     vswitch.add_flow(bridge, flow)
169                 elif self._frame_mod == "ttl":
170                     # 251 and 241 are the highest prime numbers < 255
171                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
172                             'dl_type':'0x0800',
173                             'actions': ['mod_nw_ttl:251', 'goto_table:3']}
174                     vswitch.add_flow(bridge, flow)
175                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
176                             'dl_type':'0x0800',
177                             'actions': ['mod_nw_ttl:241', 'goto_table:3']}
178                     vswitch.add_flow(bridge, flow)
179                 elif self._frame_mod == "ip_addr":
180                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
181                             'dl_type':'0x0800',
182                             'actions': ['mod_nw_src:10.10.10.10',
183                                         'mod_nw_dst:20.20.20.20',
184                                         'goto_table:3']}
185                     vswitch.add_flow(bridge, flow)
186                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
187                             'dl_type':'0x0800',
188                             'actions': ['mod_nw_src:20.20.20.20',
189                                         'mod_nw_dst:10.10.10.10',
190                                         'goto_table:3']}
191                     vswitch.add_flow(bridge, flow)
192                 elif self._frame_mod == "ip_port":
193                     # TODO BOM 15-08-27 The traffic generated is assumed
194                     # to be UDP (nw_proto 17d) which is the default case but
195                     # we will need to pick up the actual traffic params in use.
196                     flow = {'table':'2', 'priority':'1000', 'metadata':'2',
197                             'dl_type':'0x0800', 'nw_proto':'17',
198                             'actions': ['mod_tp_src:44444',
199                                         'mod_tp_dst:44444', 'goto_table:3']}
200                     vswitch.add_flow(bridge, flow)
201                     flow = {'table':'2', 'priority':'1000', 'metadata':'1',
202                             'dl_type':'0x0800', 'nw_proto':'17',
203                             'actions': ['mod_tp_src:44444',
204                                         'mod_tp_dst:44444', 'goto_table:3']}
205                     vswitch.add_flow(bridge, flow)
206                 else:
207                     pass
208
209                 with traffic_ctl:
210                     traffic_ctl.send_traffic(self._traffic)
211
212                 # dump vswitch flows before they are affected by VNF termination
213                 vswitch_ctl.dump_vswitch_flows()
214
215         self._logger.debug("Traffic Results:")
216         traffic_ctl.print_results()
217
218         self._logger.debug("Collector Results:")
219         collector.print_results()
220
221         output_file = os.path.join(self._results_dir, "result_" + self.name +
222                                    "_" + self.deployment + ".csv")
223
224         tc_results = self._append_results(traffic_ctl.get_results())
225         TestCase._write_result_to_file(tc_results, output_file)
226
227         report.generate(output_file, tc_results, collector.get_results())
228
229     def _append_results(self, results):
230         """
231         Method appends mandatory Test Case results to list of dictionaries.
232
233         :param results: list of dictionaries which contains results from
234                 traffic generator.
235
236         :returns: modified list of dictionaries.
237         """
238         for item in results:
239             item[ResultsConstants.ID] = self.name
240             item[ResultsConstants.DEPLOYMENT] = self.deployment
241             if len(self.guest_loopback):
242                 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(self.guest_loopback)
243
244         return results
245
246     def _copy_fwd_tools_for_guest(self):
247         """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] for use by guests.
248         """
249         counter = 0
250         # method is executed only for pvp and pvvp, so let's count number of 'v'
251         while counter < self.deployment.count('v'):
252             guest_dir = S.getValue('GUEST_SHARE_DIR')[counter]
253
254             if not os.path.exists(guest_dir):
255                 os.makedirs(guest_dir)
256
257             try:
258                 tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"',
259                                 os.path.join(S.getValue('RTE_SDK'), ''),
260                                 os.path.join(guest_dir, 'DPDK')],
261                                self._logger,
262                                'Copying DPDK to shared directory...',
263                                True)
264                 tasks.run_task(['rsync', '-a', '-r', '-l',
265                                 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
266                                 os.path.join(guest_dir, 'l2fwd')],
267                                self._logger,
268                                'Copying l2fwd to shared directory...',
269                                True)
270             except subprocess.CalledProcessError:
271                 self._logger.error('Unable to copy DPDK and l2fwd to shared directory')
272
273             counter += 1
274
275
276     @staticmethod
277     def _write_result_to_file(results, output):
278         """Write list of dictionaries to a CSV file.
279
280         Each element on list will create separate row in output file.
281         If output file already exists, data will be appended at the end,
282         otherwise it will be created.
283
284         :param results: list of dictionaries.
285         :param output: path to output file.
286         """
287         with open(output, 'a') as csvfile:
288
289             logging.info("Write results to file: " + output)
290             fieldnames = TestCase._get_unique_keys(results)
291
292             writer = csv.DictWriter(csvfile, fieldnames)
293
294             if not csvfile.tell():  # file is now empty
295                 writer.writeheader()
296
297             for result in results:
298                 writer.writerow(result)
299
300
301     @staticmethod
302     def _get_unique_keys(list_of_dicts):
303         """Gets unique key values as ordered list of strings in given dicts
304
305         :param list_of_dicts: list of dictionaries.
306
307         :returns: list of unique keys(strings).
308         """
309         result = OrderedDict()
310         for item in list_of_dicts:
311             for key in item.keys():
312                 result[key] = ''
313
314         return list(result.keys())