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
58 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
59 """Custom Request Executor class that formats HTTP errors in JSON.
62 error_content_type = http.HTTP_APP_JSON
64 def _FormatErrorMessage(self, values):
65 """Formats the body of an error message.
68 @param values: dictionary with keys code, message and explain.
70 @return: the body of the message
73 return serializer.DumpJson(values, indent=True)
76 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
77 http.server.HttpServer):
78 """REST Request Handler Class.
81 AUTH_REALM = "Ganeti Remote API"
83 def __init__(self, *args, **kwargs):
84 # pylint: disable-msg=W0233
85 # it seems pylint doesn't see the second parent class there
86 http.server.HttpServer.__init__(self, *args, **kwargs)
87 http.auth.HttpServerRequestAuthentication.__init__(self)
88 self._resmap = connector.Mapper()
91 if os.path.isfile(constants.RAPI_USERS_FILE):
92 self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
96 def _GetRequestContext(self, req):
97 """Returns the context for a request.
99 The context is cached in the req.private variable.
102 if req.private is None:
103 (HandlerClass, items, args) = \
104 self._resmap.getController(req.request_path)
106 ctx = RemoteApiRequestContext()
107 ctx.handler = HandlerClass(items, args, req)
109 method = req.request_method.upper()
111 ctx.handler_fn = getattr(ctx.handler, method)
112 except AttributeError:
113 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
114 (method, req.request_path))
116 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
118 # Require permissions definition (usually in the base class)
119 if ctx.handler_access is None:
120 raise AssertionError("Permissions definition missing")
122 # This is only made available in HandleRequest
127 # Check for expected attributes
128 assert req.private.handler
129 assert req.private.handler_fn
130 assert req.private.handler_access is not None
134 def AuthenticationRequired(self, req):
135 """Determine whether authentication is required.
138 return bool(self._GetRequestContext(req).handler_access)
140 def Authenticate(self, req, username, password):
141 """Checks whether a user can access a resource.
144 ctx = self._GetRequestContext(req)
146 # Check username and password
149 user = self._users.get(username, None)
150 if user and self.VerifyBasicAuthPassword(req, username, password,
155 # Unknown user or password wrong
158 if (not ctx.handler_access or
159 set(user.options).intersection(ctx.handler_access)):
164 raise http.HttpForbidden()
166 def HandleRequest(self, req):
167 """Handles a request.
170 ctx = self._GetRequestContext(req)
172 # Deserialize request parameters
174 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
175 # include a Content-Type header field defining the media type of that
176 # body. [...] If the media type remains unknown, the recipient SHOULD
177 # treat it as type "application/octet-stream".
178 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
179 http.HTTP_APP_OCTET_STREAM)
180 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
181 raise http.HttpUnsupportedMediaType()
184 ctx.body_data = serializer.LoadJson(req.request_body)
186 raise http.HttpBadRequest(message="Unable to parse JSON data")
191 result = ctx.handler_fn()
192 except luxi.TimeoutError:
193 raise http.HttpGatewayTimeout()
194 except luxi.ProtocolError, err:
195 raise http.HttpBadGateway(str(err))
197 method = req.request_method.upper()
198 logging.exception("Error while handling the %s request", method)
201 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
203 return serializer.DumpJson(result)
206 def CheckRapi(options, args):
207 """Initial checks whether to run or exit with a failure.
210 if args: # rapi doesn't take any arguments
211 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
213 sys.exit(constants.EXIT_FAILURE)
215 ssconf.CheckMaster(options.debug)
217 # Read SSL certificate (this is a little hackish to read the cert as root)
219 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
220 ssl_cert_path=options.ssl_cert)
222 options.ssl_params = None
225 def ExecRapi(options, _):
226 """Main remote API function, executed with the PID file held.
230 mainloop = daemon.Mainloop()
231 server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
232 ssl_params=options.ssl_params,
233 ssl_verify_peer=False,
234 request_executor_class=JsonErrorRequestExecutor)
235 # pylint: disable-msg=E1101
236 # it seems pylint doesn't see the second parent class there
248 parser = optparse.OptionParser(description="Ganeti Remote API",
249 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
250 version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
252 dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
253 dirs.append((constants.LOG_OS_DIR, 0750))
254 daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
255 default_ssl_cert=constants.RAPI_CERT_FILE,
256 default_ssl_key=constants.RAPI_CERT_FILE,
257 user=constants.RAPI_USER, group=constants.DAEMONS_GROUP)
260 if __name__ == "__main__":