nodes have a node_type (NodeType object and api calls still missing)
authorThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Thu, 4 Dec 2008 22:18:57 +0000 (22:18 +0000)
committerThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Thu, 4 Dec 2008 22:18:57 +0000 (22:18 +0000)
nodes tags might be exposed through GetNodes (implicit initializations still missing)
cannot filter on tags yet & very ilghtly tested

PLC/Filter.py
PLC/Nodes.py
PLC/Table.py
migrations/v4-to-v5/migrate.sql
planetlab5.sql

index 9e2640c..807ce00 100644 (file)
@@ -1,4 +1,4 @@
-# $Id#
+# $Id$
 from types import StringTypes
 try:
     set
@@ -74,28 +74,13 @@ class Filter(Parameter, dict):
         # Declare ourselves as a type of parameter that can take
         # either a value or a list of values for each of the specified
         # fields.
-        self.fields = {}
-
-        for field, expected in fields.iteritems():
-            # Cannot filter on sequences
-            if python_type(expected) in (list, tuple, set):
-                continue
-            
-            # Accept either a value or a list of values of the specified type
-            self.fields[field] = Mixed(expected, [expected])
+        self.fields = dict ( [ ( field, Mixed (expected, [expected])) 
+                                 for (field,expected) in fields.iteritems()
+                                 if python_type(expected) not in (list, tuple, set) ] )
 
         # Null filter means no filter
         Parameter.__init__(self, self.fields, doc = doc, nullok = True)
 
-    # this code is not used anymore
-    # at some point the select in the DB for event objects was done on
-    # the events table directly, that is stored as a timestamp, thus comparisons
-    # needed to be done based on SQL timestamps as well
-    def unix2timestamp (self,unix):
-       s = time.gmtime(unix)
-       return "TIMESTAMP'%04d-%02d-%02d %02d:%02d:%02d'" % (s.tm_year,s.tm_mon,s.tm_mday,
-                                                            s.tm_hour,s.tm_min,s.tm_sec)
-
     def sql(self, api, join_with = "AND"):
         """
         Returns a SQL conditional that represents this filter.
index 0734e58..7a164c7 100644 (file)
@@ -43,6 +43,7 @@ class Node(Row):
                     'node_tag', 'conf_file_node', 'pcu_node', ]
     fields = {
         'node_id': Parameter(int, "Node identifier"),
+        'node_type': Parameter(str,"Node type",max=20),
         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
         'site_id': Parameter(int, "Site at which this node is located"),
         'boot_state': Parameter(str, "Boot state", max = 20),
@@ -69,7 +70,7 @@ class Node(Row):
         }
     related_fields = {
        'interfaces': [Mixed(Parameter(int, "Interface identifier"),
-                                      Filter(Interface.fields))],
+                             Filter(Interface.fields))],
        'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
                              Parameter(str, "NodeGroup name"))],
        'conf_files': [Parameter(int, "ConfFile identifier")],
@@ -78,6 +79,14 @@ class Node(Row):
        'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
                                    Parameter(str, "Slice name"))]
        }
+    view_name = "view_nodes"
+    view_tags_name = "view_node_tags"
+    tags = {
+        # regular
+        'arch': Parameter(str, "node/config", ro=True),
+        'deployment': Parameter(str, "node/operation"),
+        # dummynet
+        }
 
     def validate_hostname(self, hostname):
         if not valid_hostname(hostname):
@@ -260,8 +269,14 @@ class Nodes(Table):
     def __init__(self, api, node_filter = None, columns = None):
         Table.__init__(self, api, Node, columns)
 
-        sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
-              ", ".join(self.columns)
+        # the view that we're selecting upon: start with view_nodes
+        view = "view_nodes"
+        # as many left joins as requested tags
+        for tagname in self.tag_columns:
+            view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),Node.primary_key)
+            
+        sql = "SELECT %s FROM %s WHERE deleted IS False" % \
+              (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
 
         if node_filter is not None:
             if isinstance(node_filter, (list, tuple, set)):
index 8a0cde4..eeac6c1 100644 (file)
@@ -14,11 +14,13 @@ class Row(dict):
     """
 
     # Set this to the name of the table that stores the row.
+    # e.g. table_name = "nodes"
     table_name = None
 
     # Set this to the name of the primary key of the table. It is
     # assumed that the this key is a sequence if it is not set when
     # sync() is called.
+    # e.g. primary_key="node_id"
     primary_key = None
 
     # Set this to the names of tables that reference this table's
@@ -30,6 +32,17 @@ class Row(dict):
     # sync().
     fields = {}
 
+    # Set this to the name of the view that gathers the row and its relations
+    # e.g. view_name = "view_nodes"
+    view_name = None
+
+    # The name of the view that extends objects with tags
+    # e.g. view_tags_name = "view_node_tags"
+    view_tags_name = None
+
+    # Set this to the set of tags that can be returned by the Get function
+    tags = {}
+
     def __init__(self, api, fields = {}):
         dict.__init__(self, fields)
         self.api = api
@@ -69,8 +82,8 @@ class Row(dict):
 
     def associate(self, *args):
        """
-       Provides a means for high lvl api calls to associate objects
-        using low lvl calls.
+       Provides a means for high level api calls to associate objects
+        using low level calls.
        """
 
        if len(args) < 3:
@@ -180,6 +193,17 @@ class Row(dict):
 
     remove_object = classmethod(remove_object)
 
