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')
404 self.instance = instance
405 LOG.info('Reusing existing instance %s on %s',
406 self.name, self.get_hypervisor_name())
407 # create or reuse/discover 2 ports per instance
408 self.ports = [ChainVnfPort(self.name + '-' + str(index),
411 self._get_vnic_type(index)) for index in [0, 1]]
412 # if no reuse, actual vm creation is deferred after all ports in the chain are created
413 # since we need to know the next mac in a multi-vnf chain
415 def create_vnf(self, remote_mac_pair):
416 """Create the VNF instance if it does not already exist."""
417 if self.instance is None:
418 port_ids = [{'port-id': vnf_port.port['id']}
419 for vnf_port in self.ports]
420 vm_config = self._get_vm_config(remote_mac_pair)
421 az = self.manager.placer.get_required_az()
422 server = self.manager.comp.create_server(self.name,
423 self.manager.image_instance,
424 self.manager.flavor.flavor,
431 files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
433 self.instance = server
434 if self.manager.placer.is_resolved():
435 LOG.info('Created instance %s on %s', self.name, az)
437 # the location is undetermined at this point
438 # self.get_hypervisor_name() will return None
439 LOG.info('Created instance %s - waiting for placement resolution...', self.name)
440 # here we MUST wait until this instance is resolved otherwise subsequent
441 # VNF creation can be placed in other hypervisors!
442 config = self.manager.config
443 max_retries = (config.check_traffic_time_sec +
444 config.generic_poll_sec - 1) / config.generic_poll_sec
446 for retry in range(max_retries):
447 status = self.get_status()
448 if status == 'ACTIVE':
449 hyp_name = self.get_hypervisor_name()
450 LOG.info('Instance %s is active and has been placed on %s',
452 self.manager.placer.register_full_name(hyp_name)
454 if status == 'ERROR':
455 raise ChainException('Instance %s creation error: %s' %
457 self.instance.fault['message']))
458 LOG.info('Waiting for instance %s to become active (retry %d/%d)...',
459 self.name, retry + 1, max_retries + 1)
460 time.sleep(config.generic_poll_sec)
463 LOG.error('Instance %s creation timed out', self.name)
464 raise ChainException('Instance %s creation timed out' % self.name)
467 raise ChainException('Unable to create instance: %s' % (self.name))
469 def _reuse_exception(self, reason):
470 raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
472 def get_status(self):
473 """Get the statis of this instance."""
474 if self.instance.status != 'ACTIVE':
475 self.instance = self.manager.comp.poll_server(self.instance)
476 return self.instance.status
478 def get_hostname(self):
479 """Get the hypervisor host name running this VNF instance."""
480 return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
482 def get_host_ip(self):
483 """Get the IP address of the host where this instance runs.
485 return: the IP address
488 self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
491 def get_hypervisor_name(self):
492 """Get hypervisor name (az:hostname) for this VNF instance."""
494 az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
495 hostname = self.get_hostname()
497 return az + ':' + hostname
502 """Get the uuid for this instance."""
503 return self.instance.id
505 def delete(self, forced=False):
506 """Delete this VNF instance."""
508 LOG.info("Instance %s not deleted (reused)", self.name)
511 self.manager.comp.delete_server(self.instance)
512 LOG.info("Deleted instance %s", self.name)
513 for port in self.ports:
517 """A class to manage a single chain.
519 Can handle any type of chain (EXT, PVP, PVVP)
522 def __init__(self, chain_id, manager):
523 """Create a new chain.
525 chain_id: chain index (first chain is 0)
526 manager: the chain manager that owns all chains
528 self.chain_id = chain_id
529 self.manager = manager
530 self.encaps = manager.encaps
534 self.networks = manager.get_networks(chain_id)
535 # For external chain VNFs can only be discovered from their MAC addresses
536 # either from config or from ARP
537 if manager.config.service_chain != ChainType.EXT:
538 for chain_instance_index in range(self.get_length()):
539 self.instances.append(ChainVnf(self,
540 chain_instance_index,
542 # at this point new VNFs are not created yet but
543 # verify that all discovered VNFs are on the same hypervisor
544 self._check_hypervisors()
545 # now that all VNF ports are created we need to calculate the
546 # left/right remote MAC for each VNF in the chain
547 # before actually creating the VNF itself
548 rem_mac_pairs = self._get_remote_mac_pairs()
549 for instance in self.instances:
550 rem_mac_pair = rem_mac_pairs.pop(0)
551 instance.create_vnf(rem_mac_pair)
556 def _check_hypervisors(self):
557 common_hypervisor = None
558 for instance in self.instances:
559 # get the full hypervizor name (az:compute)
560 hname = instance.get_hypervisor_name()
562 if common_hypervisor:
563 if hname != common_hypervisor:
564 raise ChainException('Discovered instances on different hypervisors:'
565 ' %s and %s' % (hname, common_hypervisor))
567 common_hypervisor = hname
568 if common_hypervisor:
569 # check that the common hypervisor name matchs the requested hypervisor name
570 # and set the name to be used by all future instances (if any)
571 if not self.manager.placer.register_full_name(common_hypervisor):
572 raise ChainException('Discovered hypervisor placement %s is incompatible' %
575 def get_length(self):
576 """Get the number of VNF in the chain."""
577 return len(self.networks) - 1
579 def _get_remote_mac_pairs(self):
580 """Get the list of remote mac pairs for every VNF in the chain.
582 Traverse the chain from left to right and establish the
583 left/right remote MAC for each VNF in the chainself.
586 mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
587 must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
588 the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
591 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
592 Must produce the following list:
593 [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
595 General case with 3 VMs in chain, the list of consecutive macs (left to right):
596 tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
597 Must produce the following list:
598 [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
599 or index: [[0, 3], [2, 5], [4, 7]]
601 The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
603 # line up all mac from left to right
604 mac_seq = [self.manager.generator_config.devices[LEFT].mac]
605 for instance in self.instances:
606 mac_seq.append(instance.ports[0].get_mac())
607 mac_seq.append(instance.ports[1].get_mac())
608 mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
611 for _ in self.instances:
612 rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
616 def get_instances(self):
617 """Return all instances for this chain."""
618 return self.instances
620 def get_vlan(self, port_index):
621 """Get the VLAN id on a given port.
623 port_index: left port is 0, right port is 1
624 return: the vlan_id or None if there is no vlan tagging
626 # for port 1 we need to return the VLAN of the last network in the chain
627 # The networks array contains 2 networks for PVP [left, right]
628 # and 3 networks in the case of PVVP [left.middle,right]
630 # this will pick the last item in array
632 return self.networks[port_index].get_vlan()
634 def get_dest_mac(self, port_index):
635 """Get the dest MAC on a given port.
637 port_index: left port is 0, right port is 1
641 # for right port, use the right port MAC of the last (right most) VNF In chain
642 return self.instances[-1].ports[1].get_mac()
643 # for left port use the left port MAC of the first (left most) VNF in chain
644 return self.instances[0].ports[0].get_mac()
646 def get_network_uuids(self):
647 """Get UUID of networks in this chain from left to right (order is important).
649 :return: list of UUIDs of networks (2 or 3 elements)
651 return [net['id'] for net in self.networks]
653 def get_host_ips(self):
654 """Return the IP adresss(es) of the host compute nodes used for this chain.
656 :return: a list of 1 or 2 IP addresses
658 return [vnf.get_host_ip() for vnf in self.instances]
660 def get_compute_nodes(self):
661 """Return the name of the host compute nodes used for this chain.
663 :return: a list of 1 host name in the az:host format
665 # Since all chains go through the same compute node(s) we can just retrieve the
666 # compute node name(s) for the first chain
667 return [vnf.get_hypervisor_name() for vnf in self.instances]
670 """Delete this chain."""
671 for instance in self.instances:
673 # only delete if these are chain private networks (not shared)
674 if not self.manager.config.service_chain_shared_net:
675 for network in self.networks:
679 class InstancePlacer(object):
680 """A class to manage instance placement for all VNFs in all chains.
682 A full az string is made of 2 parts AZ and hypervisor.
683 The placement is resolved when both parts az and hypervisor names are known.
686 def __init__(self, req_az, req_hyp):
687 """Create a new instance placer.
689 req_az: requested AZ (can be None or empty if no preference)
690 req_hyp: requested hypervisor name (can be None of empty if no preference)
691 can be any of 'nova:', 'comp1', 'nova:comp1'
692 if it is a list, only the first item is used (backward compatibility in config)
694 req_az is ignored if req_hyp has an az part
695 all other parts beyond the first 2 are ignored in req_hyp
697 # if passed a list just pick the first item
698 if req_hyp and isinstance(req_hyp, list):
700 # only pick first part of az
701 if req_az and ':' in req_az:
702 req_az = req_az.split(':')[0]
704 # check if requested hypervisor string has an AZ part
705 split_hyp = req_hyp.split(':')
706 if len(split_hyp) > 1:
707 # override the AZ part and hypervisor part
708 req_az = split_hyp[0]
709 req_hyp = split_hyp[1]
710 self.requested_az = req_az if req_az else ''
711 self.requested_hyp = req_hyp if req_hyp else ''
712 # Nova can accept AZ only (e.g. 'nova:', use any hypervisor in that AZ)
713 # or hypervisor only (e.g. ':comp1')
714 # or both (e.g. 'nova:comp1')
716 self.required_az = req_az + ':' + self.requested_hyp
719 self.required_az = self.requested_hyp if req_hyp else ''
720 # placement is resolved when both AZ and hypervisor names are known and set
721 self.resolved = self.requested_az != '' and self.requested_hyp != ''
723 def get_required_az(self):
724 """Return the required az (can be resolved or not)."""
725 return self.required_az
727 def register_full_name(self, discovered_az):
728 """Verify compatibility and register a discovered hypervisor full name.
730 discovered_az: a discovered AZ in az:hypervisor format
731 return: True if discovered_az is compatible and set
732 False if discovered_az is not compatible
735 return discovered_az == self.required_az
737 # must be in full az format
738 split_daz = discovered_az.split(':')
739 if len(split_daz) != 2:
741 if self.requested_az and self.requested_az != split_daz[0]:
743 if self.requested_hyp and self.requested_hyp != split_daz[1]:
745 self.required_az = discovered_az
749 def is_resolved(self):
750 """Check if the full AZ is resolved.
752 return: True if resolved
757 class ChainManager(object):
758 """A class for managing all chains for a given run.
760 Supports openstack or no openstack.
761 Supports EXT, PVP and PVVP chains.
764 def __init__(self, chain_runner):
765 """Create a chain manager to take care of discovering or bringing up the requested chains.
767 A new instance must be created every time a new config is used.
768 config: the nfvbench config to use
769 cred: openstack credentials to use of None if there is no openstack
771 self.chain_runner = chain_runner
772 self.config = chain_runner.config
773 self.generator_config = chain_runner.traffic_client.generator_config
775 self.image_instance = None
776 self.image_name = None
777 # Left and right networks shared across all chains (only if shared)
782 self.nova_client = None
783 self.neutron_client = None
784 self.glance_client = None
785 self.existing_instances = []
786 # existing ports keyed by the network uuid they belong to
787 self._existing_ports = {}
789 self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
790 self.chain_count = config.service_chain_count
794 session = chain_runner.cred.get_session()
795 self.nova_client = Client(2, session=session)
796 self.neutron_client = neutronclient.Client('2.0', session=session)
797 self.glance_client = glanceclient.Client('2', session=session)
798 self.comp = compute.Compute(self.nova_client,
802 if config.service_chain != ChainType.EXT:
803 self.placer = InstancePlacer(config.availability_zone, config.compute_nodes)
805 self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
806 # Get list of all existing instances to check if some instances can be reused
807 self.existing_instances = self.comp.get_server_list()
808 # If networks are shared across chains, get the list of networks
809 if config.service_chain_shared_net:
810 self.networks = self.get_networks()
811 # Reuse/create chains
812 for chain_id in range(self.chain_count):
813 self.chains.append(Chain(chain_id, self))
814 if config.service_chain == ChainType.EXT:
815 # if EXT and no ARP we need to read dest MACs from config
817 self._get_dest_macs_from_config()
819 # Make sure all instances are active before proceeding
820 self._ensure_instances_active()
825 # no openstack, no need to create chains
826 # make sure there at least as many entries as chains in each left/right list
827 if len(config.vlans) != 2:
828 raise ChainException('The config vlans property must be a list '
829 'with 2 lists of VLAN IDs')
830 if not config.l2_loopback:
831 self._get_dest_macs_from_config()
834 self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
835 self._check_list('vlans[1]', config.vlans[1], re_vlan)]
837 def _get_dest_macs_from_config(self):
838 re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
839 tg_config = self.config.traffic_generator
840 self.dest_macs = [self._check_list("mac_addrs_left",
841 tg_config.mac_addrs_left, re_mac),
842 self._check_list("mac_addrs_right",
843 tg_config.mac_addrs_right, re_mac)]
845 def _check_list(self, list_name, ll, pattern):
846 # if it is a single int or mac, make it a list of 1 int
847 if isinstance(ll, (int, str)):
849 if not ll or len(ll) < self.chain_count:
850 raise ChainException('%s=%s must be a list with 1 element per chain' % (list_name, ll))
852 if not re.match(pattern, str(item)):
853 raise ChainException("Invalid format '{item}' specified in {fname}"
854 .format(item=item, fname=list_name))
857 def _setup_image(self):
858 # To avoid reuploading image in server mode, check whether image_name is set or not
860 self.image_instance = self.comp.find_image(self.image_name)
861 if self.image_instance:
862 LOG.info("Reusing image %s", self.image_name)
864 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
865 if self.config.vm_image_file:
866 match = re.search(image_name_search_pattern, self.config.vm_image_file)
868 self.image_name = match.group(1)
869 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
871 raise ChainException('Provided VM image file name %s must start with '
872 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
874 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
875 for f in os.listdir(pkg_root):
876 if re.search(image_name_search_pattern, f):
877 self.config.vm_image_file = pkg_root + '/' + f
878 self.image_name = f.replace('.qcow2', '')
879 LOG.info('Found built-in VM image file %s', f)
882 raise ChainException('Cannot find any built-in VM image file.')
884 self.image_instance = self.comp.find_image(self.image_name)
885 if not self.image_instance:
886 LOG.info('Uploading %s', self.image_name)
887 res = self.comp.upload_image_via_url(self.image_name,
888 self.config.vm_image_file)
891 raise ChainException('Error uploading image %s from %s. ABORTING.' %
892 (self.image_name, self.config.vm_image_file))
893 LOG.info('Image %s successfully uploaded.', self.image_name)
894 self.image_instance = self.comp.find_image(self.image_name)
896 def _ensure_instances_active(self):
898 for chain in self.chains:
899 instances.extend(chain.get_instances())
900 initial_instance_count = len(instances)
901 max_retries = (self.config.check_traffic_time_sec +
902 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
905 remaining_instances = []
906 for instance in instances:
907 status = instance.get_status()
908 if status == 'ACTIVE':
909 LOG.info('Instance %s is ACTIVE on %s',
910 instance.name, instance.get_hypervisor_name())
912 if status == 'ERROR':
913 raise ChainException('Instance %s creation error: %s' %
915 instance.instance.fault['message']))
916 remaining_instances.append(instance)
917 if not remaining_instances:
920 if retry >= max_retries:
921 raise ChainException('Time-out: %d/%d instances still not active' %
922 (len(remaining_instances), initial_instance_count))
923 LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
924 len(remaining_instances), initial_instance_count,
926 instances = remaining_instances
927 time.sleep(self.config.generic_poll_sec)
928 if initial_instance_count:
929 LOG.info('All instances are active')
931 def get_networks(self, chain_id=None):
932 """Get the networks for given EXT, PVP or PVVP chain.
934 For EXT packet path, these networks must pre-exist.
935 For PVP, PVVP these networks will be created if they do not exist.
936 chain_id: to which chain the networks belong.
937 a None value will mean that these networks are shared by all chains
940 # the only case where self.networks exists is when the networks are shared
943 if self.config.service_chain == ChainType.EXT:
945 ext_net = self.config.external_networks
946 net_cfg = [AttrDict({'name': name,
947 'segmentation_id': None,
948 'physical_network': None})
949 for name in [ext_net.left, ext_net.right]]
952 int_nets = self.config.internal_networks
953 if self.config.service_chain == ChainType.PVP:
954 net_cfg = [int_nets.left, int_nets.right]
956 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
960 networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
962 # need to cleanup all successful networks prior to bailing out
968 def get_existing_ports(self):
969 """Get the list of existing ports.
971 Lazy retrieval of ports as this can be costly if there are lots of ports and
972 is only needed when VM and network are being reused.
974 return: a dict of list of neutron ports indexed by the network uuid they are attached to
976 Each port is a dict with fields such as below:
977 {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
978 'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
979 'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
980 'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
981 'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
982 'security_groups': [],
983 'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
984 'vhostuser_mode': 'server'},
985 'binding:vif_type': 'vhostuser',
986 'mac_address': 'fa:16:3e:3c:63:04',
987 'project_id': '977ac76a63d7492f927fa80e86baff4c',
989 'binding:host_id': 'a20-champagne-compute-1',
991 'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
992 'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
993 'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
994 'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
995 'created_at': '2018-10-06T07:15:10Z',
996 'binding:vnic_type': 'normal'}
998 if not self._existing_ports:
999 LOG.info('Loading list of all ports...')
1000 existing_ports = self.neutron_client.list_ports()['ports']
1001 # place all ports in the dict keyed by the port network uuid
1002 for port in existing_ports:
1003 port_list = self._existing_ports.setdefault(port['network_id'], [])
1004 port_list.append(port)
1005 LOG.info("Loaded %d ports attached to %d networks",
1006 len(existing_ports), len(self._existing_ports))
1007 return self._existing_ports
1009 def get_ports_from_network(self, chain_network):
1010 """Get the list of existing ports that belong to a network.
1012 Lazy retrieval of ports as this can be costly if there are lots of ports and
1013 is only needed when VM and network are being reused.
1015 chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
1016 return: list of neutron ports attached to requested network
1018 return self.get_existing_ports().get(chain_network.get_uuid(), None)
1020 def get_host_ip_from_mac(self, mac):
1021 """Get the host IP address matching a MAC.
1023 mac: MAC address to look for
1024 return: the IP address of the host where the matching port runs or None if not found
1026 # _existing_ports is a dict of list of ports indexed by network id
1027 for port_list in self.get_existing_ports().values():
1028 for port in port_list:
1030 if port['mac_address'] == mac:
1031 host_id = port['binding:host_id']
1032 return self.comp.get_hypervisor(host_id).host_ip
1037 def get_chain_vlans(self, port_index):
1038 """Get the list of per chain VLAN id on a given port.
1040 port_index: left port is 0, right port is 1
1041 return: a VLAN ID list indexed by the chain index or None if no vlan tagging
1044 return [self.chains[chain_index].get_vlan(port_index)
1045 for chain_index in range(self.chain_count)]
1047 return self.vlans[port_index]
1049 def get_dest_macs(self, port_index):
1050 """Get the list of per chain dest MACs on a given port.
1052 Should not be called if EXT+ARP is used (in that case the traffic gen will
1053 have the ARP responses back from VNFs with the dest MAC to use).
1055 port_index: left port is 0, right port is 1
1056 return: a list of dest MACs indexed by the chain index
1058 if self.chains and self.config.service_chain != ChainType.EXT:
1059 return [self.chains[chain_index].get_dest_mac(port_index)
1060 for chain_index in range(self.chain_count)]
1061 # no openstack or EXT+no-arp
1062 return self.dest_macs[port_index]
1064 def get_host_ips(self):
1065 """Return the IP adresss(es) of the host compute nodes used for this run.
1067 :return: a list of 1 IP address
1069 # Since all chains go through the same compute node(s) we can just retrieve the
1070 # compute node(s) for the first chain
1072 if self.config.service_chain != ChainType.EXT:
1073 return self.chains[0].get_host_ips()
1074 # in the case of EXT, the compute node must be retrieved from the port
1075 # associated to any of the dest MACs
1076 dst_macs = self.chain_runner.traffic_client.gen.get_dest_macs()
1077 # dest MAC on port 0, chain 0
1078 dst_mac = dst_macs[0][0]
1079 host_ip = self.get_host_ip_from_mac(dst_mac)
1081 LOG.info('Found compute node IP for EXT chain: %s', host_ip)
1085 def get_compute_nodes(self):
1086 """Return the name of the host compute nodes used for this run.
1088 :return: a list of 0 or 1 host name in the az:host format
1090 # Since all chains go through the same compute node(s) we can just retrieve the
1091 # compute node name(s) for the first chain
1093 # in the case of EXT, the compute node must be retrieved from the port
1094 # associated to any of the dest MACs
1095 return self.chains[0].get_compute_nodes()
1096 # no openstack = no chains
1100 """Delete resources for all chains.
1102 Will not delete any resource if no-cleanup has been requested.
1104 if self.config.no_cleanup:
1106 for chain in self.chains:
1108 for network in self.networks:
1111 self.flavor.delete()