2 # -*- coding: utf-8 -*-
6 from constants import TESTBED_ID
7 from nepi.core import metadata
8 from nepi.core.attributes import Attribute
9 from nepi.util import validation
10 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
11 STATUS_FINISHED, ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP
18 NODEIFACE = "NodeInterface"
19 TUNIFACE = "TunInterface"
20 APPLICATION = "Application"
21 DEPENDENCY = "Dependency"
22 NEPIDEPENDENCY = "NepiDependency"
23 NS3DEPENDENCY = "NS3Dependency"
27 PL_TESTBED_ID = "planetlab"
30 ### Custom validation functions ###
31 def is_addrlist(attribute, value):
32 if not validation.is_string(attribute, value):
39 components = value.split(',')
41 for component in components:
43 addr, mask = component.split('/',1)
45 addr, mask = component, 32
47 if mask is not None and not (mask and mask.isdigit()):
48 # No empty or nonnumeric masks
51 if not validation.is_ip4_address(attribute, value):
52 # Address part must be ipv4
57 def is_portlist(attribute, value):
58 if not validation.is_string(attribute, value):
65 components = value.split(',')
67 for component in components:
69 pfrom, pto = component.split('-',1)
71 pfrom = pto = component
73 if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
74 # No empty or nonnumeric ports
80 ### Connection functions ####
82 def connect_node_iface_node(testbed_instance, node_guid, iface_guid):
83 node = testbed_instance._elements[node_guid]
84 iface = testbed_instance._elements[iface_guid]
87 def connect_node_iface_inet(testbed_instance, iface_guid, inet_guid):
88 iface = testbed_instance._elements[iface_guid]
89 iface.has_internet = True
91 def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
92 node = testbed_instance._elements[node_guid]
93 iface = testbed_instance._elements[iface_guid]
94 if not node.emulation:
95 raise RuntimeError, "Use of TUN interfaces requires emulation"
97 node.required_vsys.update(('fd_tuntap', 'vif_up'))
99 def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
100 iface = testbed_instance._elements[iface_guid]
101 peer_iface = testbed_instance._elements[peer_iface_guid]
102 iface.peer_iface = peer_iface
104 iface.tun_proto = proto
106 def connect_dep(testbed_instance, node_guid, app_guid):
107 node = testbed_instance._elements[node_guid]
108 app = testbed_instance._elements[app_guid]
112 node.required_packages.update(set(
113 app.depends.split() ))
116 if app.home_path and app.home_path not in node.pythonpath:
117 node.pythonpath.append(app.home_path)
120 for envkey, envval in app.env.iteritems():
121 envval = app._replace_paths(envval)
122 node.env[envkey].append(envval)
124 def connect_node_netpipe(testbed_instance, node_guid, netpipe_guid):
125 node = testbed_instance._elements[node_guid]
126 netpipe = testbed_instance._elements[netpipe_guid]
127 if not node.emulation:
128 raise RuntimeError, "Use of NetPipes requires emulation"
132 ### Creation functions ###
134 def create_node(testbed_instance, guid):
135 parameters = testbed_instance._get_parameters(guid)
137 # create element with basic attributes
138 element = testbed_instance._make_node(parameters)
140 # add constraint on number of (real) interfaces
141 # by counting connected devices
142 dev_guids = testbed_instance.get_connected(guid, "node", "devs")
143 num_open_ifaces = sum( # count True values
144 NODEIFACE == testbed_instance._get_factory_id(guid)
145 for guid in dev_guids )
146 element.min_num_external_ifaces = num_open_ifaces
148 testbed_instance.elements[guid] = element
150 def create_nodeiface(testbed_instance, guid):
151 parameters = testbed_instance._get_parameters(guid)
152 element = testbed_instance._make_node_iface(parameters)
153 testbed_instance.elements[guid] = element
155 def create_tuniface(testbed_instance, guid):
156 parameters = testbed_instance._get_parameters(guid)
157 element = testbed_instance._make_tun_iface(parameters)
158 testbed_instance.elements[guid] = element
160 def create_application(testbed_instance, guid):
161 parameters = testbed_instance._get_parameters(guid)
162 element = testbed_instance._make_application(parameters)
164 # Just inject configuration stuff
165 element.home_path = "nepi-app-%s" % (guid,)
167 testbed_instance.elements[guid] = element
169 def create_dependency(testbed_instance, guid):
170 parameters = testbed_instance._get_parameters(guid)
171 element = testbed_instance._make_dependency(parameters)
173 # Just inject configuration stuff
174 element.home_path = "nepi-dep-%s" % (guid,)
176 testbed_instance.elements[guid] = element
178 def create_nepi_dependency(testbed_instance, guid):
179 parameters = testbed_instance._get_parameters(guid)
180 element = testbed_instance._make_nepi_dependency(parameters)
182 # Just inject configuration stuff
183 element.home_path = "nepi-nepi-%s" % (guid,)
185 testbed_instance.elements[guid] = element
187 def create_ns3_dependency(testbed_instance, guid):
188 parameters = testbed_instance._get_parameters(guid)
189 element = testbed_instance._make_ns3_dependency(parameters)
191 # Just inject configuration stuff
192 element.home_path = "nepi-ns3-%s" % (guid,)
194 testbed_instance.elements[guid] = element
196 def create_internet(testbed_instance, guid):
197 parameters = testbed_instance._get_parameters(guid)
198 element = testbed_instance._make_internet(parameters)
199 testbed_instance.elements[guid] = element
201 def create_netpipe(testbed_instance, guid):
202 parameters = testbed_instance._get_parameters(guid)
203 element = testbed_instance._make_netpipe(parameters)
204 testbed_instance.elements[guid] = element
206 ### Start/Stop functions ###
208 def start_application(testbed_instance, guid):
209 parameters = testbed_instance._get_parameters(guid)
210 traces = testbed_instance._get_traces(guid)
211 app = testbed_instance.elements[guid]
213 app.stdout = "stdout" in traces
214 app.stderr = "stderr" in traces
215 app.buildlog = "buildlog" in traces
219 def stop_application(testbed_instance, guid):
220 app = testbed_instance.elements[guid]
223 ### Status functions ###
225 def status_application(testbed_instance, guid):
226 if guid not in testbed_instance.elements.keys():
227 return STATUS_NOT_STARTED
229 app = testbed_instance.elements[guid]
232 ### Configure functions ###
234 def configure_nodeiface(testbed_instance, guid):
235 element = testbed_instance._elements[guid]
237 # Cannot explicitly configure addresses
238 if guid in testbed_instance._add_address:
239 raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
242 node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
243 dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
244 siblings = [ self._element[dev_guid]
245 for dev_guid in dev_guids
246 if dev_guid != guid ]
248 # Fetch address from PLC api
249 element.pick_iface(siblings)
251 # Do some validations
254 def preconfigure_tuniface(testbed_instance, guid):
255 element = testbed_instance._elements[guid]
257 # Set custom addresses if any
258 if guid in testbed_instance._add_address:
259 addresses = testbed_instance._add_address[guid]
260 for address in addresses:
261 (address, netprefix, broadcast) = address
262 element.add_address(address, netprefix, broadcast)
264 # Link to external interface, if any
265 for iface in testbed_instance._elements.itervalues():
266 if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
267 element.external_iface = iface
270 # Set standard TUN attributes
271 element.tun_addr = element.external_iface.address
272 element.tun_port = 15000 + int(guid)
275 traces = testbed_instance._get_traces(guid)
276 element.capture = 'packets' in traces
278 # Do some validations
284 id(element) < id(element.peer_iface) )
286 def postconfigure_tuniface(testbed_instance, guid):
287 element = testbed_instance._elements[guid]
293 def configure_node(testbed_instance, guid):
294 node = testbed_instance._elements[guid]
296 # Just inject configuration stuff
297 node.home_path = "nepi-node-%s" % (guid,)
298 node.ident_path = testbed_instance.sliceSSHKey
299 node.slicename = testbed_instance.slicename
301 # Do some validations
304 # recently provisioned nodes may not be up yet
306 while not node.is_alive():
307 time.sleep(sleeptime)
308 sleeptime = min(30.0, sleeptime*1.5)
310 # this will be done in parallel in all nodes
311 # this call only spawns the process
312 node.install_dependencies()
314 def configure_application(testbed_instance, guid):
315 app = testbed_instance._elements[guid]
317 # Do some validations
320 # Wait for dependencies
321 app.node.wait_dependencies()
326 def configure_dependency(testbed_instance, guid):
327 dep = testbed_instance._elements[guid]
329 # Do some validations
332 # Wait for dependencies
333 dep.node.wait_dependencies()
338 def configure_netpipe(testbed_instance, guid):
339 netpipe = testbed_instance._elements[guid]
341 # Do some validations
344 # Wait for dependencies
345 netpipe.node.wait_dependencies()
350 ### Factory information ###
352 connector_types = dict({
354 "help": "Connector from node to applications",
360 "help": "Connector from node to network interfaces",
366 "help": "Connector from node to application dependencies "
367 "(packages and applications that need to be installed)",
373 "help": "Connector from network interfaces to the internet",
379 "help": "Connector to a Node",
385 "help": "Connector to a NetPipe",
392 "help": "ip-ip tunneling over TCP link",
398 "help": "ip-ip tunneling over UDP datagrams",
407 "from": (TESTBED_ID, NODE, "devs"),
408 "to": (TESTBED_ID, NODEIFACE, "node"),
409 "init_code": connect_node_iface_node,
413 "from": (TESTBED_ID, NODE, "devs"),
414 "to": (TESTBED_ID, TUNIFACE, "node"),
415 "init_code": connect_tun_iface_node,
419 "from": (TESTBED_ID, NODEIFACE, "inet"),
420 "to": (TESTBED_ID, INTERNET, "devs"),
421 "init_code": connect_node_iface_inet,
425 "from": (TESTBED_ID, NODE, "apps"),
426 "to": (TESTBED_ID, APPLICATION, "node"),
427 "init_code": connect_dep,
431 "from": (TESTBED_ID, NODE, "deps"),
432 "to": (TESTBED_ID, DEPENDENCY, "node"),
433 "init_code": connect_dep,
437 "from": (TESTBED_ID, NODE, "deps"),
438 "to": (TESTBED_ID, NEPIDEPENDENCY, "node"),
439 "init_code": connect_dep,
443 "from": (TESTBED_ID, NODE, "deps"),
444 "to": (TESTBED_ID, NS3DEPENDENCY, "node"),
445 "init_code": connect_dep,
449 "from": (TESTBED_ID, NODE, "pipes"),
450 "to": (TESTBED_ID, NETPIPE, "node"),
451 "init_code": connect_node_netpipe,
455 "from": (TESTBED_ID, TUNIFACE, "tcp"),
456 "to": (TESTBED_ID, TUNIFACE, "tcp"),
457 "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
461 "from": (TESTBED_ID, TUNIFACE, "udp"),
462 "to": (TESTBED_ID, TUNIFACE, "udp"),
463 "init_code": functools.partial(connect_tun_iface_peer,"udp"),
469 "forward_X11": dict({
470 "name": "forward_X11",
471 "help": "Forward x11 from main namespace to the node",
472 "type": Attribute.BOOL,
474 "flags": Attribute.DesignOnly,
475 "validation_function": validation.is_bool,
479 "help": "Constrain hostname during resource discovery. May use wildcards.",
480 "type": Attribute.STRING,
481 "flags": Attribute.DesignOnly,
482 "validation_function": validation.is_string,
484 "architecture": dict({
485 "name": "architecture",
486 "help": "Constrain architexture during resource discovery.",
487 "type": Attribute.ENUM,
488 "flags": Attribute.DesignOnly,
489 "allowed": ["x86_64",
491 "validation_function": validation.is_enum,
493 "operating_system": dict({
494 "name": "operatingSystem",
495 "help": "Constrain operating system during resource discovery.",
496 "type": Attribute.ENUM,
497 "flags": Attribute.DesignOnly,
503 "validation_function": validation.is_enum,
507 "help": "Constrain the PlanetLab site this node should reside on.",
508 "type": Attribute.ENUM,
509 "flags": Attribute.DesignOnly,
513 "validation_function": validation.is_enum,
517 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
518 "type": Attribute.BOOL,
520 "flags": Attribute.DesignOnly,
521 "validation_function": validation.is_bool,
523 "min_reliability": dict({
524 "name": "minReliability",
525 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
526 "type": Attribute.DOUBLE,
528 "flags": Attribute.DesignOnly,
529 "validation_function": validation.is_double,
531 "max_reliability": dict({
532 "name": "maxReliability",
533 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
534 "type": Attribute.DOUBLE,
536 "flags": Attribute.DesignOnly,
537 "validation_function": validation.is_double,
539 "min_bandwidth": dict({
540 "name": "minBandwidth",
541 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
542 "type": Attribute.DOUBLE,
544 "flags": Attribute.DesignOnly,
545 "validation_function": validation.is_double,
547 "max_bandwidth": dict({
548 "name": "maxBandwidth",
549 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
550 "type": Attribute.DOUBLE,
552 "flags": Attribute.DesignOnly,
553 "validation_function": validation.is_double,
559 "type": Attribute.BOOL,
561 "validation_function": validation.is_bool
565 "help": "This is the primary interface for the attached node",
566 "type": Attribute.BOOL,
568 "validation_function": validation.is_bool
570 "device_name": dict({
572 "help": "Device name",
573 "type": Attribute.STRING,
574 "flags": Attribute.DesignOnly,
575 "validation_function": validation.is_string
579 "help": "Maximum transmition unit for device",
580 "type": Attribute.INTEGER,
582 "validation_function": validation.is_integer_range(0,1500)
586 "help": "Network mask for the device (eg: 24 for /24 network)",
587 "type": Attribute.INTEGER,
588 "validation_function": validation.is_integer_range(8,24)
592 "help": "Enable SNAT (source NAT to the internet) no this device",
593 "type": Attribute.BOOL,
595 "validation_function": validation.is_bool
599 "help": "Transmission queue length (in packets)",
600 "type": Attribute.INTEGER,
601 "flags": Attribute.DesignOnly,
603 "validation_function": validation.is_integer
608 "help": "Command line string",
609 "type": Attribute.STRING,
610 "flags": Attribute.DesignOnly,
611 "validation_function": validation.is_string
615 "help": "Run with root privileges",
616 "type": Attribute.BOOL,
617 "flags": Attribute.DesignOnly,
619 "validation_function": validation.is_bool
623 "help": "Standard input",
624 "type": Attribute.STRING,
625 "flags": Attribute.DesignOnly,
626 "validation_function": validation.is_string
631 "help": "Space-separated list of packages required to run the application",
632 "type": Attribute.STRING,
633 "flags": Attribute.DesignOnly,
634 "validation_function": validation.is_string
636 "build-depends": dict({
637 "name": "buildDepends",
638 "help": "Space-separated list of packages required to build the application",
639 "type": Attribute.STRING,
640 "flags": Attribute.DesignOnly,
641 "validation_function": validation.is_string
645 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
646 "Archives won't be expanded automatically.",
647 "type": Attribute.STRING,
648 "flags": Attribute.DesignOnly,
649 "validation_function": validation.is_string
653 "help": "Build commands to execute after deploying the sources. "
654 "Sources will be in the ${SOURCES} folder. "
655 "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
656 "Try to make the commands return with a nonzero exit code on error.\n"
657 "Also, do not install any programs here, use the 'install' attribute. This will "
658 "help keep the built files constrained to the build folder (which may "
659 "not be the home folder), and will result in faster deployment. Also, "
660 "make sure to clean up temporary files, to reduce bandwidth usage between "
661 "nodes when transferring built packages.",
662 "type": Attribute.STRING,
663 "flags": Attribute.DesignOnly,
664 "validation_function": validation.is_string
668 "help": "Commands to transfer built files to their final destinations. "
669 "Sources will be in the initial working folder, and a special "
670 "tag ${SOURCES} can be used to reference the experiment's "
671 "home folder (where the application commands will run).\n"
672 "ALL sources and targets needed for execution must be copied there, "
673 "if building has been enabled.\n"
674 "That is, 'slave' nodes will not automatically get any source files. "
675 "'slave' nodes don't get build dependencies either, so if you need "
676 "make and other tools to install, be sure to provide them as "
677 "actual dependencies instead.",
678 "type": Attribute.STRING,
679 "flags": Attribute.DesignOnly,
680 "validation_function": validation.is_string
682 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP: dict({
683 "name": ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
684 "help": "Commands to set up the environment needed to run NEPI testbeds",
685 "type": Attribute.STRING,
686 "flags": Attribute.Invisible | Attribute.ReadOnly,
687 "validation_function": validation.is_string
690 "netpipe_mode": dict({
692 "help": "Link mode:\n"
693 " * SERVER: applies to incoming connections\n"
694 " * CLIENT: applies to outgoing connections\n"
695 " * SERVICE: applies to both",
696 "type": Attribute.ENUM,
697 "flags": Attribute.DesignOnly,
698 "allowed": ["SERVER",
701 "validation_function": validation.is_enum,
705 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
706 "type": Attribute.STRING,
707 "validation_function": is_portlist,
711 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
712 "type": Attribute.STRING,
713 "validation_function": is_addrlist,
717 "help": "Inbound bandwidth limit (in Mbit/s)",
718 "type": Attribute.DOUBLE,
719 "validation_function": validation.is_double,
723 "help": "Outbound bandwidth limit (in Mbit/s)",
724 "type": Attribute.DOUBLE,
725 "validation_function": validation.is_double,
729 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
730 "type": Attribute.DOUBLE,
731 "validation_function": validation.is_double,
735 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
736 "type": Attribute.DOUBLE,
737 "validation_function": validation.is_double,
741 "help": "Inbound packet delay (in milliseconds)",
742 "type": Attribute.INTEGER,
744 "validation_function": validation.is_integer,
748 "help": "Outbound packet delay (in milliseconds)",
749 "type": Attribute.INTEGER,
751 "validation_function": validation.is_integer,
758 "help": "Standard output stream"
762 "help": "Application standard error",
766 "help": "Output of the build process",
769 "netpipe_stats": dict({
770 "name": "netpipeStats",
771 "help": "Information about rule match counters, packets dropped, etc.",
776 "help": "Detailled log of all packets going through the interface",
780 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
782 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
784 factories_info = dict({
786 "allow_routes": False,
787 "help": "Virtualized Node (V-Server style)",
788 "category": "topology",
789 "create_function": create_node,
790 "preconfigure_function": configure_node,
803 # NEPI-in-NEPI attributes
804 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
806 "connector_types": ["devs", "apps", "pipes", "deps"]
809 "has_addresses": True,
810 "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
811 "category": "devices",
812 "create_function": create_nodeiface,
813 "preconfigure_function": configure_nodeiface,
814 "box_attributes": [ ],
815 "connector_types": ["node", "inet"]
818 "allow_addresses": True,
819 "help": "Virtual TUN network interface",
820 "category": "devices",
821 "create_function": create_tuniface,
822 "preconfigure_function": preconfigure_tuniface,
823 "configure_function": postconfigure_tuniface,
825 "up", "device_name", "mtu", "snat",
828 "traces": ["packets"],
829 "connector_types": ["node","udp","tcp"]
832 "help": "Generic executable command line application",
833 "category": "applications",
834 "create_function": create_application,
835 "start_function": start_application,
836 "status_function": status_application,
837 "stop_function": stop_application,
838 "configure_function": configure_application,
839 "box_attributes": ["command", "sudo", "stdin",
840 "depends", "build-depends", "build", "install",
842 "connector_types": ["node"],
843 "traces": ["stdout", "stderr", "buildlog"]
846 "help": "Requirement for package or application to be installed on some node",
847 "category": "applications",
848 "create_function": create_dependency,
849 "configure_function": configure_dependency,
850 "box_attributes": ["depends", "build-depends", "build", "install",
852 "connector_types": ["node"],
853 "traces": ["buildlog"]
855 NEPIDEPENDENCY: dict({
856 "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
857 "category": "applications",
858 "create_function": create_nepi_dependency,
859 "configure_function": configure_dependency,
860 "box_attributes": [ ],
861 "connector_types": ["node"],
862 "traces": ["buildlog"]
864 NS3DEPENDENCY: dict({
865 "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
866 "category": "applications",
867 "create_function": create_ns3_dependency,
868 "configure_function": configure_dependency,
869 "box_attributes": [ ],
870 "connector_types": ["node"],
871 "traces": ["buildlog"]
874 "help": "Internet routing",
875 "category": "topology",
876 "create_function": create_internet,
877 "connector_types": ["devs"],
880 "help": "Link emulation",
881 "category": "topology",
882 "create_function": create_netpipe,
883 "configure_function": configure_netpipe,
884 "box_attributes": ["netpipe_mode",
885 "addr_list", "port_list",
886 "bw_in","plr_in","delay_in",
887 "bw_out","plr_out","delay_out"],
888 "connector_types": ["node"],
889 "traces": ["netpipe_stats"]
893 testbed_attributes = dict({
896 "help": "The name of the PlanetLab slice to use",
897 "type": Attribute.STRING,
898 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
899 "validation_function": validation.is_string
903 "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
904 "type": Attribute.STRING,
905 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
906 "validation_function": validation.is_string
910 "help": "The PlanetLab user's password.",
911 "type": Attribute.STRING,
912 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
913 "validation_function": validation.is_string
915 "slice_ssh_key": dict({
916 "name": "sliceSSHKey",
917 "help": "The controller-local path to the slice user's ssh private key. "
918 "It is the user's responsability to deploy this file where the controller "
919 "will run, it won't be done automatically because it's sensitive information. "
920 "It is recommended that a NEPI-specific user be created for this purpose and "
921 "this purpose alone.",
922 "type": Attribute.STRING,
923 "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
924 "validation_function": validation.is_string
928 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
930 def connector_types(self):
931 return connector_types
934 def connections(self):
938 def attributes(self):
946 def create_order(self):
950 def configure_order(self):
951 return configure_order
954 def factories_info(self):
955 return factories_info
958 def testbed_attributes(self):
959 return testbed_attributes