+    # convenience: check in dict (self.fields or self.tags) that a key is writable
+    @staticmethod
+    def is_writable (key,value,dict):
+        # if not mentioned, assume it's writable (e.g. deleted ...)
+        if key not in dict: return True
+        # if mentioned but not linked to a Parameter object, idem
+        if not isinstance(dict[key], Parameter): return True
+        # if not marked ro, it's writable
+        if not dict[key].ro: return True
+        return False
+
     def db_fields(self, obj = None):
         """
         Return only those fields that can be set or updated directly
@@ -187,16 +211,65 @@ class Row(dict):
         for this object, and are not marked as a read-only Parameter.
         """
 
-        if obj is None:
-            obj = self
+        if obj is None: obj = self
 
         db_fields = self.api.db.fields(self.table_name)
-        return dict(filter(lambda (key, value): \
-                           key in db_fields and \
-                           (key not in self.fields or \
-                            not isinstance(self.fields[key], Parameter) or \
-                            not self.fields[key].ro),
-                           obj.items()))
+        return dict ( [ (key,value) for (key,value) in obj.items()
+                        if key in db_fields and
+                        Row.is_writable(key,value,self.fields) ] )
+
+    def tag_fields (self, obj=None):
+        """
+        Return the fields of obj that are mentioned in tags
+        """
+        if obj is None: obj=self
+        
+        return dict ( [ (key,value) for (key,value) in obj.iteritems() 
+                        if key in self.tags and Row.is_writable(key,value,self.tags) ] )
+    
+    # takes in input a list of columns, returns three lists
+    # fields, tags, rejected
+    @classmethod
+    def parse_columns (cls, columns):
+        (fields,tags,rejected)=({},{},{})
+        for column in columns:
+            if column in cls.fields: fields[column]=cls.fields[column]
+            elif column in cls.tags: tags[column]=cls.tags[column]
+            else: rejected.append(column)
+        return (fields,tags,rejected)
+
+    @classmethod
+    def tagvalue_view_name (cls, tagname):
+        return "tagvalue_view_%s_%s"%(cls.primary_key,tagname)
+
+    @classmethod
+    def tagvalue_view_create (cls,tagname):
+        """
+        returns an SQL sentence that creates a view named after the primary_key and tagname, 
+        with 2 columns
+        (*) column 1: name=self.primary_key 
+        (*) column 2: name=tagname value=tagvalue
+        """
+
+        if not cls.view_tags_name: return ""
+
+        table_name=cls.table_name
+        primary_key=cls.primary_key
+        view_tags_name=cls.view_tags_name
+        tagvalue_view_name=cls.tagvalue_view_name(tagname)
+        return 'CREATE OR REPLACE VIEW %(tagvalue_view_name)s ' \
+            'as SELECT %(table_name)s.%(primary_key)s,%(view_tags_name)s.tagvalue as "%(tagname)s" ' \
+            'from %(table_name)s right join %(view_tags_name)s using (%(primary_key)s) ' \
+            'WHERE tagname = \'%(tagname)s\';'%locals()
+
+    @classmethod
+    def tagvalue_views_create (cls):
+        if not cls.tags: return
+        sql = []
+        for (type,type_dict) in cls.tags.iteritems():
+            for (tagname,details) in type_dict.iteritems():
+                sql.append(cls.tagvalue_view_create (tagname))
+        return sql
 
     def __eq__(self, y):
         """
@@ -293,12 +366,16 @@ class Table(list):
 
         if columns is None:
             columns = classobj.fields
+            tag_columns={}
         else:
-            columns = filter(lambda x: x in classobj.fields, columns)
+            (columns,tag_columns,rejected) = classobj.parse_columns(columns)
             if not columns:
-                raise PLCInvalidArgument, "No valid return fields specified"
+                raise PLCInvalidArgument, "No valid return fields specified for class %s"%classobj.__name__
+            if rejected:
+                raise PLCInvalidArgument, "unknown column(s) specified %r in %s"%(rejected,classobj.__name__)
 
         self.columns = columns
+        self.tag_columns = tag_columns
 
     def sync(self, commit = True):
         """
index 535fad0..4013754 100644 (file)
@@ -101,6 +101,11 @@ select * from mgn_all_views;
 drop view mgn_all_views;
 drop function mgn_drop_all_views ();
 
+----------------------------------------
+-- nodes
+----------------------------------------
+ALTER TABLE nodes ADD COLUMN node_type TEXT NOT NULL DEFAULT 'regular';
+
 ----------------------------------------
 -- tag types
 ----------------------------------------
index 90f4917..304a2f9 100644 (file)
@@ -264,13 +264,22 @@ INSERT INTO boot_states (boot_state) VALUES ('disabled');
 INSERT INTO boot_states (boot_state) VALUES ('install');
 INSERT INTO boot_states (boot_state) VALUES ('reinstall');
 
+-- Known node types (Nodes.py expect max length to be 20)
+CREATE TABLE node_types (
+    node_type text PRIMARY KEY
+) WITH OIDS;
+INSERT INTO node_types (node_type) VALUES ('regular');
+INSERT INTO node_types (node_type) VALUES ('dummynet');
+
 -- Nodes
 CREATE TABLE nodes (
     -- Mandatory
     node_id serial PRIMARY KEY,                                -- Node identifier
+    node_type text REFERENCES node_types               -- node type
+              DEFAULT 'regular',
+
     hostname text NOT NULL,                            -- Node hostname
     site_id integer REFERENCES sites NOT NULL,         -- At which site 
-
     boot_state text REFERENCES boot_states NOT NULL    -- Node boot state
               DEFAULT 'install', 
     deleted boolean NOT NULL DEFAULT false,            -- Is deleted
@@ -1058,6 +1067,7 @@ INNER JOIN nodes USING (node_id);
 CREATE OR REPLACE VIEW view_nodes AS
 SELECT
 nodes.node_id,
+nodes.node_type,
 nodes.hostname,
 nodes.site_id,
 nodes.boot_state,