ganeti-rapi, replace hardcoded exit value
[ganeti-local] / daemons / ganeti-rapi
index 78da6dc..2288f17 100755 (executable)
@@ -26,69 +26,161 @@ import logging
 import optparse
 import sys
 import os
+import os.path
 import signal
 
-from ganeti import logger
 from ganeti import constants
 from ganeti import errors
 from ganeti import http
+from ganeti import daemon
 from ganeti import ssconf
 from ganeti import utils
+from ganeti import luxi
+from ganeti import serializer
 from ganeti.rapi import connector
 
+import ganeti.http.auth
+import ganeti.http.server
 
-class RESTRequestHandler(http.HTTPRequestHandler):
+
+class RemoteApiRequestContext(object):
+  """Data structure for Remote API requests.
+
+  """
+  def __init__(self):
+    self.handler = None
+    self.handler_fn = None
+    self.handler_access = None
+
+
+class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
+  """Custom Request Executor class that formats HTTP errors in JSON.
+
+  """
+  error_content_type = "application/json"
+
+  def _FormatErrorMessage(self, values):
+    """Formats the body of an error message.
+
+    @type values: dict
+    @param values: dictionary with keys code, message and explain.
+    @rtype: string
+    @return: the body of the message
+
+    """
+    return serializer.DumpJson(values, indent=True)
+
+
+class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
+                          http.server.HttpServer):
   """REST Request Handler Class.
 
   """
-  def setup(self):
-    super(RESTRequestHandler, self).setup()
+  AUTH_REALM = "Ganeti Remote API"
+
+  def __init__(self, *args, **kwargs):
+    http.server.HttpServer.__init__(self, *args, **kwargs)
+    http.auth.HttpServerRequestAuthentication.__init__(self)
     self._resmap = connector.Mapper()
 
-  def HandleRequest(self):
-    """ Handels a request.
+    # Load password file
+    if os.path.isfile(constants.RAPI_USERS_FILE):
+      self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
+    else:
+      self._users = None
+
+  def _GetRequestContext(self, req):
+    """Returns the context for a request.
+
+    The context is cached in the req.private variable.
 
     """
-    (HandlerClass, items, args) = self._resmap.getController(self.path)
-    handler = HandlerClass(self, items, args, self.post_data)
+    if req.private is None:
+      (HandlerClass, items, args) = \
+                     self._resmap.getController(req.request_path)
 
-    command = self.command.upper()
-    try:
-      fn = getattr(handler, command)
-    except AttributeError, err:
-      raise http.HTTPBadRequest()
+      ctx = RemoteApiRequestContext()
+      ctx.handler = HandlerClass(items, args, req)
 
-    try:
+      method = req.request_method.upper()
       try:
-        result = fn()
-      except:
-        logging.exception("Error while handling the %s request", command)
-        raise
+        ctx.handler_fn = getattr(ctx.handler, method)
+      except AttributeError, err:
+        raise http.HttpBadRequest("Method %s is unsupported for path %s" %
+                                  (method, req.request_path))
 
-    except errors.OpPrereqError, err:
-      # TODO: "Not found" is not always the correct error. Ganeti's core must
-      # differentiate between different error types.
-      raise http.HTTPNotFound(message=str(err))
+      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
 
-    return result
+      # Require permissions definition (usually in the base class)
+      if ctx.handler_access is None:
+        raise AssertionError("Permissions definition missing")
+
+      req.private = ctx
+
+    return req.private
 
+  def GetAuthRealm(self, req):
+    """Override the auth realm for queries.
+
+    """
+    ctx = self._GetRequestContext(req)
+    if ctx.handler_access:
+      return self.AUTH_REALM
+    else:
+      return None
+
+  def Authenticate(self, req, username, password):
+    """Checks whether a user can access a resource.
+
+    """
+    ctx = self._GetRequestContext(req)
+
+    # Check username and password
+    valid_user = False
+    if self._users:
+      user = self._users.get(username, None)
+      if user and user.password == password:
+        valid_user = True
+
+    if not valid_user:
+      # Unknown user or password wrong
+      return False
+
+    if (not ctx.handler_access or
+        set(user.options).intersection(ctx.handler_access)):
+      # Allow access
+      return True
+
+    # Access forbidden
+    raise http.HttpForbidden()
+
+  def HandleRequest(self, req):
+    """Handles a request.
+
+    """
+    ctx = self._GetRequestContext(req)
 
-class RESTHttpServer(http.HTTPServer):
-  def serve_forever(self):
-    """Handle one request at a time until told to quit."""
-    sighandler = utils.SignalHandler([signal.SIGINT, signal.SIGTERM])
     try:
-      while not sighandler.called:
-        self.handle_request()
-    finally:
-      sighandler.Reset()
+      result = ctx.handler_fn()
+      sn = ctx.handler.getSerialNumber()
+      if sn:
+        req.response_headers[http.HTTP_ETAG] = str(sn)
+    except luxi.TimeoutError:
+      raise http.HttpGatewayTimeout()
+    except luxi.ProtocolError, err:
+      raise http.HttpBadGateway(str(err))
+    except:
+      method = req.request_method.upper()
+      logging.exception("Error while handling the %s request", method)
+      raise
+
+    return result
 
 
 def ParseOptions():
   """Parse the command line options.
 
