define the 'hrn' node tag
[plcapi.git] / PLC / Peers.py
index db049b0..13eaabc 100644 (file)
@@ -1,19 +1,28 @@
+# $Id$
+# $URL$
 #
 # Thierry Parmentelat - INRIA
 # 
 
 import re
 from types import StringTypes
 #
 # Thierry Parmentelat - INRIA
 # 
 
 import re
 from types import StringTypes
+from urlparse import urlparse
 
 from PLC.Faults import *
 
 from PLC.Faults import *
-from PLC.Parameter import Parameter
+from PLC.Parameter import Parameter, Mixed
 from PLC.Filter import Filter
 from PLC.Table import Row, Table
 from PLC.Filter import Filter
 from PLC.Table import Row, Table
-
-from PLC.Nodes import Nodes,Node
-from PLC.Slices import Slices,Slice
-
-class Peer (Row):
+import PLC.Auth
+from PLC.Shell import *
+from PLC.Sites import Site, Sites
+from PLC.Persons import Person, Persons
+from PLC.Keys import Key, Keys
+from PLC.Nodes import Node, Nodes
+from PLC.TagTypes import TagType, TagTypes
+from PLC.SliceTags import SliceTag, SliceTags
+from PLC.Slices import Slice, Slices
+
+class Peer(Row):
     """
     Stores the list of peering PLCs in the peers table. 
     See the Row class for more details
     """
     Stores the list of peering PLCs in the peers table. 
     See the Row class for more details
@@ -21,207 +30,244 @@ class Peer (Row):
 
     table_name = 'peers'
     primary_key = 'peer_id'
 
     table_name = 'peers'
     primary_key = 'peer_id'
+    join_tables = ['peer_site', 'peer_person', 'peer_key', 'peer_node', 'peer_slice']
     fields = {
     fields = {
-       'peer_id' : Parameter (int, "Peer identifier"),
-       'peername' : Parameter (str, "Peer name"),
-       'peer_url' : Parameter (str, "Peer API url"),
-       'person_id' : Parameter (int, "Person_id of the account storing credentials - temporary"),
-       'node_ids' : Parameter ([int], "This peer's nodes ids"),
-       'slice_ids' : Parameter ([int], "This peer's slices ids"),
+        'peer_id': Parameter (int, "Peer identifier"),
+        'peername': Parameter (str, "Peer name"),
+        'peer_url': Parameter (str, "Peer API URL"),
+        'key': Parameter(str, "Peer GPG public key"),
+        'cacert': Parameter(str, "Peer SSL public certificate"),
+        'shortname' : Parameter(str, "Peer short name"),
+        'hrn_root' : Parameter(str, "Root of this peer in a hierarchical naming space"),
+        ### cross refs
+        'site_ids': Parameter([int], "List of sites for which this peer is authoritative"),
+        'person_ids': Parameter([int], "List of users for which this peer is authoritative"),
+        'key_ids': Parameter([int], "List of keys for which this peer is authoritative"),
+        'node_ids': Parameter([int], "List of nodes for which this peer is authoritative"),
+        'slice_ids': Parameter([int], "List of slices for which this peer is authoritative"),
        }
 
        }
 
-    def validate_peer_url (self, url):
-       """
-       Validate URL, checks it looks like https 
-       """
-       invalid_url = PLCInvalidArgument("Invalid URL")
-       if not re.compile ("^https://.*$").match(url) : 
-           raise invalid_url
+    def validate_peername(self, peername):
+        if not len(peername):
+            raise PLCInvalidArgument, "Peer name must be specified"
+
+        conflicts = Peers(self.api, [peername])
+        for peer in conflicts:
+            if 'peer_id' not in self or self['peer_id'] != peer['peer_id']:
+                raise PLCInvalidArgument, "Peer name already in use"
+
+        return peername
+
+    def validate_peer_url(self, url):
+        """
+        Validate URL. Must be HTTPS.
+        """
+
+        (scheme, netloc, path, params, query, fragment) = urlparse(url)
+        if scheme != "https":
+            raise PLCInvalidArgument, "Peer URL scheme must be https"
+        if path[-1] != '/':
+            raise PLCInvalidArgument, "Peer URL should end with /"
+        
        return url
 
        return url
 
