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
36 from pyinotify import pyinotify # pylint: disable-msg=E0611
40 from ganeti import asyncnotifier
41 from ganeti import constants
42 from ganeti import http
43 from ganeti import daemon
44 from ganeti import ssconf
45 from ganeti import luxi
46 from ganeti import serializer
47 from ganeti.rapi import connector
49 import ganeti.http.auth # pylint: disable-msg=W0611
50 import ganeti.http.server
53 class RemoteApiRequestContext(object):
54 """Data structure for Remote API requests.
59 self.handler_fn = None
60 self.handler_access = None
64 class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
65 """Custom Request Executor class that formats HTTP errors in JSON.
68 error_content_type = http.HTTP_APP_JSON
70 def _FormatErrorMessage(self, values):
71 """Formats the body of an error message.
74 @param values: dictionary with keys code, message and explain.
76 @return: the body of the message
79 return serializer.DumpJson(values, indent=True)
82 class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
83 http.server.HttpServer):
84 """REST Request Handler Class.
87 AUTH_REALM = "Ganeti Remote API"
89 def __init__(self, *args, **kwargs):
90 # pylint: disable-msg=W0233
91 # it seems pylint doesn't see the second parent class there
92 http.server.HttpServer.__init__(self, *args, **kwargs)
93 http.auth.HttpServerRequestAuthentication.__init__(self)
94 self._resmap = connector.Mapper()
97 if os.path.isfile(constants.RAPI_USERS_FILE):
98 wm = pyinotify.WatchManager()
99 hdl = asyncnotifier.SingleFileEventHandler(wm, self._OnUsersFileUpdate,
100 constants.RAPI_USERS_FILE)
101 self._users_inotify_handler = hdl
102 asyncnotifier.AsyncNotifier(wm, default_proc_fun=hdl)
104 self._OnUsersFileUpdate(False)
108 def _OnUsersFileUpdate(self, notifier_enabled):
109 """Called upon update of the RAPI users file by pyinotify.
111 @type notifier_enabled: boolean
112 @param notifier_enabled: whether the notifier is still enabled
115 logging.info("Reloading modified %s", constants.RAPI_USERS_FILE)
118 users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
120 except Exception, err: # pylint: disable-msg=W0703
121 # We don't care about the type of exception
122 logging.error("Error while reading %s: %s", constants.RAPI_USERS_FILE,
125 # Renable the watch again if we'd an atomic update of the file (e.g. mv)
126 if not notifier_enabled:
127 self._users_inotify_handler.enable()
129 def _GetRequestContext(self, req):
130 """Returns the context for a request.
132 The context is cached in the req.private variable.
135 if req.private is None:
136 (HandlerClass, items, args) = \
137 self._resmap.getController(req.request_path)
139 ctx = RemoteApiRequestContext()
140 ctx.handler = HandlerClass(items, args, req)
142 method = req.request_method.upper()
144 ctx.handler_fn = getattr(ctx.handler, method)
145 except AttributeError:
146 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
147 (method, req.request_path))
149 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
151 # Require permissions definition (usually in the base class)
152 if ctx.handler_access is None:
153 raise AssertionError("Permissions definition missing")
155 # This is only made available in HandleRequest
160 # Check for expected attributes
161 assert req.private.handler
162 assert req.private.handler_fn
163 assert req.private.handler_access is not None
167 def AuthenticationRequired(self, req):
168 """Determine whether authentication is required.
171 return bool(self._GetRequestContext(req).handler_access)
173 def Authenticate(self, req, username, password):
174 """Checks whether a user can access a resource.
177 ctx = self._GetRequestContext(req)
179 # Check username and password
182 user = self._users.get(username, None)
183 if user and self.VerifyBasicAuthPassword(req, username, password,
188 # Unknown user or password wrong
191 if (not ctx.handler_access or
192 set(user.options).intersection(ctx.handler_access)):
197 raise http.HttpForbidden()
199 def HandleRequest(self, req):
200 """Handles a request.
203 ctx = self._GetRequestContext(req)
205 # Deserialize request parameters
207 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
208 # include a Content-Type header field defining the media type of that
209 # body. [...] If the media type remains unknown, the recipient SHOULD
210 # treat it as type "application/octet-stream".
211 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
212 http.HTTP_APP_OCTET_STREAM)
213 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
214 raise http.HttpUnsupportedMediaType()
217 ctx.body_data = serializer.LoadJson(req.request_body)
219 raise http.HttpBadRequest(message="Unable to parse JSON data")
224 result = ctx.handler_fn()
225 except luxi.TimeoutError:
226 raise http.HttpGatewayTimeout()
227 except luxi.ProtocolError, err:
228 raise http.HttpBadGateway(str(err))
230 method = req.request_method.upper()
231 logging.exception("Error while handling the %s request", method)
234 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
236 return serializer.DumpJson(result)
239 def CheckRapi(options, args):
240 """Initial checks whether to run or exit with a failure.
243 if args: # rapi doesn't take any arguments
244 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
246 sys.exit(constants.EXIT_FAILURE)
248 ssconf.CheckMaster(options.debug)
250 # Read SSL certificate (this is a little hackish to read the cert as root)
252 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
253 ssl_cert_path=options.ssl_cert)
255 options.ssl_params = None
258 def ExecRapi(options, _):
259 """Main remote API function, executed with the PID file held.
263 mainloop = daemon.Mainloop()
264 server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
265 ssl_params=options.ssl_params,
266 ssl_verify_peer=False,
267 request_executor_class=JsonErrorRequestExecutor)
268 # pylint: disable-msg=E1101
269 # it seems pylint doesn't see the second parent class there
281 parser = optparse.OptionParser(description="Ganeti Remote API",
282 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
283 version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
285 daemon.GenericMain(constants.RAPI, parser, CheckRapi, ExecRapi,
286 default_ssl_cert=constants.RAPI_CERT_FILE,
287 default_ssl_key=constants.RAPI_CERT_FILE)
290 if __name__ == "__main__":