Lots of cross-connection fixes, TUN synchronization, etc
[nepi.git] / src / nepi / core / testbed_impl.py
index e0608bc..320f7b0 100644 (file)
@@ -4,12 +4,22 @@
 from nepi.core import execute
 from nepi.core.metadata import Metadata
 from nepi.util import validation
-from nepi.util.constants import AF_INET, AF_INET6, STATUS_UNDETERMINED, TIME_NOW
+from nepi.util.constants import STATUS_UNDETERMINED, TIME_NOW, \
+    TESTBED_STATUS_ZERO, \
+    TESTBED_STATUS_SETUP, \
+    TESTBED_STATUS_CREATED, \
+    TESTBED_STATUS_CONNECTED, \
+    TESTBED_STATUS_CROSS_CONNECTED, \
+    TESTBED_STATUS_CONFIGURED, \
+    TESTBED_STATUS_STARTED, \
+    TESTBED_STATUS_STOPPED
+
+import collections
 
 class TestbedController(execute.TestbedController):
     def __init__(self, testbed_id, testbed_version):
         super(TestbedController, self).__init__(testbed_id, testbed_version)
-        self._started = False
+        self._status = TESTBED_STATUS_ZERO
         # testbed attributes for validation
         self._attributes = None
         # element factories for validation
@@ -27,6 +37,8 @@ class TestbedController(execute.TestbedController):
         self._configure = dict()
 
         # log of set operations
+        self._setlog = dict()
+        # last set operations
         self._set = dict()
 
         # testbed element instances
@@ -36,6 +48,11 @@ class TestbedController(execute.TestbedController):
         for factory in self._metadata.build_execute_factories():
             self._factories[factory.factory_id] = factory
         self._attributes = self._metadata.testbed_attributes()
+        self._root_directory = None
+    
+    @property
+    def root_directory(self):
+        return self._root_directory
 
     @property
     def guids(self):
@@ -51,29 +68,30 @@ class TestbedController(execute.TestbedController):
 
     def defer_configure(self, name, value):
         if not self._attributes.has_attribute(name):
-            raise RuntimeError("Invalid attribute %s for testbed" % name)
+            raise AttributeError("Invalid attribute %s for testbed" % name)
         # Validation
         self._attributes.set_attribute_value(name, value)
         self._configure[name] = value
 
     def defer_create(self, guid, factory_id):
         if factory_id not in self._factories:
-            raise RuntimeError("Invalid element type %s for testbed version %s" %
+            raise AttributeError("Invalid element type %s for testbed version %s" %
                     (factory_id, self._testbed_version))
         if guid in self._create:
-            raise RuntimeError("Cannot add elements with the same guid: %d" %
+            raise AttributeError("Cannot add elements with the same guid: %d" %
                     guid)
         self._create[guid] = factory_id
 
     def defer_create_set(self, guid, name, value):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.box_attributes.has_attribute(name):
-            raise RuntimeError("Invalid attribute %s for element type %s" %
-                    (name, factory_id))
-        factory.box_attributes.set_attribute_value(name, value)
+            raise AttributeError("Invalid attribute %s for element type %s" %
+                    (name, factory.factory_id))
+        if not factory.box_attributes.is_attribute_value_valid(name, value):
+            raise AttributeError("Invalid value %s for attribute %s" % \
+                (value, name))
         if guid not in self._create_set:
             self._create_set[guid] = dict()
         self._create_set[guid][name] = value
@@ -81,25 +99,25 @@ class TestbedController(execute.TestbedController):
     def defer_factory_set(self, guid, name, value):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.has_attribute(name):
-            raise RuntimeError("Invalid attribute %s for element type %s" %
-                    (name, factory_id))
-        factory.set_attribute_value(name, value)
+            raise AttributeError("Invalid attribute %s for element type %s" %
+                    (name, factory.factory_id))
+        if not factory.is_attribute_value_valid(name, value):
+            raise AttributeError("Invalid value %s for attribute %s" % \
+                (value, name))
         if guid not in self._factory_set:
             self._factory_set[guid] = dict()
         self._factory_set[guid][name] = value
 
     def defer_connect(self, guid1, connector_type_name1, guid2, 
             connector_type_name2):
-        factory_id1 = self._create[guid1]
+        factory1 = self._get_factory(guid1)
         factory_id2 = self._create[guid2]
         count = self._get_connection_count(guid1, connector_type_name1)
-        factory1 = self._factories[factory_id1]
         connector_type = factory1.connector_type(connector_type_name1)
         connector_type.can_connect(self._testbed_id, factory_id2, 
-                connector_type_name2, count)
+                connector_type_name2, count, False)
         if not guid1 in self._connect:
             self._connect[guid1] = dict()
         if not connector_type_name1 in self._connect[guid1]:
@@ -114,29 +132,28 @@ class TestbedController(execute.TestbedController):
                 connector_type_name1
 
     def defer_cross_connect(self, guid, connector_type_name, cross_guid, 
-            cross_testbed_id, cross_factory_id, cross_connector_type_name):
-        factory_id = self._create[guid]
+            cross_testbed_guid, cross_testbed_id, cross_factory_id, 
+            cross_connector_type_name):
+        factory = self._get_factory(guid)
         count = self._get_connection_count(guid, connector_type_name)
