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']
293 Extract VNI for this network.
295 :return: VNI ID for this network
297 if self.network['provider:network_type'] != 'vxlan':
298 raise ChainException('Trying to retrieve VNI for non VXLAN network')
299 return self.network['provider:segmentation_id']
302 """Delete this network."""
303 if not self.reuse and self.network:
305 while retry < self.manager.config.generic_retry_count:
307 self.manager.neutron_client.delete_network(self.network['id'])
308 LOG.info("Deleted network: %s", self.name)
312 LOG.info('Error deleting network %s (retry %d/%d)...',
315 self.manager.config.generic_retry_count)
316 time.sleep(self.manager.config.generic_poll_sec)
317 LOG.error('Unable to delete network: %s', self.name)
320 class ChainVnf(object):
321 """A class to represent a VNF in a chain."""
323 def __init__(self, chain, vnf_id, networks):
324 """Reuse a VNF instance with same characteristics or create a new VNF instance.
326 chain: the chain where this vnf belongs
327 vnf_id: indicates the index of this vnf in its chain (first vnf=0)
328 networks: the list of all networks (ChainNetwork) of the current chain
330 self.manager = chain.manager
333 self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
334 if len(networks) > 2:
335 # we will have more than 1 VM in each chain
336 self.name += '-' + str(vnf_id)
343 # the vnf_id is conveniently also the starting index in networks
344 # for the left and right networks associated to this VNF
345 self._setup(networks[vnf_id:vnf_id + 2])
347 LOG.error("Error creating VNF %s", self.name)
351 def _get_vm_config(self, remote_mac_pair):
352 config = self.manager.config
353 devices = self.manager.generator_config.devices
354 with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
355 content = boot_script.read()
356 g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
357 g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
359 'forwarder': config.vm_forwarder,
360 'intf_mac1': self.ports[LEFT].get_mac(),
361 'intf_mac2': self.ports[RIGHT].get_mac(),
362 'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
363 'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
364 'tg_net1': devices[LEFT].ip_addrs,
365 'tg_net2': devices[RIGHT].ip_addrs,
366 'vnf_gateway1_cidr': g1cidr,
367 'vnf_gateway2_cidr': g2cidr,
368 'tg_mac1': remote_mac_pair[0],
369 'tg_mac2': remote_mac_pair[1]
371 return content.format(**vm_config)
373 def _get_vnic_type(self, port_index):
374 """Get the right vnic type for given port indexself.
376 If SR-IOV is speficied, middle ports in multi-VNF chains
377 can use vswitch or SR-IOV based on config.use_sriov_middle_net
379 if self.manager.config.sriov:
380 chain_length = self.chain.get_length()
381 if self.manager.config.use_sriov_middle_net or chain_length == 1:
383 if self.vnf_id == 0 and port_index == 0:
384 # first VNF in chain must use sriov for left port
386 if (self.vnf_id == chain_length - 1) and (port_index == 1):
387 # last VNF in chain must use sriov for right port
391 def _setup(self, networks):
392 flavor_id = self.manager.flavor.flavor.id
393 # Check if we can reuse an instance with same name
394 for instance in self.manager.existing_instances:
395 if instance.name == self.name:
396 # Verify that other instance characteristics match
397 if instance.flavor['id'] != flavor_id:
398 self._reuse_exception('Flavor mismatch')
399 if instance.status != "ACTIVE":
400 self._reuse_exception('Matching instance is not in ACTIVE state')
401 # The 2 networks for this instance must also be reused
402 if not networks[LEFT].reuse:
403 self._reuse_exception('network %s is new' % networks[LEFT].name)
404 if not networks[RIGHT].reuse:
405 self._reuse_exception('network %s is new' % networks[RIGHT].name)
406 # instance.networks have the network names as keys:
407 # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
408 if networks[LEFT].name not in instance.networks:
409 self._reuse_exception('Left network mismatch')
410 if networks[RIGHT].name not in instance.networks:
411 self._reuse_exception('Right network mismatch')
414 self.instance = instance
415 LOG.info('Reusing existing instance %s on %s',
416 self.name, self.get_hypervisor_name())
417 # create or reuse/discover 2 ports per instance
418 self.ports = [ChainVnfPort(self.name + '-' + str(index),
421 self._get_vnic_type(index)) for index in [0, 1]]
422 # if no reuse, actual vm creation is deferred after all ports in the chain are created
423 # since we need to know the next mac in a multi-vnf chain
425 def create_vnf(self, remote_mac_pair):
426 """Create the VNF instance if it does not already exist."""
427 if self.instance is None:
428 port_ids = [{'port-id': vnf_port.port['id']}
429 for vnf_port in self.ports]
430 vm_config = self._get_vm_config(remote_mac_pair)
431 az = self.manager.placer.get_required_az()
432 server = self.manager.comp.create_server(self.name,
433 self.manager.image_instance,
434 self.manager.flavor.flavor,
441 files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
443 self.instance = server
444 if self.manager.placer.is_resolved():
445 LOG.info('Created instance %s on %s', self.name, az)
447 # the location is undetermined at this point
448 # self.get_hypervisor_name() will return None
449 LOG.info('Created instance %s - waiting for placement resolution...', self.name)
450 # here we MUST wait until this instance is resolved otherwise subsequent
451 # VNF creation can be placed in other hypervisors!
452 config = self.manager.config
453 max_retries = (config.check_traffic_time_sec +
454 config.generic_poll_sec - 1) / config.generic_poll_sec
456 for retry in range(max_retries):
457 status = self.get_status()
458 if status == 'ACTIVE':
459 hyp_name = self.get_hypervisor_name()
460 LOG.info('Instance %s is active and has been placed on %s',
462 self.manager.placer.register_full_name(hyp_name)
464 if status == 'ERROR':
465 raise ChainException('Instance %s creation error: %s' %
467 self.instance.fault['message']))
468 LOG.info('Waiting for instance %s to become active (retry %d/%d)...',
469 self.name, retry + 1, max_retries + 1)
470 time.sleep(config.generic_poll_sec)
473 LOG.error('Instance %s creation timed out', self.name)
474 raise ChainException('Instance %s creation timed out' % self.name)
477 raise ChainException('Unable to create instance: %s' % (self.name))
479 def _reuse_exception(self, reason):
480 raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
482 def get_status(self):
483 """Get the statis of this instance."""
484 if self.instance.status != 'ACTIVE':
485 self.instance = self.manager.comp.poll_server(self.instance)
486 return self.instance.status
488 def get_hostname(self):
489 """Get the hypervisor host name running this VNF instance."""
490 return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
492 def get_host_ip(self):
493 """Get the IP address of the host where this instance runs.
495 return: the IP address
498 self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
501 def get_hypervisor_name(self):
502 """Get hypervisor name (az:hostname) for this VNF instance."""
504 az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
505 hostname = self.get_hostname()
507 return az + ':' + hostname
512 """Get the uuid for this instance."""
513 return self.instance.id
515 def delete(self, forced=False):
516 """Delete this VNF instance."""
518 LOG.info("Instance %s not deleted (reused)", self.name)
521 self.manager.comp.delete_server(self.instance)
522 LOG.info("Deleted instance %s", self.name)
523 for port in self.ports:
527 """A class to manage a single chain.
529 Can handle any type of chain (EXT, PVP, PVVP)
532 def __init__(self, chain_id, manager):
533 """Create a new chain.
535 chain_id: chain index (first chain is 0)
536 manager: the chain manager that owns all chains
538 self.chain_id = chain_id
539 self.manager = manager
540 self.encaps = manager.encaps
544 self.networks = manager.get_networks(chain_id)
545 # For external chain VNFs can only be discovered from their MAC addresses
546 # either from config or from ARP
547 if manager.config.service_chain != ChainType.EXT:
548 for chain_instance_index in range(self.get_length()):
549 self.instances.append(ChainVnf(self,
550 chain_instance_index,
552 # at this point new VNFs are not created yet but
553 # verify that all discovered VNFs are on the same hypervisor
554 self._check_hypervisors()
555 # now that all VNF ports are created we need to calculate the
556 # left/right remote MAC for each VNF in the chain
557 # before actually creating the VNF itself
558 rem_mac_pairs = self._get_remote_mac_pairs()
559 for instance in self.instances:
560 rem_mac_pair = rem_mac_pairs.pop(0)
561 instance.create_vnf(rem_mac_pair)
566 def _check_hypervisors(self):
567 common_hypervisor = None
568 for instance in self.instances:
569 # get the full hypervizor name (az:compute)
570 hname = instance.get_hypervisor_name()
572 if common_hypervisor:
573 if hname != common_hypervisor:
574 raise ChainException('Discovered instances on different hypervisors:'
575 ' %s and %s' % (hname, common_hypervisor))
577 common_hypervisor = hname
578 if common_hypervisor:
579 # check that the common hypervisor name matchs the requested hypervisor name
580 # and set the name to be used by all future instances (if any)
581 if not self.manager.placer.register_full_name(common_hypervisor):
582 raise ChainException('Discovered hypervisor placement %s is incompatible' %
585 def get_length(self):
586 """Get the number of VNF in the chain."""
587 return len(self.networks) - 1
589 def _get_remote_mac_pairs(self):
590 """Get the list of remote mac pairs for every VNF in the chain.
592 Traverse the chain from left to right and establish the
593 left/right remote MAC for each VNF in the chainself.
596 mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
597 must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
598 the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
601 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
602 Must produce the following list:
603 [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
605 General case with 3 VMs in chain, the list of consecutive macs (left to right):
606 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
607 Must produce the following list:
608 [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
609 or index: [[0, 3], [2, 5], [4, 7]]
611 The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
613 # line up all mac from left to right
614 mac_seq = [self.manager.generator_config.devices[LEFT].mac]
615 for instance in self.instances:
616 mac_seq.append(instance.ports[0].get_mac())
617 mac_seq.append(instance.ports[1].get_mac())
618 mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
621 for _ in self.instances:
622 rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
626 def get_instances(self):
627 """Return all instances for this chain."""
628 return self.instances
630 def get_vlan(self, port_index):
631 """Get the VLAN id on a given port.
633 port_index: left port is 0, right port is 1
634 return: the vlan_id or None if there is no vlan tagging
636 # for port 1 we need to return the VLAN of the last network in the chain
637 # The networks array contains 2 networks for PVP [left, right]
638 # and 3 networks in the case of PVVP [left.middle,right]
640 # this will pick the last item in array
642 return self.networks[port_index].get_vlan()
644 def get_vxlan(self, port_index):
645 """Get the VXLAN id on a given port.
647 port_index: left port is 0, right port is 1
648 return: the vxlan_id or None if there is no vxlan
650 # for port 1 we need to return the VLAN of the last network in the chain
651 # The networks array contains 2 networks for PVP [left, right]
652 # and 3 networks in the case of PVVP [left.middle,right]
654 # this will pick the last item in array
656 return self.networks[port_index].get_vxlan()
658 def get_dest_mac(self, port_index):
659 """Get the dest MAC on a given port.
661 port_index: left port is 0, right port is 1
665 # for right port, use the right port MAC of the last (right most) VNF In chain
666 return self.instances[-1].ports[1].get_mac()
667 # for left port use the left port MAC of the first (left most) VNF in chain
668 return self.instances[0].ports[0].get_mac()
670 def get_network_uuids(self):
671 """Get UUID of networks in this chain from left to right (order is important).
673 :return: list of UUIDs of networks (2 or 3 elements)
675 return [net['id'] for net in self.networks]
677 def get_host_ips(self):
678 """Return the IP adresss(es) of the host compute nodes used for this chain.
680 :return: a list of 1 or 2 IP addresses
682 return [vnf.get_host_ip() for vnf in self.instances]
684 def get_compute_nodes(self):
685 """Return the name of the host compute nodes used for this chain.
687 :return: a list of 1 host name in the az:host format
689 # Since all chains go through the same compute node(s) we can just retrieve the
690 # compute node name(s) for the first chain
691 return [vnf.get_hypervisor_name() for vnf in self.instances]
694 """Delete this chain."""
695 for instance in self.instances:
697 # only delete if these are chain private networks (not shared)
698 if not self.manager.config.service_chain_shared_net:
699 for network in self.networks:
703 class InstancePlacer(object):
704 """A class to manage instance placement for all VNFs in all chains.
706 A full az string is made of 2 parts AZ and hypervisor.
707 The placement is resolved when both parts az and hypervisor names are known.
710 def __init__(self, req_az, req_hyp):
711 """Create a new instance placer.
713 req_az: requested AZ (can be None or empty if no preference)
714 req_hyp: requested hypervisor name (can be None of empty if no preference)
715 can be any of 'nova:', 'comp1', 'nova:comp1'
716 if it is a list, only the first item is used (backward compatibility in config)
718 req_az is ignored if req_hyp has an az part
719 all other parts beyond the first 2 are ignored in req_hyp
721 # if passed a list just pick the first item
722 if req_hyp and isinstance(req_hyp, list):
724 # only pick first part of az
725 if req_az and ':' in req_az:
726 req_az = req_az.split(':')[0]
728 # check if requested hypervisor string has an AZ part
729 split_hyp = req_hyp.split(':')
730 if len(split_hyp) > 1:
731 # override the AZ part and hypervisor part
732 req_az = split_hyp[0]
733 req_hyp = split_hyp[1]
734 self.requested_az = req_az if req_az else ''
735 self.requested_hyp = req_hyp if req_hyp else ''
736 # Nova can accept AZ only (e.g. 'nova:', use any hypervisor in that AZ)
737 # or hypervisor only (e.g. ':comp1')
738 # or both (e.g. 'nova:comp1')
740 self.required_az = req_az + ':' + self.requested_hyp
742 # need to insert a ':' so nova knows this is the hypervisor name
743 self.required_az = ':' + self.requested_hyp if req_hyp else ''
744 # placement is resolved when both AZ and hypervisor names are known and set
745 self.resolved = self.requested_az != '' and self.requested_hyp != ''
747 def get_required_az(self):
748 """Return the required az (can be resolved or not)."""
749 return self.required_az
751 def register_full_name(self, discovered_az):
752 """Verify compatibility and register a discovered hypervisor full name.
754 discovered_az: a discovered AZ in az:hypervisor format
755 return: True if discovered_az is compatible and set
756 False if discovered_az is not compatible
759 return discovered_az == self.required_az
761 # must be in full az format
762 split_daz = discovered_az.split(':')
763 if len(split_daz) != 2:
765 if self.requested_az and self.requested_az != split_daz[0]:
767 if self.requested_hyp and self.requested_hyp != split_daz[1]:
769 self.required_az = discovered_az
773 def is_resolved(self):
774 """Check if the full AZ is resolved.
776 return: True if resolved
781 class ChainManager(object):
782 """A class for managing all chains for a given run.
784 Supports openstack or no openstack.
785 Supports EXT, PVP and PVVP chains.
788 def __init__(self, chain_runner):
789 """Create a chain manager to take care of discovering or bringing up the requested chains.
791 A new instance must be created every time a new config is used.
792 config: the nfvbench config to use
793 cred: openstack credentials to use of None if there is no openstack
795 self.chain_runner = chain_runner
796 self.config = chain_runner.config
797 self.generator_config = chain_runner.traffic_client.generator_config
799 self.image_instance = None
800 self.image_name = None
801 # Left and right networks shared across all chains (only if shared)
806 self.nova_client = None
807 self.neutron_client = None
808 self.glance_client = None
809 self.existing_instances = []
810 # existing ports keyed by the network uuid they belong to
811 self._existing_ports = {}
813 self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
814 self.chain_count = config.service_chain_count
818 session = chain_runner.cred.get_session()
819 self.nova_client = Client(2, session=session)
820 self.neutron_client = neutronclient.Client('2.0', session=session)
821 self.glance_client = glanceclient.Client('2', session=session)
822 self.comp = compute.Compute(self.nova_client,
826 if config.service_chain != ChainType.EXT:
827 self.placer = InstancePlacer(config.availability_zone, config.compute_nodes)
829 self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
830 # Get list of all existing instances to check if some instances can be reused
831 self.existing_instances = self.comp.get_server_list()
832 # If networks are shared across chains, get the list of networks
833 if config.service_chain_shared_net:
834 self.networks = self.get_networks()
835 # Reuse/create chains
836 for chain_id in range(self.chain_count):
837 self.chains.append(Chain(chain_id, self))
838 if config.service_chain == ChainType.EXT:
839 # if EXT and no ARP we need to read dest MACs from config
841 self._get_dest_macs_from_config()
843 # Make sure all instances are active before proceeding
844 self._ensure_instances_active()
849 # no openstack, no need to create chains
851 if not config.l2_loopback and config.no_arp:
852 self._get_dest_macs_from_config()
853 if config.vlan_tagging:
854 # make sure there at least as many entries as chains in each left/right list
855 if len(config.vlans) != 2:
856 raise ChainException('The config vlans property must be a list '
857 'with 2 lists of VLAN IDs')
859 self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
860 self._check_list('vlans[1]', config.vlans[1], re_vlan)]
862 # make sure there are 2 entries
863 if len(config.vnis) != 2:
864 raise ChainException('The config vnis property must be a list with 2 VNIs')
865 self.vnis = [self._check_list('vnis[0]', config.vnis[0], re_vlan),
866 self._check_list('vnis[1]', config.vnis[1], re_vlan)]
868 def _get_dest_macs_from_config(self):
869 re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
870 tg_config = self.config.traffic_generator
871 self.dest_macs = [self._check_list("mac_addrs_left",
872 tg_config.mac_addrs_left, re_mac),
873 self._check_list("mac_addrs_right",
874 tg_config.mac_addrs_right, re_mac)]
876 def _check_list(self, list_name, ll, pattern):
877 # if it is a single int or mac, make it a list of 1 int
878 if isinstance(ll, (int, str)):
880 if not ll or len(ll) < self.chain_count:
881 raise ChainException('%s=%s must be a list with %d elements per chain' %
882 (list_name, ll, self.chain_count))
884 if not re.match(pattern, str(item)):
885 raise ChainException("Invalid format '{item}' specified in {fname}"
886 .format(item=item, fname=list_name))
889 def _setup_image(self):
890 # To avoid reuploading image in server mode, check whether image_name is set or not
892 self.image_instance = self.comp.find_image(self.image_name)
893 if self.image_instance:
894 LOG.info("Reusing image %s", self.image_name)
896 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
897 if self.config.vm_image_file:
898 match = re.search(image_name_search_pattern, self.config.vm_image_file)
900 self.image_name = match.group(1)
901 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
903 raise ChainException('Provided VM image file name %s must start with '
904 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
906 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
907 for f in os.listdir(pkg_root):
908 if re.search(image_name_search_pattern, f):
909 self.config.vm_image_file = pkg_root + '/' + f
910 self.image_name = f.replace('.qcow2', '')
911 LOG.info('Found built-in VM image file %s', f)
914 raise ChainException('Cannot find any built-in VM image file.')
916 self.image_instance = self.comp.find_image(self.image_name)
917 if not self.image_instance:
918 LOG.info('Uploading %s', self.image_name)
919 res = self.comp.upload_image_via_url(self.image_name,
920 self.config.vm_image_file)
923 raise ChainException('Error uploading image %s from %s. ABORTING.' %
924 (self.image_name, self.config.vm_image_file))
925 LOG.info('Image %s successfully uploaded.', self.image_name)
926 self.image_instance = self.comp.find_image(self.image_name)
928 def _ensure_instances_active(self):
930 for chain in self.chains:
931 instances.extend(chain.get_instances())
932 initial_instance_count = len(instances)
933 max_retries = (self.config.check_traffic_time_sec +
934 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
937 remaining_instances = []
938 for instance in instances:
939 status = instance.get_status()
940 if status == 'ACTIVE':
941 LOG.info('Instance %s is ACTIVE on %s',
942 instance.name, instance.get_hypervisor_name())
944 if status == 'ERROR':
945 raise ChainException('Instance %s creation error: %s' %
947 instance.instance.fault['message']))
948 remaining_instances.append(instance)
949 if not remaining_instances:
952 if retry >= max_retries:
953 raise ChainException('Time-out: %d/%d instances still not active' %
954 (len(remaining_instances), initial_instance_count))
955 LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
956 len(remaining_instances), initial_instance_count,
958 instances = remaining_instances
959 time.sleep(self.config.generic_poll_sec)
960 if initial_instance_count:
961 LOG.info('All instances are active')
963 def _get_vxlan_net_cfg(self, chain_id):
964 int_nets = self.config.internal_networks
965 net_left = int_nets.left
966 net_right = int_nets.right
967 vnis = self.generator_config.vnis
969 seg_id_left = vnis[0]
970 if self.config.service_chain == ChainType.PVP:
972 seg_id_left = ((chain_id - 1) * 2) + seg_id_left
973 seg_id_right = seg_id_left + 1
974 if (seg_id_left and seg_id_right) > vnis[1]:
975 raise Exception('Segmentation ID is more than allowed '
976 'value: {}'.format(vnis[1]))
977 net_left['segmentation_id'] = seg_id_left
978 net_right['segmentation_id'] = seg_id_right
979 net_cfg = [net_left, net_right]
982 net_middle = int_nets.middle
984 seg_id_left = ((chain_id - 1) * 3) + seg_id_left
985 seg_id_middle = seg_id_left + 1
986 seg_id_right = seg_id_left + 2
987 if (seg_id_left and seg_id_right and seg_id_middle) > vnis[1]:
988 raise Exception('Segmentation ID is more than allowed '
989 'value: {}'.format(vnis[1]))
990 net_left['segmentation_id'] = seg_id_left
991 net_middle['segmentation_id'] = seg_id_middle
992 net_right['segmentation_id'] = seg_id_right
993 net_cfg = [net_left, net_middle, net_right]
996 def get_networks(self, chain_id=None):
997 """Get the networks for given EXT, PVP or PVVP chain.
999 For EXT packet path, these networks must pre-exist.
1000 For PVP, PVVP these networks will be created if they do not exist.
1001 chain_id: to which chain the networks belong.
1002 a None value will mean that these networks are shared by all chains
1005 # the only case where self.networks exists is when the networks are shared
1007 return self.networks
1008 if self.config.service_chain == ChainType.EXT:
1010 ext_net = self.config.external_networks
1011 net_cfg = [AttrDict({'name': name,
1012 'segmentation_id': None,
1013 'physical_network': None})
1014 for name in [ext_net.left, ext_net.right]]
1017 int_nets = self.config.internal_networks
1018 network_type = set([int_nets[net].get('network_type') for net in int_nets])
1019 if self.config.vxlan and 'vxlan' in network_type:
1020 net_cfg = self._get_vxlan_net_cfg(chain_id)
1023 if self.config.service_chain == ChainType.PVP:
1024 net_cfg = [int_nets.left, int_nets.right]
1026 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
1030 networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
1032 # need to cleanup all successful networks prior to bailing out
1033 for net in networks:
1038 def get_existing_ports(self):
1039 """Get the list of existing ports.
1041 Lazy retrieval of ports as this can be costly if there are lots of ports and
1042 is only needed when VM and network are being reused.
1044 return: a dict of list of neutron ports indexed by the network uuid they are attached to
1046 Each port is a dict with fields such as below:
1047 {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
1048 'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
1049 'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
1050 'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
1051 'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
1052 'security_groups': [],
1053 'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
1054 'vhostuser_mode': 'server'},
1055 'binding:vif_type': 'vhostuser',
1056 'mac_address': 'fa:16:3e:3c:63:04',
1057 'project_id': '977ac76a63d7492f927fa80e86baff4c',
1059 'binding:host_id': 'a20-champagne-compute-1',
1061 'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
1062 'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
1063 'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
1064 'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
1065 'created_at': '2018-10-06T07:15:10Z',
1066 'binding:vnic_type': 'normal'}
1068 if not self._existing_ports:
1069 LOG.info('Loading list of all ports...')
1070 existing_ports = self.neutron_client.list_ports()['ports']
1071 # place all ports in the dict keyed by the port network uuid
1072 for port in existing_ports:
1073 port_list = self._existing_ports.setdefault(port['network_id'], [])
1074 port_list.append(port)
1075 LOG.info("Loaded %d ports attached to %d networks",
1076 len(existing_ports), len(self._existing_ports))
1077 return self._existing_ports
1079 def get_ports_from_network(self, chain_network):
1080 """Get the list of existing ports that belong to a network.
1082 Lazy retrieval of ports as this can be costly if there are lots of ports and
1083 is only needed when VM and network are being reused.
1085 chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
1086 return: list of neutron ports attached to requested network
1088 return self.get_existing_ports().get(chain_network.get_uuid(), None)
1090 def get_host_ip_from_mac(self, mac):
1091 """Get the host IP address matching a MAC.
1093 mac: MAC address to look for
1094 return: the IP address of the host where the matching port runs or None if not found
1096 # _existing_ports is a dict of list of ports indexed by network id
1097 for port_list in self.get_existing_ports().values():
1098 for port in port_list:
1100 if port['mac_address'] == mac:
1101 host_id = port['binding:host_id']
1102 return self.comp.get_hypervisor(host_id).host_ip
1107 def get_chain_vlans(self, port_index):
1108 """Get the list of per chain VLAN id on a given port.
1110 port_index: left port is 0, right port is 1
1111 return: a VLAN ID list indexed by the chain index or None if no vlan tagging
1114 return [self.chains[chain_index].get_vlan(port_index)
1115 for chain_index in range(self.chain_count)]
1117 return self.vlans[port_index]
1119 def get_chain_vxlans(self, port_index):
1120 """Get the list of per chain VNIs id on a given port.
1122 port_index: left port is 0, right port is 1
1123 return: a VNIs ID list indexed by the chain index or None if no vlan tagging
1126 return [self.chains[chain_index].get_vxlan(port_index)
1127 for chain_index in range(self.chain_count)]
1129 return self.vnis[port_index]
1131 def get_dest_macs(self, port_index):
1132 """Get the list of per chain dest MACs on a given port.
1134 Should not be called if EXT+ARP is used (in that case the traffic gen will
1135 have the ARP responses back from VNFs with the dest MAC to use).
1137 port_index: left port is 0, right port is 1
1138 return: a list of dest MACs indexed by the chain index
1140 if self.chains and self.config.service_chain != ChainType.EXT:
1141 return [self.chains[chain_index].get_dest_mac(port_index)
1142 for chain_index in range(self.chain_count)]
1143 # no openstack or EXT+no-arp
1144 return self.dest_macs[port_index]
1146 def get_host_ips(self):
1147 """Return the IP adresss(es) of the host compute nodes used for this run.
1149 :return: a list of 1 IP address
1151 # Since all chains go through the same compute node(s) we can just retrieve the
1152 # compute node(s) for the first chain
1154 if self.config.service_chain != ChainType.EXT:
1155 return self.chains[0].get_host_ips()
1156 # in the case of EXT, the compute node must be retrieved from the port
1157 # associated to any of the dest MACs
1158 dst_macs = self.generator_config.get_dest_macs()
1159 # dest MAC on port 0, chain 0
1160 dst_mac = dst_macs[0][0]
1161 host_ip = self.get_host_ip_from_mac(dst_mac)
1163 LOG.info('Found compute node IP for EXT chain: %s', host_ip)
1167 def get_compute_nodes(self):
1168 """Return the name of the host compute nodes used for this run.
1170 :return: a list of 0 or 1 host name in the az:host format
1172 # Since all chains go through the same compute node(s) we can just retrieve the
1173 # compute node name(s) for the first chain
1175 # in the case of EXT, the compute node must be retrieved from the port
1176 # associated to any of the dest MACs
1177 return self.chains[0].get_compute_nodes()
1178 # no openstack = no chains
1182 """Delete resources for all chains.
1184 Will not delete any resource if no-cleanup has been requested.
1186 if self.config.no_cleanup:
1188 for chain in self.chains:
1190 for network in self.networks:
1193 self.flavor.delete()