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