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