2 # -*- coding: utf-8 -*-
6 from constants import TESTBED_ID
7 from nepi.core import metadata
8 from nepi.core.attributes import Attribute
9 from nepi.util import validation
10 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
18 NODEIFACE = "NodeInterface"
19 TUNIFACE = "TunInterface"
20 APPLICATION = "Application"
24 PL_TESTBED_ID = "planetlab"
27 ### Custom validation functions ###
28 def is_addrlist(attribute, value):
29 if not validation.is_string(attribute, value):
36 components = value.split(',')
38 for component in components:
40 addr, mask = component.split('/',1)
42 addr, mask = component, 32
44 if mask is not None and not (mask and mask.isdigit()):
45 # No empty or nonnumeric masks
48 if not validation.is_ip4_address(attribute, value):
49 # Address part must be ipv4
54 def is_portlist(attribute, value):
55 if not validation.is_string(attribute, value):
62 components = value.split(',')
64 for component in components:
66 pfrom, pto = component.split('-',1)
68 pfrom = pto = component
70 if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
71 # No empty or nonnumeric ports
77 ### Connection functions ####
79 def connect_node_iface_node(testbed_instance, node, iface):
82 def connect_node_iface_inet(testbed_instance, iface, inet):
83 iface.has_internet = True
85 def connect_tun_iface_node(testbed_instance, node, iface):
86 if not node.emulation:
87 raise RuntimeError, "Use of TUN interfaces requires emulation"
89 node.required_vsys.update(('fd_tuntap', 'vif_up'))
91 def connect_tun_iface_peer(proto, testbed_instance, iface, peer_iface):
92 iface.peer_iface = peer_iface
94 iface.tun_proto = proto
96 def connect_app(testbed_instance, node, app):
100 node.required_packages.update(set(
101 app.depends.split() ))
103 def connect_node_netpipe(testbed_instance, node, netpipe):
104 if not node.emulation:
105 raise RuntimeError, "Use of NetPipes requires emulation"
109 ### Creation functions ###
111 def create_node(testbed_instance, guid):
112 parameters = testbed_instance._get_parameters(guid)
114 # create element with basic attributes
115 element = testbed_instance._make_node(parameters)
117 # add constraint on number of (real) interfaces
118 # by counting connected devices
119 dev_guids = testbed_instance.get_connected(guid, "node", "devs")
120 num_open_ifaces = sum( # count True values
121 NODEIFACE == testbed_instance._get_factory_id(guid)
122 for guid in dev_guids )
123 element.min_num_external_ifaces = num_open_ifaces
125 testbed_instance.elements[guid] = element
127 def create_nodeiface(testbed_instance, guid):
128 parameters = testbed_instance._get_parameters(guid)
129 element = testbed_instance._make_node_iface(parameters)
130 testbed_instance.elements[guid] = element
132 def create_tuniface(testbed_instance, guid):
133 parameters = testbed_instance._get_parameters(guid)
134 element = testbed_instance._make_tun_iface(parameters)
135 testbed_instance.elements[guid] = element
137 def create_application(testbed_instance, guid):
138 parameters = testbed_instance._get_parameters(guid)
139 element = testbed_instance._make_application(parameters)
140 testbed_instance.elements[guid] = element
142 def create_internet(testbed_instance, guid):
143 parameters = testbed_instance._get_parameters(guid)
144 element = testbed_instance._make_internet(parameters)
145 testbed_instance.elements[guid] = element
147 def create_netpipe(testbed_instance, guid):
148 parameters = testbed_instance._get_parameters(guid)
149 element = testbed_instance._make_netpipe(parameters)
150 testbed_instance.elements[guid] = element
152 ### Start/Stop functions ###
154 def start_application(testbed_instance, guid):
155 parameters = testbed_instance._get_parameters(guid)
156 traces = testbed_instance._get_traces(guid)
157 app = testbed_instance.elements[guid]
159 app.stdout = "stdout" in traces
160 app.stderr = "stderr" in traces
161 app.buildlog = "buildlog" in traces
165 def stop_application(testbed_instance, guid):
166 app = testbed_instance.elements[guid]
169 ### Status functions ###
171 def status_application(testbed_instance, guid):
172 if guid not in testbed_instance.elements.keys():
173 return STATUS_NOT_STARTED
175 app = testbed_instance.elements[guid]
178 ### Configure functions ###
180 def configure_nodeiface(testbed_instance, guid):
181 element = testbed_instance._elements[guid]
183 # Cannot explicitly configure addresses
184 if guid in testbed_instance._add_address:
185 raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
188 node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
189 dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
190 siblings = [ self._element[dev_guid]
191 for dev_guid in dev_guids
192 if dev_guid != guid ]
194 # Fetch address from PLC api
195 element.pick_iface(siblings)
197 # Do some validations
200 def preconfigure_tuniface(testbed_instance, guid):
201 element = testbed_instance._elements[guid]
203 # Set custom addresses if any
204 if guid in testbed_instance._add_address:
205 addresses = testbed_instance._add_address[guid]
206 for address in addresses:
207 (address, netprefix, broadcast) = address
208 element.add_address(address, netprefix, broadcast)
210 # Link to external interface, if any
211 for iface in testbed_instance._elements.itervalues():
212 if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
213 element.external_iface = iface
216 # Set standard TUN attributes
217 element.tun_addr = element.external_iface.address
218 element.tun_port = 15000 + int(guid)
221 traces = testbed_instance._get_traces(guid)
222 element.capture = 'packets' in traces
224 # Do some validations
230 id(element) < id(element.peer_iface) )
232 def postconfigure_tuniface(testbed_instance, guid):
233 element = testbed_instance._elements[guid]
239 def configure_node(testbed_instance, guid):
240 node = testbed_instance._elements[guid]
242 # Just inject configuration stuff
243 node.home_path = "nepi-node-%s" % (guid,)
244 node.ident_path = testbed_instance.sliceSSHKey
245 node.slicename = testbed_instance.slicename
247 # Do some validations
250 # recently provisioned nodes may not be up yet
252 while not node.is_alive():
253 time.sleep(sleeptime)
254 sleeptime = min(30.0, sleeptime*1.5)
256 # this will be done in parallel in all nodes
257 # this call only spawns the process
258 node.install_dependencies()
260 def configure_application(testbed_instance, guid):
261 app = testbed_instance._elements[guid]
263 # Just inject configuration stuff
264 app.home_path = "nepi-app-%s" % (guid,)
265 app.ident_path = testbed_instance.sliceSSHKey
266 app.slicename = testbed_instance.slicename
268 # Do some validations
271 # Wait for dependencies
272 app.node.wait_dependencies()
277 def configure_netpipe(testbed_instance, guid):
278 netpipe = testbed_instance._elements[guid]
280 # Do some validations
283 # Wait for dependencies
284 netpipe.node.wait_dependencies()
289 ### Factory information ###
291 connector_types = dict({
293 "help": "Connector from node to applications",
299 "help": "Connector from node to network interfaces",
305 "help": "Connector from network interfaces to the internet",
311 "help": "Connector to a Node",
317 "help": "Connector to a NetPipe",
324 "help": "ip-ip tunneling over TCP link",
330 "help": "ip-ip tunneling over UDP datagrams",
339 "from": (TESTBED_ID, NODE, "devs"),
340 "to": (TESTBED_ID, NODEIFACE, "node"),
341 "code": connect_node_iface_node,
345 "from": (TESTBED_ID, NODE, "devs"),
346 "to": (TESTBED_ID, TUNIFACE, "node"),
347 "code": connect_tun_iface_node,
351 "from": (TESTBED_ID, NODEIFACE, "inet"),
352 "to": (TESTBED_ID, INTERNET, "devs"),
353 "code": connect_node_iface_inet,
357 "from": (TESTBED_ID, NODE, "apps"),
358 "to": (TESTBED_ID, APPLICATION, "node"),
363 "from": (TESTBED_ID, NODE, "pipes"),
364 "to": (TESTBED_ID, NETPIPE, "node"),
365 "code": connect_node_netpipe,
369 "from": (TESTBED_ID, TUNIFACE, "tcp"),
370 "to": (TESTBED_ID, TUNIFACE, "tcp"),
371 "code": functools.partial(connect_tun_iface_peer,"tcp"),
375 "from": (TESTBED_ID, TUNIFACE, "udp"),
376 "to": (TESTBED_ID, TUNIFACE, "udp"),
377 "code": functools.partial(connect_tun_iface_peer,"udp"),
383 "forward_X11": dict({
384 "name": "forward_X11",
385 "help": "Forward x11 from main namespace to the node",
386 "type": Attribute.BOOL,
388 "flags": Attribute.DesignOnly,
389 "validation_function": validation.is_bool,
393 "help": "Constrain hostname during resource discovery. May use wildcards.",
394 "type": Attribute.STRING,
395 "flags": Attribute.DesignOnly,
396 "validation_function": validation.is_string,
398 "architecture": dict({
399 "name": "architecture",
400 "help": "Constrain architexture during resource discovery.",
401 "type": Attribute.ENUM,
402 "flags": Attribute.DesignOnly,
403 "allowed": ["x86_64",
405 "validation_function": validation.is_enum,
407 "operating_system": dict({
408 "name": "operatingSystem",
409 "help": "Constrain operating system during resource discovery.",
410 "type": Attribute.ENUM,
411 "flags": Attribute.DesignOnly,
417 "validation_function": validation.is_enum,
421 "help": "Constrain the PlanetLab site this node should reside on.",
422 "type": Attribute.ENUM,
423 "flags": Attribute.DesignOnly,
427 "validation_function": validation.is_enum,
431 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
432 "type": Attribute.BOOL,
434 "flags": Attribute.DesignOnly,
435 "validation_function": validation.is_bool,
437 "min_reliability": dict({
438 "name": "minReliability",
439 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
440 "type": Attribute.DOUBLE,
442 "flags": Attribute.DesignOnly,
443 "validation_function": validation.is_double,
445 "max_reliability": dict({
446 "name": "maxReliability",
447 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
448 "type": Attribute.DOUBLE,
450 "flags": Attribute.DesignOnly,
451 "validation_function": validation.is_double,
453 "min_bandwidth": dict({
454 "name": "minBandwidth",
455 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
456 "type": Attribute.DOUBLE,
458 "flags": Attribute.DesignOnly,
459 "validation_function": validation.is_double,
461 "max_bandwidth": dict({
462 "name": "maxBandwidth",
463 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
464 "type": Attribute.DOUBLE,
466 "flags": Attribute.DesignOnly,
467 "validation_function": validation.is_double,
473 "type": Attribute.BOOL,
475 "validation_function": validation.is_bool
479 "help": "This is the primary interface for the attached node",
480 "type": Attribute.BOOL,
482 "validation_function": validation.is_bool
484 "device_name": dict({
486 "help": "Device name",
487 "type": Attribute.STRING,
488 "flags": Attribute.DesignOnly,
489 "validation_function": validation.is_string
493 "help": "Maximum transmition unit for device",
494 "type": Attribute.INTEGER,
496 "validation_function": validation.is_integer_range(0,1500)
500 "help": "Network mask for the device (eg: 24 for /24 network)",
501 "type": Attribute.INTEGER,
502 "validation_function": validation.is_integer_range(8,24)
506 "help": "Enable SNAT (source NAT to the internet) no this device",
507 "type": Attribute.BOOL,
509 "validation_function": validation.is_bool
513 "help": "Transmission queue length (in packets)",
514 "type": Attribute.INTEGER,
515 "flags": Attribute.DesignOnly,
517 "validation_function": validation.is_integer
522 "help": "Command line string",
523 "type": Attribute.STRING,
524 "flags": Attribute.DesignOnly,
525 "validation_function": validation.is_string
529 "help": "Run with root privileges",
530 "type": Attribute.BOOL,
531 "flags": Attribute.DesignOnly,
533 "validation_function": validation.is_bool
537 "help": "Standard input",
538 "type": Attribute.STRING,
539 "flags": Attribute.DesignOnly,
540 "validation_function": validation.is_string
545 "help": "Space-separated list of packages required to run the application",
546 "type": Attribute.STRING,
547 "flags": Attribute.DesignOnly,
548 "validation_function": validation.is_string
550 "build-depends": dict({
551 "name": "buildDepends",
552 "help": "Space-separated list of packages required to build the application",
553 "type": Attribute.STRING,
554 "flags": Attribute.DesignOnly,
555 "validation_function": validation.is_string
559 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
560 "Archives won't be expanded automatically.",
561 "type": Attribute.STRING,
562 "flags": Attribute.DesignOnly,
563 "validation_function": validation.is_string
567 "help": "Build commands to execute after deploying the sources. "
568 "Sources will be in the ${SOURCES} folder. "
569 "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
570 "Try to make the commands return with a nonzero exit code on error.\n"
571 "Also, do not install any programs here, use the 'install' attribute. This will "
572 "help keep the built files constrained to the build folder (which may "
573 "not be the home folder), and will result in faster deployment. Also, "
574 "make sure to clean up temporary files, to reduce bandwidth usage between "
575 "nodes when transferring built packages.",
576 "type": Attribute.STRING,
577 "flags": Attribute.DesignOnly,
578 "validation_function": validation.is_string
582 "help": "Commands to transfer built files to their final destinations. "
583 "Sources will be in the initial working folder, and a special "
584 "tag ${SOURCES} can be used to reference the experiment's "
585 "home folder (where the application commands will run).\n"
586 "ALL sources and targets needed for execution must be copied there, "
587 "if building has been enabled.\n"
588 "That is, 'slave' nodes will not automatically get any source files. "
589 "'slave' nodes don't get build dependencies either, so if you need "
590 "make and other tools to install, be sure to provide them as "
591 "actual dependencies instead.",
592 "type": Attribute.STRING,
593 "flags": Attribute.DesignOnly,
594 "validation_function": validation.is_string
597 "netpipe_mode": dict({
599 "help": "Link mode:\n"
600 " * SERVER: applies to incoming connections\n"
601 " * CLIENT: applies to outgoing connections\n"
602 " * SERVICE: applies to both",
603 "type": Attribute.ENUM,
604 "flags": Attribute.DesignOnly,
605 "allowed": ["SERVER",
608 "validation_function": validation.is_enum,
612 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
613 "type": Attribute.STRING,
614 "validation_function": is_portlist,
618 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
619 "type": Attribute.STRING,
620 "validation_function": is_addrlist,
624 "help": "Inbound bandwidth limit (in Mbit/s)",
625 "type": Attribute.DOUBLE,
626 "validation_function": validation.is_double,
630 "help": "Outbound bandwidth limit (in Mbit/s)",
631 "type": Attribute.DOUBLE,
632 "validation_function": validation.is_double,
636 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
637 "type": Attribute.DOUBLE,
638 "validation_function": validation.is_double,
642 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
643 "type": Attribute.DOUBLE,
644 "validation_function": validation.is_double,
648 "help": "Inbound packet delay (in milliseconds)",
649 "type": Attribute.INTEGER,
651 "validation_function": validation.is_integer,
655 "help": "Outbound packet delay (in milliseconds)",
656 "type": Attribute.INTEGER,
658 "validation_function": validation.is_integer,
665 "help": "Standard output stream"
669 "help": "Application standard error",
673 "help": "Output of the build process",
676 "netpipe_stats": dict({
677 "name": "netpipeStats",
678 "help": "Information about rule match counters, packets dropped, etc.",
683 "help": "Detailled log of all packets going through the interface",
687 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
689 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
691 factories_info = dict({
693 "allow_routes": False,
694 "help": "Virtualized Node (V-Server style)",
695 "category": "topology",
696 "create_function": create_node,
697 "preconfigure_function": configure_node,
710 "connector_types": ["devs", "apps", "pipes"]
713 "has_addresses": True,
714 "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
715 "category": "devices",
716 "create_function": create_nodeiface,
717 "preconfigure_function": configure_nodeiface,
718 "box_attributes": [ ],
719 "connector_types": ["node", "inet"]
722 "allow_addresses": True,
723 "help": "Virtual TUN network interface",
724 "category": "devices",
725 "create_function": create_tuniface,
726 "preconfigure_function": preconfigure_tuniface,
727 "configure_function": postconfigure_tuniface,
729 "up", "device_name", "mtu", "snat",
732 "traces": ["packets"],
733 "connector_types": ["node","udp","tcp"]
736 "help": "Generic executable command line application",
737 "category": "applications",
738 "create_function": create_application,
739 "start_function": start_application,
740 "status_function": status_application,
741 "stop_function": stop_application,
742 "configure_function": configure_application,
743 "box_attributes": ["command", "sudo", "stdin",
744 "depends", "build-depends", "build", "install",
746 "connector_types": ["node"],
747 "traces": ["stdout", "stderr"]
750 "help": "Internet routing",
751 "category": "topology",
752 "create_function": create_internet,
753 "connector_types": ["devs"],
756 "help": "Link emulation",
757 "category": "topology",
758 "create_function": create_netpipe,
759 "configure_function": configure_netpipe,
760 "box_attributes": ["netpipe_mode",
761 "addr_list", "port_list",
762 "bw_in","plr_in","delay_in",
763 "bw_out","plr_out","delay_out"],
764 "connector_types": ["node"],
765 "traces": ["netpipe_stats"]
769 testbed_attributes = dict({
772 "help": "The name of the PlanetLab slice to use",
773 "type": Attribute.STRING,
774 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
775 "validation_function": validation.is_string
779 "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
780 "type": Attribute.STRING,
781 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
782 "validation_function": validation.is_string
786 "help": "The PlanetLab user's password.",
787 "type": Attribute.STRING,
788 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
789 "validation_function": validation.is_string
791 "slice_ssh_key": dict({
792 "name": "sliceSSHKey",
793 "help": "The controller-local path to the slice user's ssh private key. "
794 "It is the user's responsability to deploy this file where the controller "
795 "will run, it won't be done automatically because it's sensitive information. "
796 "It is recommended that a NEPI-specific user be created for this purpose and "
797 "this purpose alone.",
798 "type": Attribute.STRING,
799 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
800 "validation_function": validation.is_string
804 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
806 def connector_types(self):
807 return connector_types
810 def connections(self):
814 def attributes(self):
822 def create_order(self):
826 def configure_order(self):
827 return configure_order
830 def factories_info(self):
831 return factories_info
834 def testbed_attributes(self):
835 return testbed_attributes