Ticket #28: Refactor Box classes to use mixins, and provide read-only routes/addesses
[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.metadata import Metadata
10 from nepi.util import validation
11 from nepi.util.guid import GuidGenerator
12 from nepi.util.graphical_info import GraphicalInfo
13 from nepi.util.parser._xml import XmlExperimentParser
14 import sys
15
16 class ConnectorType(object):
17     def __init__(self, testbed_id, factory_id, name, help, max = -1, min = 0):
18         super(ConnectorType, self).__init__()
19         if max == -1:
20             max = sys.maxint
21         elif max <= 0:
22                 raise RuntimeError(
23              "The maximum number of connections allowed need to be more than 0")
24         if min < 0:
25             raise RuntimeError(
26              "The minimum number of connections allowed needs to be at least 0")
27         # connector_type_id -- univoquely identifies a connector type 
28         # across testbeds
29         self._connector_type_id = (testbed_id.lower(), factory_id.lower(), 
30                 name.lower())
31         # name -- display name for the connector type
32         self._name = name
33         # help -- help text
34         self._help = help
35         # max -- maximum amount of connections that this type support, 
36         # -1 for no limit
37         self._max = max
38         # min -- minimum amount of connections required by this type of connector
39         self._min = min
40         # allowed_connections -- keys in the dictionary correspond to the 
41         # connector_type_id for possible connections. The value indicates if
42         # the connection is allowed accros different testbed instances
43         self._allowed_connections = dict()
44
45     @property
46     def connector_type_id(self):
47         return self._connector_type_id
48
49     @property
50     def help(self):
51         return self._help
52
53     @property
54     def name(self):
55         return self._name
56
57     @property
58     def max(self):
59         return self._max
60
61     @property
62     def min(self):
63         return self._min
64
65     def add_allowed_connection(self, testbed_id, factory_id, name, can_cross):
66         self._allowed_connections[(testbed_id.lower(), 
67             factory_id.lower(), name.lower())] = can_cross
68
69     def can_connect(self, connector_type_id, testbed_guid1, testbed_guid2):
70         if not connector_type_id in self._allowed_connections.keys():
71             return False
72         can_cross = self._allowed_connections[connector_type_id]
73         return can_cross or (testbed_guid1 == testbed_guid2)
74
75 class Connector(object):
76     """A Connector sepcifies the connection points in an Object"""
77     def __init__(self, box, connector_type):
78         super(Connector, self).__init__()
79         self._box = box
80         self._connector_type = connector_type
81         self._connections = list()
82
83     @property
84     def box(self):
85         return self._box
86
87     @property
88     def connector_type(self):
89         return self._connector_type
90
91     @property
92     def connections(self):
93         return self._connections
94
95     def is_full(self):
96         """Return True if the connector has the maximum number of connections
97         """
98         return len(self.connections) == self.connector_type.max
99
100     def is_complete(self):
101         """Return True if the connector has the minimum number of connections
102         """
103         return len(self.connections) >= self.connector_type.min
104
105     def is_connected(self, connector):
106         return connector in self._connections
107
108     def connect(self, connector):
109         if not self.can_connect(connector) or not connector.can_connect(self):
110             raise RuntimeError("Could not connect.")
111         self._connections.append(connector)
112         connector._connections.append(self)
113
114     def disconnect(self, connector):
115         if connector not in self._connections or\
116                 self not in connector._connections:
117                 raise RuntimeError("Could not disconnect.")
118         self._connections.remove(connector)
119         connector._connections.remove(self)
120
121     def can_connect(self, connector):
122         if self.is_full() or connector.is_full():
123             return False
124         if self.is_connected(connector):
125             return False
126         connector_type_id = connector.connector_type.connector_type_id
127         testbed_guid1 = self.box.testbed_guid
128         testbed_guid2 = connector.box.testbed_guid
129         return self.connector_type.can_connect(connector_type_id, 
130                 testbed_guid1, testbed_guid2)
131
132     def destroy(self):
133         for connector in self.connections:
134             self.disconnect(connector)
135         self._box = self._connectors = None
136
137 class Trace(AttributesMap):
138     def __init__(self, trace_id, help, enabled = False):
139         super(Trace, self).__init__()
140         self._trace_id = trace_id
141         self._help = help       
142         self.enabled = enabled
143     
144     @property
145     def trace_id(self):
146         return self._trace_id
147
148     @property
149     def help(self):
150         return self._help
151
152 class Address(AttributesMap):
153     def __init__(self):
154         super(Address, self).__init__()
155         self.add_attribute(name = "AutoConfigure", 
156                 help = "If set, this address will automatically be assigned", 
157                 type = Attribute.BOOL,
158                 value = False,
159                 flags = Attribute.DesignOnly,
160                 validation_function = validation.is_bool)
161         self.add_attribute(name = "Address",
162                 help = "Address number", 
163                 type = Attribute.STRING,
164                 flags = Attribute.HasNoDefaultValue,
165                 validation_function = validation.is_ip_address)
166         self.add_attribute(name = "NetPrefix",
167                 help = "Network prefix for the address", 
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 = "Broadcast",
174                 help = "Broadcast address", 
175                 type = Attribute.STRING,
176                 validation_function = validation.is_ip4_address)
177                 
178 class Route(AttributesMap):
179     def __init__(self):
180         super(Route, self).__init__()
181         self.add_attribute(name = "Destination", 
182                 help = "Network destintation",
183                 type = Attribute.STRING, 
184                 validation_function = validation.is_ip_address)
185         self.add_attribute(name = "NetPrefix",
186                 help = "Network destination prefix", 
187                 type = Attribute.INTEGER, 
188                 prefix_range = (0,128),
189                 value = 24,
190                 flags = Attribute.HasNoDefaultValue,
191                 validation_function = validation.is_integer)
192         self.add_attribute(name = "NextHop",
193                 help = "Address for the next hop", 
194                 type = Attribute.STRING,
195                 flags = Attribute.HasNoDefaultValue,
196                 validation_function = validation.is_ip_address)
197
198 class Box(AttributesMap):
199     def __init__(self, guid, factory, testbed_guid, container = None):
200         super(Box, self).__init__()
201         # guid -- global unique identifier
202         self._guid = guid
203         # factory_id -- factory identifier or name
204         self._factory_id = factory.factory_id
205         # testbed_guid -- parent testbed guid
206         self._testbed_guid = testbed_guid
207         # container -- boxes can be nested inside other 'container' boxes
208         self._container = container
209         # traces -- list of available traces for the box
210         self._traces = dict()
211         # connectors -- list of available connectors for the box
212         self._connectors = dict()
213         # factory_attributes -- factory attributes for box construction
214         self._factory_attributes = dict()
215         # graphical_info -- GUI position information
216         self.graphical_info = GraphicalInfo(str(self._guid))
217
218         for connector_type in factory.connector_types:
219             connector = Connector(self, connector_type)
220             self._connectors[connector_type.name] = connector
221         for trace in factory.traces:
222             tr = Trace(trace.trace_id, trace.help, trace.enabled)
223             self._traces[trace.trace_id] = tr
224         for attr in factory.box_attributes.attributes:
225             self.add_attribute(attr.name, attr.help, attr.type, attr.value, 
226                     attr.range, attr.allowed, attr.flags, 
227                     attr.validation_function)
228         for attr in factory.attributes:
229             if attr.modified:
230                 self._factory_attributes[attr.name] = attr.value
231
232     @property
233     def guid(self):
234         return self._guid
235
236     @property
237     def factory_id(self):
238         return self._factory_id
239
240     @property
241     def testbed_guid(self):
242         return self._testbed_guid
243
244     @property
245     def container(self):
246         return self._container
247
248     @property
249     def connectors(self):
250         return self._connectors.values()
251
252     @property
253     def traces(self):
254         return self._traces.values()
255
256     @property
257     def traces_name(self):
258         return self._traces.keys()
259
260     @property
261     def factory_attributes(self):
262         return self._factory_attributes
263
264     @property
265     def addresses(self):
266         return []
267
268     @property
269     def routes(self):
270         return []
271
272     def trace_help(self, trace_id):
273         return self._traces[trace_id].help
274
275     def enable_trace(self, trace_id):
276         self._traces[trace_id].enabled = True
277
278     def disable_trace(self, trace_id):
279         self._traces[trace_id].enabled = False
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._route.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._box_attributes = AttributesMap()
399         
400         if not self._has_addresses and not self._has_routes:
401             self._factory = Box
402         else:
403             addresses = 'w' if self._allow_addresses else ('r' if self._has_addresses else '-')
404             routes    = 'w' if self._allow_routes else ('r' if self._has_routes else '-')
405             key = addresses+routes
406             
407             if key in self._box_class_cache:
408                 self._factory = self._box_class_cache[key]
409             else:
410                 # Create base class
411                 class _factory(Box):
412                     def __init__(self, guid, factory, testbed_guid, container = None):
413                         super(_factory, self).__init__(guid, factory, testbed_guid, container)
414                 
415                 # Add mixins, one by one
416                 if allow_addresses:
417                     MixIn(_factory, UserAddressableMixin)
418                 elif has_addresses:
419                     MixIn(_factory, AddressableMixin)
420                     
421                 if allow_routes:
422                     MixIn(_factory, UserRoutableMixin)
423                 elif has_routes:
424                     MixIn(_factory, RoutableMixin)
425                 
426                 # Put into cache
427                 self._box_class_cache[key] = self._factory = _factory
428
429     @property
430     def factory_id(self):
431         return self._factory_id
432
433     @property
434     def allow_addresses(self):
435         return self._allow_addresses
436
437     @property
438     def allow_routes(self):
439         return self._allow_routes
440
441     @property
442     def has_addresses(self):
443         return self._has_addresses
444
445     @property
446     def has_routes(self):
447         return self._has_routes
448
449     @property
450     def help(self):
451         return self._help
452
453     @property
454     def category(self):
455         return self._category
456
457     @property
458     def connector_types(self):
459         return self._connector_types
460
461     @property
462     def traces(self):
463         return self._traces
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_box_attribute(self, name, help, type, value = None, range = None,
477         allowed = None, flags = Attribute.NoFlags, validation_function = None):
478         self._box_attributes.add_attribute(name, help, type, value, range,
479                 allowed, flags, validation_function)
480
481     def create(self, guid, testbed_description):
482         return self._factory(guid, self, testbed_description.guid)
483
484     def destroy(self):
485         super(Factory, self).destroy()
486         self._connector_types = None
487
488 class FactoriesProvider(object):
489     def __init__(self, testbed_id, testbed_version):
490         super(FactoriesProvider, self).__init__()
491         self._testbed_id = testbed_id
492         self._testbed_version = testbed_version
493         self._factories = dict()
494
495         metadata = Metadata(testbed_id, testbed_version) 
496         for factory in metadata.build_design_factories():
497             self.add_factory(factory)
498
499     @property
500     def testbed_id(self):
501         return self._testbed_id
502
503     @property
504     def testbed_version(self):
505         return self._testbed_version
506
507     @property
508     def factories(self):
509         return self._factories.values()
510
511     def factory(self, factory_id):
512         return self._factories[factory_id]
513
514     def add_factory(self, factory):
515         self._factories[factory.factory_id] = factory
516
517     def remove_factory(self, factory_id):
518         del self._factories[factory_id]
519
520 class TestbedDescription(AttributesMap):
521     def __init__(self, guid_generator, provider):
522         super(TestbedDescription, self).__init__()
523         self._guid_generator = guid_generator
524         self._guid = guid_generator.next()
525         self._provider = provider
526         self._boxes = dict()
527         self.graphical_info = GraphicalInfo(str(self._guid))
528
529         metadata = Metadata(provider.testbed_id, provider.testbed_version)
530         for attr in metadata.testbed_attributes().attributes:
531             self.add_attribute(attr.name, attr.help, attr.type, attr.value, 
532                     attr.range, attr.allowed, attr.flags, 
533                     attr.validation_function)
534
535     @property
536     def guid(self):
537         return self._guid
538
539     @property
540     def provider(self):
541         return self._provider
542
543     @property
544     def boxes(self):
545         return self._boxes.values()
546
547     def box(self, guid):
548         return self._boxes[guid] if guid in self._boxes else None
549
550     def create(self, factory_id):
551         guid = self._guid_generator.next()
552         factory = self._provider.factory(factory_id)
553         box = factory.create(guid, self)
554         self._boxes[guid] = box
555         return box
556
557     def delete(self, guid):
558         box = self._boxes[guid]
559         del self._boxes[guid]
560         box.destroy()
561
562     def destroy(self):
563         for guid, box in self._boxes.iteritems():
564             box.destroy()
565         self._boxes = None
566
567 class ExperimentDescription(object):
568     def __init__(self, guid = 0):
569         self._guid_generator = GuidGenerator(guid)
570         self._testbed_descriptions = dict()
571
572     @property
573     def testbed_descriptions(self):
574         return self._testbed_descriptions.values()
575
576     def to_xml(self):
577         parser = XmlExperimentParser()
578         return parser.to_xml(self)
579
580     def from_xml(self, xml):
581         parser = XmlExperimentParser()
582         parser.from_xml(self, xml)
583
584     def testbed_description(self, guid):
585         return self._testbed_descriptions[guid] \
586                 if guid in self._testbed_descriptions else None
587
588     def box(self, guid):
589         for testbed_description in self._testbed_descriptions.values():
590             box = testbed_description.box(guid)
591             if box: return box
592         return None
593
594     def add_testbed_description(self, provider):
595         testbed_description = TestbedDescription(self._guid_generator, 
596                 provider)
597         guid = testbed_description.guid
598         self._testbed_descriptions[guid] = testbed_description
599         return testbed_description
600
601     def remove_testbed_description(self, testbed_description):
602         guid = testbed_description.guid
603         del self._testbed_descriptions[guid]
604
605     def destroy(self):
606         for testbed_description in self.testbed_descriptions:
607             testbed_description.destroy()
608
609 # TODO: When the experiment xml is passed to the controller to execute it
610 # NetReferences in the xml need to be solved
611 #
612 #targets = re.findall(r"%target:(.*?)%", command)
613 #for target in targets:
614 #   try:
615 #      (family, address, port) = resolve_netref(target, AF_INET, 
616 #          self.server.experiment )
617 #      command = command.replace("%%target:%s%%" % target, address.address)
618 #   except:
619 #       continue
620