Fix connection resolution order check
[nepi.git] / src / nepi / core / design.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Experiment design API
6 """
7
8 from nepi.core.attributes import AttributesMap, Attribute
9 from nepi.core.connector import ConnectorTypeBase
10 from nepi.core.metadata import Metadata
11 from nepi.util import validation
12 from nepi.util.guid import GuidGenerator
13 from nepi.util.graphical_info import GraphicalInfo
14 from nepi.util.parser._xml import XmlExperimentParser
15 import sys
16     
17
18
19 class ConnectorType(ConnectorTypeBase):
20     def __init__(self, testbed_id, factory_id, name, help, max = -1, min = 0):
21         super(ConnectorType, self).__init__(testbed_id, factory_id, name, max, min)
22         
23         # help -- help text
24         self._help = help
25         
26         # allowed_connections -- keys in the dictionary correspond to the 
27         # connector_type_id for possible connections. The value indicates if
28         # the connection is allowed accros different testbed instances
29         self._allowed_connections = dict()
30
31     @property
32     def help(self):
33         return self._help
34
35     def add_allowed_connection(self, testbed_id, factory_id, name, can_cross):
36         type_id = self.make_connector_type_id(testbed_id, factory_id, name)
37         self._allowed_connections[type_id] = can_cross
38
39     def can_connect(self, connector_type_id, testbed_guid1, testbed_guid2):
40         for lookup_type_id in self._type_resolution_order(connector_type_id):
41             if lookup_type_id in self._allowed_connections:
42                 can_cross = self._allowed_connections[lookup_type_id]
43                 if can_cross or (testbed_guid1 == testbed_guid2):
44                     return True
45         else:
46             return False
47
48 class Connector(object):
49     """A Connector sepcifies the connection points in an Object"""
50     def __init__(self, box, connector_type):
51         super(Connector, self).__init__()
52         self._box = box
53         self._connector_type = connector_type
54         self._connections = list()
55
56     @property
57     def box(self):
58         return self._box
59
60     @property
61     def connector_type(self):
62         return self._connector_type
63
64     @property
65     def connections(self):
66         return self._connections
67
68     def is_full(self):
69         """Return True if the connector has the maximum number of connections
70         """
71         return len(self.connections) == self.connector_type.max
72
73     def is_complete(self):
74         """Return True if the connector has the minimum number of connections
75         """
76         return len(self.connections) >= self.connector_type.min
77
78     def is_connected(self, connector):
79         return connector in self._connections
80
81     def connect(self, connector):
82         if not self.can_connect(connector) or not connector.can_connect(self):
83             raise RuntimeError("Could not connect.")
84         self._connections.append(connector)
85         connector._connections.append(self)
86
87     def disconnect(self, connector):
88         if connector not in self._connections or\
89                 self not in connector._connections:
90                 raise RuntimeError("Could not disconnect.")
91         self._connections.remove(connector)
92         connector._connections.remove(self)
93
94     def can_connect(self, connector):
95         if self.is_full() or connector.is_full():
96             return False
97         if self.is_connected(connector):
98             return False
99         connector_type_id = connector.connector_type.connector_type_id
100         testbed_guid1 = self.box.testbed_guid
101         testbed_guid2 = connector.box.testbed_guid
102         return self.connector_type.can_connect(connector_type_id, 
103                 testbed_guid1, testbed_guid2)
104
105     def destroy(self):
106         for connector in self.connections:
107             self.disconnect(connector)
108         self._box = self._connectors = None
109
110 class Trace(AttributesMap):
111     def __init__(self, trace_id, help, enabled = False):
112         super(Trace, self).__init__()
113         self._trace_id = trace_id
114         self._help = help       
115         self.enabled = enabled
116     
117     @property
118     def trace_id(self):
119         return self._trace_id
120
121     @property
122     def help(self):
123         return self._help
124
125 class Address(AttributesMap):
126     def __init__(self):
127         super(Address, self).__init__()
128         self.add_attribute(name = "AutoConfigure", 
129                 help = "If set, this address will automatically be assigned", 
130                 type = Attribute.BOOL,
131                 value = False,
132                 flags = Attribute.DesignOnly,
133                 validation_function = validation.is_bool)
134         self.add_attribute(name = "Address",
135                 help = "Address number", 
136                 type = Attribute.STRING,
137                 flags = Attribute.HasNoDefaultValue,
138                 validation_function = validation.is_ip_address)
139         self.add_attribute(name = "NetPrefix",
140                 help = "Network prefix for the address", 
141                 type = Attribute.INTEGER, 
142                 range = (0, 128),
143                 value = 24,
144                 flags = Attribute.HasNoDefaultValue,
145                 validation_function = validation.is_integer)
146         self.add_attribute(name = "Broadcast",
147                 help = "Broadcast address", 
148                 type = Attribute.STRING,
149                 validation_function = validation.is_ip4_address)
150                 
151 class Route(AttributesMap):
152     def __init__(self):
153         super(Route, self).__init__()
154         self.add_attribute(name = "Destination", 
155                 help = "Network destintation",
156                 type = Attribute.STRING, 
157                 validation_function = validation.is_ip_address)
158         self.add_attribute(name = "NetPrefix",
159                 help = "Network destination prefix", 
160                 type = Attribute.INTEGER, 
161                 range = (0, 128),
162                 value = 24,
163                 flags = Attribute.HasNoDefaultValue,
164                 validation_function = validation.is_integer)
165         self.add_attribute(name = "NextHop",
166                 help = "Address for the next hop", 
167                 type = Attribute.STRING,
168                 flags = Attribute.HasNoDefaultValue,
169                 validation_function = validation.is_ip_address)
170
171 class Box(AttributesMap):
172     def __init__(self, guid, factory, testbed_guid, container = None):
173         super(Box, self).__init__()
174         # guid -- global unique identifier
175         self._guid = guid
176         # factory_id -- factory identifier or name
177         self._factory_id = factory.factory_id
178         # testbed_guid -- parent testbed guid
179         self._testbed_guid = testbed_guid
180         # container -- boxes can be nested inside other 'container' boxes
181         self._container = container
182         # traces -- list of available traces for the box
183         self._traces = dict()
184         # connectors -- list of available connectors for the box
185         self._connectors = dict()
186         # factory_attributes -- factory attributes for box construction
187         self._factory_attributes = dict()
188         # graphical_info -- GUI position information
189         self.graphical_info = GraphicalInfo(str(self._guid))
190
191         for connector_type in factory.connector_types:
192             connector = Connector(self, connector_type)
193             self._connectors[connector_type.name] = connector
194         for trace in factory.traces:
195             tr = Trace(trace.trace_id, trace.help, trace.enabled)
196             self._traces[trace.trace_id] = tr
197         for attr in factory.box_attributes.attributes:
198             self.add_attribute(attr.name, attr.help, attr.type, attr.value, 
199                     attr.range, attr.allowed, attr.flags, 
200                     attr.validation_function)
201         for attr in factory.attributes:
202             if attr.modified:
203                 self._factory_attributes[attr.name] = attr.value
204
205     @property
206     def guid(self):
207         return self._guid
208
209     @property
210     def factory_id(self):
211         return self._factory_id
212
213     @property
214     def testbed_guid(self):
215         return self._testbed_guid
216
217     @property
218     def container(self):
219         return self._container
220
221     @property
222     def connectors(self):
223         return self._connectors.values()
224
225     @property
226     def traces(self):
227         return self._traces.values()
228
229     @property
230     def traces_name(self):
231         return self._traces.keys()
232
233     @property
234     def factory_attributes(self):
235         return self._factory_attributes
236
237     @property
238     def addresses(self):
239         return []
240
241     @property
242     def routes(self):
243         return []
244
245     def trace_help(self, trace_id):
246         return self._traces[trace_id].help
247
248     def enable_trace(self, trace_id):
249         self._traces[trace_id].enabled = True
250
251     def disable_trace(self, trace_id):
252         self._traces[trace_id].enabled = False
253
254     def connector(self, name):
255         return self._connectors[name]
256
257     def destroy(self):
258         super(Box, self).destroy()
259         for c in self.connectors:
260             c.destroy()         
261         for t in self.traces:
262             t.destroy()
263         self._connectors = self._traces = self._factory_attributes = None
264
265 class AddressableMixin(object):
266     def __init__(self, guid, factory, testbed_guid, container = None):
267         super(AddressableMixin, self).__init__(guid, factory, testbed_guid, 
268                 container)
269         self._max_addresses = 1 # TODO: How to make this configurable!
270         self._addresses = list()
271
272     @property
273     def addresses(self):
274         return self._addresses
275
276     @property
277     def max_addresses(self):
278         return self._max_addresses
279
280 class UserAddressableMixin(AddressableMixin):
281     def __init__(self, guid, factory, testbed_guid, container = None):
282         super(UserAddressableMixin, self).__init__(guid, factory, testbed_guid, 
283                 container)
284
285     def add_address(self):
286         if len(self._addresses) == self.max_addresses:
287             raise RuntimeError("Maximun number of addresses for this box reached.")
288         address = Address()
289         self._addresses.append(address)
290         return address
291
292     def delete_address(self, address):
293         self._addresses.remove(address)
294         del address
295
296     def destroy(self):
297         super(UserAddressableMixin, self).destroy()
298         for address in list(self.addresses):
299             self.delete_address(address)
300         self._addresses = None
301
302 class RoutableMixin(object):
303     def __init__(self, guid, factory, testbed_guid, container = None):
304         super(RoutableMixin, self).__init__(guid, factory, testbed_guid, 
305             container)
306         self._routes = list()
307
308     @property
309     def routes(self):
310         return self._routes
311
312 class UserRoutableMixin(RoutableMixin):
313     def __init__(self, guid, factory, testbed_guid, container = None):
314         super(UserRoutableMixin, self).__init__(guid, factory, testbed_guid, 
315             container)
316
317     def add_route(self):
318         route = Route()
319         self._routes.append(route)
320         return route
321
322     def delete_route(self, route):
323         self._route.remove(route)
324         del route
325
326     def destroy(self):
327         super(UserRoutableMixin, self).destroy()
328         for route in list(self.routes):
329             self.delete_route(route)
330         self._route = None
331
332 def MixIn(MyClass, MixIn):
333     # Mixins are installed BEFORE "Box" because
334     # Box inherits from non-cooperative classes,
335     # so the MRO chain gets broken when it gets
336     # to Box.
337
338     # Install mixin
339     MyClass.__bases__ = (MixIn,) + MyClass.__bases__
340     
341     # Add properties
342     # Somehow it doesn't work automatically
343     for name in dir(MixIn):
344         prop = getattr(MixIn,name,None)
345         if isinstance(prop, property):
346             setattr(MyClass, name, prop)
347     
348     # Update name
349     MyClass.__name__ = MyClass.__name__.replace(
350         'Box',
351         MixIn.__name__.replace('MixIn','')+'Box',
352         1)
353
354 class Factory(AttributesMap):
355     _box_class_cache = {}
356         
357     def __init__(self, factory_id, 
358             allow_addresses = False, has_addresses = False,
359             allow_routes = False, has_routes = False,
360             Help = None, category = None):
361         super(Factory, self).__init__()
362         self._factory_id = factory_id
363         self._allow_addresses = bool(allow_addresses)
364         self._allow_routes = bool(allow_routes)
365         self._has_addresses = bool(allow_addresses) or self._allow_addresses
366         self._has_routes = bool(allow_routes) or self._allow_routes
367         self._help = help
368         self._category = category
369         self._connector_types = list()
370         self._traces = list()
371         self._box_attributes = AttributesMap()
372         
373         if not self._has_addresses and not self._has_routes:
374             self._factory = Box
375         else:
376             addresses = 'w' if self._allow_addresses else ('r' if self._has_addresses else '-')
377             routes    = 'w' if self._allow_routes else ('r' if self._has_routes else '-')
378             key = addresses+routes
379             
380             if key in self._box_class_cache:
381                 self._factory = self._box_class_cache[key]
382             else:
383                 # Create base class
384                 class _factory(Box):
385                     def __init__(self, guid, factory, testbed_guid, container = None):
386                         super(_factory, self).__init__(guid, factory, testbed_guid, container)
387                 
388                 # Add mixins, one by one
389                 if allow_addresses:
390                     MixIn(_factory, UserAddressableMixin)
391                 elif has_addresses:
392                     MixIn(_factory, AddressableMixin)
393                     
394                 if allow_routes:
395                     MixIn(_factory, UserRoutableMixin)
396                 elif has_routes:
397                     MixIn(_factory, RoutableMixin)
398                 
399                 # Put into cache
400                 self._box_class_cache[key] = self._factory = _factory
401
402     @property
403     def factory_id(self):
404         return self._factory_id
405
406     @property
407     def allow_addresses(self):
408         return self._allow_addresses
409
410     @property
411     def allow_routes(self):
412         return self._allow_routes
413
414     @property
415     def has_addresses(self):
416         return self._has_addresses
417
418     @property
419     def has_routes(self):
420         return self._has_routes
421
422     @property
423     def help(self):
424         return self._help
425
426     @property
427     def category(self):
428         return self._category
429
430     @property
431     def connector_types(self):
432         return self._connector_types
433
434     @property
435     def traces(self):
436         return self._traces
437
438     @property
439     def box_attributes(self):
440         return self._box_attributes
441
442     def add_connector_type(self, connector_type):
443         self._connector_types.append(connector_type)
444
445     def add_trace(self, trace_id, help, enabled = False):
446         trace = Trace(trace_id, help, enabled)
447         self._traces.append(trace)
448
449     def add_box_attribute(self, name, help, type, value = None, range = None,
450         allowed = None, flags = Attribute.NoFlags, validation_function = None):
451         self._box_attributes.add_attribute(name, help, type, value, range,
452                 allowed, flags, validation_function)
453
454     def create(self, guid, testbed_description):
455         return self._factory(guid, self, testbed_description.guid)
456
457     def destroy(self):
458         super(Factory, self).destroy()
459         self._connector_types = None
460
461 class FactoriesProvider(object):
462     def __init__(self, testbed_id, testbed_version):
463         super(FactoriesProvider, self).__init__()
464         self._testbed_id = testbed_id
465         self._testbed_version = testbed_version
466         self._factories = dict()
467
468         metadata = Metadata(testbed_id, testbed_version) 
469         for factory in metadata.build_design_factories():
470             self.add_factory(factory)
471
472     @property
473     def testbed_id(self):
474         return self._testbed_id
475
476     @property
477     def testbed_version(self):
478         return self._testbed_version
479
480     @property
481     def factories(self):
482         return self._factories.values()
483
484     def factory(self, factory_id):
485         return self._factories[factory_id]
486
487     def add_factory(self, factory):
488         self._factories[factory.factory_id] = factory
489
490     def remove_factory(self, factory_id):
491         del self._factories[factory_id]
492
493 class TestbedDescription(AttributesMap):
494     def __init__(self, guid_generator, provider):
495         super(TestbedDescription, self).__init__()
496         self._guid_generator = guid_generator
497         self._guid = guid_generator.next()
498         self._provider = provider
499         self._boxes = dict()
500         self.graphical_info = GraphicalInfo(str(self._guid))
501
502         metadata = Metadata(provider.testbed_id, provider.testbed_version)
503         for attr in metadata.testbed_attributes().attributes:
504             self.add_attribute(attr.name, attr.help, attr.type, attr.value, 
505                     attr.range, attr.allowed, attr.flags, 
506                     attr.validation_function)
507
508     @property
509     def guid(self):
510         return self._guid
511
512     @property
513     def provider(self):
514         return self._provider
515
516     @property
517     def boxes(self):
518         return self._boxes.values()
519
520     def box(self, guid):
521         return self._boxes[guid] if guid in self._boxes else None
522
523     def create(self, factory_id):
524         guid = self._guid_generator.next()
525         factory = self._provider.factory(factory_id)
526         box = factory.create(guid, self)
527         self._boxes[guid] = box
528         return box
529
530     def delete(self, guid):
531         box = self._boxes[guid]
532         del self._boxes[guid]
533         box.destroy()
534
535     def destroy(self):
536         for guid, box in self._boxes.iteritems():
537             box.destroy()
538         self._boxes = None
539
540 class ExperimentDescription(object):
541     def __init__(self, guid = 0):
542         self._guid_generator = GuidGenerator(guid)
543         self._testbed_descriptions = dict()
544
545     @property
546     def testbed_descriptions(self):
547         return self._testbed_descriptions.values()
548
549     def to_xml(self):
550         parser = XmlExperimentParser()
551         return parser.to_xml(self)
552
553     def from_xml(self, xml):
554         parser = XmlExperimentParser()
555         parser.from_xml(self, xml)
556
557     def testbed_description(self, guid):
558         return self._testbed_descriptions[guid] \
559                 if guid in self._testbed_descriptions else None
560
561     def box(self, guid):
562         for testbed_description in self._testbed_descriptions.values():
563             box = testbed_description.box(guid)
564             if box: return box
565         return None
566
567     def add_testbed_description(self, provider):
568         testbed_description = TestbedDescription(self._guid_generator, 
569                 provider)
570         guid = testbed_description.guid
571         self._testbed_descriptions[guid] = testbed_description
572         return testbed_description
573
574     def remove_testbed_description(self, testbed_description):
575         guid = testbed_description.guid
576         del self._testbed_descriptions[guid]
577
578     def destroy(self):
579         for testbed_description in self.testbed_descriptions:
580             testbed_description.destroy()
581
582 # TODO: When the experiment xml is passed to the controller to execute it
583 # NetReferences in the xml need to be solved
584 #
585 #targets = re.findall(r"%target:(.*?)%", command)
586 #for target in targets:
587 #   try:
588 #      (family, address, port) = resolve_netref(target, AF_INET, 
589 #          self.server.experiment )
590 #      command = command.replace("%%target:%s%%" % target, address.address)
591 #   except:
592 #       continue
593