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