ovs-l3ping: A new test utility that allows to detect L3 tunneling issues
[sliver-openvswitch.git] / python / ovstest / args.py
index d6b4756..e90db2a 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2011 Nicira Networks
+# Copyright (c) 2011, 2012 Nicira, Inc.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -17,11 +17,14 @@ ovsargs provide argument parsing for ovs-test utility
 """
 
 import argparse
-import socket
 import re
+import socket
+import sys
 
+CONTROL_PORT = 15531
+DATA_PORT = 15532
 
-def ip(string):
+def ip_address(string):
     """Verifies if string is a valid IP address"""
     try:
         socket.inet_aton(string)
@@ -30,8 +33,28 @@ def ip(string):
     return string
 
 
+def ip_optional_mask(string):
+    """
+    Verifies if string contains a valid IP address and an optional mask in
+    CIDR notation.
+    """
+    token = string.split("/")
+    if len(token) > 2:
+        raise argparse.ArgumentTypeError("IP address and netmask must be "
+                                         "separated by a single slash")
+    elif len(token) == 2:
+        try:
+            mask = int(token[1])
+        except ValueError:
+            raise argparse.ArgumentTypeError("Netmask is not a valid integer")
+        if mask < 0 or mask > 31:
+            raise argparse.ArgumentTypeError("Netmask must be in range 0..31")
+    ip_address(token[0])
+    return string
+
+
 def port(string):
-    """Convert a string into a Port (integer)"""
+    """Convert a string into a TCP/UDP Port (integer)"""
     try:
         port_number = int(string)
         if port_number < 1 or port_number > 65535:
@@ -41,75 +64,218 @@ def port(string):
     return port_number
 
 
-def ip_optional_port(string, default_port):
+def ip_optional_port(string, default_port, ip_callback):
     """Convert a string into IP and Port pair. If port was absent then use
-    default_port as the port"""
+    default_port as the port. The third argument is a callback that verifies
+    whether IP address is given in correct format."""
     value = string.split(':')
     if len(value) == 1:
-        return (ip(value[0]), default_port)
+        return (ip_callback(value[0]), default_port)
     elif len(value) == 2:
-        return (ip(value[0]), port(value[1]))
+        return (ip_callback(value[0]), port(value[1]))
     else:
         raise argparse.ArgumentTypeError("IP address from the optional Port "
                                          "must be colon-separated")
 
 
+def ip_optional_port_port(string, default_port1, default_port2, ip_callback):
+    """Convert a string into IP, Port1, Port2 tuple. If any of ports were
+     missing, then default ports will be used. The fourth argument is a
+     callback that verifies whether IP address is given in the expected
+     format."""
+    value = string.split(':')
+    if len(value) == 1:
+        return (ip_callback(value[0]), default_port1, default_port2)
+    elif len(value) == 2:
+        return (ip_callback(value[0]), port(value[1]), default_port2)
+    elif len(value) == 3:
+        return (ip_callback(value[0]), port(value[1]), port(value[2]))
+    else:
+        raise argparse.ArgumentTypeError("Expected IP address and at most "
+                                         "two colon-separated ports")
+
+
+def vlan_tag(string):
+    """
+    This function verifies whether given string is a correct VLAN tag.
+    """
+    try:
+        value = int(string)
+    except ValueError:
+        raise argparse.ArgumentTypeError("VLAN tag is not a valid integer")
+    if value < 1 or value > 4094:
+        raise argparse.ArgumentTypeError("Not a valid VLAN tag. "
+                                         "VLAN tag should be in the "
+                                         "range 1..4094.")
+    return string
+
 
 def server_endpoint(string):
-    """Converts a string in ControlIP[:ControlPort][,TestIP[:TestPort]] format
+    """Converts a string OuterIP[:OuterPort],InnerIP[/Mask][:InnerPort]
     into a 4-tuple, where:
-    1. First element is ControlIP
-    2. Second element is ControlPort (if omitted will use default value 15531)
-    3  Third element is TestIP (if omitted will be the same as ControlIP)
-    4. Fourth element is TestPort (if omitted will use default value 15532)"""
+    1. First element is OuterIP
+    2. Second element is OuterPort (if omitted will use default value 15531)
+    3  Third element is InnerIP with optional mask
+    4. Fourth element is InnerPort (if omitted will use default value 15532)
+    """
     value = string.split(',')
