blob: c396f98431a2e7cc2cdfe5f5c6f8302a191cb0ce [file] [log] [blame]
#
#
# Copyright (C) 2006, 2007, 2008, 2012 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""Remote API base resources library.
"""
# pylint: disable=C0103
# C0103: Invalid name, since the R_* names are not conforming
import logging
from ganeti import luxi
from ganeti import rapi
from ganeti import http
from ganeti import errors
from ganeti import compat
from ganeti import constants
from ganeti import pathutils
from ganeti import utils
# Dummy value to detect unchanged parameters
_DEFAULT = object()
#: Supported HTTP methods
_SUPPORTED_METHODS = compat.UniqueFrozenset([
http.HTTP_DELETE,
http.HTTP_GET,
http.HTTP_POST,
http.HTTP_PUT,
])
def _BuildOpcodeAttributes():
"""Builds list of attributes used for per-handler opcodes.
"""
return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
"Get%sOpInput" % method.capitalize())
for method in _SUPPORTED_METHODS]
OPCODE_ATTRS = _BuildOpcodeAttributes()
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
"""Builds a URI list as used by index resources.
@param ids: list of ids as strings
@param uri_format: format to be applied for URI
@param uri_fields: optional parameter for field IDs
"""
(field_id, field_uri) = uri_fields
def _MapId(m_id):
return {
field_id: m_id,
field_uri: uri_format % m_id,
}
# Make sure the result is sorted, makes it nicer to look at and simplifies
# unittests.
ids.sort()
return map(_MapId, ids)
def MapFields(names, data):
"""Maps two lists into one dictionary.
Example::
>>> MapFields(["a", "b"], ["foo", 123])
{'a': 'foo', 'b': 123}
@param names: field names (list of strings)
@param data: field data (list)
"""
if len(names) != len(data):
raise AttributeError("Names and data must have the same length")
return dict(zip(names, data))
def MapBulkFields(itemslist, fields):
"""Map value to field name in to one dictionary.
@param itemslist: a list of items values
@param fields: a list of items names
@return: a list of mapped dictionaries
"""
items_details = []
for item in itemslist:
mapped = MapFields(fields, item)
items_details.append(mapped)
return items_details
def FillOpcode(opcls, body, static, rename=None):
"""Fills an opcode with body parameters.
Parameter types are checked.
@type opcls: L{opcodes.OpCode}
@param opcls: Opcode class
@type body: dict
@param body: Body parameters as received from client
@type static: dict
@param static: Static parameters which can't be modified by client
@type rename: dict
@param rename: Renamed parameters, key as old name, value as new name
@return: Opcode object
"""
if body is None:
params = {}
else:
CheckType(body, dict, "Body contents")
# Make copy to be modified
params = body.copy()
if rename:
for old, new in rename.items():
if new in params and old in params:
raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
" both are specified" %
(old, new))
if old in params:
assert new not in params
params[new] = params.pop(old)
if static:
overwritten = set(params.keys()) & set(static.keys())
if overwritten:
raise http.HttpBadRequest("Can't overwrite static parameters %r" %
overwritten)
params.update(static)
# Convert keys to strings (simplejson decodes them as unicode)
params = dict((str(key), value) for (key, value) in params.items())
try:
op = opcls(**params) # pylint: disable=W0142
op.Validate(False)
except (errors.OpPrereqError, TypeError), err:
raise http.HttpBadRequest("Invalid body parameters: %s" % err)
return op
def HandleItemQueryErrors(fn, *args, **kwargs):
"""Converts errors when querying a single item.
"""
try:
return fn(*args, **kwargs)
except errors.OpPrereqError, err:
if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
raise http.HttpNotFound()
raise
def FeedbackFn(msg):
"""Feedback logging function for jobs.
We don't have a stdout for printing log messages, so log them to the
http log at least.
@param msg: the message
"""
(_, log_type, log_msg) = msg
logging.info("%s: %s", log_type, log_msg)
def CheckType(value, exptype, descr):
"""Abort request if value type doesn't match expected type.
@param value: Value
@type exptype: type
@param exptype: Expected type
@type descr: string
@param descr: Description of value
@return: Value (allows inline usage)
"""
if not isinstance(value, exptype):
raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
(descr, type(value).__name__, exptype.__name__))
return value
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
"""Check and return the value for a given parameter.
If no default value was given and the parameter doesn't exist in the input
data, an error is raise.
@type data: dict
@param data: Dictionary containing input data
@type name: string
@param name: Parameter name
@param default: Default value (can be None)
@param exptype: Expected type (can be None)
"""
try:
value = data[name]
except KeyError:
if default is not _DEFAULT:
return default
raise http.HttpBadRequest("Required parameter '%s' is missing" %
name)
if exptype is _DEFAULT:
return value
return CheckType(value, exptype, "'%s' parameter" % name)
class ResourceBase(object):
"""Generic class for resources.
"""
# Default permission requirements
GET_ACCESS = []
PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
def __init__(self, items, queryargs, req, _client_cls=None):
"""Generic resource constructor.
@param items: a list with variables encoded in the URL
@param queryargs: a dictionary with additional options from URL
@param req: Request context
@param _client_cls: L{luxi} client class (unittests only)
"""
assert isinstance(queryargs, dict)
self.items = items
self.queryargs = queryargs
self._req = req
if _client_cls is None:
_client_cls = luxi.Client
self._client_cls = _client_cls
def _GetRequestBody(self):
"""Returns the body data.
"""
return self._req.private.body_data
request_body = property(fget=_GetRequestBody)
def _checkIntVariable(self, name, default=0):
"""Return the parsed value of an int argument.
"""
val = self.queryargs.get(name, default)
if isinstance(val, list):
if val:
val = val[0]
else:
val = default
try:
val = int(val)
except (ValueError, TypeError):
raise http.HttpBadRequest("Invalid value for the"
" '%s' parameter" % (name,))
return val
def _checkStringVariable(self, name, default=None):
"""Return the parsed value of a string argument.
"""
val = self.queryargs.get(name, default)
if isinstance(val, list):
if val:
val = val[0]
else:
val = default
return val
def getBodyParameter(self, name, *args):
"""Check and return the value for a given parameter.
If a second parameter is not given, an error will be returned,
otherwise this parameter specifies the default value.
@param name: the required parameter
"""
if args:
return CheckParameter(self.request_body, name, default=args[0])
return CheckParameter(self.request_body, name)
def useLocking(self):
"""Check if the request specifies locking.
"""
return bool(self._checkIntVariable("lock"))
def useBulk(self):
"""Check if the request specifies bulk querying.
"""
return bool(self._checkIntVariable("bulk"))
def useForce(self):
"""Check if the request specifies a forced operation.
"""
return bool(self._checkIntVariable("force"))
def dryRun(self):
"""Check if the request specifies dry-run mode.
"""
return bool(self._checkIntVariable("dry-run"))
def GetClient(self, query=False):
"""Wrapper for L{luxi.Client} with HTTP-specific error handling.
@param query: this signifies that the client will only be used for
queries; if the build-time parameter enable-split-queries is
enabled, then the client will be connected to the query socket
instead of the masterd socket
"""
if query and constants.ENABLE_SPLIT_QUERY:
address = pathutils.QUERY_SOCKET
else:
address = None
# Could be a function, pylint: disable=R0201
try:
return self._client_cls(address=address)
except luxi.NoMasterError, err:
raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
except luxi.PermissionError:
raise http.HttpInternalServerError("Internal error: no permission to"
" connect to the master daemon")
def SubmitJob(self, op, cl=None):
"""Generic wrapper for submit job, for better http compatibility.
@type op: list
@param op: the list of opcodes for the job
@type cl: None or luxi.Client
@param cl: optional luxi client to use
@rtype: string
@return: the job ID
"""
if cl is None:
cl = self.GetClient()
try:
return cl.SubmitJob(op)
except errors.JobQueueFull:
raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
except errors.JobQueueDrainError:
raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
except luxi.NoMasterError, err:
raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
except luxi.PermissionError:
raise http.HttpInternalServerError("Internal error: no permission to"
" connect to the master daemon")
except luxi.TimeoutError, err:
raise http.HttpGatewayTimeout("Timeout while talking to the master"
" daemon: %s" % err)
def GetResourceOpcodes(cls):
"""Returns all opcodes used by a resource.
"""
return frozenset(filter(None, (getattr(cls, op_attr, None)
for (_, op_attr, _, _) in OPCODE_ATTRS)))
def GetHandlerAccess(handler, method):
"""Returns the access rights for a method on a handler.
@type handler: L{ResourceBase}
@type method: string
@rtype: string or None
"""
return getattr(handler, "%s_ACCESS" % method, None)
class _MetaOpcodeResource(type):
"""Meta class for RAPI resources.
"""
def __call__(mcs, *args, **kwargs):
"""Instantiates class and patches it for use by the RAPI daemon.
"""
# Access to private attributes of a client class, pylint: disable=W0212
obj = type.__call__(mcs, *args, **kwargs)
for (method, op_attr, rename_attr, fn_attr) in OPCODE_ATTRS:
if hasattr(obj, method):
# If the method handler is already defined, "*_RENAME" or "Get*OpInput"
# shouldn't be (they're only used by the automatically generated
# handler)
assert not hasattr(obj, rename_attr)
assert not hasattr(obj, fn_attr)
else:
# Try to generate handler method on handler instance
try:
opcode = getattr(obj, op_attr)
except AttributeError:
pass
else:
setattr(obj, method,
compat.partial(obj._GenericHandler, opcode,
getattr(obj, rename_attr, None),
getattr(obj, fn_attr, obj._GetDefaultData)))
return obj
class OpcodeResource(ResourceBase):
"""Base class for opcode-based RAPI resources.
Instances of this class automatically gain handler functions through
L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
is defined at class level. Subclasses can define a C{Get$Method$OpInput}
method to do their own opcode input processing (e.g. for static values). The
C{$METHOD$_RENAME} variable defines which values are renamed (see
L{baserlib.FillOpcode}).
@cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
automatically generate a GET handler submitting the opcode
@cvar GET_RENAME: Set this to rename parameters in the GET handler (see
L{baserlib.FillOpcode})
@ivar GetGetOpInput: Define this to override the default method for
getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
@cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
automatically generate a PUT handler submitting the opcode
@cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
L{baserlib.FillOpcode})
@ivar GetPutOpInput: Define this to override the default method for
getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
@cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
automatically generate a POST handler submitting the opcode
@cvar POST_RENAME: Set this to rename parameters in the POST handler (see
L{baserlib.FillOpcode})
@ivar GetPostOpInput: Define this to override the default method for
getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
@cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
automatically generate a DELETE handler submitting the opcode
@cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
L{baserlib.FillOpcode})
@ivar GetDeleteOpInput: Define this to override the default method for
getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
"""
__metaclass__ = _MetaOpcodeResource
def _GetDefaultData(self):
return (self.request_body, None)
def _GetRapiOpName(self):
"""Extracts the name of the RAPI operation from the class name
"""
if self.__class__.__name__.startswith("R_2_"):
return self.__class__.__name__[4:]
return self.__class__.__name__
def _GetCommonStatic(self):
"""Return the static parameters common to all the RAPI calls
The reason is a parameter present in all the RAPI calls, and the reason
trail has to be build for all of them, so the parameter is read here and
used to build the reason trail, that is the actual parameter passed
forward.
"""
trail = []
usr_reason = self._checkStringVariable("reason", default=None)
if usr_reason:
trail.append((constants.OPCODE_REASON_SRC_USER,
usr_reason,
utils.EpochNano()))
reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
self._GetRapiOpName())
trail.append((reason_src, "", utils.EpochNano()))
common_static = {
"reason": trail,
}
return common_static
def _GenericHandler(self, opcode, rename, fn):
(body, specific_static) = fn()
static = self._GetCommonStatic()
if specific_static:
static.update(specific_static)
op = FillOpcode(opcode, body, static, rename=rename)
return self.SubmitJob([op])