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

Source Code for Module ganeti.rapi.baserlib

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2006, 2007, 2008, 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   
 22  """Remote API base resources library. 
 23   
 24  """ 
 25   
 26  # pylint: disable=C0103 
 27   
 28  # C0103: Invalid name, since the R_* names are not conforming 
 29   
 30  import logging 
 31   
 32  from ganeti import luxi 
 33  from ganeti import rapi 
 34  from ganeti import http 
 35  from ganeti import errors 
 36  from ganeti import compat 
 37  from ganeti import constants 
 38  from ganeti import pathutils 
 39   
 40   
 41  # Dummy value to detect unchanged parameters 
 42  _DEFAULT = object() 
 43   
 44  #: Supported HTTP methods 
 45  _SUPPORTED_METHODS = compat.UniqueFrozenset([ 
 46    http.HTTP_DELETE, 
 47    http.HTTP_GET, 
 48    http.HTTP_POST, 
 49    http.HTTP_PUT, 
 50    ]) 
 51   
 52   
53 -def _BuildOpcodeAttributes():
54 """Builds list of attributes used for per-handler opcodes. 55 56 """ 57 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method, 58 "Get%sOpInput" % method.capitalize()) 59 for method in _SUPPORTED_METHODS]
60 61 62 _OPCODE_ATTRS = _BuildOpcodeAttributes() 63 64
65 -def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
66 """Builds a URI list as used by index resources. 67 68 @param ids: list of ids as strings 69 @param uri_format: format to be applied for URI 70 @param uri_fields: optional parameter for field IDs 71 72 """ 73 (field_id, field_uri) = uri_fields 74 75 def _MapId(m_id): 76 return { 77 field_id: m_id, 78 field_uri: uri_format % m_id, 79 }
80 81 # Make sure the result is sorted, makes it nicer to look at and simplifies 82 # unittests. 83 ids.sort() 84 85 return map(_MapId, ids) 86 87
88 -def MapFields(names, data):
89 """Maps two lists into one dictionary. 90 91 Example:: 92 >>> MapFields(["a", "b"], ["foo", 123]) 93 {'a': 'foo', 'b': 123} 94 95 @param names: field names (list of strings) 96 @param data: field data (list) 97 98 """ 99 if len(names) != len(data): 100 raise AttributeError("Names and data must have the same length") 101 return dict(zip(names, data))
102 103
104 -def MapBulkFields(itemslist, fields):
105 """Map value to field name in to one dictionary. 106 107 @param itemslist: a list of items values 108 @param fields: a list of items names 109 110 @return: a list of mapped dictionaries 111 112 """ 113 items_details = [] 114 for item in itemslist: 115 mapped = MapFields(fields, item) 116 items_details.append(mapped) 117 return items_details
118 119
120 -def FillOpcode(opcls, body, static, rename=None):
121 """Fills an opcode with body parameters. 122 123 Parameter types are checked. 124 125 @type opcls: L{opcodes.OpCode} 126 @param opcls: Opcode class 127 @type body: dict 128 @param body: Body parameters as received from client 129 @type static: dict 130 @param static: Static parameters which can't be modified by client 131 @type rename: dict 132 @param rename: Renamed parameters, key as old name, value as new name 133 @return: Opcode object 134 135 """ 136 if body is None: 137 params = {} 138 else: 139 CheckType(body, dict, "Body contents") 140 141 # Make copy to be modified 142 params = body.copy() 143 144 if rename: 145 for old, new in rename.items(): 146 if new in params and old in params: 147 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but" 148 " both are specified" % 149 (old, new)) 150 if old in params: 151 assert new not in params 152 params[new] = params.pop(old) 153 154 if static: 155 overwritten = set(params.keys()) & set(static.keys()) 156 if overwritten: 157 raise http.HttpBadRequest("Can't overwrite static parameters %r" % 158 overwritten) 159 160 params.update(static) 161 162 # Convert keys to strings (simplejson decodes them as unicode) 163 params = dict((str(key), value) for (key, value) in params.items()) 164 165 try: 166 op = opcls(**params) # pylint: disable=W0142 167 op.Validate(False) 168 except (errors.OpPrereqError, TypeError), err: 169 raise http.HttpBadRequest("Invalid body parameters: %s" % err) 170 171 return op
172 173
174 -def HandleItemQueryErrors(fn, *args, **kwargs):
175 """Converts errors when querying a single item. 176 177 """ 178 try: 179 return fn(*args, **kwargs) 180 except errors.OpPrereqError, err: 181 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT: 182 raise http.HttpNotFound() 183 184 raise
185 186
187 -def FeedbackFn(msg):
188 """Feedback logging function for jobs. 189 190 We don't have a stdout for printing log messages, so log them to the 191 http log at least. 192 193 @param msg: the message 194 195 """ 196 (_, log_type, log_msg) = msg 197 logging.info("%s: %s", log_type, log_msg)
198 199
200 -def CheckType(value, exptype, descr):
201 """Abort request if value type doesn't match expected type. 202 203 @param value: Value 204 @type exptype: type 205 @param exptype: Expected type 206 @type descr: string 207 @param descr: Description of value 208 @return: Value (allows inline usage) 209 210 """ 211 if not isinstance(value, exptype): 212 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" % 213 (descr, type(value).__name__, exptype.__name__)) 214 215 return value
216 217
218 -def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
219 """Check and return the value for a given parameter. 220 221 If no default value was given and the parameter doesn't exist in the input 222 data, an error is raise. 223 224 @type data: dict 225 @param data: Dictionary containing input data 226 @type name: string 227 @param name: Parameter name 228 @param default: Default value (can be None) 229 @param exptype: Expected type (can be None) 230 231 """ 232 try: 233 value = data[name] 234 except KeyError: 235 if default is not _DEFAULT: 236 return default 237 238 raise http.HttpBadRequest("Required parameter '%s' is missing" % 239 name) 240 241 if exptype is _DEFAULT: 242 return value 243 244 return CheckType(value, exptype, "'%s' parameter" % name)
245 246
247 -class ResourceBase(object):
248 """Generic class for resources. 249 250 """ 251 # Default permission requirements 252 GET_ACCESS = [] 253 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE] 254 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE] 255 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE] 256
257 - def __init__(self, items, queryargs, req, _client_cls=None):
258 """Generic resource constructor. 259 260 @param items: a list with variables encoded in the URL 261 @param queryargs: a dictionary with additional options from URL 262 @param req: Request context 263 @param _client_cls: L{luxi} client class (unittests only) 264 265 """ 266 self.items = items 267 self.queryargs = queryargs 268 self._req = req 269 270 if _client_cls is None: 271 _client_cls = luxi.Client 272 273 self._client_cls = _client_cls
274
275 - def _GetRequestBody(self):
276 """Returns the body data. 277 278 """ 279 return self._req.private.body_data
280 281 request_body = property(fget=_GetRequestBody) 282
283 - def _checkIntVariable(self, name, default=0):
284 """Return the parsed value of an int argument. 285 286 """ 287 val = self.queryargs.get(name, default) 288 if isinstance(val, list): 289 if val: 290 val = val[0] 291 else: 292 val = default 293 try: 294 val = int(val) 295 except (ValueError, TypeError): 296 raise http.HttpBadRequest("Invalid value for the" 297 " '%s' parameter" % (name,)) 298 return val
299
300 - def _checkStringVariable(self, name, default=None):
301 """Return the parsed value of a string argument. 302 303 """ 304 val = self.queryargs.get(name, default) 305 if isinstance(val, list): 306 if val: 307 val = val[0] 308 else: 309 val = default 310 return val
311
312 - def getBodyParameter(self, name, *args):
313 """Check and return the value for a given parameter. 314 315 If a second parameter is not given, an error will be returned, 316 otherwise this parameter specifies the default value. 317 318 @param name: the required parameter 319 320 """ 321 if args: 322 return CheckParameter(self.request_body, name, default=args[0]) 323 324 return CheckParameter(self.request_body, name)
325
326 - def useLocking(self):
327 """Check if the request specifies locking. 328 329 """ 330 return bool(self._checkIntVariable("lock"))
331
332 - def useBulk(self):
333 """Check if the request specifies bulk querying. 334 335 """ 336 return bool(self._checkIntVariable("bulk"))
337
338 - def useForce(self):
339 """Check if the request specifies a forced operation. 340 341 """ 342 return bool(self._checkIntVariable("force"))
343
344 - def dryRun(self):
345 """Check if the request specifies dry-run mode. 346 347 """ 348 return bool(self._checkIntVariable("dry-run"))
349
350 - def GetClient(self, query=False):
351 """Wrapper for L{luxi.Client} with HTTP-specific error handling. 352 353 @param query: this signifies that the client will only be used for 354 queries; if the build-time parameter enable-split-queries is 355 enabled, then the client will be connected to the query socket 356 instead of the masterd socket 357 358 """ 359 if query and constants.ENABLE_SPLIT_QUERY: 360 address = pathutils.QUERY_SOCKET 361 else: 362 address = None 363 # Could be a function, pylint: disable=R0201 364 try: 365 return self._client_cls(address=address) 366 except luxi.NoMasterError, err: 367 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err) 368 except luxi.PermissionError: 369 raise http.HttpInternalServerError("Internal error: no permission to" 370 " connect to the master daemon")
371
372 - def SubmitJob(self, op, cl=None):
373 """Generic wrapper for submit job, for better http compatibility. 374 375 @type op: list 376 @param op: the list of opcodes for the job 377 @type cl: None or luxi.Client 378 @param cl: optional luxi client to use 379 @rtype: string 380 @return: the job ID 381 382 """ 383 if cl is None: 384 cl = self.GetClient() 385 try: 386 return cl.SubmitJob(op) 387 except errors.JobQueueFull: 388 raise http.HttpServiceUnavailable("Job queue is full, needs archiving") 389 except errors.JobQueueDrainError: 390 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit") 391 except luxi.NoMasterError, err: 392 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err) 393 except luxi.PermissionError: 394 raise http.HttpInternalServerError("Internal error: no permission to" 395 " connect to the master daemon") 396 except luxi.TimeoutError, err: 397 raise http.HttpGatewayTimeout("Timeout while talking to the master" 398 " daemon: %s" % err)
399 400
401 -def GetResourceOpcodes(cls):
402 """Returns all opcodes used by a resource. 403 404 """ 405 return frozenset(filter(None, (getattr(cls, op_attr, None) 406 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
407 408
409 -class _MetaOpcodeResource(type):
410 """Meta class for RAPI resources. 411 412 """
413 - def __call__(mcs, *args, **kwargs):
414 """Instantiates class and patches it for use by the RAPI daemon. 415 416 """ 417 # Access to private attributes of a client class, pylint: disable=W0212 418 obj = type.__call__(mcs, *args, **kwargs) 419 420 for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS: 421 if hasattr(obj, method): 422 # If the method handler is already defined, "*_RENAME" or "Get*OpInput" 423 # shouldn't be (they're only used by the automatically generated 424 # handler) 425 assert not hasattr(obj, rename_attr) 426 assert not hasattr(obj, fn_attr) 427 else: 428 # Try to generate handler method on handler instance 429 try: 430 opcode = getattr(obj, op_attr) 431 except AttributeError: 432 pass 433 else: 434 setattr(obj, method, 435 compat.partial(obj._GenericHandler, opcode, 436 getattr(obj, rename_attr, None), 437 getattr(obj, fn_attr, obj._GetDefaultData))) 438 439 return obj
440 441
442 -class OpcodeResource(ResourceBase):
443 """Base class for opcode-based RAPI resources. 444 445 Instances of this class automatically gain handler functions through 446 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable 447 is defined at class level. Subclasses can define a C{Get$Method$OpInput} 448 method to do their own opcode input processing (e.g. for static values). The 449 C{$METHOD$_RENAME} variable defines which values are renamed (see 450 L{baserlib.FillOpcode}). 451 452 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 453 automatically generate a GET handler submitting the opcode 454 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see 455 L{baserlib.FillOpcode}) 456 @ivar GetGetOpInput: Define this to override the default method for 457 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 458 459 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 460 automatically generate a PUT handler submitting the opcode 461 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see 462 L{baserlib.FillOpcode}) 463 @ivar GetPutOpInput: Define this to override the default method for 464 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 465 466 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 467 automatically generate a POST handler submitting the opcode 468 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see 469 L{baserlib.FillOpcode}) 470 @ivar GetPostOpInput: Define this to override the default method for 471 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 472 473 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 474 automatically generate a DELETE handler submitting the opcode 475 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see 476 L{baserlib.FillOpcode}) 477 @ivar GetDeleteOpInput: Define this to override the default method for 478 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 479 480 """ 481 __metaclass__ = _MetaOpcodeResource 482
483 - def _GetDefaultData(self):
484 return (self.request_body, None)
485
486 - def _GenericHandler(self, opcode, rename, fn):
487 (body, static) = fn() 488 op = FillOpcode(opcode, body, static, rename=rename) 489 return self.SubmitJob([op])
490