-        factory = self._factories[factory_id]
         connector_type = factory.connector_type(connector_type_name)
         connector_type.can_connect(cross_testbed_id, cross_factory_id, 
-                cross_connector_type_name, count, must_cross = True)
+                cross_connector_type_name, count, True)
         if not guid in self._cross_connect:
             self._cross_connect[guid] = dict()
         if not connector_type_name in self._cross_connect[guid]:
              self._cross_connect[guid][connector_type_name] = dict()
         self._cross_connect[guid][connector_type_name] = \
-                (cross_guid, cross_testbed_id, cross_factory_id, 
-                        cross_connector_type_name)
+                (cross_guid, cross_testbed_guid, cross_testbed_id, 
+                cross_factory_id, cross_connector_type_name)
 
     def defer_add_trace(self, guid, trace_id):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not trace_id in factory.traces:
             raise RuntimeError("Element type '%s' has no trace '%s'" %
-                    (factory_id, trace_id))
+                    (factory.factory_id, trace_id))
         if not guid in self._add_trace:
             self._add_trace[guid] = list()
         self._add_trace[guid].append(trace_id)
@@ -144,17 +161,16 @@ class TestbedController(execute.TestbedController):
     def defer_add_address(self, guid, address, netprefix, broadcast):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.allow_addresses:
             raise RuntimeError("Element type '%s' doesn't support addresses" %
-                    factory_id)
+                    factory.factory_id)
             max_addresses = 1 # TODO: MAKE THIS PARAMETRIZABLE
         if guid in self._add_address:
             count_addresses = len(self._add_address[guid])
             if max_addresses == count_addresses:
                 raise RuntimeError("Element guid %d of type '%s' can't accept \
-                        more addresses" % (guid, factory_id))
+                        more addresses" % (guid, factory.factory_id))
         else:
             self._add_address[guid] = list()
         self._add_address[guid].append((address, netprefix, broadcast))
@@ -162,16 +178,18 @@ class TestbedController(execute.TestbedController):
     def defer_add_route(self, guid, destination, netprefix, nexthop):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.allow_routes:
             raise RuntimeError("Element type '%s' doesn't support routes" %
-                    factory_id)
+                    factory.factory_id)
         if not guid in self._add_route:
             self._add_route[guid] = list()
         self._add_route[guid].append((destination, netprefix, nexthop)) 
 
-    #do_setup(self): NotImplementedError
+    def do_setup(self):
+        self._root_directory = self._attributes.\
+            get_attribute_value("rootDirectory")
+        self._status = TESTBED_STATUS_SETUP
 
     def do_create(self):
         guids = dict()
@@ -190,17 +208,15 @@ class TestbedController(execute.TestbedController):
                 factory.create_function(self, guid)
                 parameters = self._get_parameters(guid)
                 for name, value in parameters.iteritems():
-                    self.set(TIME_NOW, guid, name, value)
+                    self.set(guid, name, value)
+        self._status = TESTBED_STATUS_CREATED
 
     def _do_connect(self, init = True):
         for guid1, connections in self._connect.iteritems():
-            element1 = self._elements[guid1]
-            factory_id1 = self._create[guid1]
-            factory1 = self._factories[factory_id1]
+            factory1 = self._get_factory(guid1)
             for connector_type_name1, connections2 in connections.iteritems():
                 connector_type1 = factory1.connector_type(connector_type_name1)
                 for guid2, connector_type_name2 in connections2.iteritems():
