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):
151 'network_id': net['id'],
152 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
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):
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 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
321 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
322 'tg_net1': self.config.traffic_generator.ip_addrs[0],
323 'tg_net2': self.config.traffic_generator.ip_addrs[1],
324 'vnf_gateway1_cidr': g1cidr,
325 'vnf_gateway2_cidr': g2cidr,
330 return content.format(**vm_config)
333 """Stores all ports of NFVbench networks."""
334 nets = self.get_networks_uuids()
335 for port in self.neutron.list_ports()['ports']:
336 if port['network_id'] in nets:
337 ports = self.ports.setdefault(port['network_id'], [])
340 def disable_port_security(self):
342 Disable security at port level.
344 vm_ids = [vm.id for vm in self.vms]
345 for net in self.nets:
346 for port in self.ports[net['id']]:
347 if port['device_id'] in vm_ids:
348 self.neutron.update_port(port['id'], {
350 'security_groups': [],
351 'port_security_enabled': False,
354 LOG.info('Security disabled on port %s', port['id'])
356 def get_loop_vm_hostnames(self):
357 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
359 def get_host_ips(self):
360 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
361 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
363 if not self.host_ips:
364 # get the hypervisor object from the host name
365 self.host_ips = [self.comp.get_hypervisor(
366 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
369 def get_loop_vm_compute_nodes(self):
372 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
373 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
374 compute_nodes.append(az + ':' + hostname)
377 def get_reusable_vm(self, name, nets, az):
378 servers = self._lookup_servers(name=name, nets=nets, az=az,
379 flavor_id=self.flavor_type['flavor'].id)
382 LOG.info('Reusing existing server: %s', name)
383 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):
442 super(EXTStageClient, self).setup()
444 # Lookup two existing networks
445 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
446 net = self._lookup_network(net_name)
448 self.nets.append(net)
450 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
453 class PVPStageClient(BasicStageClient):
454 def get_end_port_macs(self):
455 vm_ids = [vm.id for vm in self.vms]
457 for _index, net in enumerate(self.nets):
458 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
459 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
463 super(PVPStageClient, self).setup()
464 self._setup_resources()
466 # Create two networks
467 nets = self.config.internal_networks
468 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
470 az_list = self.comp.get_enabled_az_host_list(required_count=1)
472 raise Exception('Not enough hosts found.')
475 self.compute_nodes.add(az)
476 for chain_index in xrange(self.config.service_chain_count):
477 name = self.config.loop_vm_name + str(chain_index)
478 reusable_vm = self.get_reusable_vm(name, self.nets, az)
480 self.vms.append(reusable_vm)
482 config_file = self.get_config_file(chain_index,
483 self.config.generator_config.src_device.mac,
484 self.config.generator_config.dst_device.mac)
486 ports = [self._create_port(net) for net in self.nets]
487 self.created_ports.extend(ports)
488 self.vms.append(self._create_server(name, ports, az, config_file))
489 self._ensure_vms_active()
493 class PVVPStageClient(BasicStageClient):
494 def get_end_port_macs(self):
496 for index, net in enumerate(self.nets[:2]):
497 vm_ids = [vm.id for vm in self.vms[index::2]]
498 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
499 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
503 super(PVVPStageClient, self).setup()
504 self._setup_resources()
506 # Create two networks
507 nets = self.config.internal_networks
508 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
510 required_count = 2 if self.config.inter_node else 1
511 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
514 raise Exception('Not enough hosts found.')
516 az1 = az2 = az_list[0]
517 if self.config.inter_node:
522 # fallback to intra-node
523 az1 = az2 = az_list[0]
524 self.config.inter_node = False
525 LOG.info('Using intra-node instead of inter-node.')
527 self.compute_nodes.add(az1)
528 self.compute_nodes.add(az2)
531 for chain_index in xrange(self.config.service_chain_count):
532 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
533 # Attach first VM to net0 and net2
534 vm0_nets = self.nets[0::2]
535 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
537 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
538 # Attach second VM to net1 and net2
539 vm1_nets = self.nets[1:]
540 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
542 if reusable_vm0 and reusable_vm1:
543 self.vms.extend([reusable_vm0, reusable_vm1])
545 vm0_port_net0 = self._create_port(vm0_nets[0])
546 vm0_port_net2 = self._create_port(vm0_nets[1])
548 vm1_port_net2 = self._create_port(vm1_nets[1])
549 vm1_port_net1 = self._create_port(vm1_nets[0])
551 self.created_ports.extend([vm0_port_net0,
556 # order of ports is important for sections below
557 # order of MAC addresses needs to follow order of interfaces
558 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
559 config_file0 = self.get_config_file(chain_index,
560 self.config.generator_config.src_device.mac,
561 vm1_port_net2['mac_address'])
562 config_file1 = self.get_config_file(chain_index,
563 vm0_port_net2['mac_address'],
564 self.config.generator_config.dst_device.mac)
566 self.vms.append(self._create_server(name0,
567 [vm0_port_net0, vm0_port_net2],
570 self.vms.append(self._create_server(name1,
571 [vm1_port_net2, vm1_port_net1],
575 self._ensure_vms_active()