64891990edcbf999913f202b9c7c8edefc7025fb
[plcapi.git] / PLC / Peers.py
1 #
2 # Thierry Parmentelat - INRIA
3 #
4
5 import re
6 from types import StringTypes
7 from urlparse import urlparse
8
9 import PLC.Auth
10 from PLC.Debug import log
11 from PLC.Faults import *
12 from PLC.Namespace import hostname_to_hrn
13 from PLC.Parameter import Parameter, Mixed
14 from PLC.Filter import Filter
15 from PLC.Table import Row, Table
16 from PLC.Sites import Site, Sites
17 from PLC.Persons import Person, Persons
18 from PLC.Keys import Key, Keys
19 from PLC.Nodes import Node, Nodes
20 from PLC.TagTypes import TagType, TagTypes
21 from PLC.NodeTags import NodeTag, NodeTags
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         sites = Sites(self.api, node['site_id'], ['login_base'])
168         site = sites[0]
169         login_base = site['login_base']
170         try:
171             # attempt to manually update the 'hrn' tag with the remote prefix
172             hrn_root = self['hrn_root']
173             hrn = hostname_to_hrn(hrn_root, login_base, node['hostname'])
174             tags = {'hrn': hrn}
175             Node(self.api, node).update_tags(tags)
176         except:
177             print >>log, "WARNING: could not find out hrn on hostname=%s"%node['hostname']
178
179     def remove_node(self, node, commit = True):
180         """
181         Unassociate a node with this peer.
182         """
183
184         remove = Row.remove_object(Node, 'peer_node')
185         remove(self, node, commit)
186         # attempt to manually update the 'hrn' tag now that the node is local
187         root_auth = self.api.config.PLC_HRN_ROOT
188         sites = Sites(self.api, node['site_id'], ['login_base'])
189         site = sites[0]
190         login_base = site['login_base']
191         hrn = hostname_to_hrn(root_auth, login_base, node['hostname'])
192         tags = {'hrn': hrn}
193         Node(self.api, node).update_tags(tags)
194
195     def add_slice(self, slice, peer_slice_id, commit = True):
196         """
197         Associate a local slice entry with this peer.
198         """
199
200         add = Row.add_object(Slice, 'peer_slice')
201         add(self, slice,
202             {'peer_id': self['peer_id'],
203              'slice_id': slice['slice_id'],
204              'peer_slice_id': peer_slice_id},
205             commit = commit)
206
207     def remove_slice(self, slice, commit = True):
208         """
209         Unassociate a slice with this peer.
210         """
211
212         remove = Row.remove_object(Slice, 'peer_slice')
213         remove(self, slice, commit)
214
215     def connect(self, **kwds):
216         """
217         Connect to this peer via XML-RPC.
218         """
219
220         import xmlrpclib
221         from PLC.PyCurl import PyCurlTransport
222         self.server = xmlrpclib.ServerProxy(self['peer_url'],
223                                             PyCurlTransport(self['peer_url'], self['cacert']),
224                                             allow_none = 1, **kwds)
225
226     def add_auth(self, function, methodname, **kwds):
227         """
228         Sign the specified XML-RPC call and add an auth struct as the
229         first argument of the call.
230         """
231
232         def wrapper(*args, **kwds):
233             from PLC.GPG import gpg_sign
234             signature = gpg_sign(args,
235                                  self.api.config.PLC_ROOT_GPG_KEY,
236                                  self.api.config.PLC_ROOT_GPG_KEY_PUB,
237                                  methodname)
238
239             auth = {'AuthMethod': "gpg",
240                     'name': self.api.config.PLC_NAME,
241                     'signature': signature}
242
243             # Automagically add auth struct to every call
244             args = (auth,) + args
245
246             return function(*args)
247
248         return wrapper
249
250     def __getattr__(self, attr):
251         """
252         Returns a callable API function if attr is the name of a
253         PLCAPI function; otherwise, returns the specified attribute.
254         """
255
256         try:
257             # Figure out if the specified attribute is the name of a
258             # PLCAPI function. If so and the function requires an
259             # authentication structure as its first argument, return a
260             # callable that automagically adds an auth struct to the
261             # call.
262             methodname = attr
263             api_function = self.api.callable(methodname)
264             if api_function.accepts and \
265                (isinstance(api_function.accepts[0], PLC.Auth.Auth) or \
266                 (isinstance(api_function.accepts[0], Mixed) and \
267                  filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
268                 function = getattr(self.server, methodname)
269                 return self.add_auth(function, methodname)
270         except Exception, err:
271             pass
272
273         if hasattr(self, attr):
274             return getattr(self, attr)
275         else:
276             raise AttributeError, "type object 'Peer' has no attribute '%s'" % attr
277
278 class Peers (Table):
279     """
280     Maps to the peers table in the database
281     """
282
283     def __init__ (self, api, peer_filter = None, columns = None):
284         Table.__init__(self, api, Peer, columns)
285
286         sql = "SELECT %s FROM view_peers WHERE deleted IS False" % \
287               ", ".join(self.columns)
288
289         if peer_filter is not None:
290             if isinstance(peer_filter, (list, tuple, set)):
291                 # Separate the list into integers and strings
292                 ints = filter(lambda x: isinstance(x, (int, long)), peer_filter)
293                 strs = filter(lambda x: isinstance(x, StringTypes), peer_filter)
294                 peer_filter = Filter(Peer.fields, {'peer_id': ints, 'peername': strs})
295                 sql += " AND (%s) %s" % peer_filter.sql(api, "OR")
296             elif isinstance(peer_filter, dict):
297                 peer_filter = Filter(Peer.fields, peer_filter)
298                 sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
299             elif isinstance(peer_filter, (int, long)):
300                 peer_filter = Filter(Peer.fields, {'peer_id': peer_filter})
301                 sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
302             elif isinstance(peer_filter, StringTypes):
303                 peer_filter = Filter(Peer.fields, {'peername': peer_filter})
304                 sql += " AND (%s) %s" % peer_filter.sql(api, "AND")
305             else:
306                 raise PLCInvalidArgument, "Wrong peer filter %r"%peer_filter
307
308         self.selectall(sql)