|
1 | 1 | # object_format.py provides types and functions for working with |
2 | 2 | # requested output formats such as JSON, YAML, etc. |
| 3 | +"""tools for writing formatting-friendly mgr module functions |
| 4 | +
|
| 5 | +Currently, the ceph mgr code in python is most commonly written by adding mgr |
| 6 | +modules and corresponding classes and then adding methods to those classes that |
| 7 | +are decorated using `@CLICommand` from `mgr_module.py`. These methods (that |
| 8 | +will be called endpoints subsequently) then implement the logic that is |
| 9 | +executed when the mgr receives a command from a client. These endpoints are |
| 10 | +currently responsible for forming a response tuple of (int, str, str) where the |
| 11 | +int represents a return value (error code) and the first string the "body" of |
| 12 | +the response. The mgr supports a generic `format` parameter (`--format` on the |
| 13 | +ceph cli) that each endpoint must then explicitly handle. At the time of this |
| 14 | +writing, many endpoints do not handle alternate formats and are each |
| 15 | +implementing formatting/serialization of values in various different ways. |
| 16 | +
|
| 17 | +The `object_format` module aims to make the process of writing endpoint |
| 18 | +functions easier, more consistent, and (hopefully) better documented. At the |
| 19 | +highest level, the module provides a new decorator `Responder` that must be |
| 20 | +placed below the `CLICommand` decorator (so that it decorates the endpoint |
| 21 | +before `CLICommand`). This decorator helps automatically convert Python objects |
| 22 | +to response tuples expected by the manager, while handling the `format` |
| 23 | +parameter automatically. |
| 24 | +
|
| 25 | +In addition to the decorator the module provides a few other types and methods |
| 26 | +that intended to interoperate with the decorator and make small customizations |
| 27 | +and error handling easier. |
| 28 | +
|
| 29 | +== Using Responder == |
| 30 | +
|
| 31 | +The simple and intended way to use the decorator is as follows: |
| 32 | + @CLICommand("command name", perm="r") |
| 33 | + Responder() |
| 34 | + def create_something(self, name: str) -> Dict[str, str]: |
| 35 | + ... # implementation |
| 36 | + return {"name": name, "id": new_id} |
| 37 | +
|
| 38 | +In this case the `create_something` method return a python dict, |
| 39 | +and does not return a response tuple directly. Instead, the |
| 40 | +dict is converted to either JSON or YAML depending on what the |
| 41 | +client requested. Assuming no exception is raised by the |
| 42 | +implementation then the response code is always zero (success). |
| 43 | +
|
| 44 | +The object_format module provides an exception type `ErrorResponse` |
| 45 | +that assists in returning "clean" error conditions to the client. |
| 46 | +Extending the previous example to use this exception: |
| 47 | + @CLICommand("command name", perm="r") |
| 48 | + Responder() |
| 49 | + def create_something(self, name: str) -> Dict[str, str]: |
| 50 | + try: |
| 51 | + ... # implementation |
| 52 | + return {"name": name, "id": new_id} |
| 53 | + except KeyError as kerr: |
| 54 | + # explicitly set the return value to ENOENT for KeyError |
| 55 | + raise ErrorResponse.wrap(kerr, return_value=-errno.ENOENT) |
| 56 | + except (BusinessLogcError, OSError) as err: |
| 57 | + # return value is based on err when possible |
| 58 | + raise ErrorResponse.wrap(err) |
| 59 | +
|
| 60 | +Most uses of ErrorResponse are expected to use the `wrap` classmethod, |
| 61 | +as it will aid in the handling of an existing exception but `ErrorResponse` |
| 62 | +can be used directly too. |
| 63 | +
|
| 64 | +== Customizing Response Formatting == |
| 65 | +
|
| 66 | +The `Responder` is built using two additional mid-layer types. The |
| 67 | +`ObjectFormatAdapter` and the `ReturnValueAdapter` by default. These types |
| 68 | +implement the `CommonFormatter` protocol and `ReturnValueProvider` protocols |
| 69 | +respectively. Most cases will not need to customize the `ReturnValueAdapter` as |
| 70 | +returning zero on success is expected. However, if there's a need to return a |
| 71 | +non-zero error code outside of an exception, you can add the `mgr_return_value` |
| 72 | +function to the returned type of the endpoint function - causing it to meet the |
| 73 | +`ReturnValueProvider` protocol. Whatever integer that function returns will |
| 74 | +then be used in the response tuple. |
| 75 | +
|
| 76 | +The `ObjectFormatAdapter` can operate in two modes. By default, any type |
| 77 | +returned from the endpoint function will be checked for a `to_simplified` |
| 78 | +method (the type matches the SimpleDataProvider` protocol) and if it exists |
| 79 | +the method will be called and the result serialized. Example: |
| 80 | + class CoolStuff: |
| 81 | + def __init__(self, temperature: int, quantity: int) -> None: |
| 82 | + self.temperature = temperature |
| 83 | + self.quantity = quantity |
| 84 | + def to_simplified(self) -> Dict[str, int]: |
| 85 | + return {"temp": self.temperature, "qty": self.quantity} |
| 86 | +
|
| 87 | + @CLICommand("command name", perm="r") |
| 88 | + Responder() |
| 89 | + def create_something_cool(self) -> CoolStuff: |
| 90 | + cool_stuff: CoolStuff = self._make_cool_stuff() # implementation |
| 91 | + return cool_stuff |
| 92 | +
|
| 93 | +In order to serialize the result, the object returned from the wrapped |
| 94 | +function must provide the `to_simplified` method (or the compatibility methods, |
| 95 | +see below) or already be a "simplified type". Valid types include lists and |
| 96 | +dicts that contain other lists and dicts and ints, strs, bools -- basic objects |
| 97 | +that can be directly converted to json (via json.dumps) without any additional |
| 98 | +conversions. The `to_simplified` method must always return such types. |
| 99 | +
|
| 100 | +To be compatible with many existing types in the ceph mgr codebase one can pass |
| 101 | +`compatible=True` to the `ObjectFormatAdapter`. If the type provides a |
| 102 | +`to_json` and/or `to_yaml` method that returns basic python types (dict, list, |
| 103 | +str, etc...) but *not* already serialized JSON or YAML this flag can be |
| 104 | +enabled. Note that Responder takes as an argument any callable that returns a |
| 105 | +`CommonFormatter`. In this example below we enable the flag using |
| 106 | +`functools.partial`: |
| 107 | + class MyExistingClass: |
| 108 | + def to_json(self) -> Dict[str, Any]: |
| 109 | + return {"name": self.name, "height": self.height} |
| 110 | +
|
| 111 | + @CLICommand("command name", perm="r") |
| 112 | + Responder(functools.partial(ObjectFormatAdapter, compatible=True)) |
| 113 | + def create_an_item(self) -> MyExistingClass: |
| 114 | + item: MyExistingClass = self._new_item() # implementation |
| 115 | + return item |
| 116 | +
|
| 117 | +
|
| 118 | +For cases that need to return xml or plain text formatted responses one can |
| 119 | +create a new class that matches the `CommonFormatter` protocol (provides a |
| 120 | +valid_formats method) and one or more `format_x` method where x is the name of |
| 121 | +a format ("json", "yaml", "xml", "plain", etc...). |
| 122 | + class MyCustomFormatAdapter: |
| 123 | + def __init__(self, obj_to_format: Any) -> None: |
| 124 | + ... |
| 125 | + def valid_formats(self) -> Iterable[str]: |
| 126 | + ... |
| 127 | + def format_json(self) -> str: |
| 128 | + ... |
| 129 | + def format_xml(self) -> str: |
| 130 | + ... |
| 131 | +
|
| 132 | +
|
| 133 | +Of course, the Responder itself can be used as a base class and aspects of the |
| 134 | +Responder altered for specific use cases. Inheriting from `Responder` and |
| 135 | +customizing it is an exercise left for those brave enough to read the code in |
| 136 | +`object_format.py` :-). |
| 137 | +""" |
3 | 138 |
|
4 | 139 | import enum |
5 | 140 | import errno |
|
0 commit comments