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