merge changes from head
[plcapi.git] / PLC / Sites.py
index f452f6c..19b15d6 100644 (file)
@@ -3,75 +3,76 @@ import string
 
 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.Slices import Slice, Slices
 from PLC.PCUs import PCU, PCUs
 from PLC.Nodes import Node, Nodes
 from PLC.Debug import profile
 from PLC.Table import Row, Table
 from PLC.Slices import Slice, Slices
 from PLC.PCUs import PCU, PCUs
 from PLC.Nodes import Node, Nodes
-from PLC.NodeGroups import NodeGroup, NodeGroups
-import PLC.Persons
+from PLC.Addresses import Address, Addresses
+from PLC.Persons import Person, Persons
 
 class Site(Row):
     """
     Representation of a row in the sites table. To use, optionally
     instantiate with a dict of values. Update as you would a
 
 class Site(Row):
     """
     Representation of a row in the sites table. To use, optionally
     instantiate with a dict of values. Update as you would a
-    dict. Commit to the database with flush().
+    dict. Commit to the database with sync().
     """
 
     """
 
+    table_name = 'sites'
+    primary_key = 'site_id'
+    join_tables = ['person_site', 'site_address', 'peer_site']
     fields = {
         'site_id': Parameter(int, "Site identifier"),
         'name': Parameter(str, "Full site name", max = 254),
         'abbreviated_name': Parameter(str, "Abbreviated site name", max = 50),
         'login_base': Parameter(str, "Site slice prefix", max = 20),
         'is_public': Parameter(bool, "Publicly viewable site"),
     fields = {
         'site_id': Parameter(int, "Site identifier"),
         'name': Parameter(str, "Full site name", max = 254),
         'abbreviated_name': Parameter(str, "Abbreviated site name", max = 50),
         'login_base': Parameter(str, "Site slice prefix", max = 20),
         'is_public': Parameter(bool, "Publicly viewable site"),
-        'latitude': Parameter(float, "Decimal latitude of the site", min = -90.0, max = 90.0),
-        'longitude': Parameter(float, "Decimal longitude of the site", min = -180.0, max = 180.0),
-        'url': Parameter(str, "URL of a page that describes the site", max = 254),
-        'nodegroup_id': Parameter(int, "Identifier of the nodegroup containing the site's nodes"),
-        'organization_id': Parameter(int, "Organizational identifier if the site is part of a larger organization"),
-        'ext_consortium_id': Parameter(int, "Consortium identifier if the site is part of an external consortium"),
-        'date_created': Parameter(str, "Date and time when node entry was created"),        
-        'deleted': Parameter(bool, "Has been deleted"),
-        }
-
-    # These fields are derived from join tables and are not actually
-    # in the sites table.
-    join_fields = {
+        'enabled': Parameter(bool, "Has been enabled"),
+       'latitude': Parameter(float, "Decimal latitude of the site", min = -90.0, max = 90.0, nullok = True),
+        'longitude': Parameter(float, "Decimal longitude of the site", min = -180.0, max = 180.0, nullok = True),
+        'url': Parameter(str, "URL of a page that describes the site", max = 254, nullok = True),
+        'date_created': Parameter(int, "Date and time when site entry was created, in seconds since UNIX epoch", ro = True),
+        'last_updated': Parameter(int, "Date and time when site entry was last updated, in seconds since UNIX epoch", ro = True),
         'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
         'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
-        'site_share': Parameter(float, "Relative resource share for this site's slices"),
-        }        
-
-    # These fields are derived from join tables and are not returned
-    # by default unless specified.
-    extra_fields = {
+        'max_slivers': Parameter(int, "Maximum number of slivers that the site is able to create"),
         'person_ids': Parameter([int], "List of account identifiers"),
         'slice_ids': Parameter([int], "List of slice identifiers"),
         'person_ids': Parameter([int], "List of account identifiers"),
         'slice_ids': Parameter([int], "List of slice identifiers"),
-        'defaultattribute_ids': Parameter([int], "List of default slice attribute identifiers"),
+        'address_ids': Parameter([int], "List of address identifiers"),
         'pcu_ids': Parameter([int], "List of PCU identifiers"),
         'node_ids': Parameter([int], "List of site node identifiers"),
         'pcu_ids': Parameter([int], "List of PCU identifiers"),
         'node_ids': Parameter([int], "List of site node identifiers"),
+        'peer_id': Parameter(int, "Peer to which this site belongs", nullok = True),
+        'peer_site_id': Parameter(int, "Foreign site identifier at peer", nullok = True),
         }
 
         }
 
