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-msg=C0103,W0142
27 # C0103: Invalid name ganeti-watcher
37 from pyinotify import pyinotify # pylint: disable-msg=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-msg=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 JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
68 """Custom Request Executor class that formats HTTP errors in JSON.
71 error_content_type = http.HTTP_APP_JSON
73 def _FormatErrorMessage(self, values):
74 """Formats the body of an error message.
77 @param values: dictionary with keys code, message and explain.
79 @return: the body of the message
82 return serializer.DumpJson(values, indent=True)
85 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
86 http.server.HttpServer):
87 """REST Request Handler Class.
90 AUTH_REALM = "Ganeti Remote API"
92 def __init__(self, *args, **kwargs):
93 # pylint: disable-msg=W0233
94 # it seems pylint doesn't see the second parent class there
95 http.server.HttpServer.__init__(self, *args, **kwargs)
96 http.auth.HttpServerRequestAuthentication.__init__(self)
97 self._resmap = connector.Mapper()
100 def LoadUsers(self, filename):
101 """Loads a file containing users and passwords.
103 @type filename: string
104 @param filename: Path to file
107 logging.info("Reading users file at %s", filename)
110 contents = utils.ReadFile(filename)
111 except EnvironmentError, err:
113 if err.errno == errno.ENOENT:
114 logging.warning("No users file at %s", filename)
116 logging.warning("Error while reading %s: %s", filename, err)
119 users = http.auth.ParsePasswordFile(contents)
121 except Exception, err: # pylint: disable-msg=W0703
122 # We don't care about the type of exception
123 logging.error("Error while parsing %s: %s", filename, err)
130 def _GetRequestContext(self, req):
131 """Returns the context for a request.
133 The context is cached in the req.private variable.
136 if req.private is None:
137 (HandlerClass, items, args) = \
138 self._resmap.getController(req.request_path)
140 ctx = RemoteApiRequestContext()
141 ctx.handler = HandlerClass(items, args, req)
143 method = req.request_method.upper()
145 ctx.handler_fn = getattr(ctx.handler, method)
146 except AttributeError:
147 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
148 (method, req.request_path))
150 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
152 # Require permissions definition (usually in the base class)
153 if ctx.handler_access is None:
154 raise AssertionError("Permissions definition missing")
156 # This is only made available in HandleRequest
161 # Check for expected attributes
162 assert req.private.handler
163 assert req.private.handler_fn
164 assert req.private.handler_access is not None
168 def AuthenticationRequired(self, req):
169 """Determine whether authentication is required.
172 return bool(self._GetRequestContext(req).handler_access)
174 def Authenticate(self, req, username, password):
175 """Checks whether a user can access a resource.
178 ctx = self._GetRequestContext(req)
180 # Check username and password
183 user = self._users.get(username, None)
184 if user and self.VerifyBasicAuthPassword(req, username, password,
189 # Unknown user or password wrong
192 if (not ctx.handler_access or
193 set(user.options).intersection(ctx.handler_access)):
198 raise http.HttpForbidden()
200 def HandleRequest(self, req):
201 """Handles a request.
204 ctx = self._GetRequestContext(req)
206 # Deserialize request parameters
208 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
209 # include a Content-Type header field defining the media type of that
210 # body. [...] If the media type remains unknown, the recipient SHOULD
211 # treat it as type "application/octet-stream".
212 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
213 http.HTTP_APP_OCTET_STREAM)
214 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
215 raise http.HttpUnsupportedMediaType()
218 ctx.body_data = serializer.LoadJson(req.request_body)
220 raise http.HttpBadRequest(message="Unable to parse JSON data")
225 result = ctx.handler_fn()
226 except luxi.TimeoutError:
227 raise http.HttpGatewayTimeout()
228 except luxi.ProtocolError, err:
229 raise http.HttpBadGateway(str(err))
231 method = req.request_method.upper()
232 logging.exception("Error while handling the %s request", method)
235 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
237 return serializer.DumpJson(result)
240 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
241 def __init__(self, wm, path, cb):
242 """Initializes this class.
244 @param wm: Inotify watch manager
246 @param path: File path
248 @param cb: Function called on file change
251 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
254 self._filename = os.path.basename(path)
256 # Different Pyinotify versions have the flag constants at different places,
257 # hence not accessing them directly
258 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
259 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
260 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
261 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
263 self._handle = self.AddWatch(os.path.dirname(path), mask)
265 def process_default(self, event):
266 """Called upon inotify event.
269 if event.name == self._filename:
270 logging.debug("Received inotify event %s", event)
274 def SetupFileWatcher(filename, cb):
275 """Configures an inotify watcher for a file.
277 @type filename: string
278 @param filename: File to watch
280 @param cb: Function called on file change
283 wm = pyinotify.WatchManager()
284 handler = FileEventHandler(wm, filename, cb)
285 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
288 def CheckRapi(options, args):
289 """Initial checks whether to run or exit with a failure.
292 if args: # rapi doesn't take any arguments
293 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
295 sys.exit(constants.EXIT_FAILURE)
297 ssconf.CheckMaster(options.debug)
299 # Read SSL certificate (this is a little hackish to read the cert as root)
301 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
302 ssl_cert_path=options.ssl_cert)
304 options.ssl_params = None
307 def PrepRapi(options, _):
308 """Prep remote API function, executed with the PID file held.
312 mainloop = daemon.Mainloop()
313 server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
314 ssl_params=options.ssl_params,
315 ssl_verify_peer=False,
316 request_executor_class=JsonErrorRequestExecutor)
318 # Setup file watcher (it'll be driven by asyncore)
319 SetupFileWatcher(constants.RAPI_USERS_FILE,
320 compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
322 server.LoadUsers(constants.RAPI_USERS_FILE)
324 # pylint: disable-msg=E1101
325 # it seems pylint doesn't see the second parent class there
328 return (mainloop, server)
331 def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
332 """Main remote API function, executed with the PID file held.
335 (mainloop, server) = prep_data
346 parser = optparse.OptionParser(description="Ganeti Remote API",
347 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
348 version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
350 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
351 default_ssl_cert=constants.RAPI_CERT_FILE,
352 default_ssl_key=constants.RAPI_CERT_FILE)