2 # Copyright 2016 Cisco Systems, Inc. All rights reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
18 from glanceclient.v2 import client as glanceclient
20 from neutronclient.neutron import client as neutronclient
21 from novaclient.client import Client
27 class StageClientException(Exception):
31 class BasicStageClient(object):
32 """Client for spawning and accessing the VM setup"""
34 nfvbenchvm_config_name = 'nfvbenchvm.conf'
36 def __init__(self, config, cred):
38 self.image_instance = None
39 self.image_name = None
44 self.created_ports = []
46 self.compute_nodes = set([])
49 self.flavor_type = {'is_reuse': True, 'flavor': None}
52 def _ensure_vms_active(self):
53 retry_count = (self.config.check_traffic_time_sec +
54 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
55 for _ in range(retry_count):
56 for i, instance in enumerate(self.vms):
57 if instance.status == 'ACTIVE':
59 is_reuse = getattr(instance, 'is_reuse', True)
60 instance = self.comp.poll_server(instance)
61 if instance.status == 'ERROR':
62 raise StageClientException('Instance creation error: %s' %
63 instance.fault['message'])
64 if instance.status == 'ACTIVE':
65 LOG.info('Created instance: %s', instance.name)
66 self.vms[i] = instance
67 setattr(self.vms[i], 'is_reuse', is_reuse)
68 if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
70 time.sleep(self.config.generic_poll_sec)
71 raise StageClientException('Timed out waiting for VMs to spawn')
73 def _setup_openstack_clients(self):
74 self.session = self.cred.get_session()
75 nova_client = Client(2, session=self.session)
76 self.neutron = neutronclient.Client('2.0', session=self.session)
77 self.glance_client = glanceclient.Client('2',
79 self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
81 def _lookup_network(self, network_name):
82 networks = self.neutron.list_networks(name=network_name)
83 return networks['networks'][0] if networks['networks'] else None
85 def _create_net(self, name, subnet, cidr, network_type=None,
86 segmentation_id=None, physical_network=None):
87 network = self._lookup_network(name)
89 # a network of same name already exists, we need to verify it has the same
92 if network['provider:segmentation_id'] != segmentation_id:
93 raise StageClientException("Mismatch of 'segmentation_id' for reused "
94 "network '{net}'. Network has id '{seg_id1}', "
95 "configuration requires '{seg_id2}'."
97 seg_id1=network['provider:segmentation_id'],
98 seg_id2=segmentation_id))
101 if network['provider:physical_network'] != physical_network:
102 raise StageClientException("Mismatch of 'physical_network' for reused "
103 "network '{net}'. Network has '{phys1}', "
104 "configuration requires '{phys2}'."
106 phys1=network['provider:physical_network'],
107 phys2=physical_network))
109 LOG.info('Reusing existing network: ' + name)
110 network['is_reuse'] = True
116 'admin_state_up': True
121 body['network']['provider:network_type'] = network_type
123 body['network']['provider:segmentation_id'] = segmentation_id
125 body['network']['provider:physical_network'] = physical_network
127 network = self.neutron.create_network(body)['network']
132 'network_id': network['id'],
133 'enable_dhcp': False,
135 'dns_nameservers': []
138 subnet = self.neutron.create_subnet(body)['subnet']
139 # add subnet id to the network dict since it has just been added
140 network['subnets'] = [subnet['id']]
141 network['is_reuse'] = False
142 LOG.info('Created network: %s.' % name)
145 def _create_port(self, net):
148 'network_id': net['id'],
149 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
152 port = self.neutron.create_port(body)
155 def __delete_port(self, port):
157 while retry < self.config.generic_retry_count:
159 self.neutron.delete_port(port['id'])
163 time.sleep(self.config.generic_poll_sec)
164 LOG.error('Unable to delete port: %s' % (port['id']))
166 def __delete_net(self, network):
168 while retry < self.config.generic_retry_count:
170 self.neutron.delete_network(network['id'])
174 time.sleep(self.config.generic_poll_sec)
175 LOG.error('Unable to delete network: %s' % (network['name']))
177 def __get_server_az(self, server):
178 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
179 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
180 if availability_zone is None:
184 return availability_zone + ':' + host
186 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
187 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
188 networks = set(map(lambda net: net['name'], nets)) if nets else None
189 server_list = self.comp.get_server_list()
190 matching_servers = []
192 for server in server_list:
193 if name and server.name != name:
196 if az and self.__get_server_az(server) != az:
197 raise StageClientException(error_msg.format('availability zones'))
199 if flavor_id and server.flavor['id'] != flavor_id:
200 raise StageClientException(error_msg.format('flavors'))
202 if networks and not set(server.networks.keys()).issuperset(networks):
203 raise StageClientException(error_msg.format('networks'))
205 if server.status != "ACTIVE":
206 raise StageClientException(error_msg.format('state'))
209 matching_servers.append(server)
211 return matching_servers
213 def _create_server(self, name, ports, az, nfvbenchvm_config):
214 port_ids = map(lambda port: {'port-id': port['id']}, ports)
215 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
216 server = self.comp.create_server(name,
218 self.flavor_type['flavor'],
225 files={nfvbenchvm_config_location: nfvbenchvm_config})
227 setattr(server, 'is_reuse', False)
228 LOG.info('Creating instance: %s on %s' % (name, az))
230 raise StageClientException('Unable to create instance: %s.' % (name))
233 def _setup_resources(self):
234 # To avoid reuploading image in server mode, check whether image_name is set or not
236 self.image_instance = self.comp.find_image(self.image_name)
237 if self.image_instance:
238 LOG.info("Reusing image %s" % self.image_name)
240 image_name_search_pattern = '(nfvbenchvm-\d+(\.\d+)*).qcow2'
241 if self.config.vm_image_file:
242 match = re.search(image_name_search_pattern, self.config.vm_image_file)
244 self.image_name = match.group(1)
245 LOG.info('Using provided VM image file %s' % self.config.vm_image_file)
247 raise StageClientException('Provided VM image file name %s must start with '
248 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
250 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
251 for f in os.listdir(pkg_root):
252 if re.search(image_name_search_pattern, f):
253 self.config.vm_image_file = pkg_root + '/' + f
254 self.image_name = f.replace('.qcow2', '')
255 LOG.info('Found built-in VM image file %s' % f)
258 raise StageClientException('Cannot find any built-in VM image file.')
260 self.image_instance = self.comp.find_image(self.image_name)
261 if not self.image_instance:
262 LOG.info('Uploading %s'
264 res = self.comp.upload_image_via_url(self.image_name,
265 self.config.vm_image_file)
268 raise StageClientException('Error uploading image %s from %s. ABORTING.'
270 self.config.vm_image_file))
271 LOG.info('Image %s successfully uploaded.' % self.image_name)
272 self.image_instance = self.comp.find_image(self.image_name)
274 self.__setup_flavor()
276 def __setup_flavor(self):
277 if self.flavor_type.get('flavor', False):
280 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
281 if self.flavor_type['flavor']:
282 self.flavor_type['is_reuse'] = True
284 flavor_dict = self.config.flavor
285 extra_specs = flavor_dict.pop('extra_specs', None)
287 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
291 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
294 self.flavor_type['flavor'].set_keys(extra_specs)
296 self.flavor_type['is_reuse'] = False
298 if self.flavor_type['flavor'] is None:
299 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
300 % self.config.flavor_type)
302 def __delete_flavor(self, flavor):
303 if self.comp.delete_flavor(flavor=flavor):
304 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
305 self.flavor_type = {'is_reuse': False, 'flavor': None}
307 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
309 def get_config_file(self, chain_index, src_mac, dst_mac):
310 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
311 'nfvbenchvm/', self.nfvbenchvm_config_name)
313 with open(boot_script_file, 'r') as boot_script:
314 content = boot_script.read()
316 g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
317 g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
320 'forwarder': self.config.vm_forwarder,
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,
331 return content.format(**vm_config)
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'], [])
341 def disable_port_security(self):
343 Disable security at port level.
345 vm_ids = map(lambda vm: vm.id, 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'], {
351 'security_groups': [],
352 'port_security_enabled': False,
355 LOG.info('Security disabled on port {}'.format(port['id']))
357 def get_loop_vm_hostnames(self):
358 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
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)
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
371 def get_loop_vm_compute_nodes(self):
374 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
375 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
376 compute_nodes.append(az + ':' + hostname)
379 def get_reusable_vm(self, name, nets, az):
380 servers = self._lookup_servers(name=name, nets=nets, az=az,
381 flavor_id=self.flavor_type['flavor'].id)
384 LOG.info('Reusing existing server: ' + name)
385 setattr(server, 'is_reuse', True)
390 def get_networks_uuids(self):
392 Extract UUID of used networks. Order is important.
394 :return: list of UUIDs of created networks
396 return [net['id'] for net in self.nets]
400 Extract vlans of used networks. Order is important.
402 :return: list of UUIDs of created networks
405 for net in self.nets:
406 assert (net['provider:network_type'] == 'vlan')
407 vlans.append(net['provider:segmentation_id'])
413 Creates two networks and spawn a VM which act as a loop VM connected
414 with the two networks.
416 self._setup_openstack_clients()
418 def dispose(self, only_vm=False):
420 Deletes the created two networks and the VM.
424 if not getattr(vm, 'is_reuse', True):
425 self.comp.delete_server(vm)
427 LOG.info('Server %s not removed since it is reused' % vm.name)
429 for port in self.created_ports:
430 self.__delete_port(port)
433 for net in self.nets:
434 if 'is_reuse' in net and not net['is_reuse']:
435 self.__delete_net(net)
437 LOG.info('Network %s not removed since it is reused' % (net['name']))
439 if not self.flavor_type['is_reuse']:
440 self.__delete_flavor(self.flavor_type['flavor'])
443 class EXTStageClient(BasicStageClient):
444 def __init__(self, config, cred):
445 super(EXTStageClient, self).__init__(config, cred)
448 super(EXTStageClient, self).setup()
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)
454 self.nets.append(net)
456 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
459 class PVPStageClient(BasicStageClient):
460 def __init__(self, config, cred):
461 super(PVPStageClient, self).__init__(config, cred)
463 def get_end_port_macs(self):
464 vm_ids = map(lambda vm: vm.id, self.vms)
466 for index, net in enumerate(self.nets):
467 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
468 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
472 super(PVPStageClient, self).setup()
473 self._setup_resources()
475 # Create two networks
476 nets = self.config.internal_networks
477 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
479 az_list = self.comp.get_enabled_az_host_list(required_count=1)
481 raise Exception('Not enough hosts found.')
484 self.compute_nodes.add(az)
485 for chain_index in xrange(self.config.service_chain_count):
486 name = self.config.loop_vm_name + str(chain_index)
487 reusable_vm = self.get_reusable_vm(name, self.nets, az)
489 self.vms.append(reusable_vm)
491 config_file = self.get_config_file(chain_index,
492 self.config.generator_config.src_device.mac,
493 self.config.generator_config.dst_device.mac)
495 ports = [self._create_port(net) for net in self.nets]
496 self.created_ports.extend(ports)
497 self.vms.append(self._create_server(name, ports, az, config_file))
498 self._ensure_vms_active()
502 class PVVPStageClient(BasicStageClient):
503 def __init__(self, config, cred):
504 super(PVVPStageClient, self).__init__(config, cred)
506 def get_end_port_macs(self):
508 for index, net in enumerate(self.nets[:2]):
509 vm_ids = map(lambda vm: vm.id, 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])
515 super(PVVPStageClient, self).setup()
516 self._setup_resources()
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]])
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)
526 raise Exception('Not enough hosts found.')
528 az1 = az2 = az_list[0]
529 if self.config.inter_node:
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.')
539 self.compute_nodes.add(az1)
540 self.compute_nodes.add(az2)
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)
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)
554 if reusable_vm0 and reusable_vm1:
555 self.vms.extend([reusable_vm0, reusable_vm1])
557 vm0_port_net0 = self._create_port(vm0_nets[0])
558 vm0_port_net2 = self._create_port(vm0_nets[1])
560 vm1_port_net2 = self._create_port(vm1_nets[1])
561 vm1_port_net1 = self._create_port(vm1_nets[0])
563 self.created_ports.extend([vm0_port_net0,
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 config_file1 = self.get_config_file(chain_index,
575 vm0_port_net2['mac_address'],
576 self.config.generator_config.dst_device.mac)
578 self.vms.append(self._create_server(name0,
579 [vm0_port_net0, vm0_port_net2],
582 self.vms.append(self._create_server(name1,
583 [vm1_port_net2, vm1_port_net1],
587 self._ensure_vms_active()