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