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