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