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,
82 segmentation_id=None, physical_network=None):
83 network = self._lookup_network(name)
85 # a network of same name already exists, we need to verify it has the same
88 if network['provider:segmentation_id'] != segmentation_id:
89 raise StageClientException("Mismatch of 'segmentation_id' for reused "
90 "network '{net}'. Network has id '{seg_id1}', "
91 "configuration requires '{seg_id2}'."
93 seg_id1=network['provider:segmentation_id'],
94 seg_id2=segmentation_id))
97 if network['provider:physical_network'] != physical_network:
98 raise StageClientException("Mismatch of 'physical_network' for reused "
99 "network '{net}'. Network has '{phys1}', "
100 "configuration requires '{phys2}'."
102 phys1=network['provider:physical_network'],
103 phys2=physical_network))
105 LOG.info('Reusing existing network: ' + name)
106 network['is_reuse'] = True
112 'admin_state_up': True
117 body['network']['provider:network_type'] = network_type
119 body['network']['provider:segmentation_id'] = segmentation_id
121 body['network']['provider:physical_network'] = physical_network
123 network = self.neutron.create_network(body)['network']
128 'network_id': network['id'],
129 'enable_dhcp': False,
131 'dns_nameservers': []
134 subnet = self.neutron.create_subnet(body)['subnet']
135 # add subnet id to the network dict since it has just been added
136 network['subnets'] = [subnet['id']]
137 network['is_reuse'] = False
138 LOG.info('Created network: %s.' % name)
141 def _create_port(self, net):
144 'network_id': net['id'],
145 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
148 port = self.neutron.create_port(body)
151 def __delete_port(self, port):
153 while retry < self.config.generic_retry_count:
155 self.neutron.delete_port(port['id'])
159 time.sleep(self.config.generic_poll_sec)
160 LOG.error('Unable to delete port: %s' % (port['id']))
162 def __delete_net(self, network):
164 while retry < self.config.generic_retry_count:
166 self.neutron.delete_network(network['id'])
170 time.sleep(self.config.generic_poll_sec)
171 LOG.error('Unable to delete network: %s' % (network['name']))
173 def __get_server_az(self, server):
174 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
175 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
176 if availability_zone is None:
180 return availability_zone + ':' + host
182 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
183 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
184 networks = set(map(lambda net: net['name'], nets)) if nets else None
185 server_list = self.comp.get_server_list()
186 matching_servers = []
188 for server in server_list:
189 if name and server.name != name:
192 if az and self.__get_server_az(server) != az:
193 raise StageClientException(error_msg.format('availability zones'))
195 if flavor_id and server.flavor['id'] != flavor_id:
196 raise StageClientException(error_msg.format('flavors'))
198 if networks and not set(server.networks.keys()).issuperset(networks):
199 raise StageClientException(error_msg.format('networks'))
201 if server.status != "ACTIVE":
202 raise StageClientException(error_msg.format('state'))
205 matching_servers.append(server)
207 return matching_servers
209 def _create_server(self, name, ports, az, nfvbenchvm_config):
210 port_ids = map(lambda port: {'port-id': port['id']}, ports)
211 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
212 server = self.comp.create_server(name,
214 self.flavor_type['flavor'],
221 files={nfvbenchvm_config_location: nfvbenchvm_config})
223 setattr(server, 'is_reuse', False)
224 LOG.info('Creating instance: %s on %s' % (name, az))
226 raise StageClientException('Unable to create instance: %s.' % (name))
229 def _setup_resources(self):
230 if not self.image_instance:
231 self.image_instance = self.comp.find_image(self.config.image_name)
232 if self.image_instance is None:
233 if self.config.vm_image_file:
234 LOG.info('%s: image for VM not found, trying to upload it ...'
235 % self.config.image_name)
236 res = self.comp.upload_image_via_url(self.config.image_name,
237 self.config.vm_image_file)
240 raise StageClientException('Error uploading image %s from %s. ABORTING.'
241 % (self.config.image_name,
242 self.config.vm_image_file))
243 self.image_instance = self.comp.find_image(self.config.image_name)
245 raise StageClientException('%s: image to launch VM not found. ABORTING.'
246 % self.config.image_name)
248 LOG.info('Found image %s to launch VM' % self.config.image_name)
250 self.__setup_flavor()
252 def __setup_flavor(self):
253 if self.flavor_type.get('flavor', False):
256 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
257 if self.flavor_type['flavor']:
258 self.flavor_type['is_reuse'] = True
260 flavor_dict = self.config.flavor
261 extra_specs = flavor_dict.pop('extra_specs', None)
263 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
267 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
270 self.flavor_type['flavor'].set_keys(extra_specs)
272 self.flavor_type['is_reuse'] = False
274 if self.flavor_type['flavor'] is None:
275 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
276 % self.config.flavor_type)
278 def __delete_flavor(self, flavor):
279 if self.comp.delete_flavor(flavor=flavor):
280 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
281 self.flavor_type = {'is_reuse': False, 'flavor': None}
283 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
285 def get_config_file(self, chain_index, src_mac, dst_mac):
286 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
287 'nfvbenchvm/', self.nfvbenchvm_config_name)
289 with open(boot_script_file, 'r') as boot_script:
290 content = boot_script.read()
292 g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
293 g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
296 'forwarder': self.config.vm_forwarder,
297 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
298 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
299 'tg_net1': self.config.traffic_generator.ip_addrs[0],
300 'tg_net2': self.config.traffic_generator.ip_addrs[1],
301 'vnf_gateway1_cidr': g1cidr,
302 'vnf_gateway2_cidr': g2cidr,
307 return content.format(**vm_config)
310 """Stores all ports of NFVbench networks."""
311 nets = self.get_networks_uuids()
312 for port in self.neutron.list_ports()['ports']:
313 if port['network_id'] in nets:
314 ports = self.ports.setdefault(port['network_id'], [])
317 def disable_port_security(self):
319 Disable security at port level.
321 vm_ids = map(lambda vm: vm.id, self.vms)
322 for net in self.nets:
323 for port in self.ports[net['id']]:
324 if port['device_id'] in vm_ids:
325 self.neutron.update_port(port['id'], {
327 'security_groups': [],
328 'port_security_enabled': False,
331 LOG.info('Security disabled on port {}'.format(port['id']))
333 def get_loop_vm_hostnames(self):
334 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
336 def get_host_ips(self):
337 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
338 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
340 if not self.host_ips:
341 # get the hypervisor object from the host name
342 self.host_ips = [self.comp.get_hypervisor(
343 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
347 def get_loop_vm_compute_nodes(self):
350 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
351 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
352 compute_nodes.append(az + ':' + hostname)
355 def get_reusable_vm(self, name, nets, az):
356 servers = self._lookup_servers(name=name, nets=nets, az=az,
357 flavor_id=self.flavor_type['flavor'].id)
360 LOG.info('Reusing existing server: ' + name)
361 setattr(server, 'is_reuse', True)
366 def get_networks_uuids(self):
368 Extract UUID of used networks. Order is important.
370 :return: list of UUIDs of created networks
372 return [net['id'] for net in self.nets]
376 Extract vlans of used networks. Order is important.
378 :return: list of UUIDs of created networks
381 for net in self.nets:
382 assert(net['provider:network_type'] == 'vlan')
383 vlans.append(net['provider:segmentation_id'])
389 Creates two networks and spawn a VM which act as a loop VM connected
390 with the two networks.
392 self._setup_openstack_clients()
394 def dispose(self, only_vm=False):
396 Deletes the created two networks and the VM.
400 if not getattr(vm, 'is_reuse', True):
401 self.comp.delete_server(vm)
403 LOG.info('Server %s not removed since it is reused' % vm.name)
405 for port in self.created_ports:
406 self.__delete_port(port)
409 for net in self.nets:
410 if 'is_reuse' in net and not net['is_reuse']:
411 self.__delete_net(net)
413 LOG.info('Network %s not removed since it is reused' % (net['name']))
415 if not self.flavor_type['is_reuse']:
416 self.__delete_flavor(self.flavor_type['flavor'])
419 class EXTStageClient(BasicStageClient):
421 def __init__(self, config, cred):
422 super(EXTStageClient, self).__init__(config, cred)
425 super(EXTStageClient, self).setup()
427 # Lookup two existing networks
428 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
429 net = self._lookup_network(net_name)
431 self.nets.append(net)
433 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
436 class PVPStageClient(BasicStageClient):
438 def __init__(self, config, cred):
439 super(PVPStageClient, self).__init__(config, cred)
441 def get_end_port_macs(self):
442 vm_ids = map(lambda vm: vm.id, self.vms)
444 for index, net in enumerate(self.nets):
445 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
446 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
450 super(PVPStageClient, self).setup()
451 self._setup_resources()
453 # Create two networks
454 nets = self.config.internal_networks
455 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
457 az_list = self.comp.get_enabled_az_host_list(required_count=1)
459 raise Exception('Not enough hosts found.')
462 self.compute_nodes.add(az)
463 for chain_index in xrange(self.config.service_chain_count):
464 name = self.config.loop_vm_name + str(chain_index)
465 reusable_vm = self.get_reusable_vm(name, self.nets, az)
467 self.vms.append(reusable_vm)
469 config_file = self.get_config_file(chain_index,
470 self.config.generator_config.src_device.mac,
471 self.config.generator_config.dst_device.mac)
473 ports = [self._create_port(net) for net in self.nets]
474 self.created_ports.extend(ports)
475 self.vms.append(self._create_server(name, ports, az, config_file))
476 self._ensure_vms_active()
480 class PVVPStageClient(BasicStageClient):
482 def __init__(self, config, cred):
483 super(PVVPStageClient, self).__init__(config, cred)
485 def get_end_port_macs(self):
487 for index, net in enumerate(self.nets[:2]):
488 vm_ids = map(lambda vm: vm.id, self.vms[index::2])
489 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
490 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
494 super(PVVPStageClient, self).setup()
495 self._setup_resources()
497 # Create two networks
498 nets = self.config.internal_networks
499 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
501 required_count = 2 if self.config.inter_node else 1
502 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
505 raise Exception('Not enough hosts found.')
507 az1 = az2 = az_list[0]
508 if self.config.inter_node:
513 # fallback to intra-node
514 az1 = az2 = az_list[0]
515 self.config.inter_node = False
516 LOG.info('Using intra-node instead of inter-node.')
518 self.compute_nodes.add(az1)
519 self.compute_nodes.add(az2)
522 for chain_index in xrange(self.config.service_chain_count):
523 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
524 # Attach first VM to net0 and net2
525 vm0_nets = self.nets[0::2]
526 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
528 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
529 # Attach second VM to net1 and net2
530 vm1_nets = self.nets[1:]
531 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
533 if reusable_vm0 and reusable_vm1:
534 self.vms.extend([reusable_vm0, reusable_vm1])
536 vm0_port_net0 = self._create_port(vm0_nets[0])
537 vm0_port_net2 = self._create_port(vm0_nets[1])
539 vm1_port_net2 = self._create_port(vm1_nets[1])
540 vm1_port_net1 = self._create_port(vm1_nets[0])
542 self.created_ports.extend([vm0_port_net0,
547 # order of ports is important for sections below
548 # order of MAC addresses needs to follow order of interfaces
549 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
550 config_file0 = self.get_config_file(chain_index,
551 self.config.generator_config.src_device.mac,
552 vm1_port_net2['mac_address'])
553 config_file1 = self.get_config_file(chain_index,
554 vm0_port_net2['mac_address'],
555 self.config.generator_config.dst_device.mac)
557 self.vms.append(self._create_server(name0,
558 [vm0_port_net0, vm0_port_net2],
561 self.vms.append(self._create_server(name1,
562 [vm1_port_net2, vm1_port_net1],
566 self._ensure_vms_active()