-  Returns:
-    (options, args) as from OptionParser.parse_args()
+  @return: (options, args) as from OptionParser.parse_args()
 
   """
   parser = optparse.OptionParser(description="Ganeti Remote API",
@@ -102,29 +194,32 @@ def ParseOptions():
                     help="Port to run API (%s default)." %
                                  constants.RAPI_PORT,
                     default=constants.RAPI_PORT, type="int")
-  parser.add_option("-S", "--https", dest="ssl",
-                    help="Secure HTTP protocol with SSL",
-                    default=False, action="store_true")
+  parser.add_option("--no-ssl", dest="ssl",
+                    help="Do not secure HTTP protocol with SSL",
+                    default=True, action="store_false")
   parser.add_option("-K", "--ssl-key", dest="ssl_key",
                     help="SSL key",
-                    default=None, type="string")
+                    default=constants.RAPI_CERT_FILE, type="string")
   parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
                     help="SSL certificate",
-                    default=None, type="string")
+                    default=constants.RAPI_CERT_FILE, type="string")
   parser.add_option("-f", "--foreground", dest="fork",
                     help="Don't detach from the current terminal",
                     default=True, action="store_false")
+  parser.add_option("-b", "--bind", dest="bind_address",
+                     help="Bind address",
+                     default="", metavar="ADDRESS")
 
   options, args = parser.parse_args()
 
   if len(args) != 0:
     print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
-    sys.exit(1)
+    sys.exit(constants.EXIT_FAILURE)
 
   if options.ssl and not (options.ssl_cert and options.ssl_key):
     print >> sys.stderr, ("For secure mode please provide "
-                         "--ssl-key and --ssl-cert arguments")
-    sys.exit(1)
+                          "--ssl-key and --ssl-cert arguments")
+    sys.exit(constants.EXIT_FAILURE)
 
   return options, args
 
@@ -135,33 +230,43 @@ def main():
   """
   options, args = ParseOptions()
 
+  if options.fork:
+    utils.CloseFDs()
+
+  if options.ssl:
+    # Read SSL certificate
+    try:
+      ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
+                                      ssl_cert_path=options.ssl_cert)
+    except Exception, err:
+      sys.stderr.write("Can't load the SSL certificate/key: %s\n" % (err,))
+      sys.exit(constants.EXIT_FAILURE)
+  else:
+    ssl_params = None
+
   ssconf.CheckMaster(options.debug)
 
   if options.fork:
     utils.Daemonize(logfile=constants.LOG_RAPISERVER)
 
-  logger.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
+  utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
                      stderr_logging=not options.fork)
 
   utils.WritePidFile(constants.RAPI_PID)
-
-  log_fd = open(constants.LOG_RAPIACCESS, 'a')
   try:
-    apache_log = http.ApacheLogfile(log_fd)
-    httpd = RESTHttpServer(("", options.port), RESTRequestHandler,
-                           httplog=apache_log)
+    mainloop = daemon.Mainloop()
+    server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
+                                 ssl_params=ssl_params, ssl_verify_peer=False,
+                                 request_executor_class=
+                                 JsonErrorRequestExecutor)
+    server.Start()
     try:
-      httpd.serve_forever()
+      mainloop.Run()
     finally:
-      httpd.server_close()
-      utils.RemovePidFile(constants.RAPI_PID)
-
+      server.Stop()
   finally:
-    log_fd.close()
-
-  sys.exit(0)
+    utils.RemovePidFile(constants.RAPI_PID)
 
 
 if __name__ == '__main__':
-  
   main()