NFVBENCH-5 NFVBENCH-39 Fix long prep time with large number of flows
[nfvbench.git] / nfvbench / chain_clients.py
1 #!/usr/bin/env python
2 # Copyright 2016 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 import compute
18 from glanceclient.v2 import client as glanceclient
19 from log import LOG
20 from neutronclient.neutron import client as neutronclient
21 from novaclient.client import Client
22 import os
23 import re
24 import time
25
26
27 class StageClientException(Exception):
28     pass
29
30
31 class BasicStageClient(object):
32     """Client for spawning and accessing the VM setup"""
33
34     nfvbenchvm_config_name = 'nfvbenchvm.conf'
35
36     def __init__(self, config, cred):
37         self.comp = None
38         self.image_instance = None
39         self.image_name = None
40         self.config = config
41         self.cred = cred
42         self.nets = []
43         self.vms = []
44         self.created_ports = []
45         self.ports = {}
46         self.compute_nodes = set([])
47         self.comp = None
48         self.neutron = None
49         self.flavor_type = {'is_reuse': True, 'flavor': None}
50         self.host_ips = None
51
52     def _ensure_vms_active(self):
53         retry_count = (self.config.check_traffic_time_sec +
54                        self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
55         for _ in range(retry_count):
56             for i, instance in enumerate(self.vms):
57                 if instance.status == 'ACTIVE':
58                     continue
59                 is_reuse = getattr(instance, 'is_reuse', True)
60                 instance = self.comp.poll_server(instance)
61                 if instance.status == 'ERROR':
62                     raise StageClientException('Instance creation error: %s' %
63                                                instance.fault['message'])
64                 if instance.status == 'ACTIVE':
65                     LOG.info('Created instance: %s', instance.name)
66                 self.vms[i] = instance
67                 setattr(self.vms[i], 'is_reuse', is_reuse)
68             if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
69                 return
70             time.sleep(self.config.generic_poll_sec)
71         raise StageClientException('Timed out waiting for VMs to spawn')
72
73     def _setup_openstack_clients(self):
74         self.session = self.cred.get_session()
75         nova_client = Client(2, session=self.session)
76         self.neutron = neutronclient.Client('2.0', session=self.session)
77         self.glance_client = glanceclient.Client('2',
78                                                  session=self.session)
79         self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
80
81     def _lookup_network(self, network_name):
82         networks = self.neutron.list_networks(name=network_name)
83         return networks['networks'][0] if networks['networks'] else None
84
85     def _create_net(self, name, subnet, cidr, network_type=None,
86                     segmentation_id=None, physical_network=None):
87         network = self._lookup_network(name)
88         if network:
89             # a network of same name already exists, we need to verify it has the same
90             # characteristics
91             if segmentation_id:
92                 if network['provider:segmentation_id'] != segmentation_id:
93                     raise StageClientException("Mismatch of 'segmentation_id' for reused "
94                                                "network '{net}'. Network has id '{seg_id1}', "
95                                                "configuration requires '{seg_id2}'."
96                                                .format(net=name,
97                                                        seg_id1=network['provider:segmentation_id'],
98                                                        seg_id2=segmentation_id))
99
100             if physical_network:
101                 if network['provider:physical_network'] != physical_network:
102                     raise StageClientException("Mismatch of 'physical_network' for reused "
103                                                "network '{net}'. Network has '{phys1}', "
104                                                "configuration requires '{phys2}'."
105                                                .format(net=name,
106                                                        phys1=network['provider:physical_network'],
107                                                        phys2=physical_network))
108
109             LOG.info('Reusing existing network: ' + name)
110             network['is_reuse'] = True
111             return network
112
113         body = {
114             'network': {
115                 'name': name,
116                 'admin_state_up': True
117             }
118         }
119
120         if network_type:
121             body['network']['provider:network_type'] = network_type
122             if segmentation_id:
123                 body['network']['provider:segmentation_id'] = segmentation_id
124             if physical_network:
125                 body['network']['provider:physical_network'] = physical_network
126
127         network = self.neutron.create_network(body)['network']
128         body = {
129             'subnet': {
130                 'name': subnet,
131                 'cidr': cidr,
132                 'network_id': network['id'],
133                 'enable_dhcp': False,
134                 'ip_version': 4,
135                 'dns_nameservers': []
136             }
137         }
138         subnet = self.neutron.create_subnet(body)['subnet']
139         # add subnet id to the network dict since it has just been added
140         network['subnets'] = [subnet['id']]
141         network['is_reuse'] = False
142         LOG.info('Created network: %s.' % name)
143         return network
144
145     def _create_port(self, net):
146         body = {
147             "port": {
148                 'network_id': net['id'],
149                 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
150             }
151         }
152         port = self.neutron.create_port(body)
153         return port['port']
154
155     def __delete_port(self, port):
156         retry = 0
157         while retry < self.config.generic_retry_count:
158             try:
159                 self.neutron.delete_port(port['id'])
160                 return
161             except Exception:
162                 retry += 1
163                 time.sleep(self.config.generic_poll_sec)
164         LOG.error('Unable to delete port: %s' % (port['id']))
165
166     def __delete_net(self, network):
167         retry = 0
168         while retry < self.config.generic_retry_count:
169             try:
170                 self.neutron.delete_network(network['id'])
171                 return
172             except Exception:
173                 retry += 1
174                 time.sleep(self.config.generic_poll_sec)
175         LOG.error('Unable to delete network: %s' % (network['name']))
176
177     def __get_server_az(self, server):
178         availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
179         host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
180         if availability_zone is None:
181             return None
182         if host is None:
183             return None
184         return availability_zone + ':' + host
185
186     def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
187         error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
188         networks = set(map(lambda net: net['name'], nets)) if nets else None
189         server_list = self.comp.get_server_list()
190         matching_servers = []
191
192         for server in server_list:
193             if name and server.name != name:
194                 continue
195
196             if az and self.__get_server_az(server) != az:
197                 raise StageClientException(error_msg.format('availability zones'))
198
199             if flavor_id and server.flavor['id'] != flavor_id:
200                 raise StageClientException(error_msg.format('flavors'))
201
202             if networks and not set(server.networks.keys()).issuperset(networks):
203                 raise StageClientException(error_msg.format('networks'))
204
205             if server.status != "ACTIVE":
206                 raise StageClientException(error_msg.format('state'))
207
208             # everything matches
209             matching_servers.append(server)
210
211         return matching_servers
212
213     def _create_server(self, name, ports, az, nfvbenchvm_config):
214         port_ids = map(lambda port: {'port-id': port['id']}, ports)
215         nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
216         server = self.comp.create_server(name,
217                                          self.image_instance,
218                                          self.flavor_type['flavor'],
219                                          None,
220                                          port_ids,
221                                          None,
222                                          avail_zone=az,
223                                          user_data=None,
224                                          config_drive=True,
225                                          files={nfvbenchvm_config_location: nfvbenchvm_config})
226         if server:
227             setattr(server, 'is_reuse', False)
228             LOG.info('Creating instance: %s on %s' % (name, az))
229         else:
230             raise StageClientException('Unable to create instance: %s.' % (name))
231         return server
232
233     def _setup_resources(self):
234         # To avoid reuploading image in server mode, check whether image_name is set or not
235         if self.image_name:
236             self.image_instance = self.comp.find_image(self.image_name)
237         if self.image_instance:
238             LOG.info("Reusing image %s" % self.image_name)
239         else:
240             image_name_search_pattern = '(nfvbenchvm-\d+(\.\d+)*).qcow2'
241             if self.config.vm_image_file:
242                 match = re.search(image_name_search_pattern, self.config.vm_image_file)
243                 if match:
244                     self.image_name = match.group(1)
245                     LOG.info('Using provided VM image file %s' % self.config.vm_image_file)
246                 else:
247                     raise StageClientException('Provided VM image file name %s must start with '
248                                                '"nfvbenchvm-<version>"' % self.config.vm_image_file)
249             else:
250                 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
251                 for f in os.listdir(pkg_root):
252                     if re.search(image_name_search_pattern, f):
253                         self.config.vm_image_file = pkg_root + '/' + f
254                         self.image_name = f.replace('.qcow2', '')
255                         LOG.info('Found built-in VM image file %s' % f)
256                         break
257                 else:
258                     raise StageClientException('Cannot find any built-in VM image file.')
259             if self.image_name:
260                 self.image_instance = self.comp.find_image(self.image_name)
261             if not self.image_instance:
262                 LOG.info('Uploading %s'
263                          % self.image_name)
264                 res = self.comp.upload_image_via_url(self.image_name,
265                                                      self.config.vm_image_file)
266
267                 if not res:
268                     raise StageClientException('Error uploading image %s from %s. ABORTING.'
269                                                % (self.image_name,
270                                                   self.config.vm_image_file))
271                 LOG.info('Image %s successfully uploaded.' % self.image_name)
272                 self.image_instance = self.comp.find_image(self.image_name)
273
274         self.__setup_flavor()
275
276     def __setup_flavor(self):
277         if self.flavor_type.get('flavor', False):
278             return
279
280         self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
281         if self.flavor_type['flavor']:
282             self.flavor_type['is_reuse'] = True
283         else:
284             flavor_dict = self.config.flavor
285             extra_specs = flavor_dict.pop('extra_specs', None)
286
287             self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
288                                                                  override=True,
289                                                                  **flavor_dict)
290
291             LOG.info("Flavor '%s' was created." % self.config.flavor_type)
292
293             if extra_specs:
294                 self.flavor_type['flavor'].set_keys(extra_specs)
295
296             self.flavor_type['is_reuse'] = False
297
298         if self.flavor_type['flavor'] is None:
299             raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
300                                        % self.config.flavor_type)
301
302     def __delete_flavor(self, flavor):
303         if self.comp.delete_flavor(flavor=flavor):
304             LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
305             self.flavor_type = {'is_reuse': False, 'flavor': None}
306         else:
307             LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
308
309     def get_config_file(self, chain_index, src_mac, dst_mac):
310         boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
311                                         'nfvbenchvm/', self.nfvbenchvm_config_name)
312
313         with open(boot_script_file, 'r') as boot_script:
314             content = boot_script.read()
315
316         g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
317         g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
318
319         vm_config = {
320             'forwarder': self.config.vm_forwarder,
321             'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
322             'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
323             'tg_net1': self.config.traffic_generator.ip_addrs[0],
324             'tg_net2': self.config.traffic_generator.ip_addrs[1],
325             'vnf_gateway1_cidr': g1cidr,
326             'vnf_gateway2_cidr': g2cidr,
327             'tg_mac1': src_mac,
328             'tg_mac2': dst_mac
329         }
330
331         return content.format(**vm_config)
332
333     def set_ports(self):
334         """Stores all ports of NFVbench networks."""
335         nets = self.get_networks_uuids()
336         for port in self.neutron.list_ports()['ports']:
337             if port['network_id'] in nets:
338                 ports = self.ports.setdefault(port['network_id'], [])
339                 ports.append(port)
340
341     def disable_port_security(self):
342         """
343         Disable security at port level.
344         """
345         vm_ids = map(lambda vm: vm.id, self.vms)
346         for net in self.nets:
347             for port in self.ports[net['id']]:
348                 if port['device_id'] in vm_ids:
349                     self.neutron.update_port(port['id'], {
350                         'port': {
351                             'security_groups': [],
352                             'port_security_enabled': False,
353                         }
354                     })
355                     LOG.info('Security disabled on port {}'.format(port['id']))
356
357     def get_loop_vm_hostnames(self):
358         return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
359
360     def get_host_ips(self):
361         '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
362         Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
363         '''
364         if not self.host_ips:
365             #  get the hypervisor object from the host name
366             self.host_ips = [self.comp.get_hypervisor(
367                              getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
368                              for vm in self.vms]
369         return self.host_ips
370
371     def get_loop_vm_compute_nodes(self):
372         compute_nodes = []
373         for vm in self.vms:
374             az = getattr(vm, 'OS-EXT-AZ:availability_zone')
375             hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
376             compute_nodes.append(az + ':' + hostname)
377         return compute_nodes
378
379     def get_reusable_vm(self, name, nets, az):
380         servers = self._lookup_servers(name=name, nets=nets, az=az,
381                                        flavor_id=self.flavor_type['flavor'].id)
382         if servers:
383             server = servers[0]
384             LOG.info('Reusing existing server: ' + name)
385             setattr(server, 'is_reuse', True)
386             return server
387         else:
388             return None
389
390     def get_networks_uuids(self):
391         """
392         Extract UUID of used networks. Order is important.
393
394         :return: list of UUIDs of created networks
395         """
396         return [net['id'] for net in self.nets]
397
398     def get_vlans(self):
399         """
400         Extract vlans of used networks. Order is important.
401
402         :return: list of UUIDs of created networks
403         """
404         vlans = []
405         for net in self.nets:
406             assert (net['provider:network_type'] == 'vlan')
407             vlans.append(net['provider:segmentation_id'])
408
409         return vlans
410
411     def setup(self):
412         """
413         Creates two networks and spawn a VM which act as a loop VM connected
414         with the two networks.
415         """
416         self._setup_openstack_clients()
417
418     def dispose(self, only_vm=False):
419         """
420         Deletes the created two networks and the VM.
421         """
422         for vm in self.vms:
423             if vm:
424                 if not getattr(vm, 'is_reuse', True):
425                     self.comp.delete_server(vm)
426                 else:
427                     LOG.info('Server %s not removed since it is reused' % vm.name)
428
429         for port in self.created_ports:
430             self.__delete_port(port)
431
432         if not only_vm:
433             for net in self.nets:
434                 if 'is_reuse' in net and not net['is_reuse']:
435                     self.__delete_net(net)
436                 else:
437                     LOG.info('Network %s not removed since it is reused' % (net['name']))
438
439             if not self.flavor_type['is_reuse']:
440                 self.__delete_flavor(self.flavor_type['flavor'])
441
442
443 class EXTStageClient(BasicStageClient):
444     def __init__(self, config, cred):
445         super(EXTStageClient, self).__init__(config, cred)
446
447     def setup(self):
448         super(EXTStageClient, self).setup()
449
450         # Lookup two existing networks
451         for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
452             net = self._lookup_network(net_name)
453             if net:
454                 self.nets.append(net)
455             else:
456                 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
457
458
459 class PVPStageClient(BasicStageClient):
460     def __init__(self, config, cred):
461         super(PVPStageClient, self).__init__(config, cred)
462
463     def get_end_port_macs(self):
464         vm_ids = map(lambda vm: vm.id, self.vms)
465         port_macs = []
466         for index, net in enumerate(self.nets):
467             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
468             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
469         return port_macs
470
471     def setup(self):
472         super(PVPStageClient, self).setup()
473         self._setup_resources()
474
475         # Create two networks
476         nets = self.config.internal_networks
477         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
478
479         az_list = self.comp.get_enabled_az_host_list(required_count=1)
480         if not az_list:
481             raise Exception('Not enough hosts found.')
482
483         az = az_list[0]
484         self.compute_nodes.add(az)
485         for chain_index in xrange(self.config.service_chain_count):
486             name = self.config.loop_vm_name + str(chain_index)
487             reusable_vm = self.get_reusable_vm(name, self.nets, az)
488             if reusable_vm:
489                 self.vms.append(reusable_vm)
490             else:
491                 config_file = self.get_config_file(chain_index,
492                                                    self.config.generator_config.src_device.mac,
493                                                    self.config.generator_config.dst_device.mac)
494
495                 ports = [self._create_port(net) for net in self.nets]
496                 self.created_ports.extend(ports)
497                 self.vms.append(self._create_server(name, ports, az, config_file))
498         self._ensure_vms_active()
499         self.set_ports()
500
501
502 class PVVPStageClient(BasicStageClient):
503     def __init__(self, config, cred):
504         super(PVVPStageClient, self).__init__(config, cred)
505
506     def get_end_port_macs(self):
507         port_macs = []
508         for index, net in enumerate(self.nets[:2]):
509             vm_ids = map(lambda vm: vm.id, self.vms[index::2])
510             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
511             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
512         return port_macs
513
514     def setup(self):
515         super(PVVPStageClient, self).setup()
516         self._setup_resources()
517
518         # Create two networks
519         nets = self.config.internal_networks
520         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
521
522         required_count = 2 if self.config.inter_node else 1
523         az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
524
525         if not az_list:
526             raise Exception('Not enough hosts found.')
527
528         az1 = az2 = az_list[0]
529         if self.config.inter_node:
530             if len(az_list) > 1:
531                 az1 = az_list[0]
532                 az2 = az_list[1]
533             else:
534                 # fallback to intra-node
535                 az1 = az2 = az_list[0]
536                 self.config.inter_node = False
537                 LOG.info('Using intra-node instead of inter-node.')
538
539         self.compute_nodes.add(az1)
540         self.compute_nodes.add(az2)
541
542         # Create loop VMs
543         for chain_index in xrange(self.config.service_chain_count):
544             name0 = self.config.loop_vm_name + str(chain_index) + 'a'
545             # Attach first VM to net0 and net2
546             vm0_nets = self.nets[0::2]
547             reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
548
549             name1 = self.config.loop_vm_name + str(chain_index) + 'b'
550             # Attach second VM to net1 and net2
551             vm1_nets = self.nets[1:]
552             reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
553
554             if reusable_vm0 and reusable_vm1:
555                 self.vms.extend([reusable_vm0, reusable_vm1])
556             else:
557                 vm0_port_net0 = self._create_port(vm0_nets[0])
558                 vm0_port_net2 = self._create_port(vm0_nets[1])
559
560                 vm1_port_net2 = self._create_port(vm1_nets[1])
561                 vm1_port_net1 = self._create_port(vm1_nets[0])
562
563                 self.created_ports.extend([vm0_port_net0,
564                                            vm0_port_net2,
565                                            vm1_port_net2,
566                                            vm1_port_net1])
567
568                 # order of ports is important for sections below
569                 # order of MAC addresses needs to follow order of interfaces
570                 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
571                 config_file0 = self.get_config_file(chain_index,
572                                                     self.config.generator_config.src_device.mac,
573                                                     vm1_port_net2['mac_address'])
574                 config_file1 = self.get_config_file(chain_index,
575                                                     vm0_port_net2['mac_address'],
576                                                     self.config.generator_config.dst_device.mac)
577
578                 self.vms.append(self._create_server(name0,
579                                                     [vm0_port_net0, vm0_port_net2],
580                                                     az1,
581                                                     config_file0))
582                 self.vms.append(self._create_server(name1,
583                                                     [vm1_port_net2, vm1_port_net1],
584                                                     az2,
585                                                     config_file1))
586
587         self._ensure_vms_active()
588         self.set_ports()