-    def delete (self, commit=True):
-       """
-       Delete peer
-       """
-       
-       assert 'peer_id' in self
+    def delete(self, commit = True):
+        """
+        Deletes this peer and all related entities.
+        """
+
+        assert 'peer_id' in self
 
 
-        # remove nodes depending on this peer
-        for foreign_node in Nodes (self.api, self.get_node_ids()):
-            foreign_node.delete(commit)
+        # Remove all related entities
+        for obj in \
+            Slices(self.api, self['slice_ids']) + \
+            Keys(self.api, self['key_ids']) + \
+            Persons(self.api, self['person_ids']) + \
+            Nodes(self.api, self['node_ids']) + \
+            Sites(self.api, self['site_ids']):
+            assert obj['peer_id'] == self['peer_id']
+            obj.delete(commit = False)
 
 
-        # remove the peer
-       self['deleted'] = True
-       self.sync(commit)
+        # Mark as deleted
+        self['deleted'] = True
+        self.sync(commit)
 
 
-    def get_node_ids (self):
+    def add_site(self, site, peer_site_id, commit = True):
         """
         """
-        returns a list of the node ids in this peer
+        Associate a local site entry with this peer.
         """
         """
-        sql="SELECT node_ids FROM peer_nodes WHERE peer_id=%d"%self['peer_id']
-        node_ids = self.api.db.selectall(sql)
-        return node_ids[0]['node_ids']
 
 
-    def refresh_nodes (self, peer_get_nodes):
+        add = Row.add_object(Site, 'peer_site')
+        add(self, site,
+            {'peer_id': self['peer_id'],
+             'site_id': site['site_id'],
+             'peer_site_id': peer_site_id},
+            commit = commit)
+
+    def remove_site(self, site, commit = True):
+        """
+        Unassociate a site with this peer.
         """
         """
-        refreshes the foreign_nodes and peer_node tables
-        expected input is the current list of nodes as returned by GetNodes
+        
+        remove = Row.remove_object(Site, 'peer_site')
+        remove(self, site, commit)
 
 
-        returns the number of new nodes on this peer (can be negative)
+    def add_person(self, person, peer_person_id, commit = True):
+        """
+        Associate a local user entry with this peer.
         """
 
         """
 
-        peer_id = self['peer_id']
-        
-       # we get the whole table just in case 
-       # a host would have switched from one plc to the other
-        local_foreign_nodes = Nodes (self.api,None,None,'foreign')
-        
-        # index it by hostname for searching later
-        #local_foreign_nodes_index=local_foreign_nodes.dict('hostname')
-        local_foreign_nodes_index={}
-        for node in local_foreign_nodes:
-            local_foreign_nodes_index[node['hostname']]=node
-       
-       ### mark entries for this peer outofdate
-        old_count=0;
-       for foreign_node in local_foreign_nodes:
-           if foreign_node['peer_id'] == peer_id:
-               foreign_node.uptodate=False
-                old_count += 1
-
-        ### these fields get copied through
-        remote_fields = ['boot_state','model','version','date_created','last_updated']
-
-       ### scan the new entries, and mark them uptodate
-       for node in peer_get_nodes:
-           hostname = node['hostname']
-            try:
-                foreign_node = local_foreign_nodes_index[hostname]
-                if foreign_node['peer_id'] != peer_id:
-#                    ### the node has changed its plc
-                    foreign_node['peer_id'] = peer_id
-               ### update it anyway: copy other relevant fields
-                for field in remote_fields:
-                    foreign_node[field]=node[field]
-                # this row is now valid
-                foreign_node.uptodate=True
-                foreign_node.sync()
-           except:
-                new_foreign_node = Node(self.api, {'hostname':hostname})
-                new_foreign_node['peer_id']=peer_id
-                for field in remote_fields:
-                    new_foreign_node[field]=node[field]
-                ### need to sync so we get a node_id
-                new_foreign_node.sync()
-                new_foreign_node.uptodate = True
-#                self.manage_node(new_foreign_node,True,True)
-                local_foreign_nodes_index[hostname]=new_foreign_node
-
-       ### delete entries that are not uptodate
-        for foreign_node in local_foreign_nodes:
-            if not foreign_node.uptodate:
-                foreign_node.delete()
-
-        return len(peer_get_nodes)-old_count
-        
-    ### transcode node_id
-    def locate_alien_node_id_in_foreign_nodes (self, peer_foreign_nodes_dict, alien_id):
+        add = Row.add_object(Person, 'peer_person')
+        add(self, person,
+            {'peer_id': self['peer_id'],
+             'person_id': person['person_id'],
+             'peer_person_id': peer_person_id},
+            commit = commit)
+    
+    def remove_person(self, person, commit = True):
+        """
+        Unassociate a site with this peer.
+        """
+    
+        remove = Row.remove_object(Person, 'peer_person')
+        remove(self, person, commit)
+
+    def add_key(self, key, peer_key_id, commit = True):
         """
         """
