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
24 from glanceclient.v2 import client as glanceclient
25 from neutronclient.neutron import client as neutronclient
26 from novaclient.client import Client
29 class StageClientException(Exception):
33 class BasicStageClient(object):
34 """Client for spawning and accessing the VM setup"""
36 nfvbenchvm_config_name = 'nfvbenchvm.conf'
38 def __init__(self, config, cred):
40 self.image_instance = None
41 self.image_name = None
46 self.created_ports = []
48 self.compute_nodes = set([])
51 self.flavor_type = {'is_reuse': True, 'flavor': None}
54 def _ensure_vms_active(self):
55 retry_count = (self.config.check_traffic_time_sec +
56 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
57 for _ in range(retry_count):
58 for i, instance in enumerate(self.vms):
59 if instance.status == 'ACTIVE':
61 is_reuse = getattr(instance, 'is_reuse', True)
62 instance = self.comp.poll_server(instance)
63 if instance.status == 'ERROR':
64 raise StageClientException('Instance creation error: %s' %
65 instance.fault['message'])
66 if instance.status == 'ACTIVE':
67 LOG.info('Created instance: %s', instance.name)
68 self.vms[i] = instance
69 setattr(self.vms[i], 'is_reuse', is_reuse)
71 if all([(vm.status == 'ACTIVE') for vm in self.vms]):
73 time.sleep(self.config.generic_poll_sec)
74 raise StageClientException('Timed out waiting for VMs to spawn')
76 def _setup_openstack_clients(self):
77 self.session = self.cred.get_session()
78 nova_client = Client(2, session=self.session)
79 self.neutron = neutronclient.Client('2.0', session=self.session)
80 self.glance_client = glanceclient.Client('2',
82 self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
84 def _lookup_network(self, network_name):
85 networks = self.neutron.list_networks(name=network_name)
86 return networks['networks'][0] if networks['networks'] else None
88 def _create_net(self, name, subnet, cidr, network_type=None,
89 segmentation_id=None, physical_network=None):
90 network = self._lookup_network(name)
92 # a network of same name already exists, we need to verify it has the same
95 if network['provider:segmentation_id'] != segmentation_id:
96 raise StageClientException("Mismatch of 'segmentation_id' for reused "
97 "network '{net}'. Network has id '{seg_id1}', "
98 "configuration requires '{seg_id2}'."
100 seg_id1=network['provider:segmentation_id'],
101 seg_id2=segmentation_id))
104 if network['provider:physical_network'] != physical_network:
105 raise StageClientException("Mismatch of 'physical_network' for reused "
106 "network '{net}'. Network has '{phys1}', "
107 "configuration requires '{phys2}'."
109 phys1=network['provider:physical_network'],
110 phys2=physical_network))
112 LOG.info('Reusing existing network: ' + name)
113 network['is_reuse'] = True
119 'admin_state_up': True
124 body['network']['provider:network_type'] = network_type
126 body['network']['provider:segmentation_id'] = segmentation_id
128 body['network']['provider:physical_network'] = physical_network
130 network = self.neutron.create_network(body)['network']
135 'network_id': network['id'],
136 'enable_dhcp': False,
138 'dns_nameservers': []
141 subnet = self.neutron.create_subnet(body)['subnet']
142 # add subnet id to the network dict since it has just been added
143 network['subnets'] = [subnet['id']]
144 network['is_reuse'] = False
145 LOG.info('Created network: %s.', name)
148 def _create_port(self, net, vnic_type='normal'):
151 'network_id': net['id'],
152 'binding:vnic_type': vnic_type
155 port = self.neutron.create_port(body)
158 def __delete_port(self, port):
160 while retry < self.config.generic_retry_count:
162 self.neutron.delete_port(port['id'])
166 time.sleep(self.config.generic_poll_sec)
167 LOG.error('Unable to delete port: %s', port['id'])
169 def __delete_net(self, network):
171 while retry < self.config.generic_retry_count:
173 self.neutron.delete_network(network['id'])
177 time.sleep(self.config.generic_poll_sec)
178 LOG.error('Unable to delete network: %s', network['name'])
180 def __get_server_az(self, server):
181 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
182 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
183 if availability_zone is None:
187 return availability_zone + ':' + host
189 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
190 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
191 networks = set([net['name'] for net in nets]) if nets else None
192 server_list = self.comp.get_server_list()
193 matching_servers = []
195 for server in server_list:
196 if name and server.name != name:
199 if flavor_id and server.flavor['id'] != flavor_id:
200 raise StageClientException(error_msg.format('flavors'))
202 if networks and not set(server.networks.keys()).issuperset(networks):
203 raise StageClientException(error_msg.format('networks'))
205 if server.status != "ACTIVE":
206 raise StageClientException(error_msg.format('state'))
209 matching_servers.append(server)
211 return matching_servers
213 def _create_server(self, name, ports, az, nfvbenchvm_config):
214 port_ids = [{'port-id': port['id']} for port in ports]
215 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
216 server = self.comp.create_server(name,
218 self.flavor_type['flavor'],
225 files={nfvbenchvm_config_location: nfvbenchvm_config})
227 setattr(server, 'is_reuse', False)
228 LOG.info('Creating instance: %s on %s', name, az)
230 raise StageClientException('Unable to create instance: %s.' % (name))
233 def _setup_resources(self):
234 # To avoid reuploading image in server mode, check whether image_name is set or not
236 self.image_instance = self.comp.find_image(self.image_name)
237 if self.image_instance:
238 LOG.info("Reusing image %s", self.image_name)
240 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
241 if self.config.vm_image_file:
242 match = re.search(image_name_search_pattern, self.config.vm_image_file)
244 self.image_name = match.group(1)
245 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
247 raise StageClientException('Provided VM image file name %s must start with '
248 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
250 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
251 for f in os.listdir(pkg_root):
252 if re.search(image_name_search_pattern, f):
253 self.config.vm_image_file = pkg_root + '/' + f
254 self.image_name = f.replace('.qcow2', '')
255 LOG.info('Found built-in VM image file %s', f)
258 raise StageClientException('Cannot find any built-in VM image file.')
260 self.image_instance = self.comp.find_image(self.image_name)
261 if not self.image_instance:
262 LOG.info('Uploading %s', self.image_name)
263 res = self.comp.upload_image_via_url(self.image_name,
264 self.config.vm_image_file)
267 raise StageClientException('Error uploading image %s from %s. ABORTING.'
269 self.config.vm_image_file))
270 LOG.info('Image %s successfully uploaded.', self.image_name)
271 self.image_instance = self.comp.find_image(self.image_name)
273 self.__setup_flavor()
275 def __setup_flavor(self):
276 if self.flavor_type.get('flavor', False):
279 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
280 if self.flavor_type['flavor']:
281 self.flavor_type['is_reuse'] = True
283 flavor_dict = self.config.flavor
284 extra_specs = flavor_dict.pop('extra_specs', None)
286 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
290 LOG.info("Flavor '%s' was created.", self.config.flavor_type)
293 self.flavor_type['flavor'].set_keys(extra_specs)
295 self.flavor_type['is_reuse'] = False
297 if self.flavor_type['flavor'] is None:
298 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
299 % self.config.flavor_type)
301 def __delete_flavor(self, flavor):
302 if self.comp.delete_flavor(flavor=flavor):
303 LOG.info("Flavor '%s' deleted", self.config.flavor_type)
304 self.flavor_type = {'is_reuse': False, 'flavor': None}
306 LOG.error('Unable to delete flavor: %s', self.config.flavor_type)
308 def get_config_file(self, chain_index, src_mac, dst_mac, intf_mac1, intf_mac2):
309 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
310 'nfvbenchvm/', self.nfvbenchvm_config_name)
312 with open(boot_script_file, 'r') as boot_script:
313 content = boot_script.read()
315 g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
316 g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
319 'forwarder': self.config.vm_forwarder,
320 'intf_mac1': intf_mac1,
321 'intf_mac2': intf_mac2,
322 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
323 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
324 'tg_net1': self.config.traffic_generator.ip_addrs[0],
325 'tg_net2': self.config.traffic_generator.ip_addrs[1],
326 'vnf_gateway1_cidr': g1cidr,
327 'vnf_gateway2_cidr': g2cidr,
332 return content.format(**vm_config)
335 """Stores all ports of NFVbench networks."""
336 nets = self.get_networks_uuids()
337 for port in self.neutron.list_ports()['ports']:
338 if port['network_id'] in nets:
339 ports = self.ports.setdefault(port['network_id'], [])
342 def disable_port_security(self):
344 Disable security at port level.
346 vm_ids = [vm.id for vm in self.vms]
347 for net in self.nets:
348 for port in self.ports[net['id']]:
349 if port['device_id'] in vm_ids:
350 self.neutron.update_port(port['id'], {
352 'security_groups': [],
353 'port_security_enabled': False,
356 LOG.info('Security disabled on port %s', port['id'])
358 def get_loop_vm_hostnames(self):
359 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
361 def get_host_ips(self):
362 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
363 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
365 if not self.host_ips:
366 # get the hypervisor object from the host name
367 self.host_ips = [self.comp.get_hypervisor(
368 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
371 def get_loop_vm_compute_nodes(self):
374 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
375 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
376 compute_nodes.append(az + ':' + hostname)
379 def get_reusable_vm(self, name, nets, az):
380 servers = self._lookup_servers(name=name, nets=nets, az=az,
381 flavor_id=self.flavor_type['flavor'].id)
384 LOG.info('Reusing existing server: %s', name)
385 setattr(server, 'is_reuse', True)
389 def get_networks_uuids(self):
391 Extract UUID of used networks. Order is important.
393 :return: list of UUIDs of created networks
395 return [net['id'] for net in self.nets]
399 Extract vlans of used networks. Order is important.
401 :return: list of UUIDs of created networks
404 for net in self.nets:
405 assert net['provider:network_type'] == 'vlan'
406 vlans.append(net['provider:segmentation_id'])
412 Creates two networks and spawn a VM which act as a loop VM connected
413 with the two networks.
415 self._setup_openstack_clients()
417 def dispose(self, only_vm=False):
419 Deletes the created two networks and the VM.
423 if not getattr(vm, 'is_reuse', True):
424 self.comp.delete_server(vm)
426 LOG.info('Server %s not removed since it is reused', vm.name)
428 for port in self.created_ports:
429 self.__delete_port(port)
432 for net in self.nets:
433 if 'is_reuse' in net and not net['is_reuse']:
434 self.__delete_net(net)
436 LOG.info('Network %s not removed since it is reused', net['name'])
438 if not self.flavor_type['is_reuse']:
439 self.__delete_flavor(self.flavor_type['flavor'])
442 class EXTStageClient(BasicStageClient):
444 super(EXTStageClient, self).setup()
446 # Lookup two existing networks
447 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
448 net = self._lookup_network(net_name)
450 self.nets.append(net)
452 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
455 class PVPStageClient(BasicStageClient):
456 def get_end_port_macs(self):
457 vm_ids = [vm.id for vm in self.vms]
459 for _index, net in enumerate(self.nets):
460 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
461 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
465 super(PVPStageClient, self).setup()
466 self._setup_resources()
468 # Create two networks
469 nets = self.config.internal_networks
470 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
472 az_list = self.comp.get_enabled_az_host_list(required_count=1)
474 raise Exception('Not enough hosts found.')
477 self.compute_nodes.add(az)
478 for chain_index in xrange(self.config.service_chain_count):
479 name = self.config.loop_vm_name + str(chain_index)
480 reusable_vm = self.get_reusable_vm(name, self.nets, az)
482 self.vms.append(reusable_vm)
484 vnic_type = 'direct' if self.config.sriov else 'normal'
485 ports = [self._create_port(net, vnic_type) for net in self.nets]
486 config_file = self.get_config_file(chain_index,
487 self.config.generator_config.src_device.mac,
488 self.config.generator_config.dst_device.mac,
489 ports[0]['mac_address'],
490 ports[1]['mac_address'])
491 self.created_ports.extend(ports)
492 self.vms.append(self._create_server(name, ports, az, config_file))
493 self._ensure_vms_active()
497 class PVVPStageClient(BasicStageClient):
498 def get_end_port_macs(self):
500 for index, net in enumerate(self.nets[:2]):
501 vm_ids = [vm.id for vm in self.vms[index::2]]
502 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
503 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
507 super(PVVPStageClient, self).setup()
508 self._setup_resources()
510 # Create two networks
511 nets = self.config.internal_networks
512 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
514 required_count = 2 if self.config.inter_node else 1
515 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
518 raise Exception('Not enough hosts found.')
520 az1 = az2 = az_list[0]
521 if self.config.inter_node:
526 # fallback to intra-node
527 az1 = az2 = az_list[0]
528 self.config.inter_node = False
529 LOG.info('Using intra-node instead of inter-node.')
531 self.compute_nodes.add(az1)
532 self.compute_nodes.add(az2)
535 for chain_index in xrange(self.config.service_chain_count):
536 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
537 # Attach first VM to net0 and net2
538 vm0_nets = self.nets[0::2]
539 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
541 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
542 # Attach second VM to net1 and net2
543 vm1_nets = self.nets[1:]
544 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
546 if reusable_vm0 and reusable_vm1:
547 self.vms.extend([reusable_vm0, reusable_vm1])
549 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
550 middle_vnic_type = 'direct' \
551 if self.config.sriov and self.config.use_sriov_middle_net \
553 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
554 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
556 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
557 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
559 self.created_ports.extend([vm0_port_net0,
564 # order of ports is important for sections below
565 # order of MAC addresses needs to follow order of interfaces
566 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
567 config_file0 = self.get_config_file(chain_index,
568 self.config.generator_config.src_device.mac,
569 vm1_port_net2['mac_address'],
570 vm0_port_net0['mac_address'],
571 vm0_port_net2['mac_address'])
572 config_file1 = self.get_config_file(chain_index,
573 vm0_port_net2['mac_address'],
574 self.config.generator_config.dst_device.mac,
575 vm1_port_net2['mac_address'],
576 vm1_port_net1['mac_address'])
578 self.vms.append(self._create_server(name0,
579 [vm0_port_net0, vm0_port_net2],
582 self.vms.append(self._create_server(name1,
583 [vm1_port_net2, vm1_port_net1],
587 self._ensure_vms_active()