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