Ticket #28: Refactor Box classes to use mixins, and provide read-only routes/addesses
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import time
5
6 from constants import TESTBED_ID
7 from nepi.core import metadata
8 from nepi.core.attributes import Attribute
9 from nepi.util import validation
10 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
11         STATUS_FINISHED
12
13 NODE = "Node"
14 NODEIFACE = "NodeInterface"
15 TUNIFACE = "TunInterface"
16 APPLICATION = "Application"
17 INTERNET = "Internet"
18 NETPIPE = "NetPipe"
19
20 PL_TESTBED_ID = "planetlab"
21
22
23 ### Custom validation functions ###
24 def is_addrlist(attribute, value):
25     if not validation.is_string(attribute, value):
26         return False
27     
28     if not value:
29         # No empty strings
30         return False
31     
32     components = value.split(',')
33     
34     for component in components:
35         if '/' in component:
36             addr, mask = component.split('/',1)
37         else:
38             addr, mask = component, 32
39         
40         if mask is not None and not (mask and mask.isdigit()):
41             # No empty or nonnumeric masks
42             return False
43         
44         if not validation.is_ip4_address(attribute, value):
45             # Address part must be ipv4
46             return False
47         
48     return True
49
50 def is_portlist(attribute, value):
51     if not validation.is_string(attribute, value):
52         return False
53     
54     if not value:
55         # No empty strings
56         return False
57     
58     components = value.split(',')
59     
60     for component in components:
61         if '-' in component:
62             pfrom, pto = component.split('-',1)
63         else:
64             pfrom = pto = component
65         
66         if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
67             # No empty or nonnumeric ports
68             return False
69         
70     return True
71
72
73 ### Connection functions ####
74
75 def connect_node_iface_node(testbed_instance, node, iface):
76     iface.node = node
77
78 def connect_node_iface_inet(testbed_instance, iface, inet):
79     iface.has_internet = True
80
81 def connect_tun_iface_node(testbed_instance, node, iface):
82     if not node.emulation:
83         raise RuntimeError, "Use of TUN interfaces requires emulation"
84     iface.node = node
85     node.required_vsys.update(('fd_tuntap', 'vif_up'))
86
87 def connect_app(testbed_instance, node, app):
88     app.node = node
89     
90     if app.depends:
91         node.required_packages.update(set(
92             app.depends.split() ))
93
94 def connect_node_netpipe(testbed_instance, node, netpipe):
95     if not node.emulation:
96         raise RuntimeError, "Use of NetPipes requires emulation"
97     netpipe.node = node
98     
99
100 ### Creation functions ###
101
102 def create_node(testbed_instance, guid):
103     parameters = testbed_instance._get_parameters(guid)
104     
105     # create element with basic attributes
106     element = testbed_instance._make_node(parameters)
107     
108     # add constraint on number of (real) interfaces
109     # by counting connected devices
110     dev_guids = testbed_instance.get_connected(guid, "node", "devs")
111     num_open_ifaces = sum( # count True values
112         TUNEIFACE == testbed_instance._get_factory_id(guid)
113         for guid in dev_guids )
114     element.min_num_external_ifaces = num_open_ifaces
115     
116     testbed_instance.elements[guid] = element
117
118 def create_nodeiface(testbed_instance, guid):
119     parameters = testbed_instance._get_parameters(guid)
120     element = testbed_instance._make_node_iface(parameters)
121     testbed_instance.elements[guid] = element
122
123 def create_tuniface(testbed_instance, guid):
124     parameters = testbed_instance._get_parameters(guid)
125     element = testbed_instance._make_tun_iface(parameters)
126     testbed_instance.elements[guid] = element
127
128 def create_application(testbed_instance, guid):
129     parameters = testbed_instance._get_parameters(guid)
130     element = testbed_instance._make_application(parameters)
131     testbed_instance.elements[guid] = element
132
133 def create_internet(testbed_instance, guid):
134     parameters = testbed_instance._get_parameters(guid)
135     element = testbed_instance._make_internet(parameters)
136     testbed_instance.elements[guid] = element
137
138 def create_netpipe(testbed_instance, guid):
139     parameters = testbed_instance._get_parameters(guid)
140     element = testbed_instance._make_netpipe(parameters)
141     testbed_instance.elements[guid] = element
142
143 ### Start/Stop functions ###
144
145 def start_application(testbed_instance, guid):
146     parameters = testbed_instance._get_parameters(guid)
147     traces = testbed_instance._get_traces(guid)
148     app = testbed_instance.elements[guid]
149     
150     app.stdout = "stdout" in traces
151     app.stderr = "stderr" in traces
152     app.buildlog = "buildlog" in traces
153     
154     app.start()
155
156 def stop_application(testbed_instance, guid):
157     app = testbed_instance.elements[guid]
158     app.kill()
159
160 ### Status functions ###
161
162 def status_application(testbed_instance, guid):
163     if guid not in testbed_instance.elements.keys():
164         return STATUS_NOT_STARTED
165     
166     app = testbed_instance.elements[guid]
167     return app.status()
168
169 ### Configure functions ###
170
171 def configure_nodeiface(testbed_instance, guid):
172     element = testbed_instance._elements[guid]
173     
174     # Cannot explicitly configure addresses
175     if guid in testbed_instance._add_address:
176         raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
177     
178     # Get siblings
179     node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
180     dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
181     siblings = [ self._element[dev_guid] 
182                  for dev_guid in dev_guids
183                  if dev_guid != guid ]
184     
185     # Fetch address from PLC api
186     element.pick_iface(siblings)
187     
188     # Do some validations
189     element.validate()
190
191 def configure_tuniface(testbed_instance, guid):
192     element = testbed_instance._elements[guid]
193     if not guid in testbed_instance._add_address:
194         return
195     
196     addresses = testbed_instance._add_address[guid]
197     for address in addresses:
198         (address, netprefix, broadcast) = address
199         element.add_address(address, netprefix, broadcast)
200     
201     # Do some validations
202     element.validate()
203
204 def configure_node(testbed_instance, guid):
205     node = testbed_instance._elements[guid]
206     
207     # Just inject configuration stuff
208     node.home_path = "nepi-node-%s" % (guid,)
209     node.ident_path = testbed_instance.sliceSSHKey
210     node.slicename = testbed_instance.slicename
211     
212     # Do some validations
213     node.validate()
214     
215     # recently provisioned nodes may not be up yet
216     sleeptime = 1.0
217     while not node.is_alive():
218         time.sleep(sleeptime)
219         sleeptime = min(30.0, sleeptime*1.5)
220     
221     # this will be done in parallel in all nodes
222     # this call only spawns the process
223     node.install_dependencies()
224
225 def configure_application(testbed_instance, guid):
226     app = testbed_instance._elements[guid]
227     
228     # Just inject configuration stuff
229     app.home_path = "nepi-app-%s" % (guid,)
230     app.ident_path = testbed_instance.sliceSSHKey
231     app.slicename = testbed_instance.slicename
232     
233     # Do some validations
234     app.validate()
235     
236     # Wait for dependencies
237     app.node.wait_dependencies()
238     
239     # Install stuff
240     app.setup()
241
242 def configure_netpipe(testbed_instance, guid):
243     netpipe = testbed_instance._elements[guid]
244     
245     # Do some validations
246     netpipe.validate()
247     
248     # Wait for dependencies
249     netpipe.node.wait_dependencies()
250     
251     # Install rules
252     netpipe.configure()
253
254 ### Factory information ###
255
256 connector_types = dict({
257     "apps": dict({
258                 "help": "Connector from node to applications", 
259                 "name": "apps",
260                 "max": -1, 
261                 "min": 0
262             }),
263     "devs": dict({
264                 "help": "Connector from node to network interfaces", 
265                 "name": "devs",
266                 "max": -1, 
267                 "min": 0
268             }),
269     "inet": dict({
270                 "help": "Connector from network interfaces to the internet", 
271                 "name": "inet",
272                 "max": 1, 
273                 "min": 1
274             }),
275     "node": dict({
276                 "help": "Connector to a Node", 
277                 "name": "node",
278                 "max": 1, 
279                 "min": 1
280             }),
281     "pipes": dict({
282                 "help": "Connector to a NetPipe", 
283                 "name": "pipes",
284                 "max": 2, 
285                 "min": 0
286             }),
287    })
288
289 connections = [
290     dict({
291         "from": (TESTBED_ID, NODE, "devs"),
292         "to":   (TESTBED_ID, NODEIFACE, "node"),
293         "code": connect_node_iface_node,
294         "can_cross": False
295     }),
296     dict({
297         "from": (TESTBED_ID, NODE, "devs"),
298         "to":   (TESTBED_ID, TUNIFACE, "node"),
299         "code": connect_tun_iface_node,
300         "can_cross": False
301     }),
302     dict({
303         "from": (TESTBED_ID, NODEIFACE, "inet"),
304         "to":   (TESTBED_ID, INTERNET, "devs"),
305         "code": connect_node_iface_inet,
306         "can_cross": False
307     }),
308     dict({
309         "from": (TESTBED_ID, NODE, "apps"),
310         "to":   (TESTBED_ID, APPLICATION, "node"),
311         "code": connect_app,
312         "can_cross": False
313     }),
314     dict({
315         "from": (TESTBED_ID, NODE, "pipes"),
316         "to":   (TESTBED_ID, NETPIPE, "node"),
317         "code": connect_node_netpipe,
318         "can_cross": False
319     }),
320 ]
321
322 attributes = dict({
323     "forward_X11": dict({      
324                 "name": "forward_X11",
325                 "help": "Forward x11 from main namespace to the node",
326                 "type": Attribute.BOOL, 
327                 "value": False,
328                 "flags": Attribute.DesignOnly,
329                 "validation_function": validation.is_bool,
330             }),
331     "hostname": dict({      
332                 "name": "hostname",
333                 "help": "Constrain hostname during resource discovery. May use wildcards.",
334                 "type": Attribute.STRING, 
335                 "flags": Attribute.DesignOnly,
336                 "validation_function": validation.is_string,
337             }),
338     "architecture": dict({      
339                 "name": "architecture",
340                 "help": "Constrain architexture during resource discovery.",
341                 "type": Attribute.ENUM, 
342                 "flags": Attribute.DesignOnly,
343                 "allowed": ["x86_64",
344                             "i386"],
345                 "validation_function": validation.is_enum,
346             }),
347     "operating_system": dict({      
348                 "name": "operatingSystem",
349                 "help": "Constrain operating system during resource discovery.",
350                 "type": Attribute.ENUM, 
351                 "flags": Attribute.DesignOnly,
352                 "allowed": ["f8",
353                             "f12",
354                             "f14",
355                             "centos",
356                             "other"],
357                 "validation_function": validation.is_enum,
358             }),
359     "site": dict({      
360                 "name": "site",
361                 "help": "Constrain the PlanetLab site this node should reside on.",
362                 "type": Attribute.ENUM, 
363                 "flags": Attribute.DesignOnly,
364                 "allowed": ["PLE",
365                             "PLC",
366                             "PLJ"],
367                 "validation_function": validation.is_enum,
368             }),
369     "emulation": dict({      
370                 "name": "emulation",
371                 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
372                 "type": Attribute.BOOL,
373                 "value": False, 
374                 "flags": Attribute.DesignOnly,
375                 "validation_function": validation.is_bool,
376             }),
377     "min_reliability": dict({
378                 "name": "minReliability",
379                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
380                 "type": Attribute.DOUBLE,
381                 "range": (0,100),
382                 "flags": Attribute.DesignOnly,
383                 "validation_function": validation.is_double,
384             }),
385     "max_reliability": dict({
386                 "name": "maxReliability",
387                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
388                 "type": Attribute.DOUBLE,
389                 "range": (0,100),
390                 "flags": Attribute.DesignOnly,
391                 "validation_function": validation.is_double,
392             }),
393     "min_bandwidth": dict({
394                 "name": "minBandwidth",
395                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
396                 "type": Attribute.DOUBLE,
397                 "range": (0,2**31),
398                 "flags": Attribute.DesignOnly,
399                 "validation_function": validation.is_double,
400             }),
401     "max_bandwidth": dict({
402                 "name": "maxBandwidth",
403                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
404                 "type": Attribute.DOUBLE,
405                 "range": (0,2**31),
406                 "flags": Attribute.DesignOnly,
407                 "validation_function": validation.is_double,
408             }),
409             
410     "up": dict({
411                 "name": "up",
412                 "help": "Link up",
413                 "type": Attribute.BOOL,
414                 "value": False,
415                 "validation_function": validation.is_bool
416             }),
417     "primary": dict({
418                 "name": "primary",
419                 "help": "This is the primary interface for the attached node",
420                 "type": Attribute.BOOL,
421                 "value": True,
422                 "validation_function": validation.is_bool
423             }),
424     "device_name": dict({
425                 "name": "name",
426                 "help": "Device name",
427                 "type": Attribute.STRING,
428                 "flags": Attribute.DesignOnly,
429                 "validation_function": validation.is_string
430             }),
431     "mtu":  dict({
432                 "name": "mtu", 
433                 "help": "Maximum transmition unit for device",
434                 "type": Attribute.INTEGER,
435                 "range": (0,1500),
436                 "validation_function": validation.is_integer_range(0,1500)
437             }),
438     "mask":  dict({
439                 "name": "mask", 
440                 "help": "Network mask for the device (eg: 24 for /24 network)",
441                 "type": Attribute.INTEGER,
442                 "validation_function": validation.is_integer_range(8,24)
443             }),
444     "snat":  dict({
445                 "name": "snat", 
446                 "help": "Enable SNAT (source NAT to the internet) no this device",
447                 "type": Attribute.BOOL,
448                 "value": False,
449                 "validation_function": validation.is_bool
450             }),
451             
452     "command": dict({
453                 "name": "command",
454                 "help": "Command line string",
455                 "type": Attribute.STRING,
456                 "flags": Attribute.DesignOnly,
457                 "validation_function": validation.is_string
458             }),
459     "sudo": dict({
460                 "name": "sudo",
461                 "help": "Run with root privileges",
462                 "type": Attribute.BOOL,
463                 "flags": Attribute.DesignOnly,
464                 "value": False,
465                 "validation_function": validation.is_bool
466             }),
467     "stdin": dict({
468                 "name": "stdin",
469                 "help": "Standard input",
470                 "type": Attribute.STRING,
471                 "flags": Attribute.DesignOnly,
472                 "validation_function": validation.is_string
473             }),
474             
475     "depends": dict({
476                 "name": "depends",
477                 "help": "Space-separated list of packages required to run the application",
478                 "type": Attribute.STRING,
479                 "flags": Attribute.DesignOnly,
480                 "validation_function": validation.is_string
481             }),
482     "build-depends": dict({
483                 "name": "buildDepends",
484                 "help": "Space-separated list of packages required to build the application",
485                 "type": Attribute.STRING,
486                 "flags": Attribute.DesignOnly,
487                 "validation_function": validation.is_string
488             }),
489     "sources": dict({
490                 "name": "sources",
491                 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
492                         "Archives won't be expanded automatically.",
493                 "type": Attribute.STRING,
494                 "flags": Attribute.DesignOnly,
495                 "validation_function": validation.is_string
496             }),
497     "build": dict({
498                 "name": "build",
499                 "help": "Build commands to execute after deploying the sources. "
500                         "Sources will be in the ${SOURCES} folder. "
501                         "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
502                         "Try to make the commands return with a nonzero exit code on error.\n"
503                         "Also, do not install any programs here, use the 'install' attribute. This will "
504                         "help keep the built files constrained to the build folder (which may "
505                         "not be the home folder), and will result in faster deployment. Also, "
506                         "make sure to clean up temporary files, to reduce bandwidth usage between "
507                         "nodes when transferring built packages.",
508                 "type": Attribute.STRING,
509                 "flags": Attribute.DesignOnly,
510                 "validation_function": validation.is_string
511             }),
512     "install": dict({
513                 "name": "install",
514                 "help": "Commands to transfer built files to their final destinations. "
515                         "Sources will be in the initial working folder, and a special "
516                         "tag ${SOURCES} can be used to reference the experiment's "
517                         "home folder (where the application commands will run).\n"
518                         "ALL sources and targets needed for execution must be copied there, "
519                         "if building has been enabled.\n"
520                         "That is, 'slave' nodes will not automatically get any source files. "
521                         "'slave' nodes don't get build dependencies either, so if you need "
522                         "make and other tools to install, be sure to provide them as "
523                         "actual dependencies instead.",
524                 "type": Attribute.STRING,
525                 "flags": Attribute.DesignOnly,
526                 "validation_function": validation.is_string
527             }),
528     
529     "netpipe_mode": dict({      
530                 "name": "mode",
531                 "help": "Link mode:\n"
532                         " * SERVER: applies to incoming connections\n"
533                         " * CLIENT: applies to outgoing connections\n"
534                         " * SERVICE: applies to both",
535                 "type": Attribute.ENUM, 
536                 "flags": Attribute.DesignOnly,
537                 "allowed": ["SERVER",
538                             "CLIENT",
539                             "SERVICE"],
540                 "validation_function": validation.is_enum,
541             }),
542     "port_list":  dict({
543                 "name": "portList", 
544                 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
545                 "type": Attribute.STRING,
546                 "validation_function": is_portlist,
547             }),
548     "addr_list":  dict({
549                 "name": "addrList", 
550                 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
551                 "type": Attribute.STRING,
552                 "validation_function": is_addrlist,
553             }),
554     "bw_in":  dict({
555                 "name": "bwIn", 
556                 "help": "Inbound bandwidth limit (in Mbit/s)",
557                 "type": Attribute.DOUBLE,
558                 "validation_function": validation.is_double,
559             }),
560     "bw_out":  dict({
561                 "name": "bwOut", 
562                 "help": "Outbound bandwidth limit (in Mbit/s)",
563                 "type": Attribute.DOUBLE,
564                 "validation_function": validation.is_double,
565             }),
566     "plr_in":  dict({
567                 "name": "plrIn", 
568                 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
569                 "type": Attribute.DOUBLE,
570                 "validation_function": validation.is_double,
571             }),
572     "plr_out":  dict({
573                 "name": "plrOut", 
574                 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
575                 "type": Attribute.DOUBLE,
576                 "validation_function": validation.is_double,
577             }),
578     "delay_in":  dict({
579                 "name": "delayIn", 
580                 "help": "Inbound packet delay (in milliseconds)",
581                 "type": Attribute.INTEGER,
582                 "range": (0,60000),
583                 "validation_function": validation.is_integer,
584             }),
585     "delay_out":  dict({
586                 "name": "delayOut", 
587                 "help": "Outbound packet delay (in milliseconds)",
588                 "type": Attribute.INTEGER,
589                 "range": (0,60000),
590                 "validation_function": validation.is_integer,
591             }),
592     })
593
594 traces = dict({
595     "stdout": dict({
596                 "name": "stdout",
597                 "help": "Standard output stream"
598               }),
599     "stderr": dict({
600                 "name": "stderr",
601                 "help": "Application standard error",
602               }),
603     "buildlog": dict({
604                 "name": "buildlog",
605                 "help": "Output of the build process",
606               }), 
607     
608     "netpipe_stats": dict({
609                 "name": "netpipeStats",
610                 "help": "Information about rule match counters, packets dropped, etc.",
611               }),
612     })
613
614 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
615
616 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, APPLICATION ]
617
618 factories_info = dict({
619     NODE: dict({
620             "allow_routes": False,
621             "help": "Virtualized Node (V-Server style)",
622             "category": "topology",
623             "create_function": create_node,
624             "preconfigure_function": configure_node,
625             "box_attributes": [
626                 "forward_X11",
627                 "hostname",
628                 "architecture",
629                 "operating_system",
630                 "site",
631                 "emulation",
632                 "min_reliability",
633                 "max_reliability",
634                 "min_bandwidth",
635                 "max_bandwidth",
636             ],
637             "connector_types": ["devs", "apps", "pipes"]
638        }),
639     NODEIFACE: dict({
640             "has_addresses": True,
641             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
642             "category": "devices",
643             "create_function": create_nodeiface,
644             "preconfigure_function": configure_nodeiface,
645             "box_attributes": [ ],
646             "connector_types": ["node", "inet"]
647         }),
648     TUNIFACE: dict({
649             "allow_addresses": True,
650             "help": "Virtual TUN network interface",
651             "category": "devices",
652             "create_function": create_tuniface,
653             "preconfigure_function": configure_tuniface,
654             "box_attributes": [
655                 "up", "device_name", "mtu", "snat",
656             ],
657             "connector_types": ["node"]
658         }),
659     APPLICATION: dict({
660             "help": "Generic executable command line application",
661             "category": "applications",
662             "create_function": create_application,
663             "start_function": start_application,
664             "status_function": status_application,
665             "stop_function": stop_application,
666             "configure_function": configure_application,
667             "box_attributes": ["command", "sudo", "stdin",
668                                "depends", "build-depends", "build", "install",
669                                "sources" ],
670             "connector_types": ["node"],
671             "traces": ["stdout", "stderr"]
672         }),
673     INTERNET: dict({
674             "help": "Internet routing",
675             "category": "topology",
676             "create_function": create_internet,
677             "connector_types": ["devs"],
678         }),
679     NETPIPE: dict({
680             "help": "Link emulation",
681             "category": "topology",
682             "create_function": create_netpipe,
683             "configure_function": configure_netpipe,
684             "box_attributes": ["netpipe_mode",
685                                "addr_list", "port_list",
686                                "bw_in","plr_in","delay_in",
687                                "bw_out","plr_out","delay_out"],
688             "connector_types": ["node"],
689             "traces": ["netpipe_stats"]
690         }),
691 })
692
693 testbed_attributes = dict({
694         "slice": dict({
695             "name": "slice",
696             "help": "The name of the PlanetLab slice to use",
697             "type": Attribute.STRING,
698             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
699             "validation_function": validation.is_string
700         }),
701         "auth_user": dict({
702             "name": "authUser",
703             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
704             "type": Attribute.STRING,
705             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
706             "validation_function": validation.is_string
707         }),
708         "auth_pass": dict({
709             "name": "authPass",
710             "help": "The PlanetLab user's password.",
711             "type": Attribute.STRING,
712             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
713             "validation_function": validation.is_string
714         }),
715         "slice_ssh_key": dict({
716             "name": "sliceSSHKey",
717             "help": "The controller-local path to the slice user's ssh private key. "
718                     "It is the user's responsability to deploy this file where the controller "
719                     "will run, it won't be done automatically because it's sensitive information. "
720                     "It is recommended that a NEPI-specific user be created for this purpose and "
721                     "this purpose alone.",
722             "type": Attribute.STRING,
723             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
724             "validation_function": validation.is_string
725         }),
726     })
727
728 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
729     @property
730     def connector_types(self):
731         return connector_types
732
733     @property
734     def connections(self):
735         return connections
736
737     @property
738     def attributes(self):
739         return attributes
740
741     @property
742     def traces(self):
743         return traces
744
745     @property
746     def create_order(self):
747         return create_order
748
749     @property
750     def configure_order(self):
751         return configure_order
752
753     @property
754     def factories_info(self):
755         return factories_info
756
757     @property
758     def testbed_attributes(self):
759         return testbed_attributes
760