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 method = req.request_method.upper()
199 logging.exception("Error while handling the %s request", method)
202 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
204 return serializer.DumpJson(result)
209 """Initializes this class.
214 def Get(self, username):
215 """Checks whether a user exists.
219 return self._users.get(username, None)
223 def Load(self, filename):
224 """Loads a file containing users and passwords.
226 @type filename: string
227 @param filename: Path to file
230 logging.info("Reading users file at %s", filename)
233 contents = utils.ReadFile(filename)
234 except EnvironmentError, err:
236 if err.errno == errno.ENOENT:
237 logging.warning("No users file at %s", filename)
239 logging.warning("Error while reading %s: %s", filename, err)
242 users = http.auth.ParsePasswordFile(contents)
244 except Exception, err: # pylint: disable=W0703
245 # We don't care about the type of exception
246 logging.error("Error while parsing %s: %s", filename, err)
254 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
255 def __init__(self, wm, path, cb):
256 """Initializes this class.
258 @param wm: Inotify watch manager
260 @param path: File path
262 @param cb: Function called on file change
265 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
268 self._filename = os.path.basename(path)
270 # Different Pyinotify versions have the flag constants at different places,
271 # hence not accessing them directly
272 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
273 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
274 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
275 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
277 self._handle = self.AddWatch(os.path.dirname(path), mask)
279 def process_default(self, event):
280 """Called upon inotify event.
283 if event.name == self._filename:
284 logging.debug("Received inotify event %s", event)
288 def SetupFileWatcher(filename, cb):
289 """Configures an inotify watcher for a file.
291 @type filename: string
292 @param filename: File to watch
294 @param cb: Function called on file change
297 wm = pyinotify.WatchManager()
298 handler = FileEventHandler(wm, filename, cb)
299 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
302 def CheckRapi(options, args):
303 """Initial checks whether to run or exit with a failure.
306 if args: # rapi doesn't take any arguments
307 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
309 sys.exit(constants.EXIT_FAILURE)
311 ssconf.CheckMaster(options.debug)
313 # Read SSL certificate (this is a little hackish to read the cert as root)
315 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
316 ssl_cert_path=options.ssl_cert)
318 options.ssl_params = None
321 def PrepRapi(options, _):
322 """Prep remote API function, executed with the PID file held.
325 mainloop = daemon.Mainloop()
329 handler = RemoteApiHandler(users.Get)
331 # Setup file watcher (it'll be driven by asyncore)
332 SetupFileWatcher(constants.RAPI_USERS_FILE,
333 compat.partial(users.Load, constants.RAPI_USERS_FILE))
335 users.Load(constants.RAPI_USERS_FILE)
338 http.server.HttpServer(mainloop, options.bind_address, options.port,
339 handler, ssl_params=options.ssl_params, ssl_verify_peer=False)
342 return (mainloop, server)
345 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
346 """Main remote API function, executed with the PID file held.
349 (mainloop, server) = prep_data
360 parser = optparse.OptionParser(description="Ganeti Remote API",
361 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
362 version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
364 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
365 default_ssl_cert=constants.RAPI_CERT_FILE,
366 default_ssl_key=constants.RAPI_CERT_FILE)