X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=nfvbench%2Fchaining.py;h=d6f67f9bc003984a42bd6551c8e9a9575c596e03;hb=4236eba523bf0ac08ee32a010f861e00f791350d;hp=8d717aac48c5eb05dde9182373f218315fde00d4;hpb=4988edf6afb74026db81677f25877b27b8fcfc05;p=nfvbench.git diff --git a/nfvbench/chaining.py b/nfvbench/chaining.py index 8d717aa..d6f67f9 100644 --- a/nfvbench/chaining.py +++ b/nfvbench/chaining.py @@ -49,18 +49,21 @@ import os import re import time -from glanceclient.v2 import client as glanceclient +import 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 - +from .chain_router import ChainRouter +from . import compute +from .log import LOG +from .specs import ChainType # Left and right index for network and port lists LEFT = 0 RIGHT = 1 +# L3 traffic edge networks are at the end of networks list +EDGE_LEFT = -2 +EDGE_RIGHT = -1 # Name of the VM config file NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf' # full pathame of the VM config in the VM @@ -74,8 +77,6 @@ BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)), class ChainException(Exception): """Exception while operating the chains.""" - pass - class NetworkEncaps(object): """Network encapsulation.""" @@ -131,6 +132,7 @@ class ChainVnfPort(object): self.manager = vnf.manager self.reuse = False self.port = None + self.floating_ip = None if vnf.instance: # VNF instance is reused, we need to find an existing port that matches this instance # and network @@ -156,6 +158,10 @@ class ChainVnfPort(object): 'binding:vnic_type': vnic_type } } + subnet_id = chain_network.get_subnet_uuid() + if subnet_id: + body['port']['fixed_ips'] = [{'subnet_id': subnet_id}] + port = self.manager.neutron_client.create_port(body) self.port = port['port'] LOG.info('Created port %s', name) @@ -174,18 +180,39 @@ class ChainVnfPort(object): """Get the MAC address for this port.""" return self.port['mac_address'] + def get_ip(self): + """Get the IP address for this port.""" + return self.port['fixed_ips'][0]['ip_address'] + + def set_floating_ip(self, chain_network): + # create and add floating ip to port + try: + self.floating_ip = self.manager.neutron_client.create_floatingip({ + 'floatingip': { + 'floating_network_id': chain_network.get_uuid(), + 'port_id': self.port['id'], + 'description': 'nfvbench floating ip for port:' + self.port['name'], + }})['floatingip'] + LOG.info('Floating IP %s created and associated on port %s', + self.floating_ip['floating_ip_address'], self.name) + return self.floating_ip['floating_ip_address'] + except Exception: + LOG.info('Failed to created and associated floating ip on port %s (ignored)', self.name) + return self.port['fixed_ips'][0]['ip_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: + for _ in range(0, self.manager.config.generic_retry_count): try: self.manager.neutron_client.delete_port(self.port['id']) LOG.info("Deleted port %s", self.name) + if self.floating_ip: + self.manager.neutron_client.delete_floatingip(self.floating_ip['id']) + LOG.info("Deleted floating IP %s", self.floating_ip['description']) return except Exception: - retry += 1 time.sleep(self.manager.config.generic_poll_sec) LOG.error('Unable to delete port: %s', self.name) @@ -193,15 +220,39 @@ class ChainVnfPort(object): 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.""" + def __init__(self, manager, network_config, chain_id=None, lookup_only=False, + suffix=None): + """Create a network for given chain. + + network_config: a dict containing the network properties + (name, segmentation_id and physical_network) + chain_id: to which chain the networks belong. + a None value will mean that these networks are shared by all chains + suffix: a suffix to add to the network name (if not None) + """ self.manager = manager - self.name = network_config.name - if chain_id is not None: - self.name += str(chain_id) + if chain_id is None: + self.name = network_config.name + else: + # the name itself can be either a string or a list of names indexed by chain ID + if isinstance(network_config.name, tuple): + self.name = network_config.name[chain_id] + else: + # network_config.name is a prefix string + self.name = network_config.name + str(chain_id) + if suffix: + self.name = self.name + suffix + self.segmentation_id = self._get_item(network_config.segmentation_id, + chain_id, auto_index=True) + self.subnet_name = self._get_item(network_config.subnet, chain_id) + self.physical_network = self._get_item(network_config.physical_network, chain_id) + self.reuse = False self.network = None self.vlan = None + self.router_name = None + if manager.config.l3_router and hasattr(network_config, 'router_name'): + self.router_name = network_config.router_name try: self._setup(network_config, lookup_only) except Exception: @@ -212,6 +263,33 @@ class ChainNetwork(object): self.delete() raise + def _get_item(self, item_field, index, auto_index=False): + """Retrieve an item from a list or a single value. + + item_field: can be None, a tuple of a single value + index: if None is same as 0, else is the index for a chain + auto_index: if true will automatically get the final value by adding the + index to the base value (if full list not provided) + + If the item_field is not a tuple, it is considered same as a tuple with same value at any + index. + If a list is provided, its length must be > index + """ + if not item_field: + return None + if index is None: + index = 0 + if isinstance(item_field, tuple): + try: + return item_field[index] + except IndexError: + raise ChainException("List %s is too short for chain index %d" % + (str(item_field), index)) from IndexError + # single value is configured + if auto_index: + return item_field + index + return item_field + 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) @@ -219,23 +297,23 @@ class ChainNetwork(object): 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: + if self.segmentation_id: + if network['provider:segmentation_id'] != self.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)) + seg_id2=self.segmentation_id)) - if network_config.physical_network: - if network['provider:physical_network'] != network_config.physical_network: + if self.physical_network: + if network['provider:physical_network'] != self.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)) + phys2=self.physical_network)) LOG.info('Reusing existing network %s', self.name) self.reuse = True @@ -247,16 +325,17 @@ class ChainNetwork(object): '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 - + if self.segmentation_id: + body['network']['provider:segmentation_id'] = self.segmentation_id + if self.physical_network: + body['network']['provider:physical_network'] = self.physical_network self.network = self.manager.neutron_client.create_network(body)['network'] + # create associated subnet, all subnets have the same name (which is ok since + # we do not need to address them directly by name) body = { 'subnet': {'name': network_config.subnet, 'cidr': network_config.cidr, @@ -268,7 +347,7 @@ class ChainNetwork(object): 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) + LOG.info('Created network: %s', self.name) def get_uuid(self): """ @@ -278,6 +357,18 @@ class ChainNetwork(object): """ return self.network['id'] + def get_subnet_uuid(self): + """ + Extract UUID of this subnet network. + + :return: UUID of this subnet network + """ + for subnet in self.network['subnets']: + if self.subnet_name == self.manager.neutron_client \ + .show_subnet(subnet)['subnet']['name']: + return subnet + return None + def get_vlan(self): """ Extract vlan for this network. @@ -288,20 +379,36 @@ class ChainNetwork(object): raise ChainException('Trying to retrieve VLAN id for non VLAN network') return self.network['provider:segmentation_id'] + def get_vxlan(self): + """ + Extract VNI for this network. + + :return: VNI ID for this network + """ + + return self.network['provider:segmentation_id'] + + def get_mpls_inner_label(self): + """ + Extract MPLS VPN Label for this network. + + :return: MPLS VPN Label for this 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: + for retry in range(0, 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, + retry + 1, self.manager.config.generic_retry_count) time.sleep(self.manager.config.generic_poll_sec) LOG.error('Unable to delete network: %s', self.name) @@ -324,15 +431,27 @@ class ChainVnf(object): if len(networks) > 2: # we will have more than 1 VM in each chain self.name += '-' + str(vnf_id) + # A list of ports for this chain + # There are normally 2 ports carrying traffic (index 0, and index 1) and + # potentially multiple idle ports not carrying traffic (index 2 and up) + # For example if 7 idle interfaces are requested, the corresp. ports will be + # at index 2 to 8 self.ports = [] + self.management_port = None + self.routers = [] self.status = None self.instance = None self.reuse = False self.host_ip = None + self.idle_networks = [] + self.idle_ports = [] 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]) + if self.manager.config.l3_router: + self._setup(networks[vnf_id:vnf_id + 4]) + else: + self._setup(networks[vnf_id:vnf_id + 2]) except Exception: LOG.error("Error creating VNF %s", self.name) self.delete() @@ -341,82 +460,257 @@ class ChainVnf(object): 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: + + if config.l3_router: + tg_gateway1_ip = self.routers[LEFT].ports[1]['fixed_ips'][0][ + 'ip_address'] # router edge ip left + tg_gateway2_ip = self.routers[RIGHT].ports[1]['fixed_ips'][0][ + 'ip_address'] # router edge ip right + tg_mac1 = self.routers[LEFT].ports[1]['mac_address'] # router edge mac left + tg_mac2 = self.routers[RIGHT].ports[1]['mac_address'] # router edge mac right + # edge cidr mask left + vnf_gateway1_cidr = \ + self.ports[LEFT].get_ip() + self.__get_network_mask( + self.manager.config.edge_networks.left.cidr) + # edge cidr mask right + vnf_gateway2_cidr = \ + self.ports[RIGHT].get_ip() + self.__get_network_mask( + self.manager.config.edge_networks.right.cidr) + if config.vm_forwarder != 'vpp': + raise ChainException( + 'L3 router mode imply to set VPP as VM forwarder.' + 'Please update your config file with: vm_forwarder: vpp') + else: + tg_gateway1_ip = devices[LEFT].tg_gateway_ip_addrs + tg_gateway2_ip = devices[RIGHT].tg_gateway_ip_addrs + if not config.loop_vm_arp: + tg_mac1 = remote_mac_pair[0] + tg_mac2 = remote_mac_pair[1] + else: + tg_mac1 = "" + tg_mac2 = "" + + g1cidr = devices[LEFT].get_gw_ip( + self.chain.chain_id) + self.__get_network_mask( + self.manager.config.internal_networks.left.cidr) + g2cidr = devices[RIGHT].get_gw_ip( + self.chain.chain_id) + self.__get_network_mask( + self.manager.config.internal_networks.right.cidr) + + vnf_gateway1_cidr = g1cidr + vnf_gateway2_cidr = g2cidr + + with open(BOOT_SCRIPT_PATHNAME, 'r', encoding="utf-8") 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_gateway1_ip': tg_gateway1_ip, + 'tg_gateway2_ip': tg_gateway2_ip, '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] + 'vnf_gateway1_cidr': vnf_gateway1_cidr, + 'vnf_gateway2_cidr': vnf_gateway2_cidr, + 'tg_mac1': tg_mac1, + 'tg_mac2': tg_mac2, + 'vif_mq_size': config.vif_multiqueue_size, + 'num_mbufs': config.num_mbufs } + if self.manager.config.use_management_port: + mgmt_ip = self.management_port.port['fixed_ips'][0]['ip_address'] + mgmt_mask = self.__get_network_mask(self.manager.config.management_network.cidr) + vm_config['intf_mgmt_cidr'] = mgmt_ip + mgmt_mask + vm_config['intf_mgmt_ip_gw'] = self.manager.config.management_network.gateway + vm_config['intf_mac_mgmt'] = self.management_port.port['mac_address'] + else: + # Interface management config left empty to avoid error in VM spawn + # if nfvbench config has values for management network but use_management_port=false + vm_config['intf_mgmt_cidr'] = '' + vm_config['intf_mgmt_ip_gw'] = '' + vm_config['intf_mac_mgmt'] = '' return content.format(**vm_config) + @staticmethod + def __get_network_mask(network): + return '/' + network.split('/')[1] + 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 + If SR-IOV is specified, 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: + chain_length = self.chain.get_length() + if self.manager.config.use_sriov_middle_net or chain_length == 1: return 'direct' - if self.vnf_id == 0: + if self.vnf_id == 0 and port_index == 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): + return 'direct' + if (self.vnf_id == chain_length - 1) and (port_index == 1): # last VNF in chain must use sriov for right port return 'direct' return 'normal' + def _get_idle_networks_ports(self): + """Get the idle networks for PVP or PVVP chain (non shared net only) + + For EXT packet path or shared net, returns empty list. + 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 + """ + networks = [] + ports = [] + config = self.manager.config + chain_id = self.chain.chain_id + idle_interfaces_per_vm = config.idle_interfaces_per_vm + if config.service_chain == ChainType.EXT or chain_id is None or \ + idle_interfaces_per_vm == 0: + return + + # Make a copy of the idle networks dict as we may have to modify the + # segmentation ID + idle_network_cfg = AttrDict(config.idle_networks) + if idle_network_cfg.segmentation_id: + segmentation_id = idle_network_cfg.segmentation_id + \ + chain_id * idle_interfaces_per_vm + else: + segmentation_id = None + try: + # create as many idle networks and ports as requested + for idle_index in range(idle_interfaces_per_vm): + if config.service_chain == ChainType.PVP: + suffix = '.%d' % (idle_index) + else: + suffix = '.%d.%d' % (self.vnf_id, idle_index) + port_name = self.name + '-idle' + str(idle_index) + # update the segmentation id based on chain id and idle index + if segmentation_id: + idle_network_cfg.segmentation_id = segmentation_id + idle_index + port_name = port_name + "." + str(segmentation_id) + + networks.append(ChainNetwork(self.manager, + idle_network_cfg, + chain_id, + suffix=suffix)) + ports.append(ChainVnfPort(port_name, + self, + networks[idle_index], + 'normal')) + except Exception: + # need to cleanup all successful networks + for net in networks: + net.delete() + for port in ports: + port.delete() + raise + self.idle_networks = networks + self.idle_ports = ports + 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: + instance_left = LEFT + instance_right = RIGHT + # In case of L3 traffic instance use edge networks + if self.manager.config.l3_router: + instance_left = EDGE_LEFT + instance_right = EDGE_RIGHT # 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) + if not networks[instance_left].reuse: + self._reuse_exception('network %s is new' % networks[instance_left].name) + if not networks[instance_right].reuse: + self._reuse_exception('network %s is new' % networks[instance_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: + if networks[instance_left].name not in instance.networks: self._reuse_exception('Left network mismatch') - if networks[RIGHT].name not in instance.networks: + if networks[instance_right].name not in instance.networks: self._reuse_exception('Right network mismatch') self.reuse = True self.instance = instance LOG.info('Reusing existing instance %s on %s', self.name, self.get_hypervisor_name()) + # create management port if needed + if self.manager.config.use_management_port: + self.management_port = ChainVnfPort(self.name + '-mgmt', self, + self.manager.management_network, 'normal') + ip = self.management_port.port['fixed_ips'][0]['ip_address'] + if self.manager.config.use_floating_ip: + ip = self.management_port.set_floating_ip(self.manager.floating_ip_network) + LOG.info("Management interface will be active using IP: %s, " + "and you can connect over SSH with login: nfvbench and password: nfvbench", ip) # 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 self.manager.config.l3_router: + for index in [0, 1]: + self.ports.append(ChainVnfPort(self.name + '-' + str(index), + self, + networks[index + 2], + self._get_vnic_type(index))) + else: + for index in [0, 1]: + self.ports.append(ChainVnfPort(self.name + '-' + str(index), + self, + networks[index], + self._get_vnic_type(index))) + + # create idle networks and ports only if instance is not reused + # if reused, we do not care about idle networks/ports + if not self.reuse: + self._get_idle_networks_ports() + + # Create neutron routers for L3 traffic use case + if self.manager.config.l3_router and self.manager.openstack: + internal_nets = networks[:2] + if self.manager.config.service_chain == ChainType.PVP: + edge_nets = networks[2:] + else: + edge_nets = networks[3:] + subnets_left = [internal_nets[0], edge_nets[0]] + routes_left = [{'destination': self.manager.config.traffic_generator.ip_addrs[0], + 'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[ + 0]}, + {'destination': self.manager.config.traffic_generator.ip_addrs[1], + 'nexthop': self.ports[0].get_ip()}] + self.routers.append( + ChainRouter(self.manager, edge_nets[0].router_name, subnets_left, routes_left)) + subnets_right = [internal_nets[1], edge_nets[1]] + routes_right = [{'destination': self.manager.config.traffic_generator.ip_addrs[0], + 'nexthop': self.ports[1].get_ip()}, + {'destination': self.manager.config.traffic_generator.ip_addrs[1], + 'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[ + 1]}] + self.routers.append( + ChainRouter(self.manager, edge_nets[1].router_name, subnets_right, routes_right)) + # Overload gateway_ips property with router ip address for ARP and traffic calls + self.manager.generator_config.devices[LEFT].set_gw_ip( + self.routers[LEFT].ports[0]['fixed_ips'][0]['ip_address']) # router edge ip left) + self.manager.generator_config.devices[RIGHT].set_gw_ip( + self.routers[RIGHT].ports[0]['fixed_ips'][0]['ip_address']) # router edge ip right) + # 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 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] + port_ids = [] + if self.manager.config.use_management_port: + port_ids.append({'port-id': self.management_port.port['id']}) + port_ids.extend([{'port-id': vnf_port.port['id']} for vnf_port in self.ports]) + # add idle ports + for idle_port in self.idle_ports: + port_ids.append({'port-id': idle_port.port['id']}) vm_config = self._get_vm_config(remote_mac_pair) az = self.manager.placer.get_required_az() server = self.manager.comp.create_server(self.name, @@ -440,8 +734,8 @@ class ChainVnf(object): # here we MUST wait until this instance is resolved otherwise subsequent # VNF creation can be placed in other hypervisors! config = self.manager.config - max_retries = (config.check_traffic_time_sec + - config.generic_poll_sec - 1) / config.generic_poll_sec + max_retries = int((config.check_traffic_time_sec + + config.generic_poll_sec - 1) / config.generic_poll_sec) retry = 0 for retry in range(max_retries): status = self.get_status() @@ -477,7 +771,13 @@ class ChainVnf(object): def get_hostname(self): """Get the hypervisor host name running this VNF instance.""" - return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname') + if self.manager.is_admin: + hypervisor_hostname = getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname') + else: + hypervisor_hostname = self.manager.config.hypervisor_hostname + if not hypervisor_hostname: + raise ChainException('Hypervisor hostname parameter is mandatory') + return hypervisor_hostname def get_host_ip(self): """Get the IP address of the host where this instance runs. @@ -491,7 +791,12 @@ class ChainVnf(object): 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') + if self.manager.is_admin: + az = getattr(self.instance, 'OS-EXT-AZ:availability_zone') + else: + az = self.manager.config.availability_zone + if not az: + raise ChainException('Availability zone parameter is mandatory') hostname = self.get_hostname() if az: return az + ':' + hostname @@ -510,8 +815,15 @@ class ChainVnf(object): if self.instance: self.manager.comp.delete_server(self.instance) LOG.info("Deleted instance %s", self.name) + if self.manager.config.use_management_port: + self.management_port.delete() for port in self.ports: port.delete() + for port in self.idle_ports: + port.delete() + for network in self.idle_networks: + network.delete() + class Chain(object): """A class to manage a single chain. @@ -574,7 +886,8 @@ class Chain(object): def get_length(self): """Get the number of VNF in the chain.""" - return len(self.networks) - 1 + # Take into account 2 edge networks for routers + return len(self.networks) - 3 if self.manager.config.l3_router else len(self.networks) - 1 def _get_remote_mac_pairs(self): """Get the list of remote mac pairs for every VNF in the chain. @@ -629,7 +942,38 @@ class Chain(object): if port_index: # this will pick the last item in array port_index = -1 - return self.networks[port_index].get_vlan() + # This string filters networks connected to TG, in case of + # l3-router feature we have 4 networks instead of 2 + networks = [x for x in self.networks if not x.router_name] + return networks[port_index].get_vlan() + + def get_vxlan(self, port_index): + """Get the VXLAN id on a given port. + + port_index: left port is 0, right port is 1 + return: the vxlan_id or None if there is no vxlan + """ + # 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_vxlan() + + def get_mpls_inner_label(self, port_index): + """Get the MPLS VPN Label on a given port. + + port_index: left port is 0, right port is 1 + return: the mpls_label_id or None if there is no mpls + """ + # for port 1 we need to return the MPLS Label 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_mpls_inner_label() def get_dest_mac(self, port_index): """Get the dest MAC on a given port. @@ -792,6 +1136,7 @@ class ChainManager(object): if self.openstack: # openstack only session = chain_runner.cred.get_session() + self.is_admin = chain_runner.cred.is_admin self.nova_client = Client(2, session=session) self.neutron_client = neutronclient.Client('2.0', session=session) self.glance_client = glanceclient.Client('2', session=session) @@ -805,6 +1150,22 @@ class ChainManager(object): 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 management port is requested for VMs, create management network (shared) + if self.config.use_management_port: + self.management_network = ChainNetwork(self, self.config.management_network, + None, False) + # If floating IP is used for management, create and share + # across chains the floating network + if self.config.use_floating_ip: + self.floating_ip_network = ChainNetwork(self, + self.config.floating_network, + None, False) + else: + # For EXT chains, the external_networks left and right fields in the config + # must be either a prefix string or a list of at least chain-count strings + self._check_extnet('left', config.external_networks.left) + self._check_extnet('right', config.external_networks.right) + # If networks are shared across chains, get the list of networks if config.service_chain_shared_net: self.networks = self.get_networks() @@ -812,18 +1173,20 @@ class ChainManager(object): 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: + # if EXT and no ARP or VxLAN we need to read dest MACs from config + if config.no_arp or config.vxlan: self._get_dest_macs_from_config() else: # Make sure all instances are active before proceeding self._ensure_instances_active() + # network API call do not show VLANS ID if not admin read from config + if not self.is_admin and config.vlan_tagging: + self._get_config_vlans() except Exception: self.delete() raise else: # no openstack, no need to create chains - if not config.l2_loopback and config.no_arp: self._get_dest_macs_from_config() if config.vlan_tagging: @@ -831,9 +1194,26 @@ class ChainManager(object): if len(config.vlans) != 2: raise ChainException('The config vlans property must be a list ' 'with 2 lists of VLAN IDs') - 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)] + self._get_config_vlans() + if config.vxlan: + raise ChainException('VxLAN is only supported with OpenStack') + + def _check_extnet(self, side, name): + if not name: + raise ChainException('external_networks.%s must contain a valid network' + ' name prefix or a list of network names' % side) + if isinstance(name, tuple) and len(name) < self.chain_count: + raise ChainException('external_networks.%s %s' + ' must have at least %d names' % (side, name, self.chain_count)) + + def _get_config_vlans(self): + re_vlan = "[0-9]*$" + try: + self.vlans = [self._check_list('vlans[0]', self.config.vlans[0], re_vlan), + self._check_list('vlans[1]', self.config.vlans[1], re_vlan)] + except IndexError: + raise ChainException( + 'vlans parameter is mandatory. Set valid value in config file') from IndexError 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}$" @@ -847,13 +1227,24 @@ class ChainManager(object): # 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 %d elements per chain' % - (list_name, ll, self.chain_count)) + else: + ll = list(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)) + # must have at least 1 element + if not ll: + raise ChainException('%s cannot be empty' % (list_name)) + # for shared network, if 1 element is passed, replicate it as many times + # as chains + if self.config.service_chain_shared_net and len(ll) == 1: + ll = [ll[0]] * self.chain_count + + # number of elements musty be the number of chains + elif len(ll) < self.chain_count: + raise ChainException('%s=%s must be a list with %d elements per chain' % + (list_name, ll, self.chain_count)) return ll def _setup_image(self): @@ -895,12 +1286,16 @@ class ChainManager(object): LOG.info('Image %s successfully uploaded.', self.image_name) self.image_instance = self.comp.find_image(self.image_name) + # image multiqueue property must be set according to the vif_multiqueue_size + # config value (defaults to 1 or disabled) + self.comp.image_set_multiqueue(self.image_instance, self.config.vif_multiqueue_size > 1) + 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 + + max_retries = (self.config.check_traffic_time_sec + (initial_instance_count - 1) * 10 + self.config.generic_poll_sec - 1) / self.config.generic_poll_sec retry = 0 while instances: @@ -946,16 +1341,23 @@ class ChainManager(object): lookup_only = True ext_net = self.config.external_networks net_cfg = [AttrDict({'name': name, + 'subnet': None, 'segmentation_id': None, 'physical_network': None}) for name in [ext_net.left, ext_net.right]] + # segmentation id and subnet should be discovered from neutron else: lookup_only = False int_nets = self.config.internal_networks + # VLAN and VxLAN 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] + if self.config.l3_router: + edge_nets = self.config.edge_networks + net_cfg.append(edge_nets.left) + net_cfg.append(edge_nets.right) networks = [] try: for cfg in net_cfg: @@ -1019,35 +1421,70 @@ class ChainManager(object): """ 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. + def get_hypervisor_from_mac(self, mac): + """Get the hypervisor that hosts a VM MAC. mac: MAC address to look for - return: the IP address of the host where the matching port runs or None if not found + return: the hypervisor 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_list in list(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 + return self.comp.get_hypervisor(host_id) except KeyError: pass return 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 + """ + hypervisor = self.get_hypervisor_from_mac(mac) + if hypervisor: + return hypervisor.host_ip + 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: + if self.chains and self.is_admin: 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_chain_vxlans(self, port_index): + """Get the list of per chain VNIs id on a given port. + + port_index: left port is 0, right port is 1 + return: a VNIs ID list indexed by the chain index or None if no vlan tagging + """ + if self.chains and self.is_admin: + return [self.chains[chain_index].get_vxlan(port_index) + for chain_index in range(self.chain_count)] + # no openstack + raise ChainException('VxLAN is only supported with OpenStack and with admin user') + + def get_chain_mpls_inner_labels(self, port_index): + """Get the list of per chain MPLS VPN Labels on a given port. + + port_index: left port is 0, right port is 1 + return: a MPLSs ID list indexed by the chain index or None if no mpls + """ + if self.chains and self.is_admin: + return [self.chains[chain_index].get_mpls_inner_label(port_index) + for chain_index in range(self.chain_count)] + # no openstack + raise ChainException('MPLS is only supported with OpenStack and with admin user') + def get_dest_macs(self, port_index): """Get the list of per chain dest MACs on a given port. @@ -1094,20 +1531,29 @@ class ChainManager(object): 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() + if self.config.service_chain != ChainType.EXT: + return self.chains[0].get_compute_nodes() + # in the case of EXT, the compute node must be retrieved from the port + # associated to any of the dest MACs + dst_macs = self.generator_config.get_dest_macs() + # dest MAC on port 0, chain 0 + dst_mac = dst_macs[0][0] + hypervisor = self.get_hypervisor_from_mac(dst_mac) + if hypervisor: + LOG.info('Found hypervisor for EXT chain: %s', hypervisor.hypervisor_hostname) + return [':' + hypervisor.hypervisor_hostname] # 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 + """Delete resources for all chains.""" for chain in self.chains: chain.delete() for network in self.networks: network.delete() + if self.config.use_management_port and hasattr(self, 'management_network'): + self.management_network.delete() + if self.config.use_floating_ip and hasattr(self, 'floating_ip_network'): + self.floating_ip_network.delete() if self.flavor: self.flavor.delete()