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 for _ in range(self.config.generic_retry_count):
52 for i, instance in enumerate(self.vms):
53 if instance.status == 'ACTIVE':
55 is_reuse = getattr(instance, 'is_reuse', True)
56 instance = self.comp.poll_server(instance)
57 if instance.status == 'ERROR':
58 raise StageClientException('Instance creation error: %s' %
59 instance.fault['message'])
60 if instance.status == 'ACTIVE':
61 LOG.info('Created instance: %s', instance.name)
62 self.vms[i] = instance
63 setattr(self.vms[i], 'is_reuse', is_reuse)
64 if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
66 time.sleep(self.config.generic_poll_sec)
67 raise StageClientException('Timed out waiting for VMs to spawn')
69 def _setup_openstack_clients(self):
70 self.session = self.cred.get_session()
71 nova_client = Client(2, session=self.session)
72 self.neutron = neutronclient.Client('2.0', session=self.session)
73 self.glance_client = glanceclient.Client('2',
75 self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
77 def _lookup_network(self, network_name):
78 networks = self.neutron.list_networks(name=network_name)
79 return networks['networks'][0] if networks['networks'] else None
81 def _create_net(self, name, subnet, cidr, network_type=None, segmentation_id=None):
82 network = self._lookup_network(name)
84 phys_net = self.config.internal_networks.physical_network
85 if segmentation_id is not None and phys_net is not None:
86 if network['provider:segmentation_id'] != segmentation_id:
87 raise StageClientException("Mismatch of 'segmentation_id' for reused "
88 "network '{net}'. Network has id '{seg_id1}', "
89 "configuration requires '{seg_id2}'."
91 seg_id1=network['provider:segmentation_id'],
92 seg_id2=segmentation_id))
94 if network['provider:physical_network'] != phys_net:
95 raise StageClientException("Mismatch of 'physical_network' for reused "
96 "network '{net}'. Network has '{phys1}', "
97 "configuration requires '{phys2}'."
99 phys1=network['provider:physical_network'],
102 LOG.info('Reusing existing network: ' + name)
103 network['is_reuse'] = True
109 'admin_state_up': True
114 body['network']['provider:network_type'] = network_type
115 phys_net = self.config.internal_networks.physical_network
116 if segmentation_id is not None and phys_net is not None:
117 body['network']['provider:segmentation_id'] = segmentation_id
118 body['network']['provider:physical_network'] = phys_net
120 network = self.neutron.create_network(body)['network']
125 'network_id': network['id'],
126 'enable_dhcp': False,
128 'dns_nameservers': []
131 subnet = self.neutron.create_subnet(body)['subnet']
132 # add subnet id to the network dict since it has just been added
133 network['subnets'] = [subnet['id']]
134 network['is_reuse'] = False
135 LOG.info('Created network: %s.' % name)
138 def _create_port(self, net):
141 'network_id': net['id'],
142 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
145 port = self.neutron.create_port(body)
148 def __delete_port(self, port):
150 while retry < self.config.generic_retry_count:
152 self.neutron.delete_port(port['id'])
156 time.sleep(self.config.generic_poll_sec)
157 LOG.error('Unable to delete port: %s' % (port['id']))
159 def __delete_net(self, network):
161 while retry < self.config.generic_retry_count:
163 self.neutron.delete_network(network['id'])
167 time.sleep(self.config.generic_poll_sec)
168 LOG.error('Unable to delete network: %s' % (network['name']))
170 def __get_server_az(self, server):
171 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
172 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
173 if availability_zone is None:
177 return availability_zone + ':' + host
179 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
180 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
181 networks = set(map(lambda net: net['name'], nets)) if nets else None
182 server_list = self.comp.get_server_list()
183 matching_servers = []
185 for server in server_list:
186 if name and server.name != name:
189 if az and self.__get_server_az(server) != az:
190 raise StageClientException(error_msg.format('availability zones'))
192 if flavor_id and server.flavor['id'] != flavor_id:
193 raise StageClientException(error_msg.format('flavors'))
195 if networks and not set(server.networks.keys()).issuperset(networks):
196 raise StageClientException(error_msg.format('networks'))
198 if server.status != "ACTIVE":
199 raise StageClientException(error_msg.format('state'))
202 matching_servers.append(server)
204 return matching_servers
206 def _create_server(self, name, ports, az, nfvbenchvm_config):
207 port_ids = map(lambda port: {'port-id': port['id']}, ports)
208 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
209 server = self.comp.create_server(name,
211 self.flavor_type['flavor'],
218 files={nfvbenchvm_config_location: nfvbenchvm_config})
220 setattr(server, 'is_reuse', False)
221 LOG.info('Creating instance: %s on %s' % (name, az))
223 raise StageClientException('Unable to create instance: %s.' % (name))
226 def _setup_resources(self):
227 if not self.image_instance:
228 self.image_instance = self.comp.find_image(self.config.image_name)
229 if self.image_instance is None:
230 if self.config.vm_image_file:
231 LOG.info('%s: image for VM not found, trying to upload it ...'
232 % self.config.image_name)
233 res = self.comp.upload_image_via_url(self.config.image_name,
234 self.config.vm_image_file)
237 raise StageClientException('Error uploading image %s from %s. ABORTING.'
238 % (self.config.image_name,
239 self.config.vm_image_file))
240 self.image_instance = self.comp.find_image(self.config.image_name)
242 raise StageClientException('%s: image to launch VM not found. ABORTING.'
243 % self.config.image_name)
245 LOG.info('Found image %s to launch VM' % self.config.image_name)
247 self.__setup_flavor()
249 def __setup_flavor(self):
250 if self.flavor_type.get('flavor', False):
253 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
254 if self.flavor_type['flavor']:
255 self.flavor_type['is_reuse'] = True
257 flavor_dict = self.config.flavor
258 extra_specs = flavor_dict.pop('extra_specs', None)
260 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
264 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
267 self.flavor_type['flavor'].set_keys(extra_specs)
269 self.flavor_type['is_reuse'] = False
271 if self.flavor_type['flavor'] is None:
272 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
273 % self.config.flavor_type)
275 def __delete_flavor(self, flavor):
276 if self.comp.delete_flavor(flavor=flavor):
277 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
278 self.flavor_type = {'is_reuse': False, 'flavor': None}
280 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
282 def get_config_file(self, chain_index, src_mac, dst_mac):
283 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
284 'nfvbenchvm/', self.nfvbenchvm_config_name)
286 with open(boot_script_file, 'r') as boot_script:
287 content = boot_script.read()
289 g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
290 g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
293 'forwarder': self.config.vm_forwarder,
294 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
295 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
296 'tg_net1': self.config.traffic_generator.ip_addrs[0],
297 'tg_net2': self.config.traffic_generator.ip_addrs[1],
298 'vnf_gateway1_cidr': g1cidr,
299 'vnf_gateway2_cidr': g2cidr,
304 return content.format(**vm_config)
307 """Stores all ports of NFVbench networks."""
308 nets = self.get_networks_uuids()
309 for port in self.neutron.list_ports()['ports']:
310 if port['network_id'] in nets:
311 ports = self.ports.setdefault(port['network_id'], [])
314 def disable_port_security(self):
316 Disable security at port level.
318 vm_ids = map(lambda vm: vm.id, self.vms)
319 for net in self.nets:
320 for port in self.ports[net['id']]:
321 if port['device_id'] in vm_ids:
322 self.neutron.update_port(port['id'], {
324 'security_groups': [],
325 'port_security_enabled': False,
328 LOG.info('Security disabled on port {}'.format(port['id']))
330 def get_loop_vm_hostnames(self):
331 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
333 def get_host_ips(self):
334 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
335 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
337 if not self.host_ips:
338 # get the hypervisor object from the host name
339 self.host_ips = [self.comp.get_hypervisor(
340 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
344 def get_loop_vm_compute_nodes(self):
347 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
348 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
349 compute_nodes.append(az + ':' + hostname)
352 def get_reusable_vm(self, name, nets, az):
353 servers = self._lookup_servers(name=name, nets=nets, az=az,
354 flavor_id=self.flavor_type['flavor'].id)
357 LOG.info('Reusing existing server: ' + name)
358 setattr(server, 'is_reuse', True)
363 def get_networks_uuids(self):
365 Extract UUID of used networks. Order is important.
367 :return: list of UUIDs of created networks
369 return [net['id'] for net in self.nets]
373 Extract vlans of used networks. Order is important.
375 :return: list of UUIDs of created networks
378 for net in self.nets:
379 assert(net['provider:network_type'] == 'vlan')
380 vlans.append(net['provider:segmentation_id'])
386 Creates two networks and spawn a VM which act as a loop VM connected
387 with the two networks.
389 self._setup_openstack_clients()
391 def dispose(self, only_vm=False):
393 Deletes the created two networks and the VM.
397 if not getattr(vm, 'is_reuse', True):
398 self.comp.delete_server(vm)
400 LOG.info('Server %s not removed since it is reused' % vm.name)
402 for port in self.created_ports:
403 self.__delete_port(port)
406 for net in self.nets:
407 if 'is_reuse' in net and not net['is_reuse']:
408 self.__delete_net(net)
410 LOG.info('Network %s not removed since it is reused' % (net['name']))
412 if not self.flavor_type['is_reuse']:
413 self.__delete_flavor(self.flavor_type['flavor'])
416 class EXTStageClient(BasicStageClient):
418 def __init__(self, config, cred):
419 super(EXTStageClient, self).__init__(config, cred)
422 super(EXTStageClient, self).setup()
424 # Lookup two existing networks
425 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
426 net = self._lookup_network(net_name)
428 self.nets.append(net)
430 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
433 class PVPStageClient(BasicStageClient):
435 def __init__(self, config, cred):
436 super(PVPStageClient, self).__init__(config, cred)
438 def get_end_port_macs(self):
439 vm_ids = map(lambda vm: vm.id, self.vms)
441 for index, net in enumerate(self.nets):
442 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
443 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
447 super(PVPStageClient, self).setup()
448 self._setup_resources()
450 # Create two networks
451 nets = self.config.internal_networks
452 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
454 az_list = self.comp.get_enabled_az_host_list(required_count=1)
456 raise Exception('Not enough hosts found.')
459 self.compute_nodes.add(az)
460 for chain_index in xrange(self.config.service_chain_count):
461 name = self.config.loop_vm_name + str(chain_index)
462 reusable_vm = self.get_reusable_vm(name, self.nets, az)
464 self.vms.append(reusable_vm)
466 config_file = self.get_config_file(chain_index,
467 self.config.generator_config.src_device.mac,
468 self.config.generator_config.dst_device.mac)
470 ports = [self._create_port(net) for net in self.nets]
471 self.created_ports.extend(ports)
472 self.vms.append(self._create_server(name, ports, az, config_file))
473 self._ensure_vms_active()
477 class PVVPStageClient(BasicStageClient):
479 def __init__(self, config, cred):
480 super(PVVPStageClient, self).__init__(config, cred)
482 def get_end_port_macs(self):
484 for index, net in enumerate(self.nets[:2]):
485 vm_ids = map(lambda vm: vm.id, self.vms[index::2])
486 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
487 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
491 super(PVVPStageClient, self).setup()
492 self._setup_resources()
494 # Create two networks
495 nets = self.config.internal_networks
496 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
498 required_count = 2 if self.config.inter_node else 1
499 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
502 raise Exception('Not enough hosts found.')
504 az1 = az2 = az_list[0]
505 if self.config.inter_node:
510 # fallback to intra-node
511 az1 = az2 = az_list[0]
512 self.config.inter_node = False
513 LOG.info('Using intra-node instead of inter-node.')
515 self.compute_nodes.add(az1)
516 self.compute_nodes.add(az2)
519 for chain_index in xrange(self.config.service_chain_count):
520 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
521 # Attach first VM to net0 and net2
522 vm0_nets = self.nets[0::2]
523 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
525 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
526 # Attach second VM to net1 and net2
527 vm1_nets = self.nets[1:]
528 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
530 if reusable_vm0 and reusable_vm1:
531 self.vms.extend([reusable_vm0, reusable_vm1])
533 vm0_port_net0 = self._create_port(vm0_nets[0])
534 vm0_port_net2 = self._create_port(vm0_nets[1])
536 vm1_port_net2 = self._create_port(vm1_nets[1])
537 vm1_port_net1 = self._create_port(vm1_nets[0])
539 self.created_ports.extend([vm0_port_net0,
544 # order of ports is important for sections below
545 # order of MAC addresses needs to follow order of interfaces
546 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
547 config_file0 = self.get_config_file(chain_index,
548 self.config.generator_config.src_device.mac,
549 vm1_port_net2['mac_address'])
550 config_file1 = self.get_config_file(chain_index,
551 vm0_port_net2['mac_address'],
552 self.config.generator_config.dst_device.mac)
554 self.vms.append(self._create_server(name0,
555 [vm0_port_net0, vm0_port_net2],
558 self.vms.append(self._create_server(name1,
559 [vm1_port_net2, vm1_port_net1],
563 self._ensure_vms_active()