merge changes from head
[plcapi.git] / PLC / Nodes.py
index 25bee2b..9468c07 100644 (file)
@@ -4,7 +4,7 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
-# $Id: Nodes.py,v 1.4 2006/09/25 15:00:55 mlhuang Exp $
+# $Id: Nodes.py,v 1.29 2007/01/09 16:13:36 mlhuang Exp $
 #
 
 from types import StringTypes
 #
 
 from types import StringTypes
@@ -12,11 +12,23 @@ import re
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter
 
 from PLC.Faults import *
 from PLC.Parameter import Parameter
+from PLC.Filter import Filter
 from PLC.Debug import profile
 from PLC.Table import Row, Table
 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
 from PLC.BootStates import BootStates
 
 from PLC.Debug import profile
 from PLC.Table import Row, Table
 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
 from PLC.BootStates import BootStates
 
+def valid_hostname(hostname):
+    # 1. Each part begins and ends with a letter or number.
+    # 2. Each part except the last can contain letters, numbers, or hyphens.
+    # 3. Each part is between 1 and 64 characters, including the trailing dot.
+    # 4. At least two parts.
+    # 5. Last part can only contain between 2 and 6 letters.
+    good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
+                    r'[a-z]{2,6}$'
+    return hostname and \
+           re.match(good_hostname, hostname, re.IGNORECASE)
+
 class Node(Row):
     """
     Representation of a row in the nodes table. To use, optionally
 class Node(Row):
     """
     Representation of a row in the nodes table. To use, optionally
@@ -24,104 +36,64 @@ class Node(Row):
     dict. Commit to the database with sync().
     """
 
     dict. Commit to the database with sync().
     """
 
+    table_name = 'nodes'
+    primary_key = 'node_id'
+    join_tables = ['nodegroup_node', 'conf_file_node', 'nodenetworks', 'pcu_node', 'slice_node', 'slice_attribute', 'node_session', 'peer_node']
     fields = {
         'node_id': Parameter(int, "Node identifier"),
         '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),
     fields = {
         'node_id': Parameter(int, "Node identifier"),
         '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),
-        'model': Parameter(str, "Make and model of the actual machine", max = 255),
+        'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
         'version': Parameter(str, "Apparent Boot CD version", max = 64),
         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
         'version': Parameter(str, "Apparent Boot CD version", max = 64),
         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
-        'date_created': Parameter(str, "Date and time when node entry was created"),
-        'last_updated': Parameter(str, "Date and time when node entry was created"),
-        'deleted': Parameter(bool, "Has been deleted"),
+        'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
+        'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
         'key': Parameter(str, "(Admin only) Node key", max = 256),
         'key': Parameter(str, "(Admin only) Node key", max = 256),
-        'session': Parameter(str, "(Admin only) Node session value", max = 256),
+        'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
         'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
         'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
-        'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
+        'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
-        # 'slice_ids': Parameter([int], "List of slices on this node"),
-        # 'pcu_ids': Parameter([int], "List of PCUs that control this node"),
+        'slice_ids': Parameter([int], "List of slices on this node"),
+        'pcu_ids': Parameter([int], "List of PCUs that control this node"),
+        'ports': Parameter([int], "List of PCU ports that this node is connected to"),
+        'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
+        'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
         }
 
         }
 
-    def __init__(self, api, fields):
-        Row.__init__(self, fields)
-        self.api = api
+    # for Cache
+    class_key = 'hostname'
+    foreign_fields = ['boot_state','model','version']
+    # forget about these ones, they are read-only anyway
+    # handling them causes Cache to re-sync all over again 
+    # 'date_created','last_updated'
+    foreign_xrefs = [
+       # in this case, we dont need the 'table' but Cache will look it up, so...
+        {'field' : 'site_id' , 'class' : 'Site' , 'table' : 'unused-on-direct-refs' } ,
+       ]
 
     def validate_hostname(self, hostname):
 
     def validate_hostname(self, hostname):
-        # 1. Each part begins and ends with a letter or number.
-        # 2. Each part except the last can contain letters, numbers, or hyphens.
-        # 3. Each part is between 1 and 64 characters, including the trailing dot.
-        # 4. At least two parts.
-        # 5. Last part can only contain between 2 and 6 letters.
-        good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
-                        r'[a-z]{2,6}$'
-        if not hostname or \
-           not re.match(good_hostname, hostname, re.IGNORECASE):
+        if not valid_hostname(hostname):
             raise PLCInvalidArgument, "Invalid hostname"
 
         conflicts = Nodes(self.api, [hostname])
             raise PLCInvalidArgument, "Invalid hostname"
 
         conflicts = Nodes(self.api, [hostname])
