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