added tags to boxes. For now only one tag: MOBILE
[nepi.git] / src / nepi / core / metadata.py
index 2645b8d..c8015cd 100644 (file)
@@ -3,7 +3,12 @@
 
 from nepi.core.attributes import Attribute, AttributesMap
 import sys
+import getpass
 from nepi.util import validation
+from nepi.util.constants import ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP, DeploymentConfiguration
+
+# Attribute categories
+CATEGORY_DEPLOYMENT = "Deployment"
 
 class VersionedMetadataInfo(object):
     @property
@@ -24,7 +29,9 @@ class VersionedMetadataInfo(object):
         dict({
             "from": (testbed_id1, factory_id1, connector_type_name1),
             "to": (testbed_id2, factory_id2, connector_type_name2),
-            "code": connection function to invoke upon connection creation
+            "init_code": connection function to invoke for connection initiation
+            "compl_code": connection function to invoke for connection 
+                completion
             "can_cross": whether the connection can be done across testbed 
                             instances
          }),
@@ -43,6 +50,7 @@ class VersionedMetadataInfo(object):
                 "allowed": array of posible values,
                 "flags": attributes flags,
                 "validation_function": validation function for the attribute
+                "category": category for the attribute
             })
         """
         raise NotImplementedError
@@ -80,12 +88,32 @@ class VersionedMetadataInfo(object):
         """
         return self.configure_order
 
+    @property
+    def prestart_order(self):
+        """ list of factory ids that indicates the order in which the elements
+        should be prestart-configured.
+        
+        Default: same as configure_order
+        """
+        return self.configure_order
+
+    @property
+    def start_order(self):
+        """ list of factory ids that indicates the order in which the elements
+        should be started.
+        
+        Default: same as configure_order
+        """
+        return self.configure_order
+
     @property
     def factories_info(self):
         """ dictionary of dictionaries of factory specific information
             factory_id: dict({
                 "allow_addresses": whether the box allows adding IP addresses,
                 "allow_routes": wether the box allows adding routes,
+                "has_addresses": whether the box allows obtaining IP addresses,
+                "has_routes": wether the box allows obtaining routes,
                 "help": help text,
                 "category": category the element belongs to,
                 "create_function": function for element instantiation,
@@ -96,9 +124,17 @@ class VersionedMetadataInfo(object):
                     (just after connections are made, 
                     just before netrefs are resolved)
                 "configure_function": function for element configuration,
+                "prestart_function": function for pre-start
+                    element configuration (just before starting applications),
+                    useful for synchronization of background setup tasks or
+                    lazy instantiation or configuration of attributes
+                    that require connection/cross-connection state before
+                    being created.
+                    After this point, all applications should be able to run.
                 "factory_attributes": list of references to attribute_ids,
                 "box_attributes": list of regerences to attribute_ids,
                 "traces": list of references to trace_id
+                "tags": list of references to tag_id
                 "connector_types": list of references to connector_types
            })
         """
@@ -116,6 +152,7 @@ class VersionedMetadataInfo(object):
                 "allowed": array of posible values,
                 "flags": attributes flags,
                 "validation_function": validation function for the attribute
