aboutsummaryrefslogtreecommitdiffstats
path: root/nfvbench/chaining.py
diff options
context:
space:
mode:
Diffstat (limited to 'nfvbench/chaining.py')
-rw-r--r--nfvbench/chaining.py988
1 files changed, 988 insertions, 0 deletions
diff --git a/nfvbench/chaining.py b/nfvbench/chaining.py
new file mode 100644
index 0000000..e5a9f0a
--- /dev/null
+++ b/nfvbench/chaining.py
@@ -0,0 +1,988 @@
+#!/usr/bin/env python
+# Copyright 2018 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+# This module takes care of chaining networks, ports and vms
+#
+"""NFVBENCH CHAIN DISCOVERY/STAGING.
+
+This module takes care of staging/discovering all resources that are participating in a
+benchmarking session: flavors, networks, ports, VNF instances.
+If a resource is discovered with the same name, it will be reused.
+Otherwise it will be created.
+
+ChainManager: manages VM image, flavor, the staging discovery of all chains
+ has 1 or more chains
+Chain: manages one chain, has 2 or more networks and 1 or more instances
+ChainNetwork: manages 1 network in a chain
+ChainVnf: manages 1 VNF instance in a chain, has 2 ports
+ChainVnfPort: manages 1 instance port
+
+ChainManager-->Chain(*)
+Chain-->ChainNetwork(*),ChainVnf(*)
+ChainVnf-->ChainVnfPort(2)
+
+Once created/discovered, instances are checked to be in the active state (ready to pass traffic)
+Configuration parameters that will influence how these resources are staged/related:
+- openstack or no openstack
+- chain type
+- number of chains
+- number of VNF in each chain (PVP, PVVP)
+- SRIOV and middle port SRIOV for port types
+- whether networks are shared across chains or not
+
+There is not traffic generation involved in this module.
+"""
+import os
+import re
+import time
+
+from glanceclient.v2 import client as glanceclient
+from neutronclient.neutron import client as neutronclient
+from novaclient.client import Client
+
+from attrdict import AttrDict
+import compute
+from log import LOG
+from specs import ChainType
+
+# Left and right index for network and port lists
+LEFT = 0
+RIGHT = 1
+# Name of the VM config file
+NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
+# full pathame of the VM config in the VM
+NFVBENCH_CFG_VM_PATHNAME = os.path.join('/etc/', NFVBENCH_CFG_FILENAME)
+# full path of the boot shell script template file on the server where nfvbench runs
+BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ 'nfvbenchvm',
+ NFVBENCH_CFG_FILENAME)
+
+
+class ChainException(Exception):
+ """Exception while operating the chains."""
+
+ pass
+
+class NetworkEncaps(object):
+ """Network encapsulation."""
+
+
+class ChainFlavor(object):
+ """Class to manage the chain flavor."""
+
+ def __init__(self, flavor_name, flavor_dict, comp):
+ """Create a flavor."""
+ self.name = flavor_name
+ self.comp = comp
+ self.flavor = self.comp.find_flavor(flavor_name)
+ self.reuse = False
+ if self.flavor:
+ self.reuse = True
+ LOG.info("Reused flavor '%s'", flavor_name)
+ else:
+ extra_specs = flavor_dict.pop('extra_specs', None)
+
+ self.flavor = comp.create_flavor(flavor_name,
+ **flavor_dict)
+
+ LOG.info("Created flavor '%s'", flavor_name)
+ if extra_specs:
+ self.flavor.set_keys(extra_specs)
+
+ def delete(self):
+ """Delete this flavor."""
+ if not self.reuse and self.flavor:
+ self.flavor.delete()
+ LOG.info("Flavor '%s' deleted", self.name)
+
+
+class ChainVnfPort(object):
+ """A port associated to one VNF in the chain."""
+
+ def __init__(self, name, vnf, chain_network, vnic_type):
+ """Create or reuse a port on a given network.
+
+ if vnf.instance is None the VNF instance is not reused and this ChainVnfPort instance must
+ create a new port.
+ Otherwise vnf.instance is a reused VNF instance and this ChainVnfPort instance must
+ find an existing port to reuse that matches the port requirements: same attached network,
+ instance, name, vnic type
+
+ name: name for this port
+ vnf: ChainVNf instance that owns this port
+ chain_network: ChainNetwork instance where this port should attach
+ vnic_type: required vnic type for this port
+ """
+ self.name = name
+ self.vnf = vnf
+ self.manager = vnf.manager
+ self.reuse = False
+ self.port = None
+ if vnf.instance:
+ # VNF instance is reused, we need to find an existing port that matches this instance
+ # and network
+ # discover ports attached to this instance
+ port_list = self.manager.get_ports_from_network(chain_network)
+ for port in port_list:
+ if port['name'] != name:
+ continue
+ if port['binding:vnic_type'] != vnic_type:
+ continue
+ if port['device_id'] == vnf.get_uuid():
+ self.port = port
+ LOG.info('Reusing existing port %s mac=%s', name, port['mac_address'])
+ break
+ else:
+ raise ChainException('Cannot find matching port')
+ else:
+ # VNF instance is not created yet, we need to create a new port
+ body = {
+ "port": {
+ 'name': name,
+ 'network_id': chain_network.get_uuid(),
+ 'binding:vnic_type': vnic_type
+ }
+ }
+ port = self.manager.neutron_client.create_port(body)
+ self.port = port['port']
+ LOG.info('Created port %s', name)
+ try:
+ self.manager.neutron_client.update_port(self.port['id'], {
+ 'port': {
+ 'security_groups': [],
+ 'port_security_enabled': False,
+ }
+ })
+ LOG.info('Security disabled on port %s', name)
+ except Exception:
+ LOG.info('Failed to disable security on port %s (ignored)', name)
+
+ def get_mac(self):
+ """Get the MAC address for this port."""
+ return self.port['mac_address']
+
+ def delete(self):
+ """Delete this port instance."""
+ if self.reuse or not self.port:
+ return
+ retry = 0
+ while retry < self.manager.config.generic_retry_count:
+ try:
+ self.manager.neutron_client.delete_port(self.port['id'])
+ LOG.info("Deleted port %s", self.name)
+ return
+ except Exception:
+ retry += 1
+ time.sleep(self.manager.config.generic_poll_sec)
+ LOG.error('Unable to delete port: %s', self.name)
+
+
+class ChainNetwork(object):
+ """Could be a shared network across all chains or a chain private network."""
+
+ def __init__(self, manager, network_config, chain_id=None, lookup_only=False):
+ """Create a network for given chain."""
+ self.manager = manager
+ self.name = network_config.name
+ if chain_id is not None:
+ self.name += str(chain_id)
+ self.reuse = False
+ self.network = None
+ self.vlan = None
+ try:
+ self._setup(network_config, lookup_only)
+ except Exception:
+ if lookup_only:
+ LOG.error("Cannot find network %s", self.name)
+ else:
+ LOG.error("Error creating network %s", self.name)
+ self.delete()
+ raise
+
+ def _setup(self, network_config, lookup_only):
+ # Lookup if there is a matching network with same name
+ networks = self.manager.neutron_client.list_networks(name=self.name)
+ if networks['networks']:
+ network = networks['networks'][0]
+ # a network of same name already exists, we need to verify it has the same
+ # characteristics
+ if network_config.segmentation_id:
+ if network['provider:segmentation_id'] != network_config.segmentation_id:
+ raise ChainException("Mismatch of 'segmentation_id' for reused "
+ "network '{net}'. Network has id '{seg_id1}', "
+ "configuration requires '{seg_id2}'."
+ .format(net=self.name,
+ seg_id1=network['provider:segmentation_id'],
+ seg_id2=network_config.segmentation_id))
+
+ if network_config.physical_network:
+ if network['provider:physical_network'] != network_config.physical_network:
+ raise ChainException("Mismatch of 'physical_network' for reused "
+ "network '{net}'. Network has '{phys1}', "
+ "configuration requires '{phys2}'."
+ .format(net=self.name,
+ phys1=network['provider:physical_network'],
+ phys2=network_config.physical_network))
+
+ LOG.info('Reusing existing network %s', self.name)
+ self.reuse = True
+ self.network = network
+ else:
+ if lookup_only:
+ raise ChainException('Network %s not found' % self.name)
+ body = {
+ 'network': {
+ 'name': self.name,
+ 'admin_state_up': True
+ }
+ }
+ if network_config.network_type:
+ body['network']['provider:network_type'] = network_config.network_type
+ if network_config.segmentation_id:
+ body['network']['provider:segmentation_id'] = network_config.segmentation_id
+ if network_config.physical_network:
+ body['network']['provider:physical_network'] = network_config.physical_network
+
+ self.network = self.manager.neutron_client.create_network(body)['network']
+ body = {
+ 'subnet': {'name': network_config.subnet,
+ 'cidr': network_config.cidr,
+ 'network_id': self.network['id'],
+ 'enable_dhcp': False,
+ 'ip_version': 4,
+ 'dns_nameservers': []}
+ }
+ subnet = self.manager.neutron_client.create_subnet(body)['subnet']
+ # add subnet id to the network dict since it has just been added
+ self.network['subnets'] = [subnet['id']]
+ LOG.info('Created network: %s.', self.name)
+
+ def get_uuid(self):
+ """
+ Extract UUID of this network.
+
+ :return: UUID of this network
+ """
+ return self.network['id']
+
+ def get_vlan(self):
+ """
+ Extract vlan for this network.
+
+ :return: vlan ID for this network
+ """
+ if self.network['provider:network_type'] != 'vlan':
+ raise ChainException('Trying to retrieve VLAN id for non VLAN network')
+ return self.network['provider:segmentation_id']
+
+ def delete(self):
+ """Delete this network."""
+ if not self.reuse and self.network:
+ retry = 0
+ while retry < self.manager.config.generic_retry_count:
+ try:
+ self.manager.neutron_client.delete_network(self.network['id'])
+ LOG.info("Deleted network: %s", self.name)
+ return
+ except Exception:
+ retry += 1
+ LOG.info('Error deleting network %s (retry %d/%d)...',
+ self.name,
+ retry,
+ self.manager.config.generic_retry_count)
+ time.sleep(self.manager.config.generic_poll_sec)
+ LOG.error('Unable to delete network: %s', self.name)
+
+
+class ChainVnf(object):
+ """A class to represent a VNF in a chain."""
+
+ def __init__(self, chain, vnf_id, networks):
+ """Reuse a VNF instance with same characteristics or create a new VNF instance.
+
+ chain: the chain where this vnf belongs
+ vnf_id: indicates the index of this vnf in its chain (first vnf=0)
+ networks: the list of all networks (ChainNetwork) of the current chain
+ """
+ self.manager = chain.manager
+ self.chain = chain
+ self.vnf_id = vnf_id
+ self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
+ if len(networks) > 2:
+ # we will have more than 1 VM in each chain
+ self.name += '-' + str(vnf_id)
+ self.ports = []
+ self.status = None
+ self.instance = None
+ self.reuse = False
+ self.host_ip = None
+ try:
+ # the vnf_id is conveniently also the starting index in networks
+ # for the left and right networks associated to this VNF
+ self._setup(networks[vnf_id:vnf_id + 2])
+ except Exception:
+ LOG.error("Error creating VNF %s", self.name)
+ self.delete()
+ raise
+
+ def _get_vm_config(self, remote_mac_pair):
+ config = self.manager.config
+ devices = self.manager.generator_config.devices
+ with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
+ content = boot_script.read()
+ g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
+ g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
+ vm_config = {
+ 'forwarder': config.vm_forwarder,
+ 'intf_mac1': self.ports[LEFT].get_mac(),
+ 'intf_mac2': self.ports[RIGHT].get_mac(),
+ 'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
+ 'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
+ 'tg_net1': devices[LEFT].ip_addrs,
+ 'tg_net2': devices[RIGHT].ip_addrs,
+ 'vnf_gateway1_cidr': g1cidr,
+ 'vnf_gateway2_cidr': g2cidr,
+ 'tg_mac1': remote_mac_pair[0],
+ 'tg_mac2': remote_mac_pair[1]
+ }
+ return content.format(**vm_config)
+
+ def _get_vnic_type(self, port_index):
+ """Get the right vnic type for given port indexself.
+
+ If SR-IOV is speficied, middle ports in multi-VNF chains
+ can use vswitch or SR-IOV based on config.use_sriov_middle_net
+ """
+ if self.manager.config.sriov:
+ if self.manager.config.use_sriov_middle_net:
+ return 'direct'
+ if self.vnf_id == 0:
+ # first VNF in chain must use sriov for left port
+ if port_index == 0:
+ return 'direct'
+ elif (self.vnf_id == self.chain.get_length() - 1) and (port_index == 1):
+ # last VNF in chain must use sriov for right port
+ return 'direct'
+ return 'normal'
+
+ def _setup(self, networks):
+ flavor_id = self.manager.flavor.flavor.id
+ # Check if we can reuse an instance with same name
+ for instance in self.manager.existing_instances:
+ if instance.name == self.name:
+ # Verify that other instance characteristics match
+ if instance.flavor['id'] != flavor_id:
+ self._reuse_exception('Flavor mismatch')
+ if instance.status != "ACTIVE":
+ self._reuse_exception('Matching instance is not in ACTIVE state')
+ # The 2 networks for this instance must also be reused
+ if not networks[LEFT].reuse:
+ self._reuse_exception('network %s is new' % networks[LEFT].name)
+ if not networks[RIGHT].reuse:
+ self._reuse_exception('network %s is new' % networks[RIGHT].name)
+ # instance.networks have the network names as keys:
+ # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
+ if networks[LEFT].name not in instance.networks:
+ self._reuse_exception('Left network mismatch')
+ if networks[RIGHT].name not in instance.networks:
+ self._reuse_exception('Right network mismatch')
+ # Other checks not performed (yet)
+ # check if az and compute node match
+ self.reuse = True
+ self.instance = instance
+ LOG.info('Reusing existing instance %s on %s',
+ self.name, self.get_hypervisor_name())
+ # create or reuse/discover 2 ports per instance
+ self.ports = [ChainVnfPort(self.name + '-' + str(index),
+ self,
+ networks[index],
+ self._get_vnic_type(index)) for index in [0, 1]]
+ # if no reuse, actual vm creation is deferred after all ports in the chain are created
+ # since we need to know the next mac in a multi-vnf chain
+
+ def get_az(self):
+ """Get the AZ associated to this VNF."""
+ return self.manager.az[0]
+
+ def create_vnf(self, remote_mac_pair):
+ """Create the VNF instance if it does not already exist."""
+ if self.instance is None:
+ port_ids = [{'port-id': vnf_port.port['id']}
+ for vnf_port in self.ports]
+ vm_config = self._get_vm_config(remote_mac_pair)
+ az = self.get_az()
+ server = self.manager.comp.create_server(self.name,
+ self.manager.image_instance,
+ self.manager.flavor.flavor,
+ None,
+ port_ids,
+ None,
+ avail_zone=az,
+ user_data=None,
+ config_drive=True,
+ files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
+ if server:
+ LOG.info('Created instance %s on %s', self.name, az)
+ self.instance = server
+ self.reuse = False
+ else:
+ raise ChainException('Unable to create instance: %s' % (self.name))
+
+ def _reuse_exception(self, reason):
+ raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
+
+ def get_status(self):
+ """Get the statis of this instance."""
+ if self.instance.status != 'ACTIVE':
+ self.instance = self.manager.comp.poll_server(self.instance)
+ return self.instance.status
+
+ def get_hostname(self):
+ """Get the hypervisor host name running this VNF instance."""
+ return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
+
+ def get_host_ip(self):
+ """Get the IP address of the host where this instance runs.
+
+ return: the IP address
+ """
+ if not self.host_ip:
+ self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
+ return self.host_ip
+
+ def get_hypervisor_name(self):
+ """Get hypervisor name (az:hostname) for this VNF instance."""
+ if self.instance:
+ az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
+ hostname = self.get_hostname()
+ if az:
+ return az + ':' + hostname
+ return hostname
+ return None
+
+ def get_uuid(self):
+ """Get the uuid for this instance."""
+ return self.instance.id
+
+ def delete(self, forced=False):
+ """Delete this VNF instance."""
+ if self.reuse:
+ LOG.info("Instance %s not deleted (reused)", self.name)
+ else:
+ if self.instance:
+ self.manager.comp.delete_server(self.instance)
+ LOG.info("Deleted instance %s", self.name)
+ for port in self.ports:
+ port.delete()
+
+class Chain(object):
+ """A class to manage a single chain.
+
+ Can handle any type of chain (EXT, PVP, PVVP)
+ """
+
+ def __init__(self, chain_id, manager):
+ """Create a new chain.
+
+ chain_id: chain index (first chain is 0)
+ manager: the chain manager that owns all chains
+ """
+ self.chain_id = chain_id
+ self.manager = manager
+ self.encaps = manager.encaps
+ self.networks = []
+ self.instances = []
+ try:
+ self.networks = manager.get_networks(chain_id)
+ # For external chain VNFs can only be discovered from their MAC addresses
+ # either from config or from ARP
+ if manager.config.service_chain != ChainType.EXT:
+ for chain_instance_index in range(self.get_length()):
+ self.instances.append(ChainVnf(self,
+ chain_instance_index,
+ self.networks))
+ # now that all VNF ports are created we need to calculate the
+ # left/right remote MAC for each VNF in the chain
+ # before actually creating the VNF itself
+ rem_mac_pairs = self._get_remote_mac_pairs()
+ for instance in self.instances:
+ rem_mac_pair = rem_mac_pairs.pop(0)
+ instance.create_vnf(rem_mac_pair)
+ except Exception:
+ self.delete()
+ raise
+
+ def get_length(self):
+ """Get the number of VNF in the chain."""
+ return len(self.networks) - 1
+
+ def _get_remote_mac_pairs(self):
+ """Get the list of remote mac pairs for every VNF in the chain.
+
+ Traverse the chain from left to right and establish the
+ left/right remote MAC for each VNF in the chainself.
+
+ PVP case is simpler:
+ mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
+ must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
+ the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
+
+ PVVP:
+ tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
+ Must produce the following list:
+ [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
+
+ General case with 3 VMs in chain, the list of consecutive macs (left to right):
+ tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
+ Must produce the following list:
+ [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
+ or index: [[0, 3], [2, 5], [4, 7]]
+
+ The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
+ """
+ # line up all mac from left to right
+ mac_seq = [self.manager.generator_config.devices[LEFT].mac]
+ for instance in self.instances:
+ mac_seq.append(instance.ports[0].get_mac())
+ mac_seq.append(instance.ports[1].get_mac())
+ mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
+ base = 0
+ rem_mac_pairs = []
+ for _ in self.instances:
+ rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
+ base += 2
+ return rem_mac_pairs
+
+ def get_instances(self):
+ """Return all instances for this chain."""
+ return self.instances
+
+ def get_vlan(self, port_index):
+ """Get the VLAN id on a given port.
+
+ port_index: left port is 0, right port is 1
+ return: the vlan_id or None if there is no vlan tagging
+ """
+ # for port 1 we need to return the VLAN of the last network in the chain
+ # The networks array contains 2 networks for PVP [left, right]
+ # and 3 networks in the case of PVVP [left.middle,right]
+ if port_index:
+ # this will pick the last item in array
+ port_index = -1
+ return self.networks[port_index].get_vlan()
+
+ def get_dest_mac(self, port_index):
+ """Get the dest MAC on a given port.
+
+ port_index: left port is 0, right port is 1
+ return: the dest MAC
+ """
+ if port_index:
+ # for right port, use the right port MAC of the last (right most) VNF In chain
+ return self.instances[-1].ports[1].get_mac()
+ # for left port use the left port MAC of the first (left most) VNF in chain
+ return self.instances[0].ports[0].get_mac()
+
+ def get_network_uuids(self):
+ """Get UUID of networks in this chain from left to right (order is important).
+
+ :return: list of UUIDs of networks (2 or 3 elements)
+ """
+ return [net['id'] for net in self.networks]
+
+ def get_host_ips(self):
+ """Return the IP adresss(es) of the host compute nodes used for this chain.
+
+ :return: a list of 1 or 2 IP addresses
+ """
+ return [vnf.get_host_ip() for vnf in self.instances]
+
+ def get_compute_nodes(self):
+ """Return the name of the host compute nodes used for this chain.
+
+ :return: a list of 1 host name in the az:host format
+ """
+ # Since all chains go through the same compute node(s) we can just retrieve the
+ # compute node name(s) for the first chain
+ return [vnf.get_hypervisor_name() for vnf in self.instances]
+
+ def delete(self):
+ """Delete this chain."""
+ for instance in self.instances:
+ instance.delete()
+ # only delete if these are chain private networks (not shared)
+ if not self.manager.config.service_chain_shared_net:
+ for network in self.networks:
+ network.delete()
+
+
+class ChainManager(object):
+ """A class for managing all chains for a given run.
+
+ Supports openstack or no openstack.
+ Supports EXT, PVP and PVVP chains.
+ """
+
+ def __init__(self, chain_runner):
+ """Create a chain manager to take care of discovering or bringing up the requested chains.
+
+ A new instance must be created every time a new config is used.
+ config: the nfvbench config to use
+ cred: openstack credentials to use of None if there is no openstack
+ """
+ self.chain_runner = chain_runner
+ self.config = chain_runner.config
+ self.generator_config = chain_runner.traffic_client.generator_config
+ self.chains = []
+ self.image_instance = None
+ self.image_name = None
+ # Left and right networks shared across all chains (only if shared)
+ self.networks = []
+ self.encaps = None
+ self.flavor = None
+ self.comp = None
+ self.nova_client = None
+ self.neutron_client = None
+ self.glance_client = None
+ self.existing_instances = []
+ # existing ports keyed by the network uuid they belong to
+ self._existing_ports = {}
+ config = self.config
+ self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
+ self.chain_count = config.service_chain_count
+ if self.openstack:
+ # openstack only
+ session = chain_runner.cred.get_session()
+ self.nova_client = Client(2, session=session)
+ self.neutron_client = neutronclient.Client('2.0', session=session)
+ self.glance_client = glanceclient.Client('2', session=session)
+ self.comp = compute.Compute(self.nova_client,
+ self.glance_client,
+ config)
+ self.az = None
+ try:
+ if config.service_chain != ChainType.EXT:
+ # we need to find 1 hypervisor
+ az_list = self.comp.get_enabled_az_host_list(1)
+ if not az_list:
+ raise ChainException('No matching hypervisor found')
+ self.az = az_list
+ self._setup_image()
+ self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
+ # Get list of all existing instances to check if some instances can be reused
+ self.existing_instances = self.comp.get_server_list()
+ # If networks are shared across chains, get the list of networks
+ if config.service_chain_shared_net:
+ self.networks = self.get_networks()
+ # Reuse/create chains
+ for chain_id in range(self.chain_count):
+ self.chains.append(Chain(chain_id, self))
+ if config.service_chain == ChainType.EXT:
+ # if EXT and no ARP we need to read dest MACs from config
+ if config.no_arp:
+ self._get_dest_macs_from_config()
+ else:
+ # Make sure all instances are active before proceeding
+ self._ensure_instances_active()
+ except Exception:
+ self.delete()
+ raise
+ else:
+ # no openstack, no need to create chains
+ # make sure there at least as many entries as chains in each left/right list
+ if len(config.vlans) != 2:
+ raise ChainException('The config vlans property must be a list '
+ 'with 2 lists of VLAN IDs')
+ if not config.l2_loopback:
+ self._get_dest_macs_from_config()
+
+ re_vlan = "[0-9]*$"
+ self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
+ self._check_list('vlans[1]', config.vlans[1], re_vlan)]
+
+ def _get_dest_macs_from_config(self):
+ re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
+ tg_config = self.config.traffic_generator
+ self.dest_macs = [self._check_list("mac_addrs_left",
+ tg_config.mac_addrs_left, re_mac),
+ self._check_list("mac_addrs_right",
+ tg_config.mac_addrs_right, re_mac)]
+
+ def _check_list(self, list_name, ll, pattern):
+ # if it is a single int or mac, make it a list of 1 int
+ if isinstance(ll, (int, str)):
+ ll = [ll]
+ if not ll or len(ll) < self.chain_count:
+ raise ChainException('%s=%s must be a list with 1 element per chain' % (list_name, ll))
+ for item in ll:
+ if not re.match(pattern, str(item)):
+ raise ChainException("Invalid format '{item}' specified in {fname}"
+ .format(item=item, fname=list_name))
+ return ll
+
+ def _setup_image(self):
+ # To avoid reuploading image in server mode, check whether image_name is set or not
+ if self.image_name:
+ self.image_instance = self.comp.find_image(self.image_name)
+ if self.image_instance:
+ LOG.info("Reusing image %s", self.image_name)
+ else:
+ image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
+ if self.config.vm_image_file:
+ match = re.search(image_name_search_pattern, self.config.vm_image_file)
+ if match:
+ self.image_name = match.group(1)
+ LOG.info('Using provided VM image file %s', self.config.vm_image_file)
+ else:
+ raise ChainException('Provided VM image file name %s must start with '
+ '"nfvbenchvm-<version>"' % self.config.vm_image_file)
+ else:
+ pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ for f in os.listdir(pkg_root):
+ if re.search(image_name_search_pattern, f):
+ self.config.vm_image_file = pkg_root + '/' + f
+ self.image_name = f.replace('.qcow2', '')
+ LOG.info('Found built-in VM image file %s', f)
+ break
+ else:
+ raise ChainException('Cannot find any built-in VM image file.')
+ if self.image_name:
+ self.image_instance = self.comp.find_image(self.image_name)
+ if not self.image_instance:
+ LOG.info('Uploading %s', self.image_name)
+ res = self.comp.upload_image_via_url(self.image_name,
+ self.config.vm_image_file)
+
+ if not res:
+ raise ChainException('Error uploading image %s from %s. ABORTING.' %
+ (self.image_name, self.config.vm_image_file))
+ LOG.info('Image %s successfully uploaded.', self.image_name)
+ self.image_instance = self.comp.find_image(self.image_name)
+
+ def _ensure_instances_active(self):
+ instances = []
+ for chain in self.chains:
+ instances.extend(chain.get_instances())
+ initial_instance_count = len(instances)
+ max_retries = (self.config.check_traffic_time_sec +
+ self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
+ retry = 0
+ while instances:
+ remaining_instances = []
+ for instance in instances:
+ status = instance.get_status()
+ if status == 'ACTIVE':
+ continue
+ if status == 'ERROR':
+ raise ChainException('Instance %s creation error: %s' %
+ (instance.name,
+ instance.instance.fault['message']))
+ remaining_instances.append(instance)
+ if not remaining_instances:
+ break
+ retry += 1
+ if retry >= max_retries:
+ raise ChainException('Time-out: %d/%d instances still not active' %
+ (len(remaining_instances), initial_instance_count))
+ LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
+ len(remaining_instances), initial_instance_count,
+ retry, max_retries)
+ instances = remaining_instances
+ time.sleep(self.config.generic_poll_sec)
+ if initial_instance_count:
+ LOG.info('All instances are active')
+
+ def get_networks(self, chain_id=None):
+ """Get the networks for given EXT, PVP or PVVP chain.
+
+ For EXT packet path, these networks must pre-exist.
+ For PVP, PVVP these networks will be created if they do not exist.
+ chain_id: to which chain the networks belong.
+ a None value will mean that these networks are shared by all chains
+ """
+ if self.networks:
+ # the only case where self.networks exists is when the networks are shared
+ # across all chains
+ return self.networks
+ if self.config.service_chain == ChainType.EXT:
+ lookup_only = True
+ ext_net = self.config.external_networks
+ net_cfg = [AttrDict({'name': name,
+ 'segmentation_id': None,
+ 'physical_network': None})
+ for name in [ext_net.left, ext_net.right]]
+ else:
+ lookup_only = False
+ int_nets = self.config.internal_networks
+ if self.config.service_chain == ChainType.PVP:
+ net_cfg = [int_nets.left, int_nets.right]
+ else:
+ net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
+ networks = []
+ try:
+ for cfg in net_cfg:
+ networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
+ except Exception:
+ # need to cleanup all successful networks prior to bailing out
+ for net in networks:
+ net.delete()
+ raise
+ return networks
+
+ def get_existing_ports(self):
+ """Get the list of existing ports.
+
+ Lazy retrieval of ports as this can be costly if there are lots of ports and
+ is only needed when VM and network are being reused.
+
+ return: a dict of list of neutron ports indexed by the network uuid they are attached to
+
+ Each port is a dict with fields such as below:
+ {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
+ 'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
+ 'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
+ 'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
+ 'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
+ 'security_groups': [],
+ 'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
+ 'vhostuser_mode': 'server'},
+ 'binding:vif_type': 'vhostuser',
+ 'mac_address': 'fa:16:3e:3c:63:04',
+ 'project_id': '977ac76a63d7492f927fa80e86baff4c',
+ 'status': 'ACTIVE',
+ 'binding:host_id': 'a20-champagne-compute-1',
+ 'description': '',
+ 'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
+ 'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
+ 'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
+ 'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
+ 'created_at': '2018-10-06T07:15:10Z',
+ 'binding:vnic_type': 'normal'}
+ """
+ if not self._existing_ports:
+ LOG.info('Loading list of all ports...')
+ existing_ports = self.neutron_client.list_ports()['ports']
+ # place all ports in the dict keyed by the port network uuid
+ for port in existing_ports:
+ port_list = self._existing_ports.setdefault(port['network_id'], [])
+ port_list.append(port)
+ LOG.info("Loaded %d ports attached to %d networks",
+ len(existing_ports), len(self._existing_ports))
+ return self._existing_ports
+
+ def get_ports_from_network(self, chain_network):
+ """Get the list of existing ports that belong to a network.
+
+ Lazy retrieval of ports as this can be costly if there are lots of ports and
+ is only needed when VM and network are being reused.
+
+ chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
+ return: list of neutron ports attached to requested network
+ """
+ return self.get_existing_ports().get(chain_network.get_uuid(), None)
+
+ def get_host_ip_from_mac(self, mac):
+ """Get the host IP address matching a MAC.
+
+ mac: MAC address to look for
+ return: the IP address of the host where the matching port runs or None if not found
+ """
+ # _existing_ports is a dict of list of ports indexed by network id
+ for port_list in self.get_existing_ports().values():
+ for port in port_list:
+ try:
+ if port['mac_address'] == mac:
+ host_id = port['binding:host_id']
+ return self.comp.get_hypervisor(host_id).host_ip
+ except KeyError:
+ pass
+ return None
+
+ def get_chain_vlans(self, port_index):
+ """Get the list of per chain VLAN id on a given port.
+
+ port_index: left port is 0, right port is 1
+ return: a VLAN ID list indexed by the chain index or None if no vlan tagging
+ """
+ if self.chains:
+ return [self.chains[chain_index].get_vlan(port_index)
+ for chain_index in range(self.chain_count)]
+ # no openstack
+ return self.vlans[port_index]
+
+ def get_dest_macs(self, port_index):
+ """Get the list of per chain dest MACs on a given port.
+
+ Should not be called if EXT+ARP is used (in that case the traffic gen will
+ have the ARP responses back from VNFs with the dest MAC to use).
+
+ port_index: left port is 0, right port is 1
+ return: a list of dest MACs indexed by the chain index
+ """
+ if self.chains and self.config.service_chain != ChainType.EXT:
+ return [self.chains[chain_index].get_dest_mac(port_index)
+ for chain_index in range(self.chain_count)]
+ # no openstack or EXT+no-arp
+ return self.dest_macs[port_index]
+
+ def get_host_ips(self):
+ """Return the IP adresss(es) of the host compute nodes used for this run.
+
+ :return: a list of 1 IP address
+ """
+ # Since all chains go through the same compute node(s) we can just retrieve the
+ # compute node(s) for the first chain
+ if self.chains:
+ if self.config.service_chain != ChainType.EXT:
+ return self.chains[0].get_host_ips()
+ # in the case of EXT, the compute node must be retrieved from the port
+ # associated to any of the dest MACs
+ dst_macs = self.chain_runner.traffic_client.gen.get_dest_macs()
+ # dest MAC on port 0, chain 0
+ dst_mac = dst_macs[0][0]
+ host_ip = self.get_host_ip_from_mac(dst_mac)
+ if host_ip:
+ LOG.info('Found compute node IP for EXT chain: %s', host_ip)
+ return [host_ip]
+ return []
+
+ def get_compute_nodes(self):
+ """Return the name of the host compute nodes used for this run.
+
+ :return: a list of 0 or 1 host name in the az:host format
+ """
+ # Since all chains go through the same compute node(s) we can just retrieve the
+ # compute node name(s) for the first chain
+ if self.chains:
+ # in the case of EXT, the compute node must be retrieved from the port
+ # associated to any of the dest MACs
+ return self.chains[0].get_compute_nodes()
+ # no openstack = no chains
+ return []
+
+ def delete(self):
+ """Delete resources for all chains.
+
+ Will not delete any resource if no-cleanup has been requested.
+ """
+ if self.config.no_cleanup:
+ return
+ for chain in self.chains:
+ chain.delete()
+ for network in self.networks:
+ network.delete()
+ if self.flavor:
+ self.flavor.delete()