4 # Copyright (C) 2006, 2007 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
35 from ganeti import constants
36 from ganeti import http
37 from ganeti import daemon
38 from ganeti import ssconf
39 from ganeti import luxi
40 from ganeti import serializer
41 from ganeti.rapi import connector
43 import ganeti.http.auth # pylint: disable-msg=W0611
44 import ganeti.http.server
47 class RemoteApiRequestContext(object):
48 """Data structure for Remote API requests.
53 self.handler_fn = None
54 self.handler_access = None
57 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
58 """Custom Request Executor class that formats HTTP errors in JSON.
61 error_content_type = "application/json"
63 def _FormatErrorMessage(self, values):
64 """Formats the body of an error message.
67 @param values: dictionary with keys code, message and explain.
69 @return: the body of the message
72 return serializer.DumpJson(values, indent=True)
75 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
76 http.server.HttpServer):
77 """REST Request Handler Class.
80 AUTH_REALM = "Ganeti Remote API"
82 def __init__(self, *args, **kwargs):
83 # pylint: disable-msg=W0233
84 # it seems pylint doesn't see the second parent class there
85 http.server.HttpServer.__init__(self, *args, **kwargs)
86 http.auth.HttpServerRequestAuthentication.__init__(self)
87 self._resmap = connector.Mapper()
90 if os.path.isfile(constants.RAPI_USERS_FILE):
91 self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
95 def _GetRequestContext(self, req):
96 """Returns the context for a request.
98 The context is cached in the req.private variable.
101 if req.private is None:
102 (HandlerClass, items, args) = \
103 self._resmap.getController(req.request_path)
105 ctx = RemoteApiRequestContext()
106 ctx.handler = HandlerClass(items, args, req)
108 method = req.request_method.upper()
110 ctx.handler_fn = getattr(ctx.handler, method)
111 except AttributeError:
112 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
113 (method, req.request_path))
115 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
117 # Require permissions definition (usually in the base class)
118 if ctx.handler_access is None:
119 raise AssertionError("Permissions definition missing")
123 # Check for expected attributes
124 assert req.private.handler
125 assert req.private.handler_fn
126 assert req.private.handler_access is not None
130 def AuthenticationRequired(self, req):
131 """Determine whether authentication is required.
134 return bool(self._GetRequestContext(req).handler_access)
136 def Authenticate(self, req, username, password):
137 """Checks whether a user can access a resource.
140 ctx = self._GetRequestContext(req)
142 # Check username and password
145 user = self._users.get(username, None)
146 if user and self.VerifyBasicAuthPassword(req, username, password,
151 # Unknown user or password wrong
154 if (not ctx.handler_access or
155 set(user.options).intersection(ctx.handler_access)):
160 raise http.HttpForbidden()
162 def HandleRequest(self, req):
163 """Handles a request.
166 ctx = self._GetRequestContext(req)
169 result = ctx.handler_fn()
170 sn = ctx.handler.getSerialNumber()
172 req.response_headers[http.HTTP_ETAG] = str(sn)
173 except luxi.TimeoutError:
174 raise http.HttpGatewayTimeout()
175 except luxi.ProtocolError, err:
176 raise http.HttpBadGateway(str(err))
178 method = req.request_method.upper()
179 logging.exception("Error while handling the %s request", method)
185 def CheckRapi(options, args):
186 """Initial checks whether to run or exit with a failure.
189 if args: # rapi doesn't take any arguments
190 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
192 sys.exit(constants.EXIT_FAILURE)
194 ssconf.CheckMaster(options.debug)
197 def ExecRapi(options, _):
198 """Main remote API function, executed with the PID file held.
201 # Read SSL certificate
203 ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
204 ssl_cert_path=options.ssl_cert)
208 mainloop = daemon.Mainloop()
209 server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
210 ssl_params=ssl_params, ssl_verify_peer=False,
211 request_executor_class=JsonErrorRequestExecutor)
212 # pylint: disable-msg=E1101
213 # it seems pylint doesn't see the second parent class there
225 parser = optparse.OptionParser(description="Ganeti Remote API",
226 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
227 version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
229 dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
230 dirs.append((constants.LOG_OS_DIR, 0750))
231 daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
234 if __name__ == "__main__":