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