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