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