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