4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012 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=C0103,W0142
27 # C0103: Invalid name ganeti-watcher
37 from pyinotify import pyinotify # pylint: disable=E0611
41 from ganeti import asyncnotifier
42 from ganeti import constants
43 from ganeti import http
44 from ganeti import daemon
45 from ganeti import ssconf
46 from ganeti import luxi
47 from ganeti import serializer
48 from ganeti import compat
49 from ganeti import utils
50 from ganeti import pathutils
51 from ganeti.rapi import connector
53 import ganeti.http.auth # pylint: disable=W0611
54 import ganeti.http.server
57 class RemoteApiRequestContext(object):
58 """Data structure for Remote API requests.
63 self.handler_fn = None
64 self.handler_access = None
68 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
69 http.server.HttpServerHandler):
70 """REST Request Handler Class.
73 AUTH_REALM = "Ganeti Remote API"
75 def __init__(self, user_fn, _client_cls=None):
76 """Initializes this class.
78 @type user_fn: callable
79 @param user_fn: Function receiving username as string and returning
80 L{http.auth.PasswordFileUser} or C{None} if user is not found
83 # pylint: disable=W0233
84 # it seems pylint doesn't see the second parent class there
85 http.server.HttpServerHandler.__init__(self)
86 http.auth.HttpServerRequestAuthentication.__init__(self)
87 self._client_cls = _client_cls
88 self._resmap = connector.Mapper()
89 self._user_fn = user_fn
92 def FormatErrorMessage(values):
93 """Formats the body of an error message.
96 @param values: dictionary with keys C{code}, C{message} and C{explain}.
97 @rtype: tuple; (string, string)
98 @return: Content-type and response body
101 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
103 def _GetRequestContext(self, req):
104 """Returns the context for a request.
106 The context is cached in the req.private variable.
109 if req.private is None:
110 (HandlerClass, items, args) = \
111 self._resmap.getController(req.request_path)
113 ctx = RemoteApiRequestContext()
114 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
116 method = req.request_method.upper()
118 ctx.handler_fn = getattr(ctx.handler, method)
119 except AttributeError:
120 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
121 (method, req.request_path))
123 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
125 # Require permissions definition (usually in the base class)
126 if ctx.handler_access is None:
127 raise AssertionError("Permissions definition missing")
129 # This is only made available in HandleRequest
134 # Check for expected attributes
135 assert req.private.handler
136 assert req.private.handler_fn
137 assert req.private.handler_access is not None
141 def AuthenticationRequired(self, req):
142 """Determine whether authentication is required.
145 return bool(self._GetRequestContext(req).handler_access)
147 def Authenticate(self, req, username, password):
148 """Checks whether a user can access a resource.
151 ctx = self._GetRequestContext(req)
153 user = self._user_fn(username)
155 self.VerifyBasicAuthPassword(req, username, password,
157 # Unknown user or password wrong
160 if (not ctx.handler_access or
161 set(user.options).intersection(ctx.handler_access)):
166 raise http.HttpForbidden()
168 def HandleRequest(self, req):
169 """Handles a request.
172 ctx = self._GetRequestContext(req)
174 # Deserialize request parameters
176 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
177 # include a Content-Type header field defining the media type of that
178 # body. [...] If the media type remains unknown, the recipient SHOULD
179 # treat it as type "application/octet-stream".
180 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
181 http.HTTP_APP_OCTET_STREAM)
182 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
183 raise http.HttpUnsupportedMediaType()
186 ctx.body_data = serializer.LoadJson(req.request_body)
188 raise http.HttpBadRequest(message="Unable to parse JSON data")
193 result = ctx.handler_fn()
194 except luxi.TimeoutError:
195 raise http.HttpGatewayTimeout()
196 except luxi.ProtocolError, err:
197 raise http.HttpBadGateway(str(err))
199 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
201 return serializer.DumpJson(result)
206 """Initializes this class.
211 def Get(self, username):
212 """Checks whether a user exists.
216 return self._users.get(username, None)
220 def Load(self, filename):
221 """Loads a file containing users and passwords.
223 @type filename: string
224 @param filename: Path to file
227 logging.info("Reading users file at %s", filename)
230 contents = utils.ReadFile(filename)
231 except EnvironmentError, err:
233 if err.errno == errno.ENOENT:
234 logging.warning("No users file at %s", filename)
236 logging.warning("Error while reading %s: %s", filename, err)
239 users = http.auth.ParsePasswordFile(contents)
241 except Exception, err: # pylint: disable=W0703
242 # We don't care about the type of exception
243 logging.error("Error while parsing %s: %s", filename, err)
251 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
252 def __init__(self, wm, path, cb):
253 """Initializes this class.
255 @param wm: Inotify watch manager
257 @param path: File path
259 @param cb: Function called on file change
262 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
265 self._filename = os.path.basename(path)
267 # Different Pyinotify versions have the flag constants at different places,
268 # hence not accessing them directly
269 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
270 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
271 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
272 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
274 self._handle = self.AddWatch(os.path.dirname(path), mask)
276 def process_default(self, event):
277 """Called upon inotify event.
280 if event.name == self._filename:
281 logging.debug("Received inotify event %s", event)
285 def SetupFileWatcher(filename, cb):
286 """Configures an inotify watcher for a file.
288 @type filename: string
289 @param filename: File to watch
291 @param cb: Function called on file change
294 wm = pyinotify.WatchManager()
295 handler = FileEventHandler(wm, filename, cb)
296 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
299 def CheckRapi(options, args):
300 """Initial checks whether to run or exit with a failure.
303 if args: # rapi doesn't take any arguments
304 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
306 sys.exit(constants.EXIT_FAILURE)
308 ssconf.CheckMaster(options.debug)
310 # Read SSL certificate (this is a little hackish to read the cert as root)
312 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
313 ssl_cert_path=options.ssl_cert)
315 options.ssl_params = None
318 def PrepRapi(options, _):
319 """Prep remote API function, executed with the PID file held.
322 mainloop = daemon.Mainloop()
326 handler = RemoteApiHandler(users.Get)
328 # Setup file watcher (it'll be driven by asyncore)
329 SetupFileWatcher(pathutils.RAPI_USERS_FILE,
330 compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
332 users.Load(pathutils.RAPI_USERS_FILE)
335 http.server.HttpServer(mainloop, options.bind_address, options.port,
337 ssl_params=options.ssl_params, ssl_verify_peer=False)
340 return (mainloop, server)
343 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
344 """Main remote API function, executed with the PID file held.
347 (mainloop, server) = prep_data
358 parser = optparse.OptionParser(description="Ganeti Remote API",
359 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
360 version="%%prog (ganeti) %s" %
361 constants.RELEASE_VERSION)
363 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
364 default_ssl_cert=pathutils.RAPI_CERT_FILE,
365 default_ssl_key=pathutils.RAPI_CERT_FILE)