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