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