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"
21 DEPENDENCY = "Dependency"
22 NEPIDEPENDENCY = "NepiDependency"
26 PL_TESTBED_ID = "planetlab"
29 ### Custom validation functions ###
30 def is_addrlist(attribute, value):
31 if not validation.is_string(attribute, value):
38 components = value.split(',')
40 for component in components:
42 addr, mask = component.split('/',1)
44 addr, mask = component, 32
46 if mask is not None and not (mask and mask.isdigit()):
47 # No empty or nonnumeric masks
50 if not validation.is_ip4_address(attribute, value):
51 # Address part must be ipv4
56 def is_portlist(attribute, value):
57 if not validation.is_string(attribute, value):
64 components = value.split(',')
66 for component in components:
68 pfrom, pto = component.split('-',1)
70 pfrom = pto = component
72 if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
73 # No empty or nonnumeric ports
79 ### Connection functions ####
81 def connect_node_iface_node(testbed_instance, node, iface):
84 def connect_node_iface_inet(testbed_instance, iface, inet):
85 iface.has_internet = True
87 def connect_tun_iface_node(testbed_instance, node, iface):
88 if not node.emulation:
89 raise RuntimeError, "Use of TUN interfaces requires emulation"
91 node.required_vsys.update(('fd_tuntap', 'vif_up'))
93 def connect_tun_iface_peer(proto, testbed_instance, iface, peer_iface):
94 iface.peer_iface = peer_iface
96 iface.tun_proto = proto
98 def connect_dep(testbed_instance, node, app):
102 node.required_packages.update(set(
103 app.depends.split() ))
106 if app.home_path and app.home_path not in node.pythonpath:
107 node.pythonpath.append(app.home_path)
109 def connect_node_netpipe(testbed_instance, node, netpipe):
110 if not node.emulation:
111 raise RuntimeError, "Use of NetPipes requires emulation"
115 ### Creation functions ###
117 def create_node(testbed_instance, guid):
118 parameters = testbed_instance._get_parameters(guid)
120 # create element with basic attributes
121 element = testbed_instance._make_node(parameters)
123 # add constraint on number of (real) interfaces
124 # by counting connected devices
125 dev_guids = testbed_instance.get_connected(guid, "node", "devs")
126 num_open_ifaces = sum( # count True values
127 NODEIFACE == testbed_instance._get_factory_id(guid)
128 for guid in dev_guids )
129 element.min_num_external_ifaces = num_open_ifaces
131 testbed_instance.elements[guid] = element
133 def create_nodeiface(testbed_instance, guid):
134 parameters = testbed_instance._get_parameters(guid)
135 element = testbed_instance._make_node_iface(parameters)
136 testbed_instance.elements[guid] = element
138 def create_tuniface(testbed_instance, guid):
139 parameters = testbed_instance._get_parameters(guid)
140 element = testbed_instance._make_tun_iface(parameters)
141 testbed_instance.elements[guid] = element
143 def create_application(testbed_instance, guid):
144 parameters = testbed_instance._get_parameters(guid)
145 element = testbed_instance._make_application(parameters)
147 # Just inject configuration stuff
148 element.home_path = "nepi-app-%s" % (guid,)
150 testbed_instance.elements[guid] = element
152 def create_dependency(testbed_instance, guid):
153 parameters = testbed_instance._get_parameters(guid)
154 element = testbed_instance._make_dependency(parameters)
156 # Just inject configuration stuff
157 element.home_path = "nepi-dep-%s" % (guid,)
159 testbed_instance.elements[guid] = element
161 def create_nepi_dependency(testbed_instance, guid):
162 parameters = testbed_instance._get_parameters(guid)
163 element = testbed_instance._make_nepi_dependency(parameters)
165 # Just inject configuration stuff
166 element.home_path = "nepi-nepi-%s" % (guid,)
168 testbed_instance.elements[guid] = element
170 def create_internet(testbed_instance, guid):
171 parameters = testbed_instance._get_parameters(guid)
172 element = testbed_instance._make_internet(parameters)
173 testbed_instance.elements[guid] = element
175 def create_netpipe(testbed_instance, guid):
176 parameters = testbed_instance._get_parameters(guid)
177 element = testbed_instance._make_netpipe(parameters)
178 testbed_instance.elements[guid] = element
180 ### Start/Stop functions ###
182 def start_application(testbed_instance, guid):
183 parameters = testbed_instance._get_parameters(guid)
184 traces = testbed_instance._get_traces(guid)
185 app = testbed_instance.elements[guid]
187 app.stdout = "stdout" in traces
188 app.stderr = "stderr" in traces
189 app.buildlog = "buildlog" in traces
193 def stop_application(testbed_instance, guid):
194 app = testbed_instance.elements[guid]
197 ### Status functions ###
199 def status_application(testbed_instance, guid):
200 if guid not in testbed_instance.elements.keys():
201 return STATUS_NOT_STARTED
203 app = testbed_instance.elements[guid]
206 ### Configure functions ###
208 def configure_nodeiface(testbed_instance, guid):
209 element = testbed_instance._elements[guid]
211 # Cannot explicitly configure addresses
212 if guid in testbed_instance._add_address:
213 raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
216 node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
217 dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
218 siblings = [ self._element[dev_guid]
219 for dev_guid in dev_guids
220 if dev_guid != guid ]
222 # Fetch address from PLC api
223 element.pick_iface(siblings)
225 # Do some validations
228 def preconfigure_tuniface(testbed_instance, guid):
229 element = testbed_instance._elements[guid]
231 # Set custom addresses if any
232 if guid in testbed_instance._add_address:
233 addresses = testbed_instance._add_address[guid]
234 for address in addresses:
235 (address, netprefix, broadcast) = address
236 element.add_address(address, netprefix, broadcast)
238 # Link to external interface, if any
239 for iface in testbed_instance._elements.itervalues():
240 if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
241 element.external_iface = iface
244 # Set standard TUN attributes
245 element.tun_addr = element.external_iface.address
246 element.tun_port = 15000 + int(guid)
249 traces = testbed_instance._get_traces(guid)
250 element.capture = 'packets' in traces
252 # Do some validations
258 id(element) < id(element.peer_iface) )
260 def postconfigure_tuniface(testbed_instance, guid):
261 element = testbed_instance._elements[guid]
267 def configure_node(testbed_instance, guid):
268 node = testbed_instance._elements[guid]
270 # Just inject configuration stuff
271 node.home_path = "nepi-node-%s" % (guid,)
272 node.ident_path = testbed_instance.sliceSSHKey
273 node.slicename = testbed_instance.slicename
275 # Do some validations
278 # recently provisioned nodes may not be up yet
280 while not node.is_alive():
281 time.sleep(sleeptime)
282 sleeptime = min(30.0, sleeptime*1.5)
284 # this will be done in parallel in all nodes
285 # this call only spawns the process
286 node.install_dependencies()
288 def configure_application(testbed_instance, guid):
289 app = testbed_instance._elements[guid]
291 # Do some validations
294 # Wait for dependencies
295 app.node.wait_dependencies()
300 def configure_dependency(testbed_instance, guid):
301 dep = testbed_instance._elements[guid]
303 # Do some validations
306 # Wait for dependencies
307 dep.node.wait_dependencies()
312 def configure_netpipe(testbed_instance, guid):
313 netpipe = testbed_instance._elements[guid]
315 # Do some validations
318 # Wait for dependencies
319 netpipe.node.wait_dependencies()
324 ### Factory information ###
326 connector_types = dict({
328 "help": "Connector from node to applications",
334 "help": "Connector from node to network interfaces",
340 "help": "Connector from node to application dependencies "
341 "(packages and applications that need to be installed)",
347 "help": "Connector from network interfaces to the internet",
353 "help": "Connector to a Node",
359 "help": "Connector to a NetPipe",
366 "help": "ip-ip tunneling over TCP link",
372 "help": "ip-ip tunneling over UDP datagrams",
381 "from": (TESTBED_ID, NODE, "devs"),
382 "to": (TESTBED_ID, NODEIFACE, "node"),
383 "init_code": connect_node_iface_node,
387 "from": (TESTBED_ID, NODE, "devs"),
388 "to": (TESTBED_ID, TUNIFACE, "node"),
389 "init_code": connect_tun_iface_node,
393 "from": (TESTBED_ID, NODEIFACE, "inet"),
394 "to": (TESTBED_ID, INTERNET, "devs"),
395 "init_code": connect_node_iface_inet,
399 "from": (TESTBED_ID, NODE, "apps"),
400 "to": (TESTBED_ID, APPLICATION, "node"),
401 "init_code": connect_dep,
405 "from": (TESTBED_ID, NODE, "deps"),
406 "to": (TESTBED_ID, DEPENDENCY, "node"),
407 "init_code": connect_dep,
411 "from": (TESTBED_ID, NODE, "deps"),
412 "to": (TESTBED_ID, NEPIDEPENDENCY, "node"),
413 "init_code": connect_dep,
417 "from": (TESTBED_ID, NODE, "pipes"),
418 "to": (TESTBED_ID, NETPIPE, "node"),
419 "init_code": connect_node_netpipe,
423 "from": (TESTBED_ID, TUNIFACE, "tcp"),
424 "to": (TESTBED_ID, TUNIFACE, "tcp"),
425 "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
429 "from": (TESTBED_ID, TUNIFACE, "udp"),
430 "to": (TESTBED_ID, TUNIFACE, "udp"),
431 "init_code": functools.partial(connect_tun_iface_peer,"udp"),
437 "forward_X11": dict({
438 "name": "forward_X11",
439 "help": "Forward x11 from main namespace to the node",
440 "type": Attribute.BOOL,
442 "flags": Attribute.DesignOnly,
443 "validation_function": validation.is_bool,
447 "help": "Constrain hostname during resource discovery. May use wildcards.",
448 "type": Attribute.STRING,
449 "flags": Attribute.DesignOnly,
450 "validation_function": validation.is_string,
452 "architecture": dict({
453 "name": "architecture",
454 "help": "Constrain architexture during resource discovery.",
455 "type": Attribute.ENUM,
456 "flags": Attribute.DesignOnly,
457 "allowed": ["x86_64",
459 "validation_function": validation.is_enum,
461 "operating_system": dict({
462 "name": "operatingSystem",
463 "help": "Constrain operating system during resource discovery.",
464 "type": Attribute.ENUM,
465 "flags": Attribute.DesignOnly,
471 "validation_function": validation.is_enum,
475 "help": "Constrain the PlanetLab site this node should reside on.",
476 "type": Attribute.ENUM,
477 "flags": Attribute.DesignOnly,
481 "validation_function": validation.is_enum,
485 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
486 "type": Attribute.BOOL,
488 "flags": Attribute.DesignOnly,
489 "validation_function": validation.is_bool,
491 "min_reliability": dict({
492 "name": "minReliability",
493 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
494 "type": Attribute.DOUBLE,
496 "flags": Attribute.DesignOnly,
497 "validation_function": validation.is_double,
499 "max_reliability": dict({
500 "name": "maxReliability",
501 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
502 "type": Attribute.DOUBLE,
504 "flags": Attribute.DesignOnly,
505 "validation_function": validation.is_double,
507 "min_bandwidth": dict({
508 "name": "minBandwidth",
509 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
510 "type": Attribute.DOUBLE,
512 "flags": Attribute.DesignOnly,
513 "validation_function": validation.is_double,
515 "max_bandwidth": dict({
516 "name": "maxBandwidth",
517 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
518 "type": Attribute.DOUBLE,
520 "flags": Attribute.DesignOnly,
521 "validation_function": validation.is_double,
527 "type": Attribute.BOOL,
529 "validation_function": validation.is_bool
533 "help": "This is the primary interface for the attached node",
534 "type": Attribute.BOOL,
536 "validation_function": validation.is_bool
538 "device_name": dict({
540 "help": "Device name",
541 "type": Attribute.STRING,
542 "flags": Attribute.DesignOnly,
543 "validation_function": validation.is_string
547 "help": "Maximum transmition unit for device",
548 "type": Attribute.INTEGER,
550 "validation_function": validation.is_integer_range(0,1500)
554 "help": "Network mask for the device (eg: 24 for /24 network)",
555 "type": Attribute.INTEGER,
556 "validation_function": validation.is_integer_range(8,24)
560 "help": "Enable SNAT (source NAT to the internet) no this device",
561 "type": Attribute.BOOL,
563 "validation_function": validation.is_bool
567 "help": "Transmission queue length (in packets)",
568 "type": Attribute.INTEGER,
569 "flags": Attribute.DesignOnly,
571 "validation_function": validation.is_integer
576 "help": "Command line string",
577 "type": Attribute.STRING,
578 "flags": Attribute.DesignOnly,
579 "validation_function": validation.is_string
583 "help": "Run with root privileges",
584 "type": Attribute.BOOL,
585 "flags": Attribute.DesignOnly,
587 "validation_function": validation.is_bool
591 "help": "Standard input",
592 "type": Attribute.STRING,
593 "flags": Attribute.DesignOnly,
594 "validation_function": validation.is_string
599 "help": "Space-separated list of packages required to run the application",
600 "type": Attribute.STRING,
601 "flags": Attribute.DesignOnly,
602 "validation_function": validation.is_string
604 "build-depends": dict({
605 "name": "buildDepends",
606 "help": "Space-separated list of packages required to build the application",
607 "type": Attribute.STRING,
608 "flags": Attribute.DesignOnly,
609 "validation_function": validation.is_string
613 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
614 "Archives won't be expanded automatically.",
615 "type": Attribute.STRING,
616 "flags": Attribute.DesignOnly,
617 "validation_function": validation.is_string
621 "help": "Build commands to execute after deploying the sources. "
622 "Sources will be in the ${SOURCES} folder. "
623 "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
624 "Try to make the commands return with a nonzero exit code on error.\n"
625 "Also, do not install any programs here, use the 'install' attribute. This will "
626 "help keep the built files constrained to the build folder (which may "
627 "not be the home folder), and will result in faster deployment. Also, "
628 "make sure to clean up temporary files, to reduce bandwidth usage between "
629 "nodes when transferring built packages.",
630 "type": Attribute.STRING,
631 "flags": Attribute.DesignOnly,
632 "validation_function": validation.is_string
636 "help": "Commands to transfer built files to their final destinations. "
637 "Sources will be in the initial working folder, and a special "
638 "tag ${SOURCES} can be used to reference the experiment's "
639 "home folder (where the application commands will run).\n"
640 "ALL sources and targets needed for execution must be copied there, "
641 "if building has been enabled.\n"
642 "That is, 'slave' nodes will not automatically get any source files. "
643 "'slave' nodes don't get build dependencies either, so if you need "
644 "make and other tools to install, be sure to provide them as "
645 "actual dependencies instead.",
646 "type": Attribute.STRING,
647 "flags": Attribute.DesignOnly,
648 "validation_function": validation.is_string
651 "netpipe_mode": dict({
653 "help": "Link mode:\n"
654 " * SERVER: applies to incoming connections\n"
655 " * CLIENT: applies to outgoing connections\n"
656 " * SERVICE: applies to both",
657 "type": Attribute.ENUM,
658 "flags": Attribute.DesignOnly,
659 "allowed": ["SERVER",
662 "validation_function": validation.is_enum,
666 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
667 "type": Attribute.STRING,
668 "validation_function": is_portlist,
672 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
673 "type": Attribute.STRING,
674 "validation_function": is_addrlist,
678 "help": "Inbound bandwidth limit (in Mbit/s)",
679 "type": Attribute.DOUBLE,
680 "validation_function": validation.is_double,
684 "help": "Outbound bandwidth limit (in Mbit/s)",
685 "type": Attribute.DOUBLE,
686 "validation_function": validation.is_double,
690 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
691 "type": Attribute.DOUBLE,
692 "validation_function": validation.is_double,
696 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
697 "type": Attribute.DOUBLE,
698 "validation_function": validation.is_double,
702 "help": "Inbound packet delay (in milliseconds)",
703 "type": Attribute.INTEGER,
705 "validation_function": validation.is_integer,
709 "help": "Outbound packet delay (in milliseconds)",
710 "type": Attribute.INTEGER,
712 "validation_function": validation.is_integer,
719 "help": "Standard output stream"
723 "help": "Application standard error",
727 "help": "Output of the build process",
730 "netpipe_stats": dict({
731 "name": "netpipeStats",
732 "help": "Information about rule match counters, packets dropped, etc.",
737 "help": "Detailled log of all packets going through the interface",
741 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
743 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
745 factories_info = dict({
747 "allow_routes": False,
748 "help": "Virtualized Node (V-Server style)",
749 "category": "topology",
750 "create_function": create_node,
751 "preconfigure_function": configure_node,
764 "connector_types": ["devs", "apps", "pipes", "deps"]
767 "has_addresses": True,
768 "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
769 "category": "devices",
770 "create_function": create_nodeiface,
771 "preconfigure_function": configure_nodeiface,
772 "box_attributes": [ ],
773 "connector_types": ["node", "inet"]
776 "allow_addresses": True,
777 "help": "Virtual TUN network interface",
778 "category": "devices",
779 "create_function": create_tuniface,
780 "preconfigure_function": preconfigure_tuniface,
781 "configure_function": postconfigure_tuniface,
783 "up", "device_name", "mtu", "snat",
786 "traces": ["packets"],
787 "connector_types": ["node","udp","tcp"]
790 "help": "Generic executable command line application",
791 "category": "applications",
792 "create_function": create_application,
793 "start_function": start_application,
794 "status_function": status_application,
795 "stop_function": stop_application,
796 "configure_function": configure_application,
797 "box_attributes": ["command", "sudo", "stdin",
798 "depends", "build-depends", "build", "install",
800 "connector_types": ["node"],
801 "traces": ["stdout", "stderr", "buildlog"]
804 "help": "Requirement for package or application to be installed on some node",
805 "category": "applications",
806 "create_function": create_dependency,
807 "configure_function": configure_dependency,
808 "box_attributes": ["depends", "build-depends", "build", "install",
810 "connector_types": ["node"],
811 "traces": ["buildlog"]
813 NEPIDEPENDENCY: dict({
814 "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
815 "category": "applications",
816 "create_function": create_nepi_dependency,
817 "configure_function": configure_dependency,
818 "box_attributes": [ ],
819 "connector_types": ["node"],
820 "traces": ["buildlog"]
823 "help": "Internet routing",
824 "category": "topology",
825 "create_function": create_internet,
826 "connector_types": ["devs"],
829 "help": "Link emulation",
830 "category": "topology",
831 "create_function": create_netpipe,
832 "configure_function": configure_netpipe,
833 "box_attributes": ["netpipe_mode",
834 "addr_list", "port_list",
835 "bw_in","plr_in","delay_in",
836 "bw_out","plr_out","delay_out"],
837 "connector_types": ["node"],
838 "traces": ["netpipe_stats"]
842 testbed_attributes = dict({
845 "help": "The name of the PlanetLab slice to use",
846 "type": Attribute.STRING,
847 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
848 "validation_function": validation.is_string
852 "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
853 "type": Attribute.STRING,
854 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
855 "validation_function": validation.is_string
859 "help": "The PlanetLab user's password.",
860 "type": Attribute.STRING,
861 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
862 "validation_function": validation.is_string
864 "slice_ssh_key": dict({
865 "name": "sliceSSHKey",
866 "help": "The controller-local path to the slice user's ssh private key. "
867 "It is the user's responsability to deploy this file where the controller "
868 "will run, it won't be done automatically because it's sensitive information. "
869 "It is recommended that a NEPI-specific user be created for this purpose and "
870 "this purpose alone.",
871 "type": Attribute.STRING,
872 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
873 "validation_function": validation.is_string
877 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
879 def connector_types(self):
880 return connector_types
883 def connections(self):
887 def attributes(self):
895 def create_order(self):
899 def configure_order(self):
900 return configure_order
903 def factories_info(self):
904 return factories_info
907 def testbed_attributes(self):
908 return testbed_attributes