Package ganeti :: Package server :: Module rapi
[hide private]
[frames] | no frames]

Source Code for Module ganeti.server.rapi

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30  """Ganeti Remote API master script. 
 31   
 32  """ 
 33   
 34  # pylint: disable=C0103,W0142 
 35   
 36  # C0103: Invalid name ganeti-watcher 
 37   
 38  import logging 
 39  import optparse 
 40  import sys 
 41  import os 
 42  import os.path 
 43  import errno 
 44   
 45  try: 
 46    from pyinotify import pyinotify # pylint: disable=E0611 
 47  except ImportError: 
 48    import pyinotify 
 49   
 50  from ganeti import asyncnotifier 
 51  from ganeti import constants 
 52  from ganeti import http 
 53  from ganeti import daemon 
 54  from ganeti import ssconf 
 55  from ganeti import luxi 
 56  from ganeti import serializer 
 57  from ganeti import compat 
 58  from ganeti import utils 
 59  from ganeti import pathutils 
 60  from ganeti.rapi import connector 
 61  from ganeti.rapi import baserlib 
 62   
 63  import ganeti.http.auth   # pylint: disable=W0611 
 64  import ganeti.http.server 
65 66 67 -class RemoteApiRequestContext(object):
68 """Data structure for Remote API requests. 69 70 """
71 - def __init__(self):
72 self.handler = None 73 self.handler_fn = None 74 self.handler_access = None 75 self.body_data = None
76
77 78 -class RemoteApiHandler(http.auth.HttpServerRequestAuthentication, 79 http.server.HttpServerHandler):
80 """REST Request Handler Class. 81 82 """ 83 AUTH_REALM = "Ganeti Remote API" 84
85 - def __init__(self, user_fn, reqauth, _client_cls=None):
86 """Initializes this class. 87 88 @type user_fn: callable 89 @param user_fn: Function receiving username as string and returning 90 L{http.auth.PasswordFileUser} or C{None} if user is not found 91 @type reqauth: bool 92 @param reqauth: Whether to require authentication 93 94 """ 95 # pylint: disable=W0233 96 # it seems pylint doesn't see the second parent class there 97 http.server.HttpServerHandler.__init__(self) 98 http.auth.HttpServerRequestAuthentication.__init__(self) 99 self._client_cls = _client_cls 100 self._resmap = connector.Mapper() 101 self._user_fn = user_fn 102 self._reqauth = reqauth
103 104 @staticmethod
105 - def FormatErrorMessage(values):
106 """Formats the body of an error message. 107 108 @type values: dict 109 @param values: dictionary with keys C{code}, C{message} and C{explain}. 110 @rtype: tuple; (string, string) 111 @return: Content-type and response body 112 113 """ 114 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
115
116 - def _GetRequestContext(self, req):
117 """Returns the context for a request. 118 119 The context is cached in the req.private variable. 120 121 """ 122 if req.private is None: 123 (HandlerClass, items, args) = \ 124 self._resmap.getController(req.request_path) 125 126 ctx = RemoteApiRequestContext() 127 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls) 128 129 method = req.request_method.upper() 130 try: 131 ctx.handler_fn = getattr(ctx.handler, method) 132 except AttributeError: 133 raise http.HttpNotImplemented("Method %s is unsupported for path %s" % 134 (method, req.request_path)) 135 136 ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method) 137 138 # Require permissions definition (usually in the base class) 139 if ctx.handler_access is None: 140 raise AssertionError("Permissions definition missing") 141 142 # This is only made available in HandleRequest 143 ctx.body_data = None 144 145 req.private = ctx 146 147 # Check for expected attributes 148 assert req.private.handler 149 assert req.private.handler_fn 150 assert req.private.handler_access is not None 151 152 return req.private
153
154 - def AuthenticationRequired(self, req):
155 """Determine whether authentication is required. 156 157 """ 158 return self._reqauth or bool(self._GetRequestContext(req).handler_access)
159
160 - def Authenticate(self, req, username, password):
161 """Checks whether a user can access a resource. 162 163 """ 164 ctx = self._GetRequestContext(req) 165 166 user = self._user_fn(username) 167 if not (user and 168 self.VerifyBasicAuthPassword(req, username, password, 169 user.password)): 170 # Unknown user or password wrong 171 return False 172 173 if (not ctx.handler_access or 174 set(user.options).intersection(ctx.handler_access)): 175 # Allow access 176 return True 177 178 # Access forbidden 179 raise http.HttpForbidden()
180
181 - def HandleRequest(self, req):
182 """Handles a request. 183 184 """ 185 ctx = self._GetRequestContext(req) 186 187 # Deserialize request parameters 188 if req.request_body: 189 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD 190 # include a Content-Type header field defining the media type of that 191 # body. [...] If the media type remains unknown, the recipient SHOULD 192 # treat it as type "application/octet-stream". 193 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE, 194 http.HTTP_APP_OCTET_STREAM) 195 if req_content_type.lower() != http.HTTP_APP_JSON.lower(): 196 raise http.HttpUnsupportedMediaType() 197 198 try: 199 ctx.body_data = serializer.LoadJson(req.request_body) 200 except Exception: 201 raise http.HttpBadRequest(message="Unable to parse JSON data") 202 else: 203 ctx.body_data = None 204 205 try: 206 result = ctx.handler_fn() 207 except luxi.TimeoutError: 208 raise http.HttpGatewayTimeout() 209 except luxi.ProtocolError, err: 210 raise http.HttpBadGateway(str(err)) 211 212 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON 213 214 return serializer.DumpJson(result)
215
216 217 -class RapiUsers(object):
218 - def __init__(self):
219 """Initializes this class. 220 221 """ 222 self._users = None
223
224 - def Get(self, username):
225 """Checks whether a user exists. 226 227 """ 228 if self._users: 229 return self._users.get(username, None) 230 else: 231 return None
232
233 - def Load(self, filename):
234 """Loads a file containing users and passwords. 235 236 @type filename: string 237 @param filename: Path to file 238 239 """ 240 logging.info("Reading users file at %s", filename) 241 try: 242 try: 243 contents = utils.ReadFile(filename) 244 except EnvironmentError, err: 245 self._users = None 246 if err.errno == errno.ENOENT: 247 logging.warning("No users file at %s", filename) 248 else: 249 logging.warning("Error while reading %s: %s", filename, err) 250 return False 251 252 users = http.auth.ParsePasswordFile(contents) 253 254 except Exception, err: # pylint: disable=W0703 255 # We don't care about the type of exception 256 logging.error("Error while parsing %s: %s", filename, err) 257 return False 258 259 self._users = users 260 261 return True
262
263 264 -class FileEventHandler(asyncnotifier.FileEventHandlerBase):
265 - def __init__(self, wm, path, cb):
266 """Initializes this class. 267 268 @param wm: Inotify watch manager 269 @type path: string 270 @param path: File path 271 @type cb: callable 272 @param cb: Function called on file change 273 274 """ 275 asyncnotifier.FileEventHandlerBase.__init__(self, wm) 276 277 self._cb = cb 278 self._filename = os.path.basename(path) 279 280 # Different Pyinotify versions have the flag constants at different places, 281 # hence not accessing them directly 282 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] | 283 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] | 284 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] | 285 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"]) 286 287 self._handle = self.AddWatch(os.path.dirname(path), mask)
288
289 - def process_default(self, event):
290 """Called upon inotify event. 291 292 """ 293 if event.name == self._filename: 294 logging.debug("Received inotify event %s", event) 295 self._cb()
296
297 298 -def SetupFileWatcher(filename, cb):
299 """Configures an inotify watcher for a file. 300 301 @type filename: string 302 @param filename: File to watch 303 @type cb: callable 304 @param cb: Function called on file change 305 306 """ 307 wm = pyinotify.WatchManager() 308 handler = FileEventHandler(wm, filename, cb) 309 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
310
311 312 -def CheckRapi(options, args):
313 """Initial checks whether to run or exit with a failure. 314 315 """ 316 if args: # rapi doesn't take any arguments 317 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % 318 sys.argv[0]) 319 sys.exit(constants.EXIT_FAILURE) 320 321 ssconf.CheckMaster(options.debug) 322 323 # Read SSL certificate (this is a little hackish to read the cert as root) 324 if options.ssl: 325 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key, 326 ssl_cert_path=options.ssl_cert) 327 else: 328 options.ssl_params = None
329
330 331 -def PrepRapi(options, _):
332 """Prep remote API function, executed with the PID file held. 333 334 """ 335 mainloop = daemon.Mainloop() 336 337 users = RapiUsers() 338 339 handler = RemoteApiHandler(users.Get, options.reqauth) 340 341 # Setup file watcher (it'll be driven by asyncore) 342 SetupFileWatcher(pathutils.RAPI_USERS_FILE, 343 compat.partial(users.Load, pathutils.RAPI_USERS_FILE)) 344 345 users.Load(pathutils.RAPI_USERS_FILE) 346 347 server = \ 348 http.server.HttpServer(mainloop, options.bind_address, options.port, 349 handler, 350 ssl_params=options.ssl_params, ssl_verify_peer=False) 351 server.Start() 352 353 return (mainloop, server)
354
355 356 -def ExecRapi(options, args, prep_data): # pylint: disable=W0613
357 """Main remote API function, executed with the PID file held. 358 359 """ 360 (mainloop, server) = prep_data 361 try: 362 mainloop.Run() 363 finally: 364 server.Stop() 365
366 367 -def Main():
368 """Main function. 369 370 """ 371 parser = optparse.OptionParser(description="Ganeti Remote API", 372 usage=("%prog [-f] [-d] [-p port] [-b ADDRESS]" 373 " [-i INTERFACE]"), 374 version="%%prog (ganeti) %s" % 375 constants.RELEASE_VERSION) 376 parser.add_option("--require-authentication", dest="reqauth", 377 default=False, action="store_true", 378 help=("Disable anonymous HTTP requests and require" 379 " authentication")) 380 381 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi, 382 default_ssl_cert=pathutils.RAPI_CERT_FILE, 383 default_ssl_key=pathutils.RAPI_CERT_FILE)
384