NFVBENCH-155 Add options to disable extra stats, latency stats and latency streams
[nfvbench.git] / nfvbench / chaining.py
1 #!/usr/bin/env python
2 # Copyright 2018 Cisco Systems, Inc.  All rights reserved.
3 #
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
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 #    under the License.
15 #
16
17 # This module takes care of chaining networks, ports and vms
18 #
19 """NFVBENCH CHAIN DISCOVERY/STAGING.
20
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.
25
26 ChainManager: manages VM image, flavor, the staging discovery of all chains
27               has 1 or more 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
32
33 ChainManager-->Chain(*)
34 Chain-->ChainNetwork(*),ChainVnf(*)
35 ChainVnf-->ChainVnfPort(2)
36
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
40 - chain type
41 - number of chains
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
45
46 There is not traffic generation involved in this module.
47 """
48 import os
49 import re
50 import time
51
52 import glanceclient
53 from neutronclient.neutron import client as neutronclient
54 from novaclient.client import Client
55
56 from attrdict import AttrDict
57 from chain_router import ChainRouter
58 import compute
59 from log import LOG
60 from specs import ChainType
61 # Left and right index for network and port lists
62 LEFT = 0
63 RIGHT = 1
64 # L3 traffic edge networks are at the end of networks list
65 EDGE_LEFT = -2
66 EDGE_RIGHT = -1
67 # Name of the VM config file
68 NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
69 # full pathame of the VM config in the VM
70 NFVBENCH_CFG_VM_PATHNAME = os.path.join('/etc/', NFVBENCH_CFG_FILENAME)
71 # full path of the boot shell script template file on the server where nfvbench runs
72 BOOT_SCRIPT_PATHNAME = os.path.join(os.path.dirname(os.path.abspath(__file__)),
73                                     'nfvbenchvm',
74                                     NFVBENCH_CFG_FILENAME)
75
76
77 class ChainException(Exception):
78     """Exception while operating the chains."""
79
80     pass
81
82
83 class NetworkEncaps(object):
84     """Network encapsulation."""
85
86
87 class ChainFlavor(object):
88     """Class to manage the chain flavor."""
89
90     def __init__(self, flavor_name, flavor_dict, comp):
91         """Create a flavor."""
92         self.name = flavor_name
93         self.comp = comp
94         self.flavor = self.comp.find_flavor(flavor_name)
95         self.reuse = False
96         if self.flavor:
97             self.reuse = True
98             LOG.info("Reused flavor '%s'", flavor_name)
99         else:
100             extra_specs = flavor_dict.pop('extra_specs', None)
101
102             self.flavor = comp.create_flavor(flavor_name,
103                                              **flavor_dict)
104
105             LOG.info("Created flavor '%s'", flavor_name)
106             if extra_specs:
107                 self.flavor.set_keys(extra_specs)
108
109     def delete(self):
110         """Delete this flavor."""
111         if not self.reuse and self.flavor:
112             self.flavor.delete()
113             LOG.info("Flavor '%s' deleted", self.name)
114
115
116 class ChainVnfPort(object):
117     """A port associated to one VNF in the chain."""
118
119     def __init__(self, name, vnf, chain_network, vnic_type):
120         """Create or reuse a port on a given network.
121
122         if vnf.instance is None the VNF instance is not reused and this ChainVnfPort instance must
123         create a new port.
124         Otherwise vnf.instance is a reused VNF instance and this ChainVnfPort instance must
125         find an existing port to reuse that matches the port requirements: same attached network,
126         instance, name, vnic type
127
128         name: name for this port
129         vnf: ChainVNf instance that owns this port
130         chain_network: ChainNetwork instance where this port should attach
131         vnic_type: required vnic type for this port
132         """
133         self.name = name
134         self.vnf = vnf
135         self.manager = vnf.manager
136         self.reuse = False
137         self.port = None
138         self.floating_ip = None
139         if vnf.instance:
140             # VNF instance is reused, we need to find an existing port that matches this instance
141             # and network
142             # discover ports attached to this instance
143             port_list = self.manager.get_ports_from_network(chain_network)
144             for port in port_list:
145                 if port['name'] != name:
146                     continue
147                 if port['binding:vnic_type'] != vnic_type:
148                     continue
149                 if port['device_id'] == vnf.get_uuid():
150                     self.port = port
151                     LOG.info('Reusing existing port %s mac=%s', name, port['mac_address'])
152                     break
153             else:
154                 raise ChainException('Cannot find matching port')
155         else:
156             # VNF instance is not created yet, we need to create a new port
157             body = {
158                 "port": {
159                     'name': name,
160                     'network_id': chain_network.get_uuid(),
161                     'binding:vnic_type': vnic_type
162                 }
163             }
164             port = self.manager.neutron_client.create_port(body)
165             self.port = port['port']
166             LOG.info('Created port %s', name)
167             try:
168                 self.manager.neutron_client.update_port(self.port['id'], {
169                     'port': {
170                         'security_groups': [],
171                         'port_security_enabled': False,
172                     }
173                 })
174                 LOG.info('Security disabled on port %s', name)
175             except Exception:
176                 LOG.info('Failed to disable security on port %s (ignored)', name)
177
178     def get_mac(self):
179         """Get the MAC address for this port."""
180         return self.port['mac_address']
181
182     def get_ip(self):
183         """Get the IP address for this port."""
184         return self.port['fixed_ips'][0]['ip_address']
185
186     def set_floating_ip(self, chain_network):
187         # create and add floating ip to port
188         try:
189             self.floating_ip = self.manager.neutron_client.create_floatingip({
190                 'floatingip': {
191                     'floating_network_id': chain_network.get_uuid(),
192                     'port_id': self.port['id'],
193                     'description': 'nfvbench floating ip for port:' + self.port['name'],
194                 }})['floatingip']
195             LOG.info('Floating IP %s created and associated on port %s',
196                      self.floating_ip['floating_ip_address'], self.name)
197             return self.floating_ip['floating_ip_address']
198         except Exception:
199             LOG.info('Failed to created and associated floating ip on port %s (ignored)', self.name)
200             return self.port['fixed_ips'][0]['ip_address']
201
202     def delete(self):
203         """Delete this port instance."""
204         if self.reuse or not self.port:
205             return
206         for _ in range(0, self.manager.config.generic_retry_count):
207             try:
208                 self.manager.neutron_client.delete_port(self.port['id'])
209                 LOG.info("Deleted port %s", self.name)
210                 if self.floating_ip:
211                     self.manager.neutron_client.delete_floatingip(self.floating_ip['id'])
212                     LOG.info("Deleted floating IP %s", self.floating_ip['description'])
213                 return
214             except Exception:
215                 time.sleep(self.manager.config.generic_poll_sec)
216         LOG.error('Unable to delete port: %s', self.name)
217
218
219 class ChainNetwork(object):
220     """Could be a shared network across all chains or a chain private network."""
221
222     def __init__(self, manager, network_config, chain_id=None, lookup_only=False,
223                  suffix=None):
224         """Create a network for given chain.
225
226         network_config: a dict containing the network properties
227                         (name, segmentation_id and physical_network)
228         chain_id: to which chain the networks belong.
229                   a None value will mean that these networks are shared by all chains
230         suffix: a suffix to add to the network name (if not None)
231         """
232         self.manager = manager
233         if chain_id is None:
234             self.name = network_config.name
235         else:
236             # the name itself can be either a string or a list of names indexed by chain ID
237             if isinstance(network_config.name, tuple):
238                 self.name = network_config.name[chain_id]
239             else:
240                 # network_config.name is a prefix string
241                 self.name = network_config.name + str(chain_id)
242         if suffix:
243             self.name = self.name + suffix
244         self.segmentation_id = self._get_item(network_config.segmentation_id,
245                                               chain_id, auto_index=True)
246         self.physical_network = self._get_item(network_config.physical_network, chain_id)
247
248         self.reuse = False
249         self.network = None
250         self.vlan = None
251         if manager.config.l3_router and hasattr(network_config, 'router_name'):
252             self.router_name = network_config.router_name
253         try:
254             self._setup(network_config, lookup_only)
255         except Exception:
256             if lookup_only:
257                 LOG.error("Cannot find network %s", self.name)
258             else:
259                 LOG.error("Error creating network %s", self.name)
260             self.delete()
261             raise
262
263     def _get_item(self, item_field, index, auto_index=False):
264         """Retrieve an item from a list or a single value.
265
266         item_field: can be None, a tuple of a single value
267         index: if None is same as 0, else is the index for a chain
268         auto_index: if true will automatically get the final value by adding the
269                     index to the base value (if full list not provided)
270
271         If the item_field is not a tuple, it is considered same as a tuple with same value at any
272         index.
273         If a list is provided, its length must be > index
274         """
275         if not item_field:
276             return None
277         if index is None:
278             index = 0
279         if isinstance(item_field, tuple):
280             try:
281                 return item_field[index]
282             except IndexError:
283                 raise ChainException("List %s is too short for chain index %d" %
284                                      (str(item_field), index))
285         # single value is configured
286         if auto_index:
287             return item_field + index
288         return item_field
289
290     def _setup(self, network_config, lookup_only):
291         # Lookup if there is a matching network with same name
292         networks = self.manager.neutron_client.list_networks(name=self.name)
293         if networks['networks']:
294             network = networks['networks'][0]
295             # a network of same name already exists, we need to verify it has the same
296             # characteristics
297             if self.segmentation_id:
298                 if network['provider:segmentation_id'] != self.segmentation_id:
299                     raise ChainException("Mismatch of 'segmentation_id' for reused "
300                                          "network '{net}'. Network has id '{seg_id1}', "
301                                          "configuration requires '{seg_id2}'."
302                                          .format(net=self.name,
303                                                  seg_id1=network['provider:segmentation_id'],
304                                                  seg_id2=self.segmentation_id))
305
306             if self.physical_network:
307                 if network['provider:physical_network'] != self.physical_network:
308                     raise ChainException("Mismatch of 'physical_network' for reused "
309                                          "network '{net}'. Network has '{phys1}', "
310                                          "configuration requires '{phys2}'."
311                                          .format(net=self.name,
312                                                  phys1=network['provider:physical_network'],
313                                                  phys2=self.physical_network))
314
315             LOG.info('Reusing existing network %s', self.name)
316             self.reuse = True
317             self.network = network
318         else:
319             if lookup_only:
320                 raise ChainException('Network %s not found' % self.name)
321             body = {
322                 'network': {
323                     'name': self.name,
324                     'admin_state_up': True
325                 }
326             }
327             if network_config.network_type:
328                 body['network']['provider:network_type'] = network_config.network_type
329             if self.segmentation_id:
330                 body['network']['provider:segmentation_id'] = self.segmentation_id
331             if self.physical_network:
332                 body['network']['provider:physical_network'] = self.physical_network
333             self.network = self.manager.neutron_client.create_network(body)['network']
334             # create associated subnet, all subnets have the same name (which is ok since
335             # we do not need to address them directly by name)
336             body = {
337                 'subnet': {'name': network_config.subnet,
338                            'cidr': network_config.cidr,
339                            'network_id': self.network['id'],
340                            'enable_dhcp': False,
341                            'ip_version': 4,
342                            'dns_nameservers': []}
343             }
344             subnet = self.manager.neutron_client.create_subnet(body)['subnet']
345             # add subnet id to the network dict since it has just been added
346             self.network['subnets'] = [subnet['id']]
347             LOG.info('Created network: %s', self.name)
348
349     def get_uuid(self):
350         """
351         Extract UUID of this network.
352
353         :return: UUID of this network
354         """
355         return self.network['id']
356
357     def get_vlan(self):
358         """
359         Extract vlan for this network.
360
361         :return: vlan ID for this network
362         """
363         if self.network['provider:network_type'] != 'vlan':
364             raise ChainException('Trying to retrieve VLAN id for non VLAN network')
365         return self.network['provider:segmentation_id']
366
367     def get_vxlan(self):
368         """
369         Extract VNI for this network.
370
371         :return: VNI ID for this network
372         """
373
374         return self.network['provider:segmentation_id']
375
376     def delete(self):
377         """Delete this network."""
378         if not self.reuse and self.network:
379             for retry in range(0, self.manager.config.generic_retry_count):
380                 try:
381                     self.manager.neutron_client.delete_network(self.network['id'])
382                     LOG.info("Deleted network: %s", self.name)
383                     return
384                 except Exception:
385                     LOG.info('Error deleting network %s (retry %d/%d)...',
386                              self.name,
387                              retry + 1,
388                              self.manager.config.generic_retry_count)
389                     time.sleep(self.manager.config.generic_poll_sec)
390             LOG.error('Unable to delete network: %s', self.name)
391
392
393 class ChainVnf(object):
394     """A class to represent a VNF in a chain."""
395
396     def __init__(self, chain, vnf_id, networks):
397         """Reuse a VNF instance with same characteristics or create a new VNF instance.
398
399         chain: the chain where this vnf belongs
400         vnf_id: indicates the index of this vnf in its chain (first vnf=0)
401         networks: the list of all networks (ChainNetwork) of the current chain
402         """
403         self.manager = chain.manager
404         self.chain = chain
405         self.vnf_id = vnf_id
406         self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
407         if len(networks) > 2:
408             # we will have more than 1 VM in each chain
409             self.name += '-' + str(vnf_id)
410         # A list of ports for this chain
411         # There are normally 2 ports carrying traffic (index 0, and index 1) and
412         # potentially multiple idle ports not carrying traffic (index 2 and up)
413         # For example if 7 idle interfaces are requested, the corresp. ports will be
414         # at index 2 to 8
415         self.ports = []
416         self.management_port = None
417         self.routers = []
418         self.status = None
419         self.instance = None
420         self.reuse = False
421         self.host_ip = None
422         self.idle_networks = []
423         self.idle_ports = []
424         try:
425             # the vnf_id is conveniently also the starting index in networks
426             # for the left and right networks associated to this VNF
427             if self.manager.config.l3_router:
428                 self._setup(networks[vnf_id:vnf_id + 4])
429             else:
430                 self._setup(networks[vnf_id:vnf_id + 2])
431         except Exception:
432             LOG.error("Error creating VNF %s", self.name)
433             self.delete()
434             raise
435
436     def _get_vm_config(self, remote_mac_pair):
437         config = self.manager.config
438         devices = self.manager.generator_config.devices
439
440         if config.l3_router:
441             tg_gateway1_ip = self.routers[LEFT].ports[1]['fixed_ips'][0][
442                 'ip_address']  # router edge ip left
443             tg_gateway2_ip = self.routers[RIGHT].ports[1]['fixed_ips'][0][
444                 'ip_address']  # router edge ip right
445             tg_mac1 = self.routers[LEFT].ports[1]['mac_address']  # router edge mac left
446             tg_mac2 = self.routers[RIGHT].ports[1]['mac_address']  # router edge mac right
447             # edge cidr mask left
448             vnf_gateway1_cidr = \
449                 self.ports[LEFT].get_ip() + self.__get_network_mask(
450                     self.manager.config.edge_networks.left.cidr)
451             # edge cidr mask right
452             vnf_gateway2_cidr = \
453                 self.ports[RIGHT].get_ip() + self.__get_network_mask(
454                     self.manager.config.edge_networks.right.cidr)
455             if config.vm_forwarder != 'vpp':
456                 raise ChainException(
457                     'L3 router mode imply to set VPP as VM forwarder.'
458                     'Please update your config file with: vm_forwarder: vpp')
459         else:
460             tg_gateway1_ip = devices[LEFT].tg_gateway_ip_addrs
461             tg_gateway2_ip = devices[RIGHT].tg_gateway_ip_addrs
462             tg_mac1 = remote_mac_pair[0]
463             tg_mac2 = remote_mac_pair[1]
464
465             g1cidr = devices[LEFT].get_gw_ip(
466                 self.chain.chain_id) + self.__get_network_mask(
467                     self.manager.config.internal_networks.left.cidr)
468             g2cidr = devices[RIGHT].get_gw_ip(
469                 self.chain.chain_id) + self.__get_network_mask(
470                     self.manager.config.internal_networks.right.cidr)
471
472             vnf_gateway1_cidr = g1cidr
473             vnf_gateway2_cidr = g2cidr
474
475         with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
476             content = boot_script.read()
477         vm_config = {
478             'forwarder': config.vm_forwarder,
479             'intf_mac1': self.ports[LEFT].get_mac(),
480             'intf_mac2': self.ports[RIGHT].get_mac(),
481             'tg_gateway1_ip': tg_gateway1_ip,
482             'tg_gateway2_ip': tg_gateway2_ip,
483             'tg_net1': devices[LEFT].ip_addrs,
484             'tg_net2': devices[RIGHT].ip_addrs,
485             'vnf_gateway1_cidr': vnf_gateway1_cidr,
486             'vnf_gateway2_cidr': vnf_gateway2_cidr,
487             'tg_mac1': tg_mac1,
488             'tg_mac2': tg_mac2,
489             'vif_mq_size': config.vif_multiqueue_size,
490             'num_mbufs': config.num_mbufs
491         }
492         if self.manager.config.use_management_port:
493             mgmt_ip = self.management_port.port['fixed_ips'][0]['ip_address']
494             mgmt_mask = self.__get_network_mask(self.manager.config.management_network.cidr)
495             vm_config['intf_mgmt_cidr'] = mgmt_ip + mgmt_mask
496             vm_config['intf_mgmt_ip_gw'] = self.manager.config.management_network.gateway
497             vm_config['intf_mac_mgmt'] = self.management_port.port['mac_address']
498         else:
499             # Interface management config left empty to avoid error in VM spawn
500             # if nfvbench config has values for management network but use_management_port=false
501             vm_config['intf_mgmt_cidr'] = ''
502             vm_config['intf_mgmt_ip_gw'] = ''
503             vm_config['intf_mac_mgmt'] = ''
504         return content.format(**vm_config)
505
506     @staticmethod
507     def __get_network_mask(network):
508         return '/' + network.split('/')[1]
509
510     def _get_vnic_type(self, port_index):
511         """Get the right vnic type for given port indexself.
512
513         If SR-IOV is specified, middle ports in multi-VNF chains
514         can use vswitch or SR-IOV based on config.use_sriov_middle_net
515         """
516         if self.manager.config.sriov:
517             chain_length = self.chain.get_length()
518             if self.manager.config.use_sriov_middle_net or chain_length == 1:
519                 return 'direct'
520             if self.vnf_id == 0 and port_index == 0:
521                 # first VNF in chain must use sriov for left port
522                 return 'direct'
523             if (self.vnf_id == chain_length - 1) and (port_index == 1):
524                 # last VNF in chain must use sriov for right port
525                 return 'direct'
526         return 'normal'
527
528     def _get_idle_networks_ports(self):
529         """Get the idle networks for PVP or PVVP chain (non shared net only)
530
531         For EXT packet path or shared net, returns empty list.
532         For PVP, PVVP these networks will be created if they do not exist.
533         chain_id: to which chain the networks belong.
534                 a None value will mean that these networks are shared by all chains
535         """
536         networks = []
537         ports = []
538         config = self.manager.config
539         chain_id = self.chain.chain_id
540         idle_interfaces_per_vm = config.idle_interfaces_per_vm
541         if config.service_chain == ChainType.EXT or chain_id is None or \
542            idle_interfaces_per_vm == 0:
543             return
544
545         # Make a copy of the idle networks dict as we may have to modify the
546         # segmentation ID
547         idle_network_cfg = AttrDict(config.idle_networks)
548         if idle_network_cfg.segmentation_id:
549             segmentation_id = idle_network_cfg.segmentation_id + \
550                 chain_id * idle_interfaces_per_vm
551         else:
552             segmentation_id = None
553         try:
554             # create as many idle networks and ports as requested
555             for idle_index in range(idle_interfaces_per_vm):
556                 if config.service_chain == ChainType.PVP:
557                     suffix = '.%d' % (idle_index)
558                 else:
559                     suffix = '.%d.%d' % (self.vnf_id, idle_index)
560                 port_name = self.name + '-idle' + str(idle_index)
561                 # update the segmentation id based on chain id and idle index
562                 if segmentation_id:
563                     idle_network_cfg.segmentation_id = segmentation_id + idle_index
564                     port_name = port_name + "." + str(segmentation_id)
565
566                 networks.append(ChainNetwork(self.manager,
567                                              idle_network_cfg,
568                                              chain_id,
569                                              suffix=suffix))
570                 ports.append(ChainVnfPort(port_name,
571                                           self,
572                                           networks[idle_index],
573                                           'normal'))
574         except Exception:
575             # need to cleanup all successful networks
576             for net in networks:
577                 net.delete()
578             for port in ports:
579                 port.delete()
580             raise
581         self.idle_networks = networks
582         self.idle_ports = ports
583
584     def _setup(self, networks):
585         flavor_id = self.manager.flavor.flavor.id
586         # Check if we can reuse an instance with same name
587         for instance in self.manager.existing_instances:
588             if instance.name == self.name:
589                 instance_left = LEFT
590                 instance_right = RIGHT
591                 # In case of L3 traffic instance use edge networks
592                 if self.manager.config.l3_router:
593                     instance_left = EDGE_LEFT
594                     instance_right = EDGE_RIGHT
595                 # Verify that other instance characteristics match
596                 if instance.flavor['id'] != flavor_id:
597                     self._reuse_exception('Flavor mismatch')
598                 if instance.status != "ACTIVE":
599                     self._reuse_exception('Matching instance is not in ACTIVE state')
600                 # The 2 networks for this instance must also be reused
601                 if not networks[instance_left].reuse:
602                     self._reuse_exception('network %s is new' % networks[instance_left].name)
603                 if not networks[instance_right].reuse:
604                     self._reuse_exception('network %s is new' % networks[instance_right].name)
605                 # instance.networks have the network names as keys:
606                 # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
607                 if networks[instance_left].name not in instance.networks:
608                     self._reuse_exception('Left network mismatch')
609                 if networks[instance_right].name not in instance.networks:
610                     self._reuse_exception('Right network mismatch')
611
612                 self.reuse = True
613                 self.instance = instance
614                 LOG.info('Reusing existing instance %s on %s',
615                          self.name, self.get_hypervisor_name())
616         # create management port if needed
617         if self.manager.config.use_management_port:
618             self.management_port = ChainVnfPort(self.name + '-mgmt', self,
619                                                 self.manager.management_network, 'normal')
620             ip = self.management_port.port['fixed_ips'][0]['ip_address']
621             if self.manager.config.use_floating_ip:
622                 ip = self.management_port.set_floating_ip(self.manager.floating_ip_network)
623             LOG.info("Management interface will be active using IP: %s, "
624                      "and you can connect over SSH with login: nfvbench and password: nfvbench", ip)
625         # create or reuse/discover 2 ports per instance
626         if self.manager.config.l3_router:
627             for index in [0, 1]:
628                 self.ports.append(ChainVnfPort(self.name + '-' + str(index),
629                                                self,
630                                                networks[index + 2],
631                                                self._get_vnic_type(index)))
632         else:
633             for index in [0, 1]:
634                 self.ports.append(ChainVnfPort(self.name + '-' + str(index),
635                                                self,
636                                                networks[index],
637                                                self._get_vnic_type(index)))
638
639         # create idle networks and ports only if instance is not reused
640         # if reused, we do not care about idle networks/ports
641         if not self.reuse:
642             self._get_idle_networks_ports()
643
644         # Create neutron routers for L3 traffic use case
645         if self.manager.config.l3_router and self.manager.openstack:
646             internal_nets = networks[:2]
647             if self.manager.config.service_chain == ChainType.PVP:
648                 edge_nets = networks[2:]
649             else:
650                 edge_nets = networks[3:]
651             subnets_left = [internal_nets[0], edge_nets[0]]
652             routes_left = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
653                             'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
654                                 0]},
655                            {'destination': self.manager.config.traffic_generator.ip_addrs[1],
656                             'nexthop': self.ports[0].get_ip()}]
657             self.routers.append(
658                 ChainRouter(self.manager, edge_nets[0].router_name, subnets_left, routes_left))
659             subnets_right = [internal_nets[1], edge_nets[1]]
660             routes_right = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
661                              'nexthop': self.ports[1].get_ip()},
662                             {'destination': self.manager.config.traffic_generator.ip_addrs[1],
663                              'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
664                                  1]}]
665             self.routers.append(
666                 ChainRouter(self.manager, edge_nets[1].router_name, subnets_right, routes_right))
667             # Overload gateway_ips property with router ip address for ARP and traffic calls
668             self.manager.generator_config.devices[LEFT].set_gw_ip(
669                 self.routers[LEFT].ports[0]['fixed_ips'][0]['ip_address'])  # router edge ip left)
670             self.manager.generator_config.devices[RIGHT].set_gw_ip(
671                 self.routers[RIGHT].ports[0]['fixed_ips'][0]['ip_address'])  # router edge ip right)
672
673         # if no reuse, actual vm creation is deferred after all ports in the chain are created
674         # since we need to know the next mac in a multi-vnf chain
675
676     def create_vnf(self, remote_mac_pair):
677         """Create the VNF instance if it does not already exist."""
678         if self.instance is None:
679             port_ids = []
680             if self.manager.config.use_management_port:
681                 port_ids.append({'port-id': self.management_port.port['id']})
682             port_ids.extend([{'port-id': vnf_port.port['id']} for vnf_port in self.ports])
683             # add idle ports
684             for idle_port in self.idle_ports:
685                 port_ids.append({'port-id': idle_port.port['id']})
686             vm_config = self._get_vm_config(remote_mac_pair)
687             az = self.manager.placer.get_required_az()
688             server = self.manager.comp.create_server(self.name,
689                                                      self.manager.image_instance,
690                                                      self.manager.flavor.flavor,
691                                                      None,
692                                                      port_ids,
693                                                      None,
694                                                      avail_zone=az,
695                                                      user_data=None,
696                                                      config_drive=True,
697                                                      files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
698             if server:
699                 self.instance = server
700                 if self.manager.placer.is_resolved():
701                     LOG.info('Created instance %s on %s', self.name, az)
702                 else:
703                     # the location is undetermined at this point
704                     # self.get_hypervisor_name() will return None
705                     LOG.info('Created instance %s - waiting for placement resolution...', self.name)
706                     # here we MUST wait until this instance is resolved otherwise subsequent
707                     # VNF creation can be placed in other hypervisors!
708                     config = self.manager.config
709                     max_retries = (config.check_traffic_time_sec +
710                                    config.generic_poll_sec - 1) / config.generic_poll_sec
711                     retry = 0
712                     for retry in range(max_retries):
713                         status = self.get_status()
714                         if status == 'ACTIVE':
715                             hyp_name = self.get_hypervisor_name()
716                             LOG.info('Instance %s is active and has been placed on %s',
717                                      self.name, hyp_name)
718                             self.manager.placer.register_full_name(hyp_name)
719                             break
720                         if status == 'ERROR':
721                             raise ChainException('Instance %s creation error: %s' %
722                                                  (self.name,
723                                                   self.instance.fault['message']))
724                         LOG.info('Waiting for instance %s to become active (retry %d/%d)...',
725                                  self.name, retry + 1, max_retries + 1)
726                         time.sleep(config.generic_poll_sec)
727                     else:
728                         # timing out
729                         LOG.error('Instance %s creation timed out', self.name)
730                         raise ChainException('Instance %s creation timed out' % self.name)
731                 self.reuse = False
732             else:
733                 raise ChainException('Unable to create instance: %s' % (self.name))
734
735     def _reuse_exception(self, reason):
736         raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
737
738     def get_status(self):
739         """Get the statis of this instance."""
740         if self.instance.status != 'ACTIVE':
741             self.instance = self.manager.comp.poll_server(self.instance)
742         return self.instance.status
743
744     def get_hostname(self):
745         """Get the hypervisor host name running this VNF instance."""
746         if self.manager.is_admin:
747             hypervisor_hostname = getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
748         else:
749             hypervisor_hostname = self.manager.config.hypervisor_hostname
750             if not hypervisor_hostname:
751                 raise ChainException('Hypervisor hostname parameter is mandatory')
752         return hypervisor_hostname
753
754     def get_host_ip(self):
755         """Get the IP address of the host where this instance runs.
756
757         return: the IP address
758         """
759         if not self.host_ip:
760             self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
761         return self.host_ip
762
763     def get_hypervisor_name(self):
764         """Get hypervisor name (az:hostname) for this VNF instance."""
765         if self.instance:
766             if self.manager.is_admin:
767                 az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
768             else:
769                 az = self.manager.config.availability_zone
770             if not az:
771                 raise ChainException('Availability zone parameter is mandatory')
772             hostname = self.get_hostname()
773             if az:
774                 return az + ':' + hostname
775             return hostname
776         return None
777
778     def get_uuid(self):
779         """Get the uuid for this instance."""
780         return self.instance.id
781
782     def delete(self, forced=False):
783         """Delete this VNF instance."""
784         if self.reuse:
785             LOG.info("Instance %s not deleted (reused)", self.name)
786         else:
787             if self.instance:
788                 self.manager.comp.delete_server(self.instance)
789                 LOG.info("Deleted instance %s", self.name)
790             if self.manager.config.use_management_port:
791                 self.management_port.delete()
792             for port in self.ports:
793                 port.delete()
794             for port in self.idle_ports:
795                 port.delete()
796             for network in self.idle_networks:
797                 network.delete()
798
799
800 class Chain(object):
801     """A class to manage a single chain.
802
803     Can handle any type of chain (EXT, PVP, PVVP)
804     """
805
806     def __init__(self, chain_id, manager):
807         """Create a new chain.
808
809         chain_id: chain index (first chain is 0)
810         manager: the chain manager that owns all chains
811         """
812         self.chain_id = chain_id
813         self.manager = manager
814         self.encaps = manager.encaps
815         self.networks = []
816         self.instances = []
817         try:
818             self.networks = manager.get_networks(chain_id)
819             # For external chain VNFs can only be discovered from their MAC addresses
820             # either from config or from ARP
821             if manager.config.service_chain != ChainType.EXT:
822                 for chain_instance_index in range(self.get_length()):
823                     self.instances.append(ChainVnf(self,
824                                                    chain_instance_index,
825                                                    self.networks))
826                 # at this point new VNFs are not created yet but
827                 # verify that all discovered VNFs are on the same hypervisor
828                 self._check_hypervisors()
829                 # now that all VNF ports are created we need to calculate the
830                 # left/right remote MAC for each VNF in the chain
831                 # before actually creating the VNF itself
832                 rem_mac_pairs = self._get_remote_mac_pairs()
833                 for instance in self.instances:
834                     rem_mac_pair = rem_mac_pairs.pop(0)
835                     instance.create_vnf(rem_mac_pair)
836         except Exception:
837             self.delete()
838             raise
839
840     def _check_hypervisors(self):
841         common_hypervisor = None
842         for instance in self.instances:
843             # get the full hypervizor name (az:compute)
844             hname = instance.get_hypervisor_name()
845             if hname:
846                 if common_hypervisor:
847                     if hname != common_hypervisor:
848                         raise ChainException('Discovered instances on different hypervisors:'
849                                              ' %s and %s' % (hname, common_hypervisor))
850                 else:
851                     common_hypervisor = hname
852         if common_hypervisor:
853             # check that the common hypervisor name matchs the requested hypervisor name
854             # and set the name to be used by all future instances (if any)
855             if not self.manager.placer.register_full_name(common_hypervisor):
856                 raise ChainException('Discovered hypervisor placement %s is incompatible' %
857                                      common_hypervisor)
858
859     def get_length(self):
860         """Get the number of VNF in the chain."""
861         # Take into account 2 edge networks for routers
862         return len(self.networks) - 3 if self.manager.config.l3_router else len(self.networks) - 1
863
864     def _get_remote_mac_pairs(self):
865         """Get the list of remote mac pairs for every VNF in the chain.
866
867         Traverse the chain from left to right and establish the
868         left/right remote MAC for each VNF in the chainself.
869
870         PVP case is simpler:
871         mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
872         must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
873         the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
874
875         PVVP:
876         tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
877         Must produce the following list:
878         [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
879
880         General case with 3 VMs in chain, the list of consecutive macs (left to right):
881         tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
882         Must produce the following list:
883         [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
884         or index: [[0, 3], [2, 5], [4, 7]]
885
886         The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
887         """
888         # line up all mac from left to right
889         mac_seq = [self.manager.generator_config.devices[LEFT].mac]
890         for instance in self.instances:
891             mac_seq.append(instance.ports[0].get_mac())
892             mac_seq.append(instance.ports[1].get_mac())
893         mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
894         base = 0
895         rem_mac_pairs = []
896         for _ in self.instances:
897             rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
898             base += 2
899         return rem_mac_pairs
900
901     def get_instances(self):
902         """Return all instances for this chain."""
903         return self.instances
904
905     def get_vlan(self, port_index):
906         """Get the VLAN id on a given port.
907
908         port_index: left port is 0, right port is 1
909         return: the vlan_id or None if there is no vlan tagging
910         """
911         # for port 1 we need to return the VLAN of the last network in the chain
912         # The networks array contains 2 networks for PVP [left, right]
913         # and 3 networks in the case of PVVP [left.middle,right]
914         if port_index:
915             # this will pick the last item in array
916             port_index = -1
917         return self.networks[port_index].get_vlan()
918
919     def get_vxlan(self, port_index):
920         """Get the VXLAN id on a given port.
921
922         port_index: left port is 0, right port is 1
923         return: the vxlan_id or None if there is no vxlan
924         """
925         # for port 1 we need to return the VLAN of the last network in the chain
926         # The networks array contains 2 networks for PVP [left, right]
927         # and 3 networks in the case of PVVP [left.middle,right]
928         if port_index:
929             # this will pick the last item in array
930             port_index = -1
931         return self.networks[port_index].get_vxlan()
932
933     def get_dest_mac(self, port_index):
934         """Get the dest MAC on a given port.
935
936         port_index: left port is 0, right port is 1
937         return: the dest MAC
938         """
939         if port_index:
940             # for right port, use the right port MAC of the last (right most) VNF In chain
941             return self.instances[-1].ports[1].get_mac()
942         # for left port use the left port MAC of the first (left most) VNF in chain
943         return self.instances[0].ports[0].get_mac()
944
945     def get_network_uuids(self):
946         """Get UUID of networks in this chain from left to right (order is important).
947
948         :return: list of UUIDs of networks (2 or 3 elements)
949         """
950         return [net['id'] for net in self.networks]
951
952     def get_host_ips(self):
953         """Return the IP adresss(es) of the host compute nodes used for this chain.
954
955         :return: a list of 1 or 2 IP addresses
956         """
957         return [vnf.get_host_ip() for vnf in self.instances]
958
959     def get_compute_nodes(self):
960         """Return the name of the host compute nodes used for this chain.
961
962         :return: a list of 1 host name in the az:host format
963         """
964         # Since all chains go through the same compute node(s) we can just retrieve the
965         # compute node name(s) for the first chain
966         return [vnf.get_hypervisor_name() for vnf in self.instances]
967
968     def delete(self):
969         """Delete this chain."""
970         for instance in self.instances:
971             instance.delete()
972         # only delete if these are chain private networks (not shared)
973         if not self.manager.config.service_chain_shared_net:
974             for network in self.networks:
975                 network.delete()
976
977
978 class InstancePlacer(object):
979     """A class to manage instance placement for all VNFs in all chains.
980
981     A full az string is made of 2 parts AZ and hypervisor.
982     The placement is resolved when both parts az and hypervisor names are known.
983     """
984
985     def __init__(self, req_az, req_hyp):
986         """Create a new instance placer.
987
988         req_az: requested AZ (can be None or empty if no preference)
989         req_hyp: requested hypervisor name (can be None of empty if no preference)
990                  can be any of 'nova:', 'comp1', 'nova:comp1'
991                  if it is a list, only the first item is used (backward compatibility in config)
992
993         req_az is ignored if req_hyp has an az part
994         all other parts beyond the first 2 are ignored in req_hyp
995         """
996         # if passed a list just pick the first item
997         if req_hyp and isinstance(req_hyp, list):
998             req_hyp = req_hyp[0]
999         # only pick first part of az
1000         if req_az and ':' in req_az:
1001             req_az = req_az.split(':')[0]
1002         if req_hyp:
1003             # check if requested hypervisor string has an AZ part
1004             split_hyp = req_hyp.split(':')
1005             if len(split_hyp) > 1:
1006                 # override the AZ part and hypervisor part
1007                 req_az = split_hyp[0]
1008                 req_hyp = split_hyp[1]
1009         self.requested_az = req_az if req_az else ''
1010         self.requested_hyp = req_hyp if req_hyp else ''
1011         # Nova can accept AZ only (e.g. 'nova:', use any hypervisor in that AZ)
1012         # or hypervisor only (e.g. ':comp1')
1013         # or both (e.g. 'nova:comp1')
1014         if req_az:
1015             self.required_az = req_az + ':' + self.requested_hyp
1016         else:
1017             # need to insert a ':' so nova knows this is the hypervisor name
1018             self.required_az = ':' + self.requested_hyp if req_hyp else ''
1019         # placement is resolved when both AZ and hypervisor names are known and set
1020         self.resolved = self.requested_az != '' and self.requested_hyp != ''
1021
1022     def get_required_az(self):
1023         """Return the required az (can be resolved or not)."""
1024         return self.required_az
1025
1026     def register_full_name(self, discovered_az):
1027         """Verify compatibility and register a discovered hypervisor full name.
1028
1029         discovered_az: a discovered AZ in az:hypervisor format
1030         return: True if discovered_az is compatible and set
1031                 False if discovered_az is not compatible
1032         """
1033         if self.resolved:
1034             return discovered_az == self.required_az
1035
1036         # must be in full az format
1037         split_daz = discovered_az.split(':')
1038         if len(split_daz) != 2:
1039             return False
1040         if self.requested_az and self.requested_az != split_daz[0]:
1041             return False
1042         if self.requested_hyp and self.requested_hyp != split_daz[1]:
1043             return False
1044         self.required_az = discovered_az
1045         self.resolved = True
1046         return True
1047
1048     def is_resolved(self):
1049         """Check if the full AZ is resolved.
1050
1051         return: True if resolved
1052         """
1053         return self.resolved
1054
1055
1056 class ChainManager(object):
1057     """A class for managing all chains for a given run.
1058
1059     Supports openstack or no openstack.
1060     Supports EXT, PVP and PVVP chains.
1061     """
1062
1063     def __init__(self, chain_runner):
1064         """Create a chain manager to take care of discovering or bringing up the requested chains.
1065
1066         A new instance must be created every time a new config is used.
1067         config: the nfvbench config to use
1068         cred: openstack credentials to use of None if there is no openstack
1069         """
1070         self.chain_runner = chain_runner
1071         self.config = chain_runner.config
1072         self.generator_config = chain_runner.traffic_client.generator_config
1073         self.chains = []
1074         self.image_instance = None
1075         self.image_name = None
1076         # Left and right networks shared across all chains (only if shared)
1077         self.networks = []
1078         self.encaps = None
1079         self.flavor = None
1080         self.comp = None
1081         self.nova_client = None
1082         self.neutron_client = None
1083         self.glance_client = None
1084         self.existing_instances = []
1085         # existing ports keyed by the network uuid they belong to
1086         self._existing_ports = {}
1087         config = self.config
1088         self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
1089         self.chain_count = config.service_chain_count
1090         self.az = None
1091         if self.openstack:
1092             # openstack only
1093             session = chain_runner.cred.get_session()
1094             self.is_admin = chain_runner.cred.is_admin
1095             self.nova_client = Client(2, session=session)
1096             self.neutron_client = neutronclient.Client('2.0', session=session)
1097             self.glance_client = glanceclient.Client('2', session=session)
1098             self.comp = compute.Compute(self.nova_client,
1099                                         self.glance_client,
1100                                         config)
1101             try:
1102                 if config.service_chain != ChainType.EXT:
1103                     self.placer = InstancePlacer(config.availability_zone, config.compute_nodes)
1104                     self._setup_image()
1105                     self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
1106                     # Get list of all existing instances to check if some instances can be reused
1107                     self.existing_instances = self.comp.get_server_list()
1108                     # If management port is requested for VMs, create management network (shared)
1109                     if self.config.use_management_port:
1110                         self.management_network = ChainNetwork(self, self.config.management_network,
1111                                                                None, False)
1112                         # If floating IP is used for management, create and share
1113                         # across chains the floating network
1114                         if self.config.use_floating_ip:
1115                             self.floating_ip_network = ChainNetwork(self,
1116                                                                     self.config.floating_network,
1117                                                                     None, False)
1118                 else:
1119                     # For EXT chains, the external_networks left and right fields in the config
1120                     # must be either a prefix string or a list of at least chain-count strings
1121                     self._check_extnet('left', config.external_networks.left)
1122                     self._check_extnet('right', config.external_networks.right)
1123
1124                 # If networks are shared across chains, get the list of networks
1125                 if config.service_chain_shared_net:
1126                     self.networks = self.get_networks()
1127                 # Reuse/create chains
1128                 for chain_id in range(self.chain_count):
1129                     self.chains.append(Chain(chain_id, self))
1130                 if config.service_chain == ChainType.EXT:
1131                     # if EXT and no ARP or VxLAN we need to read dest MACs from config
1132                     if config.no_arp or config.vxlan:
1133                         self._get_dest_macs_from_config()
1134                 else:
1135                     # Make sure all instances are active before proceeding
1136                     self._ensure_instances_active()
1137                 # network API call do not show VLANS ID if not admin read from config
1138                 if not self.is_admin and config.vlan_tagging:
1139                     self._get_config_vlans()
1140             except Exception:
1141                 self.delete()
1142                 raise
1143         else:
1144             # no openstack, no need to create chains
1145             if not config.l2_loopback and config.no_arp:
1146                 self._get_dest_macs_from_config()
1147             if config.vlan_tagging:
1148                 # make sure there at least as many entries as chains in each left/right list
1149                 if len(config.vlans) != 2:
1150                     raise ChainException('The config vlans property must be a list '
1151                                          'with 2 lists of VLAN IDs')
1152                 self._get_config_vlans()
1153             if config.vxlan:
1154                 raise ChainException('VxLAN is only supported with OpenStack')
1155
1156     def _check_extnet(self, side, name):
1157         if not name:
1158             raise ChainException('external_networks.%s must contain a valid network'
1159                                  ' name prefix or a list of network names' % side)
1160         if isinstance(name, tuple) and len(name) < self.chain_count:
1161             raise ChainException('external_networks.%s %s'
1162                                  ' must have at least %d names' % (side, name, self.chain_count))
1163
1164     def _get_config_vlans(self):
1165         re_vlan = "[0-9]*$"
1166         try:
1167             self.vlans = [self._check_list('vlans[0]', self.config.vlans[0], re_vlan),
1168                           self._check_list('vlans[1]', self.config.vlans[1], re_vlan)]
1169         except IndexError:
1170             raise ChainException('vlans parameter is mandatory. Set valid value in config file')
1171
1172     def _get_dest_macs_from_config(self):
1173         re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
1174         tg_config = self.config.traffic_generator
1175         self.dest_macs = [self._check_list("mac_addrs_left",
1176                                            tg_config.mac_addrs_left, re_mac),
1177                           self._check_list("mac_addrs_right",
1178                                            tg_config.mac_addrs_right, re_mac)]
1179
1180     def _check_list(self, list_name, ll, pattern):
1181         # if it is a single int or mac, make it a list of 1 int
1182         if isinstance(ll, (int, str)):
1183             ll = [ll]
1184         for item in ll:
1185             if not re.match(pattern, str(item)):
1186                 raise ChainException("Invalid format '{item}' specified in {fname}"
1187                                      .format(item=item, fname=list_name))
1188         # must have at least 1 element
1189         if not ll:
1190             raise ChainException('%s cannot be empty' % (list_name))
1191         # for shared network, if 1 element is passed, replicate it as many times
1192         # as chains
1193         if self.config.service_chain_shared_net and len(ll) == 1:
1194             ll = [ll[0]] * self.chain_count
1195
1196         # number of elements musty be the number of chains
1197         elif len(ll) < self.chain_count:
1198             raise ChainException('%s=%s must be a list with %d elements per chain' %
1199                                  (list_name, ll, self.chain_count))
1200         return ll
1201
1202     def _setup_image(self):
1203         # To avoid reuploading image in server mode, check whether image_name is set or not
1204         if self.image_name:
1205             self.image_instance = self.comp.find_image(self.image_name)
1206         if self.image_instance:
1207             LOG.info("Reusing image %s", self.image_name)
1208         else:
1209             image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
1210             if self.config.vm_image_file:
1211                 match = re.search(image_name_search_pattern, self.config.vm_image_file)
1212                 if match:
1213                     self.image_name = match.group(1)
1214                     LOG.info('Using provided VM image file %s', self.config.vm_image_file)
1215                 else:
1216                     raise ChainException('Provided VM image file name %s must start with '
1217                                          '"nfvbenchvm-<version>"' % self.config.vm_image_file)
1218             else:
1219                 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1220                 for f in os.listdir(pkg_root):
1221                     if re.search(image_name_search_pattern, f):
1222                         self.config.vm_image_file = pkg_root + '/' + f
1223                         self.image_name = f.replace('.qcow2', '')
1224                         LOG.info('Found built-in VM image file %s', f)
1225                         break
1226                 else:
1227                     raise ChainException('Cannot find any built-in VM image file.')
1228             if self.image_name:
1229                 self.image_instance = self.comp.find_image(self.image_name)
1230             if not self.image_instance:
1231                 LOG.info('Uploading %s', self.image_name)
1232                 res = self.comp.upload_image_via_url(self.image_name,
1233                                                      self.config.vm_image_file)
1234
1235                 if not res:
1236                     raise ChainException('Error uploading image %s from %s. ABORTING.' %
1237                                          (self.image_name, self.config.vm_image_file))
1238                 LOG.info('Image %s successfully uploaded.', self.image_name)
1239                 self.image_instance = self.comp.find_image(self.image_name)
1240
1241         # image multiqueue property must be set according to the vif_multiqueue_size
1242         # config value (defaults to 1 or disabled)
1243         self.comp.image_set_multiqueue(self.image_instance, self.config.vif_multiqueue_size > 1)
1244
1245     def _ensure_instances_active(self):
1246         instances = []
1247         for chain in self.chains:
1248             instances.extend(chain.get_instances())
1249         initial_instance_count = len(instances)
1250         # Give additional 10 seconds per VM
1251         max_retries = (self.config.check_traffic_time_sec + (initial_instance_count - 1) * 10 +
1252                        self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
1253         retry = 0
1254         while instances:
1255             remaining_instances = []
1256             for instance in instances:
1257                 status = instance.get_status()
1258                 if status == 'ACTIVE':
1259                     LOG.info('Instance %s is ACTIVE on %s',
1260                              instance.name, instance.get_hypervisor_name())
1261                     continue
1262                 if status == 'ERROR':
1263                     raise ChainException('Instance %s creation error: %s' %
1264                                          (instance.name,
1265                                           instance.instance.fault['message']))
1266                 remaining_instances.append(instance)
1267             if not remaining_instances:
1268                 break
1269             retry += 1
1270             if retry >= max_retries:
1271                 raise ChainException('Time-out: %d/%d instances still not active' %
1272                                      (len(remaining_instances), initial_instance_count))
1273             LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
1274                      len(remaining_instances), initial_instance_count,
1275                      retry, max_retries)
1276             instances = remaining_instances
1277             time.sleep(self.config.generic_poll_sec)
1278         if initial_instance_count:
1279             LOG.info('All instances are active')
1280
1281     def get_networks(self, chain_id=None):
1282         """Get the networks for given EXT, PVP or PVVP chain.
1283
1284         For EXT packet path, these networks must pre-exist.
1285         For PVP, PVVP these networks will be created if they do not exist.
1286         chain_id: to which chain the networks belong.
1287                   a None value will mean that these networks are shared by all chains
1288         """
1289         if self.networks:
1290             # the only case where self.networks exists is when the networks are shared
1291             # across all chains
1292             return self.networks
1293         if self.config.service_chain == ChainType.EXT:
1294             lookup_only = True
1295             ext_net = self.config.external_networks
1296             net_cfg = [AttrDict({'name': name,
1297                                  'segmentation_id': None,
1298                                  'physical_network': None})
1299                        for name in [ext_net.left, ext_net.right]]
1300             # segmentation id and subnet should be discovered from neutron
1301         else:
1302             lookup_only = False
1303             int_nets = self.config.internal_networks
1304             # VLAN and VxLAN
1305             if self.config.service_chain == ChainType.PVP:
1306                 net_cfg = [int_nets.left, int_nets.right]
1307             else:
1308                 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
1309             if self.config.l3_router:
1310                 edge_nets = self.config.edge_networks
1311                 net_cfg.append(edge_nets.left)
1312                 net_cfg.append(edge_nets.right)
1313         networks = []
1314         try:
1315             for cfg in net_cfg:
1316                 networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
1317         except Exception:
1318             # need to cleanup all successful networks prior to bailing out
1319             for net in networks:
1320                 net.delete()
1321             raise
1322         return networks
1323
1324     def get_existing_ports(self):
1325         """Get the list of existing ports.
1326
1327         Lazy retrieval of ports as this can be costly if there are lots of ports and
1328         is only needed when VM and network are being reused.
1329
1330         return: a dict of list of neutron ports indexed by the network uuid they are attached to
1331
1332         Each port is a dict with fields such as below:
1333         {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
1334          'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
1335          'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
1336          'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
1337          'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
1338          'security_groups': [],
1339          'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
1340                                  'vhostuser_mode': 'server'},
1341          'binding:vif_type': 'vhostuser',
1342          'mac_address': 'fa:16:3e:3c:63:04',
1343          'project_id': '977ac76a63d7492f927fa80e86baff4c',
1344          'status': 'ACTIVE',
1345          'binding:host_id': 'a20-champagne-compute-1',
1346          'description': '',
1347          'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
1348          'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
1349          'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
1350          'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
1351          'created_at': '2018-10-06T07:15:10Z',
1352          'binding:vnic_type': 'normal'}
1353         """
1354         if not self._existing_ports:
1355             LOG.info('Loading list of all ports...')
1356             existing_ports = self.neutron_client.list_ports()['ports']
1357             # place all ports in the dict keyed by the port network uuid
1358             for port in existing_ports:
1359                 port_list = self._existing_ports.setdefault(port['network_id'], [])
1360                 port_list.append(port)
1361             LOG.info("Loaded %d ports attached to %d networks",
1362                      len(existing_ports), len(self._existing_ports))
1363         return self._existing_ports
1364
1365     def get_ports_from_network(self, chain_network):
1366         """Get the list of existing ports that belong to a network.
1367
1368         Lazy retrieval of ports as this can be costly if there are lots of ports and
1369         is only needed when VM and network are being reused.
1370
1371         chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
1372         return: list of neutron ports attached to requested network
1373         """
1374         return self.get_existing_ports().get(chain_network.get_uuid(), None)
1375
1376     def get_hypervisor_from_mac(self, mac):
1377         """Get the hypervisor that hosts a VM MAC.
1378
1379         mac: MAC address to look for
1380         return: the hypervisor where the matching port runs or None if not found
1381         """
1382         # _existing_ports is a dict of list of ports indexed by network id
1383         for port_list in self.get_existing_ports().values():
1384             for port in port_list:
1385                 try:
1386                     if port['mac_address'] == mac:
1387                         host_id = port['binding:host_id']
1388                         return self.comp.get_hypervisor(host_id)
1389                 except KeyError:
1390                     pass
1391         return None
1392
1393     def get_host_ip_from_mac(self, mac):
1394         """Get the host IP address matching a MAC.
1395
1396         mac: MAC address to look for
1397         return: the IP address of the host where the matching port runs or None if not found
1398         """
1399         hypervisor = self.get_hypervisor_from_mac(mac)
1400         if hypervisor:
1401             return hypervisor.host_ip
1402         return None
1403
1404     def get_chain_vlans(self, port_index):
1405         """Get the list of per chain VLAN id on a given port.
1406
1407         port_index: left port is 0, right port is 1
1408         return: a VLAN ID list indexed by the chain index or None if no vlan tagging
1409         """
1410         if self.chains and self.is_admin:
1411             return [self.chains[chain_index].get_vlan(port_index)
1412                     for chain_index in range(self.chain_count)]
1413         # no openstack
1414         return self.vlans[port_index]
1415
1416     def get_chain_vxlans(self, port_index):
1417         """Get the list of per chain VNIs id on a given port.
1418
1419         port_index: left port is 0, right port is 1
1420         return: a VNIs ID list indexed by the chain index or None if no vlan tagging
1421         """
1422         if self.chains and self.is_admin:
1423             return [self.chains[chain_index].get_vxlan(port_index)
1424                     for chain_index in range(self.chain_count)]
1425         # no openstack
1426         raise ChainException('VxLAN is only supported with OpenStack and with admin user')
1427
1428     def get_dest_macs(self, port_index):
1429         """Get the list of per chain dest MACs on a given port.
1430
1431         Should not be called if EXT+ARP is used (in that case the traffic gen will
1432         have the ARP responses back from VNFs with the dest MAC to use).
1433
1434         port_index: left port is 0, right port is 1
1435         return: a list of dest MACs indexed by the chain index
1436         """
1437         if self.chains and self.config.service_chain != ChainType.EXT:
1438             return [self.chains[chain_index].get_dest_mac(port_index)
1439                     for chain_index in range(self.chain_count)]
1440         # no openstack or EXT+no-arp
1441         return self.dest_macs[port_index]
1442
1443     def get_host_ips(self):
1444         """Return the IP adresss(es) of the host compute nodes used for this run.
1445
1446         :return: a list of 1 IP address
1447         """
1448         # Since all chains go through the same compute node(s) we can just retrieve the
1449         # compute node(s) for the first chain
1450         if self.chains:
1451             if self.config.service_chain != ChainType.EXT:
1452                 return self.chains[0].get_host_ips()
1453             # in the case of EXT, the compute node must be retrieved from the port
1454             # associated to any of the dest MACs
1455             dst_macs = self.generator_config.get_dest_macs()
1456             # dest MAC on port 0, chain 0
1457             dst_mac = dst_macs[0][0]
1458             host_ip = self.get_host_ip_from_mac(dst_mac)
1459             if host_ip:
1460                 LOG.info('Found compute node IP for EXT chain: %s', host_ip)
1461                 return [host_ip]
1462         return []
1463
1464     def get_compute_nodes(self):
1465         """Return the name of the host compute nodes used for this run.
1466
1467         :return: a list of 0 or 1 host name in the az:host format
1468         """
1469         # Since all chains go through the same compute node(s) we can just retrieve the
1470         # compute node name(s) for the first chain
1471         if self.chains:
1472             # in the case of EXT, the compute node must be retrieved from the port
1473             # associated to any of the dest MACs
1474             if self.config.service_chain != ChainType.EXT:
1475                 return self.chains[0].get_compute_nodes()
1476             # in the case of EXT, the compute node must be retrieved from the port
1477             # associated to any of the dest MACs
1478             dst_macs = self.generator_config.get_dest_macs()
1479             # dest MAC on port 0, chain 0
1480             dst_mac = dst_macs[0][0]
1481             hypervisor = self.get_hypervisor_from_mac(dst_mac)
1482             if hypervisor:
1483                 LOG.info('Found hypervisor for EXT chain: %s', hypervisor.hypervisor_hostname)
1484                 return[':' + hypervisor.hypervisor_hostname]
1485         # no openstack = no chains
1486         return []
1487
1488     def delete(self):
1489         """Delete resources for all chains."""
1490         for chain in self.chains:
1491             chain.delete()
1492         for network in self.networks:
1493             network.delete()
1494         if self.config.use_management_port and hasattr(self, 'management_network'):
1495             self.management_network.delete()
1496         if self.config.use_floating_ip and hasattr(self, 'floating_ip_network'):
1497             self.floating_ip_network.delete()
1498         if self.flavor:
1499             self.flavor.delete()