mock cross_connect test added to test/core/integration.py
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
index 41a123e..396c920 100644 (file)
@@ -10,35 +10,118 @@ from nepi.util import validation
 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
         STATUS_FINISHED
 
+import functools
+import os
+import os.path
+
 NODE = "Node"
 NODEIFACE = "NodeInterface"
 TUNIFACE = "TunInterface"
 APPLICATION = "Application"
+DEPENDENCY = "Dependency"
+NEPIDEPENDENCY = "NepiDependency"
 INTERNET = "Internet"
+NETPIPE = "NetPipe"
 
 PL_TESTBED_ID = "planetlab"
 
+
+### Custom validation functions ###
+def is_addrlist(attribute, value):
+    if not validation.is_string(attribute, value):
+        return False
+    
+    if not value:
+        # No empty strings
+        return False
+    
+    components = value.split(',')
+    
+    for component in components:
+        if '/' in component:
+            addr, mask = component.split('/',1)
+        else:
+            addr, mask = component, 32
+        
+        if mask is not None and not (mask and mask.isdigit()):
+            # No empty or nonnumeric masks
+            return False
+        
+        if not validation.is_ip4_address(attribute, value):
+            # Address part must be ipv4
+            return False
+        
+    return True
+
+def is_portlist(attribute, value):
+    if not validation.is_string(attribute, value):
+        return False
+    
+    if not value:
+        # No empty strings
+        return False
+    
+    components = value.split(',')
+    
+    for component in components:
+        if '-' in component:
+            pfrom, pto = component.split('-',1)
+        else:
+            pfrom = pto = component
+        
+        if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
+            # No empty or nonnumeric ports
+            return False
+        
+    return True
+
+
 ### Connection functions ####
 
-def connect_node_iface_node(testbed_instance, node, iface):
+def connect_node_iface_node(testbed_instance, node_guid, iface_guid):
+    node = testbed_instance._elements[node_guid]
+    iface = testbed_instance._elements[iface_guid]
     iface.node = node
 
-def connect_node_iface_inet(testbed_instance, iface, inet):
+def connect_node_iface_inet(testbed_instance, iface_guid, inet_guid):
+    iface = testbed_instance._elements[iface_guid]
     iface.has_internet = True
 
-def connect_tun_iface_node(testbed_instance, node, iface):
+def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
+    node = testbed_instance._elements[node_guid]
+    iface = testbed_instance._elements[iface_guid]
     if not node.emulation:
-        raise RuntimeError, "Usage of TUN interfaces requires emulation"
+        raise RuntimeError, "Use of TUN interfaces requires emulation"
     iface.node = node
     node.required_vsys.update(('fd_tuntap', 'vif_up'))
 
-def connect_app(testbed_instance, node, app):
+def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
+    iface = testbed_instance._elements[iface_guid]
+    peer_iface = testbed_instance._elements[peer_iface_guid]
+    iface.peer_iface = peer_iface
+    iface.peer_proto = \
+    iface.tun_proto = proto
+
+def connect_dep(testbed_instance, node_guid, app_guid):
+    node = testbed_instance._elements[node_guid]
+    app = testbed_instance._elements[app_guid]
     app.node = node
     
     if app.depends:
         node.required_packages.update(set(
             app.depends.split() ))
     
+    if app.add_to_path:
+        if app.home_path and app.home_path not in node.pythonpath:
+            node.pythonpath.append(app.home_path)
+
+def connect_node_netpipe(testbed_instance, node_guid, netpipe_guid):
+    node = testbed_instance._elements[node_guid]
+    netpipe = testbed_instance._elements[netpipe_guid]
+    if not node.emulation:
+        raise RuntimeError, "Use of NetPipes requires emulation"
+    netpipe.node = node
+    
 
 ### Creation functions ###
 
@@ -52,7 +135,7 @@ def create_node(testbed_instance, guid):
     # by counting connected devices
     dev_guids = testbed_instance.get_connected(guid, "node", "devs")
     num_open_ifaces = sum( # count True values
-        TUNEIFACE == testbed_instance._get_factory_id(guid)
+        NODEIFACE == testbed_instance._get_factory_id(guid)
         for guid in dev_guids )
     element.min_num_external_ifaces = num_open_ifaces
     
@@ -71,6 +154,28 @@ def create_tuniface(testbed_instance, guid):
 def create_application(testbed_instance, guid):
     parameters = testbed_instance._get_parameters(guid)
     element = testbed_instance._make_application(parameters)
