NSB update
[yardstick.git] / yardstick / benchmark / scenarios / networking / vnf_generic.py
index d7ba418..af17a31 100644 (file)
 
 from __future__ import absolute_import
 import logging
 
 from __future__ import absolute_import
 import logging
-from contextlib import contextmanager
+
+import errno
+import os
+
+import re
+from itertools import chain
 import yaml
 import yaml
+from operator import itemgetter
+from collections import defaultdict
 
 from yardstick.benchmark.scenarios import base
 from yardstick.common.utils import import_modules_from_package, itersubclasses
 
 from yardstick.benchmark.scenarios import base
 from yardstick.common.utils import import_modules_from_package, itersubclasses
@@ -49,31 +56,53 @@ class IncorrectSetup(Exception):
     pass
 
 
     pass
 
 
-@contextmanager
-def ssh_manager(node):
-    """
-    args -> network device mappings
-    returns -> ssh connection ready to be used
-    """
-    conn = None
-    try:
-        ssh_port = node.get("ssh_port", ssh.DEFAULT_PORT)
-        conn = ssh.SSH(user=node.get("user", ""),
-                       host=node.get("ip", ""),
-                       password=node.get("password", ""),
-                       port=ssh_port)
-        conn.wait()
-
-    except (SSHError) as error:
-        LOG.info("connect failed to %s, due to %s", node.get("ip", ""), error)
+class SshManager(object):
+    def __init__(self, node):
+        super(SshManager, self).__init__()
+        self.node = node
+        self.conn = None
+
+    def __enter__(self):
+        """
+        args -> network device mappings
+        returns -> ssh connection ready to be used
+        """
+        try:
+            self.conn = ssh.SSH.from_node(self.node)
+            self.conn.wait()
+        except SSHError as error:
+            LOG.info("connect failed to %s, due to %s", self.node["ip"], error)
+        # self.conn defaults to None
+        return self.conn
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if self.conn:
+            self.conn.close()
+
+
+def find_relative_file(path, task_path):
+    # fixme: create schema to validate all fields have been provided
     try:
     try:
-        if conn:
-            yield conn
+        with open(path):
+            pass
+        return path
+    except IOError as e:
+        if e.errno != errno.ENOENT:
+            raise
         else:
         else:
-            yield False
-    finally:
-        if conn:
-            conn.close()
+            rel_path = os.path.join(task_path, path)
+            with open(rel_path):
+                pass
+            return rel_path
+
+
+def open_relative_file(path, task_path):
+    try:
+        return open(path)
+    except IOError as e:
+        if e.errno == errno.ENOENT:
+            return open(os.path.join(task_path, path))
+        raise
 
 
 class NetworkServiceTestCase(base.Scenario):
 
 
 class NetworkServiceTestCase(base.Scenario):
@@ -88,179 +117,307 @@ class NetworkServiceTestCase(base.Scenario):
         self.context_cfg = context_cfg
 
         # fixme: create schema to validate all fields have been provided
         self.context_cfg = context_cfg
 
         # fixme: create schema to validate all fields have been provided
-        with open(scenario_cfg["topology"]) as stream:
-            self.topology = yaml.load(stream)["nsd:nsd-catalog"]["nsd"][0]
+        with open_relative_file(scenario_cfg["topology"],
+                                scenario_cfg['task_path']) as stream:
+            topology_yaml = yaml.safe_load(stream)
+
+        self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
         self.vnfs = []
         self.collector = None
         self.traffic_profile = None
 
         self.vnfs = []
         self.collector = None
         self.traffic_profile = None
 
-    @classmethod
-    def _get_traffic_flow(cls, scenario_cfg):
+    def _get_traffic_flow(self):
         try:
         try:
-            with open(scenario_cfg["traffic_options"]["flow"]) as fflow:
-                flow = yaml.load(fflow)
+            with open(self.scenario_cfg["traffic_options"]["flow"]) as fflow:
+                flow = yaml.safe_load(fflow)
         except (KeyError, IOError, OSError):
             flow = {}
         return flow
 
         except (KeyError, IOError, OSError):
             flow = {}
         return flow
 
