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  # 
  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. 
 10  # 
 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. 
 15  # 
 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 
 19  # 02110-1301, USA. 
 20   
 21  """Ganeti Remote API master script. 
 22   
 23  """ 
 24   
 25  # pylint: disable=C0103,W0142 
 26   
 27  # C0103: Invalid name ganeti-watcher 
 28   
 29  import logging 
 30  import optparse 
 31  import sys 
 32  import os 
 33  import os.path 
 34  import errno 
 35   
 36  try: 
 37    from pyinotify import pyinotify # pylint: disable=E0611 
 38  except ImportError: 
 39    import pyinotify 
 40   
 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 
 53   
 54  import ganeti.http.auth   # pylint: disable=W0611 
 55  import ganeti.http.server 
56 57 58 -class RemoteApiRequestContext(object):
59 """Data structure for Remote API requests. 60 61 """
62 - def __init__(self):
63 self.handler = None 64 self.handler_fn = None 65 self.handler_access = None 66 self.body_data = None
67
68 69 -class RemoteApiHandler(http.auth.HttpServerRequestAuthentication, 70 http.server.HttpServerHandler):
71 """REST Request Handler Class. 72 73 """ 74 AUTH_REALM = "Ganeti Remote API" 75
76 - def __init__(self, user_fn, reqauth, _client_cls=None):
77 """Initializes this class. 78 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 82 @type reqauth: bool 83 @param reqauth: Whether to require authentication 84 85 """ 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
94 95 @staticmethod
96 - def FormatErrorMessage(values):
97 """Formats the body of an error message. 98 99 @type values: dict 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 103 104 """ 105 return (http.HTTP_APP_JSON, serializer.DumpJson(values))
106
107 - def _GetRequestContext(self, req):
108 """Returns the context for a request. 109 110 The context is cached in the req.private variable. 111 112 """ 113 if req.private is None: 114 (HandlerClass, items, args) = \ 115 self._resmap.getController(req.request_path) 116 117 ctx = RemoteApiRequestContext() 118 ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls) 119 120 method = req.request_method.upper() 121 try: 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)) 126 127 ctx.handler_access = baserlib.GetHandlerAccess(ctx.handler, method) 128 129 # Require permissions definition (usually in the base class) 130 if ctx.handler_access is None: 131 raise AssertionError("Permissions definition missing") 132 133 # This is only made available in HandleRequest 134 ctx.body_data = None 135 136 req.private = ctx 137 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 142 143 return req.private
144
145 - def AuthenticationRequired(self, req):
146 """Determine whether authentication is required. 147 148 """ 149 return self._reqauth or bool(self._GetRequestContext(req).handler_access)
150
151 - def Authenticate(self, req, username, password):
152 """Checks whether a user can access a resource. 153 154 """ 155 ctx = self._GetRequestContext(req) 156 157 user = self._user_fn(username) 158 if not (user and 159 self.VerifyBasicAuthPassword(req, username, password, 160 user.password)): 161 # Unknown user or password wrong 162 return False 163 164 if (not ctx.handler_access or 165 set(user.options).intersection(ctx.handler_access)): 166 # Allow access 167 return True 168 169 # Access forbidden 170 raise http.HttpForbidden()
171
172 - def HandleRequest(self, req):
173 """Handles a request. 174 175 """ 176 ctx = self._GetRequestContext(req) 177 178 # Deserialize request parameters 179 if req.request_body: 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() 188 189 try: 190 ctx.body_data = serializer.LoadJson(req.request_body) 191 except Exception: 192 raise http.HttpBadRequest(message="Unable to parse JSON data") 193 else: 194 ctx.body_data = None 195 196 try: 197 result = ctx.handler_fn() 198 except luxi.TimeoutError: 199 raise http.HttpGatewayTimeout() 200 except luxi.ProtocolError, err: 201 raise http.HttpBadGateway(str(err)) 202 203 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON 204 205 return serializer.DumpJson(result)
206
207 208 -class RapiUsers:
209 - def __init__(self):
210 """Initializes this class. 211 212 """ 213 self._users = None
214
215 - def Get(self, username):
216 """Checks whether a user exists. 217 218 """ 219 if self._users: 220 return self._users.get(username, None) 221 else: 222 return None
223
224 - def Load(self, filename):
225 """Loads a file containing users and passwords. 226 227 @type filename: string 228 @param filename: Path to file 229 230 """ 231 logging.info("Reading users file at %s", filename) 232 try: 233 try: 234 contents = utils.ReadFile(filename) 235 except EnvironmentError, err: 236 self._users = None 237 if err.errno == errno.ENOENT: 238 logging.warning("No users file at %s", filename) 239 else: 240 logging.warning("Error while reading %s: %s", filename, err) 241 return False 242 243 users = http.auth.ParsePasswordFile(contents) 244 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) 248 return False 249 250 self._users = users 251 252 return True
253
254 255 -class FileEventHandler(asyncnotifier.FileEventHandlerBase):
256 - def __init__(self, wm, path, cb):
257 """Initializes this class. 258 259 @param wm: Inotify watch manager 260 @type path: string 261 @param path: File path 262 @type cb: callable 263 @param cb: Function called on file change 264 265 """ 266 asyncnotifier.FileEventHandlerBase.__init__(self, wm) 267 268 self._cb = cb 269 self._filename = os.path.basename(path) 270 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"]) 277 278 self._handle = self.AddWatch(os.path.dirname(path), mask)
279
280 - def process_default(self, event):
281 """Called upon inotify event. 282 283 """ 284 if event.name == self._filename: 285 logging.debug("Received inotify event %s", event) 286 self._cb()
287
288 289 -def SetupFileWatcher(filename, cb):
290 """Configures an inotify watcher for a file. 291 292 @type filename: string 293 @param filename: File to watch 294 @type cb: callable 295 @param cb: Function called on file change 296 297 """ 298 wm = pyinotify.WatchManager() 299 handler = FileEventHandler(wm, filename, cb) 300 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
301
302 303 -def CheckRapi(options, args):
304 """Initial checks whether to run or exit with a failure. 305 306 """ 307 if args: # rapi doesn't take any arguments 308 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % 309 sys.argv[0]) 310 sys.exit(constants.EXIT_FAILURE) 311 312 ssconf.CheckMaster(options.debug) 313 314 # Read SSL certificate (this is a little hackish to read the cert as root) 315 if options.ssl: 316 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key, 317 ssl_cert_path=options.ssl_cert) 318 else: 319 options.ssl_params = None
320
321 322 -def PrepRapi(options, _):
323 """Prep remote API function, executed with the PID file held. 324 325 """ 326 mainloop = daemon.Mainloop() 327 328 users = RapiUsers() 329 330 handler = RemoteApiHandler(users.Get, options.reqauth) 331 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)) 335 336 users.Load(pathutils.RAPI_USERS_FILE) 337 338 server = \ 339 http.server.HttpServer(mainloop, options.bind_address, options.port, 340 handler, 341 ssl_params=options.ssl_params, ssl_verify_peer=False) 342 server.Start() 343 344 return (mainloop, server)
345
346 347 -def ExecRapi(options, args, prep_data): # pylint: disable=W0613
348 """Main remote API function, executed with the PID file held. 349 350 """ 351 (mainloop, server) = prep_data 352 try: 353 mainloop.Run() 354 finally: 355 server.Stop() 356
357 358 -def Main():
359 """Main function. 360 361 """ 362 parser = optparse.OptionParser(description="Ganeti Remote API", 363 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]", 364 version="%%prog (ganeti) %s" % 365 constants.RELEASE_VERSION) 366 parser.add_option("--require-authentication", dest="reqauth", 367 default=False, action="store_true", 368 help=("Disable anonymous HTTP requests and require" 369 " authentication")) 370 371 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi, 372 default_ssl_cert=pathutils.RAPI_CERT_FILE, 373 default_ssl_key=pathutils.RAPI_CERT_FILE)
374