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:
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 LOG.warning('Failed to disable port security on port %s, ignoring...',
362 def get_loop_vm_hostnames(self):
363 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
365 def get_host_ips(self):
366 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
367 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
369 if not self.host_ips:
370 # get the hypervisor object from the host name
371 self.host_ips = [self.comp.get_hypervisor(
372 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
375 def get_loop_vm_compute_nodes(self):
378 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
379 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
380 compute_nodes.append(az + ':' + hostname)
383 def get_reusable_vm(self, name, nets, az):
384 servers = self._lookup_servers(name=name, nets=nets, az=az,
385 flavor_id=self.flavor_type['flavor'].id)
388 LOG.info('Reusing existing server: %s', name)
389 setattr(server, 'is_reuse', True)
393 def get_networks_uuids(self):
395 Extract UUID of used networks. Order is important.
397 :return: list of UUIDs of created networks
399 return [net['id'] for net in self.nets]
403 Extract vlans of used networks. Order is important.
405 :return: list of UUIDs of created networks
408 for net in self.nets:
409 assert net['provider:network_type'] == 'vlan'
410 vlans.append(net['provider:segmentation_id'])
416 Creates two networks and spawn a VM which act as a loop VM connected
417 with the two networks.
420 self._setup_openstack_clients()
422 def dispose(self, only_vm=False):
424 Deletes the created two networks and the VM.
428 if not getattr(vm, 'is_reuse', True):
429 self.comp.delete_server(vm)
431 LOG.info('Server %s not removed since it is reused', vm.name)
433 for port in self.created_ports:
434 self.__delete_port(port)
437 for net in self.nets:
438 if 'is_reuse' in net and not net['is_reuse']:
439 self.__delete_net(net)
441 LOG.info('Network %s not removed since it is reused', net['name'])
443 if not self.flavor_type['is_reuse']:
444 self.__delete_flavor(self.flavor_type['flavor'])
447 class EXTStageClient(BasicStageClient):
449 super(EXTStageClient, self).setup()
451 # Lookup two existing networks
453 for net_name in [self.config.external_networks.left,
454 self.config.external_networks.right]:
455 net = self._lookup_network(net_name)
457 self.nets.append(net)
459 raise StageClientException('Existing network {} cannot be found.'.
463 class PVPStageClient(BasicStageClient):
464 def get_end_port_macs(self):
465 vm_ids = [vm.id for vm in self.vms]
467 for _index, net in enumerate(self.nets):
468 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
469 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
473 super(PVPStageClient, self).setup()
474 self._setup_resources()
476 # Create two networks
477 nets = self.config.internal_networks
478 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
480 az_list = self.comp.get_enabled_az_host_list(required_count=1)
482 raise Exception('Not enough hosts found.')
485 self.compute_nodes.add(az)
486 for chain_index in xrange(self.config.service_chain_count):
487 name = self.config.loop_vm_name + str(chain_index)
488 reusable_vm = self.get_reusable_vm(name, self.nets, az)
490 self.vms.append(reusable_vm)
492 vnic_type = 'direct' if self.config.sriov else 'normal'
493 ports = [self._create_port(net, vnic_type) for net in self.nets]
494 config_file = self.get_config_file(chain_index,
495 self.config.generator_config.src_device.mac,
496 self.config.generator_config.dst_device.mac,
497 ports[0]['mac_address'],
498 ports[1]['mac_address'])
499 self.created_ports.extend(ports)
500 self.vms.append(self._create_server(name, ports, az, config_file))
501 self._ensure_vms_active()
505 class PVVPStageClient(BasicStageClient):
506 def get_end_port_macs(self):
508 for index, net in enumerate(self.nets[:2]):
509 vm_ids = [vm.id for vm in self.vms[index::2]]
510 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
511 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
515 super(PVVPStageClient, self).setup()
516 self._setup_resources()
518 # Create two networks
519 nets = self.config.internal_networks
520 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
522 required_count = 2 if self.config.inter_node else 1
523 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
526 raise Exception('Not enough hosts found.')
528 az1 = az2 = az_list[0]
529 if self.config.inter_node:
534 # fallback to intra-node
535 az1 = az2 = az_list[0]
536 self.config.inter_node = False
537 LOG.info('Using intra-node instead of inter-node.')
539 self.compute_nodes.add(az1)
540 self.compute_nodes.add(az2)
543 for chain_index in xrange(self.config.service_chain_count):
544 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
545 # Attach first VM to net0 and net2
546 vm0_nets = self.nets[0::2]
547 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
549 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
550 # Attach second VM to net1 and net2
551 vm1_nets = self.nets[1:]
552 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
554 if reusable_vm0 and reusable_vm1:
555 self.vms.extend([reusable_vm0, reusable_vm1])
557 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
558 middle_vnic_type = 'direct' \
559 if self.config.sriov and self.config.use_sriov_middle_net \
561 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
562 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
564 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
565 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
567 self.created_ports.extend([vm0_port_net0,
572 # order of ports is important for sections below
573 # order of MAC addresses needs to follow order of interfaces
574 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
575 config_file0 = self.get_config_file(chain_index,
576 self.config.generator_config.src_device.mac,
577 vm1_port_net2['mac_address'],
578 vm0_port_net0['mac_address'],
579 vm0_port_net2['mac_address'])
580 config_file1 = self.get_config_file(chain_index,
581 vm0_port_net2['mac_address'],
582 self.config.generator_config.dst_device.mac,
583 vm1_port_net2['mac_address'],
584 vm1_port_net1['mac_address'])
586 self.vms.append(self._create_server(name0,
587 [vm0_port_net0, vm0_port_net2],
590 self.vms.append(self._create_server(name1,
591 [vm1_port_net2, vm1_port_net1],
595 self._ensure_vms_active()