-    default_fields = dict(fields.items() + join_fields.items())
-    all_fields = dict(default_fields.items() + extra_fields.items())
+    # for Cache
+    class_key = 'login_base'
+    foreign_fields = ['abbreviated_name', 'name', 'is_public', 'latitude', 'longitude',
+                     'url', 'max_slices', 'max_slivers',
+                     ]
+    # forget about these ones, they are read-only anyway
+    # handling them causes Cache to re-sync all over again 
+    # 'last_updated', 'date_created'
+    foreign_xrefs = []
 
 
-    # Number of slices assigned to each site at the time that the site is created
-    default_max_slices = 0
+    def validate_name(self, name):
+        if not len(name):
+            raise PLCInvalidArgument, "Name must be specified"
 
 
-    # XXX Useless, unclear what this value means
-    default_site_share = 1.0
+        return name
 
 
-    def __init__(self, api, fields):
-        Row.__init__(self, fields)
-        self.api = api
+    validate_abbreviated_name = validate_name
 
     def validate_login_base(self, login_base):
 
     def validate_login_base(self, login_base):
-        if not set(login_base).issubset(string.ascii_letters):
-            raise PLCInvalidArgument, "Login base must consist only of ASCII letters"
+        if not len(login_base):
+            raise PLCInvalidArgument, "Login base must be specified"
+
+        if not set(login_base).issubset(string.lowercase + string.digits):
+            raise PLCInvalidArgument, "Login base must consist only of lowercase ASCII letters or numbers"
 
 
-        login_base = login_base.lower()
         conflicts = Sites(self.api, [login_base])
         conflicts = Sites(self.api, [login_base])
-        for site_id, site in conflicts.iteritems():
-            if not site['deleted'] and ('site_id' not in self or self['site_id'] != site_id):
+        for site in conflicts:
+            if 'site_id' not in self or self['site_id'] != site['site_id']:
                 raise PLCInvalidArgument, "login_base already in use"
 
         return login_base
                 raise PLCInvalidArgument, "login_base already in use"
 
         return login_base
@@ -90,156 +91,14 @@ class Site(Row):
 
         return longitude
 
 
         return longitude
 
