Commit 8d25d9a2 authored by Ansis Atteka's avatar Ansis Atteka

ovs-test: Enhancements to the ovs-test tool

-Implemented support for ovs-test client, so that it could automatically
spawn an ovs-test server process from itself. This reduces the number of
commands the user have to type to get tests running.
-Automated creation of OVS bridges and ports (for VLAN and GRE tests), so that
user would not need to invoke ovs-vsctl manually to switch from direct, 802.1Q
and GRE tests.
-Fixed some pylint reported warnings.
-Fixed ethtool invocation so that we always try to query the physical interface
to get the driver name and version.
-and some others enhancements.

The new usage:
Node1:ovs-test -s 15531
Node2:ovs-test -c 127.0.0.1,1.1.1.1 192.168.122.151,1.1.1.2 -d -l 125 -t gre
Signed-off-by: default avatarAnsis Atteka <aatteka@nicira.com>
parent b20a8f7c
......@@ -8,6 +8,10 @@ post-v1.6.0
Internetwork Control (0xc0).
- Added the granular link health statistics, 'cfm_health', to an
interface.
- ovs-test:
- Added support for spawning ovs-test server from the client.
- Now ovs-test is able to automatically create test bridges and ports.
v1.6.0 - xx xxx xxxx
......
......@@ -6,7 +6,8 @@ ovstest_pyfiles = \
python/ovstest/rpcserver.py \
python/ovstest/tcp.py \
python/ovstest/udp.py \
python/ovstest/util.py
python/ovstest/util.py \
python/ovstest/vswitch.py
ovs_pyfiles = \
python/ovs/__init__.py \
......
# Copyright (c) 2011 Nicira Networks
# Copyright (c) 2011, 2012 Nicira Networks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -17,11 +17,14 @@ ovsargs provide argument parsing for ovs-test utility
"""
import argparse
import socket
import re
import socket
import sys
CONTROL_PORT = 15531
DATA_PORT = 15532
def ip(string):
def ip_address(string):
"""Verifies if string is a valid IP address"""
try:
socket.inet_aton(string)
......@@ -30,8 +33,28 @@ def ip(string):
return string
def ip_optional_mask(string):
"""
Verifies if string contains a valid IP address and an optional mask in
CIDR notation.
"""
token = string.split("/")
if len(token) > 2:
raise argparse.ArgumentTypeError("IP address and netmask must be "
"separated by a single slash")
elif len(token) == 2:
try:
mask = int(token[1])
except ValueError:
raise argparse.ArgumentTypeError("Netmask is not a valid integer")
if mask < 0 or mask > 31:
raise argparse.ArgumentTypeError("Netmask must be in range 0..31")
ip_address(token[0])
return string
def port(string):
"""Convert a string into a Port (integer)"""
"""Convert a string into a TCP/UDP Port (integer)"""
try:
port_number = int(string)
if port_number < 1 or port_number > 65535:
......@@ -41,75 +64,136 @@ def port(string):
return port_number
def ip_optional_port(string, default_port):
def ip_optional_port(string, default_port, ip_callback):
"""Convert a string into IP and Port pair. If port was absent then use
default_port as the port"""
default_port as the port. The third argument is a callback that verifies
whether IP address is given in correct format."""
value = string.split(':')
if len(value) == 1:
return (ip(value[0]), default_port)
return (ip_callback(value[0]), default_port)
elif len(value) == 2:
return (ip(value[0]), port(value[1]))
return (ip_callback(value[0]), port(value[1]))
else:
raise argparse.ArgumentTypeError("IP address from the optional Port "
"must be colon-separated")
def vlan_tag(string):
"""
This function verifies whether given string is a correct VLAN tag.
"""
try:
value = int(string)
except ValueError:
raise argparse.ArgumentTypeError("VLAN tag is not a valid integer")
if value < 1 or value > 4094:
raise argparse.ArgumentTypeError("Not a valid VLAN tag. "
"VLAN tag should be in the "
"range 1..4094.")
return string
def server_endpoint(string):
"""Converts a string in ControlIP[:ControlPort][,TestIP[:TestPort]] format
"""Converts a string OuterIP[:OuterPort],InnerIP[/Mask][:InnerPort]
into a 4-tuple, where:
1. First element is ControlIP
2. Second element is ControlPort (if omitted will use default value 15531)
3 Third element is TestIP (if omitted will be the same as ControlIP)
4. Fourth element is TestPort (if omitted will use default value 15532)"""
1. First element is OuterIP
2. Second element is OuterPort (if omitted will use default value 15531)
3 Third element is InnerIP with optional mask
4. Fourth element is InnerPort (if omitted will use default value 15532)
"""
value = string.split(',')
if len(value) == 1: # TestIP and TestPort are not present
ret = ip_optional_port(value[0], 15531)
return (ret[0], ret[1], ret[0], 15532)
elif len(value) == 2:
ret1 = ip_optional_port(value[0], 15531)
ret2 = ip_optional_port(value[1], 15532)
if len(value) == 2:
ret1 = ip_optional_port(value[0], CONTROL_PORT, ip_address)
ret2 = ip_optional_port(value[1], DATA_PORT, ip_optional_mask)
return (ret1[0], ret1[1], ret2[0], ret2[1])
else:
raise argparse.ArgumentTypeError("ControlIP:ControlPort and TestIP:"
"TestPort must be comma "
"separated")
raise argparse.ArgumentTypeError("OuterIP:OuterPort and InnerIP/Mask:"
"InnerPort must be comma separated")
class UniqueServerAction(argparse.Action):
"""
This custom action class will prevent user from entering multiple ovs-test
servers with the same OuterIP. If there is an server with 127.0.0.1 outer
IP address then it will be inserted in the front of the list.
"""
def __call__(self, parser, namespace, values, option_string=None):
outer_ips = set()
endpoints = []
for server in values:
try:
endpoint = server_endpoint(server)
except argparse.ArgumentTypeError:
raise argparse.ArgumentError(self, str(sys.exc_info()[1]))
if endpoint[0] in outer_ips:
raise argparse.ArgumentError(self, "Duplicate OuterIPs found")
else:
outer_ips.add(endpoint[0])
if endpoint[0] == "127.0.0.1":
endpoints.insert(0, endpoint)
else:
endpoints.append(endpoint)
setattr(namespace, self.dest, endpoints)
def bandwidth(string):
"""Convert a string (given in bits/second with optional magnitude for
units) into a long (bytes/second)"""
if re.match("^[1-9][0-9]*[MK]?$", string) == None:
if re.match("^[1-9][0-9]*[MK]?$", string) is None:
raise argparse.ArgumentTypeError("Not a valid target bandwidth")
bwidth = string.replace("M", "000000")
bwidth = bwidth.replace("K", "000")
return long(bwidth) / 8 # Convert from bits to bytes
return long(bwidth) / 8 # Convert from bits to bytes
def tunnel_types(string):
"""
This function converts a string into a list that contains all tunnel types
that user intended to test.
"""
return string.split(',')
def ovs_initialize_args():
"""Initialize args for ovstest utility"""
parser = argparse.ArgumentParser(description = 'Test ovs connectivity')
parser.add_argument('-v', '--version', action = 'version',
version = 'ovs-test (Open vSwitch) @VERSION@')
parser.add_argument("-b", "--bandwidth", action = 'store',
dest = "targetBandwidth", default = "1M", type = bandwidth,
help = 'target bandwidth for UDP tests in bits/second. Use '
"""
Initialize argument parsing for ovs-test utility.
"""
parser = argparse.ArgumentParser(description='Test connectivity '
'between two Open vSwitches.')
parser.add_argument('-v', '--version', action='version',
version='ovs-test (Open vSwitch) @VERSION@')
parser.add_argument("-b", "--bandwidth", action='store',
dest="targetBandwidth", default="1M", type=bandwidth,
help='Target bandwidth for UDP tests in bits/second. Use '
'postfix M or K to alter unit magnitude.')
group = parser.add_mutually_exclusive_group(required = True)
group.add_argument("-s", "--server", action = "store", dest = "port",
type = port,
help = 'run in server mode and wait client to connect to this '
'port')
group.add_argument('-c', "--client", action = "store", nargs = 2,
dest = "servers", type = server_endpoint,
metavar = ("SERVER1", "SERVER2"),
help = 'run in client mode and do tests between these '
'two servers. Each server must be specified in following '
'format - ControlIP[:ControlPort][,TestIP[:TestPort]]. If '
'TestIP is omitted then ovs-test server will also use the '
'ControlIP for testing purposes. ControlPort is TCP port '
'where server will listen for incoming XML/RPC control '
'connections to schedule tests (by default 15531). TestPort '
'is port which will be used by server to send test traffic '
'(by default 15532)')
parser.add_argument("-i", "--interval", action='store',
dest="testInterval", default=5, type=int,
help='Interval for how long to run each test in seconds.')
parser.add_argument("-t", "--tunnel-modes", action='store',
dest="tunnelModes", default=(), type=tunnel_types,
help='Do L3 tests with the given tunnel modes.')
parser.add_argument("-l", "--vlan-tag", action='store',
dest="vlanTag", default=None, type=vlan_tag,
help='Do VLAN tests and use the given VLAN tag.')
parser.add_argument("-d", "--direct", action='store_true',
dest="direct", default=None,
help='Do direct tests between both ovs-test servers.')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-s", "--server", action="store", dest="port",
type=port,
help='Run in server mode and wait for the client to '
'connect to this port.')
group.add_argument('-c', "--client", nargs=2,
dest="servers", action=UniqueServerAction,
metavar=("SERVER1", "SERVER2"),
help='Run in client mode and do tests between these '
'two ovs-test servers. Each server must be specified in '
'following format - OuterIP:OuterPort,InnerIP[/mask] '
':InnerPort. It is possible to start local instance of '
'ovs-test server in the client mode by using 127.0.0.1 as '
'OuterIP.')
return parser.parse_args()
# Copyright (c) 2011 Nicira Networks
# Copyright (c) 2011, 2012 Nicira Networks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -16,26 +16,36 @@
rpcserver is an XML RPC server that allows RPC client to initiate tests
"""
import exceptions
import sys
import xmlrpclib
from twisted.internet import reactor
from twisted.web import xmlrpc, server
from twisted.internet.error import CannotListenError
import udp
from twisted.web import xmlrpc
from twisted.web import server
import tcp
import args
import udp
import util
import vswitch
class TestArena(xmlrpc.XMLRPC):
"""
This class contains all the functions that ovstest will call
This class contains all the functions that ovs-test client will call
remotely. The caller is responsible to use designated handleIds
for designated methods (e.g. do not mix UDP and TCP handles).
"""
def __init__(self):
xmlrpc.XMLRPC.__init__(self)
xmlrpc.XMLRPC.__init__(self, allowNone=True)
self.handle_id = 1
self.handle_map = {}
self.bridges = set()
self.pbridges = set()
self.ports = set()
self.request = None
def __acquire_handle(self, value):
"""
......@@ -58,6 +68,46 @@ class TestArena(xmlrpc.XMLRPC):
"""
del self.handle_map[handle]
def cleanup(self):
"""
Delete all remaining bridges and ports if ovs-test client did not had
a chance to remove them. It is necessary to call this function if
ovs-test server is abruptly terminated when doing the tests.
"""
for port in self.ports:
# Remove ports that were added to existing bridges
vswitch.ovs_vsctl_del_port_from_bridge(port)
for bridge in self.bridges:
# Remove bridges that were added for L3 tests
vswitch.ovs_vsctl_del_bridge(bridge)
for pbridge in self.pbridges:
# Remove bridges that were added for VLAN tests
vswitch.ovs_vsctl_del_pbridge(pbridge[0], pbridge[1])
def render(self, request):
"""
This method overrides the original XMLRPC.render method so that it
would be possible to get the XML RPC client IP address from the
request object.
"""
self.request = request
return xmlrpc.XMLRPC.render(self, request)
def xmlrpc_get_my_address(self):
"""
Returns the RPC client's IP address.
"""
return self.request.getClientIP()
def xmlrpc_get_my_address_from(self, his_ip, his_port):
"""
Returns the ovs-test server IP address that the other ovs-test server
with the given ip will see.
"""
server1 = xmlrpclib.Server("http://%s:%u/" % (his_ip, his_port))
return server1.get_my_address()
def xmlrpc_create_udp_listener(self, port):
"""
......@@ -171,6 +221,103 @@ class TestArena(xmlrpc.XMLRPC):
return -1
return 0
def xmlrpc_create_test_bridge(self, bridge, iface):
"""
This function creates a physical bridge from iface. It moves the
IP configuration from the physical interface to the bridge.
"""
ret = vswitch.ovs_vsctl_add_bridge(bridge)
if ret == 0:
self.pbridges.add((bridge, iface))
util.interface_up(bridge)
(ip_addr, mask) = util.interface_get_ip(iface)
util.interface_assign_ip(bridge, ip_addr, mask)
util.move_routes(iface, bridge)
util.interface_assign_ip(iface, "0.0.0.0", "255.255.255.255")
ret = vswitch.ovs_vsctl_add_port_to_bridge(bridge, iface)
if ret == 0:
self.ports.add(iface)
else:
util.interface_assign_ip(iface, ip_addr, mask)
util.move_routes(bridge, iface)
vswitch.ovs_vsctl_del_bridge(bridge)
return ret
def xmlrpc_del_test_bridge(self, bridge, iface):
"""
This function deletes the test bridge and moves its IP configuration
back to the physical interface.
"""
ret = vswitch.ovs_vsctl_del_pbridge(bridge, iface)
self.pbridges.discard((bridge, iface))
return ret
def xmlrpc_get_iface_from_bridge(self, brname):
"""
Tries to figure out physical interface from bridge.
"""
return vswitch.ovs_get_physical_interface(brname)
def xmlrpc_create_bridge(self, brname):
"""
Creates an OVS bridge.
"""
ret = vswitch.ovs_vsctl_add_bridge(brname)
if ret == 0:
self.bridges.add(brname)
return ret
def xmlrpc_del_bridge(self, brname):
"""
Deletes an OVS bridge.
"""
ret = vswitch.ovs_vsctl_del_bridge(brname)
if ret == 0:
self.bridges.discard(brname)
return ret
def xmlrpc_is_ovs_bridge(self, bridge):
"""
This function verifies whether given interface is an ovs bridge.
"""
return vswitch.ovs_vsctl_is_ovs_bridge(bridge)
def xmlrpc_add_port_to_bridge(self, bridge, port):
"""
Adds a port to the OVS bridge.
"""
ret = vswitch.ovs_vsctl_add_port_to_bridge(bridge, port)
if ret == 0:
self.ports.add(port)
return ret
def xmlrpc_del_port_from_bridge(self, port):
"""
Removes a port from OVS bridge.
"""
ret = vswitch.ovs_vsctl_del_port_from_bridge(port)
if ret == 0:
self.ports.discard(port)
return ret
def xmlrpc_ovs_vsctl_set(self, table, record, column, key, value):
"""
This function allows to alter OVS database.
"""
return vswitch.ovs_vsctl_set(table, record, column, key, value)
def xmlrpc_interface_up(self, iface):
"""
This function brings up given interface.
"""
return util.interface_up(iface)
def xmlrpc_interface_assign_ip(self, iface, ip_address, mask):
"""
This function allows to assing ip address to the given interface.
"""
return util.interface_assign_ip(iface, ip_address, mask)
def xmlrpc_get_interface(self, address):
"""
......@@ -198,6 +345,17 @@ class TestArena(xmlrpc.XMLRPC):
def start_rpc_server(port):
RPC_SERVER = TestArena()
reactor.listenTCP(port, server.Site(RPC_SERVER))
reactor.run()
"""
This function creates a RPC server and adds it to the Twisted Reactor.
"""
rpc_server = TestArena()
reactor.listenTCP(port, server.Site(rpc_server))
try:
print "Starting RPC server\n"
sys.stdout.flush()
# If this server was started from ovs-test client then we must flush
# STDOUT so that client would know that server is ready to accept
# XML RPC connections.
reactor.run()
finally:
rpc_server.cleanup()
# Copyright (c) 2011 Nicira Networks
# Copyright (c) 2011, 2012 Nicira Networks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -29,14 +29,10 @@ class TcpListenerConnection(Protocol):
def __init__(self):
self.stats = 0
def connectionMade(self):
print "Started TCP Listener connection"
def dataReceived(self, data):
self.stats += len(data)
def connectionLost(self, reason):
print "Stopped TCP Listener connection"
self.factory.stats += self.stats
......@@ -50,16 +46,10 @@ class TcpListenerFactory(Factory):
def __init__(self):
self.stats = 0
def startFactory(self):
print "Starting TCP listener factory"
def stopFactory(self):
print "Stopping TCP listener factory"
def getResults(self):
""" returns the number of bytes received as string"""
#XML RPC does not support 64bit int (http://bugs.python.org/issue2985)
#so we have to convert the amount of bytes into a string
# XML RPC does not support 64bit int (http://bugs.python.org/issue2985)
# so we have to convert the amount of bytes into a string
return str(self.stats)
......@@ -104,18 +94,13 @@ class TcpSenderConnection(Protocol):
"""
def connectionMade(self):
print "Started TCP sender connection"
producer = Producer(self, self.factory.duration)
self.transport.registerProducer(producer, True)
producer.resumeProducing()
def dataReceived(self, data):
print "Sender received data!", data
self.transport.loseConnection()
def connectionLost(self, reason):
print "Stopped TCP sender connection"
class TcpSenderFactory(ClientFactory):
"""
......@@ -128,12 +113,6 @@ class TcpSenderFactory(ClientFactory):
self.duration = duration
self.stats = 0
def startFactory(self):
print "Starting TCP sender factory"
def stopFactory(self):
print "Stopping TCP sender factory"
def getResults(self):
"""Returns amount of bytes sent to the Listener (as a string)"""
return str(self.stats)
# Copyright (c) 2011 Nicira Networks
# Copyright (c) 2011, 2012 Nicira Networks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -16,9 +16,12 @@
ovsudp contains listener and sender classes for UDP protocol
"""
import array
import struct
import time
from twisted.internet.protocol import DatagramProtocol
from twisted.internet.task import LoopingCall
import array, struct, time
class UdpListener(DatagramProtocol):
......@@ -28,18 +31,12 @@ class UdpListener(DatagramProtocol):
def __init__(self):
self.stats = []
def startProtocol(self):
print "Starting UDP listener"
def stopProtocol(self):
print "Stopping UDP listener"
def datagramReceived(self, data, (_1, _2)):
"""This function is called each time datagram is received"""
try:
self.stats.append(struct.unpack_from("Q", data, 0))
except struct.error:
pass #ignore packets that are less than 8 bytes of size
pass # ignore packets that are less than 8 bytes of size
def getResults(self):
"""Returns number of packets that were actually received"""
......@@ -51,7 +48,7 @@ class UdpSender(DatagramProtocol):
Class that will send UDP packets to UDP Listener
"""
def __init__(self, host, count, size, duration):
#LoopingCall does not know whether UDP socket is actually writable
# LoopingCall does not know whether UDP socket is actually writable
self.looper = None
self.host = host
self.count = count
......@@ -61,13 +58,11 @@ class UdpSender(DatagramProtocol):
self.data = array.array('c', 'X' * size)
def startProtocol(self):
print "Starting UDP sender"
self.looper = LoopingCall(self.sendData)
period = self.duration / float(self.count)
self.looper.start(period , now = False)
def stopProtocol(self):
print "Stopping UDP sender"
if (self.looper is not None):
self.looper.stop()
self.looper = None
......
# Copyright (c) 2011 Nicira Networks
# Copyright (c) 2011, 2012 Nicira Networks
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -15,13 +15,28 @@
"""
util module contains some helper function
"""
import socket, struct, fcntl, array, os, subprocess, exceptions
import array
import exceptions
import fcntl
import os
import socket
import struct
import subprocess
import re
def str_ip(ip):
(x1, x2, x3, x4) = struct.unpack("BBBB", ip)
def str_ip(ip_address):
"""
Converts an IP address from binary format to a string.
"""
(x1, x2, x3, x4) = struct.unpack("BBBB", ip_address)
return ("%u.%u.%u.%u") % (x1, x2, x3, x4)
def get_interface_mtu(iface):
"""
Returns MTU of the given interface.
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
indata = iface + ('\0' * (32 - len(iface)))
try:
......@@ -32,6 +47,7 @@ def get_interface_mtu(iface):
return mtu
def get_interface(address):
"""
Finds first interface that has given address
......@@ -50,25 +66,84 @@ def get_interface(address):
name = namestr[i:i + 16].split('\0', 1)[0]
if address == str_ip(namestr[i + 20:i + 24]):
return name
return "" # did not find interface we were looking for
return None # did not find interface we were looking for
def uname():
os_info = os.uname()
return os_info[2] #return only the kernel version number
return os_info[2] # return only the kernel version number
def get_driver(iface):
def start_process(args):
try:
p = subprocess.Popen(
["ethtool", "-i", iface],
p = subprocess.Popen(args,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE)
out, err = p.communicate()
if p.returncode == 0:
lines = out.split("\n")
driver = "%s(%s)" % (lines[0], lines[1]) #driver name + version
else:
driver = "no support for ethtool"
return (p.returncode, out, err)
except exceptions.OSError:
driver = ""
return (-1, None, None)
def get_driver(iface):
ret, out, _err = start_process(["ethtool", "-i", iface])
if ret == 0:
lines = out.splitlines()
driver = "%s(%s)" % (lines[0], lines[1]) # driver name + version
else:
driver = None
return driver
def interface_up(iface):
"""
This function brings given iface up.
"""
ret, _out, _err = start_process(["ifconfig", iface, "up"])
return ret
def interface_assign_ip(iface, ip_addr, mask):
"""
This function allows to assign IP address to an interface. If mask is an
empty string then ifconfig will decide what kind of mask to use. The
caller can also specify the mask by using CIDR notation in ip argument by
leaving the mask argument as an empty string. In case of success this
function returns 0.
"""
args = ["ifconfig", iface, ip_addr]
if mask is not None:
args.append("netmask")
args.append(mask)
ret, _out, _err = start_process(args)
return ret
def interface_get_ip(iface):
"""
This function returns tuple - ip and mask that was assigned to the
interface.
"""
args = ["ifconfig", iface]
ret, out, _err = start_process(args)
if ret == 0:
ip = re.search(r'inet addr:(\S+)', out)
mask = re.search(r'Mask:(\S+)', out)
if ip is not None and mask is not None:
return (ip.group(1), mask.group(1))
else:
return ret
def move_routes(iface1, iface2):
"""
This function moves routes from iface1 to iface2.
"""
args = ["ip", "route", "show", "dev", iface1]
ret, out, _err = start_process(args)