HA testcase containerized Compass support
[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     def _resolve_topology(self, context_cfg, topology):
168         for vld in topology["vld"]:
169             if len(vld["vnfd-connection-point-ref"]) > 2:
170                 raise IncorrectConfig("Topology file corrupted, "
171                                       "too many endpoint for connection")
172
173             node_0, node_1 = vld["vnfd-connection-point-ref"]
174
175             node0 = self._find_vnf_name_from_id(topology,
176                                                 node_0["member-vnf-index-ref"])
177             node1 = self._find_vnf_name_from_id(topology,
178                                                 node_1["member-vnf-index-ref"])
179
180             if0 = node_0["vnfd-connection-point-ref"]
181             if1 = node_1["vnfd-connection-point-ref"]
182
183             try:
184                 nodes = context_cfg["nodes"]
185                 nodes[node0]["interfaces"][if0]["vld_id"] = vld["id"]
186                 nodes[node1]["interfaces"][if1]["vld_id"] = vld["id"]
187
188                 nodes[node0]["interfaces"][if0]["dst_mac"] = \
189                     nodes[node1]["interfaces"][if1]["local_mac"]
190                 nodes[node0]["interfaces"][if0]["dst_ip"] = \
191                     nodes[node1]["interfaces"][if1]["local_ip"]
192
193                 nodes[node1]["interfaces"][if1]["dst_mac"] = \
194                     nodes[node0]["interfaces"][if0]["local_mac"]
195                 nodes[node1]["interfaces"][if1]["dst_ip"] = \
196                     nodes[node0]["interfaces"][if0]["local_ip"]
197             except KeyError:
198                 raise IncorrectConfig("Required interface not found,"
199                                       "topology file corrupted")
200
201     @classmethod
202     def _find_list_index_from_vnf_idx(cls, topology, vnf_idx):
203         return next((topology["constituent-vnfd"].index(vnfd)
204                      for vnfd in topology["constituent-vnfd"]
205                      if vnf_idx == vnfd["member-vnf-index"]), None)
206
207     def _update_context_with_topology(self, context_cfg, topology):
208         for idx in topology["constituent-vnfd"]:
209             vnf_idx = idx["member-vnf-index"]
210             nodes = context_cfg["nodes"]
211             node = self._find_vnf_name_from_id(topology, vnf_idx)
212             list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
213             nodes[node].update(topology["constituent-vnfd"][list_idx])
214
215     @staticmethod
216     def _sort_dpdk_port_num(netdevs):
217         # dpdk_port_num is PCI BUS ID ordering, lowest first
218         s = sorted(netdevs.values(), key=itemgetter('pci_bus_id'))
219         for dpdk_port_num, netdev in enumerate(s, 1):
220             netdev['dpdk_port_num'] = dpdk_port_num
221
222     @classmethod
223     def _probe_missing_values(cls, netdevs, network, missing):
224         mac = network['local_mac']
225         for netdev in netdevs.values():
226             if netdev['address'].lower() == mac.lower():
227                 network['driver'] = netdev['driver']
228                 network['vpci'] = netdev['pci_bus_id']
229                 network['dpdk_port_num'] = netdev['dpdk_port_num']
230                 network['ifindex'] = netdev['ifindex']
231
232     TOPOLOGY_REQUIRED_KEYS = frozenset({
233         "vpci", "local_ip", "netmask", "local_mac", "driver", "dpdk_port_num"})
234
235     def map_topology_to_infrastructure(self, context_cfg, topology):
236         """ This method should verify if the available resources defined in pod.yaml
237         match the topology.yaml file.
238
239         :param topology:
240         :return: None. Side effect: context_cfg is updated
241         """
242
243         for node, node_dict in context_cfg["nodes"].items():
244
245             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
246             with SshManager(node_dict) as conn:
247                 exit_status = conn.execute(cmd)[0]
248                 if exit_status != 0:
249                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
250                 exit_status, stdout, _ = conn.execute(
251                     self.FIND_NETDEVICE_STRING)
252                 if exit_status != 0:
253                     raise IncorrectSetup(
254                         "Cannot find netdev info in sysfs" % node)
255                 netdevs = node_dict['netdevs'] = self.parse_netdev_info(
256                     stdout)
257                 self._sort_dpdk_port_num(netdevs)
258
259                 for network in node_dict["interfaces"].values():
260                     missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
261                     if missing:
262                         try:
263                             self._probe_missing_values(netdevs, network,
264                                                        missing)
265                         except KeyError:
266                             pass
267                         else:
268                             missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
269                                 network)
270                         if missing:
271                             raise IncorrectConfig(
272                                 "Require interface fields '%s' "
273                                 "not found, topology file "
274                                 "corrupted" % ', '.join(missing))
275
276         # 3. Use topology file to find connections & resolve dest address
277         self._resolve_topology(context_cfg, topology)
278         self._update_context_with_topology(context_cfg, topology)
279
280     FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
281 $1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
282 $1/device/subsystem_vendor $1/device/subsystem_device ; \
283 printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
284 ' sh  \{\}/* \;
285 """
286     BASE_ADAPTER_RE = re.compile(
287         '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
288
289     @classmethod
290     def parse_netdev_info(cls, stdout):
291         network_devices = defaultdict(dict)
292         matches = cls.BASE_ADAPTER_RE.findall(stdout)
293         for bus_path, interface_name, name, value in matches:
294             dirname, bus_id = os.path.split(bus_path)
295             if 'virtio' in bus_id:
296                 # for some stupid reason VMs include virtio1/
297                 # in PCI device path
298                 bus_id = os.path.basename(dirname)
299             # remove extra 'device/' from 'device/vendor,
300             # device/subsystem_vendor', etc.
301             if 'device/' in name:
302                 name = name.split('/')[1]
303             network_devices[interface_name][name] = value
304             network_devices[interface_name][
305                 'interface_name'] = interface_name
306             network_devices[interface_name]['pci_bus_id'] = bus_id
307         # convert back to regular dict
308         return dict(network_devices)
309
310     @classmethod
311     def get_vnf_impl(cls, vnf_model):
312         """ Find the implementing class from vnf_model["vnf"]["name"] field
313
314         :param vnf_model: dictionary containing a parsed vnfd
315         :return: subclass of GenericVNF
316         """
317         import_modules_from_package(
318             "yardstick.network_services.vnf_generic.vnf")
319         expected_name = vnf_model['id']
320         impl = (c for c in itersubclasses(GenericVNF)
321                 if c.__name__ == expected_name)
322         try:
323             return next(impl)
324         except StopIteration:
325             raise IncorrectConfig("No implementation for %s", expected_name)
326
327     def load_vnf_models(self, scenario_cfg, context_cfg):
328         """ Create VNF objects based on YAML descriptors
329
330         :param scenario_cfg:
331         :type scenario_cfg:
332         :param context_cfg:
333         :return:
334         """
335         vnfs = []
336         for node_name, node in context_cfg["nodes"].items():
337             LOG.debug(node)
338             with open_relative_file(node["VNF model"],
339                                     scenario_cfg['task_path']) as stream:
340                 vnf_model = stream.read()
341             vnfd = vnfdgen.generate_vnfd(vnf_model, node)
342             vnf_impl = self.get_vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
343             vnf_instance = vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
344             vnf_instance.name = node_name
345             vnfs.append(vnf_instance)
346
347         return vnfs
348
349     def setup(self):
350         """ Setup infrastructure, provission VNFs & start traffic
351
352         :return:
353         """
354         # 1. Verify if infrastructure mapping can meet topology
355         self.map_topology_to_infrastructure(self.context_cfg, self.topology)
356         # 1a. Load VNF models
357         self.vnfs = self.load_vnf_models(self.scenario_cfg, self.context_cfg)
358         # 1b. Fill traffic profile with information from topology
359         self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
360                                                           self.context_cfg)
361
362         # 2. Provision VNFs
363         try:
364             for vnf in self.vnfs:
365                 LOG.info("Instantiating %s", vnf.name)
366                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
367         except RuntimeError:
368             for vnf in self.vnfs:
369                 vnf.terminate()
370             raise
371
372         # 3. Run experiment
373         # Start listeners first to avoid losing packets
374         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
375         for traffic_gen in traffic_runners:
376             traffic_gen.listen_traffic(self.traffic_profile)
377
378         # register collector with yardstick for KPI collection.
379         self.collector = Collector(self.vnfs, self.traffic_profile)
380         self.collector.start()
381
382         # Start the actual traffic
383         for traffic_gen in traffic_runners:
384             LOG.info("Starting traffic on %s", traffic_gen.name)
385             traffic_gen.run_traffic(self.traffic_profile)
386
387     def run(self, result):  # yardstick API
388         """ Yardstick calls run() at intervals defined in the yaml and
389             produces timestamped samples
390
391         :param result: dictionary with results to update
392         :return: None
393         """
394
395         for vnf in self.vnfs:
396             # Result example:
397             # {"VNF1: { "tput" : [1000, 999] }, "VNF2": { "latency": 100 }}
398             LOG.debug("vnf")
399             result.update(self.collector.get_kpi(vnf))
400
401     def teardown(self):
402         """ Stop the collector and terminate VNF & TG instance
403
404         :return
405         """
406
407         self.collector.stop()
408         for vnf in self.vnfs:
409             LOG.info("Stopping %s", vnf.name)
410             vnf.terminate()