+    
+    # Just inject configuration stuff
+    element.home_path = "nepi-app-%s" % (guid,)
+    
+    testbed_instance.elements[guid] = element
+
+def create_dependency(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_dependency(parameters)
+    
+    # Just inject configuration stuff
+    element.home_path = "nepi-dep-%s" % (guid,)
+    
+    testbed_instance.elements[guid] = element
+
+def create_nepi_dependency(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_nepi_dependency(parameters)
+    
+    # Just inject configuration stuff
+    element.home_path = "nepi-nepi-%s" % (guid,)
+    
     testbed_instance.elements[guid] = element
 
 def create_internet(testbed_instance, guid):
@@ -78,6 +183,11 @@ def create_internet(testbed_instance, guid):
     element = testbed_instance._make_internet(parameters)
     testbed_instance.elements[guid] = element
 
+def create_netpipe(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_netpipe(parameters)
+    testbed_instance.elements[guid] = element
+
 ### Start/Stop functions ###
 
 def start_application(testbed_instance, guid):
@@ -111,7 +221,7 @@ def configure_nodeiface(testbed_instance, guid):
     
     # Cannot explicitly configure addresses
     if guid in testbed_instance._add_address:
-        del testbed_instance._add_address[guid]
+        raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
     
     # Get siblings
     node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
@@ -126,18 +236,44 @@ def configure_nodeiface(testbed_instance, guid):
     # Do some validations
     element.validate()
 
-def configure_tuniface(testbed_instance, guid):
+def preconfigure_tuniface(testbed_instance, guid):
     element = testbed_instance._elements[guid]
-    if not guid in testbed_instance._add_address:
-        return
     
-    addresses = testbed_instance._add_address[guid]
-    for address in addresses:
-        (address, netprefix, broadcast) = address
-        raise NotImplementedError, "C'mon... TUNs are hard..."
+    # Set custom addresses if any
+    if guid in testbed_instance._add_address:
+        addresses = testbed_instance._add_address[guid]
+        for address in addresses:
+            (address, netprefix, broadcast) = address
+            element.add_address(address, netprefix, broadcast)
+    
+    # Link to external interface, if any
+    for iface in testbed_instance._elements.itervalues():
+        if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
+            element.external_iface = iface
+            break
+
+    # Set standard TUN attributes
+    element.tun_addr = element.external_iface.address
+    element.tun_port = 15000 + int(guid)
+
+    # Set enabled traces
+    traces = testbed_instance._get_traces(guid)
+    element.capture = 'packets' in traces
     
     # Do some validations
     element.validate()
+    
+    # First-phase setup
+    element.prepare( 
+        'tun-%s' % (guid,),
+        id(element) < id(element.peer_iface) )
+
+def postconfigure_tuniface(testbed_instance, guid):
+    element = testbed_instance._elements[guid]
+    
+    # Second-phase setup
+    element.setup()
+    
 
 def configure_node(testbed_instance, guid):
     node = testbed_instance._elements[guid]
@@ -163,11 +299,6 @@ def configure_node(testbed_instance, guid):
 def configure_application(testbed_instance, guid):
     app = testbed_instance._elements[guid]
     
-    # Just inject configuration stuff
-    app.home_path = "nepi-app-%s" % (guid,)
-    app.ident_path = testbed_instance.sliceSSHKey
-    app.slicename = testbed_instance.slicename
-    
     # Do some validations
     app.validate()
     
@@ -177,6 +308,30 @@ def configure_application(testbed_instance, guid):
     # Install stuff
     app.setup()
 
+def configure_dependency(testbed_instance, guid):
+    dep = testbed_instance._elements[guid]
+    
+    # Do some validations
+    dep.validate()
+    
+    # Wait for dependencies
+    dep.node.wait_dependencies()
+    
+    # Install stuff
+    dep.setup()
+
+def configure_netpipe(testbed_instance, guid):
+    netpipe = testbed_instance._elements[guid]
+    
+    # Do some validations
+    netpipe.validate()
+    
+    # Wait for dependencies
+    netpipe.node.wait_dependencies()
+    
+    # Install rules
+    netpipe.configure()
+
 ### Factory information ###
 
 connector_types = dict({
@@ -192,6 +347,13 @@ connector_types = dict({
                 "max": -1, 
                 "min": 0
             }),
+    "deps": dict({
+                "help": "Connector from node to application dependencies "
+                        "(packages and applications that need to be installed)", 
+                "name": "deps",
+                "max": -1, 
+                "min": 0
+            }),
     "inet": dict({
                 "help": "Connector from network interfaces to the internet", 
                 "name": "inet",
@@ -204,33 +366,82 @@ connector_types = dict({
                 "max": 1, 
                 "min": 1
             }),
+    "pipes": dict({
+                "help": "Connector to a NetPipe", 
+                "name": "pipes",
+                "max": 2, 
+                "min": 0
+            }),
+    
+    "tcp": dict({
+                "help": "ip-ip tunneling over TCP link", 
+                "name": "tcp",
+                "max": 1, 
+                "min": 0
+            }),
+    "udp": dict({
+                "help": "ip-ip tunneling over UDP datagrams", 
+                "name": "udp",
+                "max": 1, 
+                "min": 0
+            }),
    })
 
 connections = [
     dict({
         "from": (TESTBED_ID, NODE, "devs"),
         "to":   (TESTBED_ID, NODEIFACE, "node"),
-        "code": connect_node_iface_node,
+        "init_code": connect_node_iface_node,
         "can_cross": False
     }),
     dict({
         "from": (TESTBED_ID, NODE, "devs"),
         "to":   (TESTBED_ID, TUNIFACE, "node"),
-        "code": connect_tun_iface_node,
+        "init_code": connect_tun_iface_node,
         "can_cross": False
     }),
     dict({
         "from": (TESTBED_ID, NODEIFACE, "inet"),
         "to":   (TESTBED_ID, INTERNET, "devs"),
-        "code": connect_node_iface_inet,
+        "init_code": connect_node_iface_inet,
         "can_cross": False
     }),
     dict({
         "from": (TESTBED_ID, NODE, "apps"),
         "to":   (TESTBED_ID, APPLICATION, "node"),
-        "code": connect_app,
+        "init_code": connect_dep,
         "can_cross": False
-    })
+    }),
+    dict({
+        "from": (TESTBED_ID, NODE, "deps"),
+        "to":   (TESTBED_ID, DEPENDENCY, "node"),
+        "init_code": connect_dep,
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, NODE, "deps"),
+        "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
+        "init_code": connect_dep,
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, NODE, "pipes"),
+        "to":   (TESTBED_ID, NETPIPE, "node"),
+        "init_code": connect_node_netpipe,
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, TUNIFACE, "tcp"),
+        "to":   (TESTBED_ID, TUNIFACE, "tcp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, TUNIFACE, "udp"),
+        "to":   (TESTBED_ID, TUNIFACE, "udp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"udp"),
+        "can_cross": False
+    }),
 ]
 
 attributes = dict({
@@ -362,6 +573,14 @@ attributes = dict({
                 "value": False,
                 "validation_function": validation.is_bool
             }),
+    "txqueuelen":  dict({
+                "name": "mask", 
+                "help": "Transmission queue length (in packets)",
+                "type": Attribute.INTEGER,
+                "flags": Attribute.DesignOnly,
+                "range" : (1,10000),
+                "validation_function": validation.is_integer
+            }),
             
     "command": dict({
                 "name": "command",
@@ -439,6 +658,70 @@ attributes = dict({
                 "flags": Attribute.DesignOnly,
                 "validation_function": validation.is_string
             }),
+    
+    "netpipe_mode": dict({      
+                "name": "mode",
+                "help": "Link mode:\n"
+                        " * SERVER: applies to incoming connections\n"
+                        " * CLIENT: applies to outgoing connections\n"
+                        " * SERVICE: applies to both",
+                "type": Attribute.ENUM, 
+                "flags": Attribute.DesignOnly,
+                "allowed": ["SERVER",
+                            "CLIENT",
+                            "SERVICE"],
+                "validation_function": validation.is_enum,
+            }),
+    "port_list":  dict({
+                "name": "portList", 
+                "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
+                "type": Attribute.STRING,
+                "validation_function": is_portlist,
+            }),
+    "addr_list":  dict({
+                "name": "addrList", 
+                "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
+                "type": Attribute.STRING,
+                "validation_function": is_addrlist,
+            }),
+    "bw_in":  dict({
+                "name": "bwIn", 
+                "help": "Inbound bandwidth limit (in Mbit/s)",
+                "type": Attribute.DOUBLE,
+                "validation_function": validation.is_double,
+            }),
+    "bw_out":  dict({
+                "name": "bwOut", 
+                "help": "Outbound bandwidth limit (in Mbit/s)",
+                "type": Attribute.DOUBLE,
+                "validation_function": validation.is_double,
+            }),
+    "plr_in":  dict({
+                "name": "plrIn", 
+                "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
+                "type": Attribute.DOUBLE,
+                "validation_function": validation.is_double,
+            }),
+    "plr_out":  dict({
+                "name": "plrOut", 
+                "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
+                "type": Attribute.DOUBLE,
+                "validation_function": validation.is_double,
+            }),
+    "delay_in":  dict({
+                "name": "delayIn", 
+                "help": "Inbound packet delay (in milliseconds)",
+                "type": Attribute.INTEGER,
+                "range": (0,60000),
+                "validation_function": validation.is_integer,
+            }),
+    "delay_out":  dict({
+                "name": "delayOut", 
+                "help": "Outbound packet delay (in milliseconds)",
+                "type": Attribute.INTEGER,
+                "range": (0,60000),
+                "validation_function": validation.is_integer,
+            }),
     })
 
 traces = dict({
@@ -454,11 +737,21 @@ traces = dict({
                 "name": "buildlog",
                 "help": "Output of the build process",
               }), 
+    
+    "netpipe_stats": dict({
+                "name": "netpipeStats",
+                "help": "Information about rule match counters, packets dropped, etc.",
+              }),
+
+    "packets": dict({
+                "name": "packets",
+                "help": "Detailled log of all packets going through the interface",
+              }),
     })
 
-create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, APPLICATION ]
+create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
 
-configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, APPLICATION ]
+configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
 
 factories_info = dict({
     NODE: dict({
@@ -479,10 +772,10 @@ factories_info = dict({
                 "min_bandwidth",
                 "max_bandwidth",
             ],
-            "connector_types": ["devs", "apps"]
+            "connector_types": ["devs", "apps", "pipes", "deps"]
        }),
     NODEIFACE: dict({
-            "allow_addresses": True,
+            "has_addresses": True,
             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
             "category": "devices",
             "create_function": create_nodeiface,
@@ -495,11 +788,14 @@ factories_info = dict({
             "help": "Virtual TUN network interface",
             "category": "devices",
             "create_function": create_tuniface,
-            "preconfigure_function": configure_tuniface,
+            "preconfigure_function": preconfigure_tuniface,
+            "configure_function": postconfigure_tuniface,
             "box_attributes": [
                 "up", "device_name", "mtu", "snat",
+                "txqueuelen"
             ],
-            "connector_types": ["node"]
+            "traces": ["packets"],
+            "connector_types": ["node","udp","tcp"]
         }),
     APPLICATION: dict({
             "help": "Generic executable command line application",
@@ -513,7 +809,26 @@ factories_info = dict({
                                "depends", "build-depends", "build", "install",
                                "sources" ],
             "connector_types": ["node"],
-            "traces": ["stdout", "stderr"]
+            "traces": ["stdout", "stderr", "buildlog"]
+        }),
+    DEPENDENCY: dict({
+            "help": "Requirement for package or application to be installed on some node",
+            "category": "applications",
+            "create_function": create_dependency,
+            "configure_function": configure_dependency,
+            "box_attributes": ["depends", "build-depends", "build", "install",
+                               "sources" ],
+            "connector_types": ["node"],
+            "traces": ["buildlog"]
+        }),
+    NEPIDEPENDENCY: dict({
+            "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
+            "category": "applications",
+            "create_function": create_nepi_dependency,
+            "configure_function": configure_dependency,
+            "box_attributes": [ ],
+            "connector_types": ["node"],
+            "traces": ["buildlog"]
         }),
     INTERNET: dict({
             "help": "Internet routing",
@@ -521,6 +836,18 @@ factories_info = dict({
             "create_function": create_internet,
             "connector_types": ["devs"],
         }),
+    NETPIPE: dict({
+            "help": "Link emulation",
+            "category": "topology",
+            "create_function": create_netpipe,
+            "configure_function": configure_netpipe,
+            "box_attributes": ["netpipe_mode",
+                               "addr_list", "port_list",
+                               "bw_in","plr_in","delay_in",
+                               "bw_out","plr_out","delay_out"],
+            "connector_types": ["node"],
+            "traces": ["netpipe_stats"]
+        }),
 })
 
 testbed_attributes = dict({