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