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