Merge "Open storperf testcase to huawei-pod2"
[yardstick.git] / yardstick / benchmark / scenarios / networking / vnf_generic.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 """ NSPerf specific scenario definition """
15
16 from __future__ import absolute_import
17 import logging
18
19 import errno
20 import os
21
22 import re
23 from operator import itemgetter
24 from collections import defaultdict
25
26 import yaml
27
28 from yardstick.benchmark.scenarios import base
29 from yardstick.common.utils import import_modules_from_package, itersubclasses
30 from yardstick.network_services.collector.subscriber import Collector
31 from yardstick.network_services.vnf_generic import vnfdgen
32 from yardstick.network_services.vnf_generic.vnf.base import GenericVNF
33 from yardstick.network_services.traffic_profile.base import TrafficProfile
34 from yardstick import ssh
35
36 LOG = logging.getLogger(__name__)
37
38
39 class SSHError(Exception):
40     """Class handles ssh connection error exception"""
41     pass
42
43
44 class SSHTimeout(SSHError):
45     """Class handles ssh connection timeout exception"""
46     pass
47
48
49 class IncorrectConfig(Exception):
50     """Class handles incorrect configuration during setup"""
51     pass
52
53
54 class IncorrectSetup(Exception):
55     """Class handles incorrect setup during setup"""
56     pass
57
58
59 class SshManager(object):
60     def __init__(self, node):
61         super(SshManager, self).__init__()
62         self.node = node
63         self.conn = None
64
65     def __enter__(self):
66         """
67         args -> network device mappings
68         returns -> ssh connection ready to be used
69         """
70         try:
71             self.conn = ssh.SSH.from_node(self.node)
72             self.conn.wait()
73         except SSHError as error:
74             LOG.info("connect failed to %s, due to %s", self.node["ip"], error)
75         # self.conn defaults to None
76         return self.conn
77
78     def __exit__(self, exc_type, exc_val, exc_tb):
79         if self.conn:
80             self.conn.close()
81
82
83 def open_relative_file(path, task_path):
84     try:
85         return open(path)
86     except IOError as e:
87         if e.errno == errno.ENOENT:
88             return open(os.path.join(task_path, path))
89         raise
90
91
92 class NetworkServiceTestCase(base.Scenario):
93     """Class handles Generic framework to do pre-deployment VNF &
94        Network service testing  """
95
96     __scenario_type__ = "NSPerf"
97
98     def __init__(self, scenario_cfg, context_cfg):  # Yardstick API
99         super(NetworkServiceTestCase, self).__init__()
100         self.scenario_cfg = scenario_cfg
101         self.context_cfg = context_cfg
102
103         # fixme: create schema to validate all fields have been provided
104         with open_relative_file(scenario_cfg["topology"],
105                                 scenario_cfg['task_path']) as stream:
106             topology_yaml = yaml.load(stream)
107
108         self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
109         self.vnfs = []
110         self.collector = None
111         self.traffic_profile = None
112
113     @classmethod
114     def _get_traffic_flow(cls, scenario_cfg):
115         try:
116             with open(scenario_cfg["traffic_options"]["flow"]) as fflow:
117                 flow = yaml.load(fflow)
118         except (KeyError, IOError, OSError):
119             flow = {}
120         return flow
121
122     @classmethod
123     def _get_traffic_imix(cls, scenario_cfg):
124         try:
125             with open(scenario_cfg["traffic_options"]["imix"]) as fimix:
126                 imix = yaml.load(fimix)
127         except (KeyError, IOError, OSError):
128             imix = {}
129         return imix
130
131     @classmethod
132     def _get_traffic_profile(cls, scenario_cfg, context_cfg):
133         traffic_profile_tpl = ""
134         private = {}
135         public = {}
136         try:
137             with open_relative_file(scenario_cfg["traffic_profile"],
138                                     scenario_cfg["task_path"]) as infile:
139                 traffic_profile_tpl = infile.read()
140
141         except (KeyError, IOError, OSError):
142             raise
143
144         return [traffic_profile_tpl, private, public]
145
146     def _fill_traffic_profile(self, scenario_cfg, context_cfg):
147         flow = self._get_traffic_flow(scenario_cfg)
148
149         imix = self._get_traffic_imix(scenario_cfg)
150
151         traffic_mapping, private, public = \
152             self._get_traffic_profile(scenario_cfg, context_cfg)
153
154         traffic_profile = vnfdgen.generate_vnfd(traffic_mapping,
155                                                 {"imix": imix, "flow": flow,
156                                                  "private": private,
157                                                  "public": public})
158
159         return TrafficProfile.get(traffic_profile)
160
161     @classmethod
162     def _find_vnf_name_from_id(cls, topology, vnf_id):
163         return next((vnfd["vnfd-id-ref"]
164                      for vnfd in topology["constituent-vnfd"]
165                      if vnf_id == vnfd["member-vnf-index"]), None)
166
167     @staticmethod
168     def get_vld_networks(networks):
169         return {n['vld_id']: n for n in networks.values()}
170
171     def _resolve_topology(self, context_cfg, topology):
172         for vld in topology["vld"]:
173             try:
174                 node_0, node_1 = vld["vnfd-connection-point-ref"]
175             except (TypeError, ValueError):
176                 raise IncorrectConfig("Topology file corrupted, "
177                                       "wrong number of endpoints for connection")
178
179             node_0_name = self._find_vnf_name_from_id(topology,
180                                                       node_0["member-vnf-index-ref"])
181             node_1_name = self._find_vnf_name_from_id(topology,
182                                                       node_1["member-vnf-index-ref"])
183
184             node_0_ifname = node_0["vnfd-connection-point-ref"]
185             node_1_ifname = node_1["vnfd-connection-point-ref"]
186
187             node_0_if = context_cfg["nodes"][node_0_name]["interfaces"][node_0_ifname]
188             node_1_if = context_cfg["nodes"][node_1_name]["interfaces"][node_1_ifname]
189             try:
190                 vld_networks = self.get_vld_networks(context_cfg["networks"])
191
192                 node_0_if["vld_id"] = vld["id"]
193                 node_1_if["vld_id"] = vld["id"]
194
195                 # set peer name
196                 node_0_if["peer_name"] = node_1_name
197                 node_1_if["peer_name"] = node_0_name
198
199                 # set peer interface name
200                 node_0_if["peer_ifname"] = node_1_ifname
201                 node_1_if["peer_ifname"] = node_0_ifname
202
203                 # just load the whole network dict
204                 node_0_if["network"] = vld_networks.get(vld["id"], {})
205                 node_1_if["network"] = vld_networks.get(vld["id"], {})
206
207                 node_0_if["dst_mac"] = node_1_if["local_mac"]
208                 node_0_if["dst_ip"] = node_1_if["local_ip"]
209
210                 node_1_if["dst_mac"] = node_0_if["local_mac"]
211                 node_1_if["dst_ip"] = node_0_if["local_ip"]
212
213                 # add peer interface dict, but remove circular link
214                 # TODO: don't waste memory
215                 node_0_copy = node_0_if.copy()
216                 node_1_copy = node_1_if.copy()
217                 node_0_if["peer_intf"] = node_1_copy
218                 node_1_if["peer_intf"] = node_0_copy
219             except KeyError:
220                 raise IncorrectConfig("Required interface not found, "
221                                       "topology file corrupted")
222
223     @classmethod
224     def _find_list_index_from_vnf_idx(cls, topology, vnf_idx):
225         return next((topology["constituent-vnfd"].index(vnfd)
226                      for vnfd in topology["constituent-vnfd"]
227                      if vnf_idx == vnfd["member-vnf-index"]), None)
228
229     def _update_context_with_topology(self, context_cfg, topology):
230         for idx in topology["constituent-vnfd"]:
231             vnf_idx = idx["member-vnf-index"]
232             nodes = context_cfg["nodes"]
233             node = self._find_vnf_name_from_id(topology, vnf_idx)
234             list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
235             nodes[node].update(topology["constituent-vnfd"][list_idx])
236
237     @staticmethod
238     def _sort_dpdk_port_num(netdevs):
239         # dpdk_port_num is PCI BUS ID ordering, lowest first
240         s = sorted(netdevs.values(), key=itemgetter('pci_bus_id'))
241         for dpdk_port_num, netdev in enumerate(s, 1):
242             netdev['dpdk_port_num'] = dpdk_port_num
243
244     @classmethod
245     def _probe_missing_values(cls, netdevs, network, missing):
246         mac = network['local_mac']
247         for netdev in netdevs.values():
248             if netdev['address'].lower() == mac.lower():
249                 network['driver'] = netdev['driver']
250                 network['vpci'] = netdev['pci_bus_id']
251                 network['dpdk_port_num'] = netdev['dpdk_port_num']
252                 network['ifindex'] = netdev['ifindex']
253
254     TOPOLOGY_REQUIRED_KEYS = frozenset({
255         "vpci", "local_ip", "netmask", "local_mac", "driver", "dpdk_port_num"})
256
257     def map_topology_to_infrastructure(self, context_cfg, topology):
258         """ This method should verify if the available resources defined in pod.yaml
259         match the topology.yaml file.
260
261         :param topology:
262         :return: None. Side effect: context_cfg is updated
263         """
264
265         for node, node_dict in context_cfg["nodes"].items():
266
267             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
268             with SshManager(node_dict) as conn:
269                 exit_status = conn.execute(cmd)[0]
270                 if exit_status != 0:
271                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
272                 exit_status, stdout, _ = conn.execute(
273                     self.FIND_NETDEVICE_STRING)
274                 if exit_status != 0:
275                     raise IncorrectSetup(
276                         "Cannot find netdev info in sysfs" % node)
277                 netdevs = node_dict['netdevs'] = self.parse_netdev_info(
278                     stdout)
279                 self._sort_dpdk_port_num(netdevs)
280
281                 for network in node_dict["interfaces"].values():
282                     missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
283                     if missing:
284                         try:
285                             self._probe_missing_values(netdevs, network,
286                                                        missing)
287                         except KeyError:
288                             pass
289                         else:
290                             missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
291                                 network)
292                         if missing:
293                             raise IncorrectConfig(
294                                 "Require interface fields '%s' "
295                                 "not found, topology file "
296                                 "corrupted" % ', '.join(missing))
297
298         # 3. Use topology file to find connections & resolve dest address
299         self._resolve_topology(context_cfg, topology)
300         self._update_context_with_topology(context_cfg, topology)
301
302     FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
303 $1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
304 $1/device/subsystem_vendor $1/device/subsystem_device ; \
305 printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
306 ' sh  \{\}/* \;
307 """
308     BASE_ADAPTER_RE = re.compile(
309         '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
310
311     @classmethod
312     def parse_netdev_info(cls, stdout):
313         network_devices = defaultdict(dict)
314         matches = cls.BASE_ADAPTER_RE.findall(stdout)
315         for bus_path, interface_name, name, value in matches:
316             dirname, bus_id = os.path.split(bus_path)
317             if 'virtio' in bus_id:
318                 # for some stupid reason VMs include virtio1/
319                 # in PCI device path
320                 bus_id = os.path.basename(dirname)
321             # remove extra 'device/' from 'device/vendor,
322             # device/subsystem_vendor', etc.
323             if 'device/' in name:
324                 name = name.split('/')[1]
325             network_devices[interface_name][name] = value
326             network_devices[interface_name][
327                 'interface_name'] = interface_name
328             network_devices[interface_name]['pci_bus_id'] = bus_id
329         # convert back to regular dict
330         return dict(network_devices)
331
332     @classmethod
333     def get_vnf_impl(cls, vnf_model_id):
334         """ Find the implementing class from vnf_model["vnf"]["name"] field
335
336         :param vnf_model_id: parsed vnfd model ID field
337         :return: subclass of GenericVNF
338         """
339         import_modules_from_package(
340             "yardstick.network_services.vnf_generic.vnf")
341         expected_name = vnf_model_id
342         classes_found = []
343
344         def impl():
345             for name, class_ in ((c.__name__, c) for c in itersubclasses(GenericVNF)):
346                 if name == expected_name:
347                     yield class_
348                 classes_found.append(name)
349
350         try:
351             return next(impl())
352         except StopIteration:
353             pass
354
355         raise IncorrectConfig("No implementation for %s found in %s" %
356                               (expected_name, classes_found))
357
358     @staticmethod
359     def update_interfaces_from_node(vnfd, node):
360         for intf in vnfd["vdu"][0]["external-interface"]:
361             node_intf = node['interfaces'][intf['name']]
362             intf['virtual-interface'].update(node_intf)
363
364     def load_vnf_models(self, scenario_cfg, context_cfg):
365         """ Create VNF objects based on YAML descriptors
366
367         :param scenario_cfg:
368         :type scenario_cfg:
369         :param context_cfg:
370         :return:
371         """
372         vnfs = []
373         for node_name, node in context_cfg["nodes"].items():
374             LOG.debug(node)
375             with open_relative_file(node["VNF model"],
376                                     scenario_cfg['task_path']) as stream:
377                 vnf_model = stream.read()
378             vnfd = vnfdgen.generate_vnfd(vnf_model, node)
379             # TODO: here add extra context_cfg["nodes"] regardless of template
380             vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0]
381             self.update_interfaces_from_node(vnfd, node)
382             vnf_impl = self.get_vnf_impl(vnfd['id'])
383             vnf_instance = vnf_impl(vnfd)
384             vnf_instance.name = node_name
385             vnfs.append(vnf_instance)
386
387         return vnfs
388
389     def setup(self):
390         """ Setup infrastructure, provission VNFs & start traffic
391
392         :return:
393         """
394         # 1. Verify if infrastructure mapping can meet topology
395         self.map_topology_to_infrastructure(self.context_cfg, self.topology)
396         # 1a. Load VNF models
397         self.vnfs = self.load_vnf_models(self.scenario_cfg, self.context_cfg)
398         # 1b. Fill traffic profile with information from topology
399         self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
400                                                           self.context_cfg)
401
402         # 2. Provision VNFs
403         try:
404             for vnf in self.vnfs:
405                 LOG.info("Instantiating %s", vnf.name)
406                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
407         except RuntimeError:
408             for vnf in self.vnfs:
409                 vnf.terminate()
410             raise
411
412         # 3. Run experiment
413         # Start listeners first to avoid losing packets
414         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
415         for traffic_gen in traffic_runners:
416             traffic_gen.listen_traffic(self.traffic_profile)
417
418         # register collector with yardstick for KPI collection.
419         self.collector = Collector(self.vnfs, self.traffic_profile)
420         self.collector.start()
421
422         # Start the actual traffic
423         for traffic_gen in traffic_runners:
424             LOG.info("Starting traffic on %s", traffic_gen.name)
425             traffic_gen.run_traffic(self.traffic_profile)
426
427     def run(self, result):  # yardstick API
428         """ Yardstick calls run() at intervals defined in the yaml and
429             produces timestamped samples
430
431         :param result: dictionary with results to update
432         :return: None
433         """
434
435         for vnf in self.vnfs:
436             # Result example:
437             # {"VNF1: { "tput" : [1000, 999] }, "VNF2": { "latency": 100 }}
438             LOG.debug("vnf")
439             result.update(self.collector.get_kpi(vnf))
440
441     def teardown(self):
442         """ Stop the collector and terminate VNF & TG instance
443
444         :return
445         """
446
447         self.collector.stop()
448         for vnf in self.vnfs:
449             LOG.info("Stopping %s", vnf.name)
450             vnf.terminate()