- provides ability to cache foreign slices
[plcapi.git] / PLC / Peers.py
1 #
2 # Thierry Parmentelat - INRIA
3
4
5 import re
6 from types import StringTypes
7
8 from PLC.Faults import *
9 from PLC.Parameter import Parameter
10 from PLC.Filter import Filter
11 from PLC.Table import Row, Table
12
13 from PLC.Nodes import Nodes,Node
14 from PLC.ForeignNodes import ForeignNodes,ForeignNode
15 from PLC.ForeignSlices import ForeignSlices,ForeignSlice
16
17 class Peer (Row):
18     """
19     Stores the list of peering PLCs in the peers table. 
20     See the Row class for more details
21     """
22
23     table_name = 'peers'
24     primary_key = 'peer_id'
25     fields = {
26         'peer_id' : Parameter (int, "Peer identifier"),
27         'peername' : Parameter (str, "Peer name"),
28         'peer_url' : Parameter (str, "Peer API url"),
29         'person_id' : Parameter (int, "Person_id of the account storing credentials - temporary"),
30         'node_ids' : Parameter ([int], "This peer's nodes ids"),
31         'slice_ids' : Parameter ([int], "This peer's slices ids"),
32         }
33
34     def validate_peer_url (self, url):
35         """
36         Validate URL, checks it looks like https 
37         """
38         invalid_url = PLCInvalidArgument("Invalid URL")
39         if not re.compile ("^https://.*$").match(url) : 
40             raise invalid_url
41         return url
42
43     def delete (self, commit=True):
44         """
45         Delete peer
46         """
47         
48         assert 'peer_id' in self
49
50         # remove nodes depending on this peer
51         for foreign_node_id in self.get_foreign_nodes():
52             try:
53                 foreign_node = ForeignNodes(self.api,[foreign_node_id])[0]
54                 foreign_node.delete(commit)
55             except:
56                 print "Glitch : a foreign node instance was uncleanly deleted"
57
58         # remove the peer
59         self['deleted'] = True
60         self.sync(commit)
61
62     def get_foreign_nodes (self):
63         """
64         returns a list of the foreign nodes in this peer
65         """
66         sql="SELECT node_ids FROM peer_nodes WHERE peer_id=%d"%self['peer_id']
67         node_ids = self.api.db.selectall(sql)
68         return node_ids[0]['node_ids']
69
70     def manage_node (self, foreign_node, add_if_true, commit=True):
71         """
72         associate/dissociate a foreign node to/from a peer
73         foreign_node is a local object that describes a remote node
74         convention is:
75            if add_if_true is None : performs dissociation
76            otherwise:               performs association
77         """
78
79         assert 'peer_id' in self
80         assert 'node_id' in foreign_node
81
82         peer_id = self['peer_id']
83         node_id = foreign_node ['node_id']
84
85         if add_if_true:
86             ### ADDING
87             sql = "INSERT INTO peer_node VALUES (%d,%d)" % (peer_id,node_id)
88             self.api.db.do(sql)
89             if self['node_ids'] is None:
90                 self['node_ids']=[node_id,]
91             else:
92                 self['node_ids'].append(node_id)
93             ### DELETING
94         else:
95             sql = "DELETE FROM peer_node WHERE peer_id=%d AND node_id=%d" % (peer_id,node_id)
96             self.api.db.do(sql)
97             self['node_ids'].remove(node_id)
98
99         if commit:
100             self.api.db.commit()
101
102     def manage_slice (self, foreign_slice, add_if_true, commit=True):
103         """
104         associate/dissociate a foreign node to/from a peer
105         foreign_slice is a local object that describes a remote slice
106         alien_id is the unique id as provided by the remote peer
107         convention is:
108            if add_if_true is None : performs dissociation
109            otherwise:               performs association
110         """
111
112         assert 'peer_id' in self
113         assert 'slice_id' in foreign_slice
114
115         peer_id = self['peer_id']
116         slice_id = foreign_slice ['slice_id']
117
118         if add_if_true:
119             ### ADDING
120             sql = "INSERT INTO peer_slice VALUES (%d,%d)" % (peer_id,slice_id)
121             self.api.db.do(sql)
122             if self['slice_ids'] is None:
123                 self['slice_ids']=[slice_id,]
124             else:
125                 self['slice_ids'].append(slice_id)
126             ### DELETING
127         else:
128             sql = "DELETE FROM peer_slice WHERE peer_id=%d AND slice_id=%d" % (peer_id,slice_id)
129             self.api.db.do(sql)
130             self['slice_ids'].remove(slice_id)
131
132         if commit:
133             self.api.db.commit()
134
135     def refresh_nodes (self, peer_get_nodes):
136         """
137         refreshes the foreign_nodes and peer_node tables
138         expected input is the current list of nodes as returned by GetNodes
139
140         returns the number of new nodes on this peer (can be negative)
141         """
142
143         peer_id = self['peer_id']
144         
145         # we get the whole table just in case 
146         # a host would have switched from one plc to the other
147         local_foreign_nodes = ForeignNodes (self.api)
148         # index it by hostname for searching later
149         local_foreign_nodes_index = local_foreign_nodes.dict('hostname')
150         
151         ### mark entries for this peer outofdate
152         old_count=0;
153         for foreign_node in local_foreign_nodes:
154             if foreign_node['peer_id'] == peer_id:
155                 foreign_node.uptodate=False
156                 old_count += 1
157
158         ### these fields get copied through
159         remote_fields = ['boot_state','model','version','date_created','last_updated']
160
161         ### scan the new entries, and mark them uptodate
162         for node in peer_get_nodes:
163             hostname = node['hostname']
164             try:
165                 foreign_node = local_foreign_nodes_index[hostname]
166                 if foreign_node['peer_id'] != peer_id:
167                     ### the node has changed its plc, needs to update peer_node
168                     old_peer_id = foreign_node['peer_id']
169                     old_peers=Peers(self.api,[peer_id])
170                     assert old_peer[0]
171                     # remove from previous peer
172                     old_peers[0].manage_node(foreign_node,False,False)
173                     # add to new peer
174                     self.manage_node(foreign_node,True,True)
175                     foreign_node['peer_id'] = peer_id
176                 ### update it anyway: copy other relevant fields
177                 for field in remote_fields:
178                     foreign_node[field]=node[field]
179                 # this row is now valid
180                 foreign_node.uptodate=True
181                 foreign_node.sync()
182             except:
183                 new_foreign_node = ForeignNode(self.api, {'hostname':hostname})
184                 for field in remote_fields:
185                     new_foreign_node[field]=node[field]
186                 ### need to sync so we get a node_id
187                 new_foreign_node.sync()
188                 new_foreign_node.uptodate = True
189                 self.manage_node(new_foreign_node,True,True)
190                 local_foreign_nodes_index[hostname]=new_foreign_node
191
192         ### delete entries that are not uptodate
193         for foreign_node in local_foreign_nodes:
194             if not foreign_node.uptodate:
195                 foreign_node.delete()
196
197         return len(peer_get_nodes)-old_count
198         
199     ### transcode node_id
200     def locate_alien_node_id_in_foreign_nodes (self, peer_foreign_nodes_dict, alien_id):
201         """
202         returns a local node_id as transcoded from an alien node_id
203         only lookups our local nodes because we dont need to know about other sites
204         returns a valid local node_id, or throws an exception
205         """
206         peer_foreign_node = peer_foreign_nodes_dict[alien_id]
207         hostname = peer_foreign_node['hostname']
208         return Nodes(self.api,[hostname])[0]['node_id']
209
210     def refresh_slices (self, peer_get_slices, peer_foreign_nodes):
211         """
212         refreshes the foreign_slices and peer_slice tables
213         expected input is the current list of slices as returned by GetSlices
214
215         returns the number of new slices on this peer (can be negative)
216         """
217
218         peer_id = self['peer_id']
219         
220         # we get the whole table just in case 
221         # a host would have switched from one plc to the other
222         local_foreign_slices = ForeignSlices (self.api)
223         # index it by name for searching later
224         local_foreign_slices_index = local_foreign_slices.dict('name')
225         
226         ### mark entries for this peer outofdate
227         old_count=0;
228         for foreign_slice in local_foreign_slices:
229             if foreign_slice['peer_id'] == peer_id:
230                 foreign_slice.uptodate=False
231                 old_count += 1
232
233         ### these fields get copied through
234         remote_fields = ['instantiation', 'url', 'description',
235                          'max_nodes', 'created', 'expires']
236
237         ### scan the new entries, and mark them uptodate
238         new_count=0
239         for slice in peer_get_slices:
240             ### ignore system-wide slices
241             if slice['creator_person_id'] == 1:
242                 continue
243
244             name = slice['name']
245
246             # create or update 
247             try:
248                 foreign_slice = local_foreign_slices_index[name]
249                 if foreign_slice['peer_id'] != peer_id:
250                     ### the slice has changed its plc, needs to update peer_slice
251                     old_peer_id = foreign_slice['peer_id']
252                     old_peers=Peers(self.api,[peer_id])
253                     assert old_peer[0]
254                     # remove from previous peer
255                     old_peers[0].manage_slice(foreign_slice,False,False)
256                     # add to new peer
257                     self.manage_slice(foreign_slice,True,True)
258                     foreign_slice['peer_id'] = peer_id
259             except:
260                 foreign_slice = ForeignSlice(self.api, {'name':name})
261 #                ### xxx temporary 
262 #                foreign_slice['site_id']=1
263                 ### need to sync so we get a slice_id
264                 foreign_slice.sync()
265                 self.manage_slice(foreign_slice,True,True)
266                 # insert in index
267                 local_foreign_slices_index[name]=foreign_slice
268
269             # go on with update
270             for field in remote_fields:
271                 foreign_slice[field]=slice[field]
272             # this row is now valid
273             foreign_slice.uptodate=True
274             new_count += 1
275             foreign_slice.sync()
276
277             ### handle node_ids
278             # in slice we get a set of node_ids
279             # but these ids are RELATIVE TO THE PEER
280             # so we need to figure the local node_id for these nodes
281             # we do this through peer_foreign_nodes 
282             # dictify once
283             peer_foreign_nodes_dict = {}
284             for foreign_node in peer_foreign_nodes:
285                 peer_foreign_nodes_dict[foreign_node['node_id']]=foreign_node
286             updated_node_ids = []
287             for alien_node_id in slice['node_ids']:
288                 try:
289                     local_node_id=self.locate_alien_node_id_in_foreign_nodes(peer_foreign_nodes_dict,alien_node_id)
290                     updated_node_ids.append(local_node_id)
291                 except:
292                     # this node_id is not in our scope
293                     pass
294             foreign_slice.update_slice_nodes (updated_node_ids)
295
296         ### delete entries that are not uptodate
297         for foreign_slice in local_foreign_slices:
298             if not foreign_slice.uptodate:
299                 foreign_slice.delete()
300
301         return new_count-old_count
302
303 class Peers (Table):
304     """ 
305     Maps to the peers table in the database
306     """
307     
308     def __init__ (self, api, peer_filter = None, columns = None):
309         Table.__init__(self, api, Peer, columns)
310
311         sql = "SELECT %s FROM view_peers WHERE deleted IS False" % \
312               ", ".join(self.columns)
313
314         if peer_filter is not None:
315             if isinstance(peer_filter, (list, tuple, set)):
316                 # Separate the list into integers and strings
317                 ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
318                 strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
319                 peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
320                 sql += " AND (%s)" % peer_filter.sql(api, "OR")
321             elif isinstance(peer_filter, dict):
322                 peer_filter = Filter(Peer.fields, peer_filter)
323                 sql += " AND (%s)" % peer_filter.sql(api, "AND")
324
325         self.selectall(sql)