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, 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 msg = 'Creating instance: %s' % name
232 raise StageClientException('Unable to create instance: %s.' % (name))
235 def _setup_resources(self):
236 # To avoid reuploading image in server mode, check whether image_name is set or not
238 self.image_instance = self.comp.find_image(self.image_name)
239 if self.image_instance:
240 LOG.info("Reusing image %s", self.image_name)
242 image_name_search_pattern = r'(nfvbenchvm-\d+(\.\d+)*).qcow2'
243 if self.config.vm_image_file:
244 match = re.search(image_name_search_pattern, self.config.vm_image_file)
246 self.image_name = match.group(1)
247 LOG.info('Using provided VM image file %s', self.config.vm_image_file)
249 raise StageClientException('Provided VM image file name %s must start with '
250 '"nfvbenchvm-<version>"' % self.config.vm_image_file)
252 pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
253 for f in os.listdir(pkg_root):
254 if re.search(image_name_search_pattern, f):
255 self.config.vm_image_file = pkg_root + '/' + f
256 self.image_name = f.replace('.qcow2', '')
257 LOG.info('Found built-in VM image file %s', f)
260 raise StageClientException('Cannot find any built-in VM image file.')
262 self.image_instance = self.comp.find_image(self.image_name)
263 if not self.image_instance:
264 LOG.info('Uploading %s', self.image_name)
265 res = self.comp.upload_image_via_url(self.image_name,
266 self.config.vm_image_file)
269 raise StageClientException('Error uploading image %s from %s. ABORTING.'
271 self.config.vm_image_file))
272 LOG.info('Image %s successfully uploaded.', self.image_name)
273 self.image_instance = self.comp.find_image(self.image_name)
275 self.__setup_flavor()
277 def __setup_flavor(self):
278 if self.flavor_type.get('flavor', False):
281 self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
282 if self.flavor_type['flavor']:
283 self.flavor_type['is_reuse'] = True
285 flavor_dict = self.config.flavor
286 extra_specs = flavor_dict.pop('extra_specs', None)
288 self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
292 LOG.info("Flavor '%s' was created.", self.config.flavor_type)
295 self.flavor_type['flavor'].set_keys(extra_specs)
297 self.flavor_type['is_reuse'] = False
299 if self.flavor_type['flavor'] is None:
300 raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
301 % self.config.flavor_type)
303 def __delete_flavor(self, flavor):
304 if self.comp.delete_flavor(flavor=flavor):
305 LOG.info("Flavor '%s' deleted", self.config.flavor_type)
306 self.flavor_type = {'is_reuse': False, 'flavor': None}
308 LOG.error('Unable to delete flavor: %s', self.config.flavor_type)
310 def get_config_file(self, chain_index, src_mac, dst_mac, intf_mac1, intf_mac2):
311 boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
312 'nfvbenchvm/', self.nfvbenchvm_config_name)
314 with open(boot_script_file, 'r') as boot_script:
315 content = boot_script.read()
317 g1cidr = self.config.generator_config.src_device.get_gw_ip(chain_index) + '/8'
318 g2cidr = self.config.generator_config.dst_device.get_gw_ip(chain_index) + '/8'
321 'forwarder': self.config.vm_forwarder,
322 'intf_mac1': intf_mac1,
323 'intf_mac2': intf_mac2,
324 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
325 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
326 'tg_net1': self.config.traffic_generator.ip_addrs[0],
327 'tg_net2': self.config.traffic_generator.ip_addrs[1],
328 'vnf_gateway1_cidr': g1cidr,
329 'vnf_gateway2_cidr': g2cidr,
334 return content.format(**vm_config)
337 """Stores all ports of NFVbench networks."""
338 nets = self.get_networks_uuids()
339 for port in self.neutron.list_ports()['ports']:
340 if port['network_id'] in nets:
341 ports = self.ports.setdefault(port['network_id'], [])
344 def disable_port_security(self):
346 Disable security at port level.
348 vm_ids = [vm.id for vm in self.vms]
349 for net in self.nets:
350 for port in self.ports[net['id']]:
351 if port['device_id'] in vm_ids:
353 self.neutron.update_port(port['id'], {
355 'security_groups': [],
356 'port_security_enabled': False,
359 LOG.info('Security disabled on port %s', port['id'])
361 LOG.warning('Failed to disable port security on port %s, ignoring...',
365 def get_loop_vm_hostnames(self):
366 return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
368 def get_host_ips(self):
369 '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
370 Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
372 if not self.host_ips:
373 # get the hypervisor object from the host name
374 self.host_ips = [self.comp.get_hypervisor(
375 getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip for vm in self.vms]
378 def get_loop_vm_compute_nodes(self):
381 az = getattr(vm, 'OS-EXT-AZ:availability_zone')
382 hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
383 compute_nodes.append(az + ':' + hostname)
386 def get_reusable_vm(self, name, nets):
387 servers = self._lookup_servers(name=name, nets=nets,
388 flavor_id=self.flavor_type['flavor'].id)
391 LOG.info('Reusing existing server: %s', name)
392 setattr(server, 'is_reuse', True)
396 def get_networks_uuids(self):
398 Extract UUID of used networks. Order is important.
400 :return: list of UUIDs of created networks
402 return [net['id'] for net in self.nets]
406 Extract vlans of used networks. Order is important.
408 :return: list of UUIDs of created networks
411 for net in self.nets:
412 assert net['provider:network_type'] == 'vlan'
413 vlans.append(net['provider:segmentation_id'])
419 Creates two networks and spawn a VM which act as a loop VM connected
420 with the two networks.
423 self._setup_openstack_clients()
425 def dispose(self, only_vm=False):
427 Deletes the created two networks and the VM.
431 if not getattr(vm, 'is_reuse', True):
432 self.comp.delete_server(vm)
434 LOG.info('Server %s not removed since it is reused', vm.name)
436 for port in self.created_ports:
437 self.__delete_port(port)
440 for net in self.nets:
441 if 'is_reuse' in net and not net['is_reuse']:
442 self.__delete_net(net)
444 LOG.info('Network %s not removed since it is reused', net['name'])
446 if not self.flavor_type['is_reuse']:
447 self.__delete_flavor(self.flavor_type['flavor'])
450 class EXTStageClient(BasicStageClient):
452 super(EXTStageClient, self).setup()
454 # Lookup two existing networks
456 for net_name in [self.config.external_networks.left,
457 self.config.external_networks.right]:
458 net = self._lookup_network(net_name)
460 self.nets.append(net)
462 raise StageClientException('Existing network {} cannot be found.'.
466 class PVPStageClient(BasicStageClient):
467 def get_end_port_macs(self):
468 vm_ids = [vm.id for vm in self.vms]
470 for _index, net in enumerate(self.nets):
471 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
472 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
476 super(PVPStageClient, self).setup()
477 self._setup_resources()
479 # Create two networks
480 nets = self.config.internal_networks
481 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
483 if self.comp.config.compute_nodes:
484 az_list = self.comp.get_enabled_az_host_list(required_count=1)
486 raise Exception('Not enough hosts found.')
491 for chain_index in xrange(self.config.service_chain_count):
492 name = self.config.loop_vm_name + str(chain_index)
493 server = self.get_reusable_vm(name, self.nets)
495 self.vms.append(server)
497 vnic_type = 'direct' if self.config.sriov else 'normal'
498 ports = [self._create_port(net, vnic_type) for net in self.nets]
499 config_file = self.get_config_file(chain_index,
500 self.config.generator_config.src_device.mac,
501 self.config.generator_config.dst_device.mac,
502 ports[0]['mac_address'],
503 ports[1]['mac_address'])
504 self.created_ports.extend(ports)
505 server = self._create_server(name, ports, az, config_file)
506 self.vms.append(server)
509 # First VM, save the hypervisor name. Used in future for
511 self._ensure_vms_active()
512 server = self.comp.poll_server(server)
513 az = "%s:%s" % (getattr(server, 'OS-EXT-AZ:availability_zone'),
514 getattr(server, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
516 self._ensure_vms_active()
517 self.compute_nodes = set(self.get_loop_vm_compute_nodes())
521 class PVVPStageClient(BasicStageClient):
522 def get_end_port_macs(self):
524 for index, net in enumerate(self.nets[:2]):
525 vm_ids = [vm.id for vm in self.vms[index::2]]
526 vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
527 port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
531 super(PVVPStageClient, self).setup()
532 self._setup_resources()
534 # Create two networks
535 nets = self.config.internal_networks
536 self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
538 if self.comp.config.compute_nodes:
539 required_count = 2 if self.config.inter_node else 1
540 az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
542 raise Exception('Not enough hosts found.')
544 az1 = az2 = az_list[0]
545 if self.config.inter_node:
550 # fallback to intra-node
551 az1 = az2 = az_list[0]
552 self.config.inter_node = False
553 LOG.info('Using intra-node instead of inter-node.')
558 for chain_index in xrange(self.config.service_chain_count):
559 name0 = self.config.loop_vm_name + str(chain_index) + 'a'
560 # Attach first VM to net0 and net2
561 vm0_nets = self.nets[0::2]
562 reusable_vm0 = self.get_reusable_vm(name0, vm0_nets)
564 name1 = self.config.loop_vm_name + str(chain_index) + 'b'
565 # Attach second VM to net1 and net2
566 vm1_nets = self.nets[1:]
567 reusable_vm1 = self.get_reusable_vm(name1, vm1_nets)
569 if reusable_vm0 and reusable_vm1:
570 self.vms.extend([reusable_vm0, reusable_vm1])
572 edge_vnic_type = 'direct' if self.config.sriov else 'normal'
573 middle_vnic_type = 'direct' \
574 if self.config.sriov and self.config.use_sriov_middle_net \
576 vm0_port_net0 = self._create_port(vm0_nets[0], edge_vnic_type)
577 vm0_port_net2 = self._create_port(vm0_nets[1], middle_vnic_type)
579 vm1_port_net2 = self._create_port(vm1_nets[1], middle_vnic_type)
580 vm1_port_net1 = self._create_port(vm1_nets[0], edge_vnic_type)
582 self.created_ports.extend([vm0_port_net0,
587 # order of ports is important for sections below
588 # order of MAC addresses needs to follow order of interfaces
589 # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
590 config_file0 = self.get_config_file(chain_index,
591 self.config.generator_config.src_device.mac,
592 vm1_port_net2['mac_address'],
593 vm0_port_net0['mac_address'],
594 vm0_port_net2['mac_address'])
595 config_file1 = self.get_config_file(chain_index,
596 vm0_port_net2['mac_address'],
597 self.config.generator_config.dst_device.mac,
598 vm1_port_net2['mac_address'],
599 vm1_port_net1['mac_address'])
601 vm1 = self._create_server(name0, [vm0_port_net0, vm0_port_net2], az1, config_file0)
604 # First VM on first chain, save the hypervisor name. Used
605 # in future for maintain affinity.
606 self._ensure_vms_active()
607 vm1 = self.comp.poll_server(vm1)
608 az1 = "%s:%s" % (getattr(vm1, 'OS-EXT-AZ:availability_zone'),
609 getattr(vm1, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
610 if not self.config.inter_node:
611 # By default, NOVA scheduler will try first with
612 # different hypervisor for workload balance, but when
613 # inter-node is not configured, use the same AZ to run
614 # intra-node test case.
617 vm2 = self._create_server(name1, [vm1_port_net2, vm1_port_net1], az2, config_file1)
619 if chain_index == 0 and self.config.inter_node:
620 # Second VM on first chain, save the hypervisor name. Used
621 # in future for maintain affinity.
622 self._ensure_vms_active()
623 vm2 = self.comp.poll_server(vm2)
624 az2 = "%s:%s" % (getattr(vm2, 'OS-EXT-AZ:availability_zone'),
625 getattr(vm2, 'OS-EXT-SRV-ATTR:hypervisor_hostname'))
627 # Configure to run inter-node, but not enough node to run
628 self.config.inter_node = False
629 LOG.info('Using intra-node instead of inter-node.')
631 self._ensure_vms_active()
632 self.compute_nodes = set(self.get_loop_vm_compute_nodes())