-        for node_id, node in conflicts.iteritems():
-            if not node['deleted'] and ('node_id' not in self or self['node_id'] != node_id):
-                raise PLCInvalidArgument, "Hostname already in use"
-
-        # Check for conflicts with a nodenetwork hostname
-        conflicts = NodeNetworks(self.api, [hostname])
-        for nodenetwork_id in conflicts:
-            if 'nodenetwork_ids' not in self or nodenetwork_id not in self['nodenetwork_ids']:
+        for node in conflicts:
+            if 'node_id' not in self or self['node_id'] != node['node_id']:
                 raise PLCInvalidArgument, "Hostname already in use"
 
         return hostname
 
     def validate_boot_state(self, boot_state):
                 raise PLCInvalidArgument, "Hostname already in use"
 
         return hostname
 
     def validate_boot_state(self, boot_state):
-        if boot_state not in BootStates(self.api):
+        boot_states = [row['boot_state'] for row in BootStates(self.api)]
+        if boot_state not in boot_states:
             raise PLCInvalidArgument, "Invalid boot state"
 
         return boot_state
 
             raise PLCInvalidArgument, "Invalid boot state"
 
         return boot_state
 
-    def sync(self, commit = True):
-        """
-        Flush changes back to the database.
-        """
-
-        self.validate()
-
-        # Fetch a new node_id if necessary
-        if 'node_id' not in self:
-            rows = self.api.db.selectall("SELECT NEXTVAL('nodes_node_id_seq') AS node_id")
-            if not rows:
-                raise PLCDBError, "Unable to fetch new node_id"
-            self['node_id'] = rows[0]['node_id']
-            insert = True
-        else:
-            insert = False
-
-        # Filter out fields that cannot be set or updated directly
-        nodes_fields = self.api.db.fields('nodes')
-        fields = dict(filter(lambda (key, value): key in nodes_fields,
-                             self.items()))
-
-        # Parameterize for safety
-        keys = fields.keys()
-        values = [self.api.db.param(key, value) for (key, value) in fields.items()]
-
-        if insert:
-            # Insert new row in nodes table
-            sql = "INSERT INTO nodes (%s) VALUES (%s)" % \
-                  (", ".join(keys), ", ".join(values))
-        else:
-            # Update existing row in nodes table
-            columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
-            sql = "UPDATE nodes SET " + \
-                  ", ".join(columns) + \
-                  " WHERE node_id = %(node_id)d"
-
-        self.api.db.do(sql, fields)
-
-        if commit:
-            self.api.db.commit()
+    validate_date_created = Row.validate_timestamp
+    validate_last_updated = Row.validate_timestamp
 
     def delete(self, commit = True):
         """
 
     def delete(self, commit = True):
         """
@@ -130,54 +102,37 @@ class Node(Row):
 
         assert 'node_id' in self
 
 
         assert 'node_id' in self
 
-        # Delete all nodenetworks
-        nodenetworks = NodeNetworks(self.api, self['nodenetwork_ids'])
-        for nodenetwork in nodenetworks.values():
-            nodenetwork.delete(commit = False)
-
         # Clean up miscellaneous join tables
         # Clean up miscellaneous join tables
-        for table in ['nodegroup_node']:
-            self.api.db.do("DELETE FROM %s" \
-                           " WHERE node_id = %d" % \
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
                            (table, self['node_id']))
 
         # Mark as deleted
         self['deleted'] = True
         self.sync(commit)
 
                            (table, self['node_id']))
 
         # Mark as deleted
         self['deleted'] = True
         self.sync(commit)
 
+
 class Nodes(Table):
     """
     Representation of row(s) from the nodes table in the
     database.
     """
 
 class Nodes(Table):
     """
     Representation of row(s) from the nodes table in the
     database.
     """
 
-    def __init__(self, api, node_id_or_hostname_list = None, fields = Node.fields.keys()):
-        self.api = api
+    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" % \
 
         sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
-              ", ".join(fields)
-
-        if node_id_or_hostname_list:
-            # Separate the list into integers and strings
-            node_ids = filter(lambda node_id: isinstance(node_id, (int, long)),
-                              node_id_or_hostname_list)
-            hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
-                               node_id_or_hostname_list)
-            sql += " AND (False"
-            if node_ids:
-                sql += " OR node_id IN (%s)" % ", ".join(map(str, node_ids))
-            if hostnames:
-                sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
-            sql += ")"
-
-        rows = self.api.db.selectall(sql)
-
-        for row in rows:
-            self[row['node_id']] = node = Node(api, row)
-            for aggregate in ['nodenetwork_ids', 'nodegroup_ids',
-                              'conf_file_ids', 'root_person_ids', 'slice_ids',
-                              'pcu_ids']:
-                if not node.has_key(aggregate) or node[aggregate] is None:
-                    node[aggregate] = []
-                else:
-                    node[aggregate] = map(int, node[aggregate].split(','))
+              ", ".join(self.columns)
+
+        if node_filter is not None:
+            if isinstance(node_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
+                node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
+                sql += " AND (%s)" % node_filter.sql(api, "OR")
+            elif isinstance(node_filter, dict):
+                node_filter = Filter(Node.fields, node_filter)
+                sql += " AND (%s)" % node_filter.sql(api, "AND")
+
+        self.selectall(sql)