Daemonized servers are now always launched with popen, and not directly invoked in...
[nepi.git] / src / nepi / core / metadata.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from nepi.core.attributes import Attribute, AttributesMap
5 from nepi.core.connector import ConnectorType
6 from nepi.core.factory import Factory
7 import sys
8 import getpass
9 import nepi.util.environ
10 from nepi.util import tags, validation
11 from nepi.util.constants import ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP, \
12         DeploymentConfiguration as DC, \
13         AttributeCategories as AC
14
15 class Parallel(object):
16     def __init__(self, factory, maxthreads = 64):
17         self.factory = factory
18         self.maxthreads = maxthreads
19
20 class MetadataInfo(object):
21     @property
22     def connector_types(self):
23         """ dictionary of dictionaries with allowed connection information.
24             connector_id: dict({
25                 "help": help text, 
26                 "name": connector type name,
27                 "max": maximum number of connections allowed (-1 for no limit),
28                 "min": minimum number of connections allowed
29             }),
30         """
31         raise NotImplementedError
32
33     @property
34     def connections(self):
35         """ array of dictionaries with allowed connection information.
36         dict({
37             "from": (testbed_id1, factory_id1, connector_type_name1),
38             "to": (testbed_id2, factory_id2, connector_type_name2),
39             "init_code": connection function to invoke for connection initiation
40             "compl_code": connection function to invoke for connection 
41                 completion
42             "can_cross": whether the connection can be done across testbed 
43                             instances
44          }),
45         """
46         raise NotImplementedError
47
48     @property
49     def attributes(self):
50         """ dictionary of dictionaries of all available attributes.
51             attribute_id: dict({
52                 "name": attribute name,
53                 "help": help text,
54                 "type": attribute type, 
55                 "value": default attribute value,
56                 "range": (maximum, minimun) values else None if not defined,
57                 "allowed": array of posible values,
58                 "flags": attributes flags,
59                 "validation_function": validation function for the attribute
60                 "category": category for the attribute
61             })
62         """
63         raise NotImplementedError
64
65     @property
66     def traces(self):
67         """ dictionary of dictionaries of all available traces.
68             trace_id: dict({
69                 "name": trace name,
70                 "help": help text
71             })
72         """
73         raise NotImplementedError
74
75     @property
76     def create_order(self):
77         """ list of factory ids that indicates the order in which the elements
78         should be instantiated. If wrapped within a Parallel instance, they
79         will be instantiated in parallel.
80         """
81         raise NotImplementedError
82
83     @property
84     def configure_order(self):
85         """ list of factory ids that indicates the order in which the elements
86         should be configured. If wrapped within a Parallel instance, they
87         will be configured in parallel.
88         """
89         raise NotImplementedError
90
91     @property
92     def preconfigure_order(self):
93         """ list of factory ids that indicates the order in which the elements
94         should be preconfigured. If wrapped within a Parallel instance, they
95         will be configured in parallel.
96         
97         Default: same as configure_order
98         """
99         return self.configure_order
100
101     @property
102     def prestart_order(self):
103         """ list of factory ids that indicates the order in which the elements
104         should be prestart-configured. If wrapped within a Parallel instance, they
105         will be configured in parallel.
106         
107         Default: same as configure_order
108         """
109         return self.configure_order
110
111     @property
112     def start_order(self):
113         """ list of factory ids that indicates the order in which the elements
114         should be started. If wrapped within a Parallel instance, they
115         will be started in parallel.
116         
117         Default: same as configure_order
118         """
119         return self.configure_order
120
121     @property
122     def factories_info(self):
123         """ dictionary of dictionaries of factory specific information
124             factory_id: dict({
125                 "help": help text,
126                 "category": category the element belongs to,
127                 "create_function": function for element instantiation,
128                 "start_function": function for element starting,
129                 "stop_function": function for element stoping,
130                 "status_function": function for retrieving element status,
131                 "preconfigure_function": function for element preconfiguration,
132                     (just after connections are made, 
133                     just before netrefs are resolved)
134                 "configure_function": function for element configuration,
135                 "prestart_function": function for pre-start
136                     element configuration (just before starting applications),
137                     useful for synchronization of background setup tasks or
138                     lazy instantiation or configuration of attributes
139                     that require connection/cross-connection state before
140                     being created.
141                     After this point, all applications should be able to run.
142                 "factory_attributes": list of references to attribute_ids,
143                 "box_attributes": list of regerences to attribute_ids,
144                 "traces": list of references to trace_id
145                 "tags": list of references to tag_id
146                 "connector_types": list of references to connector_types
147            })
148         """
149         raise NotImplementedError
150
151     @property
152     def testbed_attributes(self):
153         """ dictionary of attributes for testbed instance configuration
154             attributes_id = dict({
155                 "name": attribute name,
156                 "help": help text,
157                 "type": attribute type, 
158                 "value": default attribute value,
159                 "range": (maximum, minimun) values else None if not defined,
160                 "allowed": array of posible values,
161                 "flags": attributes flags,
162                 "validation_function": validation function for the attribute
163                 "category": category for the attribute
164              })
165             ]
166         """
167         raise NotImplementedError
168
169     @property
170     def testbed_id(self):
171         """ ID for the testbed """
172         raise NotImplementedError
173
174     @property
175     def testbed_version(self):
176         """ version for the testbed """
177         raise NotImplementedError
178
179 class Metadata(object):
180     # These attributes should be added to all boxes
181     STANDARD_BOX_ATTRIBUTES = dict({
182         "label" : dict({
183             "name" : "label",
184             "validation_function" : validation.is_string,
185             "type" : Attribute.STRING,
186             "flags" : Attribute.ExecReadOnly |\
187                     Attribute.ExecImmutable |\
188                     Attribute.Metadata,
189             "help" : "A unique identifier for referring to this box",
190         }),
191      })
192
193     # These are the attribute definitions for tagged attributes
194     STANDARD_TAGGED_ATTRIBUTES_DEFINITIONS = dict({
195         "maxAddresses" : dict({
196             "name" : "maxAddresses",
197             "validation_function" : validation.is_integer,
198             "type" : Attribute.INTEGER,
199             "value" : 1,
200             "flags" : Attribute.DesignReadOnly |\
201                     Attribute.ExecInvisible |\
202                     Attribute.Metadata,
203             "help" : "The maximum allowed number of addresses",
204             }),
205         })
206
207     # Attributes to be added to all boxes with specific tags
208     STANDARD_TAGGED_BOX_ATTRIBUTES = dict({
209         tags.ALLOW_ADDRESSES : ["maxAddresses"],
210         tags.HAS_ADDRESSES : ["maxAddresses"],
211     })
212
213     # These attributes should be added to all testbeds
214     STANDARD_TESTBED_ATTRIBUTES = dict({
215         "home_directory" : dict({
216             "name" : "homeDirectory",
217             "validation_function" : validation.is_string,
218             "help" : "Path to the directory where traces and other files will be stored",
219             "type" : Attribute.STRING,
220             "value" : "",
221             "flags" : Attribute.ExecReadOnly |\
222                     Attribute.ExecImmutable |\
223                     Attribute.Metadata,
224             }),
225         "label" : dict({
226             "name" : "label",
227             "validation_function" : validation.is_string,
228             "type" : Attribute.STRING,
229             "flags" : Attribute.ExecReadOnly |\
230                     Attribute.ExecImmutable |\
231                     Attribute.Metadata,
232             "help" : "A unique identifier for referring to this testbed",
233             }),
234         })
235     
236     # These attributes should be added to all testbeds
237     DEPLOYMENT_ATTRIBUTES = dict({
238         # TESTBED DEPLOYMENT ATTRIBUTES
239         DC.DEPLOYMENT_ENVIRONMENT_SETUP : dict({
240             "name" : DC.DEPLOYMENT_ENVIRONMENT_SETUP,
241             "validation_function" : validation.is_string,
242             "help" : "Shell commands to run before spawning TestbedController processes",
243             "type" : Attribute.STRING,
244             "flags" : Attribute.ExecReadOnly |\
245                     Attribute.ExecImmutable |\
246                     Attribute.Metadata,
247             "category" : AC.CATEGORY_DEPLOYMENT,
248         }),
249         DC.DEPLOYMENT_MODE: dict({
250             "name" : DC.DEPLOYMENT_MODE,
251             "help" : "Instance execution mode",
252             "type" : Attribute.ENUM,
253             "value" : DC.MODE_SINGLE_PROCESS,
254             "allowed" : [
255                     DC.MODE_DAEMON,
256                     DC.MODE_SINGLE_PROCESS
257                 ],
258            "flags" : Attribute.ExecReadOnly |\
259                     Attribute.ExecImmutable |\
260                     Attribute.Metadata,
261             "validation_function" : validation.is_enum,
262             "category" : AC.CATEGORY_DEPLOYMENT,
263             }),
264         DC.DEPLOYMENT_COMMUNICATION : dict({
265             "name" : DC.DEPLOYMENT_COMMUNICATION,
266             "help" : "Instance communication mode",
267             "type" : Attribute.ENUM,
268             "value" : DC.ACCESS_LOCAL,
269             "allowed" : [
270                     DC.ACCESS_LOCAL,
271                     DC.ACCESS_SSH
272                 ],
273             "flags" : Attribute.ExecReadOnly |\
274                     Attribute.ExecImmutable |\
275                     Attribute.Metadata,
276             "validation_function" : validation.is_enum,
277             "category" : AC.CATEGORY_DEPLOYMENT,
278             }),
279         DC.DEPLOYMENT_HOST : dict({
280             "name" : DC.DEPLOYMENT_HOST,
281             "help" : "Host where the testbed will be executed",
282             "type" : Attribute.STRING,
283             "value" : "localhost",
284             "flags" : Attribute.ExecReadOnly |\
285                     Attribute.ExecImmutable |\
286                     Attribute.Metadata,
287             "validation_function" : validation.is_string,
288             "category" : AC.CATEGORY_DEPLOYMENT,
289             }),
290         DC.DEPLOYMENT_USER : dict({
291             "name" : DC.DEPLOYMENT_USER,
292             "help" : "User on the Host to execute the testbed",
293             "type" : Attribute.STRING,
294             "value" : getpass.getuser(),
295             "flags" : Attribute.ExecReadOnly |\
296                     Attribute.ExecImmutable |\
297                     Attribute.Metadata,
298             "validation_function" : validation.is_string,
299             "category" : AC.CATEGORY_DEPLOYMENT,
300             }),
301         DC.DEPLOYMENT_KEY : dict({
302             "name" : DC.DEPLOYMENT_KEY,
303             "help" : "Path to SSH key to use for connecting",
304             "type" : Attribute.STRING,
305             "flags" : Attribute.ExecReadOnly |\
306                     Attribute.ExecImmutable |\
307                     Attribute.Metadata,
308             "validation_function" : validation.is_string,
309             "category" : AC.CATEGORY_DEPLOYMENT,
310             }),
311         DC.DEPLOYMENT_PORT : dict({
312             "name" : DC.DEPLOYMENT_PORT,
313             "help" : "Port on the Host",
314             "type" : Attribute.INTEGER,
315             "value" : 22,
316             "flags" : Attribute.ExecReadOnly |\
317                     Attribute.ExecImmutable |\
318                     Attribute.Metadata,
319             "validation_function" : validation.is_integer,
320             "category" : AC.CATEGORY_DEPLOYMENT,
321             }),
322         DC.ROOT_DIRECTORY : dict({
323             "name" : DC.ROOT_DIRECTORY,
324             "help" : "Root directory for storing process files",
325             "type" : Attribute.STRING,
326             "value" : ".",
327             "flags" : Attribute.ExecReadOnly |\
328                     Attribute.ExecImmutable |\
329                     Attribute.Metadata,
330             "validation_function" : validation.is_string, # TODO: validation.is_path
331             "category" : AC.CATEGORY_DEPLOYMENT,
332             }),
333         DC.USE_AGENT : dict({
334             "name" : DC.USE_AGENT,
335             "help" : "Use -A option for forwarding of the authentication agent, if ssh access is used", 
336             "type" : Attribute.BOOL,
337             "value" : False,
338             "flags" : Attribute.ExecReadOnly |\
339                     Attribute.ExecImmutable |\
340                     Attribute.Metadata,
341             "validation_function" : validation.is_bool,
342             "category" : AC.CATEGORY_DEPLOYMENT,
343             }),
344         DC.USE_SUDO : dict({
345             "name" : DC.USE_SUDO,
346             "help" : "Use sudo to run the deamon process. This option only take flace when the server runs in daemon mode.", 
347             "type" : Attribute.BOOL,
348             "value" : False,
349             "flags" : Attribute.ExecReadOnly |\
350                     Attribute.ExecImmutable |\
351                     Attribute.Metadata,
352             "validation_function" : validation.is_bool,
353             "category" : AC.CATEGORY_DEPLOYMENT,
354             }),        
355         DC.LOG_LEVEL : dict({
356             "name" : DC.LOG_LEVEL,
357             "help" : "Log level for instance",
358             "type" : Attribute.ENUM,
359             "value" : DC.ERROR_LEVEL,
360             "allowed" : [
361                     DC.ERROR_LEVEL,
362                     DC.DEBUG_LEVEL
363                 ],
364             "flags" : Attribute.ExecReadOnly |\
365                     Attribute.ExecImmutable |\
366                     Attribute.Metadata,
367             "validation_function" : validation.is_enum,
368             "category" : AC.CATEGORY_DEPLOYMENT,
369             }),
370         DC.RECOVERY_POLICY : dict({
371             "name" : DC.RECOVERY_POLICY,
372             "help" : "Specifies what action to take in the event of a failure.", 
373             "type" : Attribute.ENUM,
374             "value" : DC.POLICY_FAIL,
375             "allowed" : [
376                     DC.POLICY_FAIL,
377                     DC.POLICY_RECOVER,
378                     DC.POLICY_RESTART,
379                 ],
380             "flags" : Attribute.ExecReadOnly |\
381                     Attribute.ExecImmutable |\
382                     Attribute.Metadata,
383             "validation_function" : validation.is_enum,
384             "category" : AC.CATEGORY_DEPLOYMENT,
385             }),
386         })
387     PROXY_ATTRIBUTES = dict({
388         DC.RECOVER : dict({
389             "name" : DC.RECOVER,
390             "help" : "Do not intantiate testbeds, rather, reconnect to already-running instances. Used to recover from a dead controller.", 
391             "type" : Attribute.BOOL,
392             "value" : False,
393             "flags" : Attribute.ExecReadOnly |\
394                     Attribute.ExecImmutable |\
395                     Attribute.Metadata,
396             "validation_function" : validation.is_bool,
397             "category" : AC.CATEGORY_DEPLOYMENT,
398             }),
399         })
400     PROXY_ATTRIBUTES.update(DEPLOYMENT_ATTRIBUTES)
401   
402     # These attributes could appear in the boxes attribute list
403     STANDARD_BOX_ATTRIBUTE_DEFINITIONS = dict({
404         "tun_proto" : dict({
405             "name" : "tun_proto", 
406             "help" : "TUNneling protocol used",
407             "type" : Attribute.STRING,
408             "flags" : Attribute.DesignInvisible | \
409                     Attribute.ExecInvisible | \
410                     Attribute.ExecImmutable | \
411                     Attribute.Metadata,
412             "validation_function" : validation.is_string,
413             }),
414         "tun_key" : dict({
415             "name" : "tun_key", 
416             "help" : "Randomly selected TUNneling protocol cryptographic key. "
417                      "Endpoints must agree to use the minimum (in lexicographic order) "
418                      "of both the remote and local sides.",
419             "type" : Attribute.STRING,
420             "flags" : Attribute.DesignInvisible | \
421                     Attribute.ExecInvisible | \
422                     Attribute.ExecImmutable | \
423                     Attribute.Metadata,
424             "validation_function" : validation.is_string,
425             }),
426         "tun_addr" : dict({
427             "name": "tun_addr", 
428             "help" : "Address (IP, unix socket, whatever) of the tunnel endpoint",
429             "type" : Attribute.STRING,
430             "flags" : Attribute.DesignInvisible | \
431                     Attribute.ExecInvisible | \
432                     Attribute.ExecImmutable | \
433                     Attribute.Metadata,
434             "validation_function" : validation.is_string,
435             }),
436         "tun_port" : dict({
437             "name" : "tun_port", 
438             "help" : "IP port of the tunnel endpoint",
439             "type" : Attribute.INTEGER,
440             "flags" : Attribute.DesignInvisible | \
441                     Attribute.ExecInvisible | \
442                     Attribute.ExecImmutable | \
443                     Attribute.Metadata,
444             "validation_function" : validation.is_integer,
445             }),
446         "tun_cipher" : dict({
447             "name" : "tun_cipher", 
448             "help" : "Cryptographic cipher used for tunnelling",
449             "type" : Attribute.ENUM,
450             "value" : "AES",
451             "allowed" : [
452                 "AES",
453                 "Blowfish",
454                 "DES3",
455                 "DES",
456                 "PLAIN",
457             ],
458             "flags" : Attribute.ExecImmutable,
459             "validation_function" : validation.is_enum,
460             }),
461         ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP : dict({
462             "name" : ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
463             "help" : "Commands to set up the environment needed to run NEPI testbeds",
464             "type" : Attribute.STRING,
465             "flags" : Attribute.DesignInvisible | \
466                     Attribute.ExecInvisible | \
467                     Attribute.ExecImmutable | \
468                     Attribute.Metadata,
469             "validation_function" : validation.is_string
470             }),
471         })
472     
473     STANDARD_TESTBED_ATTRIBUTES.update(DEPLOYMENT_ATTRIBUTES.copy())
474
475     def __init__(self, testbed_id):
476         self._testbed_id = testbed_id
477         metadata_module = self._load_metadata_module()
478         self._metadata = metadata_module.MetadataInfo()
479         if testbed_id != self._metadata.testbed_id:
480             raise RuntimeError("Bad testbed id. Asked for %s, got %s" % \
481                     (testbed_id, self._metadata.testbed_id ))
482
483     @property
484     def create_order(self):
485         return self._metadata.create_order
486
487     @property
488     def configure_order(self):
489         return self._metadata.configure_order
490
491     @property
492     def preconfigure_order(self):
493         return self._metadata.preconfigure_order
494
495     @property
496     def prestart_order(self):
497         return self._metadata.prestart_order
498
499     @property
500     def start_order(self):
501         return self._metadata.start_order
502
503     @property
504     def testbed_version(self):
505         return self._metadata.testbed_version
506
507     @property
508     def testbed_id(self):
509         return self._testbed_id
510     
511     @property
512     def supported_recovery_policies(self):
513         return self._metadata.supported_recovery_policies
514
515     def testbed_attributes(self):
516         attributes = AttributesMap()
517         testbed_attributes = self._testbed_attributes()
518         self._add_attributes(attributes.add_attribute, testbed_attributes)
519         return attributes
520
521     def build_factories(self):
522         factories = list()
523         for factory_id, info in self._metadata.factories_info.iteritems():
524             create_function = info.get("create_function")
525             start_function = info.get("start_function")
526             stop_function = info.get("stop_function")
527             status_function = info.get("status_function")
528             configure_function = info.get("configure_function")
529             preconfigure_function = info.get("preconfigure_function")
530             prestart_function = info.get("prestart_function")
531             help = info["help"]
532             category = info["category"]
533             factory = Factory(factory_id, 
534                     create_function, 
535                     start_function,
536                     stop_function, 
537                     status_function, 
538                     configure_function, 
539                     preconfigure_function,
540                     prestart_function,
541                     help,
542                     category)
543                     
544             factory_attributes = self._factory_attributes(info)
545             self._add_attributes(factory.add_attribute, factory_attributes)
546             box_attributes = self._box_attributes(info)
547             self._add_attributes(factory.add_box_attribute, box_attributes)
548             
549             self._add_traces(factory, info)
550             self._add_tags(factory, info)
551             self._add_connector_types(factory, info)
552             factories.append(factory)
553         return factories
554
555     def _load_metadata_module(self):
556         mod_name = nepi.util.environ.find_testbed(self._testbed_id) + ".metadata"
557         if not mod_name in sys.modules:
558             __import__(mod_name)
559         return sys.modules[mod_name]
560
561     def _testbed_attributes(self):
562         # standar attributes
563         attributes = self.STANDARD_TESTBED_ATTRIBUTES.copy()
564         # custom attributes
565         attributes.update(self._metadata.testbed_attributes.copy())
566         return attributes
567         
568     def _factory_attributes(self, info):
569         tagged_attributes = self._tagged_attributes(info)
570         if "factory_attributes" in info:
571             definitions = self._metadata.attributes.copy()
572             # filter attributes corresponding to the factory_id
573             factory_attributes = self._filter_attributes(info["factory_attributes"], 
574                 definitions)
575         else:
576             factory_attributes = dict()
577         attributes = dict(tagged_attributes.items() + \
578                 factory_attributes.items())
579         return attributes
580
581     def _box_attributes(self, info):
582         tagged_attributes = self._tagged_attributes(info)
583         if "box_attributes" in info:
584             definitions = self.STANDARD_BOX_ATTRIBUTE_DEFINITIONS.copy()
585             definitions.update(self._metadata.attributes)
586             box_attributes = self._filter_attributes(info["box_attributes"], 
587                 definitions)
588         else:
589             box_attributes = dict()
590         attributes = dict(tagged_attributes.items() + \
591                 box_attributes.items())
592         attributes.update(self.STANDARD_BOX_ATTRIBUTES.copy())
593         return attributes
594
595     def _tagged_attributes(self, info):
596         tagged_attributes = dict()
597         for tag_id in info.get("tags", []):
598             if tag_id in self.STANDARD_TAGGED_BOX_ATTRIBUTES:
599                 attr_list = self.STANDARD_TAGGED_BOX_ATTRIBUTES[tag_id]
600                 attributes = self._filter_attributes(attr_list,
601                     self.STANDARD_TAGGED_ATTRIBUTES_DEFINITIONS)
602                 tagged_attributes.update(attributes)
603         return tagged_attributes
604
605     def _filter_attributes(self, attr_list, definitions):
606         # filter attributes not corresponding to the factory
607         attributes = dict((attr_id, definitions[attr_id]) \
608            for attr_id in attr_list)
609         return attributes
610
611     def _add_attributes(self, add_attr_func, attributes):
612         for attr_id, attr_info in attributes.iteritems():
613             name = attr_info["name"]
614             help = attr_info["help"]
615             type = attr_info["type"] 
616             value = attr_info.get("value")
617             range = attr_info.get("range")
618             allowed = attr_info.get("allowed")
619             flags = attr_info.get("flags")
620             validation_function = attr_info["validation_function"]
621             category = attr_info.get("category")
622             add_attr_func(name, help, type, value, range, allowed, flags, 
623                     validation_function, category)
624
625     def _add_traces(self, factory, info):
626         for trace_id in info.get("traces", []):
627             trace_info = self._metadata.traces[trace_id]
628             name = trace_info["name"]
629             help = trace_info["help"]
630             factory.add_trace(name, help)
631
632     def _add_tags(self, factory, info):
633         for tag_id in info.get("tags", []):
634             factory.add_tag(tag_id)
635
636     def _add_connector_types(self, factory, info):
637         if "connector_types" in info:
638             from_connections = dict()
639             to_connections = dict()
640             for connection in self._metadata.connections:
641                 froms = connection["from"]
642                 tos = connection["to"]
643                 can_cross = connection["can_cross"]
644                 init_code = connection.get("init_code")
645                 compl_code = connection.get("compl_code")
646                 
647                 for from_ in _expand(froms):
648                     for to in _expand(tos):
649                         if from_ not in from_connections:
650                             from_connections[from_] = list()
651                         if to not in to_connections:
652                             to_connections[to] = list()
653                         from_connections[from_].append((to, can_cross, init_code, 
654                             compl_code))
655                         to_connections[to].append((from_, can_cross, init_code,
656                             compl_code))
657             for connector_id in info["connector_types"]:
658                 connector_type_info = self._metadata.connector_types[
659                         connector_id]
660                 name = connector_type_info["name"]
661                 help = connector_type_info["help"]
662                 max = connector_type_info["max"]
663                 min = connector_type_info["min"]
664                 testbed_id = self._testbed_id
665                 factory_id = factory.factory_id
666                 connector_type = ConnectorType(testbed_id, factory_id, name, 
667                         help, max, min)
668                 connector_key = (testbed_id, factory_id, name)
669                 if connector_key in to_connections:
670                     for (from_, can_cross, init_code, compl_code) in \
671                             to_connections[connector_key]:
672                         (testbed_id_from, factory_id_from, name_from) = from_
673                         connector_type.add_from_connection(testbed_id_from, 
674                                 factory_id_from, name_from, can_cross, 
675                                 init_code, compl_code)
676                 if connector_key in from_connections:
677                     for (to, can_cross, init_code, compl_code) in \
678                             from_connections[(testbed_id, factory_id, name)]:
679                         (testbed_id_to, factory_id_to, name_to) = to
680                         connector_type.add_to_connection(testbed_id_to, 
681                                 factory_id_to, name_to, can_cross, init_code,
682                                 compl_code)
683                 factory.add_connector_type(connector_type)
684  
685
686 def _expand(val):
687     """
688     Expands multiple values in the "val" tuple to create cross products:
689     
690     >>> list(_expand((1,2,3)))
691     [(1, 2, 3)]
692     >>> list(_expand((1,(2,4,5),3)))
693     [(1, 2, 3), (1, 4, 3), (1, 5, 3)]
694     >>> list(_expand(((1,2),(2,4,5),3)))
695     [(1, 2, 3), (1, 4, 3), (1, 5, 3), (2, 2, 3), (2, 4, 3), (2, 5, 3)]
696     """
697     if not val:
698         yield ()
699     elif isinstance(val[0], (list,set,tuple)):
700         for x in val[0]:
701             x = (x,)
702             for e_val in _expand(val[1:]):
703                 yield x + e_val
704     else:
705         x = (val[0],)
706         for e_val in _expand(val[1:]):
707             yield x + e_val
708