Enable vnf/tg instantiate as blocking call.
[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         :return: None. Side effect: context_cfg is updated
288         """
289         for node, node_dict in self.context_cfg["nodes"].items():
290
291             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
292             with SshManager(node_dict) as conn:
293                 exit_status = conn.execute(cmd)[0]
294                 if exit_status != 0:
295                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
296                 exit_status, stdout, _ = conn.execute(
297                     self.FIND_NETDEVICE_STRING)
298                 if exit_status != 0:
299                     raise IncorrectSetup(
300                         "Cannot find netdev info in sysfs" % node)
301                 netdevs = node_dict['netdevs'] = self.parse_netdev_info(
302                     stdout)
303
304                 for network in node_dict["interfaces"].values():
305                     missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
306                     if not missing:
307                         continue
308
309                     try:
310                         self._probe_missing_values(netdevs, network,
311                                                    missing)
312                     except KeyError:
313                         pass
314                     else:
315                         missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
316                             network)
317                     if missing:
318                         raise IncorrectConfig(
319                             "Require interface fields '%s' not found, topology file "
320                             "corrupted" % ', '.join(missing))
321
322         # 3. Use topology file to find connections & resolve dest address
323         self._resolve_topology()
324         self._update_context_with_topology()
325
326     FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
327 $1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
328 $1/device/subsystem_vendor $1/device/subsystem_device ; \
329 printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
330 ' sh  \{\}/* \;
331 """
332     BASE_ADAPTER_RE = re.compile(
333         '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
334
335     @classmethod
336     def parse_netdev_info(cls, stdout):
337         network_devices = defaultdict(dict)
338         matches = cls.BASE_ADAPTER_RE.findall(stdout)
339         for bus_path, interface_name, name, value in matches:
340             dirname, bus_id = os.path.split(bus_path)
341             if 'virtio' in bus_id:
342                 # for some stupid reason VMs include virtio1/
343                 # in PCI device path
344                 bus_id = os.path.basename(dirname)
345             # remove extra 'device/' from 'device/vendor,
346             # device/subsystem_vendor', etc.
347             if 'device/' in name:
348                 name = name.split('/')[1]
349             network_devices[interface_name][name] = value
350             network_devices[interface_name][
351                 'interface_name'] = interface_name
352             network_devices[interface_name]['pci_bus_id'] = bus_id
353         # convert back to regular dict
354         return dict(network_devices)
355
356     @classmethod
357     def get_vnf_impl(cls, vnf_model_id):
358         """ Find the implementing class from vnf_model["vnf"]["name"] field
359
360         :param vnf_model_id: parsed vnfd model ID field
361         :return: subclass of GenericVNF
362         """
363         import_modules_from_package(
364             "yardstick.network_services.vnf_generic.vnf")
365         expected_name = vnf_model_id
366         classes_found = []
367
368         def impl():
369             for name, class_ in ((c.__name__, c) for c in itersubclasses(GenericVNF)):
370                 if name == expected_name:
371                     yield class_
372                 classes_found.append(name)
373
374         try:
375             return next(impl())
376         except StopIteration:
377             pass
378
379         raise IncorrectConfig("No implementation for %s found in %s" %
380                               (expected_name, classes_found))
381
382     @staticmethod
383     def update_interfaces_from_node(vnfd, node):
384         for intf in vnfd["vdu"][0]["external-interface"]:
385             node_intf = node['interfaces'][intf['name']]
386             intf['virtual-interface'].update(node_intf)
387
388     def load_vnf_models(self, scenario_cfg=None, context_cfg=None):
389         """ Create VNF objects based on YAML descriptors
390
391         :param scenario_cfg:
392         :type scenario_cfg:
393         :param context_cfg:
394         :return:
395         """
396         if scenario_cfg is None:
397             scenario_cfg = self.scenario_cfg
398
399         if context_cfg is None:
400             context_cfg = self.context_cfg
401
402         vnfs = []
403         # we assume OrderedDict for consistenct in instantiation
404         for node_name, node in context_cfg["nodes"].items():
405             LOG.debug(node)
406             file_name = node["VNF model"]
407             file_path = scenario_cfg['task_path']
408             with open_relative_file(file_name, file_path) as stream:
409                 vnf_model = stream.read()
410             vnfd = vnfdgen.generate_vnfd(vnf_model, node)
411             # TODO: here add extra context_cfg["nodes"] regardless of template
412             vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0]
413             self.update_interfaces_from_node(vnfd, node)
414             vnf_impl = self.get_vnf_impl(vnfd['id'])
415             vnf_instance = vnf_impl(node_name, vnfd)
416             vnfs.append(vnf_instance)
417
418         self.vnfs = vnfs
419         return vnfs
420
421     def setup(self):
422         """ Setup infrastructure, provission VNFs & start traffic
423
424         :return:
425         """
426         # 1. Verify if infrastructure mapping can meet topology
427         self.map_topology_to_infrastructure()
428         # 1a. Load VNF models
429         self.load_vnf_models()
430         # 1b. Fill traffic profile with information from topology
431         self._fill_traffic_profile()
432
433         # 2. Provision VNFs
434
435         # link events will cause VNF application to exit
436         # so we should start traffic runners before VNFs
437         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
438         non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
439         try:
440             for vnf in chain(traffic_runners, non_traffic_runners):
441                 LOG.info("Instantiating %s", vnf.name)
442                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
443                 LOG.info("Waiting for %s to instantiate", vnf.name)
444                 vnf.wait_for_instantiate()
445         except RuntimeError:
446             for vnf in self.vnfs:
447                 vnf.terminate()
448             raise
449
450         # 3. Run experiment
451         # Start listeners first to avoid losing packets
452         for traffic_gen in traffic_runners:
453             traffic_gen.listen_traffic(self.traffic_profile)
454
455         # register collector with yardstick for KPI collection.
456         self.collector = Collector(self.vnfs, self.traffic_profile)
457         self.collector.start()
458
459         # Start the actual traffic
460         for traffic_gen in traffic_runners:
461             LOG.info("Starting traffic on %s", traffic_gen.name)
462             traffic_gen.run_traffic(self.traffic_profile)
463
464     def run(self, result):  # yardstick API
465         """ Yardstick calls run() at intervals defined in the yaml and
466             produces timestamped samples
467
468         :param result: dictionary with results to update
469         :return: None
470         """
471
472         for vnf in self.vnfs:
473             # Result example:
474             # {"VNF1: { "tput" : [1000, 999] }, "VNF2": { "latency": 100 }}
475             LOG.debug("vnf")
476             result.update(self.collector.get_kpi(vnf))
477
478     def teardown(self):
479         """ Stop the collector and terminate VNF & TG instance
480
481         :return
482         """
483
484         self.collector.stop()
485         for vnf in self.vnfs:
486             LOG.info("Stopping %s", vnf.name)
487             vnf.terminate()