]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
pybind/mgr: add a Responder decorator type
authorJohn Mulligan <jmulligan@redhat.com>
Sat, 9 Apr 2022 19:19:37 +0000 (15:19 -0400)
committerAdam King <adking@redhat.com>
Sat, 21 May 2022 23:21:20 +0000 (19:21 -0400)
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 <jmulligan@redhat.com>
(cherry picked from commit 6e234b190841204d076e1b622db543674d0c4b57)

src/pybind/mgr/object_format.py

index 0d2dbb439593e1c063933a8326adee00ce6d4216..fb89e9034ee9baa644214d388fb2c81466e30d8d 100644 (file)
@@ -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