NFVBENCH-102 NFVBench won't work with external chain
[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, 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             msg = 'Creating instance: %s' % name
228             if az:
229                 msg += ' on %s' % az
230             LOG.info(msg)
231         else:
232             raise StageClientException('Unable to create instance: %s.' % (name))
233         return server
234
235     def _setup_resources(self):
236         # To avoid reuploading image in server mode, check whether image_name is set or not
237         if self.image_name:
238             self.image_instance = self.comp.find_image(self.image_name)
239         if self.image_instance:
240             LOG.info("Reusing image %s", self.image_name)
241         else:
242             image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
243             if self.config.vm_image_file:
244                 match = re.search(image_name_search_pattern, self.config.vm_image_file)
245                 if match:
246                     self.image_name = match.group(1)
247                     LOG.info('Using provided VM image file %s', self.config.vm_image_file)
248                 else:
249                     raise StageClientException('Provided VM image file name %s must start with '
250                                                '"nfvbenchvm-<version>"' % self.config.vm_image_file)
251             else:
252                 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
253                 for f in os.listdir(pkg_root):
254                     if re.search(image_name_search_pattern, f):
255                         self.config.vm_image_file = pkg_root + '/' + f
256                         self.image_name = f.replace('.qcow2', '')
257                         LOG.info('Found built-in VM image file %s', f)
258                         break
259                 else:
260                     raise StageClientException('Cannot find any built-in VM image file.')
261             if self.image_name:
262                 self.image_instance = self.comp.find_image(self.image_name)
263             if not self.image_instance:
264                 LOG.info('Uploading %s', self.image_name)
265                 res = self.comp.upload_image_via_url(self.image_name,
266                                                      self.config.vm_image_file)
267
268                 if not res:
269                     raise StageClientException('Error uploading image %s from %s. ABORTING.'
270                                                % (self.image_name,
271                                                   self.config.vm_image_file))
272                 LOG.info('Image %s successfully uploaded.', self.image_name)
273                 self.image_instance = self.comp.find_image(self.image_name)
274
275         self.__setup_flavor()
276
277     def __setup_flavor(self):
278         if self.flavor_type.get('flavor', False):
279             return
280
281         self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
282         if self.flavor_type['flavor']:
283             self.flavor_type['is_reuse'] = True
284         else:
285             flavor_dict = self.config.flavor
286             extra_specs = flavor_dict.pop('extra_specs', None)
287
288             self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
289                                                                  override=True,
290                                                                  **flavor_dict)
291
292             LOG.info("Flavor '%s' was created.", self.config.flavor_type)
293
294             if extra_specs:
295                 self.flavor_type['flavor'].set_keys(extra_specs)
296
297             self.flavor_type['is_reuse'] = False
298
299         if self.flavor_type['flavor'] is None:
300             raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
301                                        % self.config.flavor_type)
302
303     def __delete_flavor(self, flavor):
304         if self.comp.delete_flavor(flavor=flavor):
305             LOG.info("Flavor '%s' deleted", self.config.flavor_type)
306             self.flavor_type = {'is_reuse': False, 'flavor': None}
307         else:
308             LOG.error('Unable to delete flavor: %s', self.config.flavor_type)
309
310     def get_config_file(self, chain_index, src_mac, dst_mac, intf_mac1, intf_mac2):
311         boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
312                                         'nfvbenchvm/', self.nfvbenchvm_config_name)
313
314         with open(boot_script_file, 'r') as boot_script:
315             content = boot_script.read()
316
317         g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
318         g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
319
320         vm_config = {
321             'forwarder': self.config.vm_forwarder,
322             'intf_mac1': intf_mac1,
323             'intf_mac2': intf_mac2,
324             'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
325             'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
326             'tg_net1': self.config.traffic_generator.ip_addrs[0],
327             'tg_net2': self.config.traffic_generator.ip_addrs[1],
328             'vnf_gateway1_cidr': g1cidr,
329             'vnf_gateway2_cidr': g2cidr,
330             'tg_mac1': src_mac,
331             'tg_mac2': dst_mac
332         }
333
334         return content.format(**vm_config)
335
336     def set_ports(self):
337         """Stores all ports of NFVbench networks."""
338         nets = self.get_networks_uuids()
339         for port in self.neutron.list_ports()['ports']:
340             if port['network_id'] in nets:
341                 ports = self.ports.setdefault(port['network_id'], [])
342                 ports.append(port)
343
344     def disable_port_security(self):
345         """
346         Disable security at port level.
347         """
348         vm_ids = [vm.id for vm in self.vms]
349         for net in self.nets:
350             for port in self.ports[net['id']]:
351                 if port['device_id'] in vm_ids:
352                     try:
353                         self.neutron.update_port(port['id'], {
354                             'port': {
355                                 'security_groups': [],
356                                 'port_security_enabled': False,
357                             }
358                         })
359                         LOG.info('Security disabled on port %s', port['id'])
360                     except Exception:
361                         LOG.warning('Failed to disable port security on port %s, ignoring...',
362                                     port['id'])
363
364
365     def get_loop_vm_hostnames(self):
366         return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
367
368     def get_host_ips(self):
369         '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
370         Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
371         '''
372         if not self.host_ips:
373             #  get the hypervisor object from the host name
374             self.host_ips = [self.comp.get_hypervisor(
375                 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
376         return self.host_ips
377
378     def get_loop_vm_compute_nodes(self):
379         compute_nodes = []
380         for vm in self.vms:
381             az = getattr(vm, 'OS-EXT-AZ:availability_zone')
382             hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
383             compute_nodes.append(az + ':' + hostname)
384         return compute_nodes
385
386     def get_reusable_vm(self, name, nets):
387         servers = self._lookup_servers(name=name, nets=nets,
388                                        flavor_id=self.flavor_type['flavor'].id)
389         if servers:
390             server = servers[0]
391             LOG.info('Reusing existing server: %s', name)
392             setattr(server, 'is_reuse', True)
393             return server
394         return None
395
396     def get_networks_uuids(self):
397         """
398         Extract UUID of used networks. Order is important.
399
400         :return: list of UUIDs of created networks
401         """
402         return [net['id'] for net in self.nets]
403
404     def get_vlans(self):
405         """
406         Extract vlans of used networks. Order is important.
407
408         :return: list of UUIDs of created networks
409         """
410         vlans = []
411         for net in self.nets:
412             assert net['provider:network_type'] == 'vlan'
413             vlans.append(net['provider:segmentation_id'])
414
415         return vlans
416
417     def setup(self):
418         """
419         Creates two networks and spawn a VM which act as a loop VM connected
420         with the two networks.
421         """
422         if self.cred:
423             self._setup_openstack_clients()
424
425     def dispose(self, only_vm=False):
426         """
427         Deletes the created two networks and the VM.
428         """
429         for vm in self.vms:
430             if vm:
431                 if not getattr(vm, 'is_reuse', True):
432                     self.comp.delete_server(vm)
433                 else:
434                     LOG.info('Server %s not removed since it is reused', vm.name)
435
436         for port in self.created_ports:
437             self.__delete_port(port)
438
439         if not only_vm:
440             for net in self.nets:
441                 if 'is_reuse' in net and not net['is_reuse']:
442                     self.__delete_net(net)
443                 else:
444                     LOG.info('Network %s not removed since it is reused', net['name'])
445
446             if not self.flavor_type['is_reuse']:
447                 self.__delete_flavor(self.flavor_type['flavor'])
448
449
450 class EXTStageClient(BasicStageClient):
451     def setup(self):
452         super(EXTStageClient, self).setup()
453
454         # Lookup two existing networks
455         if self.cred:
456             for net_name in [self.config.external_networks.left,
457                              self.config.external_networks.right]:
458                 net = self._lookup_network(net_name)
459                 if net:
460                     self.nets.append(net)
461                 else:
462                     raise StageClientException('Existing network {} cannot be found.'.
463                                                format(net_name))
464
465
466 class PVPStageClient(BasicStageClient):
467     def get_end_port_macs(self):
468         vm_ids = [vm.id for vm in self.vms]
469         port_macs = []
470         for _index, net in enumerate(self.nets):
471             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
472             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
473         return port_macs
474
475     def setup(self):
476         super(PVPStageClient, self).setup()
477         self._setup_resources()
478
479         # Create two networks
480         nets = self.config.internal_networks
481         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
482
483         if self.comp.config.compute_nodes:
484             az_list = self.comp.get_enabled_az_host_list(required_count=1)
485             if not az_list:
486                 raise Exception('Not enough hosts found.')
487             az = az_list[0]
488         else:
489             az = None
490
491         for chain_index in xrange(self.config.service_chain_count):
492             name = self.config.loop_vm_name + str(chain_index)
493             server = self.get_reusable_vm(name, self.nets)
494             if server:
495                 self.vms.append(server)
496             else:
497                 vnic_type = 'direct' if self.config.sriov else 'normal'
498                 ports = [self._create_port(net, vnic_type) for net in self.nets]
499                 config_file = self.get_config_file(chain_index,
500                                                    self.config.generator_config.src_device.mac,
501                                                    self.config.generator_config.dst_device.mac,
502                                                    ports[0]['mac_address'],
503                                                    ports[1]['mac_address'])
504                 self.created_ports.extend(ports)
505                 server = self._create_server(name, ports, az, config_file)
506                 self.vms.append(server)
507
508             if chain_index == 0:
509                 # First VM, save the hypervisor name. Used in future for
510                 # maintain affinity.
511                 self._ensure_vms_active()
512                 server = self.comp.poll_server(server)
513                 az = "%s:%s" % (getattr(server, 'OS-EXT-AZ:availability_zone'),
514                                 getattr(server, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
515
516         self._ensure_vms_active()
517         self.compute_nodes = set(self.get_loop_vm_compute_nodes())
518         self.set_ports()
519
520
521 class PVVPStageClient(BasicStageClient):
522     def get_end_port_macs(self):
523         port_macs = []
524         for index, net in enumerate(self.nets[:2]):
525             vm_ids = [vm.id for vm in self.vms[index::2]]
526             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
527             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
528         return port_macs
529
530     def setup(self):
531         super(PVVPStageClient, self).setup()
532         self._setup_resources()
533
534         # Create two networks
535         nets = self.config.internal_networks
536         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
537
538         if self.comp.config.compute_nodes:
539             required_count = 2 if self.config.inter_node else 1
540             az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
541             if not az_list:
542                 raise Exception('Not enough hosts found.')
543
544             az1 = az2 = az_list[0]
545             if self.config.inter_node:
546                 if len(az_list) > 1:
547                     az1 = az_list[0]
548                     az2 = az_list[1]
549                 else:
550                     # fallback to intra-node
551                     az1 = az2 = az_list[0]
552                     self.config.inter_node = False
553                     LOG.info('Using intra-node instead of inter-node.')
554         else:
555             az1 = az2 = None
556
557         # Create loop VMs
558         for chain_index in xrange(self.config.service_chain_count):
559             name0 = self.config.loop_vm_name + str(chain_index) + 'a'
560             # Attach first VM to net0 and net2
561             vm0_nets = self.nets[0::2]
562             reusable_vm0 = self.get_reusable_vm(name0, vm0_nets)
563
564             name1 = self.config.loop_vm_name + str(chain_index) + 'b'
565             # Attach second VM to net1 and net2
566             vm1_nets = self.nets[1:]
567             reusable_vm1 = self.get_reusable_vm(name1, vm1_nets)
568
569             if reusable_vm0 and reusable_vm1:
570                 self.vms.extend([reusable_vm0, reusable_vm1])
571             else:
572                 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
573                 middle_vnic_type = 'direct' \
574                     if self.config.sriov and self.config.use_sriov_middle_net \
575                     else 'normal'
576                 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
577                 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
578
579                 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
580                 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
581
582                 self.created_ports.extend([vm0_port_net0,
583                                            vm0_port_net2,
584                                            vm1_port_net2,
585                                            vm1_port_net1])
586
587                 # order of ports is important for sections below
588                 # order of MAC addresses needs to follow order of interfaces
589                 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
590                 config_file0 = self.get_config_file(chain_index,
591                                                     self.config.generator_config.src_device.mac,
592                                                     vm1_port_net2['mac_address'],
593                                                     vm0_port_net0['mac_address'],
594                                                     vm0_port_net2['mac_address'])
595                 config_file1 = self.get_config_file(chain_index,
596                                                     vm0_port_net2['mac_address'],
597                                                     self.config.generator_config.dst_device.mac,
598                                                     vm1_port_net2['mac_address'],
599                                                     vm1_port_net1['mac_address'])
600
601                 vm1 = self._create_server(name0, [vm0_port_net0, vm0_port_net2], az1, config_file0)
602                 self.vms.append(vm1)
603                 if chain_index == 0:
604                     # First VM on first chain, save the hypervisor name. Used
605                     # in future for maintain affinity.
606                     self._ensure_vms_active()
607                     vm1 = self.comp.poll_server(vm1)
608                     az1 = "%s:%s" % (getattr(vm1, 'OS-EXT-AZ:availability_zone'),
609                                      getattr(vm1, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
610                     if not self.config.inter_node:
611                         # By default, NOVA scheduler will try first with
612                         # different hypervisor for workload balance, but when
613                         # inter-node is not configured, use the same AZ to run
614                         # intra-node test case.
615                         az2 = az1
616
617                 vm2 = self._create_server(name1, [vm1_port_net2, vm1_port_net1], az2, config_file1)
618                 self.vms.append(vm2)
619                 if chain_index == 0 and self.config.inter_node:
620                     # Second VM on first chain, save the hypervisor name. Used
621                     # in future for maintain affinity.
622                     self._ensure_vms_active()
623                     vm2 = self.comp.poll_server(vm2)
624                     az2 = "%s:%s" % (getattr(vm2, 'OS-EXT-AZ:availability_zone'),
625                                      getattr(vm2, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
626                     if az1 == az2:
627                         # Configure to run inter-node, but not enough node to run
628                         self.config.inter_node = False
629                         LOG.info('Using intra-node instead of inter-node.')
630
631         self._ensure_vms_active()
632         self.compute_nodes = set(self.get_loop_vm_compute_nodes())
633         self.set_ports()