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 401d23ad6341b6c93e8e6e8588eb19b905f3c208..c0a7ade571bbf2ccd4f15bd652eeda077c35e0bc 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 8bb45005c2bf82caa2d2761d57f9987121ea1b0e..3b0e9ac66020707c784d7ae864d53655669d5ec5 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 d2898301affc0986b64b93012df707f66b8e29ee..755d727886a88abfe1edab625c7e80e1dcf741dc 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 1f3387a609dd0bdce9688ac85c9798ef130ebd85..c350facfdaf67d0ca17ae1be719267e066d01362 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 ef84d2bbc24c6f218f7511edd177afccfb3ce043..67792985c9809df4ac429d6fb3fecbbc4ffa7b53 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 c722d5df8b2e05ac873503a2bad1349d4d8ab14e..48f2db5fa195bc98be4ae5c009d02dae87993eec 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 420d5fc31b151a7bb3b7b54e01a4e1a7d37fd194..df94dd1e80c6a5c60b8b743e86361dea8341d34e 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 602d785d744c7bd6b67707eb132b8994f753d5b9..549dcad7829f65326d9b20c850ad5c0492dfe4df 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)