From a12987e598653f7fb2b8000d1826dab0f8e154a1 Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Thu, 4 Dec 2008 22:18:57 +0000 Subject: [PATCH] nodes have a node_type (NodeType object and api calls still missing) nodes tags might be exposed through GetNodes (implicit initializations still missing) cannot filter on tags yet & very ilghtly tested --- PLC/Filter.py | 23 ++------ PLC/Nodes.py | 21 ++++++- PLC/Table.py | 101 ++++++++++++++++++++++++++++---- migrations/v4-to-v5/migrate.sql | 5 ++ planetlab5.sql | 12 +++- 5 files changed, 127 insertions(+), 35 deletions(-) diff --git a/PLC/Filter.py b/PLC/Filter.py index 9e2640c..807ce00 100644 --- a/PLC/Filter.py +++ b/PLC/Filter.py @@ -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. diff --git a/PLC/Nodes.py b/PLC/Nodes.py index 0734e58..7a164c7 100644 --- a/PLC/Nodes.py +++ b/PLC/Nodes.py @@ -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)): diff --git a/PLC/Table.py b/PLC/Table.py index 8a0cde4..eeac6c1 100644 --- a/PLC/Table.py +++ b/PLC/Table.py @@ -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): """ diff --git a/migrations/v4-to-v5/migrate.sql b/migrations/v4-to-v5/migrate.sql index 535fad0..4013754 100644 --- a/migrations/v4-to-v5/migrate.sql +++ b/migrations/v4-to-v5/migrate.sql @@ -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 ---------------------------------------- diff --git a/planetlab5.sql b/planetlab5.sql index 90f4917..304a2f9 100644 --- a/planetlab5.sql +++ b/planetlab5.sql @@ -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, -- 2.43.0