TunChannel improvements:
[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     "gre": dict({
495                 "help": "IP or Ethernet tunneling using the GRE protocol", 
496                 "name": "gre",
497                 "max": 1, 
498                 "min": 0
499             }),
500     "fd->": dict({
501                 "help": "TUN device file descriptor provider", 
502                 "name": "fd->",
503                 "max": 1, 
504                 "min": 0
505             }),
506    })
507
508 connections = [
509     dict({
510         "from": (TESTBED_ID, NODE, "devs"),
511         "to":   (TESTBED_ID, NODEIFACE, "node"),
512         "init_code": connect_node_iface_node,
513         "can_cross": False
514     }),
515     dict({
516         "from": (TESTBED_ID, NODE, "devs"),
517         "to":   (TESTBED_ID, TUNIFACE, "node"),
518         "init_code": connect_tun_iface_node,
519         "can_cross": False
520     }),
521     dict({
522         "from": (TESTBED_ID, NODE, "devs"),
523         "to":   (TESTBED_ID, TAPIFACE, "node"),
524         "init_code": connect_tun_iface_node,
525         "can_cross": False
526     }),
527     dict({
528         "from": (TESTBED_ID, NODEIFACE, "inet"),
529         "to":   (TESTBED_ID, INTERNET, "devs"),
530         "init_code": connect_node_iface_inet,
531         "can_cross": False
532     }),
533     dict({
534         "from": (TESTBED_ID, NODE, "apps"),
535         "to":   (TESTBED_ID, APPLICATION, "node"),
536         "init_code": connect_dep,
537         "can_cross": False
538     }),
539     dict({
540         "from": (TESTBED_ID, NODE, "deps"),
541         "to":   (TESTBED_ID, DEPENDENCY, "node"),
542         "init_code": connect_dep,
543         "can_cross": False
544     }),
545     dict({
546         "from": (TESTBED_ID, NODE, "deps"),
547         "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
548         "init_code": connect_dep,
549         "can_cross": False
550     }),
551     dict({
552         "from": (TESTBED_ID, NODE, "deps"),
553         "to":   (TESTBED_ID, NS3DEPENDENCY, "node"),
554         "init_code": connect_dep,
555         "can_cross": False
556     }),
557     dict({
558         "from": (TESTBED_ID, NODE, "pipes"),
559         "to":   (TESTBED_ID, NETPIPE, "node"),
560         "init_code": connect_node_netpipe,
561         "can_cross": False
562     }),
563     dict({
564         "from": (TESTBED_ID, TUNIFACE, "tcp"),
565         "to":   (TESTBED_ID, TUNIFACE, "tcp"),
566         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
567         "can_cross": False
568     }),
569     dict({
570         "from": (TESTBED_ID, TUNIFACE, "udp"),
571         "to":   (TESTBED_ID, TUNIFACE, "udp"),
572         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
573         "can_cross": False
574     }),
575     dict({
576         "from": (TESTBED_ID, TUNIFACE, "gre"),
577         "to":   (TESTBED_ID, TUNIFACE, "gre"),
578         "init_code": functools.partial(connect_tun_iface_peer,"gre"),
579         "can_cross": False
580     }),
581     dict({
582         "from": (TESTBED_ID, TAPIFACE, "tcp"),
583         "to":   (TESTBED_ID, TAPIFACE, "tcp"),
584         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
585         "can_cross": False
586     }),
587     dict({
588         "from": (TESTBED_ID, TAPIFACE, "udp"),
589         "to":   (TESTBED_ID, TAPIFACE, "udp"),
590         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
591         "can_cross": False
592     }),
593     dict({
594         "from": (TESTBED_ID, TAPIFACE, "gre"),
595         "to":   (TESTBED_ID, TAPIFACE, "gre"),
596         "init_code": functools.partial(connect_tun_iface_peer,"gre"),
597         "can_cross": False
598     }),
599     dict({
600         "from": (TESTBED_ID, TUNIFACE, "tcp"),
601         "to":   (None, None, "tcp"),
602         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
603         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
604         "can_cross": True
605     }),
606     dict({
607         "from": (TESTBED_ID, TUNIFACE, "udp"),
608         "to":   (None, None, "udp"),
609         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
610         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
611         "can_cross": True
612     }),
613     dict({
614         "from": (TESTBED_ID, TUNIFACE, "fd->"),
615         "to":   (None, None, "->fd"),
616         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
617         "can_cross": True
618     }),
619     dict({
620         "from": (TESTBED_ID, TUNIFACE, "gre"),
621         "to":   (None, None, "gre"),
622         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"gre"),
623         "can_cross": True
624     }),
625     dict({
626         "from": (TESTBED_ID, TAPIFACE, "tcp"),
627         "to":   (None, None, "tcp"),
628         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
629         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
630         "can_cross": True
631     }),
632     dict({
633         "from": (TESTBED_ID, TAPIFACE, "udp"),
634         "to":   (None, None, "udp"),
635         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
636         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
637         "can_cross": True
638     }),
639     dict({
640         "from": (TESTBED_ID, TAPIFACE, "fd->"),
641         "to":   (None, None, "->fd"),
642         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
643         "can_cross": True
644     }),
645     # EGRE is an extension of PlanetLab, so we can't connect externally
646     # if the other testbed isn't another PlanetLab
647     dict({
648         "from": (TESTBED_ID, TAPIFACE, "gre"),
649         "to":   (TESTBED_ID, None, "gre"),
650         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"gre"),
651         "can_cross": True
652     }),
653 ]
654
655 attributes = dict({
656     "forward_X11": dict({      
657                 "name": "forward_X11",
658                 "help": "Forward x11 from main namespace to the node",
659                 "type": Attribute.BOOL, 
660                 "value": False,
661                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
662                 "validation_function": validation.is_bool,
663             }),
664     "hostname": dict({      
665                 "name": "hostname",
666                 "help": "Constrain hostname during resource discovery. May use wildcards.",
667                 "type": Attribute.STRING, 
668                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
669                 "validation_function": validation.is_string,
670             }),
671     "city": dict({      
672                 "name": "city",
673                 "help": "Constrain location (city) during resource discovery. May use wildcards.",
674                 "type": Attribute.STRING, 
675                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
676                 "validation_function": validation.is_string,
677             }),
678     "country": dict({      
679                 "name": "hostname",
680                 "help": "Constrain location (country) during resource discovery. May use wildcards.",
681                 "type": Attribute.STRING, 
682                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
683                 "validation_function": validation.is_string,
684             }),
685     "region": dict({      
686                 "name": "hostname",
687                 "help": "Constrain location (region) during resource discovery. May use wildcards.",
688                 "type": Attribute.STRING, 
689                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
690                 "validation_function": validation.is_string,
691             }),
692     "architecture": dict({      
693                 "name": "architecture",
694                 "help": "Constrain architexture during resource discovery.",
695                 "type": Attribute.ENUM, 
696                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
697                 "allowed": ["x86_64",
698                             "i386"],
699                 "validation_function": validation.is_enum,
700             }),
701     "operating_system": dict({      
702                 "name": "operatingSystem",
703                 "help": "Constrain operating system during resource discovery.",
704                 "type": Attribute.ENUM, 
705                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
706                 "allowed": ["f8",
707                             "f12",
708                             "f14",
709                             "centos",
710                             "other"],
711                 "validation_function": validation.is_enum,
712             }),
713     "site": dict({      
714                 "name": "site",
715                 "help": "Constrain the PlanetLab site this node should reside on.",
716                 "type": Attribute.ENUM, 
717                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
718                 "allowed": ["PLE",
719                             "PLC",
720                             "PLJ"],
721                 "validation_function": validation.is_enum,
722             }),
723     "min_reliability": dict({
724                 "name": "minReliability",
725                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
726                 "type": Attribute.DOUBLE,
727                 "range": (0,100),
728                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
729                 "validation_function": validation.is_number,
730             }),
731     "max_reliability": dict({
732                 "name": "maxReliability",
733                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
734                 "type": Attribute.DOUBLE,
735                 "range": (0,100),
736                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
737                 "validation_function": validation.is_number,
738             }),
739     "min_bandwidth": dict({
740                 "name": "minBandwidth",
741                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
742                 "type": Attribute.DOUBLE,
743                 "range": (0,2**31),
744                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
745                 "validation_function": validation.is_number,
746             }),
747     "max_bandwidth": dict({
748                 "name": "maxBandwidth",
749                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
750                 "type": Attribute.DOUBLE,
751                 "range": (0,2**31),
752                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
753                 "validation_function": validation.is_number,
754             }),
755     "min_load": dict({
756                 "name": "minLoad",
757                 "help": "Constrain node load average while picking PlanetLab nodes. Specifies a lower acceptable bound.",
758                 "type": Attribute.DOUBLE,
759                 "range": (0,2**31),
760                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
761                 "validation_function": validation.is_number,
762             }),
763     "max_load": dict({
764                 "name": "maxLoad",
765                 "help": "Constrain node load average while picking PlanetLab nodes. Specifies an upper acceptable bound.",
766                 "type": Attribute.DOUBLE,
767                 "range": (0,2**31),
768                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
769                 "validation_function": validation.is_number,
770             }),
771     "min_cpu": dict({
772                 "name": "minCpu",
773                 "help": "Constrain available cpu time while picking PlanetLab nodes. Specifies a lower acceptable bound.",
774                 "type": Attribute.DOUBLE,
775                 "range": (0,100),
776                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
777                 "validation_function": validation.is_number,
778             }),
779     "max_cpu": dict({
780                 "name": "maxCpu",
781                 "help": "Constrain available cpu time while picking PlanetLab nodes. Specifies an upper acceptable bound.",
782                 "type": Attribute.DOUBLE,
783                 "range": (0,100),
784                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
785                 "validation_function": validation.is_number,
786             }),
787             
788     "up": dict({
789                 "name": "up",
790                 "help": "Link up",
791                 "type": Attribute.BOOL,
792                 "value": False,
793                 "validation_function": validation.is_bool
794             }),
795     "primary": dict({
796                 "name": "primary",
797                 "help": "This is the primary interface for the attached node",
798                 "type": Attribute.BOOL,
799                 "value": True,
800                 "validation_function": validation.is_bool
801             }),
802     "device_name": dict({
803                 "name": "name",
804                 "help": "Device name",
805                 "type": Attribute.STRING,
806                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
807                 "validation_function": validation.is_string
808             }),
809     "mtu":  dict({
810                 "name": "mtu", 
811                 "help": "Maximum transmition unit for device",
812                 "type": Attribute.INTEGER,
813                 "range": (0,1500),
814                 "validation_function": validation.is_integer_range(0,1500)
815             }),
816     "mask":  dict({
817                 "name": "mask", 
818                 "help": "Network mask for the device (eg: 24 for /24 network)",
819                 "type": Attribute.INTEGER,
820                 "validation_function": validation.is_integer_range(8,24)
821             }),
822     "snat":  dict({
823                 "name": "snat", 
824                 "help": "Enable SNAT (source NAT to the internet) no this device",
825                 "type": Attribute.BOOL,
826                 "value": False,
827                 "validation_function": validation.is_bool
828             }),
829     "pointopoint":  dict({
830                 "name": "pointopoint", 
831                 "help": "If the interface is a P2P link, the remote endpoint's IP "
832                         "should be set on this attribute.",
833                 "type": Attribute.STRING,
834                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
835                 "validation_function": validation.is_string
836             }),
837     "txqueuelen":  dict({
838                 "name": "txqueuelen", 
839                 "help": "Transmission queue length (in packets)",
840                 "type": Attribute.INTEGER,
841                 "value": 1000,
842                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
843                 "range" : (1,10000),
844                 "validation_function": validation.is_integer
845             }),
846             
847     "command": dict({
848                 "name": "command",
849                 "help": "Command line string",
850                 "type": Attribute.STRING,
851                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
852                 "validation_function": validation.is_string
853             }),
854     "sudo": dict({
855                 "name": "sudo",
856                 "help": "Run with root privileges",
857                 "type": Attribute.BOOL,
858                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
859                 "value": False,
860                 "validation_function": validation.is_bool
861             }),
862     "stdin": dict({
863                 "name": "stdin",
864                 "help": "Standard input",
865                 "type": Attribute.STRING,
866                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
867                 "validation_function": validation.is_string
868             }),
869             
870     "depends": dict({
871                 "name": "depends",
872                 "help": "Space-separated list of packages required to run the application",
873                 "type": Attribute.STRING,
874                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
875                 "validation_function": validation.is_string
876             }),
877     "build-depends": dict({
878                 "name": "buildDepends",
879                 "help": "Space-separated list of packages required to build the application",
880                 "type": Attribute.STRING,
881                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
882                 "validation_function": validation.is_string
883             }),
884     "rpm-fusion": dict({
885                 "name": "rpmFusion",
886                 "help": "True if required packages can be found in the RpmFusion repository",
887                 "type": Attribute.BOOL,
888                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
889                 "value": False,
890                 "validation_function": validation.is_bool
891             }),
892     "sources": dict({
893                 "name": "sources",
894                 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
895                         "Archives won't be expanded automatically.",
896                 "type": Attribute.STRING,
897                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
898                 "validation_function": validation.is_string
899             }),
900     "build": dict({
901                 "name": "build",
902                 "help": "Build commands to execute after deploying the sources. "
903                         "Sources will be in the ${SOURCES} folder. "
904                         "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
905                         "Try to make the commands return with a nonzero exit code on error.\n"
906                         "Also, do not install any programs here, use the 'install' attribute. This will "
907                         "help keep the built files constrained to the build folder (which may "
908                         "not be the home folder), and will result in faster deployment. Also, "
909                         "make sure to clean up temporary files, to reduce bandwidth usage between "
910                         "nodes when transferring built packages.",
911                 "type": Attribute.STRING,
912                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
913                 "validation_function": validation.is_string
914             }),
915     "install": dict({
916                 "name": "install",
917                 "help": "Commands to transfer built files to their final destinations. "
918                         "Sources will be in the initial working folder, and a special "
919                         "tag ${SOURCES} can be used to reference the experiment's "
920                         "home folder (where the application commands will run).\n"
921                         "ALL sources and targets needed for execution must be copied there, "
922                         "if building has been enabled.\n"
923                         "That is, 'slave' nodes will not automatically get any source files. "
924                         "'slave' nodes don't get build dependencies either, so if you need "
925                         "make and other tools to install, be sure to provide them as "
926                         "actual dependencies instead.",
927                 "type": Attribute.STRING,
928                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
929                 "validation_function": validation.is_string
930             }),
931     
932     "netpipe_mode": dict({      
933                 "name": "mode",
934                 "help": "Link mode:\n"
935                         " * SERVER: applies to incoming connections\n"
936                         " * CLIENT: applies to outgoing connections\n"
937                         " * SERVICE: applies to both",
938                 "type": Attribute.ENUM, 
939                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
940                 "allowed": ["SERVER",
941                             "CLIENT",
942                             "SERVICE"],
943                 "validation_function": validation.is_enum,
944             }),
945     "port_list":  dict({
946                 "name": "portList", 
947                 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
948                 "type": Attribute.STRING,
949                 "validation_function": is_portlist,
950             }),
951     "addr_list":  dict({
952                 "name": "addrList", 
953                 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
954                 "type": Attribute.STRING,
955                 "validation_function": is_addrlist,
956             }),
957     "bw_in":  dict({
958                 "name": "bwIn", 
959                 "help": "Inbound bandwidth limit (in Mbit/s)",
960                 "type": Attribute.DOUBLE,
961                 "validation_function": validation.is_number,
962             }),
963     "bw_out":  dict({
964                 "name": "bwOut", 
965                 "help": "Outbound bandwidth limit (in Mbit/s)",
966                 "type": Attribute.DOUBLE,
967                 "validation_function": validation.is_number,
968             }),
969     "plr_in":  dict({
970                 "name": "plrIn", 
971                 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
972                 "type": Attribute.DOUBLE,
973                 "validation_function": validation.is_number,
974             }),
975     "plr_out":  dict({
976                 "name": "plrOut", 
977                 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
978                 "type": Attribute.DOUBLE,
979                 "validation_function": validation.is_number,
980             }),
981     "delay_in":  dict({
982                 "name": "delayIn", 
983                 "help": "Inbound packet delay (in milliseconds)",
984                 "type": Attribute.INTEGER,
985                 "range": (0,60000),
986                 "validation_function": validation.is_integer,
987             }),
988     "delay_out":  dict({
989                 "name": "delayOut", 
990                 "help": "Outbound packet delay (in milliseconds)",
991                 "type": Attribute.INTEGER,
992                 "range": (0,60000),
993                 "validation_function": validation.is_integer,
994             }),
995     })
996
997 traces = dict({
998     "stdout": dict({
999                 "name": "stdout",
1000                 "help": "Standard output stream"
1001               }),
1002     "stderr": dict({
1003                 "name": "stderr",
1004                 "help": "Application standard error",
1005               }),
1006     "buildlog": dict({
1007                 "name": "buildlog",
1008                 "help": "Output of the build process",
1009               }), 
1010     
1011     "netpipe_stats": dict({
1012                 "name": "netpipeStats",
1013                 "help": "Information about rule match counters, packets dropped, etc.",
1014               }),
1015
1016     "packets": dict({
1017                 "name": "packets",
1018                 "help": "Detailled log of all packets going through the interface",
1019               }),
1020     "pcap": dict({
1021                 "name": "pcap",
1022                 "help": "PCAP trace of all packets going through the interface",
1023               }),
1024     })
1025
1026 create_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
1027
1028 configure_order = [ INTERNET, Parallel(NODE), NODEIFACE, Parallel(TAPIFACE), Parallel(TUNIFACE), NETPIPE, Parallel(NEPIDEPENDENCY), Parallel(NS3DEPENDENCY), Parallel(DEPENDENCY), Parallel(APPLICATION) ]
1029
1030 # Start (and prestart) node after ifaces, because the node needs the ifaces in order to set up routes
1031 start_order = [ INTERNET, NODEIFACE, Parallel(TAPIFACE), Parallel(TUNIFACE), Parallel(NODE), NETPIPE, Parallel(NEPIDEPENDENCY), Parallel(NS3DEPENDENCY), Parallel(DEPENDENCY), Parallel(APPLICATION) ]
1032
1033 # cleanup order
1034 shutdown_order = [ Parallel(APPLICATION), Parallel(TAPIFACE), Parallel(TUNIFACE), Parallel(NETPIPE), Parallel(NEPIDEPENDENCY), Parallel(NS3DEPENDENCY), Parallel(DEPENDENCY), NODEIFACE, Parallel(NODE) ]
1035
1036 factories_info = dict({
1037     NODE: dict({
1038             "help": "Virtualized Node (V-Server style)",
1039             "category": FC.CATEGORY_NODES,
1040             "create_function": create_node,
1041             "preconfigure_function": configure_node,
1042             "prestart_function": configure_node_routes,
1043             "box_attributes": [
1044                 "forward_X11",
1045                 "hostname",
1046                 "architecture",
1047                 "operating_system",
1048                 "site",
1049                 "min_reliability",
1050                 "max_reliability",
1051                 "min_bandwidth",
1052                 "max_bandwidth",
1053                 
1054                 # NEPI-in-NEPI attributes
1055                 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
1056             ],
1057             "connector_types": ["devs", "apps", "pipes", "deps"],
1058             "tags": [tags.NODE, tags.ALLOW_ROUTES],
1059        }),
1060     NODEIFACE: dict({
1061             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
1062             "category": FC.CATEGORY_DEVICES,
1063             "create_function": create_nodeiface,
1064             "preconfigure_function": configure_nodeiface,
1065             "box_attributes": [ ],
1066             "connector_types": ["node", "inet"],
1067             "tags": [tags.INTERFACE, tags.HAS_ADDRESSES],
1068         }),
1069     TUNIFACE: dict({
1070             "help": "Virtual TUN network interface (layer 3)",
1071             "category": FC.CATEGORY_DEVICES,
1072             "create_function": create_tuniface,
1073             "preconfigure_function": preconfigure_tuniface,
1074             "configure_function": postconfigure_tuniface,
1075             "prestart_function": wait_tuniface,
1076             "box_attributes": [
1077                 "up", "device_name", "mtu", "snat", "pointopoint",
1078                 "txqueuelen",
1079                 "tun_proto", "tun_addr", "tun_port", "tun_key"
1080             ],
1081             "traces": ["packets", "pcap"],
1082             "connector_types": ["node","udp","tcp","fd->","gre"],
1083             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
1084         }),
1085     TAPIFACE: dict({
1086             "help": "Virtual TAP network interface (layer 2)",
1087             "category": FC.CATEGORY_DEVICES,
1088             "create_function": create_tapiface,
1089             "preconfigure_function": preconfigure_tuniface,
1090             "configure_function": postconfigure_tuniface,
1091             "prestart_function": wait_tuniface,
1092             "box_attributes": [
1093                 "up", "device_name", "mtu", "snat", "pointopoint",
1094                 "txqueuelen",
1095                 "tun_proto", "tun_addr", "tun_port", "tun_key"
1096             ],
1097             "traces": ["packets", "pcap"],
1098             "connector_types": ["node","udp","tcp","fd->","gre"],
1099             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
1100         }),
1101     APPLICATION: dict({
1102             "help": "Generic executable command line application",
1103             "category": FC.CATEGORY_APPLICATIONS,
1104             "create_function": create_application,
1105             "start_function": start_application,
1106             "status_function": status_application,
1107             "stop_function": stop_application,
1108             "configure_function": configure_application,
1109             "box_attributes": ["command", "sudo", "stdin",
1110                                "depends", "build-depends", "build", "install",
1111                                "sources", "rpm-fusion" ],
1112             "connector_types": ["node"],
1113             "traces": ["stdout", "stderr", "buildlog"],
1114             "tags": [tags.APPLICATION],
1115         }),
1116     DEPENDENCY: dict({
1117             "help": "Requirement for package or application to be installed on some node",
1118             "category": FC.CATEGORY_APPLICATIONS,
1119             "create_function": create_dependency,
1120             "preconfigure_function": configure_dependency,
1121             "box_attributes": ["depends", "build-depends", "build", "install",
1122                                "sources", "rpm-fusion" ],
1123             "connector_types": ["node"],
1124             "traces": ["buildlog"],
1125         }),
1126     NEPIDEPENDENCY: dict({
1127             "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
1128             "category": FC.CATEGORY_APPLICATIONS,
1129             "create_function": create_nepi_dependency,
1130             "preconfigure_function": configure_dependency,
1131             "box_attributes": [],
1132             "connector_types": ["node"],
1133             "traces": ["buildlog"],
1134         }),
1135     NS3DEPENDENCY: dict({
1136             "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
1137             "category": FC.CATEGORY_APPLICATIONS,
1138             "create_function": create_ns3_dependency,
1139             "preconfigure_function": configure_dependency,
1140             "box_attributes": [ ],
1141             "connector_types": ["node"],
1142             "traces": ["buildlog"],
1143         }),
1144     INTERNET: dict({
1145             "help": "Internet routing",
1146             "category": FC.CATEGORY_CHANNELS,
1147             "create_function": create_internet,
1148             "connector_types": ["devs"],
1149             "tags": [tags.INTERNET],
1150         }),
1151     NETPIPE: dict({
1152             "help": "Link emulation",
1153             "category": FC.CATEGORY_CHANNELS,
1154             "create_function": create_netpipe,
1155             "configure_function": configure_netpipe,
1156             "box_attributes": ["netpipe_mode",
1157                                "addr_list", "port_list",
1158                                "bw_in","plr_in","delay_in",
1159                                "bw_out","plr_out","delay_out"],
1160             "connector_types": ["node"],
1161             "traces": ["netpipe_stats"],
1162         }),
1163 })
1164
1165 testbed_attributes = dict({
1166         "slice": dict({
1167             "name": "slice",
1168             "help": "The name of the PlanetLab slice to use",
1169             "type": Attribute.STRING,
1170             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable | Attribute.NoDefaultValue,
1171             "validation_function": validation.is_string
1172         }),
1173         "auth_user": dict({
1174             "name": "authUser",
1175             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
1176             "type": Attribute.STRING,
1177             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable | Attribute.NoDefaultValue,
1178             "validation_function": validation.is_string
1179         }),
1180         "auth_pass": dict({
1181             "name": "authPass",
1182             "help": "The PlanetLab user's password.",
1183             "type": Attribute.STRING,
1184             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable | Attribute.NoDefaultValue,
1185             "validation_function": validation.is_string
1186         }),
1187         "plc_host": dict({
1188             "name": "plcHost",
1189             "help": "The PlanetLab PLC API host",
1190             "type": Attribute.STRING,
1191             "value": "www.planet-lab.eu",
1192             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
1193             "validation_function": validation.is_string
1194         }),
1195         "plc_url": dict({
1196             "name": "plcUrl",
1197             "help": "The PlanetLab PLC API url pattern - %(hostname)s is replaced by plcHost.",
1198             "type": Attribute.STRING,
1199             "value": "https://%(hostname)s:443/PLCAPI/",
1200             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
1201             "validation_function": validation.is_string
1202         }),
1203         "p2p_deployment": dict({
1204             "name": "p2pDeployment",
1205             "help": "Enable peer-to-peer deployment of applications and dependencies. "
1206                     "When enabled, dependency packages and applications are "
1207                     "deployed in a P2P fashion, picking a single node to do "
1208                     "the building or repo download, while all the others "
1209                     "cooperatively exchange resulting binaries or rpms. "
1210                     "When deploying to many nodes, this is a far more efficient "
1211                     "use of resources. It does require re-encrypting and distributing "
1212                     "the slice's private key. Though it is implemented in a secure "
1213                     "fashion, if they key's sole purpose is not PlanetLab, then this "
1214                     "feature should be disabled.",
1215             "type": Attribute.BOOL,
1216             "value": True,
1217             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
1218             "validation_function": validation.is_bool
1219         }),
1220         "slice_ssh_key": dict({
1221             "name": "sliceSSHKey",
1222             "help": "The controller-local path to the slice user's ssh private key. "
1223                     "It is the user's responsability to deploy this file where the controller "
1224                     "will run, it won't be done automatically because it's sensitive information. "
1225                     "It is recommended that a NEPI-specific user be created for this purpose and "
1226                     "this purpose alone.",
1227             "type": Attribute.STRING,
1228             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable | Attribute.NoDefaultValue,
1229             "validation_function": validation.is_string
1230         }),
1231         "pl_log_level": dict({      
1232             "name": "plLogLevel",
1233             "help": "Verbosity of logging of planetlab events.",
1234             "value": "ERROR",
1235             "type": Attribute.ENUM, 
1236             "allowed": ["DEBUG",
1237                         "INFO",
1238                         "WARNING",
1239                         "ERROR",
1240                         "CRITICAL"],
1241             "validation_function": validation.is_enum,
1242         }),
1243         "tap_port_base":  dict({
1244             "name": "tapPortBase", 
1245             "help": "Base port to use when connecting TUN/TAPs. Effective port will be BASE + GUID.",
1246             "type": Attribute.INTEGER,
1247             "value": 15000,
1248             "range": (2000,30000),
1249             "validation_function": validation.is_integer_range(2000,30000)
1250         }),
1251         "dedicated_slice": dict({
1252             "name": "dedicatedSlice",
1253             "help": "Set to True if the slice will be dedicated to this experiment. "
1254                     "NEPI will perform node and slice cleanup, making sure slices are "
1255                     "in a clean, repeatable state before running the experiment.",
1256             "type": Attribute.BOOL,
1257             "value": False,
1258             "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
1259             "validation_function": validation.is_bool
1260         }),
1261     })
1262
1263 supported_recovery_policies = [
1264         DC.POLICY_FAIL,
1265         DC.POLICY_RESTART,
1266         DC.POLICY_RECOVER,
1267     ]
1268
1269 class MetadataInfo(metadata.MetadataInfo):
1270     @property
1271     def connector_types(self):
1272         return connector_types
1273
1274     @property
1275     def connections(self):
1276         return connections
1277
1278     @property
1279     def attributes(self):
1280         return attributes
1281
1282     @property
1283     def traces(self):
1284         return traces
1285
1286     @property
1287     def create_order(self):
1288         return create_order
1289
1290     @property
1291     def configure_order(self):
1292         return configure_order
1293
1294     @property
1295     def prestart_order(self):
1296         return start_order
1297
1298     @property
1299     def start_order(self):
1300         return start_order
1301
1302     @property
1303     def factories_info(self):
1304         return factories_info
1305
1306     @property
1307     def testbed_attributes(self):
1308         return testbed_attributes
1309
1310     @property
1311     def testbed_id(self):
1312         return TESTBED_ID
1313
1314     @property
1315     def testbed_version(self):
1316         return TESTBED_VERSION
1317
1318     @property
1319     def supported_recovery_policies(self):
1320         return supported_recovery_policies
1321
1322