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