errors: Add exception for RAPI testing utilities
[ganeti-local] / lib / server / rapi.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """Ganeti Remote API master script.
22
23 """
24
25 # pylint: disable=C0103,W0142
26
27 # C0103: Invalid name ganeti-watcher
28
29 import logging
30 import optparse
31 import sys
32 import os
33 import os.path
34 import errno
35
36 try:
37   from pyinotify import pyinotify # pylint: disable=E0611
38 except ImportError:
39   import pyinotify
40
41 from ganeti import asyncnotifier
42 from ganeti import constants
43 from ganeti import http
44 from ganeti import daemon
45 from ganeti import ssconf
46 from ganeti import luxi
47 from ganeti import serializer
48 from ganeti import compat
49 from ganeti import utils
50 from ganeti.rapi import connector
51
52 import ganeti.http.auth   # pylint: disable=W0611
53 import ganeti.http.server
54
55
56 class RemoteApiRequestContext(object):
57   """Data structure for Remote API requests.
58
59   """
60   def __init__(self):
61     self.handler = None
62     self.handler_fn = None
63     self.handler_access = None
64     self.body_data = None
65
66
67 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
68                        http.server.HttpServerHandler):
69   """REST Request Handler Class.
70
71   """
72   AUTH_REALM = "Ganeti Remote API"
73
74   def __init__(self, user_fn, _client_cls=None):
75     """Initializes this class.
76
77     @type user_fn: callable
78     @param user_fn: Function receiving username as string and returning
79       L{http.auth.PasswordFileUser} or C{None} if user is not found
80
81     """
82     # pylint: disable=W0233
83     # it seems pylint doesn't see the second parent class there
84     http.server.HttpServerHandler.__init__(self)
85     http.auth.HttpServerRequestAuthentication.__init__(self)
86     self._client_cls = _client_cls
87     self._resmap = connector.Mapper()
88     self._user_fn = user_fn
89
90   @staticmethod
91   def FormatErrorMessage(values):
92     """Formats the body of an error message.
93
94     @type values: dict
95     @param values: dictionary with keys C{code}, C{message} and C{explain}.
96     @rtype: tuple; (string, string)
97     @return: Content-type and response body
98
99     """
100     return (http.HTTP_APP_JSON, serializer.DumpJson(values))
101
102   def _GetRequestContext(self, req):
103     """Returns the context for a request.
104
105     The context is cached in the req.private variable.
106
107     """
108     if req.private is None:
109       (HandlerClass, items, args) = \
110                      self._resmap.getController(req.request_path)
111
112       ctx = RemoteApiRequestContext()
113       ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
114
115       method = req.request_method.upper()
116       try:
117         ctx.handler_fn = getattr(ctx.handler, method)
118       except AttributeError:
119         raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
120                                       (method, req.request_path))
121
122       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
123
124       # Require permissions definition (usually in the base class)
125       if ctx.handler_access is None:
126         raise AssertionError("Permissions definition missing")
127
128       # This is only made available in HandleRequest
129       ctx.body_data = None
130
131       req.private = ctx
132
133     # Check for expected attributes
134     assert req.private.handler
135     assert req.private.handler_fn
136     assert req.private.handler_access is not None
137
138     return req.private
139
140   def AuthenticationRequired(self, req):
141     """Determine whether authentication is required.
142
143     """
144     return bool(self._GetRequestContext(req).handler_access)
145
146   def Authenticate(self, req, username, password):
147     """Checks whether a user can access a resource.
148
149     """
150     ctx = self._GetRequestContext(req)
151
152     user = self._user_fn(username)
153     if not (user and
154             self.VerifyBasicAuthPassword(req, username, password,
155                                          user.password)):
156       # Unknown user or password wrong
157       return False
158
159     if (not ctx.handler_access or
160         set(user.options).intersection(ctx.handler_access)):
161       # Allow access
162       return True
163
164     # Access forbidden
165     raise http.HttpForbidden()
166
167   def HandleRequest(self, req):
168     """Handles a request.
169
170     """
171     ctx = self._GetRequestContext(req)
172
173     # Deserialize request parameters
174     if req.request_body:
175       # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
176       # include a Content-Type header field defining the media type of that
177       # body. [...] If the media type remains unknown, the recipient SHOULD
178       # treat it as type "application/octet-stream".
179       req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
180                                                  http.HTTP_APP_OCTET_STREAM)
181       if req_content_type.lower() != http.HTTP_APP_JSON.lower():
182         raise http.HttpUnsupportedMediaType()
183
184       try:
185         ctx.body_data = serializer.LoadJson(req.request_body)
186       except Exception:
187         raise http.HttpBadRequest(message="Unable to parse JSON data")
188     else:
189       ctx.body_data = None
190
191     try:
192       result = ctx.handler_fn()
193     except luxi.TimeoutError:
194       raise http.HttpGatewayTimeout()
195     except luxi.ProtocolError, err:
196       raise http.HttpBadGateway(str(err))
197
198     req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
199
200     return serializer.DumpJson(result)
201
202
203 class RapiUsers:
204   def __init__(self):
205     """Initializes this class.
206
207     """
208     self._users = None
209
210   def Get(self, username):
211     """Checks whether a user exists.
212
213     """
214     if self._users:
215       return self._users.get(username, None)
216     else:
217       return None
218
219   def Load(self, filename):
220     """Loads a file containing users and passwords.
221
222     @type filename: string
223     @param filename: Path to file
224
225     """
226     logging.info("Reading users file at %s", filename)
227     try:
228       try:
229         contents = utils.ReadFile(filename)
230       except EnvironmentError, err:
231         self._users = None
232         if err.errno == errno.ENOENT:
233           logging.warning("No users file at %s", filename)
234         else:
235           logging.warning("Error while reading %s: %s", filename, err)
236         return False
237
238       users = http.auth.ParsePasswordFile(contents)
239
240     except Exception, err: # pylint: disable=W0703
241       # We don't care about the type of exception
242       logging.error("Error while parsing %s: %s", filename, err)
243       return False
244
245     self._users = users
246
247     return True
248
249
250 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
251   def __init__(self, wm, path, cb):
252     """Initializes this class.
253
254     @param wm: Inotify watch manager
255     @type path: string
256     @param path: File path
257     @type cb: callable
258     @param cb: Function called on file change
259
260     """
261     asyncnotifier.FileEventHandlerBase.__init__(self, wm)
262
263     self._cb = cb
264     self._filename = os.path.basename(path)
265
266     # Different Pyinotify versions have the flag constants at different places,
267     # hence not accessing them directly
268     mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
269             pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
270             pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
271             pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
272
273     self._handle = self.AddWatch(os.path.dirname(path), mask)
274
275   def process_default(self, event):
276     """Called upon inotify event.
277
278     """
279     if event.name == self._filename:
280       logging.debug("Received inotify event %s", event)
281       self._cb()
282
283
284 def SetupFileWatcher(filename, cb):
285   """Configures an inotify watcher for a file.
286
287   @type filename: string
288   @param filename: File to watch
289   @type cb: callable
290   @param cb: Function called on file change
291
292   """
293   wm = pyinotify.WatchManager()
294   handler = FileEventHandler(wm, filename, cb)
295   asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
296
297
298 def CheckRapi(options, args):
299   """Initial checks whether to run or exit with a failure.
300
301   """
302   if args: # rapi doesn't take any arguments
303     print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
304                           sys.argv[0])
305     sys.exit(constants.EXIT_FAILURE)
306
307   ssconf.CheckMaster(options.debug)
308
309   # Read SSL certificate (this is a little hackish to read the cert as root)
310   if options.ssl:
311     options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
312                                             ssl_cert_path=options.ssl_cert)
313   else:
314     options.ssl_params = None
315
316
317 def PrepRapi(options, _):
318   """Prep remote API function, executed with the PID file held.
319
320   """
321   mainloop = daemon.Mainloop()
322
323   users = RapiUsers()
324
325   handler = RemoteApiHandler(users.Get)
326
327   # Setup file watcher (it'll be driven by asyncore)
328   SetupFileWatcher(constants.RAPI_USERS_FILE,
329                    compat.partial(users.Load, constants.RAPI_USERS_FILE))
330
331   users.Load(constants.RAPI_USERS_FILE)
332
333   server = \
334     http.server.HttpServer(mainloop, options.bind_address, options.port,
335       handler, ssl_params=options.ssl_params, ssl_verify_peer=False)
336   server.Start()
337
338   return (mainloop, server)
339
340
341 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
342   """Main remote API function, executed with the PID file held.
343
344   """
345   (mainloop, server) = prep_data
346   try:
347     mainloop.Run()
348   finally:
349     server.Stop()
350
351
352 def Main():
353   """Main function.
354
355   """
356   parser = optparse.OptionParser(description="Ganeti Remote API",
357                     usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
358                     version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
359
360   daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
361                      default_ssl_cert=constants.RAPI_CERT_FILE,
362                      default_ssl_key=constants.RAPI_CERT_FILE)