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
26 class StageClientException(Exception):
30 class BasicStageClient(object):
31 """Client for spawning and accessing the VM setup"""
33 nfvbenchvm_config_name = 'nfvbenchvm.conf'
35 def __init__(self, config, cred):
37 self.image_instance = None
42 self.created_ports = []
44 self.compute_nodes = set([])
47 self.flavor_type = {'is_reuse': True, 'flavor': None}
50 def _ensure_vms_active(self):
51 retry_count = (self.config.check_traffic_time_sec +
52 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
53 for _ in range(retry_count):
54 for i, instance in enumerate(self.vms):
55 if instance.status == 'ACTIVE':
57 is_reuse = getattr(instance, 'is_reuse', True)
58 instance = self.comp.poll_server(instance)
59 if instance.status == 'ERROR':
60 raise StageClientException('Instance creation error: %s' %
61 instance.fault['message'])
62 if instance.status == 'ACTIVE':
63 LOG.info('Created instance: %s', instance.name)
64 self.vms[i] = instance
65 setattr(self.vms[i], 'is_reuse', is_reuse)
66 if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
68 time.sleep(self.config.generic_poll_sec)
69 raise StageClientException('Timed out waiting for VMs to spawn')
71 def _setup_openstack_clients(self):
72 self.session = self.cred.get_session()
73 nova_client = Client(2, session=self.session)
74 self.neutron = neutronclient.Client('2.0', session=self.session)
75 self.glance_client = glanceclient.Client('2',
77 self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
79 def _lookup_network(self, network_name):
80 networks = self.neutron.list_networks(name=network_name)
81 return networks['networks'][0] if networks['networks'] else None
83 def _create_net(self, name, subnet, cidr, network_type=None,
84 segmentation_id=None, physical_network=None):
85 network = self._lookup_network(name)
87 # a network of same name already exists, we need to verify it has the same
90 if network['provider:segmentation_id'] != segmentation_id:
91 raise StageClientException("Mismatch of 'segmentation_id' for reused "
92 "network '{net}'. Network has id '{seg_id1}', "
93 "configuration requires '{seg_id2}'."
95 seg_id1=network['provider:segmentation_id'],
96 seg_id2=segmentation_id))
99 if network['provider:physical_network'] != physical_network:
100 raise StageClientException("Mismatch of 'physical_network' for reused "
101 "network '{net}'. Network has '{phys1}', "
102 "configuration requires '{phys2}'."
104 phys1=network['provider:physical_network'],
105 phys2=physical_network))
107 LOG.info('Reusing existing network: ' + name)
108 network['is_reuse'] = True
114 'admin_state_up': True
119 body['network']['provider:network_type'] = network_type
121 body['network']['provider:segmentation_id'] = segmentation_id
123 body['network']['provider:physical_network'] = physical_network
125 network = self.neutron.create_network(body)['network']
130 'network_id': network['id'],
131 'enable_dhcp': False,
133 'dns_nameservers': []
136 subnet = self.neutron.create_subnet(body)['subnet']
137 # add subnet id to the network dict since it has just been added
138 network['subnets'] = [subnet['id']]
139 network['is_reuse'] = False
140 LOG.info('Created network: %s.' % name)
143 def _create_port(self, net):
146 'network_id': net['id'],
147 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
150 port = self.neutron.create_port(body)
153 def __delete_port(self, port):
155 while retry < self.config.generic_retry_count:
157 self.neutron.delete_port(port['id'])
161 time.sleep(self.config.generic_poll_sec)
162 LOG.error('Unable to delete port: %s' % (port['id']))
164 def __delete_net(self, network):
166 while retry < self.config.generic_retry_count:
168 self.neutron.delete_network(network['id'])
172 time.sleep(self.config.generic_poll_sec)
173 LOG.error('Unable to delete network: %s' % (network['name']))
175 def __get_server_az(self, server):
176 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
177 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
178 if availability_zone is None:
182 return availability_zone + ':' + host
184 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
185 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
186 networks = set(map(lambda net: net['name'], nets)) if nets else None
187 server_list = self.comp.get_server_list()
188 matching_servers = []
190 for server in server_list:
191 if name and server.name != name:
194 if az and self.__get_server_az(server) != az:
195 raise StageClientException(error_msg.format('availability zones'))
197 if flavor_id and server.flavor['id'] != flavor_id:
198 raise StageClientException(error_msg.format('flavors'))
200 if networks and not set(server.networks.keys()).issuperset(networks):
201 raise StageClientException(error_msg.format('networks'))
203 if server.status != "ACTIVE":
204 raise StageClientException(error_msg.format('state'))
207 matching_servers.append(server)
209 return matching_servers
211 def _create_server(self, name, ports, az, nfvbenchvm_config):
212 port_ids = map(lambda port: {'port-id': port['id']}, ports)
213 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
214 server = self.comp.create_server(name,
216 self.flavor_type['flavor'],
223 files={nfvbenchvm_config_location: nfvbenchvm_config})
225 setattr(server, 'is_reuse', False)
226 LOG.info('Creating instance: %s on %s' % (name, az))
228 raise StageClientException('Unable to create instance: %s.' % (name))
231 def _setup_resources(self):
232 if not self.image_instance:
233 self.image_instance = self.comp.find_image(self.config.image_name)
234 if self.image_instance is None:
235 if self.config.vm_image_file:
236 LOG.info('%s: image for VM not found, trying to upload it ...'
237 % self.config.image_name)
238 res = self.comp.upload_image_via_url(self.config.image_name,
239 self.config.vm_image_file)
242 raise StageClientException('Error uploading image %s from %s. ABORTING.'
243 % (self.config.image_name,
244 self.config.vm_image_file))
245 self.image_instance = self.comp.find_image(self.config.image_name)
247 raise StageClientException('%s: image to launch VM not found. ABORTING.'
248 % self.config.image_name)
250 LOG.info('Found image %s to launch VM' % self.config.image_name)
252 self.__setup_flavor()
254 def __setup_flavor(self):
255 if self.flavor_type.get('flavor', False):
258 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
259 if self.flavor_type['flavor']:
260 self.flavor_type['is_reuse'] = True
262 flavor_dict = self.config.flavor
263 extra_specs = flavor_dict.pop('extra_specs', None)
265 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
269 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
272 self.flavor_type['flavor'].set_keys(extra_specs)
274 self.flavor_type['is_reuse'] = False
276 if self.flavor_type['flavor'] is None:
277 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
278 % self.config.flavor_type)
280 def __delete_flavor(self, flavor):
281 if self.comp.delete_flavor(flavor=flavor):
282 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
283 self.flavor_type = {'is_reuse': False, 'flavor': None}
285 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
287 def get_config_file(self, chain_index, src_mac, dst_mac):
288 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
289 'nfvbenchvm/', self.nfvbenchvm_config_name)
291 with open(boot_script_file, 'r') as boot_script:
292 content = boot_script.read()
294 g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
295 g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
298 'forwarder': self.config.vm_forwarder,
299 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
300 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
301 'tg_net1': self.config.traffic_generator.ip_addrs[0],
302 'tg_net2': self.config.traffic_generator.ip_addrs[1],
303 'vnf_gateway1_cidr': g1cidr,
304 'vnf_gateway2_cidr': g2cidr,
309 return content.format(**vm_config)
312 """Stores all ports of NFVbench networks."""
313 nets = self.get_networks_uuids()
314 for port in self.neutron.list_ports()['ports']:
315 if port['network_id'] in nets:
316 ports = self.ports.setdefault(port['network_id'], [])
319 def disable_port_security(self):
321 Disable security at port level.
323 vm_ids = map(lambda vm: vm.id, self.vms)
324 for net in self.nets:
325 for port in self.ports[net['id']]:
326 if port['device_id'] in vm_ids:
327 self.neutron.update_port(port['id'], {
329 'security_groups': [],
330 'port_security_enabled': False,
333 LOG.info('Security disabled on port {}'.format(port['id']))
335 def get_loop_vm_hostnames(self):
336 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
338 def get_host_ips(self):
339 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
340 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
342 if not self.host_ips:
343 # get the hypervisor object from the host name
344 self.host_ips = [self.comp.get_hypervisor(
345 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
349 def get_loop_vm_compute_nodes(self):
352 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
353 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
354 compute_nodes.append(az + ':' + hostname)
357 def get_reusable_vm(self, name, nets, az):
358 servers = self._lookup_servers(name=name, nets=nets, az=az,
359 flavor_id=self.flavor_type['flavor'].id)
362 LOG.info('Reusing existing server: ' + name)
363 setattr(server, 'is_reuse', True)
368 def get_networks_uuids(self):
370 Extract UUID of used networks. Order is important.
372 :return: list of UUIDs of created networks
374 return [net['id'] for net in self.nets]
378 Extract vlans of used networks. Order is important.
380 :return: list of UUIDs of created networks
383 for net in self.nets:
384 assert(net['provider:network_type'] == 'vlan')
385 vlans.append(net['provider:segmentation_id'])
391 Creates two networks and spawn a VM which act as a loop VM connected
392 with the two networks.
394 self._setup_openstack_clients()
396 def dispose(self, only_vm=False):
398 Deletes the created two networks and the VM.
402 if not getattr(vm, 'is_reuse', True):
403 self.comp.delete_server(vm)
405 LOG.info('Server %s not removed since it is reused' % vm.name)
407 for port in self.created_ports:
408 self.__delete_port(port)
411 for net in self.nets:
412 if 'is_reuse' in net and not net['is_reuse']:
413 self.__delete_net(net)
415 LOG.info('Network %s not removed since it is reused' % (net['name']))
417 if not self.flavor_type['is_reuse']:
418 self.__delete_flavor(self.flavor_type['flavor'])
421 class EXTStageClient(BasicStageClient):
423 def __init__(self, config, cred):
424 super(EXTStageClient, self).__init__(config, cred)
427 super(EXTStageClient, self).setup()
429 # Lookup two existing networks
430 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
431 net = self._lookup_network(net_name)
433 self.nets.append(net)
435 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
438 class PVPStageClient(BasicStageClient):
440 def __init__(self, config, cred):
441 super(PVPStageClient, self).__init__(config, cred)
443 def get_end_port_macs(self):
444 vm_ids = map(lambda vm: vm.id, self.vms)
446 for index, net in enumerate(self.nets):
447 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
448 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
452 super(PVPStageClient, self).setup()
453 self._setup_resources()
455 # Create two networks
456 nets = self.config.internal_networks
457 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
459 az_list = self.comp.get_enabled_az_host_list(required_count=1)
461 raise Exception('Not enough hosts found.')
464 self.compute_nodes.add(az)
465 for chain_index in xrange(self.config.service_chain_count):
466 name = self.config.loop_vm_name + str(chain_index)
467 reusable_vm = self.get_reusable_vm(name, self.nets, az)
469 self.vms.append(reusable_vm)
471 config_file = self.get_config_file(chain_index,
472 self.config.generator_config.src_device.mac,
473 self.config.generator_config.dst_device.mac)
475 ports = [self._create_port(net) for net in self.nets]
476 self.created_ports.extend(ports)
477 self.vms.append(self._create_server(name, ports, az, config_file))
478 self._ensure_vms_active()
482 class PVVPStageClient(BasicStageClient):
484 def __init__(self, config, cred):
485 super(PVVPStageClient, self).__init__(config, cred)
487 def get_end_port_macs(self):
489 for index, net in enumerate(self.nets[:2]):
490 vm_ids = map(lambda vm: vm.id, self.vms[index::2])
491 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
492 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
496 super(PVVPStageClient, self).setup()
497 self._setup_resources()
499 # Create two networks
500 nets = self.config.internal_networks
501 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
503 required_count = 2 if self.config.inter_node else 1
504 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
507 raise Exception('Not enough hosts found.')
509 az1 = az2 = az_list[0]
510 if self.config.inter_node:
515 # fallback to intra-node
516 az1 = az2 = az_list[0]
517 self.config.inter_node = False
518 LOG.info('Using intra-node instead of inter-node.')
520 self.compute_nodes.add(az1)
521 self.compute_nodes.add(az2)
524 for chain_index in xrange(self.config.service_chain_count):
525 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
526 # Attach first VM to net0 and net2
527 vm0_nets = self.nets[0::2]
528 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
530 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
531 # Attach second VM to net1 and net2
532 vm1_nets = self.nets[1:]
533 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
535 if reusable_vm0 and reusable_vm1:
536 self.vms.extend([reusable_vm0, reusable_vm1])
538 vm0_port_net0 = self._create_port(vm0_nets[0])
539 vm0_port_net2 = self._create_port(vm0_nets[1])
541 vm1_port_net2 = self._create_port(vm1_nets[1])
542 vm1_port_net1 = self._create_port(vm1_nets[0])
544 self.created_ports.extend([vm0_port_net0,
549 # order of ports is important for sections below
550 # order of MAC addresses needs to follow order of interfaces
551 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
552 config_file0 = self.get_config_file(chain_index,
553 self.config.generator_config.src_device.mac,
554 vm1_port_net2['mac_address'])
555 config_file1 = self.get_config_file(chain_index,
556 vm0_port_net2['mac_address'],
557 self.config.generator_config.dst_device.mac)
559 self.vms.append(self._create_server(name0,
560 [vm0_port_net0, vm0_port_net2],
563 self.vms.append(self._create_server(name1,
564 [vm1_port_net2, vm1_port_net1],
568 self._ensure_vms_active()