-    if len(value) == 1: #  TestIP and TestPort are not present
-        ret = ip_optional_port(value[0], 15531)
-        return (ret[0], ret[1], ret[0], 15532)
-    elif len(value) == 2:
-        ret1 = ip_optional_port(value[0], 15531)
-        ret2 = ip_optional_port(value[1], 15532)
+    if len(value) == 2:
+        ret1 = ip_optional_port(value[0], CONTROL_PORT, ip_address)
+        ret2 = ip_optional_port(value[1], DATA_PORT, ip_optional_mask)
         return (ret1[0], ret1[1], ret2[0], ret2[1])
     else:
-        raise argparse.ArgumentTypeError("ControlIP:ControlPort and TestIP:"
-                                         "TestPort must be comma "
-                                         "separated")
+        raise argparse.ArgumentTypeError("OuterIP:OuterPort and InnerIP/Mask:"
+                                         "InnerPort must be comma separated")
+
+
+class UniqueServerAction(argparse.Action):
+    """
+    This custom action class will prevent user from entering multiple ovs-test
+    servers with the same OuterIP. If there is an server with 127.0.0.1 outer
+    IP address then it will be inserted in the front of the list.
+    """
+    def __call__(self, parser, namespace, values, option_string=None):
+        outer_ips = set()
+        endpoints = []
+        for server in values:
+            try:
+                endpoint = server_endpoint(server)
+            except argparse.ArgumentTypeError:
+                raise argparse.ArgumentError(self, str(sys.exc_info()[1]))
+            if endpoint[0] in outer_ips:
+                raise argparse.ArgumentError(self, "Duplicate OuterIPs found")
+            else:
+                outer_ips.add(endpoint[0])
+                if endpoint[0] == "127.0.0.1":
+                    endpoints.insert(0, endpoint)
+                else:
+                    endpoints.append(endpoint)
+        setattr(namespace, self.dest, endpoints)
 
 
 def bandwidth(string):
     """Convert a string (given in bits/second with optional magnitude for
     units) into a long (bytes/second)"""
-    if re.match("^[1-9][0-9]*[MK]?$", string) == None:
+    if re.match("^[1-9][0-9]*[MK]?$", string) is None:
         raise argparse.ArgumentTypeError("Not a valid target bandwidth")
     bwidth = string.replace("M", "000000")
     bwidth = bwidth.replace("K", "000")
-    return long(bwidth) / 8 #  Convert from bits to bytes
+    return long(bwidth) / 8  # Convert from bits to bytes
+
+
+def tunnel_types(string):
+    """
+    This function converts a string into a list that contains all tunnel types
+    that user intended to test.
+    """
+    return string.split(',')
+
+
+def l3_endpoint_client(string):
+    """
+    This function parses command line argument string in
+    remoteIP,localInnerIP[/mask][:ControlPort[:TestPort]],remoteInnerIP[:
+    ControlPort[:TestPort]] format.
+    """
+    try:
+        remote_ip, me, he = string.split(',')
+    except ValueError:
+        raise argparse.ArgumentTypeError("All 3 IP addresses must be comma "
+                                         "separated.")
+    r = (ip_address(remote_ip),
+         ip_optional_port_port(me, CONTROL_PORT, DATA_PORT, ip_optional_mask),
+         ip_optional_port_port(he, CONTROL_PORT, DATA_PORT, ip_address))
+    return r
+
+
+def l3_endpoint_server(string):
+    """
+    This function parses a command line argument string in
+    remoteIP,localInnerIP[/mask][:ControlPort] format.
+    """
+    try:
+        remote_ip, me = string.split(',')
+    except ValueError:
+        raise argparse.ArgumentTypeError("Both IP addresses must be comma "
+                                         "separated.")
+    return (ip_address(remote_ip),
+            ip_optional_port(me, CONTROL_PORT, ip_optional_mask))
 
 
 def ovs_initialize_args():
