for federation : Peers and ForeignNodes
authorThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Fri, 3 Nov 2006 20:36:05 +0000 (20:36 +0000)
committerThierry Parmentelat <thierry.parmentelat@sophia.inria.fr>
Fri, 3 Nov 2006 20:36:05 +0000 (20:36 +0000)
12 files changed:
Makefile
PLC/ForeignNodes.py [new file with mode: 0644]
PLC/Method.py
PLC/Methods/AddPeer.py [new file with mode: 0644]
PLC/Methods/GetForeignNodes.py [new file with mode: 0644]
PLC/Methods/GetPeers.py [new file with mode: 0644]
PLC/Methods/UpdatePeer.py [new file with mode: 0644]
PLC/Methods/__init__.py
PLC/Peers.py [new file with mode: 0644]
PLC/__init__.py
doc/Makefile
planetlab4.sql

index aacbe3a..8ce4051 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2005 The Trustees of Princeton University
 #
-# $Id: Makefile,v 1.1 2006/09/06 15:33:59 mlhuang Exp $
+# $Id: Makefile,v 1.2 2006/10/25 21:05:40 mlhuang Exp $
 #
 
 # Metafiles
@@ -19,10 +19,12 @@ $(SUBDIRS): %:
        $(MAKE) -C $@
 
 clean:
-       find . -name '*.pyc' -execdir rm -f {} +
+       find . -name '*.pyc' -execdir rm -f {} \;
        rm -f $(INIT)
        for dir in $(SUBDIRS) ; do $(MAKE) -C $$dir clean ; done
 