-        returns a local node_id as transcoded from an alien node_id
-        only lookups our local nodes because we dont need to know about other sites
-        returns a valid local node_id, or throws an exception
+        Associate a local key entry with this peer.
         """
         """
-        peer_foreign_node = peer_foreign_nodes_dict[alien_id]
-        hostname = peer_foreign_node['hostname']
-        return Nodes(self.api,[hostname])[0]['node_id']
 
 
-    def refresh_slices (self, peer_get_slices, peer_foreign_nodes):
+        add = Row.add_object(Key, 'peer_key')
+        add(self, key,
+            {'peer_id': self['peer_id'],
+             'key_id': key['key_id'],
+             'peer_key_id': peer_key_id},
+            commit = commit)
+
+    def remove_key(self, key, commit = True):
+        """
+        Unassociate a key with this peer.
         """
         """
-        refreshes the foreign_slices and peer_slice tables
-        expected input is the current list of slices as returned by GetSlices
+    
+        remove = Row.remove_object(Key, 'peer_key')
+        remove(self, key, commit)
 
 
-        returns the number of new slices on this peer (can be negative)
+    def add_node(self, node, peer_node_id, commit = True):
+        """
+        Associate a local node entry with this peer.
         """
 
         """
 
-        peer_id = self['peer_id']
-        
-       # we get the whole table just in case 
-       # a host would have switched from one plc to the other
-        local_foreign_slices = Slices (self.api,{'~peer_id':None})
-        # index it by name for searching later
-        local_foreign_slices_index = local_foreign_slices.dict('name')
-       
-       ### mark entries for this peer outofdate
-        old_count=0;
-       for foreign_slice in local_foreign_slices:
-           if foreign_slice['peer_id'] == peer_id:
-               foreign_slice.uptodate=False
-                old_count += 1
-
-        ### these fields get copied through
-        remote_fields = ['instantiation', 'url', 'description',
-                         'max_nodes', 'created', 'expires']
-
-       ### scan the new entries, and mark them uptodate
-        new_count=0
-       for slice in peer_get_slices:
-
-            ### ignore system-wide slices
-            if slice['creator_person_id'] == 1:
-                continue
-
-           name = slice['name']
-
-            # create or update 
-            try:
-                foreign_slice = local_foreign_slices_index[name]
-                if foreign_slice['peer_id'] != peer_id:
-                    # more suspucious ? - the slice moved on another peer
-                    foreign_slice['peer_id'] = peer_id;
-           except:
-                foreign_slice = Slice(self.api, {'name':name})
-                foreign_slice['peer_id']=self['peer_id']
-#                ### xxx temporary 
-#                foreign_slice['site_id']=1
-                ### need to sync so we get a slice_id
-                foreign_slice.sync()
-                # insert in index
-                local_foreign_slices_index[name]=foreign_slice
-
-            # go on with update
-            for field in remote_fields:
-                foreign_slice[field]=slice[field]
-            # this row is now valid
-            foreign_slice.uptodate=True
-            new_count += 1
-            foreign_slice.sync()
-
-            ### handle node_ids
-            # in slice we get a set of node_ids
-            # but these ids are RELATIVE TO THE PEER
-            # so we need to figure the local node_id for these nodes
-            # we do this through peer_foreign_nodes 
-            # dictify once
-            peer_foreign_nodes_dict = {}
-            for foreign_node in peer_foreign_nodes:
-                peer_foreign_nodes_dict[foreign_node['node_id']]=foreign_node
-            updated_node_ids = []
-            for alien_node_id in slice['node_ids']:
-                try:
-                    local_node_id=self.locate_alien_node_id_in_foreign_nodes(peer_foreign_nodes_dict,
-                                                                             alien_node_id)
-                    updated_node_ids.append(local_node_id)
-                except:
-                    # this node_id is not in our scope
-                    pass
-            foreign_slice.update_slice_nodes (updated_node_ids)
-
-       ### delete entries that are not uptodate
-        for foreign_slice in local_foreign_slices:
-            if not foreign_slice.uptodate:
-                foreign_slice.delete()
-
-        return new_count-old_count
+        add = Row.add_object(Node, 'peer_node')
+        add(self, node,
+            {'peer_id': self['peer_id'],
+             'node_id': node['node_id'],
+             'peer_node_id': peer_node_id},
+            commit = commit)
+
+        # calling UpdateNode with the node's hostname 
+        # will force the 'hrn' tag to be updated with the 
+        # correct root auth
+        shell = Shell() 
+        UpdateNode = self.api.callable('UpdateNode')
+        UpdateNode(shell.auth, node['node_id'], {'hostname': node['hostname']})  
+
+    def remove_node(self, node, commit = True):
+        """
+        Unassociate a node with this peer.
+        """
+    
+        remove = Row.remove_object(Node, 'peer_node')
+        remove(self, node, commit)
+
+        # calling UpdateNode with the node's hostname
+        # will force the 'hrn' tag to be updated with the
+        # correct root auth
+        shell = Shell() 
+        UpdateNode = self.api.callable('UpdateNode')
+        UpdateNode(shell.auth, node['node_id'], {'hostname': node['hostname']})
+
+    def add_slice(self, slice, peer_slice_id, commit = True):
+        """
+        Associate a local slice entry with this peer.
+        """
+
+        add = Row.add_object(Slice, 'peer_slice')
+        add(self, slice,
+            {'peer_id': self['peer_id'],
+             'slice_id': slice['slice_id'],
+             'peer_slice_id': peer_slice_id},
+            commit = commit)
+
+    def remove_slice(self, slice, commit = True):
+        """
+        Unassociate a slice with this peer.
+        """
+
+        remove = Row.remove_object(Slice, 'peer_slice')
+        remove(self, slice, commit)
+
+    def connect(self, **kwds):
+        """
+        Connect to this peer via XML-RPC.
+        """
+
+        import xmlrpclib
+        from PLC.PyCurl import PyCurlTransport
+        self.server = xmlrpclib.ServerProxy(self['peer_url'],
+                                            PyCurlTransport(self['peer_url'], self['cacert']),
+                                            allow_none = 1, **kwds)
+
+    def add_auth(self, function, methodname, **kwds):
+        """
+        Sign the specified XML-RPC call and add an auth struct as the
+        first argument of the call.
+        """
+
+        def wrapper(*args, **kwds):
+            from PLC.GPG import gpg_sign
+            signature = gpg_sign(args,
+                                 self.api.config.PLC_ROOT_GPG_KEY,
+                                 self.api.config.PLC_ROOT_GPG_KEY_PUB,
+                                 methodname)
+
+            auth = {'AuthMethod': "gpg",
+                    'name': self.api.config.PLC_NAME,
+                    'signature': signature}
+
+            # Automagically add auth struct to every call
+            args = (auth,) + args
+
+            return function(*args)
+
+        return wrapper
+
+    def __getattr__(self, attr):
+        """
+        Returns a callable API function if attr is the name of a
+        PLCAPI function; otherwise, returns the specified attribute.
+        """
+
+        try:
+            # Figure out if the specified attribute is the name of a
+            # PLCAPI function. If so and the function requires an
+            # authentication structure as its first argument, return a
+            # callable that automagically adds an auth struct to the
+            # call.
+            methodname = attr
+            api_function = self.api.callable(methodname)
+            if api_function.accepts and \
+               (isinstance(api_function.accepts[0], PLC.Auth.Auth) or \
+                (isinstance(api_function.accepts[0], Mixed) and \
+                 filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
+                function = getattr(self.server, methodname)
+                return self.add_auth(function, methodname)
+        except Exception, err:
+            pass
+
+        if hasattr(self, attr):
+            return getattr(self, attr)
+        else:
+            raise AttributeError, "type object 'Peer' has no attribute '%s'" % attr
 
 class Peers (Table):
     """ 
 
 class Peers (Table):
     """ 
@@ -240,9 +286,9 @@ class Peers (Table):
                 ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
                 strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
                 peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
                 ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
                 strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
                 peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
-                sql += " AND (%s)" % peer_filter.sql(api, "OR")
+                sql += " AND (%s) %s" % peer_filter.sql(api, "OR")
             elif isinstance(peer_filter, dict):
                 peer_filter = Filter(Peer.fields, peer_filter)
             elif isinstance(peer_filter, dict):
                 peer_filter = Filter(Peer.fields, peer_filter)
-                sql += " AND (%s)" % peer_filter.sql(api, "AND")
+                sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
 
        self.selectall(sql)
 
        self.selectall(sql)