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
21 from glanceclient.v2 import client as glanceclient
22 from neutronclient.neutron import client as neutronclient
23 from novaclient.client import Client
28 class StageClientException(Exception):
32 class BasicStageClient(object):
33 """Client for spawning and accessing the VM setup"""
35 nfvbenchvm_config_name = 'nfvbenchvm.conf'
37 def __init__(self, config, cred):
39 self.image_instance = None
40 self.image_name = None
45 self.created_ports = []
47 self.compute_nodes = set([])
50 self.flavor_type = {'is_reuse': True, 'flavor': None}
53 def _ensure_vms_active(self):
54 retry_count = (self.config.check_traffic_time_sec +
55 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
56 for _ in range(retry_count):
57 for i, instance in enumerate(self.vms):
58 if instance.status == 'ACTIVE':
60 is_reuse = getattr(instance, 'is_reuse', True)
61 instance = self.comp.poll_server(instance)
62 if instance.status == 'ERROR':
63 raise StageClientException('Instance creation error: %s' %
64 instance.fault['message'])
65 if instance.status == 'ACTIVE':
66 LOG.info('Created instance: %s', instance.name)
67 self.vms[i] = instance
68 setattr(self.vms[i], 'is_reuse', is_reuse)
70 if all([(vm.status == 'ACTIVE') for vm in self.vms]):
72 time.sleep(self.config.generic_poll_sec)
73 raise StageClientException('Timed out waiting for VMs to spawn')
75 def _setup_openstack_clients(self):
76 self.session = self.cred.get_session()
77 nova_client = Client(2, session=self.session)
78 self.neutron = neutronclient.Client('2.0', session=self.session)
79 self.glance_client = glanceclient.Client('2',
81 self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
83 def _lookup_network(self, network_name):
84 networks = self.neutron.list_networks(name=network_name)
85 return networks['networks'][0] if networks['networks'] else None
87 def _create_net(self, name, subnet, cidr, network_type=None,
88 segmentation_id=None, physical_network=None):
89 network = self._lookup_network(name)
91 # a network of same name already exists, we need to verify it has the same
94 if network['provider:segmentation_id'] != segmentation_id:
95 raise StageClientException("Mismatch of 'segmentation_id' for reused "
96 "network '{net}'. Network has id '{seg_id1}', "
97 "configuration requires '{seg_id2}'."
99 seg_id1=network['provider:segmentation_id'],
100 seg_id2=segmentation_id))
103 if network['provider:physical_network'] != physical_network:
104 raise StageClientException("Mismatch of 'physical_network' for reused "
105 "network '{net}'. Network has '{phys1}', "
106 "configuration requires '{phys2}'."
108 phys1=network['provider:physical_network'],
109 phys2=physical_network))
111 LOG.info('Reusing existing network: %s', name)
112 network['is_reuse'] = True
118 'admin_state_up': True
123 body['network']['provider:network_type'] = network_type
125 body['network']['provider:segmentation_id'] = segmentation_id
127 body['network']['provider:physical_network'] = physical_network
129 network = self.neutron.create_network(body)['network']
134 'network_id': network['id'],
135 'enable_dhcp': False,
137 'dns_nameservers': []
140 subnet = self.neutron.create_subnet(body)['subnet']
141 # add subnet id to the network dict since it has just been added
142 network['subnets'] = [subnet['id']]
143 network['is_reuse'] = False
144 LOG.info('Created network: %s.', name)
147 def _create_port(self, net, vnic_type='normal'):
150 'network_id': net['id'],
151 'binding:vnic_type': vnic_type
154 port = self.neutron.create_port(body)
157 def __delete_port(self, port):
159 while retry < self.config.generic_retry_count:
161 self.neutron.delete_port(port['id'])
165 time.sleep(self.config.generic_poll_sec)
166 LOG.error('Unable to delete port: %s', port['id'])
168 def __delete_net(self, network):
170 while retry < self.config.generic_retry_count:
172 self.neutron.delete_network(network['id'])
176 time.sleep(self.config.generic_poll_sec)
177 LOG.error('Unable to delete network: %s', network['name'])
179 def __get_server_az(self, server):
180 availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
181 host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
182 if availability_zone is None:
186 return availability_zone + ':' + host
188 def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
189 error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
190 networks = set([net['name'] for net in nets]) if nets else None
191 server_list = self.comp.get_server_list()
192 matching_servers = []
194 for server in server_list:
195 if name and server.name != name:
198 if flavor_id and server.flavor['id'] != flavor_id:
199 raise StageClientException(error_msg.format('flavors'))
201 if networks and not set(server.networks.keys()).issuperset(networks):
202 raise StageClientException(error_msg.format('networks'))
204 if server.status != "ACTIVE":
205 raise StageClientException(error_msg.format('state'))
208 matching_servers.append(server)
210 return matching_servers
212 def _create_server(self, name, ports, az, nfvbenchvm_config):
213 port_ids = [{'port-id': port['id']} for port in ports]
214 nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
215 server = self.comp.create_server(name,
217 self.flavor_type['flavor'],
224 files={nfvbenchvm_config_location: nfvbenchvm_config})
226 setattr(server, 'is_reuse', False)
227 LOG.info('Creating instance: %s on %s', name, az)
229 raise StageClientException('Unable to create instance: %s.' % (name))
232 def _setup_resources(self):
233 # To avoid reuploading image in server mode, check whether image_name is set or not
235 self.image_instance = self.comp.find_image(self.image_name)
236 if self.image_instance:
237 LOG.info("Reusing image %s", self.image_name)
239 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
240 if self.config.vm_image_file:
241 match = re.search(image_name_search_pattern, self.config.vm_image_file)
243 self.image_name = match.group(1)
244 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
246 raise StageClientException('Provided VM image file name %s must start with '
247 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
249 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
250 for f in os.listdir(pkg_root):
251 if re.search(image_name_search_pattern, f):
252 self.config.vm_image_file = pkg_root + '/' + f
253 self.image_name = f.replace('.qcow2', '')
254 LOG.info('Found built-in VM image file %s', f)
257 raise StageClientException('Cannot find any built-in VM image file.')
259 self.image_instance = self.comp.find_image(self.image_name)
260 if not self.image_instance:
261 LOG.info('Uploading %s', self.image_name)
262 res = self.comp.upload_image_via_url(self.image_name,
263 self.config.vm_image_file)
266 raise StageClientException('Error uploading image %s from %s. ABORTING.'
268 self.config.vm_image_file))
269 LOG.info('Image %s successfully uploaded.', self.image_name)
270 self.image_instance = self.comp.find_image(self.image_name)
272 self.__setup_flavor()
274 def __setup_flavor(self):
275 if self.flavor_type.get('flavor', False):
278 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
279 if self.flavor_type['flavor']:
280 self.flavor_type['is_reuse'] = True
282 flavor_dict = self.config.flavor
283 extra_specs = flavor_dict.pop('extra_specs', None)
285 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
289 LOG.info("Flavor '%s' was created.", self.config.flavor_type)
292 self.flavor_type['flavor'].set_keys(extra_specs)
294 self.flavor_type['is_reuse'] = False
296 if self.flavor_type['flavor'] is None:
297 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
298 % self.config.flavor_type)
300 def __delete_flavor(self, flavor):
301 if self.comp.delete_flavor(flavor=flavor):
302 LOG.info("Flavor '%s' deleted", self.config.flavor_type)
303 self.flavor_type = {'is_reuse': False, 'flavor': None}
305 LOG.error('Unable to delete flavor: %s', self.config.flavor_type)
307 def get_config_file(self, chain_index, src_mac, dst_mac, intf_mac1, intf_mac2):
308 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
309 'nfvbenchvm/', self.nfvbenchvm_config_name)
311 with open(boot_script_file, 'r') as boot_script:
312 content = boot_script.read()
314 g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
315 g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
318 'forwarder': self.config.vm_forwarder,
319 'intf_mac1': intf_mac1,
320 'intf_mac2': intf_mac2,
321 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
322 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
323 'tg_net1': self.config.traffic_generator.ip_addrs[0],
324 'tg_net2': self.config.traffic_generator.ip_addrs[1],
325 'vnf_gateway1_cidr': g1cidr,
326 'vnf_gateway2_cidr': g2cidr,
331 return content.format(**vm_config)
334 """Stores all ports of NFVbench networks."""
335 nets = self.get_networks_uuids()
336 for port in self.neutron.list_ports()['ports']:
337 if port['network_id'] in nets:
338 ports = self.ports.setdefault(port['network_id'], [])
341 def disable_port_security(self):
343 Disable security at port level.
345 vm_ids = [vm.id for vm in self.vms]
346 for net in self.nets:
347 for port in self.ports[net['id']]:
348 if port['device_id'] in vm_ids:
349 self.neutron.update_port(port['id'], {
351 'security_groups': [],
352 'port_security_enabled': False,
355 LOG.info('Security disabled on port %s', port['id'])
357 def get_loop_vm_hostnames(self):
358 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
360 def get_host_ips(self):
361 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
362 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
364 if not self.host_ips:
365 # get the hypervisor object from the host name
366 self.host_ips = [self.comp.get_hypervisor(
367 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
370 def get_loop_vm_compute_nodes(self):
373 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
374 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
375 compute_nodes.append(az + ':' + hostname)
378 def get_reusable_vm(self, name, nets, az):
379 servers = self._lookup_servers(name=name, nets=nets, az=az,
380 flavor_id=self.flavor_type['flavor'].id)
383 LOG.info('Reusing existing server: %s', name)
384 setattr(server, 'is_reuse', True)
388 def get_networks_uuids(self):
390 Extract UUID of used networks. Order is important.
392 :return: list of UUIDs of created networks
394 return [net['id'] for net in self.nets]
398 Extract vlans of used networks. Order is important.
400 :return: list of UUIDs of created networks
403 for net in self.nets:
404 assert net['provider:network_type'] == 'vlan'
405 vlans.append(net['provider:segmentation_id'])
411 Creates two networks and spawn a VM which act as a loop VM connected
412 with the two networks.
414 self._setup_openstack_clients()
416 def dispose(self, only_vm=False):
418 Deletes the created two networks and the VM.
422 if not getattr(vm, 'is_reuse', True):
423 self.comp.delete_server(vm)
425 LOG.info('Server %s not removed since it is reused', vm.name)
427 for port in self.created_ports:
428 self.__delete_port(port)
431 for net in self.nets:
432 if 'is_reuse' in net and not net['is_reuse']:
433 self.__delete_net(net)
435 LOG.info('Network %s not removed since it is reused', net['name'])
437 if not self.flavor_type['is_reuse']:
438 self.__delete_flavor(self.flavor_type['flavor'])
441 class EXTStageClient(BasicStageClient):
443 super(EXTStageClient, self).setup()
445 # Lookup two existing networks
446 for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
447 net = self._lookup_network(net_name)
449 self.nets.append(net)
451 raise StageClientException('Existing network {} cannot be found.'.format(net_name))
454 class PVPStageClient(BasicStageClient):
455 def get_end_port_macs(self):
456 vm_ids = [vm.id for vm in self.vms]
458 for _index, net in enumerate(self.nets):
459 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
460 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
464 super(PVPStageClient, self).setup()
465 self._setup_resources()
467 # Create two networks
468 nets = self.config.internal_networks
469 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
471 az_list = self.comp.get_enabled_az_host_list(required_count=1)
473 raise Exception('Not enough hosts found.')
476 self.compute_nodes.add(az)
477 for chain_index in xrange(self.config.service_chain_count):
478 name = self.config.loop_vm_name + str(chain_index)
479 reusable_vm = self.get_reusable_vm(name, self.nets, az)
481 self.vms.append(reusable_vm)
483 vnic_type = 'direct' if self.config.sriov else 'normal'
484 ports = [self._create_port(net, vnic_type) for net in self.nets]
485 config_file = self.get_config_file(chain_index,
486 self.config.generator_config.src_device.mac,
487 self.config.generator_config.dst_device.mac,
488 ports[0]['mac_address'],
489 ports[1]['mac_address'])
490 self.created_ports.extend(ports)
491 self.vms.append(self._create_server(name, ports, az, config_file))
492 self._ensure_vms_active()
496 class PVVPStageClient(BasicStageClient):
497 def get_end_port_macs(self):
499 for index, net in enumerate(self.nets[:2]):
500 vm_ids = [vm.id for vm in self.vms[index::2]]
501 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
502 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
506 super(PVVPStageClient, self).setup()
507 self._setup_resources()
509 # Create two networks
510 nets = self.config.internal_networks
511 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
513 required_count = 2 if self.config.inter_node else 1
514 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
517 raise Exception('Not enough hosts found.')
519 az1 = az2 = az_list[0]
520 if self.config.inter_node:
525 # fallback to intra-node
526 az1 = az2 = az_list[0]
527 self.config.inter_node = False
528 LOG.info('Using intra-node instead of inter-node.')
530 self.compute_nodes.add(az1)
531 self.compute_nodes.add(az2)
534 for chain_index in xrange(self.config.service_chain_count):
535 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
536 # Attach first VM to net0 and net2
537 vm0_nets = self.nets[0::2]
538 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
540 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
541 # Attach second VM to net1 and net2
542 vm1_nets = self.nets[1:]
543 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
545 if reusable_vm0 and reusable_vm1:
546 self.vms.extend([reusable_vm0, reusable_vm1])
548 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
549 middle_vnic_type = 'direct' \
550 if self.config.sriov and self.config.use_sriov_middle_net \
552 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
553 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
555 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
556 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
558 self.created_ports.extend([vm0_port_net0,
563 # order of ports is important for sections below
564 # order of MAC addresses needs to follow order of interfaces
565 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
566 config_file0 = self.get_config_file(chain_index,
567 self.config.generator_config.src_device.mac,
568 vm1_port_net2['mac_address'],
569 vm0_port_net0['mac_address'],
570 vm0_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,
574 vm1_port_net2['mac_address'],
575 vm1_port_net1['mac_address'])
577 self.vms.append(self._create_server(name0,
578 [vm0_port_net0, vm0_port_net2],
581 self.vms.append(self._create_server(name1,
582 [vm1_port_net2, vm1_port_net1],
586 self._ensure_vms_active()