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