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