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