Ticket #45: spanning tree deployment
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
index 74b22bc..85ae099 100644 (file)
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+import time
+
 from constants import TESTBED_ID
 from nepi.core import metadata
 from nepi.core.attributes import Attribute
 from nepi.util import validation
 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
-        STATUS_FINISHED
+        STATUS_FINISHED, ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP
+
+import functools
+import os
+import os.path
 
 NODE = "Node"
 NODEIFACE = "NodeInterface"
+TUNIFACE = "TunInterface"
+TAPIFACE = "TapInterface"
 APPLICATION = "Application"
+DEPENDENCY = "Dependency"
+NEPIDEPENDENCY = "NepiDependency"
+NS3DEPENDENCY = "NS3Dependency"
+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, addr):
+            # 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_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_guid, inet_guid):
+    iface = testbed_instance._elements[iface_guid]
+    iface.has_internet = True
+
+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, "Use of TUN interfaces requires emulation"
+    iface.node = node
+    node.required_vsys.update(('fd_tuntap', 'vif_up'))
+    node.required_packages.update(('python', 'python-crypto', 'python-setuptools', 'gcc'))
+
+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
+    iface.tun_key = peer_iface.tun_key
+
+def crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data):
+    iface = testbed_instance._elements[iface_guid]
+    iface.peer_iface = None
+    iface.peer_addr = peer_iface_data.get("tun_addr")
+    iface.peer_proto = peer_iface_data.get("tun_proto") or proto
+    iface.peer_port = peer_iface_data.get("tun_port")
+    iface.tun_key = min(iface.tun_key, peer_iface_data.get("tun_key"))
+    iface.tun_proto = proto
+    
+    preconfigure_tuniface(testbed_instance, iface_guid)
+
+def crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data):
+    # refresh (refreshable) attributes for second-phase
+    iface = testbed_instance._elements[iface_guid]
+    iface.peer_addr = peer_iface_data.get("tun_addr")
+    iface.peer_proto = peer_iface_data.get("tun_proto") or proto
+    iface.peer_port = peer_iface_data.get("tun_port")
+    
+    postconfigure_tuniface(testbed_instance, iface_guid)
+
+def crossconnect_tun_iface_peer_both(proto, testbed_instance, iface_guid, peer_iface_data):
+    crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data)
+    crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data)
+
+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)
+    
+    if app.env:
+        for envkey, envval in app.env.iteritems():
+            envval = app._replace_paths(envval)
+            node.env[envkey].append(envval)
+
+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 ###
 
 def create_node(testbed_instance, guid):
     parameters = testbed_instance._get_parameters(guid)
-    element = testbed_instance.pl.Node()
+    
+    # create element with basic attributes
+    element = testbed_instance._make_node(parameters)
+    
+    # add constraint on number of (real) interfaces
+    # by counting connected devices
+    dev_guids = testbed_instance.get_connected(guid, "devs", "node")
+    num_open_ifaces = sum( # count True values
+        NODEIFACE == testbed_instance._get_factory_id(guid)
+        for guid in dev_guids )
+    element.min_num_external_ifaces = num_open_ifaces
+    
+    # require vroute vsys if we have routes to set up
+    routes = testbed_instance._add_route.get(guid)
+    if routes:
+        element.required_vsys.add("vroute")
+    
     testbed_instance.elements[guid] = element
 
 def create_nodeiface(testbed_instance, guid):
     parameters = testbed_instance._get_parameters(guid)
