2 # Copyright 2018 Cisco Systems, Inc. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
17 # This module takes care of chaining networks, ports and vms
19 """NFVBENCH CHAIN DISCOVERY/STAGING.
21 This module takes care of staging/discovering all resources that are participating in a
22 benchmarking session: flavors, networks, ports, VNF instances.
23 If a resource is discovered with the same name, it will be reused.
24 Otherwise it will be created.
26 ChainManager: manages VM image, flavor, the staging discovery of all chains
28 Chain: manages one chain, has 2 or more networks and 1 or more instances
29 ChainNetwork: manages 1 network in a chain
30 ChainVnf: manages 1 VNF instance in a chain, has 2 ports
31 ChainVnfPort: manages 1 instance port
33 ChainManager-->Chain(*)
34 Chain-->ChainNetwork(*),ChainVnf(*)
35 ChainVnf-->ChainVnfPort(2)
37 Once created/discovered, instances are checked to be in the active state (ready to pass traffic)
38 Configuration parameters that will influence how these resources are staged/related:
39 - openstack or no openstack
42 - number of VNF in each chain (PVP, PVVP)
43 - SRIOV and middle port SRIOV for port types
44 - whether networks are shared across chains or not
46 There is not traffic generation involved in this module.
52 from glanceclient.v2 import client as glanceclient
53 from neutronclient.neutron import client as neutronclient
54 from novaclient.client import Client
56 from attrdict import AttrDict
59 from specs import ChainType
61 # Left and right index for network and port lists
64 # Name of the VM config file
65 NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
66 # full pathame of the VM config in the VM
67 NFVBENCH_CFG_VM_PATHNAME = os.path.join('/etc/', NFVBENCH_CFG_FILENAME)
68 # full path of the boot shell script template file on the server where nfvbench runs
69 BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
71 NFVBENCH_CFG_FILENAME)
74 class ChainException(Exception):
75 """Exception while operating the chains."""
79 class NetworkEncaps(object):
80 """Network encapsulation."""
83 class ChainFlavor(object):
84 """Class to manage the chain flavor."""
86 def __init__(self, flavor_name, flavor_dict, comp):
87 """Create a flavor."""
88 self.name = flavor_name
90 self.flavor = self.comp.find_flavor(flavor_name)
94 LOG.info("Reused flavor '%s'", flavor_name)
96 extra_specs = flavor_dict.pop('extra_specs', None)
98 self.flavor = comp.create_flavor(flavor_name,
101 LOG.info("Created flavor '%s'", flavor_name)
103 self.flavor.set_keys(extra_specs)
106 """Delete this flavor."""
107 if not self.reuse and self.flavor:
109 LOG.info("Flavor '%s' deleted", self.name)
112 class ChainVnfPort(object):
113 """A port associated to one VNF in the chain."""
115 def __init__(self, name, vnf, chain_network, vnic_type):
116 """Create or reuse a port on a given network.
118 if vnf.instance is None the VNF instance is not reused and this ChainVnfPort instance must
120 Otherwise vnf.instance is a reused VNF instance and this ChainVnfPort instance must
121 find an existing port to reuse that matches the port requirements: same attached network,
122 instance, name, vnic type
124 name: name for this port
125 vnf: ChainVNf instance that owns this port
126 chain_network: ChainNetwork instance where this port should attach
127 vnic_type: required vnic type for this port
131 self.manager = vnf.manager
135 # VNF instance is reused, we need to find an existing port that matches this instance
137 # discover ports attached to this instance
138 port_list = self.manager.get_ports_from_network(chain_network)
139 for port in port_list:
140 if port['name'] != name:
142 if port['binding:vnic_type'] != vnic_type:
144 if port['device_id'] == vnf.get_uuid():
146 LOG.info('Reusing existing port %s mac=%s', name, port['mac_address'])
149 raise ChainException('Cannot find matching port')
151 # VNF instance is not created yet, we need to create a new port
155 'network_id': chain_network.get_uuid(),
156 'binding:vnic_type': vnic_type
159 port = self.manager.neutron_client.create_port(body)
160 self.port = port['port']
161 LOG.info('Created port %s', name)
163 self.manager.neutron_client.update_port(self.port['id'], {
165 'security_groups': [],
166 'port_security_enabled': False,
169 LOG.info('Security disabled on port %s', name)
171 LOG.info('Failed to disable security on port %s (ignored)', name)
174 """Get the MAC address for this port."""
175 return self.port['mac_address']
178 """Delete this port instance."""
179 if self.reuse or not self.port:
182 while retry < self.manager.config.generic_retry_count:
184 self.manager.neutron_client.delete_port(self.port['id'])
185 LOG.info("Deleted port %s", self.name)
189 time.sleep(self.manager.config.generic_poll_sec)
190 LOG.error('Unable to delete port: %s', self.name)
193 class ChainNetwork(object):
194 """Could be a shared network across all chains or a chain private network."""
196 def __init__(self, manager, network_config, chain_id=None, lookup_only=False):
197 """Create a network for given chain."""
198 self.manager = manager
199 self.name = network_config.name
200 if chain_id is not None:
201 self.name += str(chain_id)
206 self._setup(network_config, lookup_only)
209 LOG.error("Cannot find network %s", self.name)
211 LOG.error("Error creating network %s", self.name)
215 def _setup(self, network_config, lookup_only):
216 # Lookup if there is a matching network with same name
217 networks = self.manager.neutron_client.list_networks(name=self.name)
218 if networks['networks']:
219 network = networks['networks'][0]
220 # a network of same name already exists, we need to verify it has the same
222 if network_config.segmentation_id:
223 if network['provider:segmentation_id'] != network_config.segmentation_id:
224 raise ChainException("Mismatch of 'segmentation_id' for reused "
225 "network '{net}'. Network has id '{seg_id1}', "
226 "configuration requires '{seg_id2}'."
227 .format(net=self.name,
228 seg_id1=network['provider:segmentation_id'],
229 seg_id2=network_config.segmentation_id))
231 if network_config.physical_network:
232 if network['provider:physical_network'] != network_config.physical_network:
233 raise ChainException("Mismatch of 'physical_network' for reused "
234 "network '{net}'. Network has '{phys1}', "
235 "configuration requires '{phys2}'."
236 .format(net=self.name,
237 phys1=network['provider:physical_network'],
238 phys2=network_config.physical_network))
240 LOG.info('Reusing existing network %s', self.name)
242 self.network = network
245 raise ChainException('Network %s not found' % self.name)
249 'admin_state_up': True
252 if network_config.network_type:
253 body['network']['provider:network_type'] = network_config.network_type
254 if network_config.segmentation_id:
255 body['network']['provider:segmentation_id'] = network_config.segmentation_id
256 if network_config.physical_network:
257 body['network']['provider:physical_network'] = network_config.physical_network
259 self.network = self.manager.neutron_client.create_network(body)['network']
261 'subnet': {'name': network_config.subnet,
262 'cidr': network_config.cidr,
263 'network_id': self.network['id'],
264 'enable_dhcp': False,
266 'dns_nameservers': []}
268 subnet = self.manager.neutron_client.create_subnet(body)['subnet']
269 # add subnet id to the network dict since it has just been added
270 self.network['subnets'] = [subnet['id']]
271 LOG.info('Created network: %s.', self.name)
275 Extract UUID of this network.
277 :return: UUID of this network
279 return self.network['id']
283 Extract vlan for this network.
285 :return: vlan ID for this network
287 if self.network['provider:network_type'] != 'vlan':
288 raise ChainException('Trying to retrieve VLAN id for non VLAN network')
289 return self.network['provider:segmentation_id']
292 """Delete this network."""
293 if not self.reuse and self.network:
295 while retry < self.manager.config.generic_retry_count:
297 self.manager.neutron_client.delete_network(self.network['id'])
298 LOG.info("Deleted network: %s", self.name)
302 LOG.info('Error deleting network %s (retry %d/%d)...',
305 self.manager.config.generic_retry_count)
306 time.sleep(self.manager.config.generic_poll_sec)
307 LOG.error('Unable to delete network: %s', self.name)
310 class ChainVnf(object):
311 """A class to represent a VNF in a chain."""
313 def __init__(self, chain, vnf_id, networks):
314 """Reuse a VNF instance with same characteristics or create a new VNF instance.
316 chain: the chain where this vnf belongs
317 vnf_id: indicates the index of this vnf in its chain (first vnf=0)
318 networks: the list of all networks (ChainNetwork) of the current chain
320 self.manager = chain.manager
323 self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
324 if len(networks) > 2:
325 # we will have more than 1 VM in each chain
326 self.name += '-' + str(vnf_id)
333 # the vnf_id is conveniently also the starting index in networks
334 # for the left and right networks associated to this VNF
335 self._setup(networks[vnf_id:vnf_id + 2])
337 LOG.error("Error creating VNF %s", self.name)
341 def _get_vm_config(self, remote_mac_pair):
342 config = self.manager.config
343 devices = self.manager.generator_config.devices
344 with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
345 content = boot_script.read()
346 g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
347 g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
349 'forwarder': config.vm_forwarder,
350 'intf_mac1': self.ports[LEFT].get_mac(),
351 'intf_mac2': self.ports[RIGHT].get_mac(),
352 'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
353 'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
354 'tg_net1': devices[LEFT].ip_addrs,
355 'tg_net2': devices[RIGHT].ip_addrs,
356 'vnf_gateway1_cidr': g1cidr,
357 'vnf_gateway2_cidr': g2cidr,
358 'tg_mac1': remote_mac_pair[0],
359 'tg_mac2': remote_mac_pair[1]
361 return content.format(**vm_config)
363 def _get_vnic_type(self, port_index):
364 """Get the right vnic type for given port indexself.
366 If SR-IOV is speficied, middle ports in multi-VNF chains
367 can use vswitch or SR-IOV based on config.use_sriov_middle_net
369 if self.manager.config.sriov:
370 if self.manager.config.use_sriov_middle_net:
373 # first VNF in chain must use sriov for left port
376 elif (self.vnf_id == self.chain.get_length() - 1) and (port_index == 1):
377 # last VNF in chain must use sriov for right port
381 def _setup(self, networks):
382 flavor_id = self.manager.flavor.flavor.id
383 # Check if we can reuse an instance with same name
384 for instance in self.manager.existing_instances:
385 if instance.name == self.name:
386 # Verify that other instance characteristics match
387 if instance.flavor['id'] != flavor_id:
388 self._reuse_exception('Flavor mismatch')
389 if instance.status != "ACTIVE":
390 self._reuse_exception('Matching instance is not in ACTIVE state')
391 # The 2 networks for this instance must also be reused
392 if not networks[LEFT].reuse:
393 self._reuse_exception('network %s is new' % networks[LEFT].name)
394 if not networks[RIGHT].reuse:
395 self._reuse_exception('network %s is new' % networks[RIGHT].name)
396 # instance.networks have the network names as keys:
397 # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
398 if networks[LEFT].name not in instance.networks:
399 self._reuse_exception('Left network mismatch')
400 if networks[RIGHT].name not in instance.networks:
401 self._reuse_exception('Right network mismatch')
402 # Other checks not performed (yet)
403 # check if az and compute node match
405 self.instance = instance
406 LOG.info('Reusing existing instance %s on %s',
407 self.name, self.get_hypervisor_name())
408 # create or reuse/discover 2 ports per instance
409 self.ports = [ChainVnfPort(self.name + '-' + str(index),
412 self._get_vnic_type(index)) for index in [0, 1]]
413 # if no reuse, actual vm creation is deferred after all ports in the chain are created
414 # since we need to know the next mac in a multi-vnf chain
417 """Get the AZ associated to this VNF."""
418 return self.manager.az[0]
420 def create_vnf(self, remote_mac_pair):
421 """Create the VNF instance if it does not already exist."""
422 if self.instance is None:
423 port_ids = [{'port-id': vnf_port.port['id']}
424 for vnf_port in self.ports]
425 vm_config = self._get_vm_config(remote_mac_pair)
427 server = self.manager.comp.create_server(self.name,
428 self.manager.image_instance,
429 self.manager.flavor.flavor,
436 files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
438 LOG.info('Created instance %s on %s', self.name, az)
439 self.instance = server
442 raise ChainException('Unable to create instance: %s' % (self.name))
444 def _reuse_exception(self, reason):
445 raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
447 def get_status(self):
448 """Get the statis of this instance."""
449 if self.instance.status != 'ACTIVE':
450 self.instance = self.manager.comp.poll_server(self.instance)
451 return self.instance.status
453 def get_hostname(self):
454 """Get the hypervisor host name running this VNF instance."""
455 return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
457 def get_host_ip(self):
458 """Get the IP address of the host where this instance runs.
460 return: the IP address
463 self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
466 def get_hypervisor_name(self):
467 """Get hypervisor name (az:hostname) for this VNF instance."""
469 az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
470 hostname = self.get_hostname()
472 return az + ':' + hostname
477 """Get the uuid for this instance."""
478 return self.instance.id
480 def delete(self, forced=False):
481 """Delete this VNF instance."""
483 LOG.info("Instance %s not deleted (reused)", self.name)
486 self.manager.comp.delete_server(self.instance)
487 LOG.info("Deleted instance %s", self.name)
488 for port in self.ports:
492 """A class to manage a single chain.
494 Can handle any type of chain (EXT, PVP, PVVP)
497 def __init__(self, chain_id, manager):
498 """Create a new chain.
500 chain_id: chain index (first chain is 0)
501 manager: the chain manager that owns all chains
503 self.chain_id = chain_id
504 self.manager = manager
505 self.encaps = manager.encaps
509 self.networks = manager.get_networks(chain_id)
510 # For external chain VNFs can only be discovered from their MAC addresses
511 # either from config or from ARP
512 if manager.config.service_chain != ChainType.EXT:
513 for chain_instance_index in range(self.get_length()):
514 self.instances.append(ChainVnf(self,
515 chain_instance_index,
517 # now that all VNF ports are created we need to calculate the
518 # left/right remote MAC for each VNF in the chain
519 # before actually creating the VNF itself
520 rem_mac_pairs = self._get_remote_mac_pairs()
521 for instance in self.instances:
522 rem_mac_pair = rem_mac_pairs.pop(0)
523 instance.create_vnf(rem_mac_pair)
528 def get_length(self):
529 """Get the number of VNF in the chain."""
530 return len(self.networks) - 1
532 def _get_remote_mac_pairs(self):
533 """Get the list of remote mac pairs for every VNF in the chain.
535 Traverse the chain from left to right and establish the
536 left/right remote MAC for each VNF in the chainself.
539 mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
540 must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
541 the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
544 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
545 Must produce the following list:
546 [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
548 General case with 3 VMs in chain, the list of consecutive macs (left to right):
549 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
550 Must produce the following list:
551 [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
552 or index: [[0, 3], [2, 5], [4, 7]]
554 The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
556 # line up all mac from left to right
557 mac_seq = [self.manager.generator_config.devices[LEFT].mac]
558 for instance in self.instances:
559 mac_seq.append(instance.ports[0].get_mac())
560 mac_seq.append(instance.ports[1].get_mac())
561 mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
564 for _ in self.instances:
565 rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
569 def get_instances(self):
570 """Return all instances for this chain."""
571 return self.instances
573 def get_vlan(self, port_index):
574 """Get the VLAN id on a given port.
576 port_index: left port is 0, right port is 1
577 return: the vlan_id or None if there is no vlan tagging
579 # for port 1 we need to return the VLAN of the last network in the chain
580 # The networks array contains 2 networks for PVP [left, right]
581 # and 3 networks in the case of PVVP [left.middle,right]
583 # this will pick the last item in array
585 return self.networks[port_index].get_vlan()
587 def get_dest_mac(self, port_index):
588 """Get the dest MAC on a given port.
590 port_index: left port is 0, right port is 1
594 # for right port, use the right port MAC of the last (right most) VNF In chain
595 return self.instances[-1].ports[1].get_mac()
596 # for left port use the left port MAC of the first (left most) VNF in chain
597 return self.instances[0].ports[0].get_mac()
599 def get_network_uuids(self):
600 """Get UUID of networks in this chain from left to right (order is important).
602 :return: list of UUIDs of networks (2 or 3 elements)
604 return [net['id'] for net in self.networks]
606 def get_host_ips(self):
607 """Return the IP adresss(es) of the host compute nodes used for this chain.
609 :return: a list of 1 or 2 IP addresses
611 return [vnf.get_host_ip() for vnf in self.instances]
613 def get_compute_nodes(self):
614 """Return the name of the host compute nodes used for this chain.
616 :return: a list of 1 host name in the az:host format
618 # Since all chains go through the same compute node(s) we can just retrieve the
619 # compute node name(s) for the first chain
620 return [vnf.get_hypervisor_name() for vnf in self.instances]
623 """Delete this chain."""
624 for instance in self.instances:
626 # only delete if these are chain private networks (not shared)
627 if not self.manager.config.service_chain_shared_net:
628 for network in self.networks:
632 class ChainManager(object):
633 """A class for managing all chains for a given run.
635 Supports openstack or no openstack.
636 Supports EXT, PVP and PVVP chains.
639 def __init__(self, chain_runner):
640 """Create a chain manager to take care of discovering or bringing up the requested chains.
642 A new instance must be created every time a new config is used.
643 config: the nfvbench config to use
644 cred: openstack credentials to use of None if there is no openstack
646 self.chain_runner = chain_runner
647 self.config = chain_runner.config
648 self.generator_config = chain_runner.traffic_client.generator_config
650 self.image_instance = None
651 self.image_name = None
652 # Left and right networks shared across all chains (only if shared)
657 self.nova_client = None
658 self.neutron_client = None
659 self.glance_client = None
660 self.existing_instances = []
661 # existing ports keyed by the network uuid they belong to
662 self._existing_ports = {}
664 self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
665 self.chain_count = config.service_chain_count
668 session = chain_runner.cred.get_session()
669 self.nova_client = Client(2, session=session)
670 self.neutron_client = neutronclient.Client('2.0', session=session)
671 self.glance_client = glanceclient.Client('2', session=session)
672 self.comp = compute.Compute(self.nova_client,
677 if config.service_chain != ChainType.EXT:
678 # we need to find 1 hypervisor
679 az_list = self.comp.get_enabled_az_host_list(1)
681 raise ChainException('No matching hypervisor found')
684 self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
685 # Get list of all existing instances to check if some instances can be reused
686 self.existing_instances = self.comp.get_server_list()
687 # If networks are shared across chains, get the list of networks
688 if config.service_chain_shared_net:
689 self.networks = self.get_networks()
690 # Reuse/create chains
691 for chain_id in range(self.chain_count):
692 self.chains.append(Chain(chain_id, self))
693 if config.service_chain == ChainType.EXT:
694 # if EXT and no ARP we need to read dest MACs from config
696 self._get_dest_macs_from_config()
698 # Make sure all instances are active before proceeding
699 self._ensure_instances_active()
704 # no openstack, no need to create chains
705 # make sure there at least as many entries as chains in each left/right list
706 if len(config.vlans) != 2:
707 raise ChainException('The config vlans property must be a list '
708 'with 2 lists of VLAN IDs')
709 if not config.l2_loopback:
710 self._get_dest_macs_from_config()
713 self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
714 self._check_list('vlans[1]', config.vlans[1], re_vlan)]
716 def _get_dest_macs_from_config(self):
717 re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
718 tg_config = self.config.traffic_generator
719 self.dest_macs = [self._check_list("mac_addrs_left",
720 tg_config.mac_addrs_left, re_mac),
721 self._check_list("mac_addrs_right",
722 tg_config.mac_addrs_right, re_mac)]
724 def _check_list(self, list_name, ll, pattern):
725 # if it is a single int or mac, make it a list of 1 int
726 if isinstance(ll, (int, str)):
728 if not ll or len(ll) < self.chain_count:
729 raise ChainException('%s=%s must be a list with 1 element per chain' % (list_name, ll))
731 if not re.match(pattern, str(item)):
732 raise ChainException("Invalid format '{item}' specified in {fname}"
733 .format(item=item, fname=list_name))
736 def _setup_image(self):
737 # To avoid reuploading image in server mode, check whether image_name is set or not
739 self.image_instance = self.comp.find_image(self.image_name)
740 if self.image_instance:
741 LOG.info("Reusing image %s", self.image_name)
743 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
744 if self.config.vm_image_file:
745 match = re.search(image_name_search_pattern, self.config.vm_image_file)
747 self.image_name = match.group(1)
748 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
750 raise ChainException('Provided VM image file name %s must start with '
751 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
753 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
754 for f in os.listdir(pkg_root):
755 if re.search(image_name_search_pattern, f):
756 self.config.vm_image_file = pkg_root + '/' + f
757 self.image_name = f.replace('.qcow2', '')
758 LOG.info('Found built-in VM image file %s', f)
761 raise ChainException('Cannot find any built-in VM image file.')
763 self.image_instance = self.comp.find_image(self.image_name)
764 if not self.image_instance:
765 LOG.info('Uploading %s', self.image_name)
766 res = self.comp.upload_image_via_url(self.image_name,
767 self.config.vm_image_file)
770 raise ChainException('Error uploading image %s from %s. ABORTING.' %
771 (self.image_name, self.config.vm_image_file))
772 LOG.info('Image %s successfully uploaded.', self.image_name)
773 self.image_instance = self.comp.find_image(self.image_name)
775 def _ensure_instances_active(self):
777 for chain in self.chains:
778 instances.extend(chain.get_instances())
779 initial_instance_count = len(instances)
780 max_retries = (self.config.check_traffic_time_sec +
781 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
784 remaining_instances = []
785 for instance in instances:
786 status = instance.get_status()
787 if status == 'ACTIVE':
789 if status == 'ERROR':
790 raise ChainException('Instance %s creation error: %s' %
792 instance.instance.fault['message']))
793 remaining_instances.append(instance)
794 if not remaining_instances:
797 if retry >= max_retries:
798 raise ChainException('Time-out: %d/%d instances still not active' %
799 (len(remaining_instances), initial_instance_count))
800 LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
801 len(remaining_instances), initial_instance_count,
803 instances = remaining_instances
804 time.sleep(self.config.generic_poll_sec)
805 if initial_instance_count:
806 LOG.info('All instances are active')
808 def get_networks(self, chain_id=None):
809 """Get the networks for given EXT, PVP or PVVP chain.
811 For EXT packet path, these networks must pre-exist.
812 For PVP, PVVP these networks will be created if they do not exist.
813 chain_id: to which chain the networks belong.
814 a None value will mean that these networks are shared by all chains
817 # the only case where self.networks exists is when the networks are shared
820 if self.config.service_chain == ChainType.EXT:
822 ext_net = self.config.external_networks
823 net_cfg = [AttrDict({'name': name,
824 'segmentation_id': None,
825 'physical_network': None})
826 for name in [ext_net.left, ext_net.right]]
829 int_nets = self.config.internal_networks
830 if self.config.service_chain == ChainType.PVP:
831 net_cfg = [int_nets.left, int_nets.right]
833 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
837 networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
839 # need to cleanup all successful networks prior to bailing out
845 def get_existing_ports(self):
846 """Get the list of existing ports.
848 Lazy retrieval of ports as this can be costly if there are lots of ports and
849 is only needed when VM and network are being reused.
851 return: a dict of list of neutron ports indexed by the network uuid they are attached to
853 Each port is a dict with fields such as below:
854 {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
855 'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
856 'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
857 'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
858 'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
859 'security_groups': [],
860 'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
861 'vhostuser_mode': 'server'},
862 'binding:vif_type': 'vhostuser',
863 'mac_address': 'fa:16:3e:3c:63:04',
864 'project_id': '977ac76a63d7492f927fa80e86baff4c',
866 'binding:host_id': 'a20-champagne-compute-1',
868 'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
869 'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
870 'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
871 'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
872 'created_at': '2018-10-06T07:15:10Z',
873 'binding:vnic_type': 'normal'}
875 if not self._existing_ports:
876 LOG.info('Loading list of all ports...')
877 existing_ports = self.neutron_client.list_ports()['ports']
878 # place all ports in the dict keyed by the port network uuid
879 for port in existing_ports:
880 port_list = self._existing_ports.setdefault(port['network_id'], [])
881 port_list.append(port)
882 LOG.info("Loaded %d ports attached to %d networks",
883 len(existing_ports), len(self._existing_ports))
884 return self._existing_ports
886 def get_ports_from_network(self, chain_network):
887 """Get the list of existing ports that belong to a network.
889 Lazy retrieval of ports as this can be costly if there are lots of ports and
890 is only needed when VM and network are being reused.
892 chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
893 return: list of neutron ports attached to requested network
895 return self.get_existing_ports().get(chain_network.get_uuid(), None)
897 def get_host_ip_from_mac(self, mac):
898 """Get the host IP address matching a MAC.
900 mac: MAC address to look for
901 return: the IP address of the host where the matching port runs or None if not found
903 # _existing_ports is a dict of list of ports indexed by network id
904 for port_list in self.get_existing_ports().values():
905 for port in port_list:
907 if port['mac_address'] == mac:
908 host_id = port['binding:host_id']
909 return self.comp.get_hypervisor(host_id).host_ip
914 def get_chain_vlans(self, port_index):
915 """Get the list of per chain VLAN id on a given port.
917 port_index: left port is 0, right port is 1
918 return: a VLAN ID list indexed by the chain index or None if no vlan tagging
921 return [self.chains[chain_index].get_vlan(port_index)
922 for chain_index in range(self.chain_count)]
924 return self.vlans[port_index]
926 def get_dest_macs(self, port_index):
927 """Get the list of per chain dest MACs on a given port.
929 Should not be called if EXT+ARP is used (in that case the traffic gen will
930 have the ARP responses back from VNFs with the dest MAC to use).
932 port_index: left port is 0, right port is 1
933 return: a list of dest MACs indexed by the chain index
935 if self.chains and self.config.service_chain != ChainType.EXT:
936 return [self.chains[chain_index].get_dest_mac(port_index)
937 for chain_index in range(self.chain_count)]
938 # no openstack or EXT+no-arp
939 return self.dest_macs[port_index]
941 def get_host_ips(self):
942 """Return the IP adresss(es) of the host compute nodes used for this run.
944 :return: a list of 1 IP address
946 # Since all chains go through the same compute node(s) we can just retrieve the
947 # compute node(s) for the first chain
949 if self.config.service_chain != ChainType.EXT:
950 return self.chains[0].get_host_ips()
951 # in the case of EXT, the compute node must be retrieved from the port
952 # associated to any of the dest MACs
953 dst_macs = self.chain_runner.traffic_client.gen.get_dest_macs()
954 # dest MAC on port 0, chain 0
955 dst_mac = dst_macs[0][0]
956 host_ip = self.get_host_ip_from_mac(dst_mac)
958 LOG.info('Found compute node IP for EXT chain: %s', host_ip)
962 def get_compute_nodes(self):
963 """Return the name of the host compute nodes used for this run.
965 :return: a list of 0 or 1 host name in the az:host format
967 # Since all chains go through the same compute node(s) we can just retrieve the
968 # compute node name(s) for the first chain
970 # in the case of EXT, the compute node must be retrieved from the port
971 # associated to any of the dest MACs
972 return self.chains[0].get_compute_nodes()
973 # no openstack = no chains
977 """Delete resources for all chains.
979 Will not delete any resource if no-cleanup has been requested.
981 if self.config.no_cleanup:
983 for chain in self.chains:
985 for network in self.networks: