summaryrefslogtreecommitdiffstats
path: root/nfvbench
diff options
context:
space:
mode:
authorFrançois-Régis MENGUY <francoisregis.menguy@orange.com>2018-11-27 11:31:00 +0100
committerfmenguy <francoisregis.menguy@orange.com>2019-06-05 15:40:39 +0200
commit4453818e3af2143e099a5f578c4a73b25abbfe58 (patch)
tree66783c9a978916033659ed6fe0e89035c301202b /nfvbench
parenta3578fdb7496e7f1234eef4ac73086ba1d57fcf2 (diff)
Add L3 traffic management with Neutron routers
Change-Id: Ic9bff87e0d78652de28b3a756f9ebc342983cfbb Signed-off-by: fmenguy <francoisregis.menguy@orange.com>
Diffstat (limited to 'nfvbench')
-rwxr-xr-xnfvbench/cfg.default.yaml40
-rw-r--r--nfvbench/chain_router.py186
-rw-r--r--nfvbench/chain_runner.py6
-rw-r--r--nfvbench/chaining.py135
-rw-r--r--nfvbench/cleanup.py237
-rw-r--r--nfvbench/nfvbench.py5
-rwxr-xr-xnfvbench/traffic_client.py21
7 files changed, 551 insertions, 79 deletions
diff --git a/nfvbench/cfg.default.yaml b/nfvbench/cfg.default.yaml
index b2b9f49..0d6edd8 100755
--- a/nfvbench/cfg.default.yaml
+++ b/nfvbench/cfg.default.yaml
@@ -162,6 +162,7 @@ traffic_generator:
# chain count consecutive IP addresses spaced by tg_gateway_ip_addrs_step will be used
# `tg_gateway_ip_addrs__step`: step for generating traffic generator gateway sequences. default is 0.0.0.1
tg_gateway_ip_addrs: ['1.1.0.100', '2.2.0.100']
+ tg_gateway_ip_cidrs: ['1.1.0.0/24','2.2.0.0/24']
tg_gateway_ip_addrs_step: 0.0.0.1
# `gateway_ip_addrs`: base IPs of VNF router gateways (left and right), quantity used depends on chain count
# must correspond to the public IP on the left and right networks
@@ -465,6 +466,40 @@ external_networks:
left:
right:
+# PVP with L3 router in the packet path only.
+# Only use when l3_router option is True (see l3_router)
+# Prefix names of edge networks which will be used to send traffic via traffic generator.
+# If a network with given name already exists it will be reused.
+# Otherwise a new edge network will be created with that name, subnet and CIDR.
+#
+# gateway can be set in case of L3 traffic with edge networks - refer to edge_networks
+#
+# segmentation_id can be set to enforce a specific VLAN id - by default (empty) the VLAN id
+# will be assigned by Neutron.
+# Must be unique for each network
+# physical_network can be set to pick a specific phsyical network - by default (empty) the
+# default physical network will be picked
+#
+edge_networks:
+ left:
+ name: 'nfvbench-net2'
+ router_name: 'router_left'
+ subnet: 'nfvbench-subnet2'
+ cidr: '192.168.3.0/24'
+ gateway:
+ network_type:
+ segmentation_id:
+ physical_network:
+ right:
+ name: 'nfvbench-net3'
+ router_name: 'router_right'
+ subnet: 'nfvbench-subnet3'
+ cidr: '192.168.4.0/24'
+ gateway:
+ network_type:
+ segmentation_id:
+ physical_network:
+
# Use 'true' to enable VXLAN encapsulation support and sent by the traffic generator
# When this option enabled internal networks 'network type' parameter value should be 'vxlan'
vxlan: false
@@ -525,6 +560,11 @@ traffic:
# Can be overriden by --no-traffic
no_traffic: false
+# Use an L3 router in the packet path. This option if set will create or reuse an openstack neutron
+# router (PVP, PVVP) or reuse an existing L3 router (EXT) to route traffic to the destination VM.
+# Can be overriden by --l3-router
+l3_router: false
+
# Test configuration
# The rate pps for traffic going in reverse direction in case of unidirectional flow. Default to 1.
diff --git a/nfvbench/chain_router.py b/nfvbench/chain_router.py
new file mode 100644
index 0000000..9372716
--- /dev/null
+++ b/nfvbench/chain_router.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python
+# Copyright 2018 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.
+#
+
+# This module takes care of chaining routers
+#
+"""NFVBENCH CHAIN DISCOVERY/STAGING.
+
+This module takes care of staging/discovering resources that are participating in a
+L3 benchmarking session: routers, networks, ports, routes.
+If a resource is discovered with the same name, it will be reused.
+Otherwise it will be created.
+
+Once created/discovered, instances are checked to be in the active state (ready to pass traffic)
+Configuration parameters that will influence how these resources are staged/related:
+- openstack or no openstack
+- chain type
+- number of chains
+- number of VNF in each chain (PVP, PVVP)
+- SRIOV and middle port SRIOV for port types
+- whether networks are shared across chains or not
+
+There is not traffic generation involved in this module.
+"""
+import time
+
+from netaddr import IPAddress
+from netaddr import IPNetwork
+
+from log import LOG
+
+
+class ChainException(Exception):
+ """Exception while operating the chains."""
+
+ pass
+
+
+class ChainRouter(object):
+ """Could be a shared router across all chains or a chain private router."""
+
+ def __init__(self, manager, name, subnets, routes):
+ """Create a router for given chain."""
+ self.manager = manager
+ self.subnets = subnets
+ self.routes = routes
+ self.name = name
+ self.ports = [None, None]
+ self.reuse = False
+ self.router = None
+ try:
+ self._setup()
+ except Exception:
+ LOG.error("Error creating router %s", self.name)
+ self.delete()
+ raise
+
+ def _setup(self):
+ # Lookup if there is a matching router with same name
+ routers = self.manager.neutron_client.list_routers(name=self.name)
+
+ if routers['routers']:
+ router = routers['routers'][0]
+ # a router of same name already exists, we need to verify it has the same
+ # characteristics
+ if self.subnets:
+ for subnet in self.subnets:
+ if not self.get_router_interface(router['id'], subnet.network['subnets'][0]):
+ raise ChainException("Mismatch of 'subnet_id' for reused "
+ "router '{router}'.Router has no subnet id '{sub_id}'."
+ .format(router=self.name,
+ sub_id=subnet.network['subnets'][0]))
+ interfaces = self.manager.neutron_client.list_ports(device_id=router['id'])['ports']
+ for interface in interfaces:
+ if self.is_ip_in_network(
+ interface['fixed_ips'][0]['ip_address'],
+ self.manager.config.traffic_generator.tg_gateway_ip_cidrs[0]) \
+ or self.is_ip_in_network(
+ interface['fixed_ips'][0]['ip_address'],
+ self.manager.config.traffic_generator.tg_gateway_ip_cidrs[1]):
+ self.ports[0] = interface
+ else:
+ self.ports[1] = interface
+ if self.routes:
+ for route in self.routes:
+ if route not in router['routes']:
+ LOG.info("Mismatch of 'router' for reused router '%s'."
+ "Router has no existing route destination '%s', "
+ "and nexthop '%s'.", self.name,
+ route['destination'],
+ route['nexthop'])
+ LOG.info("New route added to router %s for reused ", self.name)
+ body = {
+ 'router': {
+ 'routes': self.routes
+ }
+ }
+ self.manager.neutron_client.update_router(router['id'], body)
+
+ LOG.info('Reusing existing router: %s', self.name)
+ self.reuse = True
+ self.router = router
+ return
+
+ body = {
+ 'router': {
+ 'name': self.name,
+ 'admin_state_up': True
+ }
+ }
+ router = self.manager.neutron_client.create_router(body)['router']
+ router_id = router['id']
+
+ if self.subnets:
+ for subnet in self.subnets:
+ router_interface = {'subnet_id': subnet.network['subnets'][0]}
+ self.manager.neutron_client.add_interface_router(router_id, router_interface)
+ interfaces = self.manager.neutron_client.list_ports(device_id=router_id)['ports']
+ for interface in interfaces:
+ itf = interface['fixed_ips'][0]['ip_address']
+ cidr0 = self.manager.config.traffic_generator.tg_gateway_ip_cidrs[0]
+ cidr1 = self.manager.config.traffic_generator.tg_gateway_ip_cidrs[1]
+ if self.is_ip_in_network(itf, cidr0) or self.is_ip_in_network(itf, cidr1):
+ self.ports[0] = interface
+ else:
+ self.ports[1] = interface
+
+ if self.routes:
+ body = {
+ 'router': {
+ 'routes': self.routes
+ }
+ }
+ self.manager.neutron_client.update_router(router_id, body)
+
+ LOG.info('Created router: %s.', self.name)
+ self.router = self.manager.neutron_client.show_router(router_id)
+
+ def get_uuid(self):
+ """
+ Extract UUID of this router.
+
+ :return: UUID of this router
+ """
+ return self.router['id']
+
+ def get_router_interface(self, router_id, subnet_id):
+ interfaces = self.manager.neutron_client.list_ports(device_id=router_id)['ports']
+ matching_interface = None
+ for interface in interfaces:
+ if interface['fixed_ips'][0]['subnet_id'] == subnet_id:
+ matching_interface = interface
+ return matching_interface
+
+ def is_ip_in_network(self, interface_ip, cidr):
+ return IPAddress(interface_ip) in IPNetwork(cidr)
+
+ def delete(self):
+ """Delete this router."""
+ if not self.reuse and self.router:
+ retry = 0
+ while retry < self.manager.config.generic_retry_count:
+ try:
+ self.manager.neutron_client.delete_router(self.router['id'])
+ LOG.info("Deleted router: %s", self.name)
+ return
+ except Exception:
+ retry += 1
+ LOG.info('Error deleting router %s (retry %d/%d)...',
+ self.name,
+ retry,
+ self.manager.config.generic_retry_count)
+ time.sleep(self.manager.config.generic_poll_sec)
+ LOG.error('Unable to delete router: %s', self.name)
diff --git a/nfvbench/chain_runner.py b/nfvbench/chain_runner.py
index 627e9ea..833373c 100644
--- a/nfvbench/chain_runner.py
+++ b/nfvbench/chain_runner.py
@@ -78,7 +78,7 @@ class ChainRunner(object):
# Note that in the case of EXT+ARP+VxLAN, the dest MACs need to be loaded
# because ARP only operates on the dest VTEP IP not on the VM dest MAC
if not config.l2_loopback and \
- (config.service_chain != ChainType.EXT or config.no_arp or config.vxlan):
+ (config.service_chain != ChainType.EXT or config.no_arp or config.vxlan):
gen_config.set_dest_macs(0, self.chain_manager.get_dest_macs(0))
gen_config.set_dest_macs(1, self.chain_manager.get_dest_macs(1))
@@ -104,8 +104,8 @@ class ChainRunner(object):
self.traffic_client.setup()
if not self.config.no_traffic:
# ARP is needed for EXT chain or VxLAN overlay unless disabled explicitly
- if (self.config.service_chain == ChainType.EXT or self.config.vxlan) and \
- not self.config.no_arp:
+ if (self.config.service_chain == ChainType.EXT or
+ self.config.vxlan or self.config.l3_router) and not self.config.no_arp:
self.traffic_client.ensure_arp_successful()
self.traffic_client.ensure_end_to_end()
diff --git a/nfvbench/chaining.py b/nfvbench/chaining.py
index 898e9ea..3350299 100644
--- a/nfvbench/chaining.py
+++ b/nfvbench/chaining.py
@@ -54,13 +54,16 @@ from neutronclient.neutron import client as neutronclient
from novaclient.client import Client
from attrdict import AttrDict
+from chain_router import ChainRouter
import compute
from log import LOG
from specs import ChainType
-
# Left and right index for network and port lists
LEFT = 0
RIGHT = 1
+# L3 traffic edge networks are at the end of networks list
+EDGE_LEFT = -2
+EDGE_RIGHT = -1
# Name of the VM config file
NFVBENCH_CFG_FILENAME = 'nfvbenchvm.conf'
# full pathame of the VM config in the VM
@@ -76,6 +79,7 @@ class ChainException(Exception):
pass
+
class NetworkEncaps(object):
"""Network encapsulation."""
@@ -174,6 +178,10 @@ class ChainVnfPort(object):
"""Get the MAC address for this port."""
return self.port['mac_address']
+ def get_ip(self):
+ """Get the IP address for this port."""
+ return self.port['fixed_ips'][0]['ip_address']
+
def delete(self):
"""Delete this port instance."""
if self.reuse or not self.port:
@@ -222,6 +230,8 @@ class ChainNetwork(object):
self.reuse = False
self.network = None
self.vlan = None
+ if manager.config.l3_router and hasattr(network_config, 'router_name'):
+ self.router_name = network_config.router_name
try:
self._setup(network_config, lookup_only)
except Exception:
@@ -294,7 +304,7 @@ class ChainNetwork(object):
'network': {
'name': self.name,
'admin_state_up': True
- }
+ }
}
if network_config.network_type:
body['network']['provider:network_type'] = network_config.network_type
@@ -388,6 +398,7 @@ class ChainVnf(object):
# For example if 7 idle interfaces are requested, the corresp. ports will be
# at index 2 to 8
self.ports = []
+ self.routers = []
self.status = None
self.instance = None
self.reuse = False
@@ -397,7 +408,10 @@ class ChainVnf(object):
try:
# the vnf_id is conveniently also the starting index in networks
# for the left and right networks associated to this VNF
- self._setup(networks[vnf_id:vnf_id + 2])
+ if self.manager.config.l3_router:
+ self._setup(networks[vnf_id:vnf_id + 4])
+ else:
+ self._setup(networks[vnf_id:vnf_id + 2])
except Exception:
LOG.error("Error creating VNF %s", self.name)
self.delete()
@@ -406,22 +420,52 @@ class ChainVnf(object):
def _get_vm_config(self, remote_mac_pair):
config = self.manager.config
devices = self.manager.generator_config.devices
+
+ if config.l3_router:
+ tg_gateway1_ip = self.routers[LEFT].ports[1]['fixed_ips'][0][
+ 'ip_address'] # router edge ip left
+ tg_gateway2_ip = self.routers[RIGHT].ports[1]['fixed_ips'][0][
+ 'ip_address'] # router edge ip right
+ tg_mac1 = self.routers[LEFT].ports[1]['mac_address'] # router edge mac left
+ tg_mac2 = self.routers[RIGHT].ports[1]['mac_address'] # router edge mac right
+ # edge cidr mask left
+ vnf_gateway1_cidr = \
+ self.ports[LEFT].get_ip() + self.manager.config.edge_networks.left.cidr[-3:]
+ # edge cidr mask right
+ vnf_gateway2_cidr = \
+ self.ports[RIGHT].get_ip() + self.manager.config.edge_networks.right.cidr[-3:]
+ if config.vm_forwarder != 'vpp':
+ raise ChainException(
+ 'L3 router mode imply to set VPP as VM forwarder.'
+ 'Please update your config file with: vm_forwarder: vpp')
+ else:
+ tg_gateway1_ip = devices[LEFT].tg_gateway_ip_addrs
+ tg_gateway2_ip = devices[RIGHT].tg_gateway_ip_addrs
+ tg_mac1 = remote_mac_pair[0]
+ tg_mac2 = remote_mac_pair[1]
+
+ g1cidr = devices[LEFT].get_gw_ip(
+ self.chain.chain_id) + self.manager.config.internal_networks.left.cidr[-3:]
+ g2cidr = devices[RIGHT].get_gw_ip(
+ self.chain.chain_id) + self.manager.config.internal_networks.right.cidr[-3:]
+
+ vnf_gateway1_cidr = g1cidr
+ vnf_gateway2_cidr = g2cidr
+
with open(BOOT_SCRIPT_PATHNAME, 'r') as boot_script:
content = boot_script.read()
- g1cidr = devices[LEFT].get_gw_ip(self.chain.chain_id) + '/8'
- g2cidr = devices[RIGHT].get_gw_ip(self.chain.chain_id) + '/8'
vm_config = {
'forwarder': config.vm_forwarder,
'intf_mac1': self.ports[LEFT].get_mac(),
'intf_mac2': self.ports[RIGHT].get_mac(),
- 'tg_gateway1_ip': devices[LEFT].tg_gateway_ip_addrs,
- 'tg_gateway2_ip': devices[RIGHT].tg_gateway_ip_addrs,
+ 'tg_gateway1_ip': tg_gateway1_ip,
+ 'tg_gateway2_ip': tg_gateway2_ip,
'tg_net1': devices[LEFT].ip_addrs,
'tg_net2': devices[RIGHT].ip_addrs,
- 'vnf_gateway1_cidr': g1cidr,
- 'vnf_gateway2_cidr': g2cidr,
- 'tg_mac1': remote_mac_pair[0],
- 'tg_mac2': remote_mac_pair[1],
+ 'vnf_gateway1_cidr': vnf_gateway1_cidr,
+ 'vnf_gateway2_cidr': vnf_gateway2_cidr,
+ 'tg_mac1': tg_mac1,
+ 'tg_mac2': tg_mac2,
'vif_mq_size': config.vif_multiqueue_size
}
return content.format(**vm_config)
@@ -505,21 +549,27 @@ class ChainVnf(object):
# Check if we can reuse an instance with same name
for instance in self.manager.existing_instances:
if instance.name == self.name:
+ instance_left = LEFT
+ instance_right = RIGHT
+ # In case of L3 traffic instance use edge networks
+ if self.manager.config.l3_router:
+ instance_left = EDGE_LEFT
+ instance_right = EDGE_RIGHT
# Verify that other instance characteristics match
if instance.flavor['id'] != flavor_id:
self._reuse_exception('Flavor mismatch')
if instance.status != "ACTIVE":
self._reuse_exception('Matching instance is not in ACTIVE state')
# The 2 networks for this instance must also be reused
- if not networks[LEFT].reuse:
- self._reuse_exception('network %s is new' % networks[LEFT].name)
- if not networks[RIGHT].reuse:
- self._reuse_exception('network %s is new' % networks[RIGHT].name)
+ if not networks[instance_left].reuse:
+ self._reuse_exception('network %s is new' % networks[instance_left].name)
+ if not networks[instance_right].reuse:
+ self._reuse_exception('network %s is new' % networks[instance_right].name)
# instance.networks have the network names as keys:
# {'nfvbench-rnet0': ['192.168.2.10'], 'nfvbench-lnet0': ['192.168.1.8']}
- if networks[LEFT].name not in instance.networks:
+ if networks[instance_left].name not in instance.networks:
self._reuse_exception('Left network mismatch')
- if networks[RIGHT].name not in instance.networks:
+ if networks[instance_right].name not in instance.networks:
self._reuse_exception('Right network mismatch')
self.reuse = True
@@ -527,16 +577,51 @@ class ChainVnf(object):
LOG.info('Reusing existing instance %s on %s',
self.name, self.get_hypervisor_name())
# create or reuse/discover 2 ports per instance
- self.ports = [ChainVnfPort(self.name + '-' + str(index),
- self,
- networks[index],
- self._get_vnic_type(index)) for index in [0, 1]]
+ if self.manager.config.l3_router:
+ self.ports = [ChainVnfPort(self.name + '-' + str(index),
+ self,
+ networks[index + 2],
+ self._get_vnic_type(index)) for index in [0, 1]]
+ else:
+ self.ports = [ChainVnfPort(self.name + '-' + str(index),
+ self,
+ networks[index],
+ self._get_vnic_type(index)) for index in [0, 1]]
# create idle networks and ports only if instance is not reused
# if reused, we do not care about idle networks/ports
if not self.reuse:
self._get_idle_networks_ports()
+ # Create neutron routers for L3 traffic use case
+ if self.manager.config.l3_router and self.manager.openstack:
+ internal_nets = networks[:2]
+ if self.manager.config.service_chain == ChainType.PVP:
+ edge_nets = networks[2:]
+ else:
+ edge_nets = networks[3:]
+ subnets_left = [internal_nets[0], edge_nets[0]]
+ routes_left = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
+ 'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
+ 0]},
+ {'destination': self.manager.config.traffic_generator.ip_addrs[1],
+ 'nexthop': self.ports[0].get_ip()}]
+ self.routers.append(
+ ChainRouter(self.manager, edge_nets[0].router_name, subnets_left, routes_left))
+ subnets_right = [internal_nets[1], edge_nets[1]]
+ routes_right = [{'destination': self.manager.config.traffic_generator.ip_addrs[0],
+ 'nexthop': self.ports[1].get_ip()},
+ {'destination': self.manager.config.traffic_generator.ip_addrs[1],
+ 'nexthop': self.manager.config.traffic_generator.tg_gateway_ip_addrs[
+ 1]}]
+ self.routers.append(
+ ChainRouter(self.manager, edge_nets[1].router_name, subnets_right, routes_right))
+ # Overload gateway_ips property with router ip address for ARP and traffic calls
+ self.manager.generator_config.devices[LEFT].set_gw_ip(
+ self.routers[LEFT].ports[0]['fixed_ips'][0]['ip_address']) # router edge ip left)
+ self.manager.generator_config.devices[RIGHT].set_gw_ip(
+ self.routers[RIGHT].ports[0]['fixed_ips'][0]['ip_address']) # router edge ip right)
+
# if no reuse, actual vm creation is deferred after all ports in the chain are created
# since we need to know the next mac in a multi-vnf chain
@@ -659,6 +744,7 @@ class ChainVnf(object):
for network in self.idle_networks:
network.delete()
+
class Chain(object):
"""A class to manage a single chain.
@@ -720,7 +806,8 @@ class Chain(object):
def get_length(self):
"""Get the number of VNF in the chain."""
- return len(self.networks) - 1
+ # Take into account 2 edge networks for routers
+ return len(self.networks) - 3 if self.manager.config.l3_router else len(self.networks) - 1
def _get_remote_mac_pairs(self):
"""Get the list of remote mac pairs for every VNF in the chain.
@@ -1156,6 +1243,10 @@ class ChainManager(object):
net_cfg = [int_nets.left, int_nets.right]
else:
net_cfg = [int_nets.left, int_nets.middle, int_nets.right]
+ if self.config.l3_router:
+ edge_nets = self.config.edge_networks
+ net_cfg.append(edge_nets.left)
+ net_cfg.append(edge_nets.right)
networks = []
try:
for cfg in net_cfg:
diff --git a/nfvbench/cleanup.py b/nfvbench/cleanup.py
index 6b13f69..fc85b5d 100644
--- a/nfvbench/cleanup.py
+++ b/nfvbench/cleanup.py
@@ -25,6 +25,7 @@ from tabulate import tabulate
import credentials as credentials
from log import LOG
+
class ComputeCleaner(object):
"""A cleaner for compute resources."""
@@ -45,30 +46,42 @@ class ComputeCleaner(object):
def get_resource_list(self):
return [["Instance", server.name, server.id] for server in self.servers]
- def clean(self):
- if self.servers:
- for server in self.servers:
- try:
- LOG.info('Deleting instance %s...', server.name)
- self.nova_client.servers.delete(server.id)
- except Exception:
- LOG.exception("Instance %s deletion failed", server.name)
- LOG.info(' Waiting for %d instances to be fully deleted...', len(self.servers))
- retry_count = 15 + len(self.servers) * 5
- while True:
- retry_count -= 1
- self.servers = [server for server in self.servers if self.instance_exists(server)]
- if not self.servers:
- break
+ def get_cleaner_code(self):
+ return "instances"
- if retry_count:
- LOG.info(' %d yet to be deleted by Nova, retries left=%d...',
- len(self.servers), retry_count)
- time.sleep(2)
- else:
- LOG.warning(' instance deletion verification time-out: %d still not deleted',
- len(self.servers))
- break
+ def clean_needed(self, clean_options):
+ if clean_options is None:
+ return True
+ code = self.get_cleaner_code()
+ return code[0] in clean_options
+
+ def clean(self, clean_options):
+ if self.clean_needed(clean_options):
+ if self.servers:
+ for server in self.servers:
+ try:
+ LOG.info('Deleting instance %s...', server.name)
+ self.nova_client.servers.delete(server.id)
+ except Exception:
+ LOG.exception("Instance %s deletion failed", server.name)
+ LOG.info(' Waiting for %d instances to be fully deleted...', len(self.servers))
+ retry_count = 15 + len(self.servers) * 5
+ while True:
+ retry_count -= 1
+ self.servers = [server for server in self.servers if
+ self.instance_exists(server)]
+ if not self.servers:
+ break
+
+ if retry_count:
+ LOG.info(' %d yet to be deleted by Nova, retries left=%d...',
+ len(self.servers), retry_count)
+ time.sleep(2)
+ else:
+ LOG.warning(
+ ' instance deletion verification time-out: %d still not deleted',
+ len(self.servers))
+ break
class NetworkCleaner(object):
@@ -99,21 +112,103 @@ class NetworkCleaner(object):
res_list.extend([["Port", port['name'], port['id']] for port in self.ports])
return res_list
- def clean(self):
- for port in self.ports:
- LOG.info("Deleting port %s...", port['id'])
- try:
- self.neutron_client.delete_port(port['id'])
- except Exception:
- LOG.exception("Port deletion failed")
-
- # associated subnets are automatically deleted by neutron
- for net in self.networks:
- LOG.info("Deleting network %s...", net['name'])
- try:
- self.neutron_client.delete_network(net['id'])
- except Exception:
- LOG.exception("Network deletion failed")
+ def get_cleaner_code(self):
+ return "networks and ports"
+
+ def clean_needed(self, clean_options):
+ if clean_options is None:
+ return True
+ code = self.get_cleaner_code()
+ return code[0] in clean_options
+
+ def clean(self, clean_options):
+ if self.clean_needed(clean_options):
+ for port in self.ports:
+ LOG.info("Deleting port %s...", port['id'])
+ try:
+ self.neutron_client.delete_port(port['id'])
+ except Exception:
+ LOG.exception("Port deletion failed")
+
+ # associated subnets are automatically deleted by neutron
+ for net in self.networks:
+ LOG.info("Deleting network %s...", net['name'])
+ try:
+ self.neutron_client.delete_network(net['id'])
+ except Exception:
+ LOG.exception("Network deletion failed")
+
+
+class RouterCleaner(object):
+ """A cleaner for router resources."""
+
+ def __init__(self, neutron_client, router_names):
+ self.neutron_client = neutron_client
+ LOG.info('Discovering routers...')
+ all_routers = self.neutron_client.list_routers()['routers']
+ self.routers = []
+ self.ports = []
+ self.routes = []
+ rtr_ids = []
+ for rtr in all_routers:
+ rtrname = rtr['name']
+ for name in router_names:
+ if rtrname == name:
+ self.routers.append(rtr)
+ rtr_ids.append(rtr['id'])
+
+ LOG.info('Discovering router routes for router %s...', rtr['name'])
+ all_routes = rtr['routes']
+ for route in all_routes:
+ LOG.info("destination: %s, nexthop: %s", route['destination'],
+ route['nexthop'])
+
+ LOG.info('Discovering router ports for router %s...', rtr['name'])
+ self.ports.extend(self.neutron_client.list_ports(device_id=rtr['id'])['ports'])
+ break
+
+ def get_resource_list(self):
+ res_list = [["Router", rtr['name'], rtr['id']] for rtr in self.routers]
+ return res_list
+
+ def get_cleaner_code(self):
+ return "router"
+
+ def clean_needed(self, clean_options):
+ if clean_options is None:
+ return True
+ code = self.get_cleaner_code()
+ return code[0] in clean_options
+
+ def clean(self, clean_options):
+ if self.clean_needed(clean_options):
+ # associated routes needs to be deleted before deleting routers
+ for rtr in self.routers:
+ LOG.info("Deleting routes for %s...", rtr['name'])
+ try:
+ body = {
+ 'router': {
+ 'routes': []
+ }
+ }
+ self.neutron_client.update_router(rtr['id'], body)
+ except Exception:
+ LOG.exception("Router routes deletion failed")
+ LOG.info("Deleting ports for %s...", rtr['name'])
+ try:
+ for port in self.ports:
+ body = {
+ 'port_id': port['id']
+ }
+ self.neutron_client.remove_interface_router(rtr['id'], body)
+ except Exception:
+ LOG.exception("Router ports deletion failed")
+ LOG.info("Deleting router %s...", rtr['name'])
+ try:
+ self.neutron_client.delete_router(rtr['id'])
+ except Exception:
+ LOG.exception("Router deletion failed")
+
class FlavorCleaner(object):
"""Cleaner for NFVbench flavor."""
@@ -131,13 +226,24 @@ class FlavorCleaner(object):
return [['Flavor', self.name, self.flavor.id]]
return None
- def clean(self):
- if self.flavor:
- LOG.info("Deleting flavor %s...", self.flavor.name)
- try:
- self.flavor.delete()
- except Exception:
- LOG.exception("Flavor deletion failed")
+ def get_cleaner_code(self):
+ return "flavor"
+
+ def clean_needed(self, clean_options):
+ if clean_options is None:
+ return True
+ code = self.get_cleaner_code()
+ return code[0] in clean_options
+
+ def clean(self, clean_options):
+ if self.clean_needed(clean_options):
+ if self.flavor:
+ LOG.info("Deleting flavor %s...", self.flavor.name)
+ try:
+ self.flavor.delete()
+ except Exception:
+ LOG.exception("Flavor deletion failed")
+
class Cleaner(object):
"""Cleaner for all NFVbench resources."""
@@ -148,12 +254,15 @@ class Cleaner(object):
self.neutron_client = nclient.Client('2.0', session=session)
self.nova_client = Client(2, session=session)
network_names = [inet['name'] for inet in config.internal_networks.values()]
+ network_names.extend([inet['name'] for inet in config.edge_networks.values()])
+ router_names = [rtr['router_name'] for rtr in config.edge_networks.values()]
# add idle networks as well
if config.idle_networks.name:
network_names.append(config.idle_networks.name)
self.cleaners = [ComputeCleaner(self.nova_client, config.loop_vm_name),
FlavorCleaner(self.nova_client, config.flavor_type),
- NetworkCleaner(self.neutron_client, network_names)]
+ NetworkCleaner(self.neutron_client, network_names),
+ RouterCleaner(self.neutron_client, router_names)]
def show_resources(self):
"""Show all NFVbench resources."""
@@ -172,11 +281,37 @@ class Cleaner(object):
def clean(self, prompt):
"""Clean all resources."""
- LOG.info("NFVbench will delete all resources shown...")
+ LOG.info("NFVbench will delete resources shown...")
+ clean_options = None
if prompt:
- answer = raw_input("Are you sure? (y/n) ")
+ answer = raw_input("Do you want to delete all ressources? (y/n) ")
if answer.lower() != 'y':
- LOG.info("Exiting without deleting any resource")
- sys.exit(0)
+ print "What kind of resources do you want to delete?"
+ all_option = ""
+ all_option_codes = []
+ for cleaner in self.cleaners:
+ code = cleaner.get_cleaner_code()
+ print "%s: %s" % (code[0], code)
+ all_option += code[0]
+ all_option_codes.append(code)
+ print "a: all resources - a shortcut for '%s'" % all_option
+ all_option_codes.append("all resources")
+ print "q: quit"
+ answer_res = raw_input(":").lower()
+ # Check only first character because answer_res can be "flavor" and it is != all
+ if answer_res[0] == "a":
+ clean_options = all_option
+ elif answer_res[0] != 'q':
+ # if user write complete code instead of shortcuts
+ # Get only first character of clean code to avoid false clean request
+ # i.e "networks and ports" and "router" have 1 letter in common and router clean
+ # will be called even if user ask for networks and ports
+ if answer_res in all_option_codes:
+ clean_options = answer_res[0]
+ else:
+ clean_options = answer_res
+ else:
+ LOG.info("Exiting without deleting any resource")
+ sys.exit(0)
for cleaner in self.cleaners:
- cleaner.clean()
+ cleaner.clean(clean_options)
diff --git a/nfvbench/nfvbench.py b/nfvbench/nfvbench.py
index b2163ba..4a2a285 100644
--- a/nfvbench/nfvbench.py
+++ b/nfvbench/nfvbench.py
@@ -326,6 +326,11 @@ def _parse_opts_from_cli():
action='store',
help='Traffic generator profile to use')
+ parser.add_argument('-l3', '--l3-router', dest='l3_router',
+ default=None,
+ action='store_true',
+ help='Use L3 neutron routers to handle traffic')
+
parser.add_argument('-0', '--no-traffic', dest='no_traffic',
default=None,
action='store_true',
diff --git a/nfvbench/traffic_client.py b/nfvbench/traffic_client.py
index 75c40c1..d69da0e 100755
--- a/nfvbench/traffic_client.py
+++ b/nfvbench/traffic_client.py
@@ -23,7 +23,9 @@ from attrdict import AttrDict
import bitmath
from netaddr import IPNetwork
# pylint: disable=import-error
+from trex.stl.api import Ether
from trex.stl.api import STLError
+from trex.stl.api import UDP
# pylint: enable=import-error
from log import LOG
@@ -241,6 +243,11 @@ class Device(object):
self.vnis = vnis
LOG.info("Port %d: VNIs %s", self.port, self.vnis)
+ def set_gw_ip(self, gateway_ip):
+ self.gw_ip_block = IpBlock(gateway_ip,
+ self.generator_config.gateway_ip_addrs_step,
+ self.chain_count)
+
def get_gw_ip(self, chain_index):
"""Retrieve the IP address assigned for the gateway of a given chain."""
return self.gw_ip_block.get_ip(chain_index)
@@ -611,11 +618,10 @@ class TrafficClient(object):
self.gen.stop_traffic()
self.gen.fetch_capture_packets()
self.gen.stop_capture()
-
for packet in self.gen.packet_list:
mac_id = get_mac_id(packet)
src_mac = ':'.join(["%02x" % ord(x) for x in mac_id])
- if src_mac in mac_map:
+ if src_mac in mac_map and self.is_udp(packet):
port, chain = mac_map[src_mac]
LOG.info('Received packet from mac: %s (chain=%d, port=%d)',
src_mac, chain, port)
@@ -624,9 +630,18 @@ class TrafficClient(object):
if not mac_map:
LOG.info('End-to-end connectivity established')
return
-
+ if self.config.l3_router and not self.config.no_arp:
+ # In case of L3 traffic mode, routers are not able to route traffic
+ # until VM interfaces are up and ARP requests are done
+ LOG.info('Waiting for loopback service completely started...')
+ LOG.info('Sending ARP request to assure end-to-end connectivity established')
+ self.ensure_arp_successful()
raise TrafficClientException('End-to-end connectivity cannot be ensured')
+ def is_udp(self, packet):
+ pkt = Ether(packet['binary'])
+ return UDP in pkt
+
def ensure_arp_successful(self):
"""Resolve all IP using ARP and throw an exception in case of failure."""
dest_macs = self.gen.resolve_arp()