4 # Copyright (C) 2006, 2007, 2008, 2012 Google Inc.
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.
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.
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
22 """Remote API base resources library.
26 # pylint: disable=C0103
28 # C0103: Invalid name, since the R_* names are not conforming
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
42 # Dummy value to detect unchanged parameters
45 #: Supported HTTP methods
46 _SUPPORTED_METHODS = compat.UniqueFrozenset([
54 def _BuildOpcodeAttributes():
55 """Builds list of attributes used for per-handler opcodes.
58 return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
59 "Get%sOpInput" % method.capitalize())
60 for method in _SUPPORTED_METHODS]
63 OPCODE_ATTRS = _BuildOpcodeAttributes()
66 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
67 """Builds a URI list as used by index resources.
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
74 (field_id, field_uri) = uri_fields
79 field_uri: uri_format % m_id,
82 # Make sure the result is sorted, makes it nicer to look at and simplifies
86 return map(_MapId, ids)
89 def MapFields(names, data):
90 """Maps two lists into one dictionary.
93 >>> MapFields(["a", "b"], ["foo", 123])
94 {'a': 'foo', 'b': 123}
96 @param names: field names (list of strings)
97 @param data: field data (list)
100 if len(names) != len(data):
101 raise AttributeError("Names and data must have the same length")
102 return dict(zip(names, data))
105 def MapBulkFields(itemslist, fields):
106 """Map value to field name in to one dictionary.
108 @param itemslist: a list of items values
109 @param fields: a list of items names
111 @return: a list of mapped dictionaries
115 for item in itemslist:
116 mapped = MapFields(fields, item)
117 items_details.append(mapped)
121 def FillOpcode(opcls, body, static, rename=None):
122 """Fills an opcode with body parameters.
124 Parameter types are checked.
126 @type opcls: L{opcodes.OpCode}
127 @param opcls: Opcode class
129 @param body: Body parameters as received from client
131 @param static: Static parameters which can't be modified by client
133 @param rename: Renamed parameters, key as old name, value as new name
134 @return: Opcode object
140 CheckType(body, dict, "Body contents")
142 # Make copy to be modified
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" %
152 assert new not in params
153 params[new] = params.pop(old)
156 overwritten = set(params.keys()) & set(static.keys())
158 raise http.HttpBadRequest("Can't overwrite static parameters %r" %
161 params.update(static)
163 # Convert keys to strings (simplejson decodes them as unicode)
164 params = dict((str(key), value) for (key, value) in params.items())
167 op = opcls(**params) # pylint: disable=W0142
169 except (errors.OpPrereqError, TypeError), err:
170 raise http.HttpBadRequest("Invalid body parameters: %s" % err)
175 def HandleItemQueryErrors(fn, *args, **kwargs):
176 """Converts errors when querying a single item.
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()
189 """Feedback logging function for jobs.
191 We don't have a stdout for printing log messages, so log them to the
194 @param msg: the message
197 (_, log_type, log_msg) = msg
198 logging.info("%s: %s", log_type, log_msg)
201 def CheckType(value, exptype, descr):
202 """Abort request if value type doesn't match expected type.
206 @param exptype: Expected type
208 @param descr: Description of value
209 @return: Value (allows inline usage)
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__))
219 def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
220 """Check and return the value for a given parameter.
222 If no default value was given and the parameter doesn't exist in the input
223 data, an error is raise.
226 @param data: Dictionary containing input data
228 @param name: Parameter name
229 @param default: Default value (can be None)
230 @param exptype: Expected type (can be None)
236 if default is not _DEFAULT:
239 raise http.HttpBadRequest("Required parameter '%s' is missing" %
242 if exptype is _DEFAULT:
245 return CheckType(value, exptype, "'%s' parameter" % name)
248 class ResourceBase(object):
249 """Generic class for resources.
252 # Default permission requirements
254 PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
255 POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
256 DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
258 def __init__(self, items, queryargs, req, _client_cls=None):
259 """Generic resource constructor.
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)
267 assert isinstance(queryargs, dict)
270 self.queryargs = queryargs
273 if _client_cls is None:
274 _client_cls = luxi.Client
276 self._client_cls = _client_cls
278 def _GetRequestBody(self):
279 """Returns the body data.
282 return self._req.private.body_data
284 request_body = property(fget=_GetRequestBody)
286 def _checkIntVariable(self, name, default=0):
287 """Return the parsed value of an int argument.
290 val = self.queryargs.get(name, default)
291 if isinstance(val, list):
298 except (ValueError, TypeError):
299 raise http.HttpBadRequest("Invalid value for the"
300 " '%s' parameter" % (name,))
303 def _checkStringVariable(self, name, default=None):
304 """Return the parsed value of a string argument.
307 val = self.queryargs.get(name, default)
308 if isinstance(val, list):
315 def getBodyParameter(self, name, *args):
316 """Check and return the value for a given parameter.
318 If a second parameter is not given, an error will be returned,
319 otherwise this parameter specifies the default value.
321 @param name: the required parameter
325 return CheckParameter(self.request_body, name, default=args[0])
327 return CheckParameter(self.request_body, name)
329 def useLocking(self):
330 """Check if the request specifies locking.
333 return bool(self._checkIntVariable("lock"))
336 """Check if the request specifies bulk querying.
339 return bool(self._checkIntVariable("bulk"))
342 """Check if the request specifies a forced operation.
345 return bool(self._checkIntVariable("force"))
348 """Check if the request specifies dry-run mode.
351 return bool(self._checkIntVariable("dry-run"))
353 def GetClient(self, query=False):
354 """Wrapper for L{luxi.Client} with HTTP-specific error handling.
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
362 if query and constants.ENABLE_SPLIT_QUERY:
363 address = pathutils.QUERY_SOCKET
366 # Could be a function, pylint: disable=R0201
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")
375 def SubmitJob(self, op, cl=None):
376 """Generic wrapper for submit job, for better http compatibility.
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
387 cl = self.GetClient()
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"
404 def GetResourceOpcodes(cls):
405 """Returns all opcodes used by a resource.
408 return frozenset(filter(None, (getattr(cls, op_attr, None)
409 for (_, op_attr, _, _) in OPCODE_ATTRS)))
412 def GetHandlerAccess(handler, method):
413 """Returns the access rights for a method on a handler.
415 @type handler: L{ResourceBase}
417 @rtype: string or None
420 return getattr(handler, "%s_ACCESS" % method, None)
423 class _MetaOpcodeResource(type):
424 """Meta class for RAPI resources.
427 def __call__(mcs, *args, **kwargs):
428 """Instantiates class and patches it for use by the RAPI daemon.
431 # Access to private attributes of a client class, pylint: disable=W0212
432 obj = type.__call__(mcs, *args, **kwargs)
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
439 assert not hasattr(obj, rename_attr)
440 assert not hasattr(obj, fn_attr)
442 # Try to generate handler method on handler instance
444 opcode = getattr(obj, op_attr)
445 except AttributeError:
449 compat.partial(obj._GenericHandler, opcode,
450 getattr(obj, rename_attr, None),
451 getattr(obj, fn_attr, obj._GetDefaultData)))
456 class OpcodeResource(ResourceBase):
457 """Base class for opcode-based RAPI resources.
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}).
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})
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})
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})
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})
495 __metaclass__ = _MetaOpcodeResource
497 def _GetDefaultData(self):
498 return (self.request_body, None)
500 def _GetRapiOpName(self):
501 """Extracts the name of the RAPI operation from the class name
504 if self.__class__.__name__.startswith("R_2_"):
505 return self.__class__.__name__[4:]
506 return self.__class__.__name__
508 def _GetCommonStatic(self):
509 """Return the static parameters common to all the RAPI calls
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
518 usr_reason = self._checkStringVariable("reason", default=None)
520 trail.append((constants.OPCODE_REASON_SRC_USER,
523 reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
524 self._GetRapiOpName())
525 trail.append((reason_src, "", utils.EpochNano()))
531 def _GenericHandler(self, opcode, rename, fn):
532 (body, specific_static) = fn()
533 static = self._GetCommonStatic()
535 static.update(specific_static)
536 op = FillOpcode(opcode, body, static, rename=rename)
537 return self.SubmitJob([op])