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