#!/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 ###
"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({
"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,
+ "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_mac_address
+ "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",
"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",
"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",
"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",
"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({
"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",
"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"]
+ "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": ["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
- }),
- "home_directory": dict({
- "name": "homeDirectory",
- "help": "Path to the directory where traces and other files \
- will be stored",
- "type": Attribute.STRING,
- "value": "",
- "flags": Attribute.DesignOnly,
- "validation_function": validation.is_string
- })
+ "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):
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