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