-                    element2 = self._elements[guid2]
                     factory_id2 = self._create[guid2]
                     # Connections are executed in a "From -> To" direction only
                     # This explicitly ignores the "To -> From" (mirror) 
@@ -208,19 +224,22 @@ class TestbedController(execute.TestbedController):
                     if init:
                         connect_code = connector_type1.connect_to_init_code(
                                 self._testbed_id, factory_id2, 
-                                connector_type_name2)
+                                connector_type_name2,
+                                False)
                     else:
                         connect_code = connector_type1.connect_to_compl_code(
                                 self._testbed_id, factory_id2, 
-                                connector_type_name2)
+                                connector_type_name2,
+                                False)
                     if connect_code:
-                        connect_code(self, element1, element2)
+                        connect_code(self, guid1, guid2)
 
     def do_connect_init(self):
         self._do_connect()
 
     def do_connect_compl(self):
         self._do_connect(init = False)
+        self._status = TESTBED_STATUS_CONNECTED
 
     def do_preconfigure(self):
         guids = dict()
@@ -257,75 +276,80 @@ class TestbedController(execute.TestbedController):
                 continue
             for guid in guids[factory_id]:
                 factory.configure_function(self, guid)
+        self._status = TESTBED_STATUS_CONFIGURED
 
     def _do_cross_connect(self, cross_data, init = True):
         for guid, cross_connections in self._cross_connect.iteritems():
-            element = self._elements[guid]
-            factory_id = self._create[guid]
-            factory = self._factories[factory_id]
+            factory = self._get_factory(guid)
             for connector_type_name, cross_connection in \
                     cross_connections.iteritems():
                 connector_type = factory.connector_type(connector_type_name)
-                (cross_testbed_id, cross_factory_id, 
-                        cross_connector_type_name) = cross_connection
+                (cross_guid, cross_testbed_guid, cross_testbed_id,
+                    cross_factory_id, cross_connector_type_name) = cross_connection
                 if init:
                     connect_code = connector_type.connect_to_init_code(
                         cross_testbed_id, cross_factory_id, 
-                        cross_conector_type_name)
+                        cross_connector_type_name,
+                        True)
                 else:
                     connect_code = connector_type.connect_to_compl_code(
                         cross_testbed_id, cross_factory_id, 
-                        cross_conector_type_name)
+                        cross_connector_type_name,
+                        True)
                 if connect_code:
-                    elem_data_guid = cross_data[cross_testbed_id][cross_guid]
-                    connect_code(self, element, elem_cross_data)       
+                    elem_cross_data = cross_data[cross_testbed_guid][cross_guid]
+                    connect_code(self, guid, elem_cross_data)       
 
     def do_cross_connect_init(self, cross_data):
         self._do_cross_connect(cross_data)
 
     def do_cross_connect_compl(self, cross_data):
         self._do_cross_connect(cross_data, init = False)
+        self._status = TESTBED_STATUS_CROSS_CONNECTED
 
-    def set(self, time, guid, name, value):
+    def set(self, guid, name, value, time = TIME_NOW):
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.box_attributes.has_attribute(name):
-            raise RuntimeError("Invalid attribute %s for element type %s" %
-                    (name, factory_id))
-        if self._started and factory.is_attribute_design_only(name):
-            raise RuntimeError("Attribute %s can only be modified during experiment design" % name)
-        factory.box_attributes.set_attribute_value(name, value)
+            raise AttributeError("Invalid attribute %s for element type %s" %
+                    (name, factory.factory_id))
+        if self._status > TESTBED_STATUS_STARTED and \
+                factory.box_attributes.is_attribute_design_only(name):
+            raise AttributeError("Attribute %s can only be modified during experiment design" % name)
+        if not factory.box_attributes.is_attribute_value_valid(name, value):
+            raise AttributeError("Invalid value %s for attribute %s" % \
+                    (value, name))
         if guid not in self._set:
             self._set[guid] = dict()
-        if time not in self._set[guid]:
-            self._set[guid][time] = dict()
-        self._set[guid][time][name] = value
+            self._setlog[guid] = dict()
+        if time not in self._setlog[guid]:
+            self._setlog[guid][time] = dict()
+        self._setlog[guid][time][name] = value
+        self._set[guid][name] = value
 