-    @classmethod
-    def _get_traffic_imix(cls, scenario_cfg):
+    def _get_traffic_imix(self):
         try:
         try:
-            with open(scenario_cfg["traffic_options"]["imix"]) as fimix:
-                imix = yaml.load(fimix)
+            with open(self.scenario_cfg["traffic_options"]["imix"]) as fimix:
+                imix = yaml.safe_load(fimix)
         except (KeyError, IOError, OSError):
             imix = {}
         return imix
 
         except (KeyError, IOError, OSError):
             imix = {}
         return imix
 
-    @classmethod
-    def _get_traffic_profile(cls, scenario_cfg, context_cfg):
-        traffic_profile_tpl = ""
-        private = {}
-        public = {}
-        try:
-            with open(scenario_cfg["traffic_profile"]) as infile:
-                traffic_profile_tpl = infile.read()
+    def _get_traffic_profile(self):
+        profile = self.scenario_cfg["traffic_profile"]
+        path = self.scenario_cfg["task_path"]
+        with open_relative_file(profile, path) as infile:
+            return infile.read()
+
+    def _fill_traffic_profile(self):
+        traffic_mapping = self._get_traffic_profile()
+        traffic_map_data = {
+            'flow': self._get_traffic_flow(),
+            'imix': self._get_traffic_imix(),
+            'private': {},
+            'public': {},
+        }
+
+        traffic_vnfd = vnfdgen.generate_vnfd(traffic_mapping, traffic_map_data)
+        self.traffic_profile = TrafficProfile.get(traffic_vnfd)
+        return self.traffic_profile
+
+    def _find_vnf_name_from_id(self, vnf_id):
+        return next((vnfd["vnfd-id-ref"]
+                     for vnfd in self.topology["constituent-vnfd"]
+                     if vnf_id == vnfd["member-vnf-index"]), None)
 
 
-        except (KeyError, IOError, OSError):
-            raise
+    @staticmethod
+    def get_vld_networks(networks):
+        return {n['vld_id']: n for n in networks.values()}
 
 
-        return [traffic_profile_tpl, private, public]
+    def _resolve_topology(self):
+        for vld in self.topology["vld"]:
+            try:
+                node0_data, node1_data = vld["vnfd-connection-point-ref"]
+            except (ValueError, TypeError):
+                raise IncorrectConfig("Topology file corrupted, "
+                                      "wrong endpoint count for connection")
 
 
-    def _fill_traffic_profile(self, scenario_cfg, context_cfg):
-        traffic_profile = {}
+            node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
+            node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
 
 
-        flow = self._get_traffic_flow(scenario_cfg)
+            node0_if_name = node0_data["vnfd-connection-point-ref"]
+            node1_if_name = node1_data["vnfd-connection-point-ref"]
 
 
-        imix = self._get_traffic_imix(scenario_cfg)
+            try:
+                nodes = self.context_cfg["nodes"]
+                node0_if = nodes[node0_name]["interfaces"][node0_if_name]
+                node1_if = nodes[node1_name]["interfaces"][node1_if_name]
 
 
-        traffic_mapping, private, public = \
-            self._get_traffic_profile(scenario_cfg, context_cfg)
+                # names so we can do reverse lookups
+                node0_if["ifname"] = node0_if_name
+                node1_if["ifname"] = node1_if_name
 
 
-        traffic_profile = vnfdgen.generate_vnfd(traffic_mapping,
-                                                {"imix": imix, "flow": flow,
-                                                 "private": private,
-                                                 "public": public})
+                node0_if["node_name"] = node0_name
+                node1_if["node_name"] = node1_name
 
 
-        return TrafficProfile.get(traffic_profile)
+                vld_networks = self.get_vld_networks(self.context_cfg["networks"])
+                node0_if["vld_id"] = vld["id"]
+                node1_if["vld_id"] = vld["id"]
 
 
-    @classmethod
-    def _find_vnf_name_from_id(cls, topology, vnf_id):
-        return next((vnfd["vnfd-id-ref"]
-                     for vnfd in topology["constituent-vnfd"]
-                     if vnf_id == vnfd["member-vnf-index"]), None)
+                # set peer name
+                node0_if["peer_name"] = node1_name
+                node1_if["peer_name"] = node0_name
 
 
-    def _resolve_topology(self, context_cfg, topology):
-        for vld in topology["vld"]:
-            if len(vld["vnfd-connection-point-ref"]) > 2:
-                raise IncorrectConfig("Topology file corrupted, "
-                                      "too many endpoint for connection")
+                # set peer interface name
+                node0_if["peer_ifname"] = node1_if_name
+                node1_if["peer_ifname"] = node0_if_name
 
 
-            node_0, node_1 = vld["vnfd-connection-point-ref"]
+                # just load the network
+                node0_if["network"] = vld_networks.get(vld["id"], {})
+                node1_if["network"] = vld_networks.get(vld["id"], {})
 
 
-            node0 = self._find_vnf_name_from_id(topology,
-                                                node_0["member-vnf-index-ref"])
-            node1 = self._find_vnf_name_from_id(topology,
-                                                node_1["member-vnf-index-ref"])
+                node0_if["dst_mac"] = node1_if["local_mac"]
+                node0_if["dst_ip"] = node1_if["local_ip"]
 
 
-            if0 = node_0["vnfd-connection-point-ref"]
-            if1 = node_1["vnfd-connection-point-ref"]
+                node1_if["dst_mac"] = node0_if["local_mac"]
+                node1_if["dst_ip"] = node0_if["local_ip"]
 
 
-            try:
-                nodes = context_cfg["nodes"]
-                nodes[node0]["interfaces"][if0]["vld_id"] = vld["id"]
-                nodes[node1]["interfaces"][if1]["vld_id"] = vld["id"]
-
-                nodes[node0]["interfaces"][if0]["dst_mac"] = \
-                    nodes[node1]["interfaces"][if1]["local_mac"]
-                nodes[node0]["interfaces"][if0]["dst_ip"] = \
-                    nodes[node1]["interfaces"][if1]["local_ip"]
-
-                nodes[node1]["interfaces"][if1]["dst_mac"] = \
-                    nodes[node0]["interfaces"][if0]["local_mac"]
-                nodes[node1]["interfaces"][if1]["dst_ip"] = \
-                    nodes[node0]["interfaces"][if0]["local_ip"]
             except KeyError:
             except KeyError:
