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  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Remote API base resources library. 
 32   
 33  """ 
 34   
 35  # pylint: disable=C0103 
 36   
 37  # C0103: Invalid name, since the R_* names are not conforming 
 38   
 39  import logging 
 40   
 41  from ganeti import luxi 
 42  from ganeti import rapi 
 43  from ganeti import http 
 44  from ganeti import errors 
 45  from ganeti import compat 
 46  from ganeti import constants 
 47  from ganeti import pathutils 
 48  from ganeti import utils 
 49   
 50   
 51  # Dummy value to detect unchanged parameters 
 52  _DEFAULT = object() 
 53   
 54  #: Supported HTTP methods 
 55  _SUPPORTED_METHODS = compat.UniqueFrozenset([ 
 56    http.HTTP_DELETE, 
 57    http.HTTP_GET, 
 58    http.HTTP_POST, 
 59    http.HTTP_PUT, 
 60    ]) 
 61   
 62   
63 -def _BuildOpcodeAttributes():
64 """Builds list of attributes used for per-handler opcodes. 65 66 """ 67 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method, 68 "%s_ALIASES" % method, "Get%sOpInput" % method.capitalize()) 69 for method in _SUPPORTED_METHODS]
70 71 72 OPCODE_ATTRS = _BuildOpcodeAttributes() 73 74
75 -def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
76 """Builds a URI list as used by index resources. 77 78 @param ids: list of ids as strings 79 @param uri_format: format to be applied for URI 80 @param uri_fields: optional parameter for field IDs 81 82 """ 83 (field_id, field_uri) = uri_fields 84 85 def _MapId(m_id): 86 return { 87 field_id: m_id, 88 field_uri: uri_format % m_id, 89 }
90 91 # Make sure the result is sorted, makes it nicer to look at and simplifies 92 # unittests. 93 ids.sort() 94 95 return map(_MapId, ids) 96 97
98 -def MapFields(names, data):
99 """Maps two lists into one dictionary. 100 101 Example:: 102 >>> MapFields(["a", "b"], ["foo", 123]) 103 {'a': 'foo', 'b': 123} 104 105 @param names: field names (list of strings) 106 @param data: field data (list) 107 108 """ 109 if len(names) != len(data): 110 raise AttributeError("Names and data must have the same length") 111 return dict(zip(names, data))
112 113
114 -def MapBulkFields(itemslist, fields):
115 """Map value to field name in to one dictionary. 116 117 @param itemslist: a list of items values 118 @param fields: a list of items names 119 120 @return: a list of mapped dictionaries 121 122 """ 123 items_details = [] 124 for item in itemslist: 125 mapped = MapFields(fields, item) 126 items_details.append(mapped) 127 return items_details
128 129
130 -def FillOpcode(opcls, body, static, rename=None):
131 """Fills an opcode with body parameters. 132 133 Parameter types are checked. 134 135 @type opcls: L{opcodes.OpCode} 136 @param opcls: Opcode class 137 @type body: dict 138 @param body: Body parameters as received from client 139 @type static: dict 140 @param static: Static parameters which can't be modified by client 141 @type rename: dict 142 @param rename: Renamed parameters, key as old name, value as new name 143 @return: Opcode object 144 145 """ 146 if body is None: 147 params = {} 148 else: 149 CheckType(body, dict, "Body contents") 150 151 # Make copy to be modified 152 params = body.copy() 153 154 if rename: 155 for old, new in rename.items(): 156 if new in params and old in params: 157 raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but" 158 " both are specified" % 159 (old, new)) 160 if old in params: 161 assert new not in params 162 params[new] = params.pop(old) 163 164 if static: 165 overwritten = set(params.keys()) & set(static.keys()) 166 if overwritten: 167 raise http.HttpBadRequest("Can't overwrite static parameters %r" % 168 overwritten) 169 170 params.update(static) 171 172 # Convert keys to strings (simplejson decodes them as unicode) 173 params = dict((str(key), value) for (key, value) in params.items()) 174 175 try: 176 op = opcls(**params) # pylint: disable=W0142 177 op.Validate(False) 178 except (errors.OpPrereqError, TypeError), err: 179 raise http.HttpBadRequest("Invalid body parameters: %s" % err) 180 181 return op
182 183
184 -def HandleItemQueryErrors(fn, *args, **kwargs):
185 """Converts errors when querying a single item. 186 187 """ 188 try: 189 result = fn(*args, **kwargs) 190 except errors.OpPrereqError, err: 191 if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT: 192 raise http.HttpNotFound() 193 194 raise 195 196 # In case split query mechanism is used 197 if not result: 198 raise http.HttpNotFound() 199 200 return result
201 202
203 -def FeedbackFn(msg):
204 """Feedback logging function for jobs. 205 206 We don't have a stdout for printing log messages, so log them to the 207 http log at least. 208 209 @param msg: the message 210 211 """ 212 (_, log_type, log_msg) = msg 213 logging.info("%s: %s", log_type, log_msg)
214 215
216 -def CheckType(value, exptype, descr):
217 """Abort request if value type doesn't match expected type. 218 219 @param value: Value 220 @type exptype: type 221 @param exptype: Expected type 222 @type descr: string 223 @param descr: Description of value 224 @return: Value (allows inline usage) 225 226 """ 227 if not isinstance(value, exptype): 228 raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" % 229 (descr, type(value).__name__, exptype.__name__)) 230 231 return value
232 233
234 -def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
235 """Check and return the value for a given parameter. 236 237 If no default value was given and the parameter doesn't exist in the input 238 data, an error is raise. 239 240 @type data: dict 241 @param data: Dictionary containing input data 242 @type name: string 243 @param name: Parameter name 244 @param default: Default value (can be None) 245 @param exptype: Expected type (can be None) 246 247 """ 248 try: 249 value = data[name] 250 except KeyError: 251 if default is not _DEFAULT: 252 return default 253 254 raise http.HttpBadRequest("Required parameter '%s' is missing" % 255 name) 256 257 if exptype is _DEFAULT: 258 return value 259 260 return CheckType(value, exptype, "'%s' parameter" % name)
261 262
263 -class ResourceBase(object):
264 """Generic class for resources. 265 266 """ 267 # Default permission requirements 268 GET_ACCESS = [] 269 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE] 270 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE] 271 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE] 272
273 - def __init__(self, items, queryargs, req, _client_cls=None):
274 """Generic resource constructor. 275 276 @param items: a list with variables encoded in the URL 277 @param queryargs: a dictionary with additional options from URL 278 @param req: Request context 279 @param _client_cls: L{luxi} client class (unittests only) 280 281 """ 282 assert isinstance(queryargs, dict) 283 284 self.items = items 285 self.queryargs = queryargs 286 self._req = req 287 288 if _client_cls is None: 289 _client_cls = luxi.Client 290 291 self._client_cls = _client_cls
292
293 - def _GetRequestBody(self):
294 """Returns the body data. 295 296 """ 297 return self._req.private.body_data
298 299 request_body = property(fget=_GetRequestBody) 300
301 - def _checkIntVariable(self, name, default=0):
302 """Return the parsed value of an int argument. 303 304 """ 305 val = self.queryargs.get(name, default) 306 if isinstance(val, list): 307 if val: 308 val = val[0] 309 else: 310 val = default 311 try: 312 val = int(val) 313 except (ValueError, TypeError): 314 raise http.HttpBadRequest("Invalid value for the" 315 " '%s' parameter" % (name,)) 316 return val
317
318 - def _checkStringVariable(self, name, default=None):
319 """Return the parsed value of a string argument. 320 321 """ 322 val = self.queryargs.get(name, default) 323 if isinstance(val, list): 324 if val: 325 val = val[0] 326 else: 327 val = default 328 return val
329
330 - def getBodyParameter(self, name, *args):
331 """Check and return the value for a given parameter. 332 333 If a second parameter is not given, an error will be returned, 334 otherwise this parameter specifies the default value. 335 336 @param name: the required parameter 337 338 """ 339 if args: 340 return CheckParameter(self.request_body, name, default=args[0]) 341 342 return CheckParameter(self.request_body, name)
343
344 - def useLocking(self):
345 """Check if the request specifies locking. 346 347 """ 348 return bool(self._checkIntVariable("lock"))
349
350 - def useBulk(self):
351 """Check if the request specifies bulk querying. 352 353 """ 354 return bool(self._checkIntVariable("bulk"))
355
356 - def useForce(self):
357 """Check if the request specifies a forced operation. 358 359 """ 360 return bool(self._checkIntVariable("force"))
361
362 - def dryRun(self):
363 """Check if the request specifies dry-run mode. 364 365 """ 366 return bool(self._checkIntVariable("dry-run"))
367
368 - def GetClient(self, query=False):
369 """Wrapper for L{luxi.Client} with HTTP-specific error handling. 370 371 @param query: this signifies that the client will only be used for 372 queries; if the build-time parameter enable-split-queries is 373 enabled, then the client will be connected to the query socket 374 instead of the masterd socket 375 376 """ 377 if query and constants.ENABLE_SPLIT_QUERY: 378 address = pathutils.QUERY_SOCKET 379 else: 380 address = None 381 # Could be a function, pylint: disable=R0201 382 try: 383 return self._client_cls(address=address) 384 except luxi.NoMasterError, err: 385 raise http.HttpBadGateway("Can't connect to master daemon: %s" % err) 386 except luxi.PermissionError: 387 raise http.HttpInternalServerError("Internal error: no permission to" 388 " connect to the master daemon")
389
390 - def SubmitJob(self, op, cl=None):
391 """Generic wrapper for submit job, for better http compatibility. 392 393 @type op: list 394 @param op: the list of opcodes for the job 395 @type cl: None or luxi.Client 396 @param cl: optional luxi client to use 397 @rtype: string 398 @return: the job ID 399 400 """ 401 if cl is None: 402 cl = self.GetClient() 403 try: 404 return cl.SubmitJob(op) 405 except errors.JobQueueFull: 406 raise http.HttpServiceUnavailable("Job queue is full, needs archiving") 407 except errors.JobQueueDrainError: 408 raise http.HttpServiceUnavailable("Job queue is drained, cannot submit") 409 except luxi.NoMasterError, err: 410 raise http.HttpBadGateway("Master seems to be unreachable: %s" % err) 411 except luxi.PermissionError: 412 raise http.HttpInternalServerError("Internal error: no permission to" 413 " connect to the master daemon") 414 except luxi.TimeoutError, err: 415 raise http.HttpGatewayTimeout("Timeout while talking to the master" 416 " daemon: %s" % err)
417 418
419 -def GetResourceOpcodes(cls):
420 """Returns all opcodes used by a resource. 421 422 """ 423 return frozenset(filter(None, (getattr(cls, op_attr, None) 424 for (_, op_attr, _, _, _) in OPCODE_ATTRS)))
425 426
427 -def GetHandlerAccess(handler, method):
428 """Returns the access rights for a method on a handler. 429 430 @type handler: L{ResourceBase} 431 @type method: string 432 @rtype: string or None 433 434 """ 435 return getattr(handler, "%s_ACCESS" % method, None)
436 437
438 -def GetHandler(get_fn, aliases):
439 result = get_fn() 440 if not isinstance(result, dict) or aliases is None: 441 return result 442 443 for (param, alias) in aliases.items(): 444 if param in result: 445 if alias in result: 446 raise http.HttpBadRequest("Parameter '%s' has an alias of '%s', but" 447 " both values are present in response" % 448 (param, alias)) 449 result[alias] = result[param] 450 451 return result
452 453
454 -class _MetaOpcodeResource(type):
455 """Meta class for RAPI resources. 456 457 """
458 - def __call__(mcs, *args, **kwargs):
459 """Instantiates class and patches it for use by the RAPI daemon. 460 461 """ 462 # Access to private attributes of a client class, pylint: disable=W0212 463 obj = type.__call__(mcs, *args, **kwargs) 464 465 for (method, op_attr, rename_attr, aliases_attr, fn_attr) in OPCODE_ATTRS: 466 if hasattr(obj, method): 467 # If the method handler is already defined, "*_RENAME" or 468 # "Get*OpInput" shouldn't be (they're only used by the automatically 469 # generated handler) 470 assert not hasattr(obj, rename_attr) 471 assert not hasattr(obj, fn_attr) 472 473 # The aliases are allowed only on GET calls 474 assert not hasattr(obj, aliases_attr) or method == http.HTTP_GET 475 476 # GET methods can add aliases of values they return under a different 477 # name 478 if method == http.HTTP_GET and hasattr(obj, aliases_attr): 479 setattr(obj, method, 480 compat.partial(GetHandler, getattr(obj, method), 481 getattr(obj, aliases_attr))) 482 else: 483 # Try to generate handler method on handler instance 484 try: 485 opcode = getattr(obj, op_attr) 486 except AttributeError: 487 pass 488 else: 489 setattr(obj, method, 490 compat.partial(obj._GenericHandler, opcode, 491 getattr(obj, rename_attr, None), 492 getattr(obj, fn_attr, obj._GetDefaultData))) 493 494 return obj
495 496
497 -class OpcodeResource(ResourceBase):
498 """Base class for opcode-based RAPI resources. 499 500 Instances of this class automatically gain handler functions through 501 L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable 502 is defined at class level. Subclasses can define a C{Get$Method$OpInput} 503 method to do their own opcode input processing (e.g. for static values). The 504 C{$METHOD$_RENAME} variable defines which values are renamed (see 505 L{baserlib.FillOpcode}). 506 Still default behavior cannot be totally overriden. There are opcode params 507 that are available to all opcodes, e.g. "depends". In case those params 508 (currently only "depends") are found in the original request's body, they are 509 added to the dictionary of parsed parameters and eventually passed to the 510 opcode. If the parsed body is not represented as a dictionary object, the 511 values are not added. 512 513 @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 514 automatically generate a GET handler submitting the opcode 515 @cvar GET_RENAME: Set this to rename parameters in the GET handler (see 516 L{baserlib.FillOpcode}) 517 @cvar GET_ALIASES: Set this to duplicate return values in GET results (see 518 L{baserlib.GetHandler}) 519 @ivar GetGetOpInput: Define this to override the default method for 520 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 521 522 @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 523 automatically generate a PUT handler submitting the opcode 524 @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see 525 L{baserlib.FillOpcode}) 526 @ivar GetPutOpInput: Define this to override the default method for 527 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 528 529 @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 530 automatically generate a POST handler submitting the opcode 531 @cvar POST_RENAME: Set this to rename parameters in the POST handler (see 532 L{baserlib.FillOpcode}) 533 @ivar GetPostOpInput: Define this to override the default method for 534 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 535 536 @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to 537 automatically generate a DELETE handler submitting the opcode 538 @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see 539 L{baserlib.FillOpcode}) 540 @ivar GetDeleteOpInput: Define this to override the default method for 541 getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) 542 543 """ 544 __metaclass__ = _MetaOpcodeResource 545
546 - def _GetDefaultData(self):
547 return (self.request_body, None)
548
549 - def _GetRapiOpName(self):
550 """Extracts the name of the RAPI operation from the class name 551 552 """ 553 if self.__class__.__name__.startswith("R_2_"): 554 return self.__class__.__name__[4:] 555 return self.__class__.__name__
556
557 - def _GetCommonStatic(self):
558 """Return the static parameters common to all the RAPI calls 559 560 The reason is a parameter present in all the RAPI calls, and the reason 561 trail has to be build for all of them, so the parameter is read here and 562 used to build the reason trail, that is the actual parameter passed 563 forward. 564 565 """ 566 trail = [] 567 usr_reason = self._checkStringVariable("reason", default=None) 568 if usr_reason: 569 trail.append((constants.OPCODE_REASON_SRC_USER, 570 usr_reason, 571 utils.EpochNano())) 572 reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2, 573 self._GetRapiOpName()) 574 trail.append((reason_src, "", utils.EpochNano())) 575 common_static = { 576 "reason": trail, 577 } 578 return common_static
579
580 - def _GetDepends(self):
581 ret = {} 582 if isinstance(self.request_body, dict): 583 depends = self.getBodyParameter("depends", None) 584 if depends: 585 ret.update({"depends": depends}) 586 return ret
587
588 - def _GenericHandler(self, opcode, rename, fn):
589 (body, specific_static) = fn() 590 if isinstance(body, dict): 591 body.update(self._GetDepends()) 592 static = self._GetCommonStatic() 593 if specific_static: 594 static.update(specific_static) 595 op = FillOpcode(opcode, body, static, rename=rename) 596 return self.SubmitJob([op])
597