From 04a7de082bd221eae3c7004f4e0b99dfa4f8be91 Mon Sep 17 00:00:00 2001 From: ahothan Date: Fri, 28 Jul 2017 17:08:46 -0700 Subject: Initial code drop from Cisco Change-Id: Ie2993886dc8e95c5f73ccdb871add8b96ffcc849 Signed-off-by: ahothan --- nfvbench/chain_clients.py | 564 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100644 nfvbench/chain_clients.py (limited to 'nfvbench/chain_clients.py') 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() -- cgit 1.2.3-korg