HA testcase containerized Compass support
[yardstick.git] / yardstick / benchmark / scenarios / networking / vnf_generic.py
index d7ba418..594edea 100644 (file)
 
 from __future__ import absolute_import
 import logging
-from contextlib import contextmanager
+
+import errno
+import os
+
+import re
+from operator import itemgetter
+from collections import defaultdict
+
 import yaml
 
 from yardstick.benchmark.scenarios import base
@@ -49,31 +56,37 @@ class IncorrectSetup(Exception):
     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 open_relative_file(path, task_path):
     try:
-        if conn:
-            yield conn
-        else:
-            yield False
-    finally:
-        if conn:
-            conn.close()
+        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):
@@ -88,8 +101,11 @@ class NetworkServiceTestCase(base.Scenario):
         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.load(stream)
+
+        self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
         self.vnfs = []
         self.collector = None
         self.traffic_profile = None
@@ -118,7 +134,8 @@ class NetworkServiceTestCase(base.Scenario):
         private = {}
         public = {}
         try:
-            with open(scenario_cfg["traffic_profile"]) as infile:
+            with open_relative_file(scenario_cfg["traffic_profile"],
+                                    scenario_cfg["task_path"]) as infile:
                 traffic_profile_tpl = infile.read()
 
         except (KeyError, IOError, OSError):
@@ -127,8 +144,6 @@ class NetworkServiceTestCase(base.Scenario):
         return [traffic_profile_tpl, private, public]
 
     def _fill_traffic_profile(self, scenario_cfg, context_cfg):
-        traffic_profile = {}
-
         flow = self._get_traffic_flow(scenario_cfg)
 
         imix = self._get_traffic_imix(scenario_cfg)
@@ -197,6 +212,26 @@ class NetworkServiceTestCase(base.Scenario):
             list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
             nodes[node].update(topology["constituent-vnfd"][list_idx])
 
+    @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, 1):
+            netdev['dpdk_port_num'] = dpdk_port_num
+
+    @classmethod
+    def _probe_missing_values(cls, netdevs, network, missing):
+        mac = network['local_mac']
+        for netdev in netdevs.values():
+            if netdev['address'].lower() == mac.lower():
+                network['driver'] = netdev['driver']
+                network['vpci'] = netdev['pci_bus_id']
+                network['dpdk_port_num'] = netdev['dpdk_port_num']
+                network['ifindex'] = netdev['ifindex']
+
+    TOPOLOGY_REQUIRED_KEYS = frozenset({
+        "vpci", "local_ip", "netmask", "local_mac", "driver", "dpdk_port_num"})
+
     def map_topology_to_infrastructure(self, context_cfg, topology):
         """ This method should verify if the available resources defined in pod.yaml
         match the topology.yaml file.
@@ -208,25 +243,70 @@ class NetworkServiceTestCase(base.Scenario):
         for node, node_dict in context_cfg["nodes"].items():
 
             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)
-
-                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)
+                self._sort_dpdk_port_num(netdevs)
+
+                for network in node_dict["interfaces"].values():
+                    missing = self.TOPOLOGY_REQUIRED_KEYS.difference(network)
                     if missing:
-                        raise IncorrectConfig("Require interface fields '%s' "
-                                              "not found, topology file "
-                                              "corrupted" % ', '.join(missing))
+                        try:
+                            self._probe_missing_values(netdevs, network,
+                                                       missing)
+                        except KeyError:
+                            pass
+                        else:
+                            missing = self.TOPOLOGY_REQUIRED_KEYS.difference(
+                                network)
+                        if missing:
+                            raise IncorrectConfig(
+                                "Require interface fields '%s' "
+                                "not found, topology file "
+                                "corrupted" % ', '.join(missing))
 
         # 3. Use topology file to find connections & resolve dest address
         self._resolve_topology(context_cfg, topology)
         self._update_context_with_topology(context_cfg, 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
     def get_vnf_impl(cls, vnf_model):
         """ Find the implementing class from vnf_model["vnf"]["name"] field
@@ -237,28 +317,31 @@ class NetworkServiceTestCase(base.Scenario):
         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]
+        impl = (c for c in itersubclasses(GenericVNF)
+                if c.__name__ == expected_name)
         try:
-            return next(iter(impl))
+            return next(impl)
         except StopIteration:
             raise IncorrectConfig("No implementation for %s", expected_name)
 
-    def load_vnf_models(self, context_cfg):
+    def load_vnf_models(self, scenario_cfg, context_cfg):
         """ Create VNF objects based on YAML descriptors
 
+        :param scenario_cfg:
+        :type scenario_cfg:
         :param context_cfg:
         :return:
         """
         vnfs = []
-        for node in context_cfg["nodes"]:
-            LOG.debug(context_cfg["nodes"][node])
-            with open(context_cfg["nodes"][node]["VNF model"]) as stream:
+        for node_name, node in context_cfg["nodes"].items():
+            LOG.debug(node)
+            with open_relative_file(node["VNF model"],
+                                    scenario_cfg['task_path']) as stream:
                 vnf_model = stream.read()
-            vnfd = vnfdgen.generate_vnfd(vnf_model, context_cfg["nodes"][node])
+            vnfd = vnfdgen.generate_vnfd(vnf_model, 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
+            vnf_instance.name = node_name
             vnfs.append(vnf_instance)
 
         return vnfs
@@ -268,11 +351,10 @@ class NetworkServiceTestCase(base.Scenario):
 
         :return:
         """
-
         # 1. Verify if infrastructure mapping can meet topology
         self.map_topology_to_infrastructure(self.context_cfg, self.topology)
         # 1a. Load VNF models
-        self.vnfs = self.load_vnf_models(self.context_cfg)
+        self.vnfs = self.load_vnf_models(self.scenario_cfg, self.context_cfg)
         # 1b. Fill traffic profile with information from topology
         self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
                                                           self.context_cfg)