summaryrefslogtreecommitdiffstats
path: root/nfvbench/chain_clients.py
diff options
context:
space:
mode:
Diffstat (limited to 'nfvbench/chain_clients.py')
-rw-r--r--nfvbench/chain_clients.py564
1 files changed, 564 insertions, 0 deletions
diff --git a/nfvbench/chain_clients.py b/nfvbench/chain_clients.py
new file mode 100644
index 0000000..4be050f
--- /dev/null
+++ b/nfvbench/chain_clients.py
@@ -0,0 +1,564 @@
+#!/usr/bin/env python
+# Copyright 2016 Cisco Systems, Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import compute
+from glanceclient.v2 import client as glanceclient
+from log import LOG
+from neutronclient.neutron import client as neutronclient
+from novaclient.client import Client
+import os
+import time
+
+
+class StageClientException(Exception):
+ pass
+
+
+class BasicStageClient(object):
+ """Client for spawning and accessing the VM setup"""
+
+ nfvbenchvm_config_name = 'nfvbenchvm.conf'
+
+ def __init__(self, config, cred):
+ self.comp = None
+ self.image_instance = None
+ self.config = config
+ self.cred = cred
+ self.nets = []
+ self.vms = []
+ self.created_ports = []
+ self.ports = {}
+ self.compute_nodes = set([])
+ self.comp = None
+ self.neutron = None
+ self.flavor_type = {'is_reuse': True, 'flavor': None}
+ self.host_ips = None
+
+ def _ensure_vms_active(self):
+ for _ in range(self.config.generic_retry_count):
+ for i, instance in enumerate(self.vms):
+ if instance.status == 'ACTIVE':
+ continue
+ is_reuse = getattr(instance, 'is_reuse', True)
+ instance = self.comp.poll_server(instance)
+ if instance.status == 'ERROR':
+ raise StageClientException('Instance creation error: %s' %
+ instance.fault['message'])
+ if instance.status == 'ACTIVE':
+ LOG.info('Created instance: %s', instance.name)
+ self.vms[i] = instance
+ setattr(self.vms[i], 'is_reuse', is_reuse)
+ if all(map(lambda instance: instance.status == 'ACTIVE', self.vms)):
+ return
+ time.sleep(self.config.generic_poll_sec)
+ raise StageClientException('Timed out waiting for VMs to spawn')
+
+ def _setup_openstack_clients(self):
+ self.session = self.cred.get_session()
+ nova_client = Client(2, session=self.session)
+ self.neutron = neutronclient.Client('2.0', session=self.session)
+ self.glance_client = glanceclient.Client('2',
+ session=self.session)
+ self.comp = compute.Compute(nova_client, self.glance_client, self.neutron, self.config)
+
+ def _lookup_network(self, network_name):
+ networks = self.neutron.list_networks(name=network_name)
+ return networks['networks'][0] if networks['networks'] else None
+
+ def _create_net(self, name, subnet, cidr, network_type=None, segmentation_id=None):
+ network = self._lookup_network(name)
+ if network:
+ phys_net = self.config.internal_networks.physical_network
+ if segmentation_id is not None and phys_net is not None:
+ if network['provider:segmentation_id'] != segmentation_id:
+ raise StageClientException("Mismatch of 'segmentation_id' for reused "
+ "network '{net}'. Network has id '{seg_id1}', "
+ "configuration requires '{seg_id2}'."
+ .format(net=name,
+ seg_id1=network['provider:segmentation_id'],
+ seg_id2=segmentation_id))
+
+ if network['provider:physical_network'] != phys_net:
+ raise StageClientException("Mismatch of 'physical_network' for reused "
+ "network '{net}'. Network has '{phys1}', "
+ "configuration requires '{phys2}'."
+ .format(net=name,
+ phys1=network['provider:physical_network'],
+ phys2=phys_net))
+
+ LOG.info('Reusing existing network: ' + name)
+ network['is_reuse'] = True
+ return network
+
+ body = {
+ 'network': {
+ 'name': name,
+ 'admin_state_up': True
+ }
+ }
+
+ if network_type:
+ body['network']['provider:network_type'] = network_type
+ phys_net = self.config.internal_networks.physical_network
+ if segmentation_id is not None and phys_net is not None:
+ body['network']['provider:segmentation_id'] = segmentation_id
+ body['network']['provider:physical_network'] = phys_net
+
+ network = self.neutron.create_network(body)['network']
+ body = {
+ 'subnet': {
+ 'name': subnet,
+ 'cidr': cidr,
+ 'network_id': network['id'],
+ 'enable_dhcp': False,
+ 'ip_version': 4,
+ 'dns_nameservers': []
+ }
+ }
+ subnet = self.neutron.create_subnet(body)['subnet']
+ # add subnet id to the network dict since it has just been added
+ network['subnets'] = [subnet['id']]
+ network['is_reuse'] = False
+ LOG.info('Created network: %s.' % name)
+ return network
+
+ def _create_port(self, net):
+ body = {
+ "port": {
+ 'network_id': net['id'],
+ 'binding:vnic_type': 'direct' if self.config.sriov else 'normal'
+ }
+ }
+ port = self.neutron.create_port(body)
+ return port['port']
+
+ def __delete_port(self, port):
+ retry = 0
+ while retry < self.config.generic_retry_count:
+ try:
+ self.neutron.delete_port(port['id'])
+ return
+ except Exception:
+ retry += 1
+ time.sleep(self.config.generic_poll_sec)
+ LOG.error('Unable to delete port: %s' % (port['id']))
+
+ def __delete_net(self, network):
+ retry = 0
+ while retry < self.config.generic_retry_count:
+ try:
+ self.neutron.delete_network(network['id'])
+ return
+ except Exception:
+ retry += 1
+ time.sleep(self.config.generic_poll_sec)
+ LOG.error('Unable to delete network: %s' % (network['name']))
+
+ def __get_server_az(self, server):
+ availability_zone = getattr(server, 'OS-EXT-AZ:availability_zone', None)
+ host = getattr(server, 'OS-EXT-SRV-ATTR:host', None)
+ if availability_zone is None:
+ return None
+ if host is None:
+ return None
+ return availability_zone + ':' + host
+
+ def _lookup_servers(self, name=None, nets=None, az=None, flavor_id=None):
+ error_msg = 'VM with the same name, but non-matching {} found. Aborting.'
+ networks = set(map(lambda net: net['name'], nets)) if nets else None
+ server_list = self.comp.get_server_list()
+ matching_servers = []
+
+ for server in server_list:
+ if name and server.name != name:
+ continue
+
+ if az and self.__get_server_az(server) != az:
+ raise StageClientException(error_msg.format('availability zones'))
+
+ if flavor_id and server.flavor['id'] != flavor_id:
+ raise StageClientException(error_msg.format('flavors'))
+
+ if networks and not set(server.networks.keys()).issuperset(networks):
+ raise StageClientException(error_msg.format('networks'))
+
+ if server.status != "ACTIVE":
+ raise StageClientException(error_msg.format('state'))
+
+ # everything matches
+ matching_servers.append(server)
+
+ return matching_servers
+
+ def _create_server(self, name, ports, az, nfvbenchvm_config):
+ port_ids = map(lambda port: {'port-id': port['id']}, ports)
+ nfvbenchvm_config_location = os.path.join('/etc/', self.nfvbenchvm_config_name)
+ server = self.comp.create_server(name,
+ self.image_instance,
+ self.flavor_type['flavor'],
+ None,
+ port_ids,
+ None,
+ avail_zone=az,
+ user_data=None,
+ config_drive=True,
+ files={nfvbenchvm_config_location: nfvbenchvm_config})
+ if server:
+ setattr(server, 'is_reuse', False)
+ LOG.info('Creating instance: %s on %s' % (name, az))
+ else:
+ raise StageClientException('Unable to create instance: %s.' % (name))
+ return server
+
+ def _setup_resources(self):
+ if not self.image_instance:
+ self.image_instance = self.comp.find_image(self.config.image_name)
+ if self.image_instance is None:
+ if self.config.vm_image_file:
+ LOG.info('%s: image for VM not found, trying to upload it ...'
+ % self.config.image_name)
+ res = self.comp.upload_image_via_url(self.config.image_name,
+ self.config.vm_image_file)
+
+ if not res:
+ raise StageClientException('Error uploading image %s from %s. ABORTING.'
+ % (self.config.image_name,
+ self.config.vm_image_file))
+ self.image_instance = self.comp.find_image(self.config.image_name)
+ else:
+ raise StageClientException('%s: image to launch VM not found. ABORTING.'
+ % self.config.image_name)
+
+ LOG.info('Found image %s to launch VM' % self.config.image_name)
+
+ self.__setup_flavor()
+
+ def __setup_flavor(self):
+ if self.flavor_type.get('flavor', False):
+ return
+
+ self.flavor_type['flavor'] = self.comp.find_flavor(self.config.flavor_type)
+ if self.flavor_type['flavor']:
+ self.flavor_type['is_reuse'] = True
+ else:
+ flavor_dict = self.config.flavor
+ extra_specs = flavor_dict.pop('extra_specs', None)
+
+ self.flavor_type['flavor'] = self.comp.create_flavor(self.config.flavor_type,
+ override=True,
+ **flavor_dict)
+
+ LOG.info("Flavor '%s' was created." % self.config.flavor_type)
+
+ if extra_specs:
+ self.flavor_type['flavor'].set_keys(extra_specs)
+
+ self.flavor_type['is_reuse'] = False
+
+ if self.flavor_type['flavor'] is None:
+ raise StageClientException('%s: flavor to launch VM not found. ABORTING.'
+ % self.config.flavor_type)
+
+ def __delete_flavor(self, flavor):
+ if self.comp.delete_flavor(flavor=flavor):
+ LOG.info("Flavor '%s' deleted" % self.config.flavor_type)
+ self.flavor_type = {'is_reuse': False, 'flavor': None}
+ else:
+ LOG.error('Unable to delete flavor: %s' % self.config.flavor_type)
+
+ def get_config_file(self, chain_index, src_mac, dst_mac):
+ boot_script_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ 'nfvbenchvm/', self.nfvbenchvm_config_name)
+
+ with open(boot_script_file, 'r') as boot_script:
+ content = boot_script.read()
+
+ g1cidr = self.config.generator_config.src_device.gateway_ip_list[chain_index] + '/8'
+ g2cidr = self.config.generator_config.dst_device.gateway_ip_list[chain_index] + '/8'
+
+ vm_config = {
+ 'forwarder': self.config.vm_forwarder,
+ 'tg_gateway1_ip': self.config.traffic_generator.tg_gateway_ip_addrs[0],
+ 'tg_gateway2_ip': self.config.traffic_generator.tg_gateway_ip_addrs[1],
+ 'tg_net1': self.config.traffic_generator.ip_addrs[0],
+ 'tg_net2': self.config.traffic_generator.ip_addrs[1],
+ 'vnf_gateway1_cidr': g1cidr,
+ 'vnf_gateway2_cidr': g2cidr,
+ 'tg_mac1': src_mac,
+ 'tg_mac2': dst_mac
+ }
+
+ return content.format(**vm_config)
+
+ def set_ports(self):
+ """Stores all ports of NFVbench networks."""
+ nets = self.get_networks_uuids()
+ for port in self.neutron.list_ports()['ports']:
+ if port['network_id'] in nets:
+ ports = self.ports.setdefault(port['network_id'], [])
+ ports.append(port)
+
+ def disable_port_security(self):
+ """
+ Disable security at port level.
+ """
+ vm_ids = map(lambda vm: vm.id, self.vms)
+ for net in self.nets:
+ for port in self.ports[net['id']]:
+ if port['device_id'] in vm_ids:
+ self.neutron.update_port(port['id'], {
+ 'port': {
+ 'security_groups': [],
+ 'port_security_enabled': False,
+ }
+ })
+ LOG.info('Security disabled on port {}'.format(port['id']))
+
+ def get_loop_vm_hostnames(self):
+ return [getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname') for vm in self.vms]
+
+ def get_host_ips(self):
+ '''Return the IP adresss(es) of the host compute nodes for this VMclient instance.
+ Returns a list of 1 IP adress or 2 IP addresses (PVVP inter-node)
+ '''
+ if not self.host_ips:
+ # get the hypervisor object from the host name
+ self.host_ips = [self.comp.get_hypervisor(
+ getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')).host_ip
+ for vm in self.vms]
+ return self.host_ips
+
+ def get_loop_vm_compute_nodes(self):
+ compute_nodes = []
+ for vm in self.vms:
+ az = getattr(vm, 'OS-EXT-AZ:availability_zone')
+ hostname = getattr(vm, 'OS-EXT-SRV-ATTR:hypervisor_hostname')
+ compute_nodes.append(az + ':' + hostname)
+ return compute_nodes
+
+ def get_reusable_vm(self, name, nets, az):
+ servers = self._lookup_servers(name=name, nets=nets, az=az,
+ flavor_id=self.flavor_type['flavor'].id)
+ if servers:
+ server = servers[0]
+ LOG.info('Reusing existing server: ' + name)
+ setattr(server, 'is_reuse', True)
+ return server
+ else:
+ return None
+
+ def get_networks_uuids(self):
+ """
+ Extract UUID of used networks. Order is important.
+
+ :return: list of UUIDs of created networks
+ """
+ return [net['id'] for net in self.nets]
+
+ def get_vlans(self):
+ """
+ Extract vlans of used networks. Order is important.
+
+ :return: list of UUIDs of created networks
+ """
+ vlans = []
+ for net in self.nets:
+ assert(net['provider:network_type'] == 'vlan')
+ vlans.append(net['provider:segmentation_id'])
+
+ return vlans
+
+ def setup(self):
+ """
+ Creates two networks and spawn a VM which act as a loop VM connected
+ with the two networks.
+ """
+ self._setup_openstack_clients()
+
+ def dispose(self, only_vm=False):
+ """
+ Deletes the created two networks and the VM.
+ """
+ for vm in self.vms:
+ if vm:
+ if not getattr(vm, 'is_reuse', True):
+ self.comp.delete_server(vm)
+ else:
+ LOG.info('Server %s not removed since it is reused' % vm.name)
+
+ for port in self.created_ports:
+ self.__delete_port(port)
+
+ if not only_vm:
+ for net in self.nets:
+ if 'is_reuse' in net and not net['is_reuse']:
+ self.__delete_net(net)
+ else:
+ LOG.info('Network %s not removed since it is reused' % (net['name']))
+
+ if not self.flavor_type['is_reuse']:
+ self.__delete_flavor(self.flavor_type['flavor'])
+
+
+class EXTStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(EXTStageClient, self).__init__(config, cred)
+
+ def setup(self):
+ super(EXTStageClient, self).setup()
+
+ # Lookup two existing networks
+ for net_name in [self.config.external_networks.left, self.config.external_networks.right]:
+ net = self._lookup_network(net_name)
+ if net:
+ self.nets.append(net)
+ else:
+ raise StageClientException('Existing network {} cannot be found.'.format(net_name))
+
+
+class PVPStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(PVPStageClient, self).__init__(config, cred)
+
+ def get_end_port_macs(self):
+ vm_ids = map(lambda vm: vm.id, self.vms)
+ port_macs = []
+ for index, net in enumerate(self.nets):
+ vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
+ port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
+ return port_macs
+
+ def setup(self):
+ super(PVPStageClient, self).setup()
+ self._setup_resources()
+
+ # Create two networks
+ nets = self.config.internal_networks
+ self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right]])
+
+ az_list = self.comp.get_enabled_az_host_list(required_count=1)
+ if not az_list:
+ raise Exception('Not enough hosts found.')
+
+ az = az_list[0]
+ self.compute_nodes.add(az)
+ for chain_index in xrange(self.config.service_chain_count):
+ name = self.config.loop_vm_name + str(chain_index)
+ reusable_vm = self.get_reusable_vm(name, self.nets, az)
+ if reusable_vm:
+ self.vms.append(reusable_vm)
+ else:
+ config_file = self.get_config_file(chain_index,
+ self.config.generator_config.src_device.mac,
+ self.config.generator_config.dst_device.mac)
+
+ ports = [self._create_port(net) for net in self.nets]
+ self.created_ports.extend(ports)
+ self.vms.append(self._create_server(name, ports, az, config_file))
+ self._ensure_vms_active()
+ self.set_ports()
+
+
+class PVVPStageClient(BasicStageClient):
+
+ def __init__(self, config, cred):
+ super(PVVPStageClient, self).__init__(config, cred)
+
+ def get_end_port_macs(self):
+ port_macs = []
+ for index, net in enumerate(self.nets[:2]):
+ vm_ids = map(lambda vm: vm.id, self.vms[index::2])
+ vm_mac_map = {port['device_id']: port['mac_address'] for port in self.ports[net['id']]}
+ port_macs.append([vm_mac_map[vm_id] for vm_id in vm_ids])
+ return port_macs
+
+ def setup(self):
+ super(PVVPStageClient, self).setup()
+ self._setup_resources()
+
+ # Create two networks
+ nets = self.config.internal_networks
+ self.nets.extend([self._create_net(**n) for n in [nets.left, nets.right, nets.middle]])
+
+ required_count = 2 if self.config.inter_node else 1
+ az_list = self.comp.get_enabled_az_host_list(required_count=required_count)
+
+ if not az_list:
+ raise Exception('Not enough hosts found.')
+
+ az1 = az2 = az_list[0]
+ if self.config.inter_node:
+ if len(az_list) > 1:
+ az1 = az_list[0]
+ az2 = az_list[1]
+ else:
+ # fallback to intra-node
+ az1 = az2 = az_list[0]
+ self.config.inter_node = False
+ LOG.info('Using intra-node instead of inter-node.')
+
+ self.compute_nodes.add(az1)
+ self.compute_nodes.add(az2)
+
+ # Create loop VMs
+ for chain_index in xrange(self.config.service_chain_count):
+ name0 = self.config.loop_vm_name + str(chain_index) + 'a'
+ # Attach first VM to net0 and net2
+ vm0_nets = self.nets[0::2]
+ reusable_vm0 = self.get_reusable_vm(name0, vm0_nets, az1)
+
+ name1 = self.config.loop_vm_name + str(chain_index) + 'b'
+ # Attach second VM to net1 and net2
+ vm1_nets = self.nets[1:]
+ reusable_vm1 = self.get_reusable_vm(name1, vm1_nets, az2)
+
+ if reusable_vm0 and reusable_vm1:
+ self.vms.extend([reusable_vm0, reusable_vm1])
+ else:
+ vm0_port_net0 = self._create_port(vm0_nets[0])
+ vm0_port_net2 = self._create_port(vm0_nets[1])
+
+ vm1_port_net2 = self._create_port(vm1_nets[1])
+ vm1_port_net1 = self._create_port(vm1_nets[0])
+
+ self.created_ports.extend([vm0_port_net0,
+ vm0_port_net2,
+ vm1_port_net2,
+ vm1_port_net1])
+
+ # order of ports is important for sections below
+ # order of MAC addresses needs to follow order of interfaces
+ # TG0 (net0) -> VM0 (net2) -> VM1 (net2) -> TG1 (net1)
+ config_file0 = self.get_config_file(chain_index,
+ self.config.generator_config.src_device.mac,
+ vm1_port_net2['mac_address'])
+ config_file1 = self.get_config_file(chain_index,
+ vm0_port_net2['mac_address'],
+ self.config.generator_config.dst_device.mac)
+
+ self.vms.append(self._create_server(name0,
+ [vm0_port_net0, vm0_port_net2],
+ az1,
+ config_file0))
+ self.vms.append(self._create_server(name1,
+ [vm1_port_net2, vm1_port_net1],
+ az2,
+ config_file1))
+
+ self._ensure_vms_active()
+ self.set_ports()