af17a315005a8751278fdcfa7745a781cc5f0848
[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 itertools import chain
24 import yaml
25 from operator import itemgetter
26 from collections import defaultdict
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 find_relative_file(path, task_path):
84     # fixme: create schema to validate all fields have been provided
85     try:
86         with open(path):
87             pass
88         return path
89     except IOError as e:
90         if e.errno != errno.ENOENT:
91             raise
92         else:
93             rel_path = os.path.join(task_path, path)
94             with open(rel_path):
95                 pass
96             return rel_path
97
98
99 def open_relative_file(path, task_path):
100     try:
101         return open(path)
102     except IOError as e:
103         if e.errno == errno.ENOENT:
104             return open(os.path.join(task_path, path))
105         raise
106
107
108 class NetworkServiceTestCase(base.Scenario):
109     """Class handles Generic framework to do pre-deployment VNF &
110        Network service testing  """
111
112     __scenario_type__ = "NSPerf"
113
114     def __init__(self, scenario_cfg, context_cfg):  # Yardstick API
115         super(NetworkServiceTestCase, self).__init__()
116         self.scenario_cfg = scenario_cfg
117         self.context_cfg = context_cfg
118
119         # fixme: create schema to validate all fields have been provided
120         with open_relative_file(scenario_cfg["topology"],
121                                 scenario_cfg['task_path']) as stream:
122             topology_yaml = yaml.safe_load(stream)
123
124         self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
125         self.vnfs = []
126         self.collector = None
127         self.traffic_profile = None
128
129     def _get_traffic_flow(self):
130         try:
131             with open(self.scenario_cfg["traffic_options"]["flow"]) as fflow:
132                 flow = yaml.safe_load(fflow)
133         except (KeyError, IOError, OSError):
134             flow = {}
135         return flow
136
137     def _get_traffic_imix(self):
138         try:
139             with open(self.scenario_cfg["traffic_options"]["imix"]) as fimix:
140                 imix = yaml.safe_load(fimix)
141         except (KeyError, IOError, OSError):
142             imix = {}
143         return imix
144
145     def _get_traffic_profile(self):
146         profile = self.scenario_cfg["traffic_profile"]
147         path = self.scenario_cfg["task_path"]
148         with open_relative_file(profile, path) as infile:
149             return infile.read()
150
151     def _fill_traffic_profile(self):
152         traffic_mapping = self._get_traffic_profile()
153         traffic_map_data = {
154             'flow': self._get_traffic_flow(),
155             'imix': self._get_traffic_imix(),
156             'private': {},
157             'public': {},
158         }
159
160         traffic_vnfd = vnfdgen.generate_vnfd(traffic_mapping, traffic_map_data)
161         self.traffic_profile = TrafficProfile.get(traffic_vnfd)
162         return self.traffic_profile
163
164     def _find_vnf_name_from_id(self, vnf_id):
165         return next((vnfd["vnfd-id-ref"]
166                      for vnfd in self.topology["constituent-vnfd"]
167                      if vnf_id == vnfd["member-vnf-index"]), None)
168
169     @staticmethod
170     def get_vld_networks(networks):
171         return {n['vld_id']: n for n in networks.values()}
172
173     def _resolve_topology(self):
174         for vld in self.topology["vld"]:
175             try:
176                 node0_data, node1_data = vld["vnfd-connection-point-ref"]
177             except (ValueError, TypeError):
178                 raise IncorrectConfig("Topology file corrupted, "
179                                       "wrong endpoint count for connection")
180
181             node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
182             node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
183
184             node0_if_name = node0_data["vnfd-connection-point-ref"]
185             node1_if_name = node1_data["vnfd-connection-point-ref"]
186
187             try:
188                 nodes = self.context_cfg["nodes"]
189                 node0_if = nodes[node0_name]["interfaces"][node0_if_name]
190                 node1_if = nodes[node1_name]["interfaces"][node1_if_name]
191
192                 # names so we can do reverse lookups
193                 node0_if["ifname"] = node0_if_name
194                 node1_if["ifname"] = node1_if_name
195
196                 node0_if["node_name"] = node0_name
197                 node1_if["node_name"] = node1_name
198
199                 vld_networks = self.get_vld_networks(self.context_cfg["networks"])
200                 node0_if["vld_id"] = vld["id"]
201                 node1_if["vld_id"] = vld["id"]
202
203                 # set peer name
204                 node0_if["peer_name"] = node1_name
205                 node1_if["peer_name"] = node0_name
206
207                 # set peer interface name
208                 node0_if["peer_ifname"] = node1_if_name
209                 node1_if["peer_ifname"] = node0_if_name
210
211                 # just load the network
212                 node0_if["network"] = vld_networks.get(vld["id"], {})
213                 node1_if["network"] = vld_networks.get(vld["id"], {})
214
215                 node0_if["dst_mac"] = node1_if["local_mac"]
216                 node0_if["dst_ip"] = node1_if["local_ip"]
217
218                 node1_if["dst_mac"] = node0_if["local_mac"]
219                 node1_if["dst_ip"] = node0_if["local_ip"]
220
221             except KeyError:
222                 LOG.exception("")
223                 raise IncorrectConfig("Required interface not found, "
224                                       "topology file corrupted")
225
226         for vld in self.topology['vld']:
227             try:
228                 node0_data, node1_data = vld["vnfd-connection-point-ref"]
229             except (ValueError, TypeError):
230                 raise IncorrectConfig("Topology file corrupted, "
231                                       "wrong endpoint count for connection")
232
233             node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
234             node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
235
236             node0_if_name = node0_data["vnfd-connection-point-ref"]
237             node1_if_name = node1_data["vnfd-connection-point-ref"]
238
239             nodes = self.context_cfg["nodes"]
240             node0_if = nodes[node0_name]["interfaces"][node0_if_name]
241             node1_if = nodes[node1_name]["interfaces"][node1_if_name]
242
243             # add peer interface dict, but remove circular link
244             # TODO: don't waste memory
245             node0_copy = node0_if.copy()
246             node1_copy = node1_if.copy()
247             node0_if["peer_intf"] = node1_copy
248             node1_if["peer_intf"] = node0_copy
249
250     def _find_vnfd_from_vnf_idx(self, vnf_idx):
251         return next((vnfd for vnfd in self.topology["constituent-vnfd"]
252                      if vnf_idx == vnfd["member-vnf-index"]), None)
253
254     def _update_context_with_topology(self):
255         for vnfd in self.topology["constituent-vnfd"]:
256             vnf_idx = vnfd["member-vnf-index"]
257             vnf_name = self._find_vnf_name_from_id(vnf_idx)
258             vnfd = self._find_vnfd_from_vnf_idx(vnf_idx)
259             self.context_cfg["nodes"][vnf_name].update(vnfd)
260
261     @staticmethod
262     def _sort_dpdk_port_num(netdevs):
263         # dpdk_port_num is PCI BUS ID ordering, lowest first
264         s = sorted(netdevs.values(), key=itemgetter('pci_bus_id'))
265         for dpdk_port_num, netdev in enumerate(s):
266             netdev['dpdk_port_num'] = dpdk_port_num
267
268     @classmethod
269     def _probe_missing_values(cls, netdevs, network, missing):
270         mac_lower = network['local_mac'].lower()
271         for netdev in netdevs.values():
272             if netdev['address'].lower() != mac_lower:
273                 continue
274             network.update({
275                 'driver': netdev['driver'],
276                 'vpci': netdev['pci_bus_id'],
277                 'ifindex': netdev['ifindex'],
278             })
279
280     TOPOLOGY_REQUIRED_KEYS = frozenset({
281         "vpci", "local_ip", "netmask", "local_mac", "driver"})
282
283     def map_topology_to_infrastructure(self):
284         """ This method should verify if the available resources defined in pod.yaml
285         match the topology.yaml file.
286
287         :param context_cfg:
288         :param topology:
289         :return: None. Side effect: context_cfg is updated
290         """
291         for node, node_dict in self.context_cfg["nodes"].items():
292
293             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
294             with SshManager(node_dict) as conn:
295                 exit_status = conn.execute(cmd)[0]
296                 if exit_status != 0:
297                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
298                 exit_status, stdout, _ = conn.execute(
299                     self.FIND_NETDEVICE_STRING)
300                 if exit_status != 0:
301                     raise IncorrectSetup(
302                         "Cannot find netdev info in sysfs" % node)
303                 netdevs = node_dict['netdevs'] = self.parse_netdev_info(
304                     stdout)
305
306                 for network in node_dict["interfaces"].values():
307                     missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
308                     if not missing:
309                         continue
310
311                     try:
312                         self._probe_missing_values(netdevs, network,
313                                                    missing)
314                     except KeyError:
315                         pass
316                     else:
317                         missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
318                             network)
319                     if missing:
320                         raise IncorrectConfig(
321                             "Require interface fields '%s' not found, topology file "
322                             "corrupted" % ', '.join(missing))
323
324         # 3. Use topology file to find connections & resolve dest address
325         self._resolve_topology()
326         self._update_context_with_topology()
327
328     FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
329 $1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
330 $1/device/subsystem_vendor $1/device/subsystem_device ; \
331 printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
332 ' sh  \{\}/* \;
333 """
334     BASE_ADAPTER_RE = re.compile(
335         '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
336
337     @classmethod
338     def parse_netdev_info(cls, stdout):
339         network_devices = defaultdict(dict)
340         matches = cls.BASE_ADAPTER_RE.findall(stdout)
341         for bus_path, interface_name, name, value in matches:
342             dirname, bus_id = os.path.split(bus_path)
343             if 'virtio' in bus_id:
344                 # for some stupid reason VMs include virtio1/
345                 # in PCI device path
346                 bus_id = os.path.basename(dirname)
347             # remove extra 'device/' from 'device/vendor,
348             # device/subsystem_vendor', etc.
349             if 'device/' in name:
350                 name = name.split('/')[1]
351             network_devices[interface_name][name] = value
352             network_devices[interface_name][
353                 'interface_name'] = interface_name
354             network_devices[interface_name]['pci_bus_id'] = bus_id
355         # convert back to regular dict
356         return dict(network_devices)
357
358     @classmethod
359     def get_vnf_impl(cls, vnf_model_id):
360         """ Find the implementing class from vnf_model["vnf"]["name"] field
361
362         :param vnf_model_id: parsed vnfd model ID field
363         :return: subclass of GenericVNF
364         """
365         import_modules_from_package(
366             "yardstick.network_services.vnf_generic.vnf")
367         expected_name = vnf_model_id
368         classes_found = []
369
370         def impl():
371             for name, class_ in ((c.__name__, c) for c in itersubclasses(GenericVNF)):
372                 if name == expected_name:
373                     yield class_
374                 classes_found.append(name)
375
376         try:
377             return next(impl())
378         except StopIteration:
379             pass
380
381         raise IncorrectConfig("No implementation for %s found in %s" %
382                               (expected_name, classes_found))
383
384     @staticmethod
385     def update_interfaces_from_node(vnfd, node):
386         for intf in vnfd["vdu"][0]["external-interface"]:
387             node_intf = node['interfaces'][intf['name']]
388             intf['virtual-interface'].update(node_intf)
389
390     def load_vnf_models(self, scenario_cfg=None, context_cfg=None):
391         """ Create VNF objects based on YAML descriptors
392
393         :param scenario_cfg:
394         :type scenario_cfg:
395         :param context_cfg:
396         :return:
397         """
398         if scenario_cfg is None:
399             scenario_cfg = self.scenario_cfg
400
401         if context_cfg is None:
402             context_cfg = self.context_cfg
403
404         vnfs = []
405         # we assume OrderedDict for consistenct in instantiation
406         for node_name, node in context_cfg["nodes"].items():
407             LOG.debug(node)
408             file_name = node["VNF model"]
409             file_path = scenario_cfg['task_path']
410             with open_relative_file(file_name, file_path) as stream:
411                 vnf_model = stream.read()
412             vnfd = vnfdgen.generate_vnfd(vnf_model, node)
413             # TODO: here add extra context_cfg["nodes"] regardless of template
414             vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0]
415             self.update_interfaces_from_node(vnfd, node)
416             vnf_impl = self.get_vnf_impl(vnfd['id'])
417             vnf_instance = vnf_impl(node_name, vnfd)
418             vnfs.append(vnf_instance)
419
420         self.vnfs = vnfs
421         return vnfs
422
423     def setup(self):
424         """ Setup infrastructure, provission VNFs & start traffic
425
426         :return:
427         """
428         # 1. Verify if infrastructure mapping can meet topology
429         self.map_topology_to_infrastructure()
430         # 1a. Load VNF models
431         self.load_vnf_models()
432         # 1b. Fill traffic profile with information from topology
433         self._fill_traffic_profile()
434
435         # 2. Provision VNFs
436
437         # link events will cause VNF application to exit
438         # so we should start traffic runners before VNFs
439         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
440         non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
441         try:
442             for vnf in chain(traffic_runners, non_traffic_runners):
443                 LOG.info("Instantiating %s", vnf.name)
444                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
445             for vnf in chain(traffic_runners, non_traffic_runners):
446                 LOG.info("Waiting for %s to instantiate", vnf.name)
447                 vnf.wait_for_instantiate()
448         except RuntimeError:
449             for vnf in self.vnfs:
450                 vnf.terminate()
451             raise
452
453         # 3. Run experiment
454         # Start listeners first to avoid losing packets
455         for traffic_gen in traffic_runners:
456             traffic_gen.listen_traffic(self.traffic_profile)
457
458         # register collector with yardstick for KPI collection.
459         self.collector = Collector(self.vnfs, self.traffic_profile)
460         self.collector.start()
461
462         # Start the actual traffic
463         for traffic_gen in traffic_runners:
464             LOG.info("Starting traffic on %s", traffic_gen.name)
465             traffic_gen.run_traffic(self.traffic_profile)
466
467     def run(self, result):  # yardstick API
468         """ Yardstick calls run() at intervals defined in the yaml and
469             produces timestamped samples
470
471         :param result: dictionary with results to update
472         :return: None
473         """
474
475         for vnf in self.vnfs:
476             # Result example:
477             # {"VNF1: { "tput" : [1000, 999] }, "VNF2": { "latency": 100 }}
478             LOG.debug("vnf")
479             result.update(self.collector.get_kpi(vnf))
480
481     def teardown(self):
482         """ Stop the collector and terminate VNF & TG instance
483
484         :return
485         """
486
487         self.collector.stop()
488         for vnf in self.vnfs:
489             LOG.info("Stopping %s", vnf.name)
490             vnf.terminate()