mirror of
https://github.com/openvswitch/ovs
synced 2025-09-05 00:35:33 +00:00
ovs-test: A new tool that allows to diagnose connectivity and performance issues
This tool will be a replacement for the current ovs-vlan-test utility. Besides from connectivity issues it will also be able to detect performance related issues in Open vSwitch setups. Currently it uses UDP and TCP protocols for stressing. Issue #6976
This commit is contained in:
@@ -194,5 +194,5 @@ include vswitchd/automake.mk
|
||||
include ovsdb/automake.mk
|
||||
include rhel/automake.mk
|
||||
include xenserver/automake.mk
|
||||
include python/ovs/automake.mk
|
||||
include python/automake.mk
|
||||
include python/compat/automake.mk
|
||||
|
5
NEWS
5
NEWS
@@ -8,8 +8,13 @@ post-v1.3.0
|
||||
- Added ability to modify TTL in IPv4.
|
||||
- ovs-appctl:
|
||||
- New "fdb/flush" command to flush bridge's MAC learning table.
|
||||
- ovs-test:
|
||||
- A new distributed testing tool that allows one to diagnose performance
|
||||
and connectivity issues. This tool currently is not included in RH or
|
||||
Xen packages.
|
||||
- RHEL packaging now supports integration with Red Hat network scripts.
|
||||
|
||||
|
||||
v1.3.0 - xx xxx xxxx
|
||||
------------------------
|
||||
- OpenFlow:
|
||||
|
3
debian/automake.mk
vendored
3
debian/automake.mk
vendored
@@ -40,6 +40,9 @@ EXTRA_DIST += \
|
||||
debian/openvswitch-switch.postinst \
|
||||
debian/openvswitch-switch.postrm \
|
||||
debian/openvswitch-switch.template \
|
||||
debian/openvswitch-test.dirs \
|
||||
debian/openvswitch-test.install \
|
||||
debian/openvswitch-test.manpages \
|
||||
debian/ovsdbmonitor.install \
|
||||
debian/ovsdbmonitor.manpages \
|
||||
debian/ovs-monitor-ipsec \
|
||||
|
9
debian/control
vendored
9
debian/control
vendored
@@ -138,3 +138,12 @@ Description: Open vSwitch graphical monitoring tool
|
||||
to "ovs-vsctl list <table>").
|
||||
.
|
||||
Open vSwitch is a full-featured software-based Ethernet switch.
|
||||
|
||||
Package: openvswitch-test
|
||||
Architecture: all
|
||||
Depends: python-twisted-web, python-argparse
|
||||
Description: Open vSwitch test package
|
||||
This package contains utilities that are useful to diagnose
|
||||
performance and connectivity issues in Open vSwitch setup.
|
||||
.
|
||||
Open vSwitch is a full-featured software-based Ethernet switch.
|
||||
|
1
debian/openvswitch-test.dirs
vendored
Normal file
1
debian/openvswitch-test.dirs
vendored
Normal file
@@ -0,0 +1 @@
|
||||
usr/share/pyshared/ovstest/
|
2
debian/openvswitch-test.install
vendored
Normal file
2
debian/openvswitch-test.install
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
usr/share/openvswitch/python/ovstest usr/lib/python2.4/site-packages/
|
||||
usr/bin/ovs-test
|
1
debian/openvswitch-test.manpages
vendored
Normal file
1
debian/openvswitch-test.manpages
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_debian/utilities/ovs-test.8
|
2
debian/python-openvswitch.install
vendored
2
debian/python-openvswitch.install
vendored
@@ -1 +1 @@
|
||||
usr/share/openvswitch/python/* usr/lib/python2.4/site-packages/
|
||||
usr/share/openvswitch/python/ovs usr/lib/python2.4/site-packages/
|
||||
|
10
manpages.mk
10
manpages.mk
@@ -150,6 +150,16 @@ utilities/ovs-tcpundump.1.in:
|
||||
lib/common-syn.man:
|
||||
lib/common.man:
|
||||
|
||||
utilities/ovs-test.8: \
|
||||
utilities/ovs-test.8.in \
|
||||
lib/common-syn.man \
|
||||
lib/common.man \
|
||||
utilities/ovs-vlan-bugs.man
|
||||
utilities/ovs-test.8.in:
|
||||
lib/common-syn.man:
|
||||
lib/common.man:
|
||||
utilities/ovs-vlan-bugs.man:
|
||||
|
||||
utilities/ovs-vlan-bug-workaround.8: \
|
||||
utilities/ovs-vlan-bug-workaround.8.in \
|
||||
lib/common.man \
|
||||
|
@@ -1,5 +1,13 @@
|
||||
run_python = PYTHONPATH=$(top_srcdir)/python:$$PYTHON_PATH $(PYTHON)
|
||||
|
||||
ovstest_pyfiles = \
|
||||
python/ovstest/__init__.py \
|
||||
python/ovstest/args.py \
|
||||
python/ovstest/rpcserver.py \
|
||||
python/ovstest/tcp.py \
|
||||
python/ovstest/udp.py \
|
||||
python/ovstest/util.py
|
||||
|
||||
ovs_pyfiles = \
|
||||
python/ovs/__init__.py \
|
||||
python/ovs/daemon.py \
|
||||
@@ -22,10 +30,10 @@ ovs_pyfiles = \
|
||||
python/ovs/timeval.py \
|
||||
python/ovs/vlog.py \
|
||||
python/ovs/util.py
|
||||
EXTRA_DIST += $(ovs_pyfiles) python/ovs/dirs.py
|
||||
EXTRA_DIST += $(ovs_pyfiles) python/ovs/dirs.py $(ovstest_pyfiles)
|
||||
|
||||
if HAVE_PYTHON
|
||||
nobase_pkgdata_DATA = $(ovs_pyfiles)
|
||||
nobase_pkgdata_DATA = $(ovs_pyfiles) $(ovstest_pyfiles)
|
||||
ovs-install-data-local:
|
||||
$(MKDIR_P) python/ovs
|
||||
(echo "import os" && \
|
1
python/ovstest/__init__.py
Normal file
1
python/ovstest/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# This file intentionally left blank.
|
115
python/ovstest/args.py
Normal file
115
python/ovstest/args.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# Copyright (c) 2011 Nicira Networks
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
ovsargs provide argument parsing for ovs-test utility
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import re
|
||||
|
||||
|
||||
def ip(string):
|
||||
"""Verifies if string is a valid IP address"""
|
||||
try:
|
||||
socket.inet_aton(string)
|
||||
except socket.error:
|
||||
raise argparse.ArgumentTypeError("Not a valid IPv4 address")
|
||||
return string
|
||||
|
||||
|
||||
def port(string):
|
||||
"""Convert a string into a Port (integer)"""
|
||||
try:
|
||||
port_number = int(string)
|
||||
if port_number < 1 or port_number > 65535:
|
||||
raise argparse.ArgumentTypeError("Port is out of range")
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError("Port is not an integer")
|
||||
return port_number
|
||||
|
||||
|
||||
def ip_optional_port(string, default_port):
|
||||
"""Convert a string into IP and Port pair. If port was absent then use
|
||||
default_port as the port"""
|
||||
value = string.split(':')
|
||||
if len(value) == 1:
|
||||
return (ip(value[0]), default_port)
|
||||
elif len(value) == 2:
|
||||
return (ip(value[0]), port(value[1]))
|
||||
else:
|
||||
raise argparse.ArgumentTypeError("IP address from the optional Port "
|
||||
"must be colon-separated")
|
||||
|
||||
|
||||
|
||||
def server_endpoint(string):
|
||||
"""Converts a string in ControlIP[:ControlPort][,TestIP[:TestPort]] format
|
||||
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)"""
|
||||
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)
|
||||
return (ret1[0], ret1[1], ret2[0], ret2[1])
|
||||
else:
|
||||
raise argparse.ArgumentTypeError("ControlIP:ControlPort and TestIP:"
|
||||
"TestPort must be comma "
|
||||
"separated")
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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 '
|
||||
'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)')
|
||||
return parser.parse_args()
|
203
python/ovstest/rpcserver.py
Normal file
203
python/ovstest/rpcserver.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Copyright (c) 2011 Nicira Networks
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
rpcserver is an XML RPC server that allows RPC client to initiate tests
|
||||
"""
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.web import xmlrpc, server
|
||||
from twisted.internet.error import CannotListenError
|
||||
import udp
|
||||
import tcp
|
||||
import args
|
||||
import util
|
||||
|
||||
|
||||
class TestArena(xmlrpc.XMLRPC):
|
||||
"""
|
||||
This class contains all the functions that ovstest 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)
|
||||
self.handle_id = 1
|
||||
self.handle_map = {}
|
||||
|
||||
def __acquire_handle(self, value):
|
||||
"""
|
||||
Allocates new handle and assigns value object to it
|
||||
"""
|
||||
handle = self.handle_id
|
||||
self.handle_map[handle] = value
|
||||
self.handle_id += 1
|
||||
return handle
|
||||
|
||||
def __get_handle_resources(self, handle):
|
||||
"""
|
||||
Return resources that were assigned to handle
|
||||
"""
|
||||
return self.handle_map[handle]
|
||||
|
||||
def __delete_handle(self, handle):
|
||||
"""
|
||||
Releases handle from handle_map
|
||||
"""
|
||||
del self.handle_map[handle]
|
||||
|
||||
|
||||
def xmlrpc_create_udp_listener(self, port):
|
||||
"""
|
||||
Creates a UDP listener that will receive packets from UDP sender
|
||||
"""
|
||||
try:
|
||||
listener = udp.UdpListener()
|
||||
reactor.listenUDP(port, listener)
|
||||
handle_id = self.__acquire_handle(listener)
|
||||
except CannotListenError:
|
||||
return -1
|
||||
return handle_id
|
||||
|
||||
def xmlrpc_create_udp_sender(self, host, count, size, duration):
|
||||
"""
|
||||
Send UDP datagrams to UDP listener
|
||||
"""
|
||||
sender = udp.UdpSender(tuple(host), count, size, duration)
|
||||
reactor.listenUDP(0, sender)
|
||||
handle_id = self.__acquire_handle(sender)
|
||||
return handle_id
|
||||
|
||||
def xmlrpc_get_udp_listener_results(self, handle):
|
||||
"""
|
||||
Returns number of datagrams that were received
|
||||
"""
|
||||
listener = self.__get_handle_resources(handle)
|
||||
return listener.getResults()
|
||||
|
||||
def xmlrpc_get_udp_sender_results(self, handle):
|
||||
"""
|
||||
Returns number of datagrams that were sent
|
||||
"""
|
||||
sender = self.__get_handle_resources(handle)
|
||||
return sender.getResults()
|
||||
|
||||
def xmlrpc_close_udp_listener(self, handle):
|
||||
"""
|
||||
Releases UdpListener and all its resources
|
||||
"""
|
||||
listener = self.__get_handle_resources(handle)
|
||||
listener.transport.stopListening()
|
||||
self.__delete_handle(handle)
|
||||
return 0
|
||||
|
||||
def xmlrpc_close_udp_sender(self, handle):
|
||||
"""
|
||||
Releases UdpSender and all its resources
|
||||
"""
|
||||
sender = self.__get_handle_resources(handle)
|
||||
sender.transport.stopListening()
|
||||
self.__delete_handle(handle)
|
||||
return 0
|
||||
|
||||
def xmlrpc_create_tcp_listener(self, port):
|
||||
"""
|
||||
Creates a TcpListener that will accept connection from TcpSender
|
||||
"""
|
||||
try:
|
||||
listener = tcp.TcpListenerFactory()
|
||||
port = reactor.listenTCP(port, listener)
|
||||
handle_id = self.__acquire_handle((listener, port))
|
||||
return handle_id
|
||||
except CannotListenError:
|
||||
return -1
|
||||
|
||||
def xmlrpc_create_tcp_sender(self, his_ip, his_port, duration):
|
||||
"""
|
||||
Creates a TcpSender that will connect to TcpListener
|
||||
"""
|
||||
sender = tcp.TcpSenderFactory(duration)
|
||||
connector = reactor.connectTCP(his_ip, his_port, sender)
|
||||
handle_id = self.__acquire_handle((sender, connector))
|
||||
return handle_id
|
||||
|
||||
def xmlrpc_get_tcp_listener_results(self, handle):
|
||||
"""
|
||||
Returns number of bytes received
|
||||
"""
|
||||
(listener, _) = self.__get_handle_resources(handle)
|
||||
return listener.getResults()
|
||||
|
||||
def xmlrpc_get_tcp_sender_results(self, handle):
|
||||
"""
|
||||
Returns number of bytes sent
|
||||
"""
|
||||
(sender, _) = self.__get_handle_resources(handle)
|
||||
return sender.getResults()
|
||||
|
||||
def xmlrpc_close_tcp_listener(self, handle):
|
||||
"""
|
||||
Releases TcpListener and all its resources
|
||||
"""
|
||||
try:
|
||||
(_, port) = self.__get_handle_resources(handle)
|
||||
port.loseConnection()
|
||||
self.__delete_handle(handle)
|
||||
except exceptions.KeyError:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def xmlrpc_close_tcp_sender(self, handle):
|
||||
"""
|
||||
Releases TcpSender and all its resources
|
||||
"""
|
||||
try:
|
||||
(_, connector) = self.__get_handle_resources(handle)
|
||||
connector.disconnect()
|
||||
self.__delete_handle(handle)
|
||||
except exceptions.KeyError:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
|
||||
def xmlrpc_get_interface(self, address):
|
||||
"""
|
||||
Finds first interface that has given address
|
||||
"""
|
||||
return util.get_interface(address)
|
||||
|
||||
def xmlrpc_get_interface_mtu(self, iface):
|
||||
"""
|
||||
Returns MTU of the given interface
|
||||
"""
|
||||
return util.get_interface_mtu(iface)
|
||||
|
||||
def xmlrpc_uname(self):
|
||||
"""
|
||||
Return information about running kernel
|
||||
"""
|
||||
return util.uname()
|
||||
|
||||
def xmlrpc_get_driver(self, iface):
|
||||
"""
|
||||
Returns driver version
|
||||
"""
|
||||
return util.get_driver(iface)
|
||||
|
||||
|
||||
def start_rpc_server(port):
|
||||
RPC_SERVER = TestArena()
|
||||
reactor.listenTCP(port, server.Site(RPC_SERVER))
|
||||
reactor.run()
|
139
python/ovstest/tcp.py
Normal file
139
python/ovstest/tcp.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# Copyright (c) 2011 Nicira Networks
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
tcp module contains listener and sender classes for TCP protocol
|
||||
"""
|
||||
|
||||
from twisted.internet.protocol import Factory, ClientFactory, Protocol
|
||||
from twisted.internet import interfaces
|
||||
from zope.interface import implements
|
||||
import time
|
||||
|
||||
|
||||
class TcpListenerConnection(Protocol):
|
||||
"""
|
||||
This per-connection class is instantiated each time sender connects
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class TcpListenerFactory(Factory):
|
||||
"""
|
||||
This per-listening socket class is used to
|
||||
instantiate TcpListenerConnections
|
||||
"""
|
||||
protocol = TcpListenerConnection
|
||||
|
||||
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
|
||||
return str(self.stats)
|
||||
|
||||
|
||||
class Producer(object):
|
||||
implements(interfaces.IPushProducer)
|
||||
"""
|
||||
This producer class generates infinite byte stream for a specified time
|
||||
duration
|
||||
"""
|
||||
def __init__(self, proto, duration):
|
||||
self.proto = proto
|
||||
self.start = time.time()
|
||||
self.produced = 0
|
||||
self.paused = False
|
||||
self.data = "X" * 65535
|
||||
self.duration = duration
|
||||
|
||||
def pauseProducing(self):
|
||||
"""This function is called whenever write() to socket would block"""
|
||||
self.paused = True
|
||||
|
||||
def resumeProducing(self):
|
||||
"""This function is called whenever socket becomes writable"""
|
||||
self.paused = False
|
||||
current = time.time()
|
||||
while (not self.paused) and (current < self.start + self.duration):
|
||||
self.proto.transport.write(self.data)
|
||||
self.produced += len(self.data)
|
||||
current = time.time()
|
||||
if current >= self.start + self.duration:
|
||||
self.proto.factory.stats += self.produced
|
||||
self.proto.transport.unregisterProducer()
|
||||
self.proto.transport.loseConnection()
|
||||
|
||||
def stopProducing(self):
|
||||
pass
|
||||
|
||||
|
||||
class TcpSenderConnection(Protocol):
|
||||
"""
|
||||
TCP connection instance class that sends all traffic at full speed.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
This factory is responsible to instantiate TcpSenderConnection classes
|
||||
each time sender initiates connection
|
||||
"""
|
||||
protocol = TcpSenderConnection
|
||||
|
||||
def __init__(self, duration):
|
||||
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)
|
90
python/ovstest/udp.py
Normal file
90
python/ovstest/udp.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2011 Nicira Networks
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
ovsudp contains listener and sender classes for UDP protocol
|
||||
"""
|
||||
|
||||
from twisted.internet.protocol import DatagramProtocol
|
||||
from twisted.internet.task import LoopingCall
|
||||
import array, struct, time
|
||||
|
||||
|
||||
class UdpListener(DatagramProtocol):
|
||||
"""
|
||||
Class that will listen for incoming UDP packets
|
||||
"""
|
||||
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
|
||||
|
||||
def getResults(self):
|
||||
"""Returns number of packets that were actually received"""
|
||||
return len(self.stats)
|
||||
|
||||
|
||||
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
|
||||
self.looper = None
|
||||
self.host = host
|
||||
self.count = count
|
||||
self.duration = duration
|
||||
self.start = time.time()
|
||||
self.sent = 0
|
||||
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
|
||||
|
||||
def datagramReceived(self, data, (host, port)):
|
||||
pass
|
||||
|
||||
def sendData(self):
|
||||
"""This function is called from LoopingCall"""
|
||||
if self.start + self.duration < time.time():
|
||||
self.looper.stop()
|
||||
self.looper = None
|
||||
|
||||
self.sent += 1
|
||||
struct.pack_into('Q', self.data, 0, self.sent)
|
||||
self.transport.write(self.data, self.host)
|
||||
|
||||
def getResults(self):
|
||||
"""Returns number of packets that were sent"""
|
||||
return self.sent
|
74
python/ovstest/util.py
Normal file
74
python/ovstest/util.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2011 Nicira Networks
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
util module contains some helper function
|
||||
"""
|
||||
import socket, struct, fcntl, array, os, subprocess, exceptions
|
||||
|
||||
def str_ip(ip):
|
||||
(x1, x2, x3, x4) = struct.unpack("BBBB", ip)
|
||||
return ("%u.%u.%u.%u") % (x1, x2, x3, x4)
|
||||
|
||||
def get_interface_mtu(iface):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
indata = iface + ('\0' * (32 - len(iface)))
|
||||
try:
|
||||
outdata = fcntl.ioctl(s.fileno(), 0x8921, indata) # socket.SIOCGIFMTU
|
||||
mtu = struct.unpack("16si12x", outdata)[1]
|
||||
except:
|
||||
return 0
|
||||
|
||||
return mtu
|
||||
|
||||
def get_interface(address):
|
||||
"""
|
||||
Finds first interface that has given address
|
||||
"""
|
||||
bytes = 256 * 32
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
names = array.array('B', '\0' * bytes)
|
||||
outbytes = struct.unpack('iL', fcntl.ioctl(
|
||||
s.fileno(),
|
||||
0x8912, # SIOCGIFCONF
|
||||
struct.pack('iL', bytes, names.buffer_info()[0])
|
||||
))[0]
|
||||
namestr = names.tostring()
|
||||
|
||||
for i in range(0, outbytes, 40):
|
||||
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
|
||||
|
||||
def uname():
|
||||
os_info = os.uname()
|
||||
return os_info[2] #return only the kernel version number
|
||||
|
||||
def get_driver(iface):
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
["ethtool", "-i", iface],
|
||||
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"
|
||||
except exceptions.OSError:
|
||||
driver = ""
|
||||
return driver
|
@@ -9,6 +9,7 @@ if HAVE_PYTHON
|
||||
bin_SCRIPTS += \
|
||||
utilities/ovs-pcap \
|
||||
utilities/ovs-tcpundump \
|
||||
utilities/ovs-test \
|
||||
utilities/ovs-vlan-test
|
||||
endif
|
||||
noinst_SCRIPTS += utilities/ovs-pki-cgi
|
||||
@@ -23,6 +24,7 @@ EXTRA_DIST += \
|
||||
utilities/ovs-pki.in \
|
||||
utilities/ovs-save \
|
||||
utilities/ovs-tcpundump.in \
|
||||
utilities/ovs-test.in \
|
||||
utilities/ovs-vlan-test.in
|
||||
MAN_ROOTS += \
|
||||
utilities/ovs-appctl.8.in \
|
||||
@@ -36,6 +38,7 @@ MAN_ROOTS += \
|
||||
utilities/ovs-pki.8.in \
|
||||
utilities/ovs-tcpundump.1.in \
|
||||
utilities/ovs-vlan-bug-workaround.8.in \
|
||||
utilities/ovs-test.8.in \
|
||||
utilities/ovs-vlan-test.8.in \
|
||||
utilities/ovs-vsctl.8.in
|
||||
MAN_FRAGMENTS += utilities/ovs-vlan-bugs.man
|
||||
@@ -55,6 +58,8 @@ DISTCLEANFILES += \
|
||||
utilities/ovs-pki.8 \
|
||||
utilities/ovs-tcpundump \
|
||||
utilities/ovs-tcpundump.1 \
|
||||
utilities/ovs-test \
|
||||
utilities/ovs-test.8 \
|
||||
utilities/ovs-vlan-test \
|
||||
utilities/ovs-vlan-test.8 \
|
||||
utilities/ovs-vlan-bug-workaround.8 \
|
||||
@@ -71,6 +76,7 @@ man_MANS += \
|
||||
utilities/ovs-pki.8 \
|
||||
utilities/ovs-tcpundump.1 \
|
||||
utilities/ovs-vlan-bug-workaround.8 \
|
||||
utilities/ovs-test.8 \
|
||||
utilities/ovs-vlan-test.8 \
|
||||
utilities/ovs-vsctl.8
|
||||
dist_man_MANS += utilities/ovs-ctl.8
|
||||
|
117
utilities/ovs-test.8.in
Normal file
117
utilities/ovs-test.8.in
Normal file
@@ -0,0 +1,117 @@
|
||||
.TH ovs\-test 1 "October 2011" "Open vSwitch" "Open vSwitch Manual"
|
||||
.
|
||||
.SH NAME
|
||||
\fBovs\-test\fR \- check Linux drivers for performance and vlan problems
|
||||
.
|
||||
.SH SYNOPSIS
|
||||
\fBovs\-test\fR \fB\-s\fR \fIport\fR
|
||||
.PP
|
||||
\fBovs\-test\fR \fB\-c\fR \fIserver1\fR
|
||||
\fIserver2\fR [\fB\-b\fR \fIbandwidth\fR]
|
||||
.so lib/common-syn.man
|
||||
.
|
||||
.SH DESCRIPTION
|
||||
The \fBovs\-test\fR program may be used to check for problems sending
|
||||
802.1Q traffic that Open vSwitch may uncover. These problems can
|
||||
occur when Open vSwitch is used to send 802.1Q traffic through physical
|
||||
interfaces running certain drivers of certain Linux kernel versions. To run a
|
||||
test, configure Open vSwitch to tag traffic originating from \fIserver1\fR and
|
||||
forward it to the \fIserver2\fR. On both servers run \fBovs\-test\fR
|
||||
in server mode. Then, on any other host, run the \fBovs\-test\fR in client
|
||||
mode. The client will connect to both \fBovs\-test\fR servers and schedule
|
||||
tests between them. \fBovs\-test\fR will perform UDP and TCP tests.
|
||||
.PP
|
||||
UDP tests can report packet loss and achieved bandwidth, because UDP flow
|
||||
control is done inside \fBovs\-test\fR. It is also possible to specify target
|
||||
bandwidth for UDP. By default it is 1Mbit/s.
|
||||
.PP
|
||||
TCP tests report only achieved bandwidth, because kernel TCP stack
|
||||
takes care of flow control and packet loss. TCP tests are essential to detect
|
||||
potential TSO related VLAN issues.
|
||||
.PP
|
||||
To determine whether Open vSwitch is encountering any 802.1Q related problems,
|
||||
the user must compare packet loss and achieved bandwidth in a setup where
|
||||
traffic is being tagged against one where it is not. If in the tagged setup
|
||||
both servers are unable to communicate or the achieved bandwidth is lower,
|
||||
then, most likely, Open vSwitch has encountered a pre-existing kernel or
|
||||
driver bug.
|
||||
.PP
|
||||
Some examples of the types of problems that may be encountered are:
|
||||
.so utilities/ovs-vlan-bugs.man
|
||||
.
|
||||
.SS "Client Mode"
|
||||
An \fBovs\-test\fR client will connect to two \fBovs\-test\fR servers and
|
||||
will ask them to exchange traffic.
|
||||
.
|
||||
.SS "Server Mode"
|
||||
To conduct tests, two \fBovs\-test\fR servers must be running on two different
|
||||
hosts where client can connect. The actual test traffic is exchanged only
|
||||
between both \fBovs\-test\fR server test IP addresses. It is recommended that
|
||||
both servers have their test IP addresses in the same subnet, otherwise one
|
||||
will need to change routing so that the test traffic actually goes through the
|
||||
interface that he originally intended to test.
|
||||
.
|
||||
.SH OPTIONS
|
||||
.
|
||||
.TP
|
||||
\fB\-s\fR, \fB\-\-server\fR \fIport\fR
|
||||
Run in server mode and wait for a client to establish XML RPC Control
|
||||
Connection on TCP \fIport\fR. It is recommended to have ethtool installed on
|
||||
the server so that it could retrieve information about NIC driver.
|
||||
.TP
|
||||
\fB\-c\fR, \fB\-\-client\fR \fIserver1\fR \fIserver2\fR
|
||||
Run in client mode and schedule tests between \fIserver1\fR and \fIserver2\fR,
|
||||
where each \fIserver\fR must be given in following format -
|
||||
ControlIP[:ControlPort][,TestIP[:TestPort]]. If TestIP is omitted then
|
||||
ovs-test server will 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 it is 15531). TestPort
|
||||
is port which will be used by server to listen for test traffic
|
||||
(by default it is 15532).
|
||||
.TP
|
||||
\fB\-b\fR, \fB\-\-bandwidth\fR \fIbandwidth\fR
|
||||
Target bandwidth for UDP tests. The \fIbandwidth\fR must be given in bits per
|
||||
second. It is possible to use postfix M or K to alter the target bandwidth
|
||||
magnitude.
|
||||
.
|
||||
.so lib/common.man
|
||||
.SH EXAMPLES
|
||||
.PP
|
||||
Set up a bridge which forwards traffic originating from \fB1.2.3.4\fR out
|
||||
\fBeth1\fR with VLAN tag 10.
|
||||
.IP
|
||||
.B ovs\-vsctl \-\- add\-br vlan\-br \(rs
|
||||
.IP
|
||||
.B \-\- add\-port vlan\-br eth1 \(rs
|
||||
.IP
|
||||
.B \-\- add\-port vlan\-br vlan\-br\-tag tag=10 \(rs
|
||||
.IP
|
||||
.B \-\- set Interface vlan\-br\-tag type=internal
|
||||
.IP
|
||||
.B ifconfig vlan\-br\-tag up 1.2.3.4
|
||||
.
|
||||
.PP
|
||||
On two different hosts start \fBovs\-test\fR in server mode and tell them to
|
||||
listen on port 15531 for incoming client control connections:
|
||||
.IP
|
||||
.B 1.2.3.4: ovs\-test \-s 15531
|
||||
.IP
|
||||
.B 1.2.3.5: ovs\-test \-s 15531
|
||||
.
|
||||
.PP
|
||||
On any other host start \fBovs\-test\fR in client mode and ask it to connect
|
||||
to those two servers - one at 1.2.3.4 and another at 1.2.3.5 (by default
|
||||
client will use TCP port 15531 to establish control channel).
|
||||
.IP
|
||||
.B ovs\-test -c 1.2.3.4 1.2.3.5
|
||||
.
|
||||
.TP
|
||||
|
||||
.SH SEE ALSO
|
||||
.
|
||||
.BR ovs\-vswitchd (8),
|
||||
.BR ovs\-ofctl (8),
|
||||
.BR ovs\-vsctl (8),
|
||||
.BR ovs\-vlan\-test (8),
|
||||
.BR ethtool (8),
|
||||
.BR uname (1)
|
180
utilities/ovs-test.in
Normal file
180
utilities/ovs-test.in
Normal file
@@ -0,0 +1,180 @@
|
||||
#! @PYTHON@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
ovs test utility that allows to do tests between remote hosts
|
||||
"""
|
||||
|
||||
import twisted
|
||||
import xmlrpclib
|
||||
import time
|
||||
import socket
|
||||
import math
|
||||
from ovstest import args, rpcserver
|
||||
|
||||
|
||||
def bandwidth_to_string(bwidth):
|
||||
"""Convert bandwidth from long to string and add units"""
|
||||
bwidth = bwidth * 8 # Convert back to bits/second
|
||||
if bwidth >= 10000000:
|
||||
return str(int(bwidth / 1000000)) + "Mbps"
|
||||
elif bwidth > 10000:
|
||||
return str(int(bwidth / 1000)) + "Kbps"
|
||||
else:
|
||||
return str(int(bwidth)) + "bps"
|
||||
|
||||
|
||||
def collect_information(node):
|
||||
"""Print information about hosts that will do testing"""
|
||||
print "Node %s:%u " % (node[0], node[1])
|
||||
server1 = xmlrpclib.Server("http://%s:%u/" % (node[0], node[1]))
|
||||
interface_name = server1.get_interface(node[2])
|
||||
uname = server1.uname()
|
||||
mtu = 1500
|
||||
|
||||
if interface_name == "":
|
||||
print ("Could not find interface that has %s IP address."
|
||||
"Make sure that you specified correct Test IP." % (node[2]))
|
||||
else:
|
||||
mtu = server1.get_interface_mtu(interface_name)
|
||||
driver = server1.get_driver(interface_name)
|
||||
print "Will be using %s(%s) with MTU %u" % (interface_name, node[2],
|
||||
mtu)
|
||||
if driver == "":
|
||||
print "Install ethtool on this host to get NIC driver information"
|
||||
else:
|
||||
print "On this host %s has %s." % (interface_name, driver)
|
||||
|
||||
if uname == "":
|
||||
print "Unable to retrieve kernel information. Is this Linux?"
|
||||
else:
|
||||
print "Running kernel %s." % uname
|
||||
print "\n"
|
||||
return mtu
|
||||
|
||||
|
||||
def do_udp_tests(receiver, sender, tbwidth, duration, sender_mtu):
|
||||
"""Schedule UDP tests between receiver and sender"""
|
||||
server1 = xmlrpclib.Server("http://%s:%u/" % (receiver[0], receiver[1]))
|
||||
server2 = xmlrpclib.Server("http://%s:%u/" % (sender[0], sender[1]))
|
||||
|
||||
udpformat = '{0:>15} {1:>15} {2:>15} {3:>15} {4:>15}'
|
||||
|
||||
print ("UDP test from %s:%u to %s:%u with target bandwidth %s" %
|
||||
(sender[0], sender[1], receiver[0], receiver[1],
|
||||
bandwidth_to_string(tbwidth)))
|
||||
print udpformat.format("Datagram Size", "Snt Datagrams", "Rcv Datagrams",
|
||||
"Datagram Loss", "Bandwidth")
|
||||
|
||||
for size in [8, sender_mtu - 100, sender_mtu - 28, sender_mtu]:
|
||||
listen_handle = -1
|
||||
send_handle = -1
|
||||
try:
|
||||
packetcnt = (tbwidth * duration) / size
|
||||
|
||||
listen_handle = server1.create_udp_listener(receiver[3])
|
||||
if listen_handle == -1:
|
||||
print ("Server could not open UDP listening socket on port"
|
||||
" %u. Try to restart the server.\n" % receiver[3])
|
||||
return
|
||||
send_handle = server2.create_udp_sender(
|
||||
(receiver[2], receiver[3]),
|
||||
packetcnt, size, duration)
|
||||
|
||||
#Using sleep here because there is no other synchronization source
|
||||
#that would notify us when all sent packets were received
|
||||
time.sleep(duration + 1)
|
||||
|
||||
rcv_packets = server1.get_udp_listener_results(listen_handle)
|
||||
snt_packets = server2.get_udp_sender_results(send_handle)
|
||||
|
||||
loss = math.ceil(((snt_packets - rcv_packets) * 10000.0) /
|
||||
snt_packets) / 100
|
||||
bwidth = (rcv_packets * size) / duration
|
||||
|
||||
print udpformat.format(size, snt_packets, rcv_packets,
|
||||
'%.2f%%' % loss, bandwidth_to_string(bwidth))
|
||||
finally:
|
||||
if listen_handle != -1:
|
||||
server1.close_udp_listener(listen_handle)
|
||||
if send_handle != -1:
|
||||
server2.close_udp_sender(send_handle)
|
||||
print "\n"
|
||||
|
||||
|
||||
def do_tcp_tests(receiver, sender, duration):
|
||||
"""Schedule TCP tests between receiver and sender"""
|
||||
server1 = xmlrpclib.Server("http://%s:%u/" % (receiver[0], receiver[1]))
|
||||
server2 = xmlrpclib.Server("http://%s:%u/" % (sender[0], sender[1]))
|
||||
|
||||
tcpformat = '{0:>15} {1:>15} {2:>15}'
|
||||
print "TCP test from %s:%u to %s:%u (full speed)" % (sender[0], sender[1],
|
||||
receiver[0], receiver[1])
|
||||
print tcpformat.format("Snt Bytes", "Rcv Bytes", "Bandwidth")
|
||||
|
||||
listen_handle = -1
|
||||
send_handle = -1
|
||||
try:
|
||||
listen_handle = server1.create_tcp_listener(receiver[3])
|
||||
if listen_handle == -1:
|
||||
print ("Server was unable to open TCP listening socket on port"
|
||||
" %u. Try to restart the server.\n" % receiver[3])
|
||||
return
|
||||
send_handle = server2.create_tcp_sender(receiver[2], receiver[3],
|
||||
duration)
|
||||
|
||||
time.sleep(duration + 1)
|
||||
|
||||
rcv_bytes = long(server1.get_tcp_listener_results(listen_handle))
|
||||
snt_bytes = long(server2.get_tcp_sender_results(send_handle))
|
||||
|
||||
bwidth = rcv_bytes / duration
|
||||
|
||||
print tcpformat.format(snt_bytes, rcv_bytes,
|
||||
bandwidth_to_string(bwidth))
|
||||
finally:
|
||||
if listen_handle != -1:
|
||||
server1.close_tcp_listener(listen_handle)
|
||||
if send_handle != -1:
|
||||
server2.close_tcp_sender(send_handle)
|
||||
print "\n"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
ovs_args = args.ovs_initialize_args()
|
||||
|
||||
if ovs_args.port is not None: # Start in server mode
|
||||
print "Starting RPC server"
|
||||
try:
|
||||
rpcserver.start_rpc_server(ovs_args.port)
|
||||
except twisted.internet.error.CannotListenError:
|
||||
print "Couldn't start XMLRPC server on port %u" % ovs_args.port
|
||||
|
||||
elif ovs_args.servers is not None: # Run in client mode
|
||||
node1 = ovs_args.servers[0]
|
||||
node2 = ovs_args.servers[1]
|
||||
bandwidth = ovs_args.targetBandwidth
|
||||
|
||||
mtu_node1 = collect_information(node1)
|
||||
mtu_node2 = collect_information(node2)
|
||||
|
||||
do_udp_tests(node1, node2, bandwidth, 5, mtu_node1)
|
||||
do_udp_tests(node2, node1, bandwidth, 5, mtu_node2)
|
||||
do_tcp_tests(node1, node2, 5)
|
||||
do_tcp_tests(node2, node1, 5)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except socket.error:
|
||||
print "Couldn't establish XMLRPC control channel"
|
@@ -8,6 +8,12 @@
|
||||
.so lib/common-syn.man
|
||||
.
|
||||
.SH DESCRIPTION
|
||||
The \fBovs\-vlan\-test\fR utility has some limitations, for example, it does
|
||||
not use TCP in its tests. Also it does not take into account MTU to detect
|
||||
potential edge cases. To overcome those limitations a new tool was
|
||||
developed \- \fBovs\-test\fR. \fBovs\-test\fR is currently supported only
|
||||
on Debian so, if possible try to use that on instead of \fBovs\-vlan\-test\fR.
|
||||
.PP
|
||||
The \fBovs\-vlan\-test\fR program may be used to check for problems sending
|
||||
802.1Q traffic which may occur when running Open vSwitch. These problems can
|
||||
occur when Open vSwitch is used to send 802.1Q traffic through physical
|
||||
@@ -82,5 +88,6 @@ Run an \fBovs\-vlan\-test\fR client with a control server located at
|
||||
.BR ovs\-vswitchd (8),
|
||||
.BR ovs\-ofctl (8),
|
||||
.BR ovs\-vsctl (8),
|
||||
.BR ovs\-test (8),
|
||||
.BR ethtool (8),
|
||||
.BR uname (1)
|
||||
|
Reference in New Issue
Block a user