57b15ee102f7daa4e4c0500c2743cde7bbbefd38
[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, vnic_type='normal'):
149         body = {
150             "port": {
151                 'network_id': net['id'],
152                 'binding:vnic_type': vnic_type
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, intf_mac1, intf_mac2):
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             'intf_mac1': intf_mac1,
321             'intf_mac2': intf_mac2,
322             'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
323             'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
324             'tg_net1': self.config.traffic_generator.ip_addrs[0],
325             'tg_net2': self.config.traffic_generator.ip_addrs[1],
326             'vnf_gateway1_cidr': g1cidr,
327             'vnf_gateway2_cidr': g2cidr,
328             'tg_mac1': src_mac,
329             'tg_mac2': dst_mac
330         }
331
332         return content.format(**vm_config)
333
334     def set_ports(self):
335         """Stores all ports of NFVbench networks."""
336         nets = self.get_networks_uuids()
337         for port in self.neutron.list_ports()['ports']:
338             if port['network_id'] in nets:
339                 ports = self.ports.setdefault(port['network_id'], [])
340                 ports.append(port)
341
342     def disable_port_security(self):
343         """
344         Disable security at port level.
345         """
346         vm_ids = [vm.id for vm in self.vms]
347         for net in self.nets:
348             for port in self.ports[net['id']]:
349                 if port['device_id'] in vm_ids:
350                     self.neutron.update_port(port['id'], {
351                         'port': {
352                             'security_groups': [],
353                             'port_security_enabled': False,
354                         }
355                     })
356                     LOG.info('Security disabled on port %s', port['id'])
357
358     def get_loop_vm_hostnames(self):
359         return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
360
361     def get_host_ips(self):
362         '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
363         Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
364         '''
365         if not self.host_ips:
366             #  get the hypervisor object from the host name
367             self.host_ips = [self.comp.get_hypervisor(
368                 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
369         return self.host_ips
370
371     def get_loop_vm_compute_nodes(self):
372         compute_nodes = []
373         for vm in self.vms:
374             az = getattr(vm, 'OS-EXT-AZ:availability_zone')
375             hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
376             compute_nodes.append(az + ':' + hostname)
377         return compute_nodes
378
379     def get_reusable_vm(self, name, nets, az):
380         servers = self._lookup_servers(name=name, nets=nets, az=az,
381                                        flavor_id=self.flavor_type['flavor'].id)
382         if servers:
383             server = servers[0]
384             LOG.info('Reusing existing server: %s', name)
385             setattr(server, 'is_reuse', True)
386             return server
387         return None
388
389     def get_networks_uuids(self):
390         """
391         Extract UUID of used networks. Order is important.
392
393         :return: list of UUIDs of created networks
394         """
395         return [net['id'] for net in self.nets]
396
397     def get_vlans(self):
398         """
399         Extract vlans of used networks. Order is important.
400
401         :return: list of UUIDs of created networks
402         """
403         vlans = []
404         for net in self.nets:
405             assert net['provider:network_type'] == 'vlan'
406             vlans.append(net['provider:segmentation_id'])
407
408         return vlans
409
410     def setup(self):
411         """
412         Creates two networks and spawn a VM which act as a loop VM connected
413         with the two networks.
414         """
415         self._setup_openstack_clients()
416
417     def dispose(self, only_vm=False):
418         """
419         Deletes the created two networks and the VM.
420         """
421         for vm in self.vms:
422             if vm:
423                 if not getattr(vm, 'is_reuse', True):
424                     self.comp.delete_server(vm)
425                 else:
426                     LOG.info('Server %s not removed since it is reused', vm.name)
427
428         for port in self.created_ports:
429             self.__delete_port(port)
430
431         if not only_vm:
432             for net in self.nets:
433                 if 'is_reuse' in net and not net['is_reuse']:
434                     self.__delete_net(net)
435                 else:
436                     LOG.info('Network %s not removed since it is reused', net['name'])
437
438             if not self.flavor_type['is_reuse']:
439                 self.__delete_flavor(self.flavor_type['flavor'])
440
441
442 class EXTStageClient(BasicStageClient):
443     def setup(self):
444         super(EXTStageClient, self).setup()
445
446         # Lookup two existing networks
447         for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
448             net = self._lookup_network(net_name)
449             if net:
450                 self.nets.append(net)
451             else:
452                 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
453
454
455 class PVPStageClient(BasicStageClient):
456     def get_end_port_macs(self):
457         vm_ids = [vm.id for vm in self.vms]
458         port_macs = []
459         for _index, net in enumerate(self.nets):
460             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
461             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
462         return port_macs
463
464     def setup(self):
465         super(PVPStageClient, self).setup()
466         self._setup_resources()
467
468         # Create two networks
469         nets = self.config.internal_networks
470         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
471
472         az_list = self.comp.get_enabled_az_host_list(required_count=1)
473         if not az_list:
474             raise Exception('Not enough hosts found.')
475
476         az = az_list[0]
477         self.compute_nodes.add(az)
478         for chain_index in xrange(self.config.service_chain_count):
479             name = self.config.loop_vm_name + str(chain_index)
480             reusable_vm = self.get_reusable_vm(name, self.nets, az)
481             if reusable_vm:
482                 self.vms.append(reusable_vm)
483             else:
484                 vnic_type = 'direct' if self.config.sriov else 'normal'
485                 ports = [self._create_port(net, vnic_type) for net in self.nets]
486                 config_file = self.get_config_file(chain_index,
487                                                    self.config.generator_config.src_device.mac,
488                                                    self.config.generator_config.dst_device.mac,
489                                                    ports[0]['mac_address'],
490                                                    ports[1]['mac_address'])
491                 self.created_ports.extend(ports)
492                 self.vms.append(self._create_server(name, ports, az, config_file))
493         self._ensure_vms_active()
494         self.set_ports()
495
496
497 class PVVPStageClient(BasicStageClient):
498     def get_end_port_macs(self):
499         port_macs = []
500         for index, net in enumerate(self.nets[:2]):
501             vm_ids = [vm.id for vm in self.vms[index::2]]
502             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
503             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
504         return port_macs
505
506     def setup(self):
507         super(PVVPStageClient, self).setup()
508         self._setup_resources()
509
510         # Create two networks
511         nets = self.config.internal_networks
512         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
513
514         required_count = 2 if self.config.inter_node else 1
515         az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
516
517         if not az_list:
518             raise Exception('Not enough hosts found.')
519
520         az1 = az2 = az_list[0]
521         if self.config.inter_node:
522             if len(az_list) > 1:
523                 az1 = az_list[0]
524                 az2 = az_list[1]
525             else:
526                 # fallback to intra-node
527                 az1 = az2 = az_list[0]
528                 self.config.inter_node = False
529                 LOG.info('Using intra-node instead of inter-node.')
530
531         self.compute_nodes.add(az1)
532         self.compute_nodes.add(az2)
533
534         # Create loop VMs
535         for chain_index in xrange(self.config.service_chain_count):
536             name0 = self.config.loop_vm_name + str(chain_index) + 'a'
537             # Attach first VM to net0 and net2
538             vm0_nets = self.nets[0::2]
539             reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
540
541             name1 = self.config.loop_vm_name + str(chain_index) + 'b'
542             # Attach second VM to net1 and net2
543             vm1_nets = self.nets[1:]
544             reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
545
546             if reusable_vm0 and reusable_vm1:
547                 self.vms.extend([reusable_vm0, reusable_vm1])
548             else:
549                 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
550                 middle_vnic_type = 'direct' \
551                     if self.config.sriov and self.config.use_sriov_middle_net \
552                     else 'normal'
553                 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
554                 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
555
556                 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
557                 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
558
559                 self.created_ports.extend([vm0_port_net0,
560                                            vm0_port_net2,
561                                            vm1_port_net2,
562                                            vm1_port_net1])
563
564                 # order of ports is important for sections below
565                 # order of MAC addresses needs to follow order of interfaces
566                 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
567                 config_file0 = self.get_config_file(chain_index,
568                                                     self.config.generator_config.src_device.mac,
569                                                     vm1_port_net2['mac_address'],
570                                                     vm0_port_net0['mac_address'],
571                                                     vm0_port_net2['mac_address'])
572                 config_file1 = self.get_config_file(chain_index,
573                                                     vm0_port_net2['mac_address'],
574                                                     self.config.generator_config.dst_device.mac,
575                                                     vm1_port_net2['mac_address'],
576                                                     vm1_port_net1['mac_address'])
577
578                 self.vms.append(self._create_server(name0,
579                                                     [vm0_port_net0, vm0_port_net2],
580                                                     az1,
581                                                     config_file0))
582                 self.vms.append(self._create_server(name1,
583                                                     [vm1_port_net2, vm1_port_net1],
584                                                     az2,
585                                                     config_file1))
586
587         self._ensure_vms_active()
588         self.set_ports()