import optparse
import sys
import os
+import os.path
import signal
from ganeti import constants
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 RemoteApiHttpServer(http.server.HttpServer):
+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.
"""
+ 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()
+ # 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.
+
+ """
+ if req.private is None:
+ (HandlerClass, items, args) = \
+ self._resmap.getController(req.request_path)
+
+ ctx = RemoteApiRequestContext()
+ ctx.handler = HandlerClass(items, args, req)
+
+ method = req.request_method.upper()
+ try:
+ ctx.handler_fn = getattr(ctx.handler, method)
+ except AttributeError, err:
+ raise http.HttpBadRequest("Method %s is unsupported for path %s" %
+ (method, req.request_path))
+
+ ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
+
+ # 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.
"""
- (HandlerClass, items, args) = self._resmap.getController(req.request_path)
- handler = HandlerClass(items, args, req)
+ ctx = self._GetRequestContext(req)
- method = req.request_method.upper()
try:
- fn = getattr(handler, method)
- except AttributeError, err:
- raise http.HttpBadRequest()
-
- try:
- result = fn()
- sn = handler.getSerialNumber()
+ 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()
+def CheckRAPI(options, args):
+ """Initial checks whether to run or exit with a failure
"""
- parser = optparse.OptionParser(description="Ganeti Remote API",
- usage="%prog [-d] [-p port]",
- version="%%prog (ganeti) %s" %
- constants.RAPI_VERSION)
- parser.add_option("-d", "--debug", dest="debug",
- help="Enable some debug messages",
- default=False, action="store_true")
- parser.add_option("-p", "--port", dest="port",
- 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("-K", "--ssl-key", dest="ssl_key",
- help="SSL key",
- default=None, type="string")
- parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
- help="SSL certificate",
- default=None, type="string")
- parser.add_option("-f", "--foreground", dest="fork",
- help="Don't detach from the current terminal",
- default=True, action="store_false")
-
- options, args = parser.parse_args()
-
if len(args) != 0:
- print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
- sys.exit(1)
-
- 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)
+ print >> sys.stderr, "Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % \
+ sys.argv[0]
+ sys.exit(constants.EXIT_FAILURE)
- return options, args
+ ssconf.CheckMaster(options.debug)
-def main():
- """Main function.
+def ExecRAPI(options, args):
+ """Main RAPI function, executed with the pidfile held.
"""
- options, args = ParseOptions()
+ # Read SSL certificate
+ if options.ssl:
+ ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
+ ssl_cert_path=options.ssl_cert)
+ else:
+ ssl_params = None
+
+ 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:
+ mainloop.Run()
+ finally:
+ server.Stop()
- ssconf.CheckMaster(options.debug)
- if options.fork:
- utils.Daemonize(logfile=constants.LOG_RAPISERVER)
+def main():
+ """Main function.
- utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
- stderr_logging=not options.fork)
+ """
+ parser = optparse.OptionParser(description="Ganeti Remote API",
+ usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
+ version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
- utils.WritePidFile(constants.RAPI_PID)
- try:
- mainloop = daemon.Mainloop()
- server = RemoteApiHttpServer(mainloop, "", options.port)
- server.Start()
- try:
- mainloop.Run()
- finally:
- server.Stop()
- finally:
- utils.RemovePidFile(constants.RAPI_PID)
+ dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
+ dirs.append((constants.LOG_OS_DIR, 0750))
+ daemon.GenericMain(constants.RAPI, parser, dirs, CheckRAPI, ExecRAPI)
if __name__ == '__main__':