ovs-test: A new tool that allows to diagnose connectivity and performance issues
authorAnsis Atteka <aatteka@nicira.com>
Mon, 31 Oct 2011 21:56:08 +0000 (14:56 -0700)
committerAnsis Atteka <aatteka@nicira.com>
Fri, 18 Nov 2011 18:39:20 +0000 (10:39 -0800)
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

21 files changed:
Makefile.am
NEWS
debian/automake.mk
debian/control
debian/openvswitch-test.dirs [new file with mode: 0644]
debian/openvswitch-test.install [new file with mode: 0644]
debian/openvswitch-test.manpages [new file with mode: 0644]
debian/python-openvswitch.install
manpages.mk
python/automake.mk [new file with mode: 0644]
python/ovs/automake.mk [deleted file]
python/ovstest/__init__.py [new file with mode: 0644]
python/ovstest/args.py [new file with mode: 0644]
python/ovstest/rpcserver.py [new file with mode: 0644]
python/ovstest/tcp.py [new file with mode: 0644]
python/ovstest/udp.py [new file with mode: 0644]
python/ovstest/util.py [new file with mode: 0644]
utilities/automake.mk
utilities/ovs-test.8.in [new file with mode: 0644]
utilities/ovs-test.in [new file with mode: 0644]
utilities/ovs-vlan-test.8.in

index 401d23a..c0a7ade 100644 (file)
@@ -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
diff --git a/NEWS b/NEWS
index 8bb4500..3b0e9ac 100644 (file)
--- a/NEWS
+++ b/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:
@@ -88,7 +93,7 @@ v1.2.0 - 03 Aug 2011
       datapath/linux-2.6/compat-2.6 directories.
     - Feature removals:
       - Dropped support for "tun_id_from_cookie" OpenFlow extension.
-           Please use the extensible match extensions instead.
+           Please use the extensible match extensions instead.
       - Removed the Maintenance_Point and Monitor tables in an effort
         to simplify 802.1ag configuration.
     - Performance and scalability improvements
