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_guid, iface_guid):
82 node = testbed_instance._elements[node_guid]
83 iface = testbed_instance._elements[iface_guid]
86 def connect_node_iface_inet(testbed_instance, iface_guid, inet_guid):
87 iface = testbed_instance._elements[iface_guid]
88 iface.has_internet = True
90 def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
91 node = testbed_instance._elements[node_guid]
92 iface = testbed_instance._elements[iface_guid]
93 if not node.emulation:
94 raise RuntimeError, "Use of TUN interfaces requires emulation"
96 node.required_vsys.update(('fd_tuntap', 'vif_up'))
98 def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
99 iface = testbed_instance._elements[iface_guid]
100 peer_iface = testbed_instance._elements[peer_iface_guid]
101 iface.peer_iface = peer_iface
103 iface.tun_proto = proto
105 def connect_dep(testbed_instance, node_guid, app_guid):
106 node = testbed_instance._elements[node_guid]
107 app = testbed_instance._elements[app_guid]
111 node.required_packages.update(set(
112 app.depends.split() ))
115 if app.home_path and app.home_path not in node.pythonpath:
116 node.pythonpath.append(app.home_path)
118 def connect_node_netpipe(testbed_instance, node_guid, netpipe_guid):
119 node = testbed_instance._elements[node_guid]
120 netpipe = testbed_instance._elements[netpipe_guid]
121 if not node.emulation:
122 raise RuntimeError, "Use of NetPipes requires emulation"
126 ### Creation functions ###
128 def create_node(testbed_instance, guid):
129 parameters = testbed_instance._get_parameters(guid)
131 # create element with basic attributes
132 element = testbed_instance._make_node(parameters)
134 # add constraint on number of (real) interfaces
135 # by counting connected devices
136 dev_guids = testbed_instance.get_connected(guid, "node", "devs")
137 num_open_ifaces = sum( # count True values
138 NODEIFACE == testbed_instance._get_factory_id(guid)
139 for guid in dev_guids )
140 element.min_num_external_ifaces = num_open_ifaces
142 testbed_instance.elements[guid] = element
144 def create_nodeiface(testbed_instance, guid):
145 parameters = testbed_instance._get_parameters(guid)
146 element = testbed_instance._make_node_iface(parameters)
147 testbed_instance.elements[guid] = element
149 def create_tuniface(testbed_instance, guid):
150 parameters = testbed_instance._get_parameters(guid)
151 element = testbed_instance._make_tun_iface(parameters)
152 testbed_instance.elements[guid] = element
154 def create_application(testbed_instance, guid):
155 parameters = testbed_instance._get_parameters(guid)
156 element = testbed_instance._make_application(parameters)
158 # Just inject configuration stuff
159 element.home_path = "nepi-app-%s" % (guid,)
161 testbed_instance.elements[guid] = element
163 def create_dependency(testbed_instance, guid):
164 parameters = testbed_instance._get_parameters(guid)
165 element = testbed_instance._make_dependency(parameters)
167 # Just inject configuration stuff
168 element.home_path = "nepi-dep-%s" % (guid,)
170 testbed_instance.elements[guid] = element
172 def create_nepi_dependency(testbed_instance, guid):
173 parameters = testbed_instance._get_parameters(guid)
174 element = testbed_instance._make_nepi_dependency(parameters)
176 # Just inject configuration stuff
177 element.home_path = "nepi-nepi-%s" % (guid,)
179 testbed_instance.elements[guid] = element
181 def create_internet(testbed_instance, guid):
182 parameters = testbed_instance._get_parameters(guid)
183 element = testbed_instance._make_internet(parameters)
184 testbed_instance.elements[guid] = element
186 def create_netpipe(testbed_instance, guid):
187 parameters = testbed_instance._get_parameters(guid)
188 element = testbed_instance._make_netpipe(parameters)
189 testbed_instance.elements[guid] = element
191 ### Start/Stop functions ###
193 def start_application(testbed_instance, guid):
194 parameters = testbed_instance._get_parameters(guid)
195 traces = testbed_instance._get_traces(guid)
196 app = testbed_instance.elements[guid]
198 app.stdout = "stdout" in traces
199 app.stderr = "stderr" in traces
200 app.buildlog = "buildlog" in traces
204 def stop_application(testbed_instance, guid):
205 app = testbed_instance.elements[guid]
208 ### Status functions ###
210 def status_application(testbed_instance, guid):
211 if guid not in testbed_instance.elements.keys():
212 return STATUS_NOT_STARTED
214 app = testbed_instance.elements[guid]
217 ### Configure functions ###
219 def configure_nodeiface(testbed_instance, guid):
220 element = testbed_instance._elements[guid]
222 # Cannot explicitly configure addresses
223 if guid in testbed_instance._add_address:
224 raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
227 node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
228 dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
229 siblings = [ self._element[dev_guid]
230 for dev_guid in dev_guids
231 if dev_guid != guid ]
233 # Fetch address from PLC api
234 element.pick_iface(siblings)
236 # Do some validations
239 def preconfigure_tuniface(testbed_instance, guid):
240 element = testbed_instance._elements[guid]
242 # Set custom addresses if any
243 if guid in testbed_instance._add_address:
244 addresses = testbed_instance._add_address[guid]
245 for address in addresses:
246 (address, netprefix, broadcast) = address
247 element.add_address(address, netprefix, broadcast)
249 # Link to external interface, if any
250 for iface in testbed_instance._elements.itervalues():
251 if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
252 element.external_iface = iface
255 # Set standard TUN attributes
256 element.tun_addr = element.external_iface.address
257 element.tun_port = 15000 + int(guid)
260 traces = testbed_instance._get_traces(guid)
261 element.capture = 'packets' in traces
263 # Do some validations
269 id(element) < id(element.peer_iface) )
271 def postconfigure_tuniface(testbed_instance, guid):
272 element = testbed_instance._elements[guid]
278 def configure_node(testbed_instance, guid):
279 node = testbed_instance._elements[guid]
281 # Just inject configuration stuff
282 node.home_path = "nepi-node-%s" % (guid,)
283 node.ident_path = testbed_instance.sliceSSHKey
284 node.slicename = testbed_instance.slicename
286 # Do some validations
289 # recently provisioned nodes may not be up yet
291 while not node.is_alive():
292 time.sleep(sleeptime)
293 sleeptime = min(30.0, sleeptime*1.5)
295 # this will be done in parallel in all nodes
296 # this call only spawns the process
297 node.install_dependencies()
299 def configure_application(testbed_instance, guid):
300 app = testbed_instance._elements[guid]
302 # Do some validations
305 # Wait for dependencies
306 app.node.wait_dependencies()
311 def configure_dependency(testbed_instance, guid):
312 dep = testbed_instance._elements[guid]
314 # Do some validations
317 # Wait for dependencies
318 dep.node.wait_dependencies()
323 def configure_netpipe(testbed_instance, guid):
324 netpipe = testbed_instance._elements[guid]
326 # Do some validations
329 # Wait for dependencies
330 netpipe.node.wait_dependencies()
335 ### Factory information ###
337 connector_types = dict({
339 "help": "Connector from node to applications",
345 "help": "Connector from node to network interfaces",
351 "help": "Connector from node to application dependencies "
352 "(packages and applications that need to be installed)",
358 "help": "Connector from network interfaces to the internet",
364 "help": "Connector to a Node",
370 "help": "Connector to a NetPipe",
377 "help": "ip-ip tunneling over TCP link",
383 "help": "ip-ip tunneling over UDP datagrams",
392 "from": (TESTBED_ID, NODE, "devs"),
393 "to": (TESTBED_ID, NODEIFACE, "node"),
394 "init_code": connect_node_iface_node,
398 "from": (TESTBED_ID, NODE, "devs"),
399 "to": (TESTBED_ID, TUNIFACE, "node"),
400 "init_code": connect_tun_iface_node,
404 "from": (TESTBED_ID, NODEIFACE, "inet"),
405 "to": (TESTBED_ID, INTERNET, "devs"),
406 "init_code": connect_node_iface_inet,
410 "from": (TESTBED_ID, NODE, "apps"),
411 "to": (TESTBED_ID, APPLICATION, "node"),
412 "init_code": connect_dep,
416 "from": (TESTBED_ID, NODE, "deps"),
417 "to": (TESTBED_ID, DEPENDENCY, "node"),
418 "init_code": connect_dep,
422 "from": (TESTBED_ID, NODE, "deps"),
423 "to": (TESTBED_ID, NEPIDEPENDENCY, "node"),
424 "init_code": connect_dep,
428 "from": (TESTBED_ID, NODE, "pipes"),
429 "to": (TESTBED_ID, NETPIPE, "node"),
430 "init_code": connect_node_netpipe,
434 "from": (TESTBED_ID, TUNIFACE, "tcp"),
435 "to": (TESTBED_ID, TUNIFACE, "tcp"),
436 "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
440 "from": (TESTBED_ID, TUNIFACE, "udp"),
441 "to": (TESTBED_ID, TUNIFACE, "udp"),
442 "init_code": functools.partial(connect_tun_iface_peer,"udp"),
448 "forward_X11": dict({
449 "name": "forward_X11",
450 "help": "Forward x11 from main namespace to the node",
451 "type": Attribute.BOOL,
453 "flags": Attribute.DesignOnly,
454 "validation_function": validation.is_bool,
458 "help": "Constrain hostname during resource discovery. May use wildcards.",
459 "type": Attribute.STRING,
460 "flags": Attribute.DesignOnly,
461 "validation_function": validation.is_string,
463 "architecture": dict({
464 "name": "architecture",
465 "help": "Constrain architexture during resource discovery.",
466 "type": Attribute.ENUM,
467 "flags": Attribute.DesignOnly,
468 "allowed": ["x86_64",
470 "validation_function": validation.is_enum,
472 "operating_system": dict({
473 "name": "operatingSystem",
474 "help": "Constrain operating system during resource discovery.",
475 "type": Attribute.ENUM,
476 "flags": Attribute.DesignOnly,
482 "validation_function": validation.is_enum,
486 "help": "Constrain the PlanetLab site this node should reside on.",
487 "type": Attribute.ENUM,
488 "flags": Attribute.DesignOnly,
492 "validation_function": validation.is_enum,
496 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
497 "type": Attribute.BOOL,
499 "flags": Attribute.DesignOnly,
500 "validation_function": validation.is_bool,
502 "min_reliability": dict({
503 "name": "minReliability",
504 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
505 "type": Attribute.DOUBLE,
507 "flags": Attribute.DesignOnly,
508 "validation_function": validation.is_double,
510 "max_reliability": dict({
511 "name": "maxReliability",
512 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
513 "type": Attribute.DOUBLE,
515 "flags": Attribute.DesignOnly,
516 "validation_function": validation.is_double,
518 "min_bandwidth": dict({
519 "name": "minBandwidth",
520 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
521 "type": Attribute.DOUBLE,
523 "flags": Attribute.DesignOnly,
524 "validation_function": validation.is_double,
526 "max_bandwidth": dict({
527 "name": "maxBandwidth",
528 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
529 "type": Attribute.DOUBLE,
531 "flags": Attribute.DesignOnly,
532 "validation_function": validation.is_double,
538 "type": Attribute.BOOL,
540 "validation_function": validation.is_bool
544 "help": "This is the primary interface for the attached node",
545 "type": Attribute.BOOL,
547 "validation_function": validation.is_bool
549 "device_name": dict({
551 "help": "Device name",
552 "type": Attribute.STRING,
553 "flags": Attribute.DesignOnly,
554 "validation_function": validation.is_string
558 "help": "Maximum transmition unit for device",
559 "type": Attribute.INTEGER,
561 "validation_function": validation.is_integer_range(0,1500)
565 "help": "Network mask for the device (eg: 24 for /24 network)",
566 "type": Attribute.INTEGER,
567 "validation_function": validation.is_integer_range(8,24)
571 "help": "Enable SNAT (source NAT to the internet) no this device",
572 "type": Attribute.BOOL,
574 "validation_function": validation.is_bool
578 "help": "Transmission queue length (in packets)",
579 "type": Attribute.INTEGER,
580 "flags": Attribute.DesignOnly,
582 "validation_function": validation.is_integer
587 "help": "Command line string",
588 "type": Attribute.STRING,
589 "flags": Attribute.DesignOnly,
590 "validation_function": validation.is_string
594 "help": "Run with root privileges",
595 "type": Attribute.BOOL,
596 "flags": Attribute.DesignOnly,
598 "validation_function": validation.is_bool
602 "help": "Standard input",
603 "type": Attribute.STRING,
604 "flags": Attribute.DesignOnly,
605 "validation_function": validation.is_string
610 "help": "Space-separated list of packages required to run the application",
611 "type": Attribute.STRING,
612 "flags": Attribute.DesignOnly,
613 "validation_function": validation.is_string
615 "build-depends": dict({
616 "name": "buildDepends",
617 "help": "Space-separated list of packages required to build the application",
618 "type": Attribute.STRING,
619 "flags": Attribute.DesignOnly,
620 "validation_function": validation.is_string
624 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
625 "Archives won't be expanded automatically.",
626 "type": Attribute.STRING,
627 "flags": Attribute.DesignOnly,
628 "validation_function": validation.is_string
632 "help": "Build commands to execute after deploying the sources. "
633 "Sources will be in the ${SOURCES} folder. "
634 "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
635 "Try to make the commands return with a nonzero exit code on error.\n"
636 "Also, do not install any programs here, use the 'install' attribute. This will "
637 "help keep the built files constrained to the build folder (which may "
638 "not be the home folder), and will result in faster deployment. Also, "
639 "make sure to clean up temporary files, to reduce bandwidth usage between "
640 "nodes when transferring built packages.",
641 "type": Attribute.STRING,
642 "flags": Attribute.DesignOnly,
643 "validation_function": validation.is_string
647 "help": "Commands to transfer built files to their final destinations. "
648 "Sources will be in the initial working folder, and a special "
649 "tag ${SOURCES} can be used to reference the experiment's "
650 "home folder (where the application commands will run).\n"
651 "ALL sources and targets needed for execution must be copied there, "
652 "if building has been enabled.\n"
653 "That is, 'slave' nodes will not automatically get any source files. "
654 "'slave' nodes don't get build dependencies either, so if you need "
655 "make and other tools to install, be sure to provide them as "
656 "actual dependencies instead.",
657 "type": Attribute.STRING,
658 "flags": Attribute.DesignOnly,
659 "validation_function": validation.is_string
662 "netpipe_mode": dict({
664 "help": "Link mode:\n"
665 " * SERVER: applies to incoming connections\n"
666 " * CLIENT: applies to outgoing connections\n"
667 " * SERVICE: applies to both",
668 "type": Attribute.ENUM,
669 "flags": Attribute.DesignOnly,
670 "allowed": ["SERVER",
673 "validation_function": validation.is_enum,
677 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
678 "type": Attribute.STRING,
679 "validation_function": is_portlist,
683 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
684 "type": Attribute.STRING,
685 "validation_function": is_addrlist,
689 "help": "Inbound bandwidth limit (in Mbit/s)",
690 "type": Attribute.DOUBLE,
691 "validation_function": validation.is_double,
695 "help": "Outbound bandwidth limit (in Mbit/s)",
696 "type": Attribute.DOUBLE,
697 "validation_function": validation.is_double,
701 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
702 "type": Attribute.DOUBLE,
703 "validation_function": validation.is_double,
707 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
708 "type": Attribute.DOUBLE,
709 "validation_function": validation.is_double,
713 "help": "Inbound packet delay (in milliseconds)",
714 "type": Attribute.INTEGER,
716 "validation_function": validation.is_integer,
720 "help": "Outbound packet delay (in milliseconds)",
721 "type": Attribute.INTEGER,
723 "validation_function": validation.is_integer,
730 "help": "Standard output stream"
734 "help": "Application standard error",
738 "help": "Output of the build process",
741 "netpipe_stats": dict({
742 "name": "netpipeStats",
743 "help": "Information about rule match counters, packets dropped, etc.",
748 "help": "Detailled log of all packets going through the interface",
752 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
754 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, DEPENDENCY, APPLICATION ]
756 factories_info = dict({
758 "allow_routes": False,
759 "help": "Virtualized Node (V-Server style)",
760 "category": "topology",
761 "create_function": create_node,
762 "preconfigure_function": configure_node,
775 "connector_types": ["devs", "apps", "pipes", "deps"]
778 "has_addresses": True,
779 "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
780 "category": "devices",
781 "create_function": create_nodeiface,
782 "preconfigure_function": configure_nodeiface,
783 "box_attributes": [ ],
784 "connector_types": ["node", "inet"]
787 "allow_addresses": True,
788 "help": "Virtual TUN network interface",
789 "category": "devices",
790 "create_function": create_tuniface,
791 "preconfigure_function": preconfigure_tuniface,
792 "configure_function": postconfigure_tuniface,
794 "up", "device_name", "mtu", "snat",
797 "traces": ["packets"],
798 "connector_types": ["node","udp","tcp"]
801 "help": "Generic executable command line application",
802 "category": "applications",
803 "create_function": create_application,
804 "start_function": start_application,
805 "status_function": status_application,
806 "stop_function": stop_application,
807 "configure_function": configure_application,
808 "box_attributes": ["command", "sudo", "stdin",
809 "depends", "build-depends", "build", "install",
811 "connector_types": ["node"],
812 "traces": ["stdout", "stderr", "buildlog"]
815 "help": "Requirement for package or application to be installed on some node",
816 "category": "applications",
817 "create_function": create_dependency,
818 "configure_function": configure_dependency,
819 "box_attributes": ["depends", "build-depends", "build", "install",
821 "connector_types": ["node"],
822 "traces": ["buildlog"]
824 NEPIDEPENDENCY: dict({
825 "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
826 "category": "applications",
827 "create_function": create_nepi_dependency,
828 "configure_function": configure_dependency,
829 "box_attributes": [ ],
830 "connector_types": ["node"],
831 "traces": ["buildlog"]
834 "help": "Internet routing",
835 "category": "topology",
836 "create_function": create_internet,
837 "connector_types": ["devs"],
840 "help": "Link emulation",
841 "category": "topology",
842 "create_function": create_netpipe,
843 "configure_function": configure_netpipe,
844 "box_attributes": ["netpipe_mode",
845 "addr_list", "port_list",
846 "bw_in","plr_in","delay_in",
847 "bw_out","plr_out","delay_out"],
848 "connector_types": ["node"],
849 "traces": ["netpipe_stats"]
853 testbed_attributes = dict({
856 "help": "The name of the PlanetLab slice to use",
857 "type": Attribute.STRING,
858 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
859 "validation_function": validation.is_string
863 "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
864 "type": Attribute.STRING,
865 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
866 "validation_function": validation.is_string
870 "help": "The PlanetLab user's password.",
871 "type": Attribute.STRING,
872 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
873 "validation_function": validation.is_string
875 "slice_ssh_key": dict({
876 "name": "sliceSSHKey",
877 "help": "The controller-local path to the slice user's ssh private key. "
878 "It is the user's responsability to deploy this file where the controller "
879 "will run, it won't be done automatically because it's sensitive information. "
880 "It is recommended that a NEPI-specific user be created for this purpose and "
881 "this purpose alone.",
882 "type": Attribute.STRING,
883 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
884 "validation_function": validation.is_string
888 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
890 def connector_types(self):
891 return connector_types
894 def connections(self):
898 def attributes(self):
906 def create_order(self):
910 def configure_order(self):
911 return configure_order
914 def factories_info(self):
915 return factories_info
918 def testbed_attributes(self):
919 return testbed_attributes