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 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.rapi import connector 
 51   
 52  import ganeti.http.auth   # pylint: disable=W0611 
 53  import ganeti.http.server 
 54   
 55   
56 -class RemoteApiRequestContext(object):
57 """Data structure for Remote API requests. 58 59 """
60 - def __init__(self):
61 self.handler = None 62 self.handler_fn = None 63 self.handler_access = None 64 self.body_data = None
65 66
67 -class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
68 """Custom Request Executor class that formats HTTP errors in JSON. 69 70 """ 71 error_content_type = http.HTTP_APP_JSON 72
73 - def _FormatErrorMessage(self, values):
74 """Formats the body of an error message. 75 76 @type values: dict 77 @param values: dictionary with keys code, message and explain. 78 @rtype: string 79 @return: the body of the message 80 81 """ 82 return serializer.DumpJson(values, indent=True)
83 84
85 -class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication, 86 http.server.HttpServer):
87 """REST Request Handler Class. 88 89 """ 90 AUTH_REALM = "Ganeti Remote API" 91
92 - def __init__(self, *args, **kwargs):
93 # pylint: disable=W0233 94 # it seems pylint doesn't see the second parent class there 95 http.server.HttpServer.__init__(self, *args, **kwargs) 96 http.auth.HttpServerRequestAuthentication.__init__(self) 97 self._resmap = connector.Mapper() 98 self._users = None
99
100 - def LoadUsers(self, filename):
101 """Loads a file containing users and passwords. 102 103 @type filename: string 104 @param filename: Path to file 105 106 """ 107 logging.info("Reading users file at %s", filename) 108 try: 109 try: 110 contents = utils.ReadFile(filename) 111 except EnvironmentError, err: 112 self._users = None 113 if err.errno == errno.ENOENT: 114 logging.warning("No users file at %s", filename) 115 else: 116 logging.warning("Error while reading %s: %s", filename, err) 117 return False 118 119 users = http.auth.ParsePasswordFile(contents) 120 121 except Exception, err: # pylint: disable=W0703 122 # We don't care about the type of exception 123 logging.error("Error while parsing %s: %s", filename, err) 124 return False 125 126 self._users = users 127 128 return True
129
130 - def _GetRequestContext(self, req):
131 """Returns the context for a request. 132 133 The context is cached in the req.private variable. 134 135 """ 136 if req.private is None: 137 (HandlerClass, items, args) = \ 138 self._resmap.getController(req.request_path) 139 140 ctx = RemoteApiRequestContext() 141 ctx.handler = HandlerClass(items, args, req) 142 143 method = req.request_method.upper() 144 try: 145 ctx.handler_fn = getattr(ctx.handler, method) 146 except AttributeError: 147 raise http.HttpNotImplemented("Method %s is unsupported for path %s" % 148 (method, req.request_path)) 149 150 ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None) 151 152 # Require permissions definition (usually in the base class) 153 if ctx.handler_access is None: 154 raise AssertionError("Permissions definition missing") 155 156 # This is only made available in HandleRequest 157 ctx.body_data = None 158 159 req.private = ctx 160 161 # Check for expected attributes 162 assert req.private.handler 163 assert req.private.handler_fn 164 assert req.private.handler_access is not None 165 166 return req.private
167
168 - def AuthenticationRequired(self, req):
169 """Determine whether authentication is required. 170 171 """ 172 return bool(self._GetRequestContext(req).handler_access)
173
174 - def Authenticate(self, req, username, password):
175 """Checks whether a user can access a resource. 176 177 """ 178 ctx = self._GetRequestContext(req) 179 180 # Check username and password 181 valid_user = False 182 if self._users: 183 user = self._users.get(username, None) 184 if user and self.VerifyBasicAuthPassword(req, username, password, 185 user.password): 186 valid_user = True 187 188 if not valid_user: 189 # Unknown user or password wrong 190 return False 191 192 if (not ctx.handler_access or 193 set(user.options).intersection(ctx.handler_access)): 194 # Allow access 195 return True 196 197 # Access forbidden 198 raise http.HttpForbidden()
199
200 - def HandleRequest(self, req):
201 """Handles a request. 202 203 """ 204 ctx = self._GetRequestContext(req) 205 206 # Deserialize request parameters 207 if req.request_body: 208 # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD 209 # include a Content-Type header field defining the media type of that 210 # body. [...] If the media type remains unknown, the recipient SHOULD 211 # treat it as type "application/octet-stream". 212 req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE, 213 http.HTTP_APP_OCTET_STREAM) 214 if req_content_type.lower() != http.HTTP_APP_JSON.lower(): 215 raise http.HttpUnsupportedMediaType() 216 217 try: 218 ctx.body_data = serializer.LoadJson(req.request_body) 219 except Exception: 220 raise http.HttpBadRequest(message="Unable to parse JSON data") 221 else: 222 ctx.body_data = None 223 224 try: 225 result = ctx.handler_fn() 226 except luxi.TimeoutError: 227 raise http.HttpGatewayTimeout() 228 except luxi.ProtocolError, err: 229 raise http.HttpBadGateway(str(err)) 230 except: 231 method = req.request_method.upper() 232 logging.exception("Error while handling the %s request", method) 233 raise 234 235 req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON 236 237 return serializer.DumpJson(result)
238 239
240 -class FileEventHandler(asyncnotifier.FileEventHandlerBase):
241 - def __init__(self, wm, path, cb):
242 """Initializes this class. 243 244 @param wm: Inotify watch manager 245 @type path: string 246 @param path: File path 247 @type cb: callable 248 @param cb: Function called on file change 249 250 """ 251 asyncnotifier.FileEventHandlerBase.__init__(self, wm) 252 253 self._cb = cb 254 self._filename = os.path.basename(path) 255 256 # Different Pyinotify versions have the flag constants at different places, 257 # hence not accessing them directly 258 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] | 259 pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] | 260 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] | 261 pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"]) 262 263 self._handle = self.AddWatch(os.path.dirname(path), mask)
264
265 - def process_default(self, event):
266 """Called upon inotify event. 267 268 """ 269 if event.name == self._filename: 270 logging.debug("Received inotify event %s", event) 271 self._cb()
272 273
274 -def SetupFileWatcher(filename, cb):
275 """Configures an inotify watcher for a file. 276 277 @type filename: string 278 @param filename: File to watch 279 @type cb: callable 280 @param cb: Function called on file change 281 282 """ 283 wm = pyinotify.WatchManager() 284 handler = FileEventHandler(wm, filename, cb) 285 asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
286 287
288 -def CheckRapi(options, args):
289 """Initial checks whether to run or exit with a failure. 290 291 """ 292 if args: # rapi doesn't take any arguments 293 print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % 294 sys.argv[0]) 295 sys.exit(constants.EXIT_FAILURE) 296 297 ssconf.CheckMaster(options.debug) 298 299 # Read SSL certificate (this is a little hackish to read the cert as root) 300 if options.ssl: 301 options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key, 302 ssl_cert_path=options.ssl_cert) 303 else: 304 options.ssl_params = None
305 306
307 -def PrepRapi(options, _):
308 """Prep remote API function, executed with the PID file held. 309 310 """ 311 312 mainloop = daemon.Mainloop() 313 server = RemoteApiHttpServer(mainloop, options.bind_address, options.port, 314 ssl_params=options.ssl_params, 315 ssl_verify_peer=False, 316 request_executor_class=JsonErrorRequestExecutor) 317 318 # Setup file watcher (it'll be driven by asyncore) 319 SetupFileWatcher(constants.RAPI_USERS_FILE, 320 compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE)) 321 322 server.LoadUsers(constants.RAPI_USERS_FILE) 323 324 # pylint: disable=E1101 325 # it seems pylint doesn't see the second parent class there 326 server.Start() 327 328 return (mainloop, server)
329 330
331 -def ExecRapi(options, args, prep_data): # pylint: disable=W0613
332 """Main remote API function, executed with the PID file held. 333 334 """ 335 (mainloop, server) = prep_data 336 try: 337 mainloop.Run() 338 finally: 339 server.Stop() 340 341
342 -def Main():
343 """Main function. 344 345 """ 346 parser = optparse.OptionParser(description="Ganeti Remote API", 347 usage="%prog [-f] [-d] [-p port] [-b ADDRESS]", 348 version="%%prog (ganeti) %s" % constants.RELEASE_VERSION) 349 350 daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi, 351 default_ssl_cert=constants.RAPI_CERT_FILE, 352 default_ssl_key=constants.RAPI_CERT_FILE)
353