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 flavor_id and server.flavor['id'] != flavor_id:
197 raise StageClientException(error_msg.format('flavors'))
199 if networks and not set(server.networks.keys()).issuperset(networks):
200 raise StageClientException(error_msg.format('networks'))
202 if server.status != "ACTIVE":
203 raise StageClientException(error_msg.format('state'))
206 matching_servers.append(server)
208 return matching_servers
210 def _create_server(self, name, ports, az, nfvbenchvm_config):
211 port_ids = map(lambda port: {'port-id': port['id']}, ports)
212 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
213 server = self.comp.create_server(name,
215 self.flavor_type['flavor'],
222 files={nfvbenchvm_config_location: nfvbenchvm_config})
224 setattr(server, 'is_reuse', False)
225 LOG.info('Creating instance: %s on %s' % (name, az))
227 raise StageClientException('Unable to create instance: %s.' % (name))
230 def _setup_resources(self):
231 # To avoid reuploading image in server mode, check whether image_name is set or not
233 self.image_instance = self.comp.find_image(self.image_name)
234 if self.image_instance:
235 LOG.info("Reusing image %s" % self.image_name)
237 image_name_search_pattern = '(nfvbenchvm-\d+(\.\d+)*).qcow2'
238 if self.config.vm_image_file:
239 match = re.search(image_name_search_pattern, self.config.vm_image_file)
241 self.image_name = match.group(1)
242 LOG.info('Using provided VM image file %s' % self.config.vm_image_file)
244 raise StageClientException('Provided VM image file name %s must start with '
245 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
247 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
248 for f in os.listdir(pkg_root):
249 if re.search(image_name_search_pattern, f):
250 self.config.vm_image_file = pkg_root + '/' + f
251 self.image_name = f.replace('.qcow2', '')
252 LOG.info('Found built-in VM image file %s' % f)
255 raise StageClientException('Cannot find any built-in VM image file.')
257 self.image_instance = self.comp.find_image(self.image_name)
258 if not self.image_instance:
259 LOG.info('Uploading %s'
261 res = self.comp.upload_image_via_url(self.image_name,
262 self.config.vm_image_file)
265 raise StageClientException('Error uploading image %s from %s. ABORTING.'
267 self.config.vm_image_file))
268 LOG.info('Image %s successfully uploaded.' % self.image_name)
269 self.image_instance = self.comp.find_image(self.image_name)
271 self.__setup_flavor()
273 def __setup_flavor(self):
274 if self.flavor_type.get('flavor', False):
277 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
278 if self.flavor_type['flavor']:
279 self.flavor_type['is_reuse'] = True
281 flavor_dict = self.config.flavor
282 extra_specs = flavor_dict.pop('extra_specs', None)
284 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
288 LOG.info("Flavor '%s' was created." % self.config.flavor_type)
291 self.flavor_type['flavor'].set_keys(extra_specs)
293 self.flavor_type['is_reuse'] = False
295 if self.flavor_type['flavor'] is None:
296 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
297 % self.config.flavor_type)
299 def __delete_flavor(self, flavor):
300 if self.comp.delete_flavor(flavor=flavor):
301 LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
302 self.flavor_type = {'is_reuse': False, 'flavor': None}
304 LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
306 def get_config_file(self, chain_index, src_mac, dst_mac):
307 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
308 'nfvbenchvm/', self.nfvbenchvm_config_name)
310 with open(boot_script_file, 'r') as boot_script:
311 content = boot_script.read()
313 g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
314 g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
317 'forwarder': self.config.vm_forwarder,
318 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
319 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
320 'tg_net1': self.config.traffic_generator.ip_addrs[0],
321 'tg_net2': self.config.traffic_generator.ip_addrs[1],
322 'vnf_gateway1_cidr': g1cidr,
323 'vnf_gateway2_cidr': g2cidr,
328 return content.format(**vm_config)
331 """Stores all ports of NFVbench networks."""
332 nets = self.get_networks_uuids()
333 for port in self.neutron.list_ports()['ports']:
334 if port['network_id'] in nets:
335 ports = self.ports.setdefault(port['network_id'], [])
338 def disable_port_security(self):
340 Disable security at port level.
342 vm_ids = map(lambda vm: vm.id, self.vms)
343 for net in self.nets:
344 for port in self.ports[net['id']]:
345 if port['device_id'] in vm_ids:
346 self.neutron.update_port(port['id'], {
348 'security_groups': [],
349 'port_security_enabled': False,
352 LOG.info('Security disabled on port {}'.format(port['id']))
354 def get_loop_vm_hostnames(self):
355 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
357 def get_host_ips(self):
358 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
359 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
361 if not self.host_ips:
362 # get the hypervisor object from the host name
363 self.host_ips = [self.comp.get_hypervisor(
364 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
368 def get_loop_vm_compute_nodes(self):
371 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
372 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
373 compute_nodes.append(az + ':' + hostname)
376 def get_reusable_vm(self, name, nets, az):
377 servers = self._lookup_servers(name=name, nets=nets, az=az,
378 flavor_id=self.flavor_type['flavor'].id)
381 LOG.info('Reusing existing server: ' + name)
382 setattr(server, 'is_reuse', True)
387 def get_networks_uuids(self):
389 Extract UUID of used networks. Order is important.
391 :return: list of UUIDs of created networks
393 return [net['id'] for net in self.nets]
397 Extract vlans of used networks. Order is important.
399 :return: list of UUIDs of created networks
402 for net in self.nets:
403 assert (net['provider:network_type'] == 'vlan')
404 vlans.append(net['provider:segmentation_id'])
410 Creates two networks and spawn a VM which act as a loop VM connected
411 with the two networks.
413 self._setup_openstack_clients()
415 def dispose(self, only_vm=False):
417 Deletes the created two networks and the VM.
421 if not getattr(vm, 'is_reuse', True):
422 self.comp.delete_server(vm)
424 LOG.info('Server %s not removed since it is reused' % vm.name)
426 for port in self.created_ports:
427 self.__delete_port(port)
430 for net in self.nets:
431 if 'is_reuse' in net and not net['is_reuse']:
432 self.__delete_net(net)
434 LOG.info('Network %s not removed since it is reused' % (net['name']))
436 if not self.flavor_type['is_reuse']:
437 self.__delete_flavor(self.flavor_type['flavor'])
440 class EXTStageClient(BasicStageClient):
441 def __init__(self, config, cred):
442 super(EXTStageClient, self).__init__(config, cred)
445 super(EXTStageClient, self).setup()
447 # Lookup two existing networks
448 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
449 net = self._lookup_network(net_name)
451 self.nets.append(net)
453 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
456 class PVPStageClient(BasicStageClient):
457 def __init__(self, config, cred):
458 super(PVPStageClient, self).__init__(config, cred)
460 def get_end_port_macs(self):
461 vm_ids = map(lambda vm: vm.id, self.vms)
463 for index, net in enumerate(self.nets):
464 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
465 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
469 super(PVPStageClient, self).setup()
470 self._setup_resources()
472 # Create two networks
473 nets = self.config.internal_networks
474 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
476 az_list = self.comp.get_enabled_az_host_list(required_count=1)
478 raise Exception('Not enough hosts found.')
481 self.compute_nodes.add(az)
482 for chain_index in xrange(self.config.service_chain_count):
483 name = self.config.loop_vm_name + str(chain_index)
484 reusable_vm = self.get_reusable_vm(name, self.nets, az)
486 self.vms.append(reusable_vm)
488 config_file = self.get_config_file(chain_index,
489 self.config.generator_config.src_device.mac,
490 self.config.generator_config.dst_device.mac)
492 ports = [self._create_port(net) for net in self.nets]
493 self.created_ports.extend(ports)
494 self.vms.append(self._create_server(name, ports, az, config_file))
495 self._ensure_vms_active()
499 class PVVPStageClient(BasicStageClient):
500 def __init__(self, config, cred):
501 super(PVVPStageClient, self).__init__(config, cred)
503 def get_end_port_macs(self):
505 for index, net in enumerate(self.nets[:2]):
506 vm_ids = map(lambda vm: vm.id, self.vms[index::2])
507 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
508 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
512 super(PVVPStageClient, self).setup()
513 self._setup_resources()
515 # Create two networks
516 nets = self.config.internal_networks
517 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
519 required_count = 2 if self.config.inter_node else 1
520 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
523 raise Exception('Not enough hosts found.')
525 az1 = az2 = az_list[0]
526 if self.config.inter_node:
531 # fallback to intra-node
532 az1 = az2 = az_list[0]
533 self.config.inter_node = False
534 LOG.info('Using intra-node instead of inter-node.')
536 self.compute_nodes.add(az1)
537 self.compute_nodes.add(az2)
540 for chain_index in xrange(self.config.service_chain_count):
541 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
542 # Attach first VM to net0 and net2
543 vm0_nets = self.nets[0::2]
544 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
546 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
547 # Attach second VM to net1 and net2
548 vm1_nets = self.nets[1:]
549 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
551 if reusable_vm0 and reusable_vm1:
552 self.vms.extend([reusable_vm0, reusable_vm1])
554 vm0_port_net0 = self._create_port(vm0_nets[0])
555 vm0_port_net2 = self._create_port(vm0_nets[1])
557 vm1_port_net2 = self._create_port(vm1_nets[1])
558 vm1_port_net1 = self._create_port(vm1_nets[0])
560 self.created_ports.extend([vm0_port_net0,
565 # order of ports is important for sections below
566 # order of MAC addresses needs to follow order of interfaces
567 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
568 config_file0 = self.get_config_file(chain_index,
569 self.config.generator_config.src_device.mac,
570 vm1_port_net2['mac_address'])
571 config_file1 = self.get_config_file(chain_index,
572 vm0_port_net2['mac_address'],
573 self.config.generator_config.dst_device.mac)
575 self.vms.append(self._create_server(name0,
576 [vm0_port_net0, vm0_port_net2],
579 self.vms.append(self._create_server(name1,
580 [vm1_port_net2, vm1_port_net1],
584 self._ensure_vms_active()