-                raise IncorrectConfig("Required interface not found,"
+                LOG.exception("")
+                raise IncorrectConfig("Required interface not found, "
                                       "topology file corrupted")
 
                                       "topology file corrupted")
 
-    @classmethod
-    def _find_list_index_from_vnf_idx(cls, topology, vnf_idx):
-        return next((topology["constituent-vnfd"].index(vnfd)
-                     for vnfd in topology["constituent-vnfd"]
+        for vld in self.topology['vld']:
+            try:
+                node0_data, node1_data = vld["vnfd-connection-point-ref"]
+            except (ValueError, TypeError):
+                raise IncorrectConfig("Topology file corrupted, "
+                                      "wrong endpoint count for connection")
+
+            node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
+            node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
+
+            node0_if_name = node0_data["vnfd-connection-point-ref"]
+            node1_if_name = node1_data["vnfd-connection-point-ref"]
+
+            nodes = self.context_cfg["nodes"]
+            node0_if = nodes[node0_name]["interfaces"][node0_if_name]
+            node1_if = nodes[node1_name]["interfaces"][node1_if_name]
+
+            # add peer interface dict, but remove circular link
+            # TODO: don't waste memory
+            node0_copy = node0_if.copy()
+            node1_copy = node1_if.copy()
+            node0_if["peer_intf"] = node1_copy
+            node1_if["peer_intf"] = node0_copy
+
+    def _find_vnfd_from_vnf_idx(self, vnf_idx):
+        return next((vnfd for vnfd in self.topology["constituent-vnfd"]
                      if vnf_idx == vnfd["member-vnf-index"]), None)
 
                      if vnf_idx == vnfd["member-vnf-index"]), None)
 