-    def box_get(self, time, guid, name):
+    def get(self, guid, name, time = TIME_NOW):
         """
-        Helper for subclasses, gets an attribute from box definitions
-        if available. Throws KeyError if the GUID wasn't created
+        gets an attribute from box definitions if available. 
+        Throws KeyError if the GUID wasn't created
         through the defer_create interface, and AttributeError if the
         attribute isn't available (doesn't exist or is design-only)
         """
         if not guid in self._create:
             raise KeyError, "Element guid %d doesn't exist" % guid
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         if not factory.box_attributes.has_attribute(name):
-            raise AttributeError, "Invalid attribute %s for element type %s" % (name, factory_id)
-        if self._started and factory.is_attribute_design_only(name):
-            raise AttributeError, "Attribute %s can only be queried during experiment design" % name
+            raise AttributeError, "Invalid attribute %s for element type %s" % \
+            (name, factory.factory_id)
+        if guid in self._set and name in self._set[guid]:
+            return self._set[guid][name]
+        if guid in self._create_set and name in self._create_set[guid]:
+            return self._create_set[guid][name]
         return factory.box_attributes.get_attribute_value(name)
 
-    #get: NotImplementedError
-
-    def box_get_route(self, guid, index, attribute):
+    def get_route(self, guid, index, attribute):
         """
-        Helper implementation for get_route, returns information
-        given to defer_add_route.
+        returns information given to defer_add_route.
         
         Raises AttributeError if an invalid attribute is requested
             or if the indexed routing rule does not exist.
@@ -343,17 +367,17 @@ class TestbedController(execute.TestbedController):
         routes = self._add_route.get(guid)
         if not routes:
             raise KeyError, "GUID %r not found in %s" % (guid, self._testbed_id)
-        
+       
+        index = int(index)
         if not (0 <= index < len(addresses)):
             raise AttributeError, "GUID %r at %s does not have a routing entry #%s" % (
                 guid, self._testbed_id, index)
         
         return routes[index][attribute_index]
 
-    def box_get_address(self, guid, index, attribute='Address'):
+    def get_address(self, guid, index, attribute='Address'):
         """
-        Helper implementation for get_address, returns information
-        given to defer_add_address
+        returns information given to defer_add_address
         
         Raises AttributeError if an invalid attribute is requested
             or if the indexed routing rule does not exist.
@@ -372,6 +396,7 @@ class TestbedController(execute.TestbedController):
         if not addresses:
             raise KeyError, "GUID %r not found in %s" % (guid, self._testbed_id)
         
+        index = int(index)
         if not (0 <= index < len(addresses)):
             raise AttributeError, "GUID %r at %s does not have an address #%s" % (
                 guid, self._testbed_id, index)
@@ -379,18 +404,29 @@ class TestbedController(execute.TestbedController):
         return addresses[index][attribute_index]
 
     def get_attribute_list(self, guid):
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         attribute_list = list()
         return factory.box_attributes.attributes_list
 
     def start(self, time = TIME_NOW):
+        # Plan everything
+        #  - group by factory_id
+        #  - enqueue task callables
+        plan = collections.defaultdict(list)
+        
         for guid, factory_id in self._create.iteritems():
             factory = self._factories[factory_id]
             start_function = factory.start_function
             if start_function:
-                start_function(self, guid)
-        self._started = True
+                plan[factory_id].append((start_function, guid))
+
+        # Execute plan, following the factory_id order
+        for factory_id in self._metadata.start_order:
+            if factory_id in plan:
+                for start_function, guid in plan[factory_id]:
+                    start_function(self, guid)
+        
+        self._status = TESTBED_STATUS_STARTED
 
     #action: NotImplementedError
 
@@ -400,12 +436,14 @@ class TestbedController(execute.TestbedController):
             stop_function = factory.stop_function
             if stop_function:
                 stop_function(self, guid)
+        self._status = TESTBED_STATUS_STOPPED
 
-    def status(self, guid):
+    def status(self, guid = None):
+        if not guid:
+            return self._status
         if not guid in self._create:
             raise RuntimeError("Element guid %d doesn't exist" % guid)
-        factory_id = self._create[guid]
-        factory = self._factories[factory_id]
+        factory = self._get_factory(guid)
         status_function = factory.status_function
         if status_function:
             return status_function(self, guid)
@@ -466,3 +504,7 @@ class TestbedController(execute.TestbedController):
         return dict() if guid not in self._create_set else \
                 self._create_set[guid]
 
+    def _get_factory(self, guid):
+        factory_id = self._create[guid]
+        return self._factories[factory_id]
+