-    """Initialize args for ovstest utility"""
-    parser = argparse.ArgumentParser(description = 'Test ovs connectivity')
-    parser.add_argument('-v', '--version', action = 'version',
-                version = 'ovs-test (Open vSwitch) @VERSION@')
-    parser.add_argument("-b", "--bandwidth", action = 'store',
-                dest = "targetBandwidth", default = "1M", type = bandwidth,
-                help = 'target bandwidth for UDP tests in bits/second. Use '
+    """
+    Initialize argument parsing for ovs-test utility.
+    """
+    parser = argparse.ArgumentParser(description='Test connectivity '
+                                                'between two Open vSwitches.')
+
+    parser.add_argument('-v', '--version', action='version',
+                version='ovs-test (Open vSwitch) @VERSION@')
+
+    parser.add_argument("-b", "--bandwidth", action='store',
+                dest="targetBandwidth", default="1M", type=bandwidth,
+                help='Target bandwidth for UDP tests in bits/second. Use '
                 'postfix M or K to alter unit magnitude.')
-    group = parser.add_mutually_exclusive_group(required = True)
-    group.add_argument("-s", "--server", action = "store", dest = "port",
-                type = port,
-                help = 'run in server mode and wait client to connect to this '
-                'port')
-    group.add_argument('-c', "--client", action = "store", nargs = 2,
-                dest = "servers", type = server_endpoint,
-                metavar = ("SERVER1", "SERVER2"),
-                help = 'run in client mode and do tests between these '
-                'two servers. Each server must be specified in following '
-                'format - ControlIP[:ControlPort][,TestIP[:TestPort]]. If '
-                'TestIP is omitted then ovs-test server will also use the '
-                'ControlIP for testing purposes. ControlPort is TCP port '
-                'where server will listen for incoming XML/RPC control '
-                'connections to schedule tests (by default 15531). TestPort '
-                'is port which will be used by server to send test traffic '
-                '(by default 15532)')
+    parser.add_argument("-i", "--interval", action='store',
+                dest="testInterval", default=5, type=int,
+                help='Interval for how long to run each test in seconds.')
+
+    parser.add_argument("-t", "--tunnel-modes", action='store',
+                dest="tunnelModes", default=(), type=tunnel_types,
+                help='Do L3 tests with the given tunnel modes.')
+    parser.add_argument("-l", "--vlan-tag", action='store',
+                dest="vlanTag", default=None, type=vlan_tag,
+                help='Do VLAN tests and use the given VLAN tag.')
+    parser.add_argument("-d", "--direct", action='store_true',
+                dest="direct", default=None,
+                help='Do direct tests between both ovs-test servers.')
+
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument("-s", "--server", action="store", dest="port",
+                type=port,
+                help='Run in server mode and wait for the client to '
+                'connect to this port.')
+    group.add_argument('-c', "--client", nargs=2,
+                dest="servers", action=UniqueServerAction,
+                metavar=("SERVER1", "SERVER2"),
+                help='Run in client mode and do tests between these '
+                'two ovs-test servers. Each server must be specified in '
+                'following format - OuterIP:OuterPort,InnerIP[/mask] '
+                ':InnerPort. It is possible to start local instance of '
+                'ovs-test server in the client mode by using 127.0.0.1 as '
+                'OuterIP.')
+    return parser.parse_args()
+
+def l3_initialize_args():
+    """
+    Initialize argument parsing for ovs-l3ping utility.
+    """
+    parser = argparse.ArgumentParser(description='Test L3 tunnel '
+                        'connectivity between two Open vSwitch instances.')
+
+    parser.add_argument('-v', '--version', action='version',
+                version='ovs-l3ping (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.')
+    parser.add_argument("-i", "--interval", action='store',
+                dest="testInterval", default=5, type=int,
+                help='Interval for how long to run each test in seconds.')
+
+    parser.add_argument("-t", "--tunnel-mode", action='store',
+                dest="tunnelMode", required=True,
+                help='Do L3 tests with this tunnel type.')
+
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument("-s", "--server", action="store", dest="server",
+                metavar="TUNNELIP,SERVER",
+                type=l3_endpoint_server,
+                help='Run in server mode and wait for the client to '
+                'connect.')
+    group.add_argument('-c', "--client", action="store", dest="client",
+                metavar="TUNNELIP,CLIENT,SERVER",
+                type=l3_endpoint_client,
+                help='Run in client mode and connect to the server.')
     return parser.parse_args()