Change nfvbench git checkout to stable/fraser in Dockerfile
[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         if self.cred:
420             self._setup_openstack_clients()
421
422     def dispose(self, only_vm=False):
423         """
424         Deletes the created two networks and the VM.
425         """
426         for vm in self.vms:
427             if vm:
428                 if not getattr(vm, 'is_reuse', True):
429                     self.comp.delete_server(vm)
430                 else:
431                     LOG.info('Server %s not removed since it is reused', vm.name)
432
433         for port in self.created_ports:
434             self.__delete_port(port)
435
436         if not only_vm:
437             for net in self.nets:
438                 if 'is_reuse' in net and not net['is_reuse']:
439                     self.__delete_net(net)
440                 else:
441                     LOG.info('Network %s not removed since it is reused', net['name'])
442
443             if not self.flavor_type['is_reuse']:
444                 self.__delete_flavor(self.flavor_type['flavor'])
445
446
447 class EXTStageClient(BasicStageClient):
448     def setup(self):
449         super(EXTStageClient, self).setup()
450
451         # Lookup two existing networks
452         if self.cred:
453             for net_name in [self.config.external_networks.left,
454                              self.config.external_networks.right]:
455                 net = self._lookup_network(net_name)
456                 if net:
457                     self.nets.append(net)
458                 else:
459                     raise StageClientException('Existing network {} cannot be found.'.
460                                                format(net_name))
461
462
463 class PVPStageClient(BasicStageClient):
464     def get_end_port_macs(self):
465         vm_ids = [vm.id for vm in self.vms]
466         port_macs = []
467         for _index, net in enumerate(self.nets):
468             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
469             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
470         return port_macs
471
472     def setup(self):
473         super(PVPStageClient, self).setup()
474         self._setup_resources()
475
476         # Create two networks
477         nets = self.config.internal_networks
478         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
479
480         az_list = self.comp.get_enabled_az_host_list(required_count=1)
481         if not az_list:
482             raise Exception('Not enough hosts found.')
483
484         az = az_list[0]
485         self.compute_nodes.add(az)
486         for chain_index in xrange(self.config.service_chain_count):
487             name = self.config.loop_vm_name + str(chain_index)
488             reusable_vm = self.get_reusable_vm(name, self.nets, az)
489             if reusable_vm:
490                 self.vms.append(reusable_vm)
491             else:
492                 vnic_type = 'direct' if self.config.sriov else 'normal'
493                 ports = [self._create_port(net, vnic_type) for net in self.nets]
494                 config_file = self.get_config_file(chain_index,
495                                                    self.config.generator_config.src_device.mac,
496                                                    self.config.generator_config.dst_device.mac,
497                                                    ports[0]['mac_address'],
498                                                    ports[1]['mac_address'])
499                 self.created_ports.extend(ports)
500                 self.vms.append(self._create_server(name, ports, az, config_file))
501         self._ensure_vms_active()
502         self.set_ports()
503
504
505 class PVVPStageClient(BasicStageClient):
506     def get_end_port_macs(self):
507         port_macs = []
508         for index, net in enumerate(self.nets[:2]):
509             vm_ids = [vm.id for vm in self.vms[index::2]]
510             vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
511             port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
512         return port_macs
513
514     def setup(self):
515         super(PVVPStageClient, self).setup()
516         self._setup_resources()
517
518         # Create two networks
519         nets = self.config.internal_networks
520         self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
521
522         required_count = 2 if self.config.inter_node else 1
523         az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
524
525         if not az_list:
526             raise Exception('Not enough hosts found.')
527
528         az1 = az2 = az_list[0]
529         if self.config.inter_node:
530             if len(az_list) > 1:
531                 az1 = az_list[0]
532                 az2 = az_list[1]
533             else:
534                 # fallback to intra-node
535                 az1 = az2 = az_list[0]
536                 self.config.inter_node = False
537                 LOG.info('Using intra-node instead of inter-node.')
538
539         self.compute_nodes.add(az1)
540         self.compute_nodes.add(az2)
541
542         # Create loop VMs
543         for chain_index in xrange(self.config.service_chain_count):
544             name0 = self.config.loop_vm_name + str(chain_index) + 'a'
545             # Attach first VM to net0 and net2
546             vm0_nets = self.nets[0::2]
547             reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
548
549             name1 = self.config.loop_vm_name + str(chain_index) + 'b'
550             # Attach second VM to net1 and net2
551             vm1_nets = self.nets[1:]
552             reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
553
554             if reusable_vm0 and reusable_vm1:
555                 self.vms.extend([reusable_vm0, reusable_vm1])
556             else:
557                 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
558                 middle_vnic_type = 'direct' \
559                     if self.config.sriov and self.config.use_sriov_middle_net \
560                     else 'normal'
561                 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
562                 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
563
564                 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
565                 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
566
567                 self.created_ports.extend([vm0_port_net0,
568                                            vm0_port_net2,
569                                            vm1_port_net2,
570                                            vm1_port_net1])
571
572                 # order of ports is important for sections below
573                 # order of MAC addresses needs to follow order of interfaces
574                 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
575                 config_file0 = self.get_config_file(chain_index,
576                                                     self.config.generator_config.src_device.mac,
577                                                     vm1_port_net2['mac_address'],
578                                                     vm0_port_net0['mac_address'],
579                                                     vm0_port_net2['mac_address'])
580                 config_file1 = self.get_config_file(chain_index,
581                                                     vm0_port_net2['mac_address'],
582                                                     self.config.generator_config.dst_device.mac,
583                                                     vm1_port_net2['mac_address'],
584                                                     vm1_port_net1['mac_address'])
585
586                 self.vms.append(self._create_server(name0,
587                                                     [vm0_port_net0, vm0_port_net2],
588                                                     az1,
589                                                     config_file0))
590                 self.vms.append(self._create_server(name1,
591                                                     [vm1_port_net2, vm1_port_net1],
592                                                     az2,
593                                                     config_file1))
594
595         self._ensure_vms_active()
596         self.set_ports()