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