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