X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=nfvbench%2Fchaining.py;h=ee8a8e0a4ab658a062410ff8ea24b461ee53c852;hb=e7fdfb5c5e386bb8851b6c583c44ae61bd188627;hp=a5ae6800cba2c66f90a3dc4bda44d41bf88836a4;hpb=c0ef57f8ec086c07053d529510992c869c30c9d2;p=nfvbench.git diff --git a/nfvbench/chaining.py b/nfvbench/chaining.py index a5ae680..ee8a8e0 100644 --- a/nfvbench/chaining.py +++ b/nfvbench/chaining.py @@ -194,11 +194,27 @@ 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.""" + """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 + """ 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) + self.segmentation_id = self._get_item(network_config.segmentation_id, + chain_id, auto_index=True) + self.physical_network = self._get_item(network_config.physical_network, chain_id) + self.reuse = False self.network = None self.vlan = None @@ -212,6 +228,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)) + # 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 +262,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 @@ -251,11 +294,10 @@ class ChainNetwork(object): } 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'] body = { 'subnet': {'name': network_config.subnet, @@ -268,7 +310,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): """ @@ -288,6 +330,16 @@ 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 + """ + if 'vxlan' not in self.network['provider:network_type']: + raise ChainException('Trying to retrieve VNI for non VXLAN network') + return self.network['provider:segmentation_id'] + def delete(self): """Delete this network.""" if not self.reuse and self.network: @@ -367,13 +419,13 @@ class ChainVnf(object): 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' @@ -477,7 +529,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 +549,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 @@ -631,6 +694,20 @@ class Chain(object): port_index = -1 return self.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_dest_mac(self, port_index): """Get the dest MAC on a given port. @@ -715,8 +792,8 @@ class InstancePlacer(object): if req_az: self.required_az = req_az + ':' + self.requested_hyp else: - # no ":" needed - self.required_az = self.requested_hyp if req_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 != '' @@ -792,6 +869,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 +883,12 @@ 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() + 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,27 +896,46 @@ 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 - # 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: + if not config.l2_loopback and config.no_arp: 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)] + 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') 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}$" @@ -846,12 +949,22 @@ 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 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)) + # 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): @@ -947,9 +1060,11 @@ class ChainManager(object): '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: @@ -1017,11 +1132,11 @@ 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(): @@ -1029,23 +1144,46 @@ class ChainManager(object): 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_dest_macs(self, port_index): """Get the list of per chain dest MACs on a given port. @@ -1073,7 +1211,7 @@ class ChainManager(object): 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() + 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) @@ -1092,17 +1230,23 @@ 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: