| # |
| # |
| |
| # Copyright (C) 2006, 2007, 2008, 2012 Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, |
| # this list of conditions and the following disclaimer. |
| # |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| """Remote API base resources library. |
| |
| """ |
| |
| # pylint: disable=C0103 |
| |
| # C0103: Invalid name, since the R_* names are not conforming |
| |
| import logging |
| |
| from ganeti import luxi |
| import ganeti.rpc.errors as rpcerr |
| from ganeti import rapi |
| from ganeti import http |
| from ganeti import errors |
| from ganeti import compat |
| from ganeti import constants |
| 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, |
| ]) |
| |
| |
| class OpcodeAttributes(object): |
| """Acts as a structure containing the per-method attribute names. |
| |
| """ |
| __slots__ = [ |
| "method", |
| "opcode", |
| "rename", |
| "aliases", |
| "forbidden", |
| "get_input", |
| ] |
| |
| def __init__(self, method_name): |
| """Initializes the opcode attributes for the given method name. |
| |
| """ |
| self.method = method_name |
| self.opcode = "%s_OPCODE" % method_name |
| self.rename = "%s_RENAME" % method_name |
| self.aliases = "%s_ALIASES" % method_name |
| self.forbidden = "%s_FORBIDDEN" % method_name |
| self.get_input = "Get%sOpInput" % method_name.capitalize() |
| |
| def GetModifiers(self): |
| """Returns the names of all the attributes that replace or modify a method. |
| |
| """ |
| return [self.opcode, self.rename, self.aliases, self.forbidden, |
| self.get_input] |
| |
| def GetAll(self): |
| return [self.method] + self.GetModifiers() |
| |
| |
| def _BuildOpcodeAttributes(): |
| """Builds list of attributes used for per-handler opcodes. |
| |
| """ |
| return [OpcodeAttributes(method) 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: |
| result = fn(*args, **kwargs) |
| except errors.OpPrereqError, err: |
| if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT: |
| raise http.HttpNotFound() |
| |
| raise |
| |
| # In case split query mechanism is used |
| if not result: |
| raise http.HttpNotFound() |
| |
| return result |
| |
| |
| 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): |
| """Wrapper for L{luxi.Client} with HTTP-specific error handling. |
| |
| """ |
| # Could be a function, pylint: disable=R0201 |
| try: |
| return self._client_cls() |
| except rpcerr.NoMasterError, err: |
| raise http.HttpBadGateway("Can't connect to master daemon: %s" % err) |
| except rpcerr.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 rpcerr.NoMasterError, err: |
| raise http.HttpBadGateway("Master seems to be unreachable: %s" % err) |
| except rpcerr.PermissionError: |
| raise http.HttpInternalServerError("Internal error: no permission to" |
| " connect to the master daemon") |
| except rpcerr.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, method_attrs.opcode, None) |
| for method_attrs 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) |
| |
| |
| def GetHandler(get_fn, aliases): |
| result = get_fn() |
| if not isinstance(result, dict) or aliases is None: |
| return result |
| |
| for (param, alias) in aliases.items(): |
| if param in result: |
| if alias in result: |
| raise http.HttpBadRequest("Parameter '%s' has an alias of '%s', but" |
| " both values are present in response" % |
| (param, alias)) |
| result[alias] = result[param] |
| |
| return result |
| |
| |
| # Constant used to denote that a parameter cannot be set |
| ALL_VALUES_FORBIDDEN = "all_values_forbidden" |
| |
| |
| def ProduceForbiddenParamDict(class_name, method_name, param_list): |
| """Turns a list of parameter names and possibly values into a dictionary. |
| |
| @type class_name: string |
| @param class_name: The name of the handler class |
| @type method_name: string |
| @param method_name: The name of the HTTP method |
| @type param_list: list of string or tuple of (string, list of any) |
| @param param_list: A list of forbidden parameters, specified in the RAPI |
| handler class |
| |
| @return: The dictionary of forbidden param names to values or |
| ALL_VALUES_FORBIDDEN |
| |
| """ |
| # A simple error-raising function |
| def _RaiseError(message): |
| raise errors.ProgrammerError( |
| "While examining the %s_FORBIDDEN field of class %s: %s" % |
| (method_name, class_name, message) |
| ) |
| |
| param_dict = {} |
| for value in param_list: |
| if isinstance(value, basestring): |
| param_dict[value] = ALL_VALUES_FORBIDDEN |
| elif isinstance(value, tuple): |
| if len(value) != 2: |
| _RaiseError("Tuples of only length 2 allowed") |
| param_name, forbidden_values = value |
| param_dict[param_name] = forbidden_values |
| else: |
| _RaiseError("Only strings or tuples allowed, found %s" % value) |
| |
| return param_dict |
| |
| |
| def InspectParams(params_dict, forbidden_params, rename_dict): |
| """Inspects a dictionary of params, looking for forbidden values. |
| |
| @type params_dict: dict of string to anything |
| @param params_dict: A dictionary of supplied parameters |
| @type forbidden_params: dict of string to string or list of any |
| @param forbidden_params: The forbidden parameters, with a list of forbidden |
| values or the constant ALL_VALUES_FORBIDDEN |
| signifying that all values are forbidden |
| @type rename_dict: None or dict of string to string |
| @param rename_dict: The list of parameter renamings used by the method |
| |
| @raise http.HttpForbidden: If a forbidden param has been set |
| |
| """ |
| for param in params_dict: |
| # Check for possible renames to ensure nothing slips through |
| if rename_dict is not None and param in rename_dict: |
| param = rename_dict[param] |
| |
| # Now see if there are restrictions on this parameter |
| if param in forbidden_params: |
| forbidden_values = forbidden_params[param] |
| if forbidden_values == ALL_VALUES_FORBIDDEN: |
| raise http.HttpForbidden("The parameter %s cannot be set via RAPI" % |
| param) |
| |
| param_value = params_dict[param] |
| if param_value in forbidden_values: |
| raise http.HttpForbidden("The parameter %s cannot be set to the value" |
| " %s via RAPI" % (param, param_value)) |
| |
| |
| 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 m_attrs in OPCODE_ATTRS: |
| method, op_attr, rename_attr, aliases_attr, _, fn_attr = m_attrs.GetAll() |
| 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) |
| |
| # The aliases are allowed only on GET calls |
| assert not hasattr(obj, aliases_attr) or method == http.HTTP_GET |
| |
| # GET methods can add aliases of values they return under a different |
| # name |
| if method == http.HTTP_GET and hasattr(obj, aliases_attr): |
| setattr(obj, method, |
| compat.partial(GetHandler, getattr(obj, method), |
| getattr(obj, aliases_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))) |
| |
| # Finally, the method (generated or not) should be wrapped to handle |
| # forbidden values |
| if hasattr(obj, m_attrs.forbidden): |
| forbidden_dict = ProduceForbiddenParamDict( |
| obj.__class__.__name__, method, getattr(obj, m_attrs.forbidden) |
| ) |
| setattr( |
| obj, method, compat.partial(obj._ForbiddenHandler, |
| getattr(obj, method), |
| forbidden_dict, |
| getattr(obj, m_attrs.rename, None)) |
| ) |
| |
| 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}). |
| Still default behavior cannot be totally overriden. There are opcode params |
| that are available to all opcodes, e.g. "depends". In case those params |
| (currently only "depends") are found in the original request's body, they are |
| added to the dictionary of parsed parameters and eventually passed to the |
| opcode. If the parsed body is not represented as a dictionary object, the |
| values are not added. |
| |
| @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}) |
| @cvar GET_FORBIDDEN: Set this to disable listed parameters and optionally |
| specific values from being set through the GET handler (see |
| L{baserlib.InspectParams}) |
| @cvar GET_ALIASES: Set this to duplicate return values in GET results (see |
| L{baserlib.GetHandler}) |
| @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}) |
| @cvar PUT_FORBIDDEN: Set this to disable listed parameters and optionally |
| specific values from being set through the PUT handler (see |
| L{baserlib.InspectParams}) |
| @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}) |
| @cvar POST_FORBIDDEN: Set this to disable listed parameters and optionally |
| specific values from being set through the POST handler (see |
| L{baserlib.InspectParams}) |
| @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}) |
| @cvar DELETE_FORBIDDEN: Set this to disable listed parameters and optionally |
| specific values from being set through the DELETE handler (see |
| L{baserlib.InspectParams}) |
| @ivar GetDeleteOpInput: Define this to override the default method for |
| getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData}) |
| |
| """ |
| __metaclass__ = _MetaOpcodeResource |
| |
| def _ForbiddenHandler(self, method_fn, forbidden_params, rename_dict): |
| """Examines provided parameters for forbidden values. |
| |
| """ |
| InspectParams(self.queryargs, forbidden_params, rename_dict) |
| InspectParams(self.request_body, forbidden_params, rename_dict) |
| return method_fn() |
| |
| 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 _GetDepends(self): |
| ret = {} |
| if isinstance(self.request_body, dict): |
| depends = self.getBodyParameter("depends", None) |
| if depends: |
| ret.update({"depends": depends}) |
| return ret |
| |
| def _GenericHandler(self, opcode, rename, fn): |
| (body, specific_static) = fn() |
| if isinstance(body, dict): |
| body.update(self._GetDepends()) |
| static = self._GetCommonStatic() |
| if specific_static: |
| static.update(specific_static) |
| op = FillOpcode(opcode, body, static, rename=rename) |
| return self.SubmitJob([op]) |