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