Lots of cross-connection fixes, TUN synchronization, etc
[nepi.git] / src / nepi / core / testbed_impl.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from nepi.core import execute
5 from nepi.core.metadata import Metadata
6 from nepi.util import validation
7 from nepi.util.constants import STATUS_UNDETERMINED, TIME_NOW, \
8     TESTBED_STATUS_ZERO, \
9     TESTBED_STATUS_SETUP, \
10     TESTBED_STATUS_CREATED, \
11     TESTBED_STATUS_CONNECTED, \
12     TESTBED_STATUS_CROSS_CONNECTED, \
13     TESTBED_STATUS_CONFIGURED, \
14     TESTBED_STATUS_STARTED, \
15     TESTBED_STATUS_STOPPED
16
17 import collections
18
19 class TestbedController(execute.TestbedController):
20     def __init__(self, testbed_id, testbed_version):
21         super(TestbedController, self).__init__(testbed_id, testbed_version)
22         self._status = TESTBED_STATUS_ZERO
23         # testbed attributes for validation
24         self._attributes = None
25         # element factories for validation
26         self._factories = dict()
27
28         # experiment construction instructions
29         self._create = dict()
30         self._create_set = dict()
31         self._factory_set = dict()
32         self._connect = dict()
33         self._cross_connect = dict()
34         self._add_trace = dict()
35         self._add_address = dict()
36         self._add_route = dict()
37         self._configure = dict()
38
39         # log of set operations
40         self._setlog = dict()
41         # last set operations
42         self._set = dict()
43
44         # testbed element instances
45         self._elements = dict()
46
47         self._metadata = Metadata(self._testbed_id, self._testbed_version)
48         for factory in self._metadata.build_execute_factories():
49             self._factories[factory.factory_id] = factory
50         self._attributes = self._metadata.testbed_attributes()
51         self._root_directory = None
52     
53     @property
54     def root_directory(self):
55         return self._root_directory
56
57     @property
58     def guids(self):
59         return self._create.keys()
60
61     @property
62     def elements(self):
63         return self._elements
64     
65     def _get_factory_id(self, guid):
66         """ Returns the factory ID of the (perhaps not yet) created object """
67         return self._create.get(guid, None)
68
69     def defer_configure(self, name, value):
70         if not self._attributes.has_attribute(name):
71             raise AttributeError("Invalid attribute %s for testbed" % name)
72         # Validation
73         self._attributes.set_attribute_value(name, value)
74         self._configure[name] = value
75
76     def defer_create(self, guid, factory_id):
77         if factory_id not in self._factories:
78             raise AttributeError("Invalid element type %s for testbed version %s" %
79                     (factory_id, self._testbed_version))
80         if guid in self._create:
81             raise AttributeError("Cannot add elements with the same guid: %d" %
82                     guid)
83         self._create[guid] = factory_id
84
85     def defer_create_set(self, guid, name, value):
86         if not guid in self._create:
87             raise RuntimeError("Element guid %d doesn't exist" % guid)
88         factory = self._get_factory(guid)
89         if not factory.box_attributes.has_attribute(name):
90             raise AttributeError("Invalid attribute %s for element type %s" %
91                     (name, factory.factory_id))
92         if not factory.box_attributes.is_attribute_value_valid(name, value):
93             raise AttributeError("Invalid value %s for attribute %s" % \
94                 (value, name))
95         if guid not in self._create_set:
96             self._create_set[guid] = dict()
97         self._create_set[guid][name] = value
98
99     def defer_factory_set(self, guid, name, value):
100         if not guid in self._create:
101             raise RuntimeError("Element guid %d doesn't exist" % guid)
102         factory = self._get_factory(guid)
103         if not factory.has_attribute(name):
104             raise AttributeError("Invalid attribute %s for element type %s" %
105                     (name, factory.factory_id))
106         if not factory.is_attribute_value_valid(name, value):
107             raise AttributeError("Invalid value %s for attribute %s" % \
108                 (value, name))
109         if guid not in self._factory_set:
110             self._factory_set[guid] = dict()
111         self._factory_set[guid][name] = value
112
113     def defer_connect(self, guid1, connector_type_name1, guid2, 
114             connector_type_name2):
115         factory1 = self._get_factory(guid1)
116         factory_id2 = self._create[guid2]
117         count = self._get_connection_count(guid1, connector_type_name1)
118         connector_type = factory1.connector_type(connector_type_name1)
119         connector_type.can_connect(self._testbed_id, factory_id2, 
120                 connector_type_name2, count, False)
121         if not guid1 in self._connect:
122             self._connect[guid1] = dict()
123         if not connector_type_name1 in self._connect[guid1]:
124              self._connect[guid1][connector_type_name1] = dict()
125         self._connect[guid1][connector_type_name1][guid2] = \
126                connector_type_name2
127         if not guid2 in self._connect:
128             self._connect[guid2] = dict()
129         if not connector_type_name2 in self._connect[guid2]:
130              self._connect[guid2][connector_type_name2] = dict()
131         self._connect[guid2][connector_type_name2][guid1] = \
132                 connector_type_name1
133
134     def defer_cross_connect(self, guid, connector_type_name, cross_guid, 
135             cross_testbed_guid, cross_testbed_id, cross_factory_id, 
136             cross_connector_type_name):
137         factory = self._get_factory(guid)
138         count = self._get_connection_count(guid, connector_type_name)
139         connector_type = factory.connector_type(connector_type_name)
140         connector_type.can_connect(cross_testbed_id, cross_factory_id, 
141                 cross_connector_type_name, count, True)
142         if not guid in self._cross_connect:
143             self._cross_connect[guid] = dict()
144         if not connector_type_name in self._cross_connect[guid]:
145              self._cross_connect[guid][connector_type_name] = dict()
146         self._cross_connect[guid][connector_type_name] = \
147                 (cross_guid, cross_testbed_guid, cross_testbed_id, 
148                 cross_factory_id, cross_connector_type_name)
149
150     def defer_add_trace(self, guid, trace_id):
151         if not guid in self._create:
152             raise RuntimeError("Element guid %d doesn't exist" % guid)
153         factory = self._get_factory(guid)
154         if not trace_id in factory.traces:
155             raise RuntimeError("Element type '%s' has no trace '%s'" %
156                     (factory.factory_id, trace_id))
157         if not guid in self._add_trace:
158             self._add_trace[guid] = list()
159         self._add_trace[guid].append(trace_id)
160
161     def defer_add_address(self, guid, address, netprefix, broadcast):
162         if not guid in self._create:
163             raise RuntimeError("Element guid %d doesn't exist" % guid)
164         factory = self._get_factory(guid)
165         if not factory.allow_addresses:
166             raise RuntimeError("Element type '%s' doesn't support addresses" %
167                     factory.factory_id)
168             max_addresses = 1 # TODO: MAKE THIS PARAMETRIZABLE
169         if guid in self._add_address:
170             count_addresses = len(self._add_address[guid])
171             if max_addresses == count_addresses:
172                 raise RuntimeError("Element guid %d of type '%s' can't accept \
173                         more addresses" % (guid, factory.factory_id))
174         else:
175             self._add_address[guid] = list()
176         self._add_address[guid].append((address, netprefix, broadcast))
177
178     def defer_add_route(self, guid, destination, netprefix, nexthop):
179         if not guid in self._create:
180             raise RuntimeError("Element guid %d doesn't exist" % guid)
181         factory = self._get_factory(guid)
182         if not factory.allow_routes:
183             raise RuntimeError("Element type '%s' doesn't support routes" %
184                     factory.factory_id)
185         if not guid in self._add_route:
186             self._add_route[guid] = list()
187         self._add_route[guid].append((destination, netprefix, nexthop)) 
188
189     def do_setup(self):
190         self._root_directory = self._attributes.\
191             get_attribute_value("rootDirectory")
192         self._status = TESTBED_STATUS_SETUP
193
194     def do_create(self):
195         guids = dict()
196         # order guids (elements) according to factory_id
197         for guid, factory_id in self._create.iteritems():
198             if not factory_id in guids:
199                guids[factory_id] = list()
200             guids[factory_id].append(guid)
201         # create elements following the factory_id order
202         for factory_id in self._metadata.create_order:
203             # omit the factories that have no element to create
204             if factory_id not in guids:
205                 continue
206             factory = self._factories[factory_id]
207             for guid in guids[factory_id]:
208                 factory.create_function(self, guid)
209                 parameters = self._get_parameters(guid)
210                 for name, value in parameters.iteritems():
211                     self.set(guid, name, value)
212         self._status = TESTBED_STATUS_CREATED
213
214     def _do_connect(self, init = True):
215         for guid1, connections in self._connect.iteritems():
216             factory1 = self._get_factory(guid1)
217             for connector_type_name1, connections2 in connections.iteritems():
218                 connector_type1 = factory1.connector_type(connector_type_name1)
219                 for guid2, connector_type_name2 in connections2.iteritems():
220                     factory_id2 = self._create[guid2]
221                     # Connections are executed in a "From -> To" direction only
222                     # This explicitly ignores the "To -> From" (mirror) 
223                     # connections of every connection pair.
224                     if init:
225                         connect_code = connector_type1.connect_to_init_code(
226                                 self._testbed_id, factory_id2, 
227                                 connector_type_name2,
228                                 False)
229                     else:
230                         connect_code = connector_type1.connect_to_compl_code(
231                                 self._testbed_id, factory_id2, 
232                                 connector_type_name2,
233                                 False)
234                     if connect_code:
235                         connect_code(self, guid1, guid2)
236
237     def do_connect_init(self):
238         self._do_connect()
239
240     def do_connect_compl(self):
241         self._do_connect(init = False)
242         self._status = TESTBED_STATUS_CONNECTED
243
244     def do_preconfigure(self):
245         guids = dict()
246         # order guids (elements) according to factory_id
247         for guid, factory_id in self._create.iteritems():
248             if not factory_id in guids:
249                guids[factory_id] = list()
250             guids[factory_id].append(guid)
251         # configure elements following the factory_id order
252         for factory_id in self._metadata.preconfigure_order:
253             # omit the factories that have no element to create
254             if factory_id not in guids:
255                 continue
256             factory = self._factories[factory_id]
257             if not factory.preconfigure_function:
258                 continue
259             for guid in guids[factory_id]:
260                 factory.preconfigure_function(self, guid)
261
262     def do_configure(self):
263         guids = dict()
264         # order guids (elements) according to factory_id
265         for guid, factory_id in self._create.iteritems():
266             if not factory_id in guids:
267                guids[factory_id] = list()
268             guids[factory_id].append(guid)
269         # configure elements following the factory_id order
270         for factory_id in self._metadata.configure_order:
271             # omit the factories that have no element to create
272             if factory_id not in guids:
273                 continue
274             factory = self._factories[factory_id]
275             if not factory.configure_function:
276                 continue
277             for guid in guids[factory_id]:
278                 factory.configure_function(self, guid)
279         self._status = TESTBED_STATUS_CONFIGURED
280
281     def _do_cross_connect(self, cross_data, init = True):
282         for guid, cross_connections in self._cross_connect.iteritems():
283             factory = self._get_factory(guid)
284             for connector_type_name, cross_connection in \
285                     cross_connections.iteritems():
286                 connector_type = factory.connector_type(connector_type_name)
287                 (cross_guid, cross_testbed_guid, cross_testbed_id,
288                     cross_factory_id, cross_connector_type_name) = cross_connection
289                 if init:
290                     connect_code = connector_type.connect_to_init_code(
291                         cross_testbed_id, cross_factory_id, 
292                         cross_connector_type_name,
293                         True)
294                 else:
295                     connect_code = connector_type.connect_to_compl_code(
296                         cross_testbed_id, cross_factory_id, 
297                         cross_connector_type_name,
298                         True)
299                 if connect_code:
300                     elem_cross_data = cross_data[cross_testbed_guid][cross_guid]
301                     connect_code(self, guid, elem_cross_data)       
302
303     def do_cross_connect_init(self, cross_data):
304         self._do_cross_connect(cross_data)
305
306     def do_cross_connect_compl(self, cross_data):
307         self._do_cross_connect(cross_data, init = False)
308         self._status = TESTBED_STATUS_CROSS_CONNECTED
309
310     def set(self, guid, name, value, time = TIME_NOW):
311         if not guid in self._create:
312             raise RuntimeError("Element guid %d doesn't exist" % guid)
313         factory = self._get_factory(guid)
314         if not factory.box_attributes.has_attribute(name):
315             raise AttributeError("Invalid attribute %s for element type %s" %
316                     (name, factory.factory_id))
317         if self._status > TESTBED_STATUS_STARTED and \
318                 factory.box_attributes.is_attribute_design_only(name):
319             raise AttributeError("Attribute %s can only be modified during experiment design" % name)
320         if not factory.box_attributes.is_attribute_value_valid(name, value):
321             raise AttributeError("Invalid value %s for attribute %s" % \
322                     (value, name))
323         if guid not in self._set:
324             self._set[guid] = dict()
325             self._setlog[guid] = dict()
326         if time not in self._setlog[guid]:
327             self._setlog[guid][time] = dict()
328         self._setlog[guid][time][name] = value
329         self._set[guid][name] = value
330
331     def get(self, guid, name, time = TIME_NOW):
332         """
333         gets an attribute from box definitions if available. 
334         Throws KeyError if the GUID wasn't created
335         through the defer_create interface, and AttributeError if the
336         attribute isn't available (doesn't exist or is design-only)
337         """
338         if not guid in self._create:
339             raise KeyError, "Element guid %d doesn't exist" % guid
340         factory = self._get_factory(guid)
341         if not factory.box_attributes.has_attribute(name):
342             raise AttributeError, "Invalid attribute %s for element type %s" % \
343             (name, factory.factory_id)
344         if guid in self._set and name in self._set[guid]:
345             return self._set[guid][name]
346         if guid in self._create_set and name in self._create_set[guid]:
347             return self._create_set[guid][name]
348         return factory.box_attributes.get_attribute_value(name)
349
350     def get_route(self, guid, index, attribute):
351         """
352         returns information given to defer_add_route.
353         
354         Raises AttributeError if an invalid attribute is requested
355             or if the indexed routing rule does not exist.
356         
357         Raises KeyError if the GUID has not been seen by
358             defer_add_route
359         """
360         ATTRIBUTES = ['Destination', 'NetPrefix', 'NextHop']
361         
362         if attribute not in ATTRIBUTES:
363             raise AttributeError, "Attribute %r invalid for addresses of %r" % (attribute, guid)
364         
365         attribute_index = ATTRIBUTES.index(attribute)
366         
367         routes = self._add_route.get(guid)
368         if not routes:
369             raise KeyError, "GUID %r not found in %s" % (guid, self._testbed_id)
370        
371         index = int(index)
372         if not (0 <= index < len(addresses)):
373             raise AttributeError, "GUID %r at %s does not have a routing entry #%s" % (
374                 guid, self._testbed_id, index)
375         
376         return routes[index][attribute_index]
377
378     def get_address(self, guid, index, attribute='Address'):
379         """
380         returns information given to defer_add_address
381         
382         Raises AttributeError if an invalid attribute is requested
383             or if the indexed routing rule does not exist.
384         
385         Raises KeyError if the GUID has not been seen by
386             defer_add_address
387         """
388         ATTRIBUTES = ['Address', 'NetPrefix', 'Broadcast']
389         
390         if attribute not in ATTRIBUTES:
391             raise AttributeError, "Attribute %r invalid for addresses of %r" % (attribute, guid)
392         
393         attribute_index = ATTRIBUTES.index(attribute)
394         
395         addresses = self._add_address.get(guid)
396         if not addresses:
397             raise KeyError, "GUID %r not found in %s" % (guid, self._testbed_id)
398         
399         index = int(index)
400         if not (0 <= index < len(addresses)):
401             raise AttributeError, "GUID %r at %s does not have an address #%s" % (
402                 guid, self._testbed_id, index)
403         
404         return addresses[index][attribute_index]
405
406     def get_attribute_list(self, guid):
407         factory = self._get_factory(guid)
408         attribute_list = list()
409         return factory.box_attributes.attributes_list
410
411     def start(self, time = TIME_NOW):
412         # Plan everything
413         #  - group by factory_id
414         #  - enqueue task callables
415         plan = collections.defaultdict(list)
416         
417         for guid, factory_id in self._create.iteritems():
418             factory = self._factories[factory_id]
419             start_function = factory.start_function
420             if start_function:
421                 plan[factory_id].append((start_function, guid))
422
423         # Execute plan, following the factory_id order
424         for factory_id in self._metadata.start_order:
425             if factory_id in plan:
426                 for start_function, guid in plan[factory_id]:
427                     start_function(self, guid)
428         
429         self._status = TESTBED_STATUS_STARTED
430
431     #action: NotImplementedError
432
433     def stop(self, time = TIME_NOW):
434         for guid, factory_id in self._create.iteritems():
435             factory = self._factories[factory_id]
436             stop_function = factory.stop_function
437             if stop_function:
438                 stop_function(self, guid)
439         self._status = TESTBED_STATUS_STOPPED
440
441     def status(self, guid = None):
442         if not guid:
443             return self._status
444         if not guid in self._create:
445             raise RuntimeError("Element guid %d doesn't exist" % guid)
446         factory = self._get_factory(guid)
447         status_function = factory.status_function
448         if status_function:
449             return status_function(self, guid)
450         return STATUS_UNDETERMINED
451
452     def trace(self, guid, trace_id, attribute='value'):
453         if attribute == 'value':
454             fd = open("%s" % self.trace_filename(guid, trace_id), "r")
455             content = fd.read()
456             fd.close()
457         elif attribute == 'path':
458             content = self.trace_filename(guid, trace_id)
459         else:
460             content = None
461         return content
462
463     def trace_filename(self, guid, trace_id):
464         """
465         Return a trace's file path, for TestbedController's default 
466         implementation of trace()
467         """
468         raise NotImplementedError
469
470     #shutdown: NotImplementedError
471
472     def get_connected(self, guid, connector_type_name, 
473             other_connector_type_name):
474         """searchs the connected elements for the specific connector_type_name 
475         pair"""
476         if guid not in self._connect:
477             return []
478         # all connections for all connectors for guid
479         all_connections = self._connect[guid]
480         if connector_type_name not in all_connections:
481             return []
482         # all connections for the specific connector
483         connections = all_connections[connector_type_name]
484         specific_connections = [otr_guid for otr_guid, otr_connector_type_name \
485                 in connections.iteritems() if \
486                 otr_connector_type_name == other_connector_type_name]
487         return specific_connections
488
489     def _get_connection_count(self, guid, connection_type_name):
490         count = 0
491         cross_count = 0
492         if guid in self._connect and connection_type_name in \
493                 self._connect[guid]:
494             count = len(self._connect[guid][connection_type_name])
495         if guid in self._cross_connect and connection_type_name in \
496                 self._cross_connect[guid]:
497             cross_count = len(self._cross_connect[guid][connection_type_name])
498         return count + cross_count
499
500     def _get_traces(self, guid):
501         return [] if guid not in self._add_trace else self._add_trace[guid]
502
503     def _get_parameters(self, guid):
504         return dict() if guid not in self._create_set else \
505                 self._create_set[guid]
506
507     def _get_factory(self, guid):
508         factory_id = self._create[guid]
509         return self._factories[factory_id]
510