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