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