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