4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013 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
52 from ganeti.rapi import baserlib
54 import ganeti.http.auth # pylint: disable=W0611
55 import ganeti.http.server
58 class RemoteApiRequestContext(object):
59 """Data structure for Remote API requests.
64 self.handler_fn = None
65 self.handler_access = None
69 class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
70 http.server.HttpServerHandler):
71 """REST Request Handler Class.
74 AUTH_REALM = "Ganeti Remote API"
76 def __init__(self, user_fn, reqauth, _client_cls=None):
77 """Initializes this class.
79 @type user_fn: callable
80 @param user_fn: Function receiving username as string and returning
81 L{http.auth.PasswordFileUser} or C{None} if user is not found
83 @param reqauth: Whether to require authentication
86 # pylint: disable=W0233
87 # it seems pylint doesn't see the second parent class there
88 http.server.HttpServerHandler.__init__(self)
89 http.auth.HttpServerRequestAuthentication.__init__(self)
90 self._client_cls = _client_cls
91 self._resmap = connector.Mapper()
92 self._user_fn = user_fn
93 self._reqauth = reqauth
96 def FormatErrorMessage(values):
97 """Formats the body of an error message.
100 @param values: dictionary with keys C{code}, C{message} and C{explain}.
101 @rtype: tuple; (string, string)
102 @return: Content-type and response body
105 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
107 def _GetRequestContext(self, req):
108 """Returns the context for a request.
110 The context is cached in the req.private variable.
113 if req.private is None:
114 (HandlerClass, items, args) = \
115 self._resmap.getController(req.request_path)
117 ctx = RemoteApiRequestContext()
118 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
120 method = req.request_method.upper()
122 ctx.handler_fn = getattr(ctx.handler, method)
123 except AttributeError:
124 raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
125 (method, req.request_path))
127 ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method)
129 # Require permissions definition (usually in the base class)
130 if ctx.handler_access is None:
131 raise AssertionError("Permissions definition missing")
133 # This is only made available in HandleRequest
138 # Check for expected attributes
139 assert req.private.handler
140 assert req.private.handler_fn
141 assert req.private.handler_access is not None
145 def AuthenticationRequired(self, req):
146 """Determine whether authentication is required.
149 return self._reqauth or bool(self._GetRequestContext(req).handler_access)
151 def Authenticate(self, req, username, password):
152 """Checks whether a user can access a resource.
155 ctx = self._GetRequestContext(req)
157 user = self._user_fn(username)
159 self.VerifyBasicAuthPassword(req, username, password,
161 # Unknown user or password wrong
164 if (not ctx.handler_access or
165 set(user.options).intersection(ctx.handler_access)):
170 raise http.HttpForbidden()
172 def HandleRequest(self, req):
173 """Handles a request.
176 ctx = self._GetRequestContext(req)
178 # Deserialize request parameters
180 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
181 # include a Content-Type header field defining the media type of that
182 # body. [...] If the media type remains unknown, the recipient SHOULD
183 # treat it as type "application/octet-stream".
184 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
185 http.HTTP_APP_OCTET_STREAM)
186 if req_content_type.lower() != http.HTTP_APP_JSON.lower():
187 raise http.HttpUnsupportedMediaType()
190 ctx.body_data = serializer.LoadJson(req.request_body)
192 raise http.HttpBadRequest(message="Unable to parse JSON data")
197 result = ctx.handler_fn()
198 except luxi.TimeoutError:
199 raise http.HttpGatewayTimeout()
200 except luxi.ProtocolError, err:
201 raise http.HttpBadGateway(str(err))
203 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
205 return serializer.DumpJson(result)
210 """Initializes this class.
215 def Get(self, username):
216 """Checks whether a user exists.
220 return self._users.get(username, None)
224 def Load(self, filename):
225 """Loads a file containing users and passwords.
227 @type filename: string
228 @param filename: Path to file
231 logging.info("Reading users file at %s", filename)
234 contents = utils.ReadFile(filename)
235 except EnvironmentError, err:
237 if err.errno == errno.ENOENT:
238 logging.warning("No users file at %s", filename)
240 logging.warning("Error while reading %s: %s", filename, err)
243 users = http.auth.ParsePasswordFile(contents)
245 except Exception, err: # pylint: disable=W0703
246 # We don't care about the type of exception
247 logging.error("Error while parsing %s: %s", filename, err)
255 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
256 def __init__(self, wm, path, cb):
257 """Initializes this class.
259 @param wm: Inotify watch manager
261 @param path: File path
263 @param cb: Function called on file change
266 asyncnotifier.FileEventHandlerBase.__init__(self, wm)
269 self._filename = os.path.basename(path)
271 # Different Pyinotify versions have the flag constants at different places,
272 # hence not accessing them directly
273 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
274 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
275 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
276 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
278 self._handle = self.AddWatch(os.path.dirname(path), mask)
280 def process_default(self, event):
281 """Called upon inotify event.
284 if event.name == self._filename:
285 logging.debug("Received inotify event %s", event)
289 def SetupFileWatcher(filename, cb):
290 """Configures an inotify watcher for a file.
292 @type filename: string
293 @param filename: File to watch
295 @param cb: Function called on file change
298 wm = pyinotify.WatchManager()
299 handler = FileEventHandler(wm, filename, cb)
300 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
303 def CheckRapi(options, args):
304 """Initial checks whether to run or exit with a failure.
307 if args: # rapi doesn't take any arguments
308 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
310 sys.exit(constants.EXIT_FAILURE)
312 ssconf.CheckMaster(options.debug)
314 # Read SSL certificate (this is a little hackish to read the cert as root)
316 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
317 ssl_cert_path=options.ssl_cert)
319 options.ssl_params = None
322 def PrepRapi(options, _):
323 """Prep remote API function, executed with the PID file held.
326 mainloop = daemon.Mainloop()
330 handler = RemoteApiHandler(users.Get, options.reqauth)
332 # Setup file watcher (it'll be driven by asyncore)
333 SetupFileWatcher(pathutils.RAPI_USERS_FILE,
334 compat.partial(users.Load, pathutils.RAPI_USERS_FILE))
336 users.Load(pathutils.RAPI_USERS_FILE)
339 http.server.HttpServer(mainloop, options.bind_address, options.port,
341 ssl_params=options.ssl_params, ssl_verify_peer=False)
344 return (mainloop, server)
347 def ExecRapi(options, args, prep_data): # pylint: disable=W0613
348 """Main remote API function, executed with the PID file held.
351 (mainloop, server) = prep_data
362 parser = optparse.OptionParser(description="Ganeti Remote API",
363 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]\
365 version="%%prog (ganeti) %s" %
366 constants.RELEASE_VERSION)
367 parser.add_option("--require-authentication", dest="reqauth",
368 default=False, action="store_true",
369 help=("Disable anonymous HTTP requests and require"
372 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
373 default_ssl_cert=pathutils.RAPI_CERT_FILE,
374 default_ssl_key=pathutils.RAPI_CERT_FILE)