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