+index: PLC/__init__.py PLC/Methods/__init__.py
+
 # All .py files in PLC/
 PLC := $(filter-out %/__init__.py, $(wildcard PLC/*.py))
 PLC_init := all = '$(notdir $(PLC:.py=))'.split()
diff --git a/PLC/ForeignNodes.py b/PLC/ForeignNodes.py
new file mode 100644 (file)
index 0000000..c58bdf2
--- /dev/null
@@ -0,0 +1,63 @@
+#
+# the nodes standing on peer plc's
+#
+# Thierry Parmentelat
+# 
+
+from types import StringTypes
+
+from PLC.Table import Row, Table
+from PLC.Parameter import Parameter
+
+class ForeignNode (Row) :
+    """
+    This object stores information about nodes hosted on 
+    other peering instances of myplc
+    """
+
+    table_name = 'foreign_nodes'
+    primary_key = 'foreign_node_id'
+
+    fields = {
+       'foreign_node_id': Parameter (int, "Foreign Node Id"),
+       'hostname': Parameter (str, "Node name"),
+       'boot_state' : Parameter (str, "Boot state"),
+       'peer_id': Parameter (str, "Peer id"),
+       }
+
+    def __init__(self,api,fields={},uptodate=True):
+       Row.__init__(self,api,fields)
+       self.uptodate=uptodate
+
+class ForeignNodes (Table):
+
+    def __init__ (self, api, foreign_node_id_or_peername_list=None):
+
+       self.api=api
+
+       # must qualify fields because peer_id otherwise gets ambiguous
+       fields = ["foreign_nodes.%s"%x for x in ForeignNode.fields]
+                 
+       sql =""
+       sql += "SELECT %s FROM foreign_nodes, peers " % ", ".join(fields)
+       sql += "WHERE foreign_nodes.peer_id=peers.peer_id "
+       sql += "AND foreign_nodes.deleted IS False " 
+
+       if foreign_node_id_or_peername_list:
+           foreign_node_id_list = [ x for x in foreign_node_id_or_peername_list if isinstance(x, (int,long))]
+           peername_list = [ x for x in foreign_node_id_or_peername_list if isinstance(x, StringTypes)]
+           sql += " AND (False"
+           if foreign_node_id_list:
+               sql += " OR foreign_node_id in (%s)" % ", ".join([str(i) for i in foreign_node_id_list])
+           if peername_list:
+               ## figure how to retrieve peer_id from the peername(s)
+               sql += " OR peername IN (%s)" % ", ".join(api.db.quote(peername_list))
+           sql += ")"
+
+       rows = self.api.db.selectall (sql)
+
+       for row in rows:
+           self[row['hostname']] = ForeignNode (api,row)
+
+
+       
index 4cda6bb..31f365d 100644 (file)
@@ -4,7 +4,7 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
-# $Id: Method.py,v 1.14 2006/10/31 21:47:21 mlhuang Exp $
+# $Id: Method.py,v 1.15 2006/11/02 18:32:55 mlhuang Exp $
 #
 
 import xmlrpclib
@@ -14,6 +14,8 @@ import os
 import time
 import pprint
 
+from types import StringTypes
+
 from PLC.Faults import *
 from PLC.Parameter import Parameter, Mixed
 from PLC.Auth import Auth
diff --git a/PLC/Methods/AddPeer.py b/PLC/Methods/AddPeer.py
new file mode 100644 (file)
index 0000000..f636add
--- /dev/null
@@ -0,0 +1,34 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+
+can_update = lambda(k,v): k in ['peername','peer_url','person_id']
+
+class AddPeer (Method):
+    """
+    Creates a peer entry in the database and returns its id
+    Temporarily, requires to provide a person_id 
+    this is used to store the credentials that we'll
+    use when connecting to the peer's API
+    """
+
+    roles = ['admin']
+    peer_fields = dict( [x for x in Peer.fields.iteritems() if can_update(x)] )
+
+    accepts = [ Auth(),
+               peer_fields
+               ]
+
+    returns = Parameter (int, "peer_id")
+
+    def call (self, auth, fields):
+
+       peer = Peer (self.api,fields);
+       peer.sync()
+       
+       return peer['peer_id']
+       
+
diff --git a/PLC/Methods/GetForeignNodes.py b/PLC/Methods/GetForeignNodes.py
new file mode 100644 (file)
index 0000000..100f3ea
--- /dev/null
@@ -0,0 +1,25 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.ForeignNodes import ForeignNode, ForeignNodes
+
+class GetForeignNodes (Method):
+    """
+    returns information on foreign nodes
+    """
+
+    roles = ['admin']
+
+    accepts = [ Auth(),
+               [ Mixed(ForeignNode.fields['foreign_node_id'],
+                       ForeignNode.fields['hostname'])]
+               ]
+    
+    returns = [ ForeignNode.fields]
+
+    def call (self, auth, foreign_id_or_peername_list = None):
+
+       return ForeignNodes (self.api, foreign_id_or_peername_list).values()
+       
diff --git a/PLC/Methods/GetPeers.py b/PLC/Methods/GetPeers.py
new file mode 100644 (file)
index 0000000..3be2b18
--- /dev/null
@@ -0,0 +1,24 @@
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+
+class GetPeers (Method):
+    """
+    returns information on known peers
+    """
+
+    roles = ['admin']
+
+    accepts = [Auth(),
+              [Mixed(Peer.fields['peer_id'],
+                     Peer.fields['peername'])],
+              ]
+
+    returns = [Peer.fields]
+
+    def call (self, auth, peer_id_or_peername_list = None):
+
+       return Peers (self.api, peer_id_or_peername_list).values()
diff --git a/PLC/Methods/UpdatePeer.py b/PLC/Methods/UpdatePeer.py
new file mode 100644 (file)
index 0000000..09cf3be
--- /dev/null
@@ -0,0 +1,84 @@
+import xmlrpclib
+
+from PLC.Faults import *
+from PLC.Method import Method
+from PLC.Parameter import Parameter, Mixed
+from PLC.Auth import Auth
+
+from PLC.Peers import Peer, Peers
+from PLC.Persons import Person, Persons
+from PLC.ForeignNodes import ForeignNode, ForeignNodes
+
+
+class UpdatePeer(Method):
+    """
+    Query a peer PLC for its list of nodes, and refreshes
+    the local database accordingly
+    
+    Returns None
+    """
+    
+    roles = ['admin']
+    
+    accepts = [ Auth(),
+               Parameter (int, "Peer id") ]
+    
+    returns = None
+
+    def call (self, auth, peer_id):
+       
+       ### retrieve peer info
+       peers = Peers (self.api)
+       peer = peers[peer_id]
+       
+       ### retrieve account info
+       person_id = peer['person_id']
+       persons = Persons (self.api,[person_id])
+       person = persons[person_id]
+       
+       ### build up foreign auth
+       auth={ 'Username': person['email'],
+              'AuthMethod' : 'password',
+              'AuthString' : person['password'],
+              'Role' : 'admin' }
+
+       ## connect to the peer's API
+       apiserver = xmlrpclib.Server (peer['peer_url']+"/PLCAPI/")
+       print 'auth',auth
+       current_peer_nodes = apiserver.GetNodes(auth,[])
+       
+       ## manual feed for tests
+#      n1 = {'hostname': 'n1.plc', 'boot_state': 'inst'}
+#      n2 = {'hostname': 'n2.plc', 'boot_state': 'inst'}
+#      n3 = {'hostname': 'n3.plc', 'boot_state': 'inst'}
+#      current_peer_nodes = [n2,n3]
+
+       ### now to the db
+       # we get the whole table just in case 
+       # a host would have switched from one plc to the other
+       foreign_nodes = ForeignNodes (self.api)
+       
+       ### mark entries for this peer outofdate
+       for foreign_node in foreign_nodes.values():
+           if foreign_node['peer_id'] == peer_id:
+               foreign_node.uptodate=False
+
+       ### scan the new entries, and mark them uptodate
+       for node in current_peer_nodes:
+           hostname = node['hostname']
+           foreign_node = foreign_nodes.get(hostname)
+           if foreign_node:
+               ### update it anyway
+               foreign_node['peer_id'] = peer_id
+               foreign_node['boot_state'] = node['boot_state']
+               foreign_node.uptodate = True
+           else:
+               foreign_nodes[hostname] = ForeignNode(self.api,
+                                                     {'hostname':hostname,
+                                                      'boot_state':node['boot_state'],
+                                                      'peer_id':peer_id})
+           foreign_nodes[hostname].sync()
+
+       ### delete entries that are not uptodate
+       [ x.delete() for x in foreign_nodes.values() if not x.uptodate ]
+       
index a54ea41..ae8fed1 100644 (file)
@@ -1 +1 @@
-methods = 'AddAddressType AddAddressTypeToAddress AddBootState AddConfFile AddConfFileToNodeGroup AddConfFileToNode AddKeyType AddMessage AddNetworkMethod AddNetworkType AddNodeGroup AddNodeNetwork AddNode AddNodeToNodeGroup AddNodeToPCU AddPCU AddPersonKey AddPerson AddPersonToSite AddPersonToSlice AddRole AddRoleToPerson AddSiteAddress AddSite AddSliceAttribute AddSliceAttributeType AddSlice AddSliceToNodes AdmAddAddressType AdmAddNodeGroup AdmAddNodeNetwork AdmAddNode AdmAddNodeToNodeGroup AdmAddPersonKey AdmAddPerson AdmAddPersonToSite AdmAddSitePowerControlUnit AdmAddSite AdmAssociateNodeToPowerControlUnitPort AdmAuthCheck AdmDeleteAddressType AdmDeleteAllPersonKeys AdmDeleteNodeGroup AdmDeleteNodeNetwork AdmDeleteNode AdmDeletePersonKeys AdmDeletePerson AdmDeleteSitePowerControlUnit AdmDeleteSite AdmDisassociatePowerControlUnitPort AdmGenerateNodeConfFile AdmGetAllAddressTypes AdmGetAllKeyTypes AdmGetAllNodeNetworks AdmGetAllRoles AdmGetNodeGroupNodes AdmGetNodeGroups AdmGetNodes AdmGetPersonKeys AdmGetPersonRoles AdmGetPersonSites AdmGetPersons AdmGetPowerControlUnitNodes AdmGetPowerControlUnits AdmGetSiteNodes AdmGetSitePersons AdmGetSitePIs AdmGetSitePowerControlUnits AdmGetSites AdmGetSiteTechContacts AdmGrantRoleToPerson AdmIsPersonInRole AdmQueryConfFile AdmQueryNode AdmQueryPerson AdmQueryPowerControlUnit AdmQuerySite AdmRebootNode AdmRemoveNodeFromNodeGroup AdmRemovePersonFromSite AdmRevokeRoleFromPerson AdmSetPersonEnabled AdmSetPersonPrimarySite AdmUpdateNodeGroup AdmUpdateNodeNetwork AdmUpdateNode AdmUpdatePerson AdmUpdateSitePowerControlUnit AdmUpdateSite AuthCheck BlacklistKey BootCheckAuthentication BootGetNodeDetails BootNotifyOwners BootUpdateNode DeleteAddress DeleteAddressTypeFromAddress DeleteAddressType DeleteBootState DeleteConfFileFromNodeGroup DeleteConfFileFromNode DeleteConfFile DeleteKey DeleteKeyType DeleteMessage DeleteNetworkMethod DeleteNetworkType DeleteNodeFromNodeGroup DeleteNodeFromPCU DeleteNodeGroup DeleteNodeNetwork DeleteNode DeletePCU DeletePersonFromSite DeletePersonFromSlice DeletePerson DeleteRoleFromPerson DeleteRole DeleteSession DeleteSite DeleteSliceAttribute DeleteSliceAttributeType DeleteSliceFromNodes DeleteSlice GetAddresses GetAddressTypes GetBootStates GetConfFiles GetEvents GetKeys GetKeyTypes GetMessages GetNetworkMethods GetNetworkTypes GetNodeGroups GetNodeNetworks GetNodes GetPCUs GetPersons GetRoles GetSession GetSites GetSliceAttributes GetSliceAttributeTypes GetSlices GetSlivers RebootNode SetPersonPrimarySite SliceCreate SliceDelete UpdateAddress UpdateAddressType UpdateConfFile UpdateKey UpdateMessage UpdateNodeGroup UpdateNodeNetwork UpdateNode UpdatePCU UpdatePerson UpdateSite UpdateSliceAttribute UpdateSliceAttributeType UpdateSlice  system.listMethods  system.methodHelp  system.methodSignature  system.multicall'.split()
+methods = 'AddAddressType AddAddressTypeToAddress AddBootState AddConfFile AddConfFileToNode AddConfFileToNodeGroup AddKeyType AddMessage AddNetworkMethod AddNetworkType AddNode AddNodeGroup AddNodeNetwork AddNodeToNodeGroup AddNodeToPCU AddPCU AddPeer AddPerson AddPersonKey AddPersonToSite AddPersonToSlice AddRole AddRoleToPerson AddSite AddSiteAddress AddSlice AddSliceAttribute AddSliceAttributeType AddSliceToNodes AdmAddAddressType AdmAddNode AdmAddNodeGroup AdmAddNodeNetwork AdmAddNodeToNodeGroup AdmAddPerson AdmAddPersonKey AdmAddPersonToSite AdmAddSite AdmAddSitePowerControlUnit AdmAssociateNodeToPowerControlUnitPort AdmAuthCheck AdmDeleteAddressType AdmDeleteAllPersonKeys AdmDeleteNode AdmDeleteNodeGroup AdmDeleteNodeNetwork AdmDeletePerson AdmDeletePersonKeys AdmDeleteSite AdmDeleteSitePowerControlUnit AdmDisassociatePowerControlUnitPort AdmGenerateNodeConfFile AdmGetAllAddressTypes AdmGetAllKeyTypes AdmGetAllNodeNetworks AdmGetAllRoles AdmGetNodeGroupNodes AdmGetNodeGroups AdmGetNodes AdmGetPersonKeys AdmGetPersonRoles AdmGetPersonSites AdmGetPersons AdmGetPowerControlUnitNodes AdmGetPowerControlUnits AdmGetSiteNodes AdmGetSitePIs AdmGetSitePersons AdmGetSitePowerControlUnits AdmGetSiteTechContacts AdmGetSites AdmGrantRoleToPerson AdmIsPersonInRole AdmQueryConfFile AdmQueryNode AdmQueryPerson AdmQueryPowerControlUnit AdmQuerySite AdmRebootNode AdmRemoveNodeFromNodeGroup AdmRemovePersonFromSite AdmRevokeRoleFromPerson AdmSetPersonEnabled AdmSetPersonPrimarySite AdmUpdateNode AdmUpdateNodeGroup AdmUpdateNodeNetwork AdmUpdatePerson AdmUpdateSite AdmUpdateSitePowerControlUnit AuthCheck BlacklistKey BootCheckAuthentication BootGetNodeDetails BootNotifyOwners BootUpdateNode DeleteAddress DeleteAddressType DeleteAddressTypeFromAddress DeleteBootState DeleteConfFile DeleteConfFileFromNode DeleteConfFileFromNodeGroup DeleteKey DeleteKeyType DeleteMessage DeleteNetworkMethod DeleteNetworkType DeleteNode DeleteNodeFromNodeGroup DeleteNodeFromPCU DeleteNodeGroup DeleteNodeNetwork DeletePCU DeletePerson DeletePersonFromSite DeletePersonFromSlice DeleteRole DeleteRoleFromPerson DeleteSession DeleteSite DeleteSlice DeleteSliceAttribute DeleteSliceAttributeType DeleteSliceFromNodes GetAddressTypes GetAddresses GetBootStates GetConfFiles GetEvents GetForeignNodes GetKeyTypes GetKeys GetMessages GetNetworkMethods GetNetworkTypes GetNodeGroups GetNodeNetworks GetNodes GetPCUs GetPeers GetPersons GetRoles GetSession GetSites GetSliceAttributeTypes GetSliceAttributes GetSlices GetSlivers RebootNode SetPersonPrimarySite SliceCreate SliceDelete UpdateAddress UpdateAddressType UpdateConfFile UpdateKey UpdateMessage UpdateNode UpdateNodeGroup UpdateNodeNetwork UpdatePCU UpdatePeer UpdatePerson UpdateSite UpdateSlice UpdateSliceAttribute UpdateSliceAttributeType  system.listMethods  system.methodHelp  system.methodSignature  system.multicall'.split()
diff --git a/PLC/Peers.py b/PLC/Peers.py
new file mode 100644 (file)
index 0000000..be4b24c
--- /dev/null
@@ -0,0 +1,74 @@
+import re
+
+from types import StringTypes
+
+from PLC.Faults import *
+from PLC.Parameter import Parameter
+from PLC.Table import Row, Table
+
+class Peer (Row):
+    """
+    Stores the list of peering PLCs in the peers table. 
+    See the Row class for more details
+    """
+
+    table_name = 'peers'
+    primary_key = 'peer_id'
+    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 used to log there"),
+       'foreign_node_ids' : Parameter ([int], "doc")
+       }
+
+    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
+       return url
+
+    def delete (self, commit=True):
+       """
+       Delete peer
+       """
+       
+       assert 'peer_id' in self
+
+       self['deleted'] = True
+       self.sync(commit)
+
+class Peers (Table):
+    """ 
+    Maps to the peers table in the database
+    """
+    
+    def __init__ (self, api, peer_id_or_peername_list = None):
+       self.api = api
+
+       sql="SELECT %s FROM view_peers WHERE deleted IS False" % \
+           ", ".join(Peer.fields)
+       if peer_id_or_peername_list:
+            peer_ids = [x for x in peer_id_or_peername_list if isinstance(x, (int, long))]
+            peernames = [x for x in peer_id_or_peername_list if isinstance(x, StringTypes)]
+           sql += " AND (False"
+           if peer_ids:
+               sql += " OR peer_id in (%s)"% ", ".join([str(i) for i in peer_ids])
+           if peernames:
+               sql += " OR peername in (%s)"% ". ".join(api.db.quote(peernames)).lower()
+           sql += ")"
+
+       rows = self.api.db.selectall(sql)
+
+       for row in rows:
+           self[row['peer_id']] = peer = Peer(api,row)
+            for aggregate in ['foreign_node_ids']:
+                if not peer.has_key(aggregate) or peer[aggregate] is None:
+                    peer[aggregate] = []
+                else:
+                    peer[aggregate] = map(int, peer[aggregate].split(','))
+
+
index 53ddb6b..1d981e0 100644 (file)
@@ -1 +1 @@
-all = 'Addresses AddressTypes API Auth BootStates ConfFiles Config Debug Events Faults Keys KeyTypes Messages Method NetworkMethods NetworkTypes NodeGroups NodeNetworks Nodes Parameter PCUs Persons POD PostgreSQL Roles Sessions Sites SliceAttributes SliceAttributeTypes SliceInstantiations Slices Table'.split()
+all = 'API AddressTypes Addresses Auth BootStates ConfFiles Config Debug Events Faults ForeignNodes KeyTypes Keys Messages Method NetworkMethods NetworkTypes NodeGroups NodeNetworks Nodes PCUs POD Parameter Peers Persons PostgreSQL Roles Sessions Sites SliceAttributeTypes SliceAttributes SliceInstantiations Slices Table'.split()
index 1340f8e..83c84e6 100644 (file)
@@ -4,7 +4,7 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
-# $Id: plcsh,v 1.3 2006/01/09 19:57:24 mlhuang Exp $
+# $Id: Makefile,v 1.1 2006/09/06 15:34:41 mlhuang Exp $
 #
 
 all: PLCAPI.pdf
@@ -40,6 +40,9 @@ $(foreach format,$(FORMATS),$(eval $(call docbook2,$(format))))
 clean:
        rm -f $(patsubst %,*.%,$(FORMATS)) .*.xml.valid
 
+docclean:
+       rm -f Methods.xml PLCAPI.pdf
+
 force:
 
 .PHONY: force clean docclean
index bb8d1bd..5d3e264 100644 (file)
@@ -9,7 +9,7 @@
 --
 -- Copyright (C) 2006 The Trustees of Princeton University
 --
--- $Id: planetlab4.sql,v 1.25 2006/10/31 21:45:45 mlhuang Exp $
+-- $Id: planetlab4.sql,v 1.26 2006/11/03 16:05:20 mlhuang Exp $
 --
 
 --------------------------------------------------------------------------------
@@ -302,7 +302,7 @@ CREATE TABLE nodegroup_node (
 CREATE INDEX nodegroup_node_nodegroup_id_idx ON nodegroup_node (nodegroup_id);
 CREATE INDEX nodegroup_node_node_id_idx ON nodegroup_node (node_id);
 
--- Nodes in each node gruop
+-- Nodes in each node group
 CREATE VIEW nodegroup_nodes AS
 SELECT nodegroup_id,
 array_to_string(array_accum(node_id), ',') AS node_ids
@@ -935,3 +935,37 @@ INSERT INTO sites
 (login_base, name, abbreviated_name, max_slices)
 VALUES
 ('pl', 'PlanetLab Central', 'PLC', 100);
+
+-- federation stuff starting here
+INSERT INTO roles (role_id, name) VALUES (3000, 'peer');
+
+CREATE TABLE peers (
+     peer_id  serial PRIMARY KEY, -- identifier
+     peername text NOT NULL,      -- free text
+     peer_url text NOT NULL,      -- the url of that peer's API
+     person_id integer REFERENCES persons NOT NULL, -- the account we use for logging in
+       
+     deleted boolean NOT NULL DEFAULT false
+) WITH OIDS;
+
+CREATE TABLE foreign_nodes (
+     foreign_node_id serial PRIMARY KEY, -- identifier
+     hostname text NOT NULL, 
+     boot_state text NOT NULL,
+     peer_id integer REFERENCES peers NOT NULL,
+       
+     deleted boolean NOT NULL DEFAULT false
+) WITH OIDS;
+
+CREATE VIEW peer_foreign_nodes AS
+SELECT peer_id,
+array_to_string(array_accum(foreign_node_id), ',') AS foreign_node_ids
+FROM foreign_nodes
+GROUP BY peer_id;
+
+CREATE VIEW view_peers AS
+SELECT 
+peers.*, 
+peer_foreign_nodes.foreign_node_ids 
+FROM peers
+LEFT JOIN peer_foreign_nodes USING (peer_id);