4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
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.
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.
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
21 """Ganeti Remote API master script.
25 # pylint: disable=C0103,W0142
27 # C0103: Invalid name ganeti-watcher
37 from pyinotify import pyinotify # pylint: disable=E0611
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
52 import ganeti.http.auth # pylint: disable=W0611
53 import ganeti.http.server
56 class RemoteApiRequestContext(object):
57 """Data structure for Remote API requests.
62 self.handler_fn = None
63 self.handler_access = None
67 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
68 http.server.HttpServerHandler):
69 """REST Request Handler Class.
72 AUTH_REALM = "Ganeti Remote API"
74 def __init__(self, user_fn, _client_cls=None):
75 """Initializes this class.
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
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
91 def FormatErrorMessage(values):
92 """Formats the body of an error message.
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
100 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
102 def _GetRequestContext(self, req):
103 """Returns the context for a request.
105 The context is cached in the req.private variable.
108 if req.private is None:
109 (HandlerClass, items, args) = \
110 self._resmap.getController(req.request_path)
112 ctx = RemoteApiRequestContext()
113 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
115 method = req.request_method.upper()
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))
122 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
124 # Require permissions definition (usually in the base class)
125 if ctx.handler_access is None:
126 raise AssertionError("Permissions definition missing")
128 # This is only made available in HandleRequest
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
140 def AuthenticationRequired(self, req):
141 """Determine whether authentication is required.
144 return bool(self._GetRequestContext(req).handler_access)
146 def Authenticate(self, req, username, password):
147 """Checks whether a user can access a resource.
150 ctx = self._GetRequestContext(req)
152 user = self._user_fn(username)
154 self.VerifyBasicAuthPassword(req, username, password,
156 # Unknown user or password wrong
159 if (not ctx.handler_access or
160 set(user.options).intersection(ctx.handler_access)):
165 raise http.HttpForbidden()
167 def HandleRequest(self, req):
168 """Handles a request.
171 ctx = self._GetRequestContext(req)
173 # Deserialize request parameters
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()
185 ctx.body_data = serializer.LoadJson(req.request_body)
187 raise http.HttpBadRequest(message="Unable to parse JSON data")
192 result = ctx.handler_fn()
193 except luxi.TimeoutError:
194 raise http.HttpGatewayTimeout()
195 except luxi.ProtocolError, err:
196 raise http.HttpBadGateway(str(err))
198 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
200 return serializer.DumpJson(result)
205 """Initializes this class.
210 def Get(self, username):
211 """Checks whether a user exists.
215 return self._users.get(username, None)
219 def Load(self, filename):
220 """Loads a file containing users and passwords.
222 @type filename: string
223 @param filename: Path to file
226 logging.info("Reading users file at %s", filename)
229 contents = utils.ReadFile(filename)
230 except EnvironmentError, err:
232 if err.errno == errno.ENOENT:
233 logging.warning("No users file at %s", filename)
235 logging.warning("Error while reading %s: %s", filename, err)
238 users = http.auth.ParsePasswordFile(contents)
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)
250 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
251 def __init__(self, wm, path, cb):
252 """Initializes this class.
254 @param wm: Inotify watch manager
256 @param path: File path
258 @param cb: Function called on file change
261 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
264 self._filename = os.path.basename(path)
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"])
273 self._handle = self.AddWatch(os.path.dirname(path), mask)
275 def process_default(self, event):
276 """Called upon inotify event.
279 if event.name == self._filename:
280 logging.debug("Received inotify event %s", event)
284 def SetupFileWatcher(filename, cb):
285 """Configures an inotify watcher for a file.
287 @type filename: string
288 @param filename: File to watch
290 @param cb: Function called on file change
293 wm = pyinotify.WatchManager()
294 handler = FileEventHandler(wm, filename, cb)
295 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
298 def CheckRapi(options, args):
299 """Initial checks whether to run or exit with a failure.
302 if args: # rapi doesn't take any arguments
303 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
305 sys.exit(constants.EXIT_FAILURE)
307 ssconf.CheckMaster(options.debug)
309 # Read SSL certificate (this is a little hackish to read the cert as root)
311 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
312 ssl_cert_path=options.ssl_cert)
314 options.ssl_params = None
317 def PrepRapi(options, _):
318 """Prep remote API function, executed with the PID file held.
321 mainloop = daemon.Mainloop()
325 handler = RemoteApiHandler(users.Get)
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))
331 users.Load(constants.RAPI_USERS_FILE)
334 http.server.HttpServer(mainloop, options.bind_address, options.port,
335 handler, ssl_params=options.ssl_params, ssl_verify_peer=False)
338 return (mainloop, server)
341 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
342 """Main remote API function, executed with the PID file held.
345 (mainloop, server) = prep_data
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)
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)