index d289830..755d727 100644 (file)
@@ -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 \
index 1f3387a..c350fac 100644 (file)
@@ -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.
diff --git a/debian/openvswitch-test.dirs b/debian/openvswitch-test.dirs
new file mode 100644 (file)
index 0000000..daaae31
--- /dev/null
@@ -0,0 +1 @@
+usr/share/pyshared/ovstest/
diff --git a/debian/openvswitch-test.install b/debian/openvswitch-test.install
new file mode 100644 (file)
index 0000000..a152aff
--- /dev/null
@@ -0,0 +1,2 @@
+usr/share/openvswitch/python/ovstest usr/lib/python2.4/site-packages/
+usr/bin/ovs-test
diff --git a/debian/openvswitch-test.manpages b/debian/openvswitch-test.manpages
new file mode 100644 (file)
index 0000000..683c978
--- /dev/null
@@ -0,0 +1 @@
+_debian/utilities/ovs-test.8
index ef84d2b..6779298 100644 (file)
@@ -1 +1 @@
-usr/share/openvswitch/python/* usr/lib/python2.4/site-packages/
+usr/share/openvswitch/python/ovs usr/lib/python2.4/site-packages/
index c722d5d..48f2db5 100644 (file)
@@ -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 \
diff --git a/python/automake.mk b/python/automake.mk
new file mode 100644 (file)
index 0000000..089ef36
--- /dev/null
@@ -0,0 +1,56 @@
+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 \
+       python/ovs/db/__init__.py \
+       python/ovs/db/data.py \
+       python/ovs/db/error.py \
+       python/ovs/db/idl.py \
+       python/ovs/db/parser.py \
+       python/ovs/db/schema.py \
+       python/ovs/db/types.py \
+       python/ovs/fatal_signal.py \
+       python/ovs/json.py \
+       python/ovs/jsonrpc.py \
+       python/ovs/ovsuuid.py \
+       python/ovs/poller.py \
+       python/ovs/process.py \
+       python/ovs/reconnect.py \
+       python/ovs/socket_util.py \
+       python/ovs/stream.py \
+       python/ovs/timeval.py \
+       python/ovs/vlog.py \
+       python/ovs/util.py
+EXTRA_DIST += $(ovs_pyfiles) python/ovs/dirs.py $(ovstest_pyfiles)
+
+if HAVE_PYTHON
+nobase_pkgdata_DATA = $(ovs_pyfiles) $(ovstest_pyfiles)
+ovs-install-data-local:
+       $(MKDIR_P) python/ovs
+       (echo "import os" && \
+        echo 'PKGDATADIR = os.environ.get("OVS_PKGDATADIR", """$(pkgdatadir)""")' && \
+        echo 'RUNDIR = os.environ.get("OVS_RUNDIR", """@RUNDIR@""")' && \
+        echo 'LOGDIR = os.environ.get("OVS_LOGDIR", """@LOGDIR@""")' && \
+        echo 'BINDIR = os.environ.get("OVS_BINDIR", """$(bindir)""")') \
+               > python/ovs/dirs.py.tmp
+       $(MKDIR_P) $(DESTDIR)$(pkgdatadir)/python/ovs
+       $(INSTALL_DATA) python/ovs/dirs.py.tmp $(DESTDIR)$(pkgdatadir)/python/ovs/dirs.py
+       rm python/ovs/dirs.py.tmp
+else
+ovs-install-data-local:
+       @:
+endif
+install-data-local: ovs-install-data-local
+
+UNINSTALL_LOCAL += ovs-uninstall-local
+ovs-uninstall-local:
+       rm -f $(DESTDIR)$(pkgdatadir)/python/ovs/dirs.py
diff --git a/python/ovs/automake.mk b/python/ovs/automake.mk
deleted file mode 100644 (file)
index 2247328..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-run_python = PYTHONPATH=$(top_srcdir)/python:$$PYTHON_PATH $(PYTHON)
-
-ovs_pyfiles = \
-       python/ovs/__init__.py \
-       python/ovs/daemon.py \
-       python/ovs/db/__init__.py \
-       python/ovs/db/data.py \
-       python/ovs/db/error.py \
-       python/ovs/db/idl.py \
-       python/ovs/db/parser.py \
-       python/ovs/db/schema.py \
-       python/ovs/db/types.py \
-       python/ovs/fatal_signal.py \
-       python/ovs/json.py \
-       python/ovs/jsonrpc.py \
-       python/ovs/ovsuuid.py \
-       python/ovs/poller.py \
-       python/ovs/process.py \
-       python/ovs/reconnect.py \
-       python/ovs/socket_util.py \
-       python/ovs/stream.py \
-       python/ovs/timeval.py \
-       python/ovs/vlog.py \
-       python/ovs/util.py
-EXTRA_DIST += $(ovs_pyfiles) python/ovs/dirs.py
-
-if HAVE_PYTHON
-nobase_pkgdata_DATA = $(ovs_pyfiles)
-ovs-install-data-local:
-       $(MKDIR_P) python/ovs
-       (echo "import os" && \
-        echo 'PKGDATADIR = os.environ.get("OVS_PKGDATADIR", """$(pkgdatadir)""")' && \
-        echo 'RUNDIR = os.environ.get("OVS_RUNDIR", """@RUNDIR@""")' && \
-        echo 'LOGDIR = os.environ.get("OVS_LOGDIR", """@LOGDIR@""")' && \
-        echo 'BINDIR = os.environ.get("OVS_BINDIR", """$(bindir)""")') \
-               > python/ovs/dirs.py.tmp
-       $(MKDIR_P) $(DESTDIR)$(pkgdatadir)/python/ovs
-       $(INSTALL_DATA) python/ovs/dirs.py.tmp $(DESTDIR)$(pkgdatadir)/python/ovs/dirs.py
-       rm python/ovs/dirs.py.tmp
-else
-ovs-install-data-local:
-       @:
-endif
-install-data-local: ovs-install-data-local
-
-UNINSTALL_LOCAL += ovs-uninstall-local
-ovs-uninstall-local:
-       rm -f $(DESTDIR)$(pkgdatadir)/python/ovs/dirs.py
diff --git a/python/ovstest/__init__.py b/python/ovstest/__init__.py
new file mode 100644 (file)
index 0000000..218d892
--- /dev/null
@@ -0,0 +1 @@
+# This file intentionally left blank.
diff --git a/python/ovstest/args.py b/python/ovstest/args.py
new file mode 100644 (file)
index 0000000..d6b4756
--- /dev/null
@@ -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()
diff --git a/python/ovstest/rpcserver.py b/python/ovstest/rpcserver.py
new file mode 100644 (file)
index 0000000..41d2569
--- /dev/null
@@ -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()
diff --git a/python/ovstest/tcp.py b/python/ovstest/tcp.py
new file mode 100644 (file)
index 0000000..33dc719
--- /dev/null
@@ -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)
diff --git a/python/ovstest/udp.py b/python/ovstest/udp.py
new file mode 100644 (file)
index 0000000..e09569d
--- /dev/null
@@ -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
diff --git a/python/ovstest/util.py b/python/ovstest/util.py
new file mode 100644 (file)
index 0000000..3321e69
--- /dev/null
@@ -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
index 420d5fc..df94dd1 100644 (file)
@@ -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
diff --git a/utilities/ovs-test.8.in b/utilities/ovs-test.8.in
new file mode 100644 (file)
index 0000000..afc8221
--- /dev/null
@@ -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)
diff --git a/utilities/ovs-test.in b/utilities/ovs-test.in
new file mode 100644 (file)
index 0000000..6518dbc
--- /dev/null
@@ -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"
index 602d785..549dcad 100644 (file)
@@ -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)