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