Ticket #29: introduce some "standard" box attributes to support testbed-in-testbed...
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import time
5
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, \
11         STATUS_FINISHED, ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP
12
13 import functools
14 import os
15 import os.path
16
17 NODE = "Node"
18 NODEIFACE = "NodeInterface"
19 TUNIFACE = "TunInterface"
20 APPLICATION = "Application"
21 DEPENDENCY = "Dependency"
22 NEPIDEPENDENCY = "NepiDependency"
23 NS3DEPENDENCY = "NS3Dependency"
24 INTERNET = "Internet"
25 NETPIPE = "NetPipe"
26
27 PL_TESTBED_ID = "planetlab"
28
29
30 ### Custom validation functions ###
31 def is_addrlist(attribute, value):
32     if not validation.is_string(attribute, value):
33         return False
34     
35     if not value:
36         # No empty strings
37         return False
38     
39     components = value.split(',')
40     
41     for component in components:
42         if '/' in component:
43             addr, mask = component.split('/',1)
44         else:
45             addr, mask = component, 32
46         
47         if mask is not None and not (mask and mask.isdigit()):
48             # No empty or nonnumeric masks
49             return False
50         
51         if not validation.is_ip4_address(attribute, value):
52             # Address part must be ipv4
53             return False
54         
55     return True
56
57 def is_portlist(attribute, value):
58     if not validation.is_string(attribute, value):
59         return False
60     
61     if not value:
62         # No empty strings
63         return False
64     
65     components = value.split(',')
66     
67     for component in components:
68         if '-' in component:
69             pfrom, pto = component.split('-',1)
70         else:
71             pfrom = pto = component
72         
73         if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
74             # No empty or nonnumeric ports
75             return False
76         
77     return True
78
79
80 ### Connection functions ####
81
82 def connect_node_iface_node(testbed_instance, node_guid, iface_guid):
83     node = testbed_instance._elements[node_guid]
84     iface = testbed_instance._elements[iface_guid]
85     iface.node = node
86
87 def connect_node_iface_inet(testbed_instance, iface_guid, inet_guid):
88     iface = testbed_instance._elements[iface_guid]
89     iface.has_internet = True
90
91 def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
92     node = testbed_instance._elements[node_guid]
93     iface = testbed_instance._elements[iface_guid]
94     if not node.emulation:
95         raise RuntimeError, "Use of TUN interfaces requires emulation"
96     iface.node = node
97     node.required_vsys.update(('fd_tuntap', 'vif_up'))
98
99 def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
100     iface = testbed_instance._elements[iface_guid]
101     peer_iface = testbed_instance._elements[peer_iface_guid]
102     iface.peer_iface = peer_iface
103     iface.peer_proto = \
104     iface.tun_proto = proto
105
106 def connect_dep(testbed_instance, node_guid, app_guid):
107     node = testbed_instance._elements[node_guid]
108     app = testbed_instance._elements[app_guid]
109     app.node = node
110     
111     if app.depends:
112         node.required_packages.update(set(
113             app.depends.split() ))
114     
115     if app.add_to_path:
116         if app.home_path and app.home_path not in node.pythonpath:
117             node.pythonpath.append(app.home_path)
118     
119     if app.env:
120         for envkey, envval in app.env.iteritems():
121             envval = app._replace_paths(envval)
122             node.env[envkey].append(envval)
123
124 def connect_node_netpipe(testbed_instance, node_guid, netpipe_guid):
125     node = testbed_instance._elements[node_guid]
126     netpipe = testbed_instance._elements[netpipe_guid]
127     if not node.emulation:
128         raise RuntimeError, "Use of NetPipes requires emulation"
129     netpipe.node = node
130     
131
132 ### Creation functions ###
133
134 def create_node(testbed_instance, guid):
135     parameters = testbed_instance._get_parameters(guid)
136     
137     # create element with basic attributes
138     element = testbed_instance._make_node(parameters)
139     
140     # add constraint on number of (real) interfaces
141     # by counting connected devices
142     dev_guids = testbed_instance.get_connected(guid, "node", "devs")
143     num_open_ifaces = sum( # count True values
144         NODEIFACE == testbed_instance._get_factory_id(guid)
145         for guid in dev_guids )
146     element.min_num_external_ifaces = num_open_ifaces
147     
148     testbed_instance.elements[guid] = element
149
150 def create_nodeiface(testbed_instance, guid):
151     parameters = testbed_instance._get_parameters(guid)
152     element = testbed_instance._make_node_iface(parameters)
153     testbed_instance.elements[guid] = element
154
155 def create_tuniface(testbed_instance, guid):
156     parameters = testbed_instance._get_parameters(guid)
157     element = testbed_instance._make_tun_iface(parameters)
158     testbed_instance.elements[guid] = element
159
160 def create_application(testbed_instance, guid):
161     parameters = testbed_instance._get_parameters(guid)
162     element = testbed_instance._make_application(parameters)
163     
164     # Just inject configuration stuff
165     element.home_path = "nepi-app-%s" % (guid,)
166     
167     testbed_instance.elements[guid] = element
168
169 def create_dependency(testbed_instance, guid):
170     parameters = testbed_instance._get_parameters(guid)
171     element = testbed_instance._make_dependency(parameters)
172     
173     # Just inject configuration stuff
174     element.home_path = "nepi-dep-%s" % (guid,)
175     
176     testbed_instance.elements[guid] = element
177
178 def create_nepi_dependency(testbed_instance, guid):
179     parameters = testbed_instance._get_parameters(guid)
180     element = testbed_instance._make_nepi_dependency(parameters)
181     
182     # Just inject configuration stuff
183     element.home_path = "nepi-nepi-%s" % (guid,)
184     
185     testbed_instance.elements[guid] = element
186
187 def create_ns3_dependency(testbed_instance, guid):
188     parameters = testbed_instance._get_parameters(guid)
189     element = testbed_instance._make_ns3_dependency(parameters)
190     
191     # Just inject configuration stuff
192     element.home_path = "nepi-ns3-%s" % (guid,)
193     
194     testbed_instance.elements[guid] = element
195
196 def create_internet(testbed_instance, guid):
197     parameters = testbed_instance._get_parameters(guid)
198     element = testbed_instance._make_internet(parameters)
199     testbed_instance.elements[guid] = element
200
201 def create_netpipe(testbed_instance, guid):
202     parameters = testbed_instance._get_parameters(guid)
203     element = testbed_instance._make_netpipe(parameters)
204     testbed_instance.elements[guid] = element
205
206 ### Start/Stop functions ###
207
208 def start_application(testbed_instance, guid):
209     parameters = testbed_instance._get_parameters(guid)
210     traces = testbed_instance._get_traces(guid)
211     app = testbed_instance.elements[guid]
212     
213     app.stdout = "stdout" in traces
214     app.stderr = "stderr" in traces
215     app.buildlog = "buildlog" in traces
216     
217     app.start()
218
219 def stop_application(testbed_instance, guid):
220     app = testbed_instance.elements[guid]
221     app.kill()
222
223 ### Status functions ###
224
225 def status_application(testbed_instance, guid):
226     if guid not in testbed_instance.elements.keys():
227         return STATUS_NOT_STARTED
228     
229     app = testbed_instance.elements[guid]
230     return app.status()
231
232 ### Configure functions ###
233
234 def configure_nodeiface(testbed_instance, guid):
235     element = testbed_instance._elements[guid]
236     
237     # Cannot explicitly configure addresses
238     if guid in testbed_instance._add_address:
239         raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
240     
241     # Get siblings
242     node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
243     dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
244     siblings = [ self._element[dev_guid] 
245                  for dev_guid in dev_guids
246                  if dev_guid != guid ]
247     
248     # Fetch address from PLC api
249     element.pick_iface(siblings)
250     
251     # Do some validations
252     element.validate()
253
254 def preconfigure_tuniface(testbed_instance, guid):
255     element = testbed_instance._elements[guid]
256     
257     # Set custom addresses if any
258     if guid in testbed_instance._add_address:
259         addresses = testbed_instance._add_address[guid]
260         for address in addresses:
261             (address, netprefix, broadcast) = address
262             element.add_address(address, netprefix, broadcast)
263     
264     # Link to external interface, if any
265     for iface in testbed_instance._elements.itervalues():
266         if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
267             element.external_iface = iface
268             break
269
270     # Set standard TUN attributes
271     element.tun_addr = element.external_iface.address
272     element.tun_port = 15000 + int(guid)
273
274     # Set enabled traces
275     traces = testbed_instance._get_traces(guid)
276     element.capture = 'packets' in traces
277     
278     # Do some validations
279     element.validate()
280     
281     # First-phase setup
282     element.prepare( 
283         'tun-%s' % (guid,),
284         id(element) < id(element.peer_iface) )
285
286 def postconfigure_tuniface(testbed_instance, guid):
287     element = testbed_instance._elements[guid]
288     
289     # Second-phase setup
290     element.setup()
291     
292
293 def configure_node(testbed_instance, guid):
294     node = testbed_instance._elements[guid]
295     
296     # Just inject configuration stuff
297     node.home_path = "nepi-node-%s" % (guid,)
298     node.ident_path = testbed_instance.sliceSSHKey
299     node.slicename = testbed_instance.slicename
300     
301     # Do some validations
302     node.validate()
303     
304     # recently provisioned nodes may not be up yet
305     sleeptime = 1.0
306     while not node.is_alive():
307         time.sleep(sleeptime)
308         sleeptime = min(30.0, sleeptime*1.5)
309     
310     # this will be done in parallel in all nodes
311     # this call only spawns the process
312     node.install_dependencies()
313
314 def configure_application(testbed_instance, guid):
315     app = testbed_instance._elements[guid]
316     
317     # Do some validations
318     app.validate()
319     
320     # Wait for dependencies
321     app.node.wait_dependencies()
322     
323     # Install stuff
324     app.setup()
325
326 def configure_dependency(testbed_instance, guid):
327     dep = testbed_instance._elements[guid]
328     
329     # Do some validations
330     dep.validate()
331     
332     # Wait for dependencies
333     dep.node.wait_dependencies()
334     
335     # Install stuff
336     dep.setup()
337
338 def configure_netpipe(testbed_instance, guid):
339     netpipe = testbed_instance._elements[guid]
340     
341     # Do some validations
342     netpipe.validate()
343     
344     # Wait for dependencies
345     netpipe.node.wait_dependencies()
346     
347     # Install rules
348     netpipe.configure()
349
350 ### Factory information ###
351
352 connector_types = dict({
353     "apps": dict({
354                 "help": "Connector from node to applications", 
355                 "name": "apps",
356                 "max": -1, 
357                 "min": 0
358             }),
359     "devs": dict({
360                 "help": "Connector from node to network interfaces", 
361                 "name": "devs",
362                 "max": -1, 
363                 "min": 0
364             }),
365     "deps": dict({
366                 "help": "Connector from node to application dependencies "
367                         "(packages and applications that need to be installed)", 
368                 "name": "deps",
369                 "max": -1, 
370                 "min": 0
371             }),
372     "inet": dict({
373                 "help": "Connector from network interfaces to the internet", 
374                 "name": "inet",
375                 "max": 1, 
376                 "min": 1
377             }),
378     "node": dict({
379                 "help": "Connector to a Node", 
380                 "name": "node",
381                 "max": 1, 
382                 "min": 1
383             }),
384     "pipes": dict({
385                 "help": "Connector to a NetPipe", 
386                 "name": "pipes",
387                 "max": 2, 
388                 "min": 0
389             }),
390     
391     "tcp": dict({
392                 "help": "ip-ip tunneling over TCP link", 
393                 "name": "tcp",
394                 "max": 1, 
395                 "min": 0
396             }),
397     "udp": dict({
398                 "help": "ip-ip tunneling over UDP datagrams", 
399                 "name": "udp",
400                 "max": 1, 
401                 "min": 0
402             }),
403    })
404
405 connections = [
406     dict({
407         "from": (TESTBED_ID, NODE, "devs"),
408         "to":   (TESTBED_ID, NODEIFACE, "node"),
409         "init_code": connect_node_iface_node,
410         "can_cross": False
411     }),
412     dict({
413         "from": (TESTBED_ID, NODE, "devs"),
414         "to":   (TESTBED_ID, TUNIFACE, "node"),
415         "init_code": connect_tun_iface_node,
416         "can_cross": False
417     }),
418     dict({
419         "from": (TESTBED_ID, NODEIFACE, "inet"),
420         "to":   (TESTBED_ID, INTERNET, "devs"),
421         "init_code": connect_node_iface_inet,
422         "can_cross": False
423     }),
424     dict({
425         "from": (TESTBED_ID, NODE, "apps"),
426         "to":   (TESTBED_ID, APPLICATION, "node"),
427         "init_code": connect_dep,
428         "can_cross": False
429     }),
430     dict({
431         "from": (TESTBED_ID, NODE, "deps"),
432         "to":   (TESTBED_ID, DEPENDENCY, "node"),
433         "init_code": connect_dep,
434         "can_cross": False
435     }),
436     dict({
437         "from": (TESTBED_ID, NODE, "deps"),
438         "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
439         "init_code": connect_dep,
440         "can_cross": False
441     }),
442     dict({
443         "from": (TESTBED_ID, NODE, "deps"),
444         "to":   (TESTBED_ID, NS3DEPENDENCY, "node"),
445         "init_code": connect_dep,
446         "can_cross": False
447     }),
448     dict({
449         "from": (TESTBED_ID, NODE, "pipes"),
450         "to":   (TESTBED_ID, NETPIPE, "node"),
451         "init_code": connect_node_netpipe,
452         "can_cross": False
453     }),
454     dict({
455         "from": (TESTBED_ID, TUNIFACE, "tcp"),
456         "to":   (TESTBED_ID, TUNIFACE, "tcp"),
457         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
458         "can_cross": False
459     }),
460     dict({
461         "from": (TESTBED_ID, TUNIFACE, "udp"),
462         "to":   (TESTBED_ID, TUNIFACE, "udp"),
463         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
464         "can_cross": False
465     }),
466 ]
467
468 attributes = dict({
469     "forward_X11": dict({      
470                 "name": "forward_X11",
471                 "help": "Forward x11 from main namespace to the node",
472                 "type": Attribute.BOOL, 
473                 "value": False,
474                 "flags": Attribute.DesignOnly,
475                 "validation_function": validation.is_bool,
476             }),
477     "hostname": dict({      
478                 "name": "hostname",
479                 "help": "Constrain hostname during resource discovery. May use wildcards.",
480                 "type": Attribute.STRING, 
481                 "flags": Attribute.DesignOnly,
482                 "validation_function": validation.is_string,
483             }),
484     "architecture": dict({      
485                 "name": "architecture",
486                 "help": "Constrain architexture during resource discovery.",
487                 "type": Attribute.ENUM, 
488                 "flags": Attribute.DesignOnly,
489                 "allowed": ["x86_64",
490                             "i386"],
491                 "validation_function": validation.is_enum,
492             }),
493     "operating_system": dict({      
494                 "name": "operatingSystem",
495                 "help": "Constrain operating system during resource discovery.",
496                 "type": Attribute.ENUM, 
497                 "flags": Attribute.DesignOnly,
498                 "allowed": ["f8",
499                             "f12",
500                             "f14",
501                             "centos",
502                             "other"],
503                 "validation_function": validation.is_enum,
504             }),
505     "site": dict({      
506                 "name": "site",
507                 "help": "Constrain the PlanetLab site this node should reside on.",
508                 "type": Attribute.ENUM, 
509                 "flags": Attribute.DesignOnly,
510                 "allowed": ["PLE",
511                             "PLC",
512                             "PLJ"],
513                 "validation_function": validation.is_enum,
514             }),
515     "emulation": dict({      
516                 "name": "emulation",
517                 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
518                 "type": Attribute.BOOL,
519                 "value": False, 
520                 "flags": Attribute.DesignOnly,
521                 "validation_function": validation.is_bool,
522             }),
523     "min_reliability": dict({
524                 "name": "minReliability",
525                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
526                 "type": Attribute.DOUBLE,
527                 "range": (0,100),
528                 "flags": Attribute.DesignOnly,
529                 "validation_function": validation.is_double,
530             }),
531     "max_reliability": dict({
532                 "name": "maxReliability",
533                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
534                 "type": Attribute.DOUBLE,
535                 "range": (0,100),
536                 "flags": Attribute.DesignOnly,
537                 "validation_function": validation.is_double,
538             }),
539     "min_bandwidth": dict({
540                 "name": "minBandwidth",
541                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
542                 "type": Attribute.DOUBLE,
543                 "range": (0,2**31),
544                 "flags": Attribute.DesignOnly,
545                 "validation_function": validation.is_double,
546             }),
547     "max_bandwidth": dict({
548                 "name": "maxBandwidth",
549                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
550                 "type": Attribute.DOUBLE,
551                 "range": (0,2**31),
552                 "flags": Attribute.DesignOnly,
553                 "validation_function": validation.is_double,
554             }),
555             
556     "up": dict({
557                 "name": "up",
558                 "help": "Link up",
559                 "type": Attribute.BOOL,
560                 "value": False,
561                 "validation_function": validation.is_bool
562             }),
563     "primary": dict({
564                 "name": "primary",
565                 "help": "This is the primary interface for the attached node",
566                 "type": Attribute.BOOL,
567                 "value": True,
568                 "validation_function": validation.is_bool
569             }),
570     "device_name": dict({
571                 "name": "name",
572                 "help": "Device name",
573                 "type": Attribute.STRING,
574                 "flags": Attribute.DesignOnly,
575                 "validation_function": validation.is_string
576             }),
577     "mtu":  dict({
578                 "name": "mtu", 
579                 "help": "Maximum transmition unit for device",
580                 "type": Attribute.INTEGER,
581                 "range": (0,1500),
582                 "validation_function": validation.is_integer_range(0,1500)
583             }),
584     "mask":  dict({
585                 "name": "mask", 
586                 "help": "Network mask for the device (eg: 24 for /24 network)",
587                 "type": Attribute.INTEGER,
588                 "validation_function": validation.is_integer_range(8,24)
589             }),
590     "snat":  dict({
591                 "name": "snat", 
592                 "help": "Enable SNAT (source NAT to the internet) no this device",
593                 "type": Attribute.BOOL,
594                 "value": False,
595                 "validation_function": validation.is_bool
596             }),
597     "txqueuelen":  dict({
598                 "name": "mask", 
599                 "help": "Transmission queue length (in packets)",
600                 "type": Attribute.INTEGER,
601                 "flags": Attribute.DesignOnly,
602                 "range" : (1,10000),
603                 "validation_function": validation.is_integer
604             }),
605             
606     "command": dict({
607                 "name": "command",
608                 "help": "Command line string",
609                 "type": Attribute.STRING,
610                 "flags": Attribute.DesignOnly,
611                 "validation_function": validation.is_string
612             }),
613     "sudo": dict({
614                 "name": "sudo",
615                 "help": "Run with root privileges",
616                 "type": Attribute.BOOL,
617                 "flags": Attribute.DesignOnly,
618                 "value": False,
619                 "validation_function": validation.is_bool
620             }),
621     "stdin": dict({
622                 "name": "stdin",
623                 "help": "Standard input",
624                 "type": Attribute.STRING,
625                 "flags": Attribute.DesignOnly,
626                 "validation_function": validation.is_string
627             }),
628             
629     "depends": dict({
630                 "name": "depends",
631                 "help": "Space-separated list of packages required to run the application",
632                 "type": Attribute.STRING,
633                 "flags": Attribute.DesignOnly,
634                 "validation_function": validation.is_string
635             }),
636     "build-depends": dict({
637                 "name": "buildDepends",
638                 "help": "Space-separated list of packages required to build the application",
639                 "type": Attribute.STRING,
640                 "flags": Attribute.DesignOnly,
641                 "validation_function": validation.is_string
642             }),
643     "sources": dict({
644                 "name": "sources",
645                 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
646                         "Archives won't be expanded automatically.",
647                 "type": Attribute.STRING,
648                 "flags": Attribute.DesignOnly,
649                 "validation_function": validation.is_string
650             }),
651     "build": dict({
652                 "name": "build",
653                 "help": "Build commands to execute after deploying the sources. "
654                         "Sources will be in the ${SOURCES} folder. "
655                         "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
656                         "Try to make the commands return with a nonzero exit code on error.\n"
657                         "Also, do not install any programs here, use the 'install' attribute. This will "
658                         "help keep the built files constrained to the build folder (which may "
659                         "not be the home folder), and will result in faster deployment. Also, "
660                         "make sure to clean up temporary files, to reduce bandwidth usage between "
661                         "nodes when transferring built packages.",
662                 "type": Attribute.STRING,
663                 "flags": Attribute.DesignOnly,
664                 "validation_function": validation.is_string
665             }),
666     "install": dict({
667                 "name": "install",
668                 "help": "Commands to transfer built files to their final destinations. "
669                         "Sources will be in the initial working folder, and a special "
670                         "tag ${SOURCES} can be used to reference the experiment's "
671                         "home folder (where the application commands will run).\n"
672                         "ALL sources and targets needed for execution must be copied there, "
673                         "if building has been enabled.\n"
674                         "That is, 'slave' nodes will not automatically get any source files. "
675                         "'slave' nodes don't get build dependencies either, so if you need "
676                         "make and other tools to install, be sure to provide them as "
677                         "actual dependencies instead.",
678                 "type": Attribute.STRING,
679                 "flags": Attribute.DesignOnly,
680                 "validation_function": validation.is_string
681             }),
682     ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP: dict({
683                 "name": ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
684                 "help": "Commands to set up the environment needed to run NEPI testbeds",
685                 "type": Attribute.STRING,
686                 "flags": Attribute.Invisible | Attribute.ReadOnly,
687                 "validation_function": validation.is_string
688             }),
689     
690     "netpipe_mode": dict({      
691                 "name": "mode",
692                 "help": "Link mode:\n"
693                         " * SERVER: applies to incoming connections\n"
694                         " * CLIENT: applies to outgoing connections\n"
695                         " * SERVICE: applies to both",
696                 "type": Attribute.ENUM, 
697                 "flags": Attribute.DesignOnly,
698                 "allowed": ["SERVER",
699                             "CLIENT",
700                             "SERVICE"],
701                 "validation_function": validation.is_enum,
702             }),
703     "port_list":  dict({
704                 "name": "portList", 
705                 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
706                 "type": Attribute.STRING,
707                 "validation_function": is_portlist,
708             }),
709     "addr_list":  dict({
710                 "name": "addrList", 
711                 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
712                 "type": Attribute.STRING,
713                 "validation_function": is_addrlist,
714             }),
715     "bw_in":  dict({
716                 "name": "bwIn", 
717                 "help": "Inbound bandwidth limit (in Mbit/s)",
718                 "type": Attribute.DOUBLE,
719                 "validation_function": validation.is_double,
720             }),
721     "bw_out":  dict({
722                 "name": "bwOut", 
723                 "help": "Outbound bandwidth limit (in Mbit/s)",
724                 "type": Attribute.DOUBLE,
725                 "validation_function": validation.is_double,
726             }),
727     "plr_in":  dict({
728                 "name": "plrIn", 
729                 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
730                 "type": Attribute.DOUBLE,
731                 "validation_function": validation.is_double,
732             }),
733     "plr_out":  dict({
734                 "name": "plrOut", 
735                 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
736                 "type": Attribute.DOUBLE,
737                 "validation_function": validation.is_double,
738             }),
739     "delay_in":  dict({
740                 "name": "delayIn", 
741                 "help": "Inbound packet delay (in milliseconds)",
742                 "type": Attribute.INTEGER,
743                 "range": (0,60000),
744                 "validation_function": validation.is_integer,
745             }),
746     "delay_out":  dict({
747                 "name": "delayOut", 
748                 "help": "Outbound packet delay (in milliseconds)",
749                 "type": Attribute.INTEGER,
750                 "range": (0,60000),
751                 "validation_function": validation.is_integer,
752             }),
753     })
754
755 traces = dict({
756     "stdout": dict({
757                 "name": "stdout",
758                 "help": "Standard output stream"
759               }),
760     "stderr": dict({
761                 "name": "stderr",
762                 "help": "Application standard error",
763               }),
764     "buildlog": dict({
765                 "name": "buildlog",
766                 "help": "Output of the build process",
767               }), 
768     
769     "netpipe_stats": dict({
770                 "name": "netpipeStats",
771                 "help": "Information about rule match counters, packets dropped, etc.",
772               }),
773
774     "packets": dict({
775                 "name": "packets",
776                 "help": "Detailled log of all packets going through the interface",
777               }),
778     })
779
780 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
781
782 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
783
784 factories_info = dict({
785     NODE: dict({
786             "allow_routes": False,
787             "help": "Virtualized Node (V-Server style)",
788             "category": "topology",
789             "create_function": create_node,
790             "preconfigure_function": configure_node,
791             "box_attributes": [
792                 "forward_X11",
793                 "hostname",
794                 "architecture",
795                 "operating_system",
796                 "site",
797                 "emulation",
798                 "min_reliability",
799                 "max_reliability",
800                 "min_bandwidth",
801                 "max_bandwidth",
802                 
803                 # NEPI-in-NEPI attributes
804                 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
805             ],
806             "connector_types": ["devs", "apps", "pipes", "deps"]
807        }),
808     NODEIFACE: dict({
809             "has_addresses": True,
810             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
811             "category": "devices",
812             "create_function": create_nodeiface,
813             "preconfigure_function": configure_nodeiface,
814             "box_attributes": [ ],
815             "connector_types": ["node", "inet"]
816         }),
817     TUNIFACE: dict({
818             "allow_addresses": True,
819             "help": "Virtual TUN network interface",
820             "category": "devices",
821             "create_function": create_tuniface,
822             "preconfigure_function": preconfigure_tuniface,
823             "configure_function": postconfigure_tuniface,
824             "box_attributes": [
825                 "up", "device_name", "mtu", "snat",
826                 "txqueuelen"
827             ],
828             "traces": ["packets"],
829             "connector_types": ["node","udp","tcp"]
830         }),
831     APPLICATION: dict({
832             "help": "Generic executable command line application",
833             "category": "applications",
834             "create_function": create_application,
835             "start_function": start_application,
836             "status_function": status_application,
837             "stop_function": stop_application,
838             "configure_function": configure_application,
839             "box_attributes": ["command", "sudo", "stdin",
840                                "depends", "build-depends", "build", "install",
841                                "sources" ],
842             "connector_types": ["node"],
843             "traces": ["stdout", "stderr", "buildlog"]
844         }),
845     DEPENDENCY: dict({
846             "help": "Requirement for package or application to be installed on some node",
847             "category": "applications",
848             "create_function": create_dependency,
849             "configure_function": configure_dependency,
850             "box_attributes": ["depends", "build-depends", "build", "install",
851                                "sources" ],
852             "connector_types": ["node"],
853             "traces": ["buildlog"]
854         }),
855     NEPIDEPENDENCY: dict({
856             "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
857             "category": "applications",
858             "create_function": create_nepi_dependency,
859             "configure_function": configure_dependency,
860             "box_attributes": [ ],
861             "connector_types": ["node"],
862             "traces": ["buildlog"]
863         }),
864     NS3DEPENDENCY: dict({
865             "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
866             "category": "applications",
867             "create_function": create_ns3_dependency,
868             "configure_function": configure_dependency,
869             "box_attributes": [ ],
870             "connector_types": ["node"],
871             "traces": ["buildlog"]
872         }),
873     INTERNET: dict({
874             "help": "Internet routing",
875             "category": "topology",
876             "create_function": create_internet,
877             "connector_types": ["devs"],
878         }),
879     NETPIPE: dict({
880             "help": "Link emulation",
881             "category": "topology",
882             "create_function": create_netpipe,
883             "configure_function": configure_netpipe,
884             "box_attributes": ["netpipe_mode",
885                                "addr_list", "port_list",
886                                "bw_in","plr_in","delay_in",
887                                "bw_out","plr_out","delay_out"],
888             "connector_types": ["node"],
889             "traces": ["netpipe_stats"]
890         }),
891 })
892
893 testbed_attributes = dict({
894         "slice": dict({
895             "name": "slice",
896             "help": "The name of the PlanetLab slice to use",
897             "type": Attribute.STRING,
898             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
899             "validation_function": validation.is_string
900         }),
901         "auth_user": dict({
902             "name": "authUser",
903             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
904             "type": Attribute.STRING,
905             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
906             "validation_function": validation.is_string
907         }),
908         "auth_pass": dict({
909             "name": "authPass",
910             "help": "The PlanetLab user's password.",
911             "type": Attribute.STRING,
912             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
913             "validation_function": validation.is_string
914         }),
915         "slice_ssh_key": dict({
916             "name": "sliceSSHKey",
917             "help": "The controller-local path to the slice user's ssh private key. "
918                     "It is the user's responsability to deploy this file where the controller "
919                     "will run, it won't be done automatically because it's sensitive information. "
920                     "It is recommended that a NEPI-specific user be created for this purpose and "
921                     "this purpose alone.",
922             "type": Attribute.STRING,
923             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
924             "validation_function": validation.is_string
925         }),
926     })
927
928 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
929     @property
930     def connector_types(self):
931         return connector_types
932
933     @property
934     def connections(self):
935         return connections
936
937     @property
938     def attributes(self):
939         return attributes
940
941     @property
942     def traces(self):
943         return traces
944
945     @property
946     def create_order(self):
947         return create_order
948
949     @property
950     def configure_order(self):
951         return configure_order
952
953     @property
954     def factories_info(self):
955         return factories_info
956
957     @property
958     def testbed_attributes(self):
959         return testbed_attributes
960