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