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