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, physical_network=None):
82 network = self._lookup_network(name)
84 # a network of same name already exists, we need to verify it has the same
87 if network['provider:segmentation_id'] != segmentation_id:
88 raise StageClientException("Mismatch of 'segmentation_id' for reused "
89 "network '{net}'. Network has id '{seg_id1}', "
90 "configuration requires '{seg_id2}'."
92 seg_id1=network['provider:segmentation_id'],
93 seg_id2=segmentation_id))
96 if network['provider:physical_network'] != physical_network:
97 raise StageClientException("Mismatch of 'physical_network' for reused "
98 "network '{net}'. Network has '{phys1}', "
99 "configuration requires '{phys2}'."
101 phys1=network['provider:physical_network'],
102 phys2=physical_network))
104 LOG.info('Reusing existing network: ' + name)
105 network['is_reuse'] = True
111 'admin_state_up': True
116 body['network']['provider:network_type'] = network_type
118 body['network']['provider:segmentation_id'] = segmentation_id
120 body['network']['provider:physical_network'] = physical_network
122 network = self.neutron.create_network(body)['network']
127 'network_id': network['id'],
128 'enable_dhcp': False,
130 'dns_nameservers': []
133 subnet = self.neutron.create_subnet(body)['subnet']
134 # add subnet id to the network dict since it has just been added
135 network['subnets'] = [subnet['id']]
136 network['is_reuse'] = False
137 LOG.info('Created network: %s.' % name)
140 def _create_port(self, net):
143 'network_id': net['id'],
144 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
147 port = self.neutron.create_port(body)
150 def __delete_port(self, port):
152 while retry < self.config.generic_retry_count:
154 self.neutron.delete_port(port['id'])
158 time.sleep(self.config.generic_poll_sec)
159 LOG.error('Unable to delete port: %s' % (port['id']))
161 def __delete_net(self, network):
163 while retry < self.config.generic_retry_count:
165 self.neutron.delete_network(network['id'])
169 time.sleep(self.config.generic_poll_sec)
170 LOG.error('Unable to delete network: %s' % (network['name']))
172 def __get_server_az(self, server):
173 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
174 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
175 if availability_zone is None:
179 return availability_zone + ':' + host
181 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
182 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
183 networks = set(map(lambda net: net['name'], nets)) if nets else None
184 server_list = self.comp.get_server_list()
185 matching_servers = []
187 for server in server_list:
188 if name and server.name != name:
191 if az and self.__get_server_az(server) != az:
192 raise StageClientException(error_msg.format('availability zones'))
194 if flavor_id and server.flavor['id'] != flavor_id:
195 raise StageClientException(error_msg.format('flavors'))
197 if networks and not set(server.networks.keys()).issuperset(networks):
198 raise StageClientException(error_msg.format('networks'))
200 if server.status != "ACTIVE":
201 raise StageClientException(error_msg.format('state'))
204 matching_servers.append(server)
206 return matching_servers
208 def _create_server(self, name, ports, az, nfvbenchvm_config):
209 port_ids = map(lambda port: {'port-id': port['id']}, ports)
210 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
211 server = self.comp.create_server(name,
213 self.flavor_type['flavor'],
220 files={nfvbenchvm_config_location: nfvbenchvm_config})
222 setattr(server, 'is_reuse', False)
223 LOG.info('Creating instance: %s on %s' % (name, az))
225 raise StageClientException('Unable to create instance: %s.' % (name))
228 def _setup_resources(self):
229 if not self.image_instance:
230 self.image_instance = self.comp.find_image(self.config.image_name)
231 if self.image_instance is None:
232 if self.config.vm_image_file:
233 LOG.info('%s: image for VM not found, trying to upload it ...'
234 % self.config.image_name)
235 res = self.comp.upload_image_via_url(self.config.image_name,
236 self.config.vm_image_file)
239 raise StageClientException('Error uploading image %s from %s. ABORTING.'
240 % (self.config.image_name,
241 self.config.vm_image_file))
242 self.image_instance = self.comp.find_image(self.config.image_name)
244 raise StageClientException('%s: image to launch VM not found. ABORTING.'
245 % self.config.image_name)
247 LOG.info('Found image %s to launch VM' % self.config.image_name)
249 self.__setup_flavor()
251 def __setup_flavor(self):
252 if self.flavor_type.get('flavor', False):
255 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
256 if self.flavor_type['flavor']:
257 self.flavor_type['is_reuse'] = True
259 flavor_dict = self.config.flavor
260 extra_specs = flavor_dict.pop('extra_specs', None)
262 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
266 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
269 self.flavor_type['flavor'].set_keys(extra_specs)
271 self.flavor_type['is_reuse'] = False
273 if self.flavor_type['flavor'] is None:
274 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
275 % self.config.flavor_type)
277 def __delete_flavor(self, flavor):
278 if self.comp.delete_flavor(flavor=flavor):
279 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
280 self.flavor_type = {'is_reuse': False, 'flavor': None}
282 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
284 def get_config_file(self, chain_index, src_mac, dst_mac):
285 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
286 'nfvbenchvm/', self.nfvbenchvm_config_name)
288 with open(boot_script_file, 'r') as boot_script:
289 content = boot_script.read()
291 g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
292 g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
295 'forwarder': self.config.vm_forwarder,
296 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
297 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
298 'tg_net1': self.config.traffic_generator.ip_addrs[0],
299 'tg_net2': self.config.traffic_generator.ip_addrs[1],
300 'vnf_gateway1_cidr': g1cidr,
301 'vnf_gateway2_cidr': g2cidr,
306 return content.format(**vm_config)
309 """Stores all ports of NFVbench networks."""
310 nets = self.get_networks_uuids()
311 for port in self.neutron.list_ports()['ports']:
312 if port['network_id'] in nets:
313 ports = self.ports.setdefault(port['network_id'], [])
316 def disable_port_security(self):
318 Disable security at port level.
320 vm_ids = map(lambda vm: vm.id, self.vms)
321 for net in self.nets:
322 for port in self.ports[net['id']]:
323 if port['device_id'] in vm_ids:
324 self.neutron.update_port(port['id'], {
326 'security_groups': [],
327 'port_security_enabled': False,
330 LOG.info('Security disabled on port {}'.format(port['id']))
332 def get_loop_vm_hostnames(self):
333 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
335 def get_host_ips(self):
336 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
337 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
339 if not self.host_ips:
340 # get the hypervisor object from the host name
341 self.host_ips = [self.comp.get_hypervisor(
342 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
346 def get_loop_vm_compute_nodes(self):
349 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
350 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
351 compute_nodes.append(az + ':' + hostname)
354 def get_reusable_vm(self, name, nets, az):
355 servers = self._lookup_servers(name=name, nets=nets, az=az,
356 flavor_id=self.flavor_type['flavor'].id)
359 LOG.info('Reusing existing server: ' + name)
360 setattr(server, 'is_reuse', True)
365 def get_networks_uuids(self):
367 Extract UUID of used networks. Order is important.
369 :return: list of UUIDs of created networks
371 return [net['id'] for net in self.nets]
375 Extract vlans of used networks. Order is important.
377 :return: list of UUIDs of created networks
380 for net in self.nets:
381 assert(net['provider:network_type'] == 'vlan')
382 vlans.append(net['provider:segmentation_id'])
388 Creates two networks and spawn a VM which act as a loop VM connected
389 with the two networks.
391 self._setup_openstack_clients()
393 def dispose(self, only_vm=False):
395 Deletes the created two networks and the VM.
399 if not getattr(vm, 'is_reuse', True):
400 self.comp.delete_server(vm)
402 LOG.info('Server %s not removed since it is reused' % vm.name)
404 for port in self.created_ports:
405 self.__delete_port(port)
408 for net in self.nets:
409 if 'is_reuse' in net and not net['is_reuse']:
410 self.__delete_net(net)
412 LOG.info('Network %s not removed since it is reused' % (net['name']))
414 if not self.flavor_type['is_reuse']:
415 self.__delete_flavor(self.flavor_type['flavor'])
418 class EXTStageClient(BasicStageClient):
420 def __init__(self, config, cred):
421 super(EXTStageClient, self).__init__(config, cred)
424 super(EXTStageClient, self).setup()
426 # Lookup two existing networks
427 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
428 net = self._lookup_network(net_name)
430 self.nets.append(net)
432 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
435 class PVPStageClient(BasicStageClient):
437 def __init__(self, config, cred):
438 super(PVPStageClient, self).__init__(config, cred)
440 def get_end_port_macs(self):
441 vm_ids = map(lambda vm: vm.id, self.vms)
443 for index, net in enumerate(self.nets):
444 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
445 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
449 super(PVPStageClient, self).setup()
450 self._setup_resources()
452 # Create two networks
453 nets = self.config.internal_networks
454 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
456 az_list = self.comp.get_enabled_az_host_list(required_count=1)
458 raise Exception('Not enough hosts found.')
461 self.compute_nodes.add(az)
462 for chain_index in xrange(self.config.service_chain_count):
463 name = self.config.loop_vm_name + str(chain_index)
464 reusable_vm = self.get_reusable_vm(name, self.nets, az)
466 self.vms.append(reusable_vm)
468 config_file = self.get_config_file(chain_index,
469 self.config.generator_config.src_device.mac,
470 self.config.generator_config.dst_device.mac)
472 ports = [self._create_port(net) for net in self.nets]
473 self.created_ports.extend(ports)
474 self.vms.append(self._create_server(name, ports, az, config_file))
475 self._ensure_vms_active()
479 class PVVPStageClient(BasicStageClient):
481 def __init__(self, config, cred):
482 super(PVVPStageClient, self).__init__(config, cred)
484 def get_end_port_macs(self):
486 for index, net in enumerate(self.nets[:2]):
487 vm_ids = map(lambda vm: vm.id, self.vms[index::2])
488 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
489 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
493 super(PVVPStageClient, self).setup()
494 self._setup_resources()
496 # Create two networks
497 nets = self.config.internal_networks
498 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
500 required_count = 2 if self.config.inter_node else 1
501 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
504 raise Exception('Not enough hosts found.')
506 az1 = az2 = az_list[0]
507 if self.config.inter_node:
512 # fallback to intra-node
513 az1 = az2 = az_list[0]
514 self.config.inter_node = False
515 LOG.info('Using intra-node instead of inter-node.')
517 self.compute_nodes.add(az1)
518 self.compute_nodes.add(az2)
521 for chain_index in xrange(self.config.service_chain_count):
522 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
523 # Attach first VM to net0 and net2
524 vm0_nets = self.nets[0::2]
525 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
527 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
528 # Attach second VM to net1 and net2
529 vm1_nets = self.nets[1:]
530 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
532 if reusable_vm0 and reusable_vm1:
533 self.vms.extend([reusable_vm0, reusable_vm1])
535 vm0_port_net0 = self._create_port(vm0_nets[0])
536 vm0_port_net2 = self._create_port(vm0_nets[1])
538 vm1_port_net2 = self._create_port(vm1_nets[1])
539 vm1_port_net1 = self._create_port(vm1_nets[0])
541 self.created_ports.extend([vm0_port_net0,
546 # order of ports is important for sections below
547 # order of MAC addresses needs to follow order of interfaces
548 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
549 config_file0 = self.get_config_file(chain_index,
550 self.config.generator_config.src_device.mac,
551 vm1_port_net2['mac_address'])
552 config_file1 = self.get_config_file(chain_index,
553 vm0_port_net2['mac_address'],
554 self.config.generator_config.dst_device.mac)
556 self.vms.append(self._create_server(name0,
557 [vm0_port_net0, vm0_port_net2],
560 self.vms.append(self._create_server(name1,
561 [vm1_port_net2, vm1_port_net1],
565 self._ensure_vms_active()