-    def validate_nodegroup_id(self, nodegroup_id):
-        nodegroups = NodeGroups(self.api)
-        if nodegroup_id not in nodegroups:
-            raise PLCInvalidArgument, "No such nodegroup"
-
-        return nodegroup_id
-
-    def validate_organization_id(self, organization_id):
-        organizations = Organizations(self.api)
-        if role_id not in organizations:
-            raise PLCInvalidArgument, "No such organization"
-
-        return organization_id
-
-    def validate_ext_consortium_id(self, organization_id):
-        consortiums = Consortiums(self.api)
-        if consortium_id not in consortiums:
-            raise PLCInvalidArgument, "No such consortium"
-
-        return nodegroup_id
+    validate_date_created = Row.validate_timestamp
+    validate_last_updated = Row.validate_timestamp
 
 
-    def add_person(self, person, commit = True):
-        """
-        Add person to existing site.
-        """
-
-        assert 'site_id' in self
-        assert isinstance(person, PLC.Persons.Person)
-        assert 'person_id' in person
-
-        site_id = self['site_id']
-        person_id = person['person_id']
-        self.api.db.do("INSERT INTO person_site (person_id, site_id)" \
-                       " VALUES(%(person_id)d, %(site_id)d)",
-                       locals())
+    add_person = Row.add_object(Person, 'person_site')
+    remove_person = Row.remove_object(Person, 'person_site')
 
 
-        if commit:
-            self.api.db.commit()
-
-        if 'person_ids' in self and person_id not in self['person_ids']:
-            self['person_ids'].append(person_id)
-
-        if 'site_ids' in person and site_id not in person['site_ids']:
-            person['site_ids'].append(site_id)
-
-    def remove_person(self, person, commit = True):
-        """
-        Remove person from existing site.
-        """
-
-        assert 'site_id' in self
-        assert isinstance(person, PLC.Persons.Person)
-        assert 'person_id' in person
-
-        site_id = self['site_id']
-        person_id = person['person_id']
-        self.api.db.do("DELETE FROM person_site" \
-                       " WHERE person_id = %(person_id)d" \
-                       " AND site_id = %(site_id)d",
-                       locals())
-
-        if commit:
-            self.api.db.commit()
-
-        if 'person_ids' in self and person_id in self['person_ids']:
-            self['person_ids'].remove(person_id)
-
-        if 'site_ids' in person and site_id in person['site_ids']:
-            person['site_ids'].remove(site_id)
-
-    def flush(self, commit = True):
-        """
-        Flush changes back to the database.
-        """
-
-        self.validate()
-
-        try:
-            if not self['name'] or \
-               not self['abbreviated_name'] or \
-               not self['login_base']:
-                raise KeyError
-        except KeyError:
-            raise PLCInvalidArgument, "name, abbreviated_name, and login_base must all be specified"
-
-        # Fetch a new site_id if necessary
-        if 'site_id' not in self:
-            rows = self.api.db.selectall("SELECT NEXTVAL('sites_site_id_seq') AS site_id")
-            if not rows:
-                raise PLCDBError, "Unable to fetch new site_id"
-            self['site_id'] = rows[0]['site_id']
-            insert = True
-        else:
-            insert = False
-
-        # Create site node group if necessary
-        if 'nodegroup_id' not in self:
-            rows = self.api.db.selectall("SELECT NEXTVAL('nodegroups_nodegroup_id_seq') as nodegroup_id")
-            if not rows:
-                raise PLCDBError, "Unable to fetch new nodegroup_id"
-            self['nodegroup_id'] = rows[0]['nodegroup_id']
-
-            nodegroup_id = self['nodegroup_id']
-            # XXX Needs a unique name because we cannot delete site node groups yet
-            name = self['login_base'] + str(self['site_id'])
-            description = "Nodes at " + self['login_base']
-            is_custom = False
-            self.api.db.do("INSERT INTO nodegroups (nodegroup_id, name, description, is_custom)" \
-                           " VALUES (%(nodegroup_id)d, %(name)s, %(description)s, %(is_custom)s)",
-                           locals())
-
-        # Filter out fields that cannot be set or updated directly
-        fields = dict(filter(lambda (key, value): key in self.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 sites table
-            self.api.db.do("INSERT INTO sites (%s) VALUES (%s)" % \
-                           (", ".join(keys), ", ".join(values)),
-                           fields)
-
-            # Setup default slice site info
-            # XXX Will go away soon
-            self['max_slices'] = self.default_max_slices
-            self['site_share'] = self.default_site_share
-            self.api.db.do("INSERT INTO dslice03_siteinfo (site_id, max_slices, site_share)" \
-                           " VALUES (%(site_id)d, %(max_slices)d, %(site_share)f)",
-                           self)
-        else:
-            # Update default slice site info
-            # XXX Will go away soon
-            if 'max_slices' in self and 'site_share' in self:
-                self.api.db.do("UPDATE dslice03_siteinfo SET " \
-                               " max_slices = %(max_slices)d, site_share = %(site_share)f" \
-                               " WHERE site_id = %(site_id)d",
-                               self)
-
-            # Update existing row in sites table
-            columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
-            self.api.db.do("UPDATE sites SET " + \
-                           ", ".join(columns) + \
-                           " WHERE site_id = %(site_id)d",
-                           fields)
-
-        if commit:
-            self.api.db.commit()
+    add_address = Row.add_object(Address, 'site_address')
+    remove_address = Row.remove_object(Address, 'site_address')
 
     def delete(self, commit = True):
         """
 
     def delete(self, commit = True):
         """
@@ -248,116 +107,71 @@ class Site(Row):
 
         assert 'site_id' in self
 
 
         assert 'site_id' in self
 
-        # Make sure extra fields are present
-        sites = Sites(self.api, [self['site_id']],
-                      ['person_ids', 'slice_ids', 'pcu_ids', 'node_ids'])
-        assert sites
-        self.update(sites.values()[0])
-
         # Delete accounts of all people at the site who are not
         # members of at least one other non-deleted site.
         # Delete accounts of all people at the site who are not
         # members of at least one other non-deleted site.
-        persons = PLC.Persons.Persons(self.api, self['person_ids'])
-        for person_id, person in persons.iteritems():
+        persons = Persons(self.api, self['person_ids'])
+        for person in persons:
             delete = True
 
             person_sites = Sites(self.api, person['site_ids'])
             delete = True
 
             person_sites = Sites(self.api, person['site_ids'])
-            for person_site_id, person_site in person_sites.iteritems():
-                if person_site_id != self['site_id'] and \
-                   not person_site['deleted']:
+            for person_site in person_sites:
+                if person_site['site_id'] != self['site_id']:
                     delete = False
                     break
 
             if delete:
                 person.delete(commit = False)
 
                     delete = False
                     break
 
             if delete:
                 person.delete(commit = False)
 
+        # Delete all site addresses
+        addresses = Addresses(self.api, self['address_ids'])
+        for address in addresses:
+            address.delete(commit = False)
+
         # Delete all site slices
         slices = Slices(self.api, self['slice_ids'])
         # Delete all site slices
         slices = Slices(self.api, self['slice_ids'])
-        for slice in slices.values():
+        for slice in slices:
             slice.delete(commit = False)
 
         # Delete all site PCUs
         pcus = PCUs(self.api, self['pcu_ids'])
             slice.delete(commit = False)
 
         # Delete all site PCUs
         pcus = PCUs(self.api, self['pcu_ids'])
-        for pcu in pcus.values():
+        for pcu in pcus:
             pcu.delete(commit = False)
 
         # Delete all site nodes
         nodes = Nodes(self.api, self['node_ids'])
             pcu.delete(commit = False)
 
         # Delete all site nodes
         nodes = Nodes(self.api, self['node_ids'])
-        for node in nodes.values():
+        for node in nodes:
             node.delete(commit = False)
 
         # Clean up miscellaneous join tables
             node.delete(commit = False)
 
         # Clean up miscellaneous join tables
-        for table in ['site_authorized_subnets',
-                      'dslice03_defaultattribute',
-                      'dslice03_siteinfo']:
-            self.api.db.do("DELETE FROM %s" \
-                           " WHERE site_id = %d" % \
+        for table in self.join_tables:
+            self.api.db.do("DELETE FROM %s WHERE site_id = %d" % \
                            (table, self['site_id']))
 
                            (table, self['site_id']))
 
-        # XXX Cannot delete site node groups yet
-
         # Mark as deleted
         self['deleted'] = True
         # Mark as deleted
         self['deleted'] = True
-        self.flush(commit)
+        self.sync(commit)
 
 class Sites(Table):
     """
     Representation of row(s) from the sites table in the
 
 class Sites(Table):
     """
     Representation of row(s) from the sites table in the
-    database. Specify extra_fields to be able to view and modify extra
-    fields.
+    database.
     """
 
     """
 
-    def __init__(self, api, site_id_or_login_base_list = None, extra_fields = []):
-        self.api = api
-
-        sql = "SELECT sites.*" \
-              ", dslice03_siteinfo.max_slices"
-
-        # N.B.: Joined IDs may be marked as deleted in their primary tables
-        join_tables = {
-            # extra_field: (extra_table, extra_column, join_using)
-            'person_ids': ('person_site', 'person_id', 'site_id'),
-            'slice_ids': ('dslice03_slices', 'slice_id', 'site_id'),
-            'defaultattribute_ids': ('dslice03_defaultattribute', 'defaultattribute_id', 'site_id'),
-            'pcu_ids': ('pcu', 'pcu_id', 'site_id'),
-            'node_ids': ('nodegroup_nodes', 'node_id', 'nodegroup_id'),
-            }
-
-        extra_fields = filter(join_tables.has_key, extra_fields)
-        extra_tables = ["%s USING (%s)" % \
-                        (join_tables[field][0], join_tables[field][2]) \
-                        for field in extra_fields]
-        extra_columns = ["%s.%s" % \
-                         (join_tables[field][0], join_tables[field][1]) \
-                         for field in extra_fields]
-
-        if extra_columns:
-            sql += ", " + ", ".join(extra_columns)
-
-        sql += " FROM sites" \
-               " LEFT JOIN dslice03_siteinfo USING (site_id)"
-
-        if extra_tables:
-            sql += " LEFT JOIN " + " LEFT JOIN ".join(extra_tables)
-
-        sql += " WHERE sites.deleted IS False"
-
-        if site_id_or_login_base_list:
-            # Separate the list into integers and strings
-            site_ids = filter(lambda site_id: isinstance(site_id, (int, long)),
-                              site_id_or_login_base_list)
-            login_bases = filter(lambda login_base: isinstance(login_base, StringTypes),
-                                 site_id_or_login_base_list)
-            sql += " AND (False"
-            if site_ids:
-                sql += " OR site_id IN (%s)" % ", ".join(map(str, site_ids))
-            if login_bases:
-                sql += " OR login_base IN (%s)" % ", ".join(api.db.quote(login_bases))
-            sql += ")"
-
-        rows = self.api.db.selectall(sql)
-        for row in rows:
-            if self.has_key(row['site_id']):
-                site = self[row['site_id']]
-                site.update(row)
-            else:
-                self[row['site_id']] = Site(api, row)
+    def __init__(self, api, site_filter = None, columns = None):
+        Table.__init__(self, api, Site, columns)
+
+        sql = "SELECT %s FROM view_sites WHERE deleted IS False" % \
+              ", ".join(self.columns)
+
+        if site_filter is not None:
+            if isinstance(site_filter, (list, tuple, set)):
+                # Separate the list into integers and strings
+                ints = filter(lambda x: isinstance(x, (int, long)), site_filter)
+                strs = filter(lambda x: isinstance(x, StringTypes), site_filter)
+                site_filter = Filter(Site.fields, {'site_id': ints, 'login_base': strs})
+                sql += " AND (%s)" % site_filter.sql(api, "OR")
+            elif isinstance(site_filter, dict):
+                site_filter = Filter(Site.fields, site_filter)
+                sql += " AND (%s)" % site_filter.sql(api, "AND")
+
+        self.selectall(sql)