From: John Mulligan Date: Sat, 9 Apr 2022 19:19:37 +0000 (-0400) Subject: pybind/mgr: add a Responder decorator type X-Git-Tag: v17.2.1~48^2~10 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=228cad03fafc71e61be6d8b2dae3d4d056909cd4;p=ceph.git pybind/mgr: add a Responder decorator type The Responder is the decorator that future endpoint functions in the mgr can use to automatically handle conversions of returned types to serilaized data (JSON, YAML, etc) as well as automatically convert exceptions into error responses. The Responder makes use of format and return-value adapter types, previously added to the module, to convert a returned value into a mgr response. This change adds some exception types to return error responses to the clients. Simple customizations can be done by passing an alternate format adapter type when the Responder is being constructed. Additional customization can be done by subclassing the Responder. Signed-off-by: John Mulligan (cherry picked from commit 6e234b190841204d076e1b622db543674d0c4b57) --- diff --git a/src/pybind/mgr/object_format.py b/src/pybind/mgr/object_format.py index 0d2dbb439593e..fb89e9034ee9b 100644 --- a/src/pybind/mgr/object_format.py +++ b/src/pybind/mgr/object_format.py @@ -2,15 +2,21 @@ # requested output formats such as JSON, YAML, etc. import enum +import errno import json import sys +from functools import wraps from typing import ( Any, + Callable, Dict, Iterable, + List, Optional, TYPE_CHECKING, + Tuple, + Union, ) import yaml @@ -27,6 +33,8 @@ else: class Protocol: # type: ignore pass +from mgr_module import HandlerFuncType + DEFAULT_JSON_INDENT: int = 2 @@ -45,6 +53,12 @@ class Format(str, enum.Enum): SimpleData = Any +ObjectResponseFuncType = Union[ + Callable[..., Dict[Any, Any]], + Callable[..., List[Any]], +] + + class SimpleDataProvider(Protocol): def to_simplified(self) -> SimpleData: """Return a simplified representation of the current object. @@ -219,3 +233,115 @@ class ReturnValueAdapter: if _is_return_value_provider(self.obj): return int(self.obj.mgr_return_value()) return self.default_return_value + + +class ErrorResponseBase(Exception): + """An exception that can directly be converted to a mgr reponse.""" + + def format_response(self) -> Tuple[int, str, str]: + raise NotImplementedError() + + +class UnknownFormat(ErrorResponseBase): + """Raised if the format name is unexpected. + This can help distinguish typos from formats that are known but + not implemented. + """ + + def __init__(self, format_name: str) -> None: + self.format_name = format_name + + def format_response(self) -> Tuple[int, str, str]: + return -errno.EINVAL, "", f"Unknown format name: {self.format_name}" + + +class UnsupportedFormat(ErrorResponseBase): + """Raised if the format name does not correspond to any valid + conversion functions. + """ + + def __init__(self, format_name: str) -> None: + self.format_name = format_name + + def format_response(self) -> Tuple[int, str, str]: + return -errno.EINVAL, "", f"Unsupported format: {self.format_name}" + + +def _get_requested_format(f: ObjectResponseFuncType, kw: Dict[str, Any]) -> str: + # todo: leave 'format' in kw dict iff its part of f's signature + return kw.pop("format", None) + + +class Responder: + """A decorator type intended to assist in converting Python return types + into valid responses for the Ceph MGR. + + A function that returns a Python object will have the object converted into + a return value and formatted response body, based on the `format` argument + passed to the mgr. + + The Responder object is callable and is expected to be used as a decorator. + """ + + def __init__( + self, formatter: Optional[Callable[..., CommonFormatter]] = None + ) -> None: + self.formatter = formatter + self.default_format = "json" + + def _formatter(self, obj: Any) -> CommonFormatter: + """Return the formatter/format-adapter for the object.""" + if self.formatter is not None: + return self.formatter(obj) + return ObjectFormatAdapter(obj) + + def _retval_provider(self, obj: Any) -> ReturnValueProvider: + """Return a ReturnValueProvider for the given object.""" + return ReturnValueAdapter(obj) + + def _get_format_func( + self, obj: Any, format_req: Optional[str] = None + ) -> Callable: + formatter = self._formatter(obj) + if format_req is None: + format_req = self.default_format + if format_req not in formatter.valid_formats(): + raise UnknownFormat(format_req) + req = str(format_req).replace("-", "_") + ffunc = getattr(formatter, f"format_{req}", None) + if ffunc is None: + raise UnsupportedFormat(format_req) + return ffunc + + def _dry_run(self, format_req: Optional[str] = None) -> None: + """Raise an exception if the format_req is not supported.""" + # call with an empty dict to see if format_req is valid and supported + self._get_format_func({}, format_req) + + def _formatted(self, obj: Any, format_req: Optional[str] = None) -> str: + """Return the object formatted/serialized.""" + ffunc = self._get_format_func(obj, format_req) + return ffunc() + + def _return_value(self, obj: Any) -> int: + """Return a mgr return-value for the given object (usually zero).""" + return self._retval_provider(obj).mgr_return_value() + + def __call__(self, f: ObjectResponseFuncType) -> HandlerFuncType: + """Wrap a python function so that the original function's return value + becomes the source for an automatically formatted mgr response. + """ + + @wraps(f) + def _format_response(*args: Any, **kwargs: Any) -> Tuple[int, str, str]: + format_req = _get_requested_format(f, kwargs) + try: + self._dry_run(format_req) + robj = f(*args, **kwargs) + body = self._formatted(robj, format_req) + retval = self._return_value(robj) + except ErrorResponseBase as e: + return e.format_response() + return retval, body, "" + + return _format_response