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