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, \
14 NODEIFACE = "NodeInterface"
15 TUNIFACE = "TunInterface"
16 APPLICATION = "Application"
20 PL_TESTBED_ID = "planetlab"
23 ### Custom validation functions ###
24 def is_addrlist(attribute, value):
25 if not validation.is_string(attribute, value):
32 components = value.split(',')
34 for component in components:
36 addr, mask = component.split('/',1)
38 addr, mask = component, 32
40 if mask is not None and not (mask and mask.isdigit()):
41 # No empty or nonnumeric masks
44 if not validation.is_ip4_address(attribute, value):
45 # Address part must be ipv4
50 def is_portlist(attribute, value):
51 if not validation.is_string(attribute, value):
58 components = value.split(',')
60 for component in components:
62 pfrom, pto = component.split('-',1)
64 pfrom = pto = component
66 if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
67 # No empty or nonnumeric ports
73 ### Connection functions ####
75 def connect_node_iface_node(testbed_instance, node, iface):
78 def connect_node_iface_inet(testbed_instance, iface, inet):
79 iface.has_internet = True
81 def connect_tun_iface_node(testbed_instance, node, iface):
82 if not node.emulation:
83 raise RuntimeError, "Use of TUN interfaces requires emulation"
85 node.required_vsys.update(('fd_tuntap', 'vif_up'))
87 def connect_app(testbed_instance, node, app):
91 node.required_packages.update(set(
92 app.depends.split() ))
94 def connect_node_netpipe(testbed_instance, node, netpipe):
95 if not node.emulation:
96 raise RuntimeError, "Use of NetPipes requires emulation"
100 ### Creation functions ###
102 def create_node(testbed_instance, guid):
103 parameters = testbed_instance._get_parameters(guid)
105 # create element with basic attributes
106 element = testbed_instance._make_node(parameters)
108 # add constraint on number of (real) interfaces
109 # by counting connected devices
110 dev_guids = testbed_instance.get_connected(guid, "node", "devs")
111 num_open_ifaces = sum( # count True values
112 TUNEIFACE == testbed_instance._get_factory_id(guid)
113 for guid in dev_guids )
114 element.min_num_external_ifaces = num_open_ifaces
116 testbed_instance.elements[guid] = element
118 def create_nodeiface(testbed_instance, guid):
119 parameters = testbed_instance._get_parameters(guid)
120 element = testbed_instance._make_node_iface(parameters)
121 testbed_instance.elements[guid] = element
123 def create_tuniface(testbed_instance, guid):
124 parameters = testbed_instance._get_parameters(guid)
125 element = testbed_instance._make_tun_iface(parameters)
126 testbed_instance.elements[guid] = element
128 def create_application(testbed_instance, guid):
129 parameters = testbed_instance._get_parameters(guid)
130 element = testbed_instance._make_application(parameters)
131 testbed_instance.elements[guid] = element
133 def create_internet(testbed_instance, guid):
134 parameters = testbed_instance._get_parameters(guid)
135 element = testbed_instance._make_internet(parameters)
136 testbed_instance.elements[guid] = element
138 def create_netpipe(testbed_instance, guid):
139 parameters = testbed_instance._get_parameters(guid)
140 element = testbed_instance._make_netpipe(parameters)
141 testbed_instance.elements[guid] = element
143 ### Start/Stop functions ###
145 def start_application(testbed_instance, guid):
146 parameters = testbed_instance._get_parameters(guid)
147 traces = testbed_instance._get_traces(guid)
148 app = testbed_instance.elements[guid]
150 app.stdout = "stdout" in traces
151 app.stderr = "stderr" in traces
152 app.buildlog = "buildlog" in traces
156 def stop_application(testbed_instance, guid):
157 app = testbed_instance.elements[guid]
160 ### Status functions ###
162 def status_application(testbed_instance, guid):
163 if guid not in testbed_instance.elements.keys():
164 return STATUS_NOT_STARTED
166 app = testbed_instance.elements[guid]
169 ### Configure functions ###
171 def configure_nodeiface(testbed_instance, guid):
172 element = testbed_instance._elements[guid]
174 # Cannot explicitly configure addresses
175 if guid in testbed_instance._add_address:
176 del testbed_instance._add_address[guid]
179 node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
180 dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
181 siblings = [ self._element[dev_guid]
182 for dev_guid in dev_guids
183 if dev_guid != guid ]
185 # Fetch address from PLC api
186 element.pick_iface(siblings)
188 # Do some validations
191 def configure_tuniface(testbed_instance, guid):
192 element = testbed_instance._elements[guid]
193 if not guid in testbed_instance._add_address:
196 addresses = testbed_instance._add_address[guid]
197 for address in addresses:
198 (address, netprefix, broadcast) = address
199 raise NotImplementedError, "C'mon... TUNs are hard..."
201 # Do some validations
204 def configure_node(testbed_instance, guid):
205 node = testbed_instance._elements[guid]
207 # Just inject configuration stuff
208 node.home_path = "nepi-node-%s" % (guid,)
209 node.ident_path = testbed_instance.sliceSSHKey
210 node.slicename = testbed_instance.slicename
212 # Do some validations
215 # recently provisioned nodes may not be up yet
217 while not node.is_alive():
218 time.sleep(sleeptime)
219 sleeptime = min(30.0, sleeptime*1.5)
221 # this will be done in parallel in all nodes
222 # this call only spawns the process
223 node.install_dependencies()
225 def configure_application(testbed_instance, guid):
226 app = testbed_instance._elements[guid]
228 # Just inject configuration stuff
229 app.home_path = "nepi-app-%s" % (guid,)
230 app.ident_path = testbed_instance.sliceSSHKey
231 app.slicename = testbed_instance.slicename
233 # Do some validations
236 # Wait for dependencies
237 app.node.wait_dependencies()
242 def configure_netpipe(testbed_instance, guid):
243 netpipe = testbed_instance._elements[guid]
245 # Do some validations
248 # Wait for dependencies
249 netpipe.node.wait_dependencies()
254 ### Factory information ###
256 connector_types = dict({
258 "help": "Connector from node to applications",
264 "help": "Connector from node to network interfaces",
270 "help": "Connector from network interfaces to the internet",
276 "help": "Connector to a Node",
282 "help": "Connector to a NetPipe",
291 "from": (TESTBED_ID, NODE, "devs"),
292 "to": (TESTBED_ID, NODEIFACE, "node"),
293 "code": connect_node_iface_node,
297 "from": (TESTBED_ID, NODE, "devs"),
298 "to": (TESTBED_ID, TUNIFACE, "node"),
299 "code": connect_tun_iface_node,
303 "from": (TESTBED_ID, NODEIFACE, "inet"),
304 "to": (TESTBED_ID, INTERNET, "devs"),
305 "code": connect_node_iface_inet,
309 "from": (TESTBED_ID, NODE, "apps"),
310 "to": (TESTBED_ID, APPLICATION, "node"),
315 "from": (TESTBED_ID, NODE, "pipes"),
316 "to": (TESTBED_ID, NETPIPE, "node"),
317 "code": connect_node_netpipe,
323 "forward_X11": dict({
324 "name": "forward_X11",
325 "help": "Forward x11 from main namespace to the node",
326 "type": Attribute.BOOL,
328 "flags": Attribute.DesignOnly,
329 "validation_function": validation.is_bool,
333 "help": "Constrain hostname during resource discovery. May use wildcards.",
334 "type": Attribute.STRING,
335 "flags": Attribute.DesignOnly,
336 "validation_function": validation.is_string,
338 "architecture": dict({
339 "name": "architecture",
340 "help": "Constrain architexture during resource discovery.",
341 "type": Attribute.ENUM,
342 "flags": Attribute.DesignOnly,
343 "allowed": ["x86_64",
345 "validation_function": validation.is_enum,
347 "operating_system": dict({
348 "name": "operatingSystem",
349 "help": "Constrain operating system during resource discovery.",
350 "type": Attribute.ENUM,
351 "flags": Attribute.DesignOnly,
357 "validation_function": validation.is_enum,
361 "help": "Constrain the PlanetLab site this node should reside on.",
362 "type": Attribute.ENUM,
363 "flags": Attribute.DesignOnly,
367 "validation_function": validation.is_enum,
371 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
372 "type": Attribute.BOOL,
374 "flags": Attribute.DesignOnly,
375 "validation_function": validation.is_bool,
377 "min_reliability": dict({
378 "name": "minReliability",
379 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
380 "type": Attribute.DOUBLE,
382 "flags": Attribute.DesignOnly,
383 "validation_function": validation.is_double,
385 "max_reliability": dict({
386 "name": "maxReliability",
387 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
388 "type": Attribute.DOUBLE,
390 "flags": Attribute.DesignOnly,
391 "validation_function": validation.is_double,
393 "min_bandwidth": dict({
394 "name": "minBandwidth",
395 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
396 "type": Attribute.DOUBLE,
398 "flags": Attribute.DesignOnly,
399 "validation_function": validation.is_double,
401 "max_bandwidth": dict({
402 "name": "maxBandwidth",
403 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
404 "type": Attribute.DOUBLE,
406 "flags": Attribute.DesignOnly,
407 "validation_function": validation.is_double,
413 "type": Attribute.BOOL,
415 "validation_function": validation.is_bool
419 "help": "This is the primary interface for the attached node",
420 "type": Attribute.BOOL,
422 "validation_function": validation.is_bool
424 "device_name": dict({
426 "help": "Device name",
427 "type": Attribute.STRING,
428 "flags": Attribute.DesignOnly,
429 "validation_function": validation.is_string
433 "help": "Maximum transmition unit for device",
434 "type": Attribute.INTEGER,
436 "validation_function": validation.is_integer_range(0,1500)
440 "help": "Network mask for the device (eg: 24 for /24 network)",
441 "type": Attribute.INTEGER,
442 "validation_function": validation.is_integer_range(8,24)
446 "help": "Enable SNAT (source NAT to the internet) no this device",
447 "type": Attribute.BOOL,
449 "validation_function": validation.is_bool
454 "help": "Command line string",
455 "type": Attribute.STRING,
456 "flags": Attribute.DesignOnly,
457 "validation_function": validation.is_string
461 "help": "Run with root privileges",
462 "type": Attribute.BOOL,
463 "flags": Attribute.DesignOnly,
465 "validation_function": validation.is_bool
469 "help": "Standard input",
470 "type": Attribute.STRING,
471 "flags": Attribute.DesignOnly,
472 "validation_function": validation.is_string
477 "help": "Space-separated list of packages required to run the application",
478 "type": Attribute.STRING,
479 "flags": Attribute.DesignOnly,
480 "validation_function": validation.is_string
482 "build-depends": dict({
483 "name": "buildDepends",
484 "help": "Space-separated list of packages required to build the application",
485 "type": Attribute.STRING,
486 "flags": Attribute.DesignOnly,
487 "validation_function": validation.is_string
491 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
492 "Archives won't be expanded automatically.",
493 "type": Attribute.STRING,
494 "flags": Attribute.DesignOnly,
495 "validation_function": validation.is_string
499 "help": "Build commands to execute after deploying the sources. "
500 "Sources will be in the ${SOURCES} folder. "
501 "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
502 "Try to make the commands return with a nonzero exit code on error.\n"
503 "Also, do not install any programs here, use the 'install' attribute. This will "
504 "help keep the built files constrained to the build folder (which may "
505 "not be the home folder), and will result in faster deployment. Also, "
506 "make sure to clean up temporary files, to reduce bandwidth usage between "
507 "nodes when transferring built packages.",
508 "type": Attribute.STRING,
509 "flags": Attribute.DesignOnly,
510 "validation_function": validation.is_string
514 "help": "Commands to transfer built files to their final destinations. "
515 "Sources will be in the initial working folder, and a special "
516 "tag ${SOURCES} can be used to reference the experiment's "
517 "home folder (where the application commands will run).\n"
518 "ALL sources and targets needed for execution must be copied there, "
519 "if building has been enabled.\n"
520 "That is, 'slave' nodes will not automatically get any source files. "
521 "'slave' nodes don't get build dependencies either, so if you need "
522 "make and other tools to install, be sure to provide them as "
523 "actual dependencies instead.",
524 "type": Attribute.STRING,
525 "flags": Attribute.DesignOnly,
526 "validation_function": validation.is_string
529 "netpipe_mode": dict({
531 "help": "Link mode:\n"
532 " * SERVER: applies to incoming connections\n"
533 " * CLIENT: applies to outgoing connections\n"
534 " * SERVICE: applies to both",
535 "type": Attribute.ENUM,
536 "flags": Attribute.DesignOnly,
537 "allowed": ["SERVER",
540 "validation_function": validation.is_enum,
544 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
545 "type": Attribute.STRING,
546 "validation_function": is_portlist,
550 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
551 "type": Attribute.STRING,
552 "validation_function": is_addrlist,
556 "help": "Inbound bandwidth limit (in Mbit/s)",
557 "type": Attribute.DOUBLE,
558 "validation_function": validation.is_double,
562 "help": "Outbound bandwidth limit (in Mbit/s)",
563 "type": Attribute.DOUBLE,
564 "validation_function": validation.is_double,
568 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
569 "type": Attribute.DOUBLE,
570 "validation_function": validation.is_double,
574 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
575 "type": Attribute.DOUBLE,
576 "validation_function": validation.is_double,
580 "help": "Inbound packet delay (in milliseconds)",
581 "type": Attribute.INTEGER,
583 "validation_function": validation.is_integer,
587 "help": "Outbound packet delay (in milliseconds)",
588 "type": Attribute.INTEGER,
590 "validation_function": validation.is_integer,
597 "help": "Standard output stream"
601 "help": "Application standard error",
605 "help": "Output of the build process",
609 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
611 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
613 factories_info = dict({
615 "allow_routes": False,
616 "help": "Virtualized Node (V-Server style)",
617 "category": "topology",
618 "create_function": create_node,
619 "preconfigure_function": configure_node,
632 "connector_types": ["devs", "apps", "pipes"]
635 "allow_addresses": True,
636 "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
637 "category": "devices",
638 "create_function": create_nodeiface,
639 "preconfigure_function": configure_nodeiface,
640 "box_attributes": [ ],
641 "connector_types": ["node", "inet"]
644 "allow_addresses": True,
645 "help": "Virtual TUN network interface",
646 "category": "devices",
647 "create_function": create_tuniface,
648 "preconfigure_function": configure_tuniface,
650 "up", "device_name", "mtu", "snat",
652 "connector_types": ["node"]
655 "help": "Generic executable command line application",
656 "category": "applications",
657 "create_function": create_application,
658 "start_function": start_application,
659 "status_function": status_application,
660 "stop_function": stop_application,
661 "configure_function": configure_application,
662 "box_attributes": ["command", "sudo", "stdin",
663 "depends", "build-depends", "build", "install",
665 "connector_types": ["node"],
666 "traces": ["stdout", "stderr"]
669 "help": "Internet routing",
670 "category": "topology",
671 "create_function": create_internet,
672 "connector_types": ["devs"],
675 "help": "Link emulation",
676 "category": "topology",
677 "create_function": create_netpipe,
678 "configure_function": configure_netpipe,
679 "box_attributes": ["netpipe_mode",
680 "addr_list", "port_list",
681 "bw_in","plr_in","delay_in",
682 "bw_out","plr_out","delay_out"],
683 "connector_types": ["node"],
684 "traces": ["stdout", "stderr"]
688 testbed_attributes = dict({
691 "help": "The name of the PlanetLab slice to use",
692 "type": Attribute.STRING,
693 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
694 "validation_function": validation.is_string
698 "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
699 "type": Attribute.STRING,
700 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
701 "validation_function": validation.is_string
705 "help": "The PlanetLab user's password.",
706 "type": Attribute.STRING,
707 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
708 "validation_function": validation.is_string
710 "slice_ssh_key": dict({
711 "name": "sliceSSHKey",
712 "help": "The controller-local path to the slice user's ssh private key. "
713 "It is the user's responsability to deploy this file where the controller "
714 "will run, it won't be done automatically because it's sensitive information. "
715 "It is recommended that a NEPI-specific user be created for this purpose and "
716 "this purpose alone.",
717 "type": Attribute.STRING,
718 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
719 "validation_function": validation.is_string
723 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
725 def connector_types(self):
726 return connector_types
729 def connections(self):
733 def attributes(self):
741 def create_order(self):
745 def configure_order(self):
746 return configure_order
749 def factories_info(self):
750 return factories_info
753 def testbed_attributes(self):
754 return testbed_attributes