+                "category": category for the attribute
              })
             ]
         """
@@ -123,25 +160,178 @@ class VersionedMetadataInfo(object):
 
 class Metadata(object):
     STANDARD_BOX_ATTRIBUTES = (
-        ("label", dict({
-            "name": "label",
-            "validation_function": validation.is_string,
-            "type": Attribute.STRING,
-            "flags": Attribute.DesignOnly,
-            "help": "A unique identifier for referring to this box",
-        })),
+        ("label", dict(
+            name = "label",
+            validation_function = validation.is_string,
+            type = Attribute.STRING,
+            flags = Attribute.DesignOnly,
+            help = "A unique identifier for referring to this box",
+        )),
     )
+    
+    # Shorthand for DeploymentConfiguration
+    # Syntactic sugar to shorten stuff
+    DC = DeploymentConfiguration
 
     STANDARD_TESTBED_ATTRIBUTES = (
-        ("home_directory", dict({
-            "name": "homeDirectory",
-            "validation_function": validation.is_string,
-            "help": "Path to the directory where traces and other files will be stored",
-            "type": Attribute.STRING,
-            "value": "",
-            "flags": Attribute.DesignOnly,
-        })),
+        ("home_directory", dict(
+            name = "homeDirectory",
+            validation_function = validation.is_string,
+            help = "Path to the directory where traces and other files will be stored",
+            type = Attribute.STRING,
+            value = "",
+            flags = Attribute.DesignOnly
+        )),
+    )
+    
+    DEPLOYMENT_ATTRIBUTES = (
+        # TESTBED DEPLOYMENT ATTRIBUTES
+        (DC.DEPLOYMENT_ENVIRONMENT_SETUP, dict(
+                name = DC.DEPLOYMENT_ENVIRONMENT_SETUP,
+                validation_function = validation.is_string,
+                help = "Shell commands to run before spawning TestbedController processes",
+                type = Attribute.STRING,
+                flags = Attribute.DesignOnly,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_MODE, dict(name = DC.DEPLOYMENT_MODE,
+                help = "Instance execution mode",
+                type = Attribute.ENUM,
+                value = DC.MODE_SINGLE_PROCESS,
+                allowed = [
+                    DC.MODE_DAEMON,
+                    DC.MODE_SINGLE_PROCESS
+                ],
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_enum,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_COMMUNICATION, dict(name = DC.DEPLOYMENT_COMMUNICATION,
+                help = "Instance communication mode",
+                type = Attribute.ENUM,
+                value = DC.ACCESS_LOCAL,
+                allowed = [
+                    DC.ACCESS_LOCAL,
+                    DC.ACCESS_SSH
+                ],
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_enum,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_HOST, dict(name = DC.DEPLOYMENT_HOST,
+                help = "Host where the testbed will be executed",
+                type = Attribute.STRING,
+                value = "localhost",
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_string,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_USER, dict(name = DC.DEPLOYMENT_USER,
+                help = "User on the Host to execute the testbed",
+                type = Attribute.STRING,
+                value = getpass.getuser(),
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_string,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_KEY, dict(name = DC.DEPLOYMENT_KEY,
+                help = "Path to SSH key to use for connecting",
+                type = Attribute.STRING,
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_string,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.DEPLOYMENT_PORT, dict(name = DC.DEPLOYMENT_PORT,
+                help = "Port on the Host",
+                type = Attribute.INTEGER,
+                value = 22,
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_integer,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.ROOT_DIRECTORY, dict(name = DC.ROOT_DIRECTORY,
+                help = "Root directory for storing process files",
+                type = Attribute.STRING,
+                value = ".",
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_string, # TODO: validation.is_path
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.USE_AGENT, dict(name = DC.USE_AGENT,
+                help = "Use -A option for forwarding of the authentication agent, if ssh access is used", 
+                type = Attribute.BOOL,
+                value = False,
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_bool,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.LOG_LEVEL, dict(name = DC.LOG_LEVEL,
+                help = "Log level for instance",
+                type = Attribute.ENUM,
+                value = DC.ERROR_LEVEL,
+                allowed = [
+                    DC.ERROR_LEVEL,
+                    DC.DEBUG_LEVEL
+                ],
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_enum,
+                category = CATEGORY_DEPLOYMENT,
+        )),
+        (DC.RECOVER, dict(name = DC.RECOVER,
+                help = "Do not intantiate testbeds, rather, reconnect to already-running instances. Used to recover from a dead controller.", 
+                type = Attribute.BOOL,
+                value = False,
+                flags = Attribute.DesignOnly,
+                validation_function = validation.is_bool,
+                category = CATEGORY_DEPLOYMENT,
+        )),
     )
+    
+    STANDARD_TESTBED_ATTRIBUTES += DEPLOYMENT_ATTRIBUTES
+    
+    del DC
+    
+    
+    STANDARD_ATTRIBUTE_BUNDLES = {
+            "tun_proto" : dict({
+                "name": "tun_proto", 
+                "help": "TUNneling protocol used",
+                "type": Attribute.STRING,
+                "flags": Attribute.Invisible,
+                "validation_function": validation.is_string,
+            }),
+            "tun_key" : dict({
+                "name": "tun_key", 
+                "help": "Randomly selected TUNneling protocol cryptographic key. "
+                        "Endpoints must agree to use the minimum (in lexicographic order) "
+                        "of both the remote and local sides.",
+                "type": Attribute.STRING,
+                "flags": Attribute.Invisible,
+                "validation_function": validation.is_string,
+            }),
+            "tun_addr" : dict({
+                "name": "tun_addr", 
+                "help": "Address (IP, unix socket, whatever) of the tunnel endpoint",
+                "type": Attribute.STRING,
+                "flags": Attribute.Invisible,
+                "validation_function": validation.is_string,
+            }),
+            "tun_port" : dict({
+                "name": "tun_port", 
+                "help": "IP port of the tunnel endpoint",
+                "type": Attribute.INTEGER,
+                "flags": Attribute.Invisible,
+                "validation_function": validation.is_integer,
+            }),
+            ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP : dict({
+                "name": ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
+                "help": "Commands to set up the environment needed to run NEPI testbeds",
+                "type": Attribute.STRING,
+                "flags": Attribute.Invisible,
+                "validation_function": validation.is_string
+            }),
+    }
+    
 
     def __init__(self, testbed_id, version):
         self._version = version
@@ -161,6 +351,14 @@ class Metadata(object):
     def preconfigure_order(self):
         return self._metadata.preconfigure_order
 
+    @property
+    def prestart_order(self):
+        return self._metadata.prestart_order
+
+    @property
+    def start_order(self):
+        return self._metadata.start_order
+
     def testbed_attributes(self):
         attributes = AttributesMap()
 
@@ -179,8 +377,9 @@ class Metadata(object):
             flags =  attr_info["flags"] if "flags" in attr_info \
                     else Attribute.NoFlags
             validation_function = attr_info["validation_function"]
+            category = attr_info["category"] if "category" in attr_info else None
             attributes.add_attribute(name, help, type, value, 
-                    range, allowed, flags, validation_function)
+                    range, allowed, flags, validation_function, category)
         
         return attributes
 
@@ -190,11 +389,13 @@ class Metadata(object):
         for factory_id, info in self._metadata.factories_info.iteritems():
             help = info["help"]
             category = info["category"]
-            allow_addresses = info["allow_addresses"] \
-                    if "allow_addresses" in info else False
-            allow_routes = info["allow_routes"] \
-                    if "allow_routes" in info else False
-            factory = Factory(factory_id, allow_addresses, allow_routes,
+            allow_addresses = info.get("allow_addresses", False)
+            allow_routes = info.get("allow_routes", False)
+            has_addresses = info.get("has_addresses", False)
+            has_routes = info.get("has_routes", False)
+            factory = Factory(factory_id, 
+                    allow_addresses, has_addresses,
+                    allow_routes, has_routes,
                     help, category)
             
             # standard attributes
@@ -206,6 +407,7 @@ class Metadata(object):
             self._add_attributes(factory, info, "box_attributes", True)
             
             self._add_design_traces(factory, info)
+            self._add_tags(factory, info)
             self._add_design_connector_types(factory, info)
             factories.append(factory)
         return factories
@@ -220,12 +422,17 @@ class Metadata(object):
             status_function = info.get("status_function")
             configure_function = info.get("configure_function")
             preconfigure_function = info.get("preconfigure_function")
+            prestart_function = info.get("prestart_function")
             allow_addresses = info.get("allow_addresses", False)
             allow_routes = info.get("allow_routes", False)
+            has_addresses = info.get("has_addresses", False)
+            has_routes = info.get("has_routes", False)
             factory = Factory(factory_id, create_function, start_function,
                     stop_function, status_function, 
                     configure_function, preconfigure_function,
-                    allow_addresses, allow_routes)
+                    prestart_function,
+                    allow_addresses, has_addresses,
+                    allow_routes, has_routes)
                     
             # standard attributes
             self._add_standard_attributes(factory, info, False, True,
@@ -236,6 +443,7 @@ class Metadata(object):
             self._add_attributes(factory, info, "box_attributes", True)
             
             self._add_execute_traces(factory, info)
+            self._add_tags(factory, info)
             self._add_execute_connector_types(factory, info)
             factories.append(factory)
         return factories
@@ -260,7 +468,9 @@ class Metadata(object):
 
     def _add_attributes(self, factory, info, attr_key, box_attributes = False, attr_bundle = ()):
         if not attr_bundle and info and attr_key in info:
-            attr_bundle = [ (attr_id, self._metadata.attributes[attr_id])
+            definitions = self.STANDARD_ATTRIBUTE_BUNDLES.copy()
+            definitions.update(self._metadata.attributes)
+            attr_bundle = [ (attr_id, definitions[attr_id])
                             for attr_id in info[attr_key] ]
         for attr_id, attr_info in attr_bundle:
             name = attr_info["name"]
@@ -274,12 +484,13 @@ class Metadata(object):
                     and attr_info["flags"] != None \
                     else Attribute.NoFlags
             validation_function = attr_info["validation_function"]
+            category = attr_info["category"] if "category" in attr_info else None
             if box_attributes:
                 factory.add_box_attribute(name, help, type, value, range, 
-                        allowed, flags, validation_function)
+                        allowed, flags, validation_function, category)
             else:
                 factory.add_attribute(name, help, type, value, range, 
-                        allowed, flags, validation_function)
+                        allowed, flags, validation_function, category)
 
     def _add_design_traces(self, factory, info):
         if "traces" in info:
@@ -296,6 +507,11 @@ class Metadata(object):
                 trace_id = trace_info["name"]
                 factory.add_trace(trace_id)
 
+    def _add_tags(self, factory, info):
+        if "tags" in info:
+            for tag_id in info["tags"]:
+                factory.add_tag(tag_id)
+
     def _add_design_connector_types(self, factory, info):
         from nepi.core.design import ConnectorType
         if "connector_types" in info:
@@ -337,13 +553,18 @@ class Metadata(object):
                 from_ = connection["from"]
                 to = connection["to"]
                 can_cross = connection["can_cross"]
-                code = connection["code"]
+                init_code = connection["init_code"] \
+                        if "init_code" in connection else None
+                compl_code = connection["compl_code"] \
+                        if "compl_code" in connection else None
                 if from_ not in from_connections:
                     from_connections[from_] = list()
                 if to not in to_connections:
                     to_connections[to] = list()
-                from_connections[from_].append((to, can_cross, code))
-                to_connections[to].append((from_, can_cross, code))
+                from_connections[from_].append((to, can_cross, init_code, 
+                    compl_code))
+                to_connections[to].append((from_, can_cross, init_code,
+                    compl_code))
             for connector_id in info["connector_types"]:
                 connector_type_info = self._metadata.connector_types[
                         connector_id]
@@ -356,15 +577,18 @@ class Metadata(object):
                         max, min)
                 connector_key = (testbed_id, factory_id, name)
                 if connector_key in to_connections:
-                    for (from_, can_cross, code) in to_connections[connector_key]:
+                    for (from_, can_cross, init_code, compl_code) in \
+                            to_connections[connector_key]:
                         (testbed_id_from, factory_id_from, name_from) = from_
                         connector_type.add_from_connection(testbed_id_from, 
-                                factory_id_from, name_from, can_cross, code)
+                                factory_id_from, name_from, can_cross, 
+                                init_code, compl_code)
                 if connector_key in from_connections:
-                    for (to, can_cross, code) in from_connections[(testbed_id, 
-                            factory_id, name)]:
+                    for (to, can_cross, init_code, compl_code) in \
+                            from_connections[(testbed_id, factory_id, name)]:
                         (testbed_id_to, factory_id_to, name_to) = to
                         connector_type.add_to_connection(testbed_id_to, 
-                                factory_id_to, name_to, can_cross, code)
+                                factory_id_to, name_to, can_cross, init_code,
+                                compl_code)
                 factory.add_connector_type(connector_type)