Add option to pass config file to vPE
[yardstick.git] / yardstick / network_services / vnf_generic / vnf / vpe_vnf.py
1 # Copyright (c) 2016-2017 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 """ vPE (Power Edge router) VNF model definitions based on IETS Spec """
15
16 from __future__ import absolute_import
17 from __future__ import print_function
18
19
20 import os
21 import logging
22 import re
23 import posixpath
24
25 from six.moves import configparser, zip
26
27 from yardstick.common import utils
28 from yardstick.common.process import check_if_process_failed
29 from yardstick.network_services.helpers.samplevnf_helper import PortPairs
30 from yardstick.network_services.pipeline import PipelineRules
31 from yardstick.network_services.vnf_generic.vnf.sample_vnf import SampleVNF, DpdkVnfSetupEnvHelper
32 from yardstick.benchmark.contexts import base as ctx_base
33
34 LOG = logging.getLogger(__name__)
35
36 VPE_PIPELINE_COMMAND = "sudo {tool_path} -p {port_mask_hex} -f {cfg_file} -s {script} {hwlb}"
37
38 VPE_COLLECT_KPI = """\
39 Pkts in:\\s(\\d+)\r\n\
40 \tPkts dropped by AH:\\s(\\d+)\r\n\
41 \tPkts dropped by other:\\s(\\d+)\
42 """
43
44
45 class ConfigCreate(object):
46
47     @staticmethod
48     def vpe_tmq(config, index):
49         tm_q = 'TM{0}'.format(index)
50         config.add_section(tm_q)
51         config.set(tm_q, 'burst_read', '24')
52         config.set(tm_q, 'burst_write', '32')
53         config.set(tm_q, 'cfg', '/tmp/full_tm_profile_10G.cfg')
54         return config
55
56     def __init__(self, vnfd_helper, socket):
57         super(ConfigCreate, self).__init__()
58         self.sw_q = -1
59         self.sink_q = -1
60         self.n_pipeline = 1
61         self.vnfd_helper = vnfd_helper
62         self.uplink_ports = self.vnfd_helper.port_pairs.uplink_ports
63         self.downlink_ports = self.vnfd_helper.port_pairs.downlink_ports
64         self.pipeline_per_port = 9
65         self.socket = socket
66         self._dpdk_port_to_link_id_map = None
67
68     @property
69     def dpdk_port_to_link_id_map(self):
70         # we need interface name -> DPDK port num (PMD ID) -> LINK ID
71         # LINK ID -> PMD ID is governed by the port mask
72         # LINK instances are created implicitly based on the PORT_MASK application startup
73         # argument. LINK0 is the first port enabled in the PORT_MASK, port 1 is the next one,
74         # etc. The LINK ID is different than the DPDK PMD-level NIC port ID, which is the actual
75         #  position in the bitmask mentioned above. For example, if bit 5 is the first bit set
76         # in the bitmask, then LINK0 is having the PMD ID of 5. This mechanism creates a
77         # contiguous LINK ID space and isolates the configuration file against changes in the
78         # board PCIe slots where NICs are plugged in.
79         if self._dpdk_port_to_link_id_map is None:
80             self._dpdk_port_to_link_id_map = {}
81             for link_id, port_name in enumerate(sorted(self.vnfd_helper.port_pairs.all_ports,
82                                                        key=self.vnfd_helper.port_num)):
83                 self._dpdk_port_to_link_id_map[port_name] = link_id
84         return self._dpdk_port_to_link_id_map
85
86     def vpe_initialize(self, config):
87         config.add_section('EAL')
88         config.set('EAL', 'log_level', '0')
89
90         config.add_section('PIPELINE0')
91         config.set('PIPELINE0', 'type', 'MASTER')
92         config.set('PIPELINE0', 'core', 's%sC0' % self.socket)
93
94         config.add_section('MEMPOOL0')
95         config.set('MEMPOOL0', 'pool_size', '256K')
96
97         config.add_section('MEMPOOL1')
98         config.set('MEMPOOL1', 'pool_size', '2M')
99         return config
100
101     def vpe_rxq(self, config):
102         for port in self.downlink_ports:
103             new_section = 'RXQ{0}.0'.format(self.dpdk_port_to_link_id_map[port])
104             config.add_section(new_section)
105             config.set(new_section, 'mempool', 'MEMPOOL1')
106
107         return config
108
109     def get_sink_swq(self, parser, pipeline, k, index):
110         sink = ""
111         pktq = parser.get(pipeline, k)
112         if "SINK" in pktq:
113             self.sink_q += 1
114             sink = " SINK{0}".format(self.sink_q)
115         if "TM" in pktq:
116             sink = " TM{0}".format(index)
117         pktq = "SWQ{0}{1}".format(self.sw_q, sink)
118         return pktq
119
120     def vpe_upstream(self, vnf_cfg, index=0):  # pragma: no cover
121         # NOTE(ralonsoh): this function must be covered in UTs.
122         parser = configparser.ConfigParser()
123         parser.read(os.path.join(vnf_cfg, 'vpe_upstream'))
124
125         for pipeline in parser.sections():
126             for k, v in parser.items(pipeline):
127                 if k == "pktq_in":
128                     if "RXQ" in v:
129                         port = self.dpdk_port_to_link_id_map[self.uplink_ports[index]]
130                         value = "RXQ{0}.0".format(port)
131                     else:
132                         value = self.get_sink_swq(parser, pipeline, k, index)
133
134                     parser.set(pipeline, k, value)
135
136                 elif k == "pktq_out":
137                     if "TXQ" in v:
138                         port = self.dpdk_port_to_link_id_map[self.downlink_ports[index]]
139                         value = "TXQ{0}.0".format(port)
140                     else:
141                         self.sw_q += 1
142                         value = self.get_sink_swq(parser, pipeline, k, index)
143
144                     parser.set(pipeline, k, value)
145
146             new_pipeline = 'PIPELINE{0}'.format(self.n_pipeline)
147             if new_pipeline != pipeline:
148                 parser._sections[new_pipeline] = parser._sections[pipeline]
149                 parser._sections.pop(pipeline)
150             self.n_pipeline += 1
151         return parser
152
153     def vpe_downstream(self, vnf_cfg, index):  # pragma: no cover
154         # NOTE(ralonsoh): this function must be covered in UTs.
155         parser = configparser.ConfigParser()
156         parser.read(os.path.join(vnf_cfg, 'vpe_downstream'))
157         for pipeline in parser.sections():
158             for k, v in parser.items(pipeline):
159
160                 if k == "pktq_in":
161                     port = self.dpdk_port_to_link_id_map[self.downlink_ports[index]]
162                     if "RXQ" not in v:
163                         value = self.get_sink_swq(parser, pipeline, k, index)
164                     elif "TM" in v:
165                         value = "RXQ{0}.0 TM{1}".format(port, index)
166                     else:
167                         value = "RXQ{0}.0".format(port)
168
169                     parser.set(pipeline, k, value)
170
171                 if k == "pktq_out":
172                     port = self.dpdk_port_to_link_id_map[self.uplink_ports[index]]
173                     if "TXQ" not in v:
174                         self.sw_q += 1
175                         value = self.get_sink_swq(parser, pipeline, k, index)
176                     elif "TM" in v:
177                         value = "TXQ{0}.0 TM{1}".format(port, index)
178                     else:
179                         value = "TXQ{0}.0".format(port)
180
181                     parser.set(pipeline, k, value)
182
183             new_pipeline = 'PIPELINE{0}'.format(self.n_pipeline)
184             if new_pipeline != pipeline:
185                 parser._sections[new_pipeline] = parser._sections[pipeline]
186                 parser._sections.pop(pipeline)
187             self.n_pipeline += 1
188         return parser
189
190     def create_vpe_config(self, vnf_cfg):
191         config = configparser.ConfigParser()
192         vpe_cfg = os.path.join("/tmp/vpe_config")
193         with open(vpe_cfg, 'w') as cfg_file:
194             config = self.vpe_initialize(config)
195             config = self.vpe_rxq(config)
196             config.write(cfg_file)
197             for index, _ in enumerate(self.uplink_ports):
198                 config = self.vpe_upstream(vnf_cfg, index)
199                 config.write(cfg_file)
200                 config = self.vpe_downstream(vnf_cfg, index)
201                 config = self.vpe_tmq(config, index)
202                 config.write(cfg_file)
203
204     def generate_vpe_script(self, interfaces):
205         rules = PipelineRules(pipeline_id=1)
206         for uplink_port, downlink_port in zip(self.uplink_ports, self.downlink_ports):
207
208             uplink_intf = \
209                 next(intf["virtual-interface"] for intf in interfaces
210                      if intf["name"] == uplink_port)
211             downlink_intf = \
212                 next(intf["virtual-interface"] for intf in interfaces
213                      if intf["name"] == downlink_port)
214
215             dst_port0_ip = uplink_intf["dst_ip"]
216             dst_port1_ip = downlink_intf["dst_ip"]
217             dst_port0_mac = uplink_intf["dst_mac"]
218             dst_port1_mac = downlink_intf["dst_mac"]
219
220             rules.add_firewall_script(dst_port0_ip)
221             rules.next_pipeline()
222             rules.add_flow_classification_script()
223             rules.next_pipeline()
224             rules.add_flow_action()
225             rules.next_pipeline()
226             rules.add_flow_action2()
227             rules.next_pipeline()
228             rules.add_route_script(dst_port1_ip, dst_port1_mac)
229             rules.next_pipeline()
230             rules.add_route_script2(dst_port0_ip, dst_port0_mac)
231             rules.next_pipeline(num=4)
232
233         return rules.get_string()
234
235     def generate_tm_cfg(self, vnf_cfg):
236         vnf_cfg = os.path.join(vnf_cfg, "full_tm_profile_10G.cfg")
237         if os.path.exists(vnf_cfg):
238             return open(vnf_cfg).read()
239
240 class VpeApproxSetupEnvHelper(DpdkVnfSetupEnvHelper):
241
242     APP_NAME = 'vPE_vnf'
243     CFG_SCRIPT = "/tmp/vpe_script"
244     TM_CONFIG = "/tmp/full_tm_profile_10G.cfg"
245     CORES = ['0', '1', '2', '3', '4', '5']
246     PIPELINE_COMMAND = VPE_PIPELINE_COMMAND
247
248     def _build_vnf_ports(self):
249         self._port_pairs = PortPairs(self.vnfd_helper.interfaces)
250         self.uplink_ports = self._port_pairs.uplink_ports
251         self.downlink_ports = self._port_pairs.downlink_ports
252         self.all_ports = self._port_pairs.all_ports
253
254     def build_config(self):
255         vnf_cfg = self.scenario_helper.vnf_cfg
256         task_path = self.scenario_helper.task_path
257         action_bulk_file = vnf_cfg.get('action_bulk_file', '/tmp/action_bulk_512.txt')
258         full_tm_profile_file = vnf_cfg.get('full_tm_profile_file', '/tmp/full_tm_profile_10G.cfg')
259         config_file = vnf_cfg.get('file', '/tmp/vpe_config')
260         vpe_vars = {
261             "bin_path": self.ssh_helper.bin_path,
262             "socket": self.socket,
263         }
264         self._build_vnf_ports()
265         vpe_conf = ConfigCreate(self.vnfd_helper, self.socket)
266
267         config_basename = posixpath.basename(config_file)
268         script_basename = posixpath.basename(self.CFG_SCRIPT)
269
270         with utils.open_relative_file(action_bulk_file, task_path) as handle:
271             action_bulk = handle.read()
272
273         with utils.open_relative_file(full_tm_profile_file, task_path) as handle:
274             full_tm_profile = handle.read()
275
276         with utils.open_relative_file(config_file, task_path) as handle:
277             vpe_config = handle.read()
278
279         # vpe_script needs to be autogenerated
280         vpe_script = vpe_conf.generate_vpe_script(self.vnfd_helper.interfaces)
281         # upload the 4 config files to the target server
282         self.ssh_helper.upload_config_file(config_basename, vpe_config.format(**vpe_vars))
283         self.ssh_helper.upload_config_file(script_basename, vpe_script.format(**vpe_vars))
284         self.ssh_helper.upload_config_file(posixpath.basename(action_bulk_file),
285                                            action_bulk.format(**vpe_vars))
286         self.ssh_helper.upload_config_file(posixpath.basename(full_tm_profile_file),
287                                            full_tm_profile.format(**vpe_vars))
288
289         LOG.info("Provision and start the %s", self.APP_NAME)
290         LOG.info(config_file)
291         LOG.info(self.CFG_SCRIPT)
292         self._build_pipeline_kwargs(cfg_file='/tmp/' + config_basename)
293         return self.PIPELINE_COMMAND.format(**self.pipeline_kwargs)
294
295
296 class VpeApproxVnf(SampleVNF):
297     """ This class handles vPE VNF model-driver definitions """
298
299     APP_NAME = 'vPE_vnf'
300     APP_WORD = 'vpe'
301     COLLECT_KPI = VPE_COLLECT_KPI
302     WAIT_TIME = 20
303
304     def __init__(self, name, vnfd, task_id, setup_env_helper_type=None,
305                  resource_helper_type=None):
306         if setup_env_helper_type is None:
307             setup_env_helper_type = VpeApproxSetupEnvHelper
308         super(VpeApproxVnf, self).__init__(
309             name, vnfd, task_id, setup_env_helper_type, resource_helper_type)
310
311     def get_stats(self, *args, **kwargs):
312         raise NotImplementedError
313
314     def collect_kpi(self):
315         # we can't get KPIs if the VNF is down
316         check_if_process_failed(self._vnf_process)
317         physical_node = ctx_base.Context.get_physical_node_from_server(
318             self.scenario_helper.nodes[self.name])
319
320         result = {
321             "physical_node": physical_node,
322             'pkt_in_up_stream': 0,
323             'pkt_drop_up_stream': 0,
324             'pkt_in_down_stream': 0,
325             'pkt_drop_down_stream': 0,
326             'collect_stats': self.resource_helper.collect_kpi(),
327         }
328
329         indexes_in = [1]
330         indexes_drop = [2, 3]
331         command = 'p {0} stats port {1} 0'
332         for index, direction in ((5, 'up'), (9, 'down')):
333             key_in = "pkt_in_{0}_stream".format(direction)
334             key_drop = "pkt_drop_{0}_stream".format(direction)
335             for mode in ('in', 'out'):
336                 stats = self.vnf_execute(command.format(index, mode))
337                 match = re.search(self.COLLECT_KPI, stats, re.MULTILINE)
338                 if not match:
339                     continue
340                 result[key_in] += sum(int(match.group(x)) for x in indexes_in)
341                 result[key_drop] += sum(int(match.group(x)) for x in indexes_drop)
342
343         LOG.debug("%s collect KPIs %s", self.APP_NAME, result)
344         return result