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