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