NFVBENCH-38 Fix typo drop_percentage
[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 flavor_id and server.flavor['id'] != flavor_id:
197                 raise StageClientException(error_msg.format('flavors'))
198
199             if networks and not set(server.networks.keys()).issuperset(networks):
200                 raise StageClientException(error_msg.format('networks'))
201
202             if server.status != "ACTIVE":
203                 raise StageClientException(error_msg.format('state'))
204
205             # everything matches
206             matching_servers.append(server)
207
208         return matching_servers
209
210     def _create_server(self, name, ports, az, nfvbenchvm_config):
211         port_ids = map(lambda port: {'port-id': port['id']}, ports)
212         nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
213         server = self.comp.create_server(name,
214                                          self.image_instance,
215                                          self.flavor_type['flavor'],
216                                          None,
217                                          port_ids,
218                                          None,
219                                          avail_zone=az,
220                                          user_data=None,
221                                          config_drive=True,
222                                          files={nfvbenchvm_config_location: nfvbenchvm_config})
223         if server:
224             setattr(server, 'is_reuse', False)
225             LOG.info('Creating instance: %s on %s' % (name, az))
226         else:
227             raise StageClientException('Unable to create instance: %s.' % (name))
228         return server
229
230     def _setup_resources(self):
231         # To avoid reuploading image in server mode, check whether image_name is set or not
232         if self.image_name:
233             self.image_instance = self.comp.find_image(self.image_name)
234         if self.image_instance:
235             LOG.info("Reusing image %s" % self.image_name)
236         else:
237             image_name_search_pattern = '(nfvbenchvm-\d+(\.\d+)*).qcow2'
238             if self.config.vm_image_file:
239                 match = re.search(image_name_search_pattern, self.config.vm_image_file)
240                 if match:
241                     self.image_name = match.group(1)
242                     LOG.info('Using provided VM image file %s' % self.config.vm_image_file)
243                 else:
244                     raise StageClientException('Provided VM image file name %s must start with '
245                                                '"nfvbenchvm-<version>"' % self.config.vm_image_file)
246             else:
247                 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
248                 for f in os.listdir(pkg_root):
249                     if re.search(image_name_search_pattern, f):
250                         self.config.vm_image_file = pkg_root + '/' + f
251                         self.image_name = f.replace('.qcow2', '')
252                         LOG.info('Found built-in VM image file %s' % f)
253                         break
254                 else:
255                     raise StageClientException('Cannot find any built-in VM image file.')
256             if self.image_name:
257                 self.image_instance = self.comp.find_image(self.image_name)
258             if not self.image_instance:
259                 LOG.info('Uploading %s'
260                          % self.image_name)
261                 res = self.comp.upload_image_via_url(self.image_name,
262                                                      self.config.vm_image_file)
263
264                 if not res:
265                     raise StageClientException('Error uploading image %s from %s. ABORTING.'
266                                                % (self.image_name,
267                                                   self.config.vm_image_file))
268                 LOG.info('Image %s successfully uploaded.' % self.image_name)
269                 self.image_instance = self.comp.find_image(self.image_name)
270
271         self.__setup_flavor()
272
273     def __setup_flavor(self):
274         if self.flavor_type.get('flavor', False):
275             return
276
277         self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
278         if self.flavor_type['flavor']:
279             self.flavor_type['is_reuse'] = True
280         else:
281             flavor_dict = self.config.flavor
282             extra_specs = flavor_dict.pop('extra_specs', None)
283
284             self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
285                                                                  override=True,
286                                                                  **flavor_dict)
287
288             LOG.info("Flavor '%s' was created." % self.config.flavor_type)
289
290             if extra_specs:
291                 self.flavor_type['flavor'].set_keys(extra_specs)
292
293             self.flavor_type['is_reuse'] = False
294
295         if self.flavor_type['flavor'] is None:
296             raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
297                                        % self.config.flavor_type)
298
299     def __delete_flavor(self, flavor):
300         if self.comp.delete_flavor(flavor=flavor):
301             LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
302             self.flavor_type = {'is_reuse': False, 'flavor': None}
303         else:
304             LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
305
306     def get_config_file(self, chain_index, src_mac, dst_mac):
307         boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
308                                         'nfvbenchvm/', self.nfvbenchvm_config_name)
309
310         with open(boot_script_file, 'r') as boot_script:
311             content = boot_script.read()
312
313         g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
314         g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
315
316         vm_config = {
317             'forwarder': self.config.vm_forwarder,
318             'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
319             'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
320             'tg_net1': self.config.traffic_generator.ip_addrs[0],
321             'tg_net2': self.config.traffic_generator.ip_addrs[1],
322             'vnf_gateway1_cidr': g1cidr,
323             'vnf_gateway2_cidr': g2cidr,
324             'tg_mac1': src_mac,
325             'tg_mac2': dst_mac
326         }
327
328         return content.format(**vm_config)
329
330     def set_ports(self):
331         """Stores all ports of NFVbench networks."""
332         nets = self.get_networks_uuids()
333         for port in self.neutron.list_ports()['ports']:
334             if port['network_id'] in nets:
335                 ports = self.ports.setdefault(port['network_id'], [])
336                 ports.append(port)
337
338     def disable_port_security(self):
339         """
340         Disable security at port level.
341         """
342         vm_ids = map(lambda vm: vm.id, self.vms)
343         for net in self.nets:
344             for port in self.ports[net['id']]:
345                 if port['device_id'] in vm_ids:
346                     self.neutron.update_port(port['id'], {
347                         'port': {
348                             'security_groups': [],
349                             'port_security_enabled': False,
350                         }
351                     })
352                     LOG.info('Security disabled on port {}'.format(port['id']))
353
354     def get_loop_vm_hostnames(self):
355         return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
356
357     def get_host_ips(self):
358         '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
359         Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
360         '''
361         if not self.host_ips:
362             #  get the hypervisor object from the host name
363             self.host_ips = [self.comp.get_hypervisor(
364                              getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
365                              for vm in self.vms]
366         return self.host_ips
367
368     def get_loop_vm_compute_nodes(self):
369         compute_nodes = []
370         for vm in self.vms:
371             az = getattr(vm, 'OS-EXT-AZ:availability_zone')
372             hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
373             compute_nodes.append(az + ':' + hostname)
374         return compute_nodes
375
376     def get_reusable_vm(self, name, nets, az):
377         servers = self._lookup_servers(name=name, nets=nets, az=az,
378                                        flavor_id=self.flavor_type['flavor'].id)
379         if servers:
380             server = servers[0]
381             LOG.info('Reusing existing server: ' + name)
382             setattr(server, 'is_reuse', True)
383             return server
384         else:
385             return None
386
387     def get_networks_uuids(self):
388         """
389         Extract UUID of used networks. Order is important.
390
391         :return: list of UUIDs of created networks
392         """
393         return [net['id'] for net in self.nets]
394
395     def get_vlans(self):
396         """
397         Extract vlans of used networks. Order is important.
398
399         :return: list of UUIDs of created networks
400         """
401         vlans = []
402         for net in self.nets:
403             assert (net['provider:network_type'] == 'vlan')
404             vlans.append(net['provider:segmentation_id'])
405
406         return vlans
407
408     def setup(self):
409         """
410         Creates two networks and spawn a VM which act as a loop VM connected
411         with the two networks.
412         """
413         self._setup_openstack_clients()
414
415     def dispose(self, only_vm=False):
416         """
417         Deletes the created two networks and the VM.
418         """
419         for vm in self.vms:
420             if vm:
421                 if not getattr(vm, 'is_reuse', True):
422                     self.comp.delete_server(vm)
423                 else:
424                     LOG.info('Server %s not removed since it is reused' % vm.name)
425
426         for port in self.created_ports:
427             self.__delete_port(port)
428
429         if not only_vm:
430             for net in self.nets:
431                 if 'is_reuse' in net and not net['is_reuse']:
432                     self.__delete_net(net)
433                 else:
434                     LOG.info('Network %s not removed since it is reused' % (net['name']))
435
436             if not self.flavor_type['is_reuse']:
437                 self.__delete_flavor(self.flavor_type['flavor'])
438
439
440 class EXTStageClient(BasicStageClient):
441     def __init__(self, config, cred):
442         super(EXTStageClient, self).__init__(config, cred)
443
444     def setup(self):
445         super(EXTStageClient, self).setup()
446
447         # Lookup two existing networks
448         for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
449             net = self._lookup_network(net_name)
450             if net:
451                 self.nets.append(net)
452             else:
453                 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
454
455
456 class PVPStageClient(BasicStageClient):
457     def __init__(self, config, cred):
458         super(PVPStageClient, self).__init__(config, cred)
459
460     def get_end_port_macs(self):
461         vm_ids = map(lambda vm: vm.id, self.vms)
462         port_macs = []
463         for index, net in enumerate(self.nets):
464             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
465             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
466         return port_macs
467
468     def setup(self):
469         super(PVPStageClient, self).setup()
470         self._setup_resources()
471
472         # Create two networks
473         nets = self.config.internal_networks
474         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
475
476         az_list = self.comp.get_enabled_az_host_list(required_count=1)
477         if not az_list:
478             raise Exception('Not enough hosts found.')
479
480         az = az_list[0]
481         self.compute_nodes.add(az)
482         for chain_index in xrange(self.config.service_chain_count):
483             name = self.config.loop_vm_name + str(chain_index)
484             reusable_vm = self.get_reusable_vm(name, self.nets, az)
485             if reusable_vm:
486                 self.vms.append(reusable_vm)
487             else:
488                 config_file = self.get_config_file(chain_index,
489                                                    self.config.generator_config.src_device.mac,
490                                                    self.config.generator_config.dst_device.mac)
491
492                 ports = [self._create_port(net) for net in self.nets]
493                 self.created_ports.extend(ports)
494                 self.vms.append(self._create_server(name, ports, az, config_file))
495         self._ensure_vms_active()
496         self.set_ports()
497
498
499 class PVVPStageClient(BasicStageClient):
500     def __init__(self, config, cred):
501         super(PVVPStageClient, self).__init__(config, cred)
502
503     def get_end_port_macs(self):
504         port_macs = []
505         for index, net in enumerate(self.nets[:2]):
506             vm_ids = map(lambda vm: vm.id, self.vms[index::2])
507             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
508             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
509         return port_macs
510
511     def setup(self):
512         super(PVVPStageClient, self).setup()
513         self._setup_resources()
514
515         # Create two networks
516         nets = self.config.internal_networks
517         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
518
519         required_count = 2 if self.config.inter_node else 1
520         az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
521
522         if not az_list:
523             raise Exception('Not enough hosts found.')
524
525         az1 = az2 = az_list[0]
526         if self.config.inter_node:
527             if len(az_list) > 1:
528                 az1 = az_list[0]
529                 az2 = az_list[1]
530             else:
531                 # fallback to intra-node
532                 az1 = az2 = az_list[0]
533                 self.config.inter_node = False
534                 LOG.info('Using intra-node instead of inter-node.')
535
536         self.compute_nodes.add(az1)
537         self.compute_nodes.add(az2)
538
539         # Create loop VMs
540         for chain_index in xrange(self.config.service_chain_count):
541             name0 = self.config.loop_vm_name + str(chain_index) + 'a'
542             # Attach first VM to net0 and net2
543             vm0_nets = self.nets[0::2]
544             reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
545
546             name1 = self.config.loop_vm_name + str(chain_index) + 'b'
547             # Attach second VM to net1 and net2
548             vm1_nets = self.nets[1:]
549             reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
550
551             if reusable_vm0 and reusable_vm1:
552                 self.vms.extend([reusable_vm0, reusable_vm1])
553             else:
554                 vm0_port_net0 = self._create_port(vm0_nets[0])
555                 vm0_port_net2 = self._create_port(vm0_nets[1])
556
557                 vm1_port_net2 = self._create_port(vm1_nets[1])
558                 vm1_port_net1 = self._create_port(vm1_nets[0])
559
560                 self.created_ports.extend([vm0_port_net0,
561                                            vm0_port_net2,
562                                            vm1_port_net2,
563                                            vm1_port_net1])
564
565                 # order of ports is important for sections below
566                 # order of MAC addresses needs to follow order of interfaces
567                 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
568                 config_file0 = self.get_config_file(chain_index,
569                                                     self.config.generator_config.src_device.mac,
570                                                     vm1_port_net2['mac_address'])
571                 config_file1 = self.get_config_file(chain_index,
572                                                     vm0_port_net2['mac_address'],
573                                                     self.config.generator_config.dst_device.mac)
574
575                 self.vms.append(self._create_server(name0,
576                                                     [vm0_port_net0, vm0_port_net2],
577                                                     az1,
578                                                     config_file0))
579                 self.vms.append(self._create_server(name1,
580                                                     [vm1_port_net2, vm1_port_net1],
581                                                     az2,
582                                                     config_file1))
583
584         self._ensure_vms_active()
585         self.set_ports()