Lots of cross-connection fixes, TUN synchronization, etc
[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") or proto
114     iface.peer_port = peer_iface_data.get("tun_port")
115     iface.tun_key = min(iface.tun_key, peer_iface_data.get("tun_key"))
116     iface.tun_proto = proto
117     
118     preconfigure_tuniface(testbed_instance, iface_guid)
119
120 def crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data):
121     # refresh (refreshable) attributes for second-phase
122     iface = testbed_instance._elements[iface_guid]
123     iface.peer_addr = peer_iface_data.get("tun_addr")
124     iface.peer_proto = peer_iface_data.get("tun_proto") or proto
125     iface.peer_port = peer_iface_data.get("tun_port")
126     
127     postconfigure_tuniface(testbed_instance, iface_guid)
128
129 def crossconnect_tun_iface_peer_both(proto, testbed_instance, iface_guid, peer_iface_data):
130     crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data)
131     crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data)
132
133 def connect_dep(testbed_instance, node_guid, app_guid):
134     node = testbed_instance._elements[node_guid]
135     app = testbed_instance._elements[app_guid]
136     app.node = node
137     
138     if app.depends:
139         node.required_packages.update(set(
140             app.depends.split() ))
141     
142     if app.add_to_path:
143         if app.home_path and app.home_path not in node.pythonpath:
144             node.pythonpath.append(app.home_path)
145     
146     if app.env:
147         for envkey, envval in app.env.iteritems():
148             envval = app._replace_paths(envval)
149             node.env[envkey].append(envval)
150
151 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 and not (element.address or element.netmask or element.netprefix):
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 def wait_tuniface(testbed_instance, guid):
340     element = testbed_instance._elements[guid]
341     
342     # Second-phase setup
343     element.async_launch_wait()
344     
345
346 def configure_node(testbed_instance, guid):
347     node = testbed_instance._elements[guid]
348     
349     # Just inject configuration stuff
350     node.home_path = "nepi-node-%s" % (guid,)
351     node.ident_path = testbed_instance.sliceSSHKey
352     node.slicename = testbed_instance.slicename
353     
354     # Do some validations
355     node.validate()
356     
357     # recently provisioned nodes may not be up yet
358     sleeptime = 1.0
359     while not node.is_alive():
360         time.sleep(sleeptime)
361         sleeptime = min(30.0, sleeptime*1.5)
362     
363     # this will be done in parallel in all nodes
364     # this call only spawns the process
365     node.install_dependencies()
366
367 def configure_application(testbed_instance, guid):
368     app = testbed_instance._elements[guid]
369     
370     # Do some validations
371     app.validate()
372     
373     # Wait for dependencies
374     app.node.wait_dependencies()
375     
376     # Install stuff
377     app.setup()
378
379 def configure_dependency(testbed_instance, guid):
380     dep = testbed_instance._elements[guid]
381     
382     # Do some validations
383     dep.validate()
384     
385     # Wait for dependencies
386     dep.node.wait_dependencies()
387     
388     # Install stuff
389     dep.setup()
390
391 def configure_netpipe(testbed_instance, guid):
392     netpipe = testbed_instance._elements[guid]
393     
394     # Do some validations
395     netpipe.validate()
396     
397     # Wait for dependencies
398     netpipe.node.wait_dependencies()
399     
400     # Install rules
401     netpipe.configure()
402
403 ### Factory information ###
404
405 connector_types = dict({
406     "apps": dict({
407                 "help": "Connector from node to applications", 
408                 "name": "apps",
409                 "max": -1, 
410                 "min": 0
411             }),
412     "devs": dict({
413                 "help": "Connector from node to network interfaces", 
414                 "name": "devs",
415                 "max": -1, 
416                 "min": 0
417             }),
418     "deps": dict({
419                 "help": "Connector from node to application dependencies "
420                         "(packages and applications that need to be installed)", 
421                 "name": "deps",
422                 "max": -1, 
423                 "min": 0
424             }),
425     "inet": dict({
426                 "help": "Connector from network interfaces to the internet", 
427                 "name": "inet",
428                 "max": 1, 
429                 "min": 1
430             }),
431     "node": dict({
432                 "help": "Connector to a Node", 
433                 "name": "node",
434                 "max": 1, 
435                 "min": 1
436             }),
437     "pipes": dict({
438                 "help": "Connector to a NetPipe", 
439                 "name": "pipes",
440                 "max": 2, 
441                 "min": 0
442             }),
443     
444     "tcp": dict({
445                 "help": "ip-ip tunneling over TCP link", 
446                 "name": "tcp",
447                 "max": 1, 
448                 "min": 0
449             }),
450     "udp": dict({
451                 "help": "ip-ip tunneling over UDP datagrams", 
452                 "name": "udp",
453                 "max": 1, 
454                 "min": 0
455             }),
456     "fd->": dict({
457                 "help": "TUN device file descriptor provider", 
458                 "name": "fd->",
459                 "max": 1, 
460                 "min": 0
461             }),
462    })
463
464 connections = [
465     dict({
466         "from": (TESTBED_ID, NODE, "devs"),
467         "to":   (TESTBED_ID, NODEIFACE, "node"),
468         "init_code": connect_node_iface_node,
469         "can_cross": False
470     }),
471     dict({
472         "from": (TESTBED_ID, NODE, "devs"),
473         "to":   (TESTBED_ID, TUNIFACE, "node"),
474         "init_code": connect_tun_iface_node,
475         "can_cross": False
476     }),
477     dict({
478         "from": (TESTBED_ID, NODE, "devs"),
479         "to":   (TESTBED_ID, TAPIFACE, "node"),
480         "init_code": connect_tun_iface_node,
481         "can_cross": False
482     }),
483     dict({
484         "from": (TESTBED_ID, NODEIFACE, "inet"),
485         "to":   (TESTBED_ID, INTERNET, "devs"),
486         "init_code": connect_node_iface_inet,
487         "can_cross": False
488     }),
489     dict({
490         "from": (TESTBED_ID, NODE, "apps"),
491         "to":   (TESTBED_ID, APPLICATION, "node"),
492         "init_code": connect_dep,
493         "can_cross": False
494     }),
495     dict({
496         "from": (TESTBED_ID, NODE, "deps"),
497         "to":   (TESTBED_ID, DEPENDENCY, "node"),
498         "init_code": connect_dep,
499         "can_cross": False
500     }),
501     dict({
502         "from": (TESTBED_ID, NODE, "deps"),
503         "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
504         "init_code": connect_dep,
505         "can_cross": False
506     }),
507     dict({
508         "from": (TESTBED_ID, NODE, "deps"),
509         "to":   (TESTBED_ID, NS3DEPENDENCY, "node"),
510         "init_code": connect_dep,
511         "can_cross": False
512     }),
513     dict({
514         "from": (TESTBED_ID, NODE, "pipes"),
515         "to":   (TESTBED_ID, NETPIPE, "node"),
516         "init_code": connect_node_netpipe,
517         "can_cross": False
518     }),
519     dict({
520         "from": (TESTBED_ID, TUNIFACE, "tcp"),
521         "to":   (TESTBED_ID, TUNIFACE, "tcp"),
522         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
523         "can_cross": False
524     }),
525     dict({
526         "from": (TESTBED_ID, TUNIFACE, "udp"),
527         "to":   (TESTBED_ID, TUNIFACE, "udp"),
528         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
529         "can_cross": False
530     }),
531     dict({
532         "from": (TESTBED_ID, TAPIFACE, "tcp"),
533         "to":   (TESTBED_ID, TAPIFACE, "tcp"),
534         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
535         "can_cross": False
536     }),
537     dict({
538         "from": (TESTBED_ID, TAPIFACE, "udp"),
539         "to":   (TESTBED_ID, TAPIFACE, "udp"),
540         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
541         "can_cross": False
542     }),
543     dict({
544         "from": (TESTBED_ID, TUNIFACE, "tcp"),
545         "to":   (None, None, "tcp"),
546         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
547         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
548         "can_cross": True
549     }),
550     dict({
551         "from": (TESTBED_ID, TUNIFACE, "udp"),
552         "to":   (None, None, "udp"),
553         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
554         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
555         "can_cross": True
556     }),
557     dict({
558         "from": (TESTBED_ID, TUNIFACE, "fd->"),
559         "to":   (None, None, "->fd"),
560         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
561         "can_cross": True
562     }),
563     dict({
564         "from": (TESTBED_ID, TAPIFACE, "tcp"),
565         "to":   (None, None, "tcp"),
566         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
567         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
568         "can_cross": True
569     }),
570     dict({
571         "from": (TESTBED_ID, TAPIFACE, "udp"),
572         "to":   (None, None, "udp"),
573         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
574         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
575         "can_cross": True
576     }),
577     dict({
578         "from": (TESTBED_ID, TAPIFACE, "fd->"),
579         "to":   (None, None, "->fd"),
580         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
581         "can_cross": True
582     }),
583 ]
584
585 attributes = dict({
586     "forward_X11": dict({      
587                 "name": "forward_X11",
588                 "help": "Forward x11 from main namespace to the node",
589                 "type": Attribute.BOOL, 
590                 "value": False,
591                 "flags": Attribute.DesignOnly,
592                 "validation_function": validation.is_bool,
593             }),
594     "hostname": dict({      
595                 "name": "hostname",
596                 "help": "Constrain hostname during resource discovery. May use wildcards.",
597                 "type": Attribute.STRING, 
598                 "flags": Attribute.DesignOnly,
599                 "validation_function": validation.is_string,
600             }),
601     "architecture": dict({      
602                 "name": "architecture",
603                 "help": "Constrain architexture during resource discovery.",
604                 "type": Attribute.ENUM, 
605                 "flags": Attribute.DesignOnly,
606                 "allowed": ["x86_64",
607                             "i386"],
608                 "validation_function": validation.is_enum,
609             }),
610     "operating_system": dict({      
611                 "name": "operatingSystem",
612                 "help": "Constrain operating system during resource discovery.",
613                 "type": Attribute.ENUM, 
614                 "flags": Attribute.DesignOnly,
615                 "allowed": ["f8",
616                             "f12",
617                             "f14",
618                             "centos",
619                             "other"],
620                 "validation_function": validation.is_enum,
621             }),
622     "site": dict({      
623                 "name": "site",
624                 "help": "Constrain the PlanetLab site this node should reside on.",
625                 "type": Attribute.ENUM, 
626                 "flags": Attribute.DesignOnly,
627                 "allowed": ["PLE",
628                             "PLC",
629                             "PLJ"],
630                 "validation_function": validation.is_enum,
631             }),
632     "emulation": dict({      
633                 "name": "emulation",
634                 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
635                 "type": Attribute.BOOL,
636                 "value": False, 
637                 "flags": Attribute.DesignOnly,
638                 "validation_function": validation.is_bool,
639             }),
640     "min_reliability": dict({
641                 "name": "minReliability",
642                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
643                 "type": Attribute.DOUBLE,
644                 "range": (0,100),
645                 "flags": Attribute.DesignOnly,
646                 "validation_function": validation.is_double,
647             }),
648     "max_reliability": dict({
649                 "name": "maxReliability",
650                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
651                 "type": Attribute.DOUBLE,
652                 "range": (0,100),
653                 "flags": Attribute.DesignOnly,
654                 "validation_function": validation.is_double,
655             }),
656     "min_bandwidth": dict({
657                 "name": "minBandwidth",
658                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
659                 "type": Attribute.DOUBLE,
660                 "range": (0,2**31),
661                 "flags": Attribute.DesignOnly,
662                 "validation_function": validation.is_double,
663             }),
664     "max_bandwidth": dict({
665                 "name": "maxBandwidth",
666                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
667                 "type": Attribute.DOUBLE,
668                 "range": (0,2**31),
669                 "flags": Attribute.DesignOnly,
670                 "validation_function": validation.is_double,
671             }),
672             
673     "up": dict({
674                 "name": "up",
675                 "help": "Link up",
676                 "type": Attribute.BOOL,
677                 "value": False,
678                 "validation_function": validation.is_bool
679             }),
680     "primary": dict({
681                 "name": "primary",
682                 "help": "This is the primary interface for the attached node",
683                 "type": Attribute.BOOL,
684                 "value": True,
685                 "validation_function": validation.is_bool
686             }),
687     "device_name": dict({
688                 "name": "name",
689                 "help": "Device name",
690                 "type": Attribute.STRING,
691                 "flags": Attribute.DesignOnly,
692                 "validation_function": validation.is_string
693             }),
694     "mtu":  dict({
695                 "name": "mtu", 
696                 "help": "Maximum transmition unit for device",
697                 "type": Attribute.INTEGER,
698                 "range": (0,1500),
699                 "validation_function": validation.is_integer_range(0,1500)
700             }),
701     "mask":  dict({
702                 "name": "mask", 
703                 "help": "Network mask for the device (eg: 24 for /24 network)",
704                 "type": Attribute.INTEGER,
705                 "validation_function": validation.is_integer_range(8,24)
706             }),
707     "snat":  dict({
708                 "name": "snat", 
709                 "help": "Enable SNAT (source NAT to the internet) no this device",
710                 "type": Attribute.BOOL,
711                 "value": False,
712                 "validation_function": validation.is_bool
713             }),
714     "txqueuelen":  dict({
715                 "name": "mask", 
716                 "help": "Transmission queue length (in packets)",
717                 "type": Attribute.INTEGER,
718                 "flags": Attribute.DesignOnly,
719                 "range" : (1,10000),
720                 "validation_function": validation.is_integer
721             }),
722             
723     "command": dict({
724                 "name": "command",
725                 "help": "Command line string",
726                 "type": Attribute.STRING,
727                 "flags": Attribute.DesignOnly,
728                 "validation_function": validation.is_string
729             }),
730     "sudo": dict({
731                 "name": "sudo",
732                 "help": "Run with root privileges",
733                 "type": Attribute.BOOL,
734                 "flags": Attribute.DesignOnly,
735                 "value": False,
736                 "validation_function": validation.is_bool
737             }),
738     "stdin": dict({
739                 "name": "stdin",
740                 "help": "Standard input",
741                 "type": Attribute.STRING,
742                 "flags": Attribute.DesignOnly,
743                 "validation_function": validation.is_string
744             }),
745             
746     "depends": dict({
747                 "name": "depends",
748                 "help": "Space-separated list of packages required to run the application",
749                 "type": Attribute.STRING,
750                 "flags": Attribute.DesignOnly,
751                 "validation_function": validation.is_string
752             }),
753     "build-depends": dict({
754                 "name": "buildDepends",
755                 "help": "Space-separated list of packages required to build the application",
756                 "type": Attribute.STRING,
757                 "flags": Attribute.DesignOnly,
758                 "validation_function": validation.is_string
759             }),
760     "sources": dict({
761                 "name": "sources",
762                 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
763                         "Archives won't be expanded automatically.",
764                 "type": Attribute.STRING,
765                 "flags": Attribute.DesignOnly,
766                 "validation_function": validation.is_string
767             }),
768     "build": dict({
769                 "name": "build",
770                 "help": "Build commands to execute after deploying the sources. "
771                         "Sources will be in the ${SOURCES} folder. "
772                         "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
773                         "Try to make the commands return with a nonzero exit code on error.\n"
774                         "Also, do not install any programs here, use the 'install' attribute. This will "
775                         "help keep the built files constrained to the build folder (which may "
776                         "not be the home folder), and will result in faster deployment. Also, "
777                         "make sure to clean up temporary files, to reduce bandwidth usage between "
778                         "nodes when transferring built packages.",
779                 "type": Attribute.STRING,
780                 "flags": Attribute.DesignOnly,
781                 "validation_function": validation.is_string
782             }),
783     "install": dict({
784                 "name": "install",
785                 "help": "Commands to transfer built files to their final destinations. "
786                         "Sources will be in the initial working folder, and a special "
787                         "tag ${SOURCES} can be used to reference the experiment's "
788                         "home folder (where the application commands will run).\n"
789                         "ALL sources and targets needed for execution must be copied there, "
790                         "if building has been enabled.\n"
791                         "That is, 'slave' nodes will not automatically get any source files. "
792                         "'slave' nodes don't get build dependencies either, so if you need "
793                         "make and other tools to install, be sure to provide them as "
794                         "actual dependencies instead.",
795                 "type": Attribute.STRING,
796                 "flags": Attribute.DesignOnly,
797                 "validation_function": validation.is_string
798             }),
799     
800     "netpipe_mode": dict({      
801                 "name": "mode",
802                 "help": "Link mode:\n"
803                         " * SERVER: applies to incoming connections\n"
804                         " * CLIENT: applies to outgoing connections\n"
805                         " * SERVICE: applies to both",
806                 "type": Attribute.ENUM, 
807                 "flags": Attribute.DesignOnly,
808                 "allowed": ["SERVER",
809                             "CLIENT",
810                             "SERVICE"],
811                 "validation_function": validation.is_enum,
812             }),
813     "port_list":  dict({
814                 "name": "portList", 
815                 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
816                 "type": Attribute.STRING,
817                 "validation_function": is_portlist,
818             }),
819     "addr_list":  dict({
820                 "name": "addrList", 
821                 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
822                 "type": Attribute.STRING,
823                 "validation_function": is_addrlist,
824             }),
825     "bw_in":  dict({
826                 "name": "bwIn", 
827                 "help": "Inbound bandwidth limit (in Mbit/s)",
828                 "type": Attribute.DOUBLE,
829                 "validation_function": validation.is_double,
830             }),
831     "bw_out":  dict({
832                 "name": "bwOut", 
833                 "help": "Outbound bandwidth limit (in Mbit/s)",
834                 "type": Attribute.DOUBLE,
835                 "validation_function": validation.is_double,
836             }),
837     "plr_in":  dict({
838                 "name": "plrIn", 
839                 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
840                 "type": Attribute.DOUBLE,
841                 "validation_function": validation.is_double,
842             }),
843     "plr_out":  dict({
844                 "name": "plrOut", 
845                 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
846                 "type": Attribute.DOUBLE,
847                 "validation_function": validation.is_double,
848             }),
849     "delay_in":  dict({
850                 "name": "delayIn", 
851                 "help": "Inbound packet delay (in milliseconds)",
852                 "type": Attribute.INTEGER,
853                 "range": (0,60000),
854                 "validation_function": validation.is_integer,
855             }),
856     "delay_out":  dict({
857                 "name": "delayOut", 
858                 "help": "Outbound packet delay (in milliseconds)",
859                 "type": Attribute.INTEGER,
860                 "range": (0,60000),
861                 "validation_function": validation.is_integer,
862             }),
863     })
864
865 traces = dict({
866     "stdout": dict({
867                 "name": "stdout",
868                 "help": "Standard output stream"
869               }),
870     "stderr": dict({
871                 "name": "stderr",
872                 "help": "Application standard error",
873               }),
874     "buildlog": dict({
875                 "name": "buildlog",
876                 "help": "Output of the build process",
877               }), 
878     
879     "netpipe_stats": dict({
880                 "name": "netpipeStats",
881                 "help": "Information about rule match counters, packets dropped, etc.",
882               }),
883
884     "packets": dict({
885                 "name": "packets",
886                 "help": "Detailled log of all packets going through the interface",
887               }),
888     })
889
890 create_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
891
892 configure_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
893
894 factories_info = dict({
895     NODE: dict({
896             "allow_routes": False,
897             "help": "Virtualized Node (V-Server style)",
898             "category": "topology",
899             "create_function": create_node,
900             "preconfigure_function": configure_node,
901             "box_attributes": [
902                 "forward_X11",
903                 "hostname",
904                 "architecture",
905                 "operating_system",
906                 "site",
907                 "emulation",
908                 "min_reliability",
909                 "max_reliability",
910                 "min_bandwidth",
911                 "max_bandwidth",
912                 
913                 # NEPI-in-NEPI attributes
914                 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
915             ],
916             "connector_types": ["devs", "apps", "pipes", "deps"]
917        }),
918     NODEIFACE: dict({
919             "has_addresses": True,
920             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
921             "category": "devices",
922             "create_function": create_nodeiface,
923             "preconfigure_function": configure_nodeiface,
924             "box_attributes": [ ],
925             "connector_types": ["node", "inet"]
926         }),
927     TUNIFACE: dict({
928             "allow_addresses": True,
929             "help": "Virtual TUN network interface (layer 3)",
930             "category": "devices",
931             "create_function": create_tuniface,
932             "preconfigure_function": preconfigure_tuniface,
933             "configure_function": postconfigure_tuniface,
934             "start_function": wait_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             "start_function": wait_tuniface,
951             "box_attributes": [
952                 "up", "device_name", "mtu", "snat",
953                 "txqueuelen",
954                 "tun_proto", "tun_addr", "tun_port"
955             ],
956             "traces": ["packets"],
957             "connector_types": ["node","udp","tcp","fd->"]
958         }),
959     APPLICATION: dict({
960             "help": "Generic executable command line application",
961             "category": "applications",
962             "create_function": create_application,
963             "start_function": start_application,
964             "status_function": status_application,
965             "stop_function": stop_application,
966             "configure_function": configure_application,
967             "box_attributes": ["command", "sudo", "stdin",
968                                "depends", "build-depends", "build", "install",
969                                "sources" ],
970             "connector_types": ["node"],
971             "traces": ["stdout", "stderr", "buildlog"]
972         }),
973     DEPENDENCY: dict({
974             "help": "Requirement for package or application to be installed on some node",
975             "category": "applications",
976             "create_function": create_dependency,
977             "preconfigure_function": configure_dependency,
978             "box_attributes": ["depends", "build-depends", "build", "install",
979                                "sources" ],
980             "connector_types": ["node"],
981             "traces": ["buildlog"]
982         }),
983     NEPIDEPENDENCY: dict({
984             "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
985             "category": "applications",
986             "create_function": create_nepi_dependency,
987             "preconfigure_function": configure_dependency,
988             "box_attributes": [ ],
989             "connector_types": ["node"],
990             "traces": ["buildlog"]
991         }),
992     NS3DEPENDENCY: dict({
993             "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
994             "category": "applications",
995             "create_function": create_ns3_dependency,
996             "preconfigure_function": configure_dependency,
997             "box_attributes": [ ],
998             "connector_types": ["node"],
999             "traces": ["buildlog"]
1000         }),
1001     INTERNET: dict({
1002             "help": "Internet routing",
1003             "category": "topology",
1004             "create_function": create_internet,
1005             "connector_types": ["devs"],
1006         }),
1007     NETPIPE: dict({
1008             "help": "Link emulation",
1009             "category": "topology",
1010             "create_function": create_netpipe,
1011             "configure_function": configure_netpipe,
1012             "box_attributes": ["netpipe_mode",
1013                                "addr_list", "port_list",
1014                                "bw_in","plr_in","delay_in",
1015                                "bw_out","plr_out","delay_out"],
1016             "connector_types": ["node"],
1017             "traces": ["netpipe_stats"]
1018         }),
1019 })
1020
1021 testbed_attributes = dict({
1022         "slice": dict({
1023             "name": "slice",
1024             "help": "The name of the PlanetLab slice to use",
1025             "type": Attribute.STRING,
1026             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1027             "validation_function": validation.is_string
1028         }),
1029         "auth_user": dict({
1030             "name": "authUser",
1031             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
1032             "type": Attribute.STRING,
1033             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1034             "validation_function": validation.is_string
1035         }),
1036         "auth_pass": dict({
1037             "name": "authPass",
1038             "help": "The PlanetLab user's password.",
1039             "type": Attribute.STRING,
1040             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1041             "validation_function": validation.is_string
1042         }),
1043         "plc_host": dict({
1044             "name": "plcHost",
1045             "help": "The PlanetLab PLC API host",
1046             "type": Attribute.STRING,
1047             "value": "www.planet-lab.eu",
1048             "flags": Attribute.DesignOnly,
1049             "validation_function": validation.is_string
1050         }),
1051         "plc_url": dict({
1052             "name": "plcUrl",
1053             "help": "The PlanetLab PLC API url pattern - %(hostname)s is replaced by plcHost.",
1054             "type": Attribute.STRING,
1055             "value": "https://%(hostname)s:443/PLCAPI/",
1056             "flags": Attribute.DesignOnly,
1057             "validation_function": validation.is_string
1058         }),
1059         "slice_ssh_key": dict({
1060             "name": "sliceSSHKey",
1061             "help": "The controller-local path to the slice user's ssh private key. "
1062                     "It is the user's responsability to deploy this file where the controller "
1063                     "will run, it won't be done automatically because it's sensitive information. "
1064                     "It is recommended that a NEPI-specific user be created for this purpose and "
1065                     "this purpose alone.",
1066             "type": Attribute.STRING,
1067             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1068             "validation_function": validation.is_string
1069         }),
1070     })
1071
1072 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
1073     @property
1074     def connector_types(self):
1075         return connector_types
1076
1077     @property
1078     def connections(self):
1079         return connections
1080
1081     @property
1082     def attributes(self):
1083         return attributes
1084
1085     @property
1086     def traces(self):
1087         return traces
1088
1089     @property
1090     def create_order(self):
1091         return create_order
1092
1093     @property
1094     def configure_order(self):
1095         return configure_order
1096
1097     @property
1098     def factories_info(self):
1099         return factories_info
1100
1101     @property
1102     def testbed_attributes(self):
1103         return testbed_attributes
1104