13eaabcdd92c7d5e1a0a4c54708d5f3609a30d7d
[plcapi.git] / PLC / Peers.py
1 # $Id$
2 # $URL$
3 #
4 # Thierry Parmentelat - INRIA
5
6
7 import re
8 from types import StringTypes
9 from urlparse import urlparse
10
11 from PLC.Faults import *
12 from PLC.Parameter import Parameter, Mixed
13 from PLC.Filter import Filter
14 from PLC.Table import Row, Table
15 import PLC.Auth
16 from PLC.Shell import *
17 from PLC.Sites import Site, Sites
18 from PLC.Persons import Person, Persons
19 from PLC.Keys import Key, Keys
20 from PLC.Nodes import Node, Nodes
21 from PLC.TagTypes import TagType, TagTypes
22 from PLC.SliceTags import SliceTag, SliceTags
23 from PLC.Slices import Slice, Slices
24
25 class Peer(Row):
26     """
27     Stores the list of peering PLCs in the peers table. 
28     See the Row class for more details
29     """
30
31     table_name = 'peers'
32     primary_key = 'peer_id'
33     join_tables = ['peer_site', 'peer_person', 'peer_key', 'peer_node', 'peer_slice']
34     fields = {
35         'peer_id': Parameter (int, "Peer identifier"),
36         'peername': Parameter (str, "Peer name"),
37         'peer_url': Parameter (str, "Peer API URL"),
38         'key': Parameter(str, "Peer GPG public key"),
39         'cacert': Parameter(str, "Peer SSL public certificate"),
40         'shortname' : Parameter(str, "Peer short name"),
41         'hrn_root' : Parameter(str, "Root of this peer in a hierarchical naming space"),
42         ### cross refs
43         'site_ids': Parameter([int], "List of sites for which this peer is authoritative"),
44         'person_ids': Parameter([int], "List of users for which this peer is authoritative"),
45         'key_ids': Parameter([int], "List of keys for which this peer is authoritative"),
46         'node_ids': Parameter([int], "List of nodes for which this peer is authoritative"),
47         'slice_ids': Parameter([int], "List of slices for which this peer is authoritative"),
48         }
49
50     def validate_peername(self, peername):
51         if not len(peername):
52             raise PLCInvalidArgument, "Peer name must be specified"
53
54         conflicts = Peers(self.api, [peername])
55         for peer in conflicts:
56             if 'peer_id' not in self or self['peer_id'] != peer['peer_id']:
57                 raise PLCInvalidArgument, "Peer name already in use"
58
59         return peername
60
61     def validate_peer_url(self, url):
62         """
63         Validate URL. Must be HTTPS.
64         """
65
66         (scheme, netloc, path, params, query, fragment) = urlparse(url)
67         if scheme != "https":
68             raise PLCInvalidArgument, "Peer URL scheme must be https"
69         if path[-1] != '/':
70             raise PLCInvalidArgument, "Peer URL should end with /"
71         
72         return url
73
74     def delete(self, commit = True):
75         """
76         Deletes this peer and all related entities.
77         """
78
79         assert 'peer_id' in self
80
81         # Remove all related entities
82         for obj in \
83             Slices(self.api, self['slice_ids']) + \
84             Keys(self.api, self['key_ids']) + \
85             Persons(self.api, self['person_ids']) + \
86             Nodes(self.api, self['node_ids']) + \
87             Sites(self.api, self['site_ids']):
88             assert obj['peer_id'] == self['peer_id']
89             obj.delete(commit = False)
90
91         # Mark as deleted
92         self['deleted'] = True
93         self.sync(commit)
94
95     def add_site(self, site, peer_site_id, commit = True):
96         """
97         Associate a local site entry with this peer.
98         """
99
100         add = Row.add_object(Site, 'peer_site')
101         add(self, site,
102             {'peer_id': self['peer_id'],
103              'site_id': site['site_id'],
104              'peer_site_id': peer_site_id},
105             commit = commit)
106
107     def remove_site(self, site, commit = True):
108         """
109         Unassociate a site with this peer.
110         """
111         
112         remove = Row.remove_object(Site, 'peer_site')
113         remove(self, site, commit)
114
115     def add_person(self, person, peer_person_id, commit = True):
116         """
117         Associate a local user entry with this peer.
118         """
119
120         add = Row.add_object(Person, 'peer_person')
121         add(self, person,
122             {'peer_id': self['peer_id'],
123              'person_id': person['person_id'],
124              'peer_person_id': peer_person_id},
125             commit = commit)
126     
127     def remove_person(self, person, commit = True):
128         """
129         Unassociate a site with this peer.
130         """
131     
132         remove = Row.remove_object(Person, 'peer_person')
133         remove(self, person, commit)
134
135     def add_key(self, key, peer_key_id, commit = True):
136         """
137         Associate a local key entry with this peer.
138         """
139
140         add = Row.add_object(Key, 'peer_key')
141         add(self, key,
142             {'peer_id': self['peer_id'],
143              'key_id': key['key_id'],
144              'peer_key_id': peer_key_id},
145             commit = commit)
146
147     def remove_key(self, key, commit = True):
148         """
149         Unassociate a key with this peer.
150         """
151     
152         remove = Row.remove_object(Key, 'peer_key')
153         remove(self, key, commit)
154
155     def add_node(self, node, peer_node_id, commit = True):
156         """
157         Associate a local node entry with this peer.
158         """
159
160         add = Row.add_object(Node, 'peer_node')
161         add(self, node,
162             {'peer_id': self['peer_id'],
163              'node_id': node['node_id'],
164              'peer_node_id': peer_node_id},
165             commit = commit)
166
167         # calling UpdateNode with the node's hostname 
168         # will force the 'hrn' tag to be updated with the 
169         # correct root auth
170         shell = Shell() 
171         UpdateNode = self.api.callable('UpdateNode')
172         UpdateNode(shell.auth, node['node_id'], {'hostname': node['hostname']})  
173
174     def remove_node(self, node, commit = True):
175         """
176         Unassociate a node with this peer.
177         """
178     
179         remove = Row.remove_object(Node, 'peer_node')
180         remove(self, node, commit)
181
182         # calling UpdateNode with the node's hostname
183         # will force the 'hrn' tag to be updated with the
184         # correct root auth
185         shell = Shell() 
186         UpdateNode = self.api.callable('UpdateNode')
187         UpdateNode(shell.auth, node['node_id'], {'hostname': node['hostname']})
188
189     def add_slice(self, slice, peer_slice_id, commit = True):
190         """
191         Associate a local slice entry with this peer.
192         """
193
194         add = Row.add_object(Slice, 'peer_slice')
195         add(self, slice,
196             {'peer_id': self['peer_id'],
197              'slice_id': slice['slice_id'],
198              'peer_slice_id': peer_slice_id},
199             commit = commit)
200
201     def remove_slice(self, slice, commit = True):
202         """
203         Unassociate a slice with this peer.
204         """
205
206         remove = Row.remove_object(Slice, 'peer_slice')
207         remove(self, slice, commit)
208
209     def connect(self, **kwds):
210         """
211         Connect to this peer via XML-RPC.
212         """
213
214         import xmlrpclib
215         from PLC.PyCurl import PyCurlTransport
216         self.server = xmlrpclib.ServerProxy(self['peer_url'],
217                                             PyCurlTransport(self['peer_url'], self['cacert']),
218                                             allow_none = 1, **kwds)
219
220     def add_auth(self, function, methodname, **kwds):
221         """
222         Sign the specified XML-RPC call and add an auth struct as the
223         first argument of the call.
224         """
225
226         def wrapper(*args, **kwds):
227             from PLC.GPG import gpg_sign
228             signature = gpg_sign(args,
229                                  self.api.config.PLC_ROOT_GPG_KEY,
230                                  self.api.config.PLC_ROOT_GPG_KEY_PUB,
231                                  methodname)
232
233             auth = {'AuthMethod': "gpg",
234                     'name': self.api.config.PLC_NAME,
235                     'signature': signature}
236
237             # Automagically add auth struct to every call
238             args = (auth,) + args
239
240             return function(*args)
241
242         return wrapper
243
244     def __getattr__(self, attr):
245         """
246         Returns a callable API function if attr is the name of a
247         PLCAPI function; otherwise, returns the specified attribute.
248         """
249
250         try:
251             # Figure out if the specified attribute is the name of a
252             # PLCAPI function. If so and the function requires an
253             # authentication structure as its first argument, return a
254             # callable that automagically adds an auth struct to the
255             # call.
256             methodname = attr
257             api_function = self.api.callable(methodname)
258             if api_function.accepts and \
259                (isinstance(api_function.accepts[0], PLC.Auth.Auth) or \
260                 (isinstance(api_function.accepts[0], Mixed) and \
261                  filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
262                 function = getattr(self.server, methodname)
263                 return self.add_auth(function, methodname)
264         except Exception, err:
265             pass
266
267         if hasattr(self, attr):
268             return getattr(self, attr)
269         else:
270             raise AttributeError, "type object 'Peer' has no attribute '%s'" % attr
271
272 class Peers (Table):
273     """ 
274     Maps to the peers table in the database
275     """
276     
277     def __init__ (self, api, peer_filter = None, columns = None):
278         Table.__init__(self, api, Peer, columns)
279
280         sql = "SELECT %s FROM view_peers WHERE deleted IS False" % \
281               ", ".join(self.columns)
282
283         if peer_filter is not None:
284             if isinstance(peer_filter, (list, tuple, set)):
285                 # Separate the list into integers and strings
286                 ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
287                 strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
288                 peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
289                 sql += " AND (%s) %s" % peer_filter.sql(api, "OR")
290             elif isinstance(peer_filter, dict):
291                 peer_filter = Filter(Peer.fields, peer_filter)
292                 sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
293
294         self.selectall(sql)