-    def _update_context_with_topology(self, context_cfg, topology):
-        for idx in topology["constituent-vnfd"]:
-            vnf_idx = idx["member-vnf-index"]
-            nodes = context_cfg["nodes"]
-            node = self._find_vnf_name_from_id(topology, vnf_idx)
-            list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
-            nodes[node].update(topology["constituent-vnfd"][list_idx])
+    def _update_context_with_topology(self):
+        for vnfd in self.topology["constituent-vnfd"]:
+            vnf_idx = vnfd["member-vnf-index"]
+            vnf_name = self._find_vnf_name_from_id(vnf_idx)
+            vnfd = self._find_vnfd_from_vnf_idx(vnf_idx)
+            self.context_cfg["nodes"][vnf_name].update(vnfd)
 
 
-    def map_topology_to_infrastructure(self, context_cfg, topology):
+    @staticmethod
+    def _sort_dpdk_port_num(netdevs):
+        # dpdk_port_num is PCI BUS ID ordering, lowest first
+        s = sorted(netdevs.values(), key=itemgetter('pci_bus_id'))
+        for dpdk_port_num, netdev in enumerate(s):
+            netdev['dpdk_port_num'] = dpdk_port_num
+
+    @classmethod
+    def _probe_missing_values(cls, netdevs, network, missing):
+        mac_lower = network['local_mac'].lower()
+        for netdev in netdevs.values():
+            if netdev['address'].lower() != mac_lower:
+                continue
+            network.update({
+                'driver': netdev['driver'],
+                'vpci': netdev['pci_bus_id'],
+                'ifindex': netdev['ifindex'],
+            })
+
+    TOPOLOGY_REQUIRED_KEYS = frozenset({
+        "vpci", "local_ip", "netmask", "local_mac", "driver"})
+
+    def map_topology_to_infrastructure(self):
         """ This method should verify if the available resources defined in pod.yaml
         match the topology.yaml file.
 
         """ This method should verify if the available resources defined in pod.yaml
         match the topology.yaml file.
 
+        :param context_cfg:
         :param topology:
         :return: None. Side effect: context_cfg is updated
         """
         :param topology:
         :return: None. Side effect: context_cfg is updated
         """
-
-        for node, node_dict in context_cfg["nodes"].items():
+        for node, node_dict in self.context_cfg["nodes"].items():
 
             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
 
             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
-            with ssh_manager(node_dict) as conn:
+            with SshManager(node_dict) as conn:
                 exit_status = conn.execute(cmd)[0]
                 if exit_status != 0:
                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
                 exit_status = conn.execute(cmd)[0]
                 if exit_status != 0:
                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
-
-                for interface in node_dict["interfaces"]:
-                    network = node_dict["interfaces"][interface]
-                    keys = ["vpci", "local_ip", "netmask",
-                            "local_mac", "driver", "dpdk_port_num"]
-                    missing = set(keys).difference(network)
+                exit_status, stdout, _ = conn.execute(
+                    self.FIND_NETDEVICE_STRING)
+                if exit_status != 0:
+                    raise IncorrectSetup(
+                        "Cannot find netdev info in sysfs" % node)
+                netdevs = node_dict['netdevs'] = self.parse_netdev_info(
+                    stdout)
+
+                for network in node_dict["interfaces"].values():
+                    missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
+                    if not missing:
+                        continue
+
+                    try:
+                        self._probe_missing_values(netdevs, network,
+                                                   missing)
+                    except KeyError:
+                        pass
+                    else:
+                        missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
+                            network)
                     if missing:
                     if missing:
-                        raise IncorrectConfig("Require interface fields '%s' "
-                                              "not found, topology file "
-                                              "corrupted" % ', '.join(missing))
+                        raise IncorrectConfig(
+                            "Require interface fields '%s' not found, topology file "
+                            "corrupted" % ', '.join(missing))
 
         # 3. Use topology file to find connections & resolve dest address
 
         # 3. Use topology file to find connections & resolve dest address
-        self._resolve_topology(context_cfg, topology)
-        self._update_context_with_topology(context_cfg, topology)
+        self._resolve_topology()
+        self._update_context_with_topology()
+
+    FIND_NETDEVICE_STRING = r"""find /sys/devices/pci* -type d -name net -exec sh -c '{ grep -sH ^ \
+$1/ifindex $1/address $1/operstate $1/device/vendor $1/device/device \
+$1/device/subsystem_vendor $1/device/subsystem_device ; \
+printf "%s/driver:" $1 ; basename $(readlink -s $1/device/driver); } \
+' sh  \{\}/* \;
+"""
+    BASE_ADAPTER_RE = re.compile(
+        '^/sys/devices/(.*)/net/([^/]*)/([^:]*):(.*)$', re.M)
+
+    @classmethod
+    def parse_netdev_info(cls, stdout):
+        network_devices = defaultdict(dict)
+        matches = cls.BASE_ADAPTER_RE.findall(stdout)
+        for bus_path, interface_name, name, value in matches:
+            dirname, bus_id = os.path.split(bus_path)
+            if 'virtio' in bus_id:
+                # for some stupid reason VMs include virtio1/
+                # in PCI device path
+                bus_id = os.path.basename(dirname)
+            # remove extra 'device/' from 'device/vendor,
+            # device/subsystem_vendor', etc.
+            if 'device/' in name:
+                name = name.split('/')[1]
+            network_devices[interface_name][name] = value
+            network_devices[interface_name][
+                'interface_name'] = interface_name
+            network_devices[interface_name]['pci_bus_id'] = bus_id
+        # convert back to regular dict
+        return dict(network_devices)
 
     @classmethod
 
     @classmethod
-    def get_vnf_impl(cls, vnf_model):
+    def get_vnf_impl(cls, vnf_model_id):
         """ Find the implementing class from vnf_model["vnf"]["name"] field
 
         """ Find the implementing class from vnf_model["vnf"]["name"] field
 
-        :param vnf_model: dictionary containing a parsed vnfd
+        :param vnf_model_id: parsed vnfd model ID field
         :return: subclass of GenericVNF
         """
         import_modules_from_package(
             "yardstick.network_services.vnf_generic.vnf")
         :return: subclass of GenericVNF
         """
         import_modules_from_package(
             "yardstick.network_services.vnf_generic.vnf")
-        expected_name = vnf_model['id']
-        impl = [c for c in itersubclasses(GenericVNF)
-                if c.__name__ == expected_name]
+        expected_name = vnf_model_id
+        classes_found = []
+
+        def impl():
+            for name, class_ in ((c.__name__, c) for c in itersubclasses(GenericVNF)):
+                if name == expected_name:
+                    yield class_
+                classes_found.append(name)
+
         try:
         try:
-            return next(iter(impl))
+            return next(impl())
         except StopIteration:
         except StopIteration:
-            raise IncorrectConfig("No implementation for %s", expected_name)
+            pass
+
+        raise IncorrectConfig("No implementation for %s found in %s" %
+                              (expected_name, classes_found))
+
+    @staticmethod
+    def update_interfaces_from_node(vnfd, node):
+        for intf in vnfd["vdu"][0]["external-interface"]:
+            node_intf = node['interfaces'][intf['name']]
+            intf['virtual-interface'].update(node_intf)
 
 
-    def load_vnf_models(self, context_cfg):
+    def load_vnf_models(self, scenario_cfg=None, context_cfg=None):
         """ Create VNF objects based on YAML descriptors
 
         """ Create VNF objects based on YAML descriptors
 
