#!/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 import glanceclient from neutronclient.neutron import client as neutronclient from novaclient.client import Client from attrdict import AttrDict 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 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.""" 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 self.floating_ip = 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 } } 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) 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 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 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: 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, 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 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: if lookup_only: LOG.error("Cannot find network %s", self.name) else: LOG.error("Error creating network %s", self.name) 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) if networks['networks']: network = networks['networks'][0] # a network of same name already exists, we need to verify it has the same # characteristics 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=self.segmentation_id)) 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=self.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 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, '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_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. :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 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: 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: LOG.info('Error deleting network %s (retry %d/%d)...', self.name, 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) 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) # 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 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() raise def _get_vm_config(self, remote_mac_pair): config = self.manager.config devices = self.manager.generator_config.devices 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') as boot_script: content = boot_script.read() vm_config = { 'forwarder': config.vm_forwarder, 'intf_mac1': self.ports[LEFT].get_mac(), 'intf_mac2': self.ports[RIGHT].get_mac(), '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': 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 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: 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 and port_index == 0: # first VNF in chain must use sriov for left port 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[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[instance_left].name not in instance.networks: self._reuse_exception('Left network mismatch') 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 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 = [] 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, 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: self.instance = server if self.manager.placer.is_resolved(): LOG.info('Created instance %s on %s', self.name, az) else: # the location is undetermined at this point # self.get_hypervisor_name() will return None LOG.info('Created instance %s - waiting for placement resolution...', self.name) # 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 = 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() if status == 'ACTIVE': hyp_name = self.get_hypervisor_name() LOG.info('Instance %s is active and has been placed on %s', self.name, hyp_name) self.manager.placer.register_full_name(hyp_name) break if status == 'ERROR': raise ChainException('Instance %s creation error: %s' % (self.name, self.instance.fault['message'])) LOG.info('Waiting for instance %s to become active (retry %d/%d)...', self.name, retry + 1, max_retries + 1) time.sleep(config.generic_poll_sec) else: # timing out LOG.error('Instance %s creation timed out', self.name) raise ChainException('Instance %s creation timed out' % self.name) 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.""" 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. 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: 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 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) 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. 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)) # at this point new VNFs are not created yet but # verify that all discovered VNFs are on the same hypervisor self._check_hypervisors() # 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 _check_hypervisors(self): common_hypervisor = None for instance in self.instances: # get the full hypervizor name (az:compute) hname = instance.get_hypervisor_name() if hname: if common_hypervisor: if hname != common_hypervisor: raise ChainException('Discovered instances on different hypervisors:' ' %s and %s' % (hname, common_hypervisor)) else: common_hypervisor = hname if common_hypervisor: # check that the common hypervisor name matchs the requested hypervisor name # and set the name to be used by all future instances (if any) if not self.manager.placer.register_full_name(common_hypervisor): raise ChainException('Discovered hypervisor placement %s is incompatible' % common_hypervisor) def get_length(self): """Get the number of VNF in the chain.""" # 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. 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 # 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. 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 InstancePlacer(object): """A class to manage instance placement for all VNFs in all chains. A full az string is made of 2 parts AZ and hypervisor. The placement is resolved when both parts az and hypervisor names are known. """ def __init__(self, req_az, req_hyp): """Create a new instance placer. req_az: requested AZ (can be None or empty if no preference) req_hyp: requested hypervisor name (can be None of empty if no preference) can be any of 'nova:', 'comp1', 'nova:comp1' if it is a list, only the first item is used (backward compatibility in config) req_az is ignored if req_hyp has an az part all other parts beyond the first 2 are ignored in req_hyp """ # if passed a list just pick the first item if req_hyp and isinstance(req_hyp, list): req_hyp = req_hyp[0] # only pick first part of az if req_az and ':' in req_az: req_az = req_az.split(':')[0] if req_hyp: # check if requested hypervisor string has an AZ part split_hyp = req_hyp.split(':') if len(split_hyp) > 1: # override the AZ part and hypervisor part req_az = split_hyp[0] req_hyp = split_hyp[1] self.requested_az = req_az if req_az else '' self.requested_hyp = req_hyp if req_hyp else '' # Nova can accept AZ only (e.g. 'nova:', use any hypervisor in that AZ) # or hypervisor only (e.g. ':comp1') # or both (e.g. 'nova:comp1') if req_az: self.required_az = req_az + ':' + self.requested_hyp else: # need to insert a ':' so nova knows this is the hypervisor name self.required_az = ':' + self.requested_hyp if req_hyp else '' # placement is resolved when both AZ and hypervisor names are known and set self.resolved = self.requested_az != '' and self.requested_hyp != '' def get_required_az(self): """Return the required az (can be resolved or not).""" return self.required_az def register_full_name(self, discovered_az): """Verify compatibility and register a discovered hypervisor full name. discovered_az: a discovered AZ in az:hypervisor format return: True if discovered_az is compatible and set False if discovered_az is not compatible """ if self.resolved: return discovered_az == self.required_az # must be in full az format split_daz = discovered_az.split(':') if len(split_daz) != 2: return False if self.requested_az and self.requested_az != split_daz[0]: return False if self.requested_hyp and self.requested_hyp != split_daz[1]: return False self.required_az = discovered_az self.resolved = True return True def is_resolved(self): """Check if the full AZ is resolved. return: True if resolved """ return self.resolved 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 self.az = None 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) self.comp = compute.Compute(self.nova_client, self.glance_client, config) try: if config.service_chain != ChainType.EXT: self.placer = InstancePlacer(config.availability_zone, config.compute_nodes) 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 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() # 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 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: # 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') 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}$" 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] 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): # 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-"' % 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) # 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 + (initial_instance_count - 1) * 10 + 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': LOG.info('Instance %s is ACTIVE on %s', instance.name, instance.get_hypervisor_name()) 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, '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: 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_hypervisor_from_mac(self, mac): """Get the hypervisor that hosts a VM MAC. mac: MAC address to look for 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 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) 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 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. 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.generator_config.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 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.""" 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()