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