2.0 beta NFVBENCH-91 Allow multi-chaining with separate edge networks
[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 delete(self):
292         """Delete this network."""
293         if not self.reuse and self.network:
294             retry = 0
295             while retry < self.manager.config.generic_retry_count:
296                 try:
297                     self.manager.neutron_client.delete_network(self.network['id'])
298                     LOG.info("Deleted network: %s", self.name)
299                     return
300                 except Exception:
301                     retry += 1
302                     LOG.info('Error deleting network %s (retry %d/%d)...',
303                              self.name,
304                              retry,
305                              self.manager.config.generic_retry_count)
306                     time.sleep(self.manager.config.generic_poll_sec)
307             LOG.error('Unable to delete network: %s', self.name)
308
309
310 class ChainVnf(object):
311     """A class to represent a VNF in a chain."""
312
313     def __init__(self, chain, vnf_id, networks):
314         """Reuse a VNF instance with same characteristics or create a new VNF instance.
315
316         chain: the chain where this vnf belongs
317         vnf_id: indicates the index of this vnf in its chain (first vnf=0)
318         networks: the list of all networks (ChainNetwork) of the current chain
319         """
320         self.manager = chain.manager
321         self.chain = chain
322         self.vnf_id = vnf_id
323         self.name = self.manager.config.loop_vm_name + str(chain.chain_id)
324         if len(networks) > 2:
325             # we will have more than 1 VM in each chain
326             self.name += '-' + str(vnf_id)
327         self.ports = []
328         self.status = None
329         self.instance = None
330         self.reuse = False
331         self.host_ip = None
332         try:
333             # the vnf_id is conveniently also the starting index in networks
334             # for the left and right networks associated to this VNF
335             self._setup(networks[vnf_id:vnf_id + 2])
336         except Exception:
337             LOG.error("Error creating VNF %s", self.name)
338             self.delete()
339             raise
340
341     def _get_vm_config(self, remote_mac_pair):
342         config = self.manager.config
343         devices = self.manager.generator_config.devices
344         with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
345             content = boot_script.read()
346         g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
347         g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
348         vm_config = {
349             'forwarder': config.vm_forwarder,
350             'intf_mac1': self.ports[LEFT].get_mac(),
351             'intf_mac2': self.ports[RIGHT].get_mac(),
352             'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
353             'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
354             'tg_net1': devices[LEFT].ip_addrs,
355             'tg_net2': devices[RIGHT].ip_addrs,
356             'vnf_gateway1_cidr': g1cidr,
357             'vnf_gateway2_cidr': g2cidr,
358             'tg_mac1': remote_mac_pair[0],
359             'tg_mac2': remote_mac_pair[1]
360         }
361         return content.format(**vm_config)
362
363     def _get_vnic_type(self, port_index):
364         """Get the right vnic type for given port indexself.
365
366         If SR-IOV is speficied, middle ports in multi-VNF chains
367         can use vswitch or SR-IOV based on config.use_sriov_middle_net
368         """
369         if self.manager.config.sriov:
370             if self.manager.config.use_sriov_middle_net:
371                 return 'direct'
372             if self.vnf_id == 0:
373                 # first VNF in chain must use sriov for left port
374                 if port_index == 0:
375                     return 'direct'
376             elif (self.vnf_id == self.chain.get_length() - 1) and (port_index == 1):
377                 # last VNF in chain must use sriov for right port
378                 return 'direct'
379         return 'normal'
380
381     def _setup(self, networks):
382         flavor_id = self.manager.flavor.flavor.id
383         # Check if we can reuse an instance with same name
384         for instance in self.manager.existing_instances:
385             if instance.name == self.name:
386                 # Verify that other instance characteristics match
387                 if instance.flavor['id'] != flavor_id:
388                     self._reuse_exception('Flavor mismatch')
389                 if instance.status != "ACTIVE":
390                     self._reuse_exception('Matching instance is not in ACTIVE state')
391                 # The 2 networks for this instance must also be reused
392                 if not networks[LEFT].reuse:
393                     self._reuse_exception('network %s is new' % networks[LEFT].name)
394                 if not networks[RIGHT].reuse:
395                     self._reuse_exception('network %s is new' % networks[RIGHT].name)
396                 # instance.networks have the network names as keys:
397                 # {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
398                 if networks[LEFT].name not in instance.networks:
399                     self._reuse_exception('Left network mismatch')
400                 if networks[RIGHT].name not in instance.networks:
401                     self._reuse_exception('Right network mismatch')
402                 # Other checks not performed (yet)
403                 # check if az and compute node match
404                 self.reuse = True
405                 self.instance = instance
406                 LOG.info('Reusing existing instance %s on %s',
407                          self.name, self.get_hypervisor_name())
408         # create or reuse/discover 2 ports per instance
409         self.ports = [ChainVnfPort(self.name + '-' + str(index),
410                                    self,
411                                    networks[index],
412                                    self._get_vnic_type(index)) for index in [0, 1]]
413         # if no reuse, actual vm creation is deferred after all ports in the chain are created
414         # since we need to know the next mac in a multi-vnf chain
415
416     def get_az(self):
417         """Get the AZ associated to this VNF."""
418         return self.manager.az[0]
419
420     def create_vnf(self, remote_mac_pair):
421         """Create the VNF instance if it does not already exist."""
422         if self.instance is None:
423             port_ids = [{'port-id': vnf_port.port['id']}
424                         for vnf_port in self.ports]
425             vm_config = self._get_vm_config(remote_mac_pair)
426             az = self.get_az()
427             server = self.manager.comp.create_server(self.name,
428                                                      self.manager.image_instance,
429                                                      self.manager.flavor.flavor,
430                                                      None,
431                                                      port_ids,
432                                                      None,
433                                                      avail_zone=az,
434                                                      user_data=None,
435                                                      config_drive=True,
436                                                      files={NFVBENCH_CFG_VM_PATHNAME: vm_config})
437             if server:
438                 LOG.info('Created instance %s on %s', self.name, az)
439                 self.instance = server
440                 self.reuse = False
441             else:
442                 raise ChainException('Unable to create instance: %s' % (self.name))
443
444     def _reuse_exception(self, reason):
445         raise ChainException('Instance %s cannot be reused (%s)' % (self.name, reason))
446
447     def get_status(self):
448         """Get the statis of this instance."""
449         if self.instance.status != 'ACTIVE':
450             self.instance = self.manager.comp.poll_server(self.instance)
451         return self.instance.status
452
453     def get_hostname(self):
454         """Get the hypervisor host name running this VNF instance."""
455         return getattr(self.instance, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
456
457     def get_host_ip(self):
458         """Get the IP address of the host where this instance runs.
459
460         return: the IP address
461         """
462         if not self.host_ip:
463             self.host_ip = self.manager.comp.get_hypervisor(self.get_hostname()).host_ip
464         return self.host_ip
465
466     def get_hypervisor_name(self):
467         """Get hypervisor name (az:hostname) for this VNF instance."""
468         if self.instance:
469             az = getattr(self.instance, 'OS-EXT-AZ:availability_zone')
470             hostname = self.get_hostname()
471             if az:
472                 return az + ':' + hostname
473             return hostname
474         return None
475
476     def get_uuid(self):
477         """Get the uuid for this instance."""
478         return self.instance.id
479
480     def delete(self, forced=False):
481         """Delete this VNF instance."""
482         if self.reuse:
483             LOG.info("Instance %s not deleted (reused)", self.name)
484         else:
485             if self.instance:
486                 self.manager.comp.delete_server(self.instance)
487                 LOG.info("Deleted instance %s", self.name)
488             for port in self.ports:
489                 port.delete()
490
491 class Chain(object):
492     """A class to manage a single chain.
493
494     Can handle any type of chain (EXT, PVP, PVVP)
495     """
496
497     def __init__(self, chain_id, manager):
498         """Create a new chain.
499
500         chain_id: chain index (first chain is 0)
501         manager: the chain manager that owns all chains
502         """
503         self.chain_id = chain_id
504         self.manager = manager
505         self.encaps = manager.encaps
506         self.networks = []
507         self.instances = []
508         try:
509             self.networks = manager.get_networks(chain_id)
510             # For external chain VNFs can only be discovered from their MAC addresses
511             # either from config or from ARP
512             if manager.config.service_chain != ChainType.EXT:
513                 for chain_instance_index in range(self.get_length()):
514                     self.instances.append(ChainVnf(self,
515                                                    chain_instance_index,
516                                                    self.networks))
517                 # now that all VNF ports are created we need to calculate the
518                 # left/right remote MAC for each VNF in the chain
519                 # before actually creating the VNF itself
520                 rem_mac_pairs = self._get_remote_mac_pairs()
521                 for instance in self.instances:
522                     rem_mac_pair = rem_mac_pairs.pop(0)
523                     instance.create_vnf(rem_mac_pair)
524         except Exception:
525             self.delete()
526             raise
527
528     def get_length(self):
529         """Get the number of VNF in the chain."""
530         return len(self.networks) - 1
531
532     def _get_remote_mac_pairs(self):
533         """Get the list of remote mac pairs for every VNF in the chain.
534
535         Traverse the chain from left to right and establish the
536         left/right remote MAC for each VNF in the chainself.
537
538         PVP case is simpler:
539         mac sequence: tg_src_mac, vm0-mac0, vm0-mac1, tg_dst_mac
540         must produce [[tg_src_mac, tg_dst_mac]] or looking at index in mac sequence: [[0, 3]]
541         the mac pair is what the VNF at that position (index 0) sees as next hop mac left and right
542
543         PVVP:
544         tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, tg_dst_mac
545         Must produce the following list:
546         [[tg_src_mac, vm1-mac0], [vm0-mac1, tg_dst_mac]] or index: [[0, 3], [2, 5]]
547
548         General case with 3 VMs in chain, the list of consecutive macs (left to right):
549         tg_src_mac, vm0-mac0, vm0-mac1, vm1-mac0, vm1-mac1, vm2-mac0, vm2-mac1, tg_dst_mac
550         Must produce the following list:
551         [[tg_src_mac, vm1-mac0], [vm0-mac1, vm2-mac0], [vm1-mac1, tg_dst_mac]]
552         or index: [[0, 3], [2, 5], [4, 7]]
553
554         The series pattern is pretty clear: [[n, n+3],... ] where n is multiple of 2
555         """
556         # line up all mac from left to right
557         mac_seq = [self.manager.generator_config.devices[LEFT].mac]
558         for instance in self.instances:
559             mac_seq.append(instance.ports[0].get_mac())
560             mac_seq.append(instance.ports[1].get_mac())
561         mac_seq.append(self.manager.generator_config.devices[RIGHT].mac)
562         base = 0
563         rem_mac_pairs = []
564         for _ in self.instances:
565             rem_mac_pairs.append([mac_seq[base], mac_seq[base + 3]])
566             base += 2
567         return rem_mac_pairs
568
569     def get_instances(self):
570         """Return all instances for this chain."""
571         return self.instances
572
573     def get_vlan(self, port_index):
574         """Get the VLAN id on a given port.
575
576         port_index: left port is 0, right port is 1
577         return: the vlan_id or None if there is no vlan tagging
578         """
579         # for port 1 we need to return the VLAN of the last network in the chain
580         # The networks array contains 2 networks for PVP [left, right]
581         # and 3 networks in the case of PVVP [left.middle,right]
582         if port_index:
583             # this will pick the last item in array
584             port_index = -1
585         return self.networks[port_index].get_vlan()
586
587     def get_dest_mac(self, port_index):
588         """Get the dest MAC on a given port.
589
590         port_index: left port is 0, right port is 1
591         return: the dest MAC
592         """
593         if port_index:
594             # for right port, use the right port MAC of the last (right most) VNF In chain
595             return self.instances[-1].ports[1].get_mac()
596         # for left port use the left port MAC of the first (left most) VNF in chain
597         return self.instances[0].ports[0].get_mac()
598
599     def get_network_uuids(self):
600         """Get UUID of networks in this chain from left to right (order is important).
601
602         :return: list of UUIDs of networks (2 or 3 elements)
603         """
604         return [net['id'] for net in self.networks]
605
606     def get_host_ips(self):
607         """Return the IP adresss(es) of the host compute nodes used for this chain.
608
609         :return: a list of 1 or 2 IP addresses
610         """
611         return [vnf.get_host_ip() for vnf in self.instances]
612
613     def get_compute_nodes(self):
614         """Return the name of the host compute nodes used for this chain.
615
616         :return: a list of 1 host name in the az:host format
617         """
618         # Since all chains go through the same compute node(s) we can just retrieve the
619         # compute node name(s) for the first chain
620         return [vnf.get_hypervisor_name() for vnf in self.instances]
621
622     def delete(self):
623         """Delete this chain."""
624         for instance in self.instances:
625             instance.delete()
626         # only delete if these are chain private networks (not shared)
627         if not self.manager.config.service_chain_shared_net:
628             for network in self.networks:
629                 network.delete()
630
631
632 class ChainManager(object):
633     """A class for managing all chains for a given run.
634
635     Supports openstack or no openstack.
636     Supports EXT, PVP and PVVP chains.
637     """
638
639     def __init__(self, chain_runner):
640         """Create a chain manager to take care of discovering or bringing up the requested chains.
641
642         A new instance must be created every time a new config is used.
643         config: the nfvbench config to use
644         cred: openstack credentials to use of None if there is no openstack
645         """
646         self.chain_runner = chain_runner
647         self.config = chain_runner.config
648         self.generator_config = chain_runner.traffic_client.generator_config
649         self.chains = []
650         self.image_instance = None
651         self.image_name = None
652         # Left and right networks shared across all chains (only if shared)
653         self.networks = []
654         self.encaps = None
655         self.flavor = None
656         self.comp = None
657         self.nova_client = None
658         self.neutron_client = None
659         self.glance_client = None
660         self.existing_instances = []
661         # existing ports keyed by the network uuid they belong to
662         self._existing_ports = {}
663         config = self.config
664         self.openstack = (chain_runner.cred is not None) and not config.l2_loopback
665         self.chain_count = config.service_chain_count
666         if self.openstack:
667             # openstack only
668             session = chain_runner.cred.get_session()
669             self.nova_client = Client(2, session=session)
670             self.neutron_client = neutronclient.Client('2.0', session=session)
671             self.glance_client = glanceclient.Client('2', session=session)
672             self.comp = compute.Compute(self.nova_client,
673                                         self.glance_client,
674                                         config)
675             self.az = None
676             try:
677                 if config.service_chain != ChainType.EXT:
678                     # we need to find 1 hypervisor
679                     az_list = self.comp.get_enabled_az_host_list(1)
680                     if not az_list:
681                         raise ChainException('No matching hypervisor found')
682                     self.az = az_list
683                     self._setup_image()
684                     self.flavor = ChainFlavor(config.flavor_type, config.flavor, self.comp)
685                     # Get list of all existing instances to check if some instances can be reused
686                     self.existing_instances = self.comp.get_server_list()
687                 # If networks are shared across chains, get the list of networks
688                 if config.service_chain_shared_net:
689                     self.networks = self.get_networks()
690                 # Reuse/create chains
691                 for chain_id in range(self.chain_count):
692                     self.chains.append(Chain(chain_id, self))
693                 if config.service_chain == ChainType.EXT:
694                     # if EXT and no ARP we need to read dest MACs from config
695                     if config.no_arp:
696                         self._get_dest_macs_from_config()
697                 else:
698                     # Make sure all instances are active before proceeding
699                     self._ensure_instances_active()
700             except Exception:
701                 self.delete()
702                 raise
703         else:
704             # no openstack, no need to create chains
705             # make sure there at least as many entries as chains in each left/right list
706             if len(config.vlans) != 2:
707                 raise ChainException('The config vlans property must be a list '
708                                      'with 2 lists of VLAN IDs')
709             if not config.l2_loopback:
710                 self._get_dest_macs_from_config()
711
712             re_vlan = "[0-9]*$"
713             self.vlans = [self._check_list('vlans[0]', config.vlans[0], re_vlan),
714                           self._check_list('vlans[1]', config.vlans[1], re_vlan)]
715
716     def _get_dest_macs_from_config(self):
717         re_mac = "[0-9a-fA-F]{2}([-:])[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$"
718         tg_config = self.config.traffic_generator
719         self.dest_macs = [self._check_list("mac_addrs_left",
720                                            tg_config.mac_addrs_left, re_mac),
721                           self._check_list("mac_addrs_right",
722                                            tg_config.mac_addrs_right, re_mac)]
723
724     def _check_list(self, list_name, ll, pattern):
725         # if it is a single int or mac, make it a list of 1 int
726         if isinstance(ll, (int, str)):
727             ll = [ll]
728         if not ll or len(ll) < self.chain_count:
729             raise ChainException('%s=%s must be a list with 1 element per chain' % (list_name, ll))
730         for item in ll:
731             if not re.match(pattern, str(item)):
732                 raise ChainException("Invalid format '{item}' specified in {fname}"
733                                      .format(item=item, fname=list_name))
734         return ll
735
736     def _setup_image(self):
737         # To avoid reuploading image in server mode, check whether image_name is set or not
738         if self.image_name:
739             self.image_instance = self.comp.find_image(self.image_name)
740         if self.image_instance:
741             LOG.info("Reusing image %s", self.image_name)
742         else:
743             image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
744             if self.config.vm_image_file:
745                 match = re.search(image_name_search_pattern, self.config.vm_image_file)
746                 if match:
747                     self.image_name = match.group(1)
748                     LOG.info('Using provided VM image file %s', self.config.vm_image_file)
749                 else:
750                     raise ChainException('Provided VM image file name %s must start with '
751                                          '"nfvbenchvm-<version>"' % self.config.vm_image_file)
752             else:
753                 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
754                 for f in os.listdir(pkg_root):
755                     if re.search(image_name_search_pattern, f):
756                         self.config.vm_image_file = pkg_root + '/' + f
757                         self.image_name = f.replace('.qcow2', '')
758                         LOG.info('Found built-in VM image file %s', f)
759                         break
760                 else:
761                     raise ChainException('Cannot find any built-in VM image file.')
762             if self.image_name:
763                 self.image_instance = self.comp.find_image(self.image_name)
764             if not self.image_instance:
765                 LOG.info('Uploading %s', self.image_name)
766                 res = self.comp.upload_image_via_url(self.image_name,
767                                                      self.config.vm_image_file)
768
769                 if not res:
770                     raise ChainException('Error uploading image %s from %s. ABORTING.' %
771                                          (self.image_name, self.config.vm_image_file))
772                 LOG.info('Image %s successfully uploaded.', self.image_name)
773                 self.image_instance = self.comp.find_image(self.image_name)
774
775     def _ensure_instances_active(self):
776         instances = []
777         for chain in self.chains:
778             instances.extend(chain.get_instances())
779         initial_instance_count = len(instances)
780         max_retries = (self.config.check_traffic_time_sec +
781                        self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
782         retry = 0
783         while instances:
784             remaining_instances = []
785             for instance in instances:
786                 status = instance.get_status()
787                 if status == 'ACTIVE':
788                     continue
789                 if status == 'ERROR':
790                     raise ChainException('Instance %s creation error: %s' %
791                                          (instance.name,
792                                           instance.instance.fault['message']))
793                 remaining_instances.append(instance)
794             if not remaining_instances:
795                 break
796             retry += 1
797             if retry >= max_retries:
798                 raise ChainException('Time-out: %d/%d instances still not active' %
799                                      (len(remaining_instances), initial_instance_count))
800             LOG.info('Waiting for %d/%d instance to become active (retry %d/%d)...',
801                      len(remaining_instances), initial_instance_count,
802                      retry, max_retries)
803             instances = remaining_instances
804             time.sleep(self.config.generic_poll_sec)
805         if initial_instance_count:
806             LOG.info('All instances are active')
807
808     def get_networks(self, chain_id=None):
809         """Get the networks for given EXT, PVP or PVVP chain.
810
811         For EXT packet path, these networks must pre-exist.
812         For PVP, PVVP these networks will be created if they do not exist.
813         chain_id: to which chain the networks belong.
814                   a None value will mean that these networks are shared by all chains
815         """
816         if self.networks:
817             # the only case where self.networks exists is when the networks are shared
818             # across all chains
819             return self.networks
820         if self.config.service_chain == ChainType.EXT:
821             lookup_only = True
822             ext_net = self.config.external_networks
823             net_cfg = [AttrDict({'name': name,
824                                  'segmentation_id': None,
825                                  'physical_network': None})
826                        for name in [ext_net.left, ext_net.right]]
827         else:
828             lookup_only = False
829             int_nets = self.config.internal_networks
830             if self.config.service_chain == ChainType.PVP:
831                 net_cfg = [int_nets.left, int_nets.right]
832             else:
833                 net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
834         networks = []
835         try:
836             for cfg in net_cfg:
837                 networks.append(ChainNetwork(self, cfg, chain_id, lookup_only=lookup_only))
838         except Exception:
839             # need to cleanup all successful networks prior to bailing out
840             for net in networks:
841                 net.delete()
842             raise
843         return networks
844
845     def get_existing_ports(self):
846         """Get the list of existing ports.
847
848         Lazy retrieval of ports as this can be costly if there are lots of ports and
849         is only needed when VM and network are being reused.
850
851         return: a dict of list of neutron ports indexed by the network uuid they are attached to
852
853         Each port is a dict with fields such as below:
854         {'allowed_address_pairs': [], 'extra_dhcp_opts': [],
855          'updated_at': '2018-10-06T07:15:35Z', 'device_owner': 'compute:nova',
856          'revision_number': 10, 'port_security_enabled': False, 'binding:profile': {},
857          'fixed_ips': [{'subnet_id': '6903a3b3-49a1-4ba4-8259-4a90e7a44b21',
858          'ip_address': '192.168.1.4'}], 'id': '3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
859          'security_groups': [],
860          'binding:vif_details': {'vhostuser_socket': '/tmp/3dcb9cfa-d82a-4dd1-85a1-fd8284b52d72',
861                                  'vhostuser_mode': 'server'},
862          'binding:vif_type': 'vhostuser',
863          'mac_address': 'fa:16:3e:3c:63:04',
864          'project_id': '977ac76a63d7492f927fa80e86baff4c',
865          'status': 'ACTIVE',
866          'binding:host_id': 'a20-champagne-compute-1',
867          'description': '',
868          'device_id': 'a98e2ad2-5371-4aa5-a356-8264a970ce4b',
869          'name': 'nfvbench-loop-vm0-0', 'admin_state_up': True,
870          'network_id': '3ea5fd88-278f-4d9d-b24d-1e443791a055',
871          'tenant_id': '977ac76a63d7492f927fa80e86baff4c',
872          'created_at': '2018-10-06T07:15:10Z',
873          'binding:vnic_type': 'normal'}
874         """
875         if not self._existing_ports:
876             LOG.info('Loading list of all ports...')
877             existing_ports = self.neutron_client.list_ports()['ports']
878             # place all ports in the dict keyed by the port network uuid
879             for port in existing_ports:
880                 port_list = self._existing_ports.setdefault(port['network_id'], [])
881                 port_list.append(port)
882             LOG.info("Loaded %d ports attached to %d networks",
883                      len(existing_ports), len(self._existing_ports))
884         return self._existing_ports
885
886     def get_ports_from_network(self, chain_network):
887         """Get the list of existing ports that belong to a network.
888
889         Lazy retrieval of ports as this can be costly if there are lots of ports and
890         is only needed when VM and network are being reused.
891
892         chain_network: a ChainNetwork instance for which attached ports neeed to be retrieved
893         return: list of neutron ports attached to requested network
894         """
895         return self.get_existing_ports().get(chain_network.get_uuid(), None)
896
897     def get_host_ip_from_mac(self, mac):
898         """Get the host IP address matching a MAC.
899
900         mac: MAC address to look for
901         return: the IP address of the host where the matching port runs or None if not found
902         """
903         # _existing_ports is a dict of list of ports indexed by network id
904         for port_list in self.get_existing_ports().values():
905             for port in port_list:
906                 try:
907                     if port['mac_address'] == mac:
908                         host_id = port['binding:host_id']
909                         return self.comp.get_hypervisor(host_id).host_ip
910                 except KeyError:
911                     pass
912         return None
913
914     def get_chain_vlans(self, port_index):
915         """Get the list of per chain VLAN id on a given port.
916
917         port_index: left port is 0, right port is 1
918         return: a VLAN ID list indexed by the chain index or None if no vlan tagging
919         """
920         if self.chains:
921             return [self.chains[chain_index].get_vlan(port_index)
922                     for chain_index in range(self.chain_count)]
923         # no openstack
924         return self.vlans[port_index]
925
926     def get_dest_macs(self, port_index):
927         """Get the list of per chain dest MACs on a given port.
928
929         Should not be called if EXT+ARP is used (in that case the traffic gen will
930         have the ARP responses back from VNFs with the dest MAC to use).
931
932         port_index: left port is 0, right port is 1
933         return: a list of dest MACs indexed by the chain index
934         """
935         if self.chains and self.config.service_chain != ChainType.EXT:
936             return [self.chains[chain_index].get_dest_mac(port_index)
937                     for chain_index in range(self.chain_count)]
938         # no openstack or EXT+no-arp
939         return self.dest_macs[port_index]
940
941     def get_host_ips(self):
942         """Return the IP adresss(es) of the host compute nodes used for this run.
943
944         :return: a list of 1 IP address
945         """
946         # Since all chains go through the same compute node(s) we can just retrieve the
947         # compute node(s) for the first chain
948         if self.chains:
949             if self.config.service_chain != ChainType.EXT:
950                 return self.chains[0].get_host_ips()
951             # in the case of EXT, the compute node must be retrieved from the port
952             # associated to any of the dest MACs
953             dst_macs = self.chain_runner.traffic_client.gen.get_dest_macs()
954             # dest MAC on port 0, chain 0
955             dst_mac = dst_macs[0][0]
956             host_ip = self.get_host_ip_from_mac(dst_mac)
957             if host_ip:
958                 LOG.info('Found compute node IP for EXT chain: %s', host_ip)
959                 return [host_ip]
960         return []
961
962     def get_compute_nodes(self):
963         """Return the name of the host compute nodes used for this run.
964
965         :return: a list of 0 or 1 host name in the az:host format
966         """
967         # Since all chains go through the same compute node(s) we can just retrieve the
968         # compute node name(s) for the first chain
969         if self.chains:
970             # in the case of EXT, the compute node must be retrieved from the port
971             # associated to any of the dest MACs
972             return self.chains[0].get_compute_nodes()
973         # no openstack = no chains
974         return []
975
976     def delete(self):
977         """Delete resources for all chains.
978
979         Will not delete any resource if no-cleanup has been requested.
980         """
981         if self.config.no_cleanup:
982             return
983         for chain in self.chains:
984             chain.delete()
985         for network in self.networks:
986             network.delete()
987         if self.flavor:
988             self.flavor.delete()