-    element = testbed_instance.pl.Iface()
+    element = testbed_instance._make_node_iface(parameters)
+    testbed_instance.elements[guid] = element
+
+def create_tuniface(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_tun_iface(parameters)
+    
+    # Set custom addresses, if there are any already
+    # Setting this early helps set up P2P links
+    if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
+        addresses = testbed_instance._add_address[guid]
+        for address in addresses:
+            (address, netprefix, broadcast) = address
+            element.add_address(address, netprefix, broadcast)
+    
+    testbed_instance.elements[guid] = element
+
+def create_tapiface(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_tap_iface(parameters)
+    
+    # Set custom addresses, if there are any already
+    # Setting this early helps set up P2P links
+    if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
+        addresses = testbed_instance._add_address[guid]
+        for address in addresses:
+            (address, netprefix, broadcast) = address
+            element.add_address(address, netprefix, broadcast)
+    
     testbed_instance.elements[guid] = element
 
 def create_application(testbed_instance, guid):
-    testbed_instance.elements[guid] = None # Delayed construction 
+    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_ns3_dependency(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(guid)
+    element = testbed_instance._make_ns3_dependency(parameters)
+    
+    # Just inject configuration stuff
+    element.home_path = "nepi-ns3-%s" % (guid,)
+    
+    testbed_instance.elements[guid] = element
+
+def create_internet(testbed_instance, guid):
+    parameters = testbed_instance._get_parameters(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):
     parameters = testbed_instance._get_parameters(guid)
     traces = testbed_instance._get_traces(guid)
-    user = parameters["user"]
-    command = parameters["command"]
-    stdout = stderr = None
-    if "stdout" in traces:
-        filename = testbed_instance.trace_filename(guid, "stdout")
-        stdout = open(filename, "wb")
-        testbed_instance.follow_trace("stdout", stdout)
-    if "stderr" in traces:
-        filename = testbed_instance.trace_filename(guid, "stderr")
-        stderr = open(filename, "wb")
-        testbed_instance.follow_trace("stderr", stderr)
-
-    node_guid = testbed_instance.get_connected(guid, "node", "apps")
-    if len(node_guid) == 0:
-        raise RuntimeError("Can't instantiate interface %d outside netns \
-                node" % guid)
-    node = testbed_instance.elements[node_guid[0]]
-    element  = node.Popen(command, shell = True, stdout = stdout, 
-            stderr = stderr, user = user)
-    testbed_instance.elements[guid] = element
+    app = testbed_instance.elements[guid]
+    
+    app.stdout = "stdout" in traces
+    app.stderr = "stderr" in traces
+    app.buildlog = "buildlog" in traces
+    
+    app.start()
+
+def stop_application(testbed_instance, guid):
+    app = testbed_instance.elements[guid]
+    app.kill()
 
 ### Status functions ###
 
 def status_application(testbed_instance, guid):
     if guid not in testbed_instance.elements.keys():
         return STATUS_NOT_STARTED
+    
     app = testbed_instance.elements[guid]
-    if app.poll() == None:
-        return STATUS_RUNNING
-    return STATUS_FINISHED
+    return app.status()
 
 ### Configure functions ###
 
-def configure_device(testbed_instance, guid):
+def configure_nodeiface(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
-        # TODO: Decide if we should add a ipv4 or ipv6 address
-        element.add_v4_address(address, netprefix)
+    
+    # Cannot explicitly configure addresses
+    if guid in testbed_instance._add_address:
+        raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
+    
+    # Get siblings
+    node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
+    dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
+    siblings = [ self._element[dev_guid] 
+                 for dev_guid in dev_guids
+                 if dev_guid != guid ]
+    
+    # Fetch address from PLC api
+    element.pick_iface(siblings)
+    
+    # Do some validations
+    element.validate()
 
-def configure_node(testbed_instance, guid):
+def preconfigure_tuniface(testbed_instance, guid):
     element = testbed_instance._elements[guid]
-    if not guid in testbed_instance._add_route:
-        return
-    routes = testbed_instance._add_route[guid]
-    for route in routes:
-        (destination, netprefix, nexthop) = route
-        element.add_route(prefix = destination, prefix_len = netprefix,
-            nexthop = nexthop)
+    
+    # Set custom addresses if any, and if not set already
+    if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
+        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
+    if (not element.tun_addr or not element.tun_port) and element.external_iface:
+        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
+    if element.peer_proto:
+        if element.peer_iface and isinstance(element.peer_iface, testbed_instance._interfaces.TunIface):
+            # intra tun
+            listening = id(element) < id(element.peer_iface)
+        else:
+            # cross tun
+            if not element.tun_addr or not element.tun_port:
+                listening = True
+            elif not element.peer_addr or not element.peer_port:
+                listening = True
+            else:
+                # both have addresses...
+                # ...the one with the lesser address listens
+                listening = element.tun_addr < element.peer_addr
+        element.prepare( 
+            'tun-%s' % (guid,),
+             listening)
+
+def postconfigure_tuniface(testbed_instance, guid):
+    element = testbed_instance._elements[guid]
+    
+    # Second-phase setup
+    element.setup()
+    
+def wait_tuniface(testbed_instance, guid):
+    element = testbed_instance._elements[guid]
+    
+    # Second-phase setup
+    element.async_launch_wait()
+    
+
+def configure_node(testbed_instance, guid):
+    node = testbed_instance._elements[guid]
+    
+    # Just inject configuration stuff
+    node.home_path = "nepi-node-%s" % (guid,)
+    node.ident_path = testbed_instance.sliceSSHKey
+    node.slicename = testbed_instance.slicename
+    
+    # Do some validations
+    node.validate()
+    
+    # recently provisioned nodes may not be up yet
+    sleeptime = 1.0
+    while not node.is_alive():
+        time.sleep(sleeptime)
+        sleeptime = min(30.0, sleeptime*1.5)
+    
+    # this will be done in parallel in all nodes
+    # this call only spawns the process
+    node.install_dependencies()
+
+def configure_node_routes(testbed_instance, guid):
+    node = testbed_instance._elements[guid]
+    routes = testbed_instance._add_route.get(guid)
+    
+    if routes:
+        devs = [ dev
+            for dev_guid in testbed_instance.get_connected(guid, "devs", "node")
+            for dev in ( testbed_instance._elements.get(dev_guid) ,)
+            if dev and isinstance(dev, testbed_instance._interfaces.TunIface) ]
+        
+        node.configure_routes(routes, devs)
+
+def configure_application(testbed_instance, guid):
+    app = testbed_instance._elements[guid]
+    
+    # Do some validations
+    app.validate()
+    
+    # Wait for dependencies
+    app.node.wait_dependencies()
+    
+    # Install stuff
+    app.async_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.async_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 ###
 
@@ -104,75 +450,171 @@ 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",
+                "max": 1, 
+                "min": 1
+            }),
     "node": dict({
                 "help": "Connector to a Node", 
                 "name": "node",
                 "max": 1, 
                 "min": 1
             }),
-    "p2p": dict({
-                "help": "Connector to a P2PInterface", 
-                "name": "p2p",
+    "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
             }),
-    "fd": dict({
-                "help": "Connector to a network interface that can receive a file descriptor", 
-                "name": "fd",
+    "udp": dict({
+                "help": "ip-ip tunneling over UDP datagrams", 
+                "name": "udp",
                 "max": 1, 
                 "min": 0
             }),
-    "switch": dict({
-                "help": "Connector to a switch", 
-                "name": "switch",
+    "fd->": dict({
+                "help": "TUN device file descriptor provider", 
+                "name": "fd->",
                 "max": 1, 
                 "min": 0
-            })
+            }),
    })
 
 connections = [
     dict({
         "from": (TESTBED_ID, NODE, "devs"),
-        "to":   (TESTBED_ID, P2PIFACE, "node"),
-        "code": None,
+        "to":   (TESTBED_ID, NODEIFACE, "node"),
+        "init_code": connect_node_iface_node,
         "can_cross": False
     }),
     dict({
         "from": (TESTBED_ID, NODE, "devs"),
-        "to":   (TESTBED_ID, TAPIFACE, "node"),
-        "code": None,
+        "to":   (TESTBED_ID, TUNIFACE, "node"),
+        "init_code": connect_tun_iface_node,
         "can_cross": False
     }),
     dict({
         "from": (TESTBED_ID, NODE, "devs"),
-        "to":   (TESTBED_ID, NODEIFACE, "node"),
-        "code": None,
+        "to":   (TESTBED_ID, TAPIFACE, "node"),
+        "init_code": connect_tun_iface_node,
         "can_cross": False
     }),
     dict({
-        "from": (TESTBED_ID, P2PIFACE, "p2p"),
-        "to":   (TESTBED_ID, P2PIFACE, "p2p"),
-        "code": None,
+        "from": (TESTBED_ID, NODEIFACE, "inet"),
+        "to":   (TESTBED_ID, INTERNET, "devs"),
+        "init_code": connect_node_iface_inet,
         "can_cross": False
     }),
     dict({
-        "from": (TESTBED_ID, TAPIFACE, "fd"),
-        "to":   (NS3_TESTBED_ID, FDNETDEV, "fd"),
-        "code": connect_fd_local,
-        "can_cross": True
+        "from": (TESTBED_ID, NODE, "apps"),
+        "to":   (TESTBED_ID, APPLICATION, "node"),
+        "init_code": connect_dep,
+        "can_cross": False
     }),
-     dict({
-        "from": (TESTBED_ID, SWITCH, "devs"),
-        "to":   (TESTBED_ID, NODEIFACE, "switch"),
-        "code": connect_switch,
+    dict({
+        "from": (TESTBED_ID, NODE, "deps"),
+        "to":   (TESTBED_ID, DEPENDENCY, "node"),
+        "init_code": connect_dep,
         "can_cross": False
     }),
     dict({
-        "from": (TESTBED_ID, NODE, "apps"),
-        "to":   (TESTBED_ID, APPLICATION, "node"),
-        "code": None,
+        "from": (TESTBED_ID, NODE, "deps"),
+        "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
+        "init_code": connect_dep,
         "can_cross": False
-    })
+    }),
+    dict({
+        "from": (TESTBED_ID, NODE, "deps"),
+        "to":   (TESTBED_ID, NS3DEPENDENCY, "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
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "tcp"),
+        "to":   (TESTBED_ID, TAPIFACE, "tcp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "udp"),
+        "to":   (TESTBED_ID, TAPIFACE, "udp"),
+        "init_code": functools.partial(connect_tun_iface_peer,"udp"),
+        "can_cross": False
+    }),
+    dict({
+        "from": (TESTBED_ID, TUNIFACE, "tcp"),
+        "to":   (None, None, "tcp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TUNIFACE, "udp"),
+        "to":   (None, None, "udp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TUNIFACE, "fd->"),
+        "to":   (None, None, "->fd"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "tcp"),
+        "to":   (None, None, "tcp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "udp"),
+        "to":   (None, None, "udp"),
+        "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
+        "can_cross": True
+    }),
+    dict({
+        "from": (TESTBED_ID, TAPIFACE, "fd->"),
+        "to":   (None, None, "->fd"),
+        "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
+        "can_cross": True
+    }),
 ]
 
 attributes = dict({
@@ -182,15 +624,87 @@ attributes = dict({
                 "type": Attribute.BOOL, 
                 "value": False,
                 "flags": Attribute.DesignOnly,
-                "validation_function": validation.is_bool
+                "validation_function": validation.is_bool,
             }),
-    "lladdr": dict({      
-                "name": "lladdr", 
-                "help": "Mac address", 
-                "type": Attribute.STRING,
+    "hostname": dict({      
+                "name": "hostname",
+                "help": "Constrain hostname during resource discovery. May use wildcards.",
+                "type": Attribute.STRING, 
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string,
+            }),
+    "architecture": dict({      
+                "name": "architecture",
+                "help": "Constrain architexture during resource discovery.",
+                "type": Attribute.ENUM, 
                 "flags": Attribute.DesignOnly,
-                "validation_function": validation.is_mac_address
+                "allowed": ["x86_64",
+                            "i386"],
+                "validation_function": validation.is_enum,
             }),
+    "operating_system": dict({      
+                "name": "operatingSystem",
+                "help": "Constrain operating system during resource discovery.",
+                "type": Attribute.ENUM, 
+                "flags": Attribute.DesignOnly,
+                "allowed": ["f8",
+                            "f12",
+                            "f14",
+                            "centos",
+                            "other"],
+                "validation_function": validation.is_enum,
+            }),
+    "site": dict({      
+                "name": "site",
+                "help": "Constrain the PlanetLab site this node should reside on.",
+                "type": Attribute.ENUM, 
+                "flags": Attribute.DesignOnly,
+                "allowed": ["PLE",
+                            "PLC",
+                            "PLJ"],
+                "validation_function": validation.is_enum,
+            }),
+    "emulation": dict({      
+                "name": "emulation",
+                "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
+                "type": Attribute.BOOL,
+                "value": False, 
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_bool,
+            }),
+    "min_reliability": dict({
+                "name": "minReliability",
+                "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
+                "type": Attribute.DOUBLE,
+                "range": (0,100),
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_double,
+            }),
+    "max_reliability": dict({
+                "name": "maxReliability",
+                "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
+                "type": Attribute.DOUBLE,
+                "range": (0,100),
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_double,
+            }),
+    "min_bandwidth": dict({
+                "name": "minBandwidth",
+                "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
+                "type": Attribute.DOUBLE,
+                "range": (0,2**31),
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_double,
+            }),
+    "max_bandwidth": dict({
+                "name": "maxBandwidth",
+                "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
+                "type": Attribute.DOUBLE,
+                "range": (0,2**31),
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_double,
+            }),
+            
     "up": dict({
                 "name": "up",
                 "help": "Link up",
@@ -198,6 +712,13 @@ attributes = dict({
                 "value": False,
                 "validation_function": validation.is_bool
             }),
+    "primary": dict({
+                "name": "primary",
+                "help": "This is the primary interface for the attached node",
+                "type": Attribute.BOOL,
+                "value": True,
+                "validation_function": validation.is_bool
+            }),
     "device_name": dict({
                 "name": "name",
                 "help": "Device name",
@@ -209,28 +730,39 @@ attributes = dict({
                 "name": "mtu", 
                 "help": "Maximum transmition unit for device",
                 "type": Attribute.INTEGER,
-                "validation_function": validation.is_integer
+                "range": (0,1500),
+                "validation_function": validation.is_integer_range(0,1500)
             }),
-    "broadcast": dict({ 
-                "name": "broadcast",
-                "help": "Broadcast address",
-                "type": Attribute.STRING,
-                "validation_function": validation.is_string # TODO: should be is address!
+    "mask":  dict({
+                "name": "mask", 
+                "help": "Network mask for the device (eg: 24 for /24 network)",
+                "type": Attribute.INTEGER,
+                "validation_function": validation.is_integer_range(8,24)
             }),
-    "multicast": dict({      
-                "name": "multicast",
-                "help": "Multicast enabled",
+    "snat":  dict({
+                "name": "snat", 
+                "help": "Enable SNAT (source NAT to the internet) no this device",
                 "type": Attribute.BOOL,
                 "value": False,
                 "validation_function": validation.is_bool
             }),
-    "arp": dict({
-                "name": "arp",
-                "help": "ARP enabled",
-                "type": Attribute.BOOL,
-                "value": False,
-                "validation_function": validation.is_bool
+    "pointopoint":  dict({
+                "name": "pointopoint", 
+                "help": "If the interface is a P2P link, the remote endpoint's IP "
+                        "should be set on this attribute.",
+                "type": Attribute.STRING,
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string
+            }),
+    "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",
                 "help": "Command line string",
@@ -238,12 +770,13 @@ attributes = dict({
                 "flags": Attribute.DesignOnly,
                 "validation_function": validation.is_string
             }),
-    "user": dict({
-                "name": "user",
-                "help": "System user",
-                "type": Attribute.STRING,
+    "sudo": dict({
+                "name": "sudo",
+                "help": "Run with root privileges",
+                "type": Attribute.BOOL,
                 "flags": Attribute.DesignOnly,
-                "validation_function": validation.is_string
+                "value": False,
+                "validation_function": validation.is_bool
             }),
     "stdin": dict({
                 "name": "stdin",
@@ -252,6 +785,124 @@ attributes = dict({
                 "flags": Attribute.DesignOnly,
                 "validation_function": validation.is_string
             }),
+            
+    "depends": dict({
+                "name": "depends",
+                "help": "Space-separated list of packages required to run the application",
+                "type": Attribute.STRING,
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string
+            }),
+    "build-depends": dict({
+                "name": "buildDepends",
+                "help": "Space-separated list of packages required to build the application",
+                "type": Attribute.STRING,
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string
+            }),
+    "sources": dict({
+                "name": "sources",
+                "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
+                        "Archives won't be expanded automatically.",
+                "type": Attribute.STRING,
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string
+            }),
+    "build": dict({
+                "name": "build",
+                "help": "Build commands to execute after deploying the sources. "
+                        "Sources will be in the ${SOURCES} folder. "
+                        "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
+                        "Try to make the commands return with a nonzero exit code on error.\n"
+                        "Also, do not install any programs here, use the 'install' attribute. This will "
+                        "help keep the built files constrained to the build folder (which may "
+                        "not be the home folder), and will result in faster deployment. Also, "
+                        "make sure to clean up temporary files, to reduce bandwidth usage between "
+                        "nodes when transferring built packages.",
+                "type": Attribute.STRING,
+                "flags": Attribute.DesignOnly,
+                "validation_function": validation.is_string
+            }),
+    "install": dict({
+                "name": "install",
+                "help": "Commands to transfer built files to their final destinations. "
+                        "Sources will be in the initial working folder, and a special "
+                        "tag ${SOURCES} can be used to reference the experiment's "
+                        "home folder (where the application commands will run).\n"
+                        "ALL sources and targets needed for execution must be copied there, "
+                        "if building has been enabled.\n"
+                        "That is, 'slave' nodes will not automatically get any source files. "
+                        "'slave' nodes don't get build dependencies either, so if you need "
+                        "make and other tools to install, be sure to provide them as "
+                        "actual dependencies instead.",
+                "type": Attribute.STRING,
+                "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({
@@ -262,67 +913,95 @@ traces = dict({
     "stderr": dict({
                 "name": "stderr",
                 "help": "Application standard error",
-        }) 
+              }),
+    "buildlog": 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 = [ NODE, P2PIFACE, NODEIFACE, TAPIFACE, SWITCH,
-        APPLICATION ]
+create_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
+
+configure_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
 
-configure_order = [ P2PIFACE, NODEIFACE, TAPIFACE, SWITCH, NODE,
-        APPLICATION ]
+# Start (and prestart) node after ifaces, because the node needs the ifaces in order to set up routes
+start_order = [ INTERNET, NODEIFACE, TAPIFACE, TUNIFACE, NODE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
 
 factories_info = dict({
     NODE: dict({
             "allow_routes": True,
-            "help": "Emulated Node with virtualized network stack",
+            "help": "Virtualized Node (V-Server style)",
             "category": "topology",
             "create_function": create_node,
-            "configure_function": configure_node,
-            "box_attributes": ["forward_X11"],
-            "connector_types": ["devs", "apps"]
+            "preconfigure_function": configure_node,
+            "prestart_function": configure_node_routes,
+            "box_attributes": [
+                "forward_X11",
+                "hostname",
+                "architecture",
+                "operating_system",
+                "site",
+                "emulation",
+                "min_reliability",
+                "max_reliability",
+                "min_bandwidth",
+                "max_bandwidth",
+                
+                # NEPI-in-NEPI attributes
+                ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
+            ],
+            "connector_types": ["devs", "apps", "pipes", "deps"]
        }),
-    P2PIFACE: dict({
-            "allow_addresses": True,
-            "help": "Point to point network interface",
-            "category": "devices",
-            "create_function": create_p2piface,
-            "configure_function": configure_device,
-            "box_attributes": ["lladdr", "up", "device_name", "mtu", 
-                "multicast", "broadcast", "arp"],
-            "connector_types": ["node", "p2p"]
-       }),
-    TAPIFACE: dict({
-            "allow_addresses": True,
-            "help": "Tap device network interface",
+    NODEIFACE: dict({
+            "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_tapiface,
-            "configure_function": configure_device,
-            "box_attributes": ["lladdr", "up", "device_name", "mtu", 
-                "multicast", "broadcast", "arp"],
-            "connector_types": ["node", "fd"]
+            "create_function": create_nodeiface,
+            "preconfigure_function": configure_nodeiface,
+            "box_attributes": [ ],
+            "connector_types": ["node", "inet"]
         }),
-    NODEIFACE: dict({
+    TUNIFACE: dict({
             "allow_addresses": True,
-            "help": "Node network interface",
+            "help": "Virtual TUN network interface (layer 3)",
             "category": "devices",
-            "create_function": create_nodeiface,
-            "configure_function": configure_device,
-            "box_attributes": ["lladdr", "up", "device_name", "mtu", 
-                "multicast", "broadcast", "arp"],
-            "connector_types": ["node", "switch"]
+            "create_function": create_tuniface,
+            "preconfigure_function": preconfigure_tuniface,
+            "configure_function": postconfigure_tuniface,
+            "prestart_function": wait_tuniface,
+            "box_attributes": [
+                "up", "device_name", "mtu", "snat", "pointopoint",
+                "txqueuelen",
+                "tun_proto", "tun_addr", "tun_port", "tun_key"
+            ],
+            "traces": ["packets"],
+            "connector_types": ["node","udp","tcp","fd->"]
         }),
-    SWITCH: dict({
-            "display_name": "Switch",
-            "help": "Switch interface",
+    TAPIFACE: dict({
+            "allow_addresses": True,
+            "help": "Virtual TAP network interface (layer 2)",
             "category": "devices",
-            "create_function": create_switch,
-            "box_attributes": ["up", "device_name", "mtu", "multicast"],
-             #TODO: Add attribute ("Stp", help, type, value, range, allowed, readonly, validation_function),
-             #TODO: Add attribute ("ForwarddDelay", help, type, value, range, allowed, readonly, validation_function),
-             #TODO: Add attribute ("HelloTime", help, type, value, range, allowed, readonly, validation_function),
-             #TODO: Add attribute ("AgeingTime", help, type, value, range, allowed, readonly, validation_function),
-             #TODO: Add attribute ("MaxAge", help, type, value, range, allowed, readonly, validation_function)
-           "connector_types": ["devs"]
+            "create_function": create_tapiface,
+            "preconfigure_function": preconfigure_tuniface,
+            "configure_function": postconfigure_tuniface,
+            "prestart_function": wait_tuniface,
+            "box_attributes": [
+                "up", "device_name", "mtu", "snat", "pointopoint",
+                "txqueuelen",
+                "tun_proto", "tun_addr", "tun_port", "tun_key"
+            ],
+            "traces": ["packets"],
+            "connector_types": ["node","udp","tcp","fd->"]
         }),
     APPLICATION: dict({
             "help": "Generic executable command line application",
@@ -330,20 +1009,111 @@ factories_info = dict({
             "create_function": create_application,
             "start_function": start_application,
             "status_function": status_application,
-            "box_attributes": ["command", "user"],
+            "stop_function": stop_application,
+            "configure_function": configure_application,
+            "box_attributes": ["command", "sudo", "stdin",
+                               "depends", "build-depends", "build", "install",
+                               "sources" ],
+            "connector_types": ["node"],
+            "traces": ["stdout", "stderr", "buildlog"]
+        }),
+    DEPENDENCY: dict({
+            "help": "Requirement for package or application to be installed on some node",
+            "category": "applications",
+            "create_function": create_dependency,
+            "preconfigure_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,
+            "preconfigure_function": configure_dependency,
+            "box_attributes": [ ],
+            "connector_types": ["node"],
+            "traces": ["buildlog"]
+        }),
+    NS3DEPENDENCY: dict({
+            "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
+            "category": "applications",
+            "create_function": create_ns3_dependency,
+            "preconfigure_function": configure_dependency,
+            "box_attributes": [ ],
+            "connector_types": ["node"],
+            "traces": ["buildlog"]
+        }),
+    INTERNET: dict({
+            "help": "Internet routing",
+            "category": "topology",
+            "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": ["stdout", "stderr"]
+            "traces": ["netpipe_stats"]
         }),
 })
 
 testbed_attributes = dict({
-        "enable_debug": dict({
-                "name": "enableDebug",
-                "help": "Enable netns debug output",
-                "type": Attribute.BOOL,
-                "value": False,
-                "validation_function": validation.is_bool
-            }),
+        "slice": dict({
+            "name": "slice",
+            "help": "The name of the PlanetLab slice to use",
+            "type": Attribute.STRING,
+            "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
+            "validation_function": validation.is_string
+        }),
+        "auth_user": dict({
+            "name": "authUser",
+            "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
+            "type": Attribute.STRING,
+            "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
+            "validation_function": validation.is_string
+        }),
+        "auth_pass": dict({
+            "name": "authPass",
+            "help": "The PlanetLab user's password.",
+            "type": Attribute.STRING,
+            "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
+            "validation_function": validation.is_string
+        }),
+        "plc_host": dict({
+            "name": "plcHost",
+            "help": "The PlanetLab PLC API host",
+            "type": Attribute.STRING,
+            "value": "www.planet-lab.eu",
+            "flags": Attribute.DesignOnly,
+            "validation_function": validation.is_string
+        }),
+        "plc_url": dict({
+            "name": "plcUrl",
+            "help": "The PlanetLab PLC API url pattern - %(hostname)s is replaced by plcHost.",
+            "type": Attribute.STRING,
+            "value": "https://%(hostname)s:443/PLCAPI/",
+            "flags": Attribute.DesignOnly,
+            "validation_function": validation.is_string
+        }),
+        "slice_ssh_key": dict({
+            "name": "sliceSSHKey",
+            "help": "The controller-local path to the slice user's ssh private key. "
+                    "It is the user's responsability to deploy this file where the controller "
+                    "will run, it won't be done automatically because it's sensitive information. "
+                    "It is recommended that a NEPI-specific user be created for this purpose and "
+                    "this purpose alone.",
+            "type": Attribute.STRING,
+            "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
+            "validation_function": validation.is_string
+        }),
     })
 
 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
@@ -371,6 +1141,14 @@ class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
     def configure_order(self):
         return configure_order
 
+    @property
+    def prestart_order(self):
+        return start_order
+
+    @property
+    def start_order(self):
+        return start_order
+
     @property
     def factories_info(self):
         return factories_info