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