#
#
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# 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
"""
-# pylint: disable-msg=C0103
+# pylint: disable=C0103
# C0103: Invalid name, since the R_* names are not conforming
from ganeti import luxi
from ganeti import rapi
from ganeti import http
-from ganeti import ssconf
-from ganeti import constants
-from ganeti import opcodes
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.
(field_id, field_uri) = uri_fields
def _MapId(m_id):
- return { field_id: m_id, field_uri: uri_format % 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.
return map(_MapId, ids)
-def ExtractField(sequence, index):
- """Creates a list containing one column out of a list of lists.
-
- @param sequence: sequence of lists
- @param index: index of field
-
- """
- return map(lambda item: item[index], sequence)
-
-
def MapFields(names, data):
"""Maps two lists into one dictionary.
return dict(zip(names, data))
-def _Tags_GET(kind, name):
- """Helper function to retrieve tags.
-
- """
- if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
- if not name:
- raise http.HttpBadRequest("Missing name on tag request")
- cl = GetClient()
- if kind == constants.TAG_INSTANCE:
- fn = cl.QueryInstances
- else:
- fn = cl.QueryNodes
- result = fn(names=[name], fields=["tags"], use_locking=False)
- if not result or not result[0]:
- raise http.HttpBadGateway("Invalid response from tag query")
- tags = result[0][0]
- elif kind == constants.TAG_CLUSTER:
- ssc = ssconf.SimpleStore()
- tags = ssc.GetClusterTags()
-
- return list(tags)
-
-
-def _Tags_PUT(kind, tags, name, dry_run):
- """Helper function to set tags.
-
- """
- return SubmitJob([opcodes.OpAddTags(kind=kind, name=name,
- tags=tags, dry_run=dry_run)])
-
-
-def _Tags_DELETE(kind, tags, name, dry_run):
- """Helper function to delete tags.
-
- """
- return SubmitJob([opcodes.OpDelTags(kind=kind, name=name,
- tags=tags, dry_run=dry_run)])
-
-
def MapBulkFields(itemslist, fields):
"""Map value to field name in to one dictionary.
return items_details
-def MakeParamsDict(opts, params):
- """Makes params dictionary out of a option set.
-
- This function returns a dictionary needed for hv or be parameters. But only
- those fields which provided in the option set. Takes parameters frozensets
- from constants.
-
- @type opts: dict
- @param opts: selected options
- @type params: frozenset
- @param params: subset of options
- @rtype: dict
- @return: dictionary of options, filtered by given subset.
-
- """
- result = {}
-
- for p in params:
- try:
- value = opts[p]
- except KeyError:
- continue
- result[p] = value
-
- return result
-
-
-def FillOpcode(opcls, body, static):
+def FillOpcode(opcls, body, static, rename=None):
"""Fills an opcode with body parameters.
Parameter types are checked.
@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
"""
- CheckType(body, dict, "Body contents")
+ 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(body.keys()) & set(static.keys())
+ overwritten = set(params.keys()) & set(static.keys())
if overwritten:
raise http.HttpBadRequest("Can't overwrite static parameters %r" %
overwritten)
- # Combine parameters
- params = body.copy()
-
- if static:
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-msg=W0142
+ 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 SubmitJob(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
-
- """
- try:
- if cl is None:
- cl = GetClient()
- 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" % str(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. Error: %s" % str(err))
-
-
def HandleItemQueryErrors(fn, *args, **kwargs):
"""Converts errors when querying a single item.
raise
-def GetClient():
- """Geric wrapper for luxi.Client(), for better http compatiblity.
-
- """
- try:
- return luxi.Client()
- except luxi.NoMasterError, err:
- raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
- except luxi.PermissionError:
- raise http.HttpInternalServerError("Internal error: no permission to"
- " connect to the master daemon")
-
-
def FeedbackFn(msg):
"""Feedback logging function for jobs.
return CheckType(value, exptype, "'%s' parameter" % name)
-class R_Generic(object):
+class ResourceBase(object):
"""Generic class for resources.
"""
POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
- def __init__(self, items, queryargs, req):
+ 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 val
def _checkStringVariable(self, name, default=None):
- """Return the parsed value of an int argument.
+ """Return the parsed value of a string argument.
"""
val = self.queryargs.get(name, default)
"""
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])