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