+        :param scenario_cfg:
+        :type scenario_cfg:
         :param context_cfg:
         :return:
         """
         :param context_cfg:
         :return:
         """
+        if scenario_cfg is None:
+            scenario_cfg = self.scenario_cfg
+
+        if context_cfg is None:
+            context_cfg = self.context_cfg
+
         vnfs = []
         vnfs = []
-        for node in context_cfg["nodes"]:
-            LOG.debug(context_cfg["nodes"][node])
-            with open(context_cfg["nodes"][node]["VNF model"]) as stream:
+        # we assume OrderedDict for consistenct in instantiation
+        for node_name, node in context_cfg["nodes"].items():
+            LOG.debug(node)
+            file_name = node["VNF model"]
+            file_path = scenario_cfg['task_path']
+            with open_relative_file(file_name, file_path) as stream:
                 vnf_model = stream.read()
                 vnf_model = stream.read()
-            vnfd = vnfdgen.generate_vnfd(vnf_model, context_cfg["nodes"][node])
-            vnf_impl = self.get_vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
-            vnf_instance = vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
-            vnf_instance.name = node
+            vnfd = vnfdgen.generate_vnfd(vnf_model, node)
+            # TODO: here add extra context_cfg["nodes"] regardless of template
+            vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0]
+            self.update_interfaces_from_node(vnfd, node)
+            vnf_impl = self.get_vnf_impl(vnfd['id'])
+            vnf_instance = vnf_impl(node_name, vnfd)
             vnfs.append(vnf_instance)
 
             vnfs.append(vnf_instance)
 
+        self.vnfs = vnfs
         return vnfs
 
     def setup(self):
         return vnfs
 
     def setup(self):
@@ -268,20 +425,26 @@ class NetworkServiceTestCase(base.Scenario):
 
         :return:
         """
 
         :return:
         """
-
         # 1. Verify if infrastructure mapping can meet topology
         # 1. Verify if infrastructure mapping can meet topology
-        self.map_topology_to_infrastructure(self.context_cfg, self.topology)
+        self.map_topology_to_infrastructure()
         # 1a. Load VNF models
         # 1a. Load VNF models
-        self.vnfs = self.load_vnf_models(self.context_cfg)
+        self.load_vnf_models()
         # 1b. Fill traffic profile with information from topology
         # 1b. Fill traffic profile with information from topology
-        self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
-                                                          self.context_cfg)
+        self._fill_traffic_profile()
 
         # 2. Provision VNFs
 
         # 2. Provision VNFs
+
+        # link events will cause VNF application to exit
+        # so we should start traffic runners before VNFs
+        traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
+        non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
         try:
         try:
-            for vnf in self.vnfs:
+            for vnf in chain(traffic_runners, non_traffic_runners):
                 LOG.info("Instantiating %s", vnf.name)
                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
                 LOG.info("Instantiating %s", vnf.name)
                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
+            for vnf in chain(traffic_runners, non_traffic_runners):
+                LOG.info("Waiting for %s to instantiate", vnf.name)
+                vnf.wait_for_instantiate()
         except RuntimeError:
             for vnf in self.vnfs:
                 vnf.terminate()
         except RuntimeError:
             for vnf in self.vnfs:
                 vnf.terminate()
@@ -289,7 +452,6 @@ class NetworkServiceTestCase(base.Scenario):
 
         # 3. Run experiment
         # Start listeners first to avoid losing packets
 
         # 3. Run experiment
         # Start listeners first to avoid losing packets
-        traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
         for traffic_gen in traffic_runners:
             traffic_gen.listen_traffic(self.traffic_profile)
 
         for traffic_gen in traffic_runners:
             traffic_gen.listen_traffic(self.traffic_profile)