From: Kefu Chai Date: Mon, 9 Feb 2026 02:09:14 +0000 (+0800) Subject: doc: update mgr module command documentation for per-module registries X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=1832b1572cbd303bae8709264ce46917f331350a;p=ceph.git doc: update mgr module command documentation for per-module registries Update documentation to reflect the new per-module command registry pattern introduced in PR #66467. The old global CLICommand decorators have been replaced with module-specific registries. Changes: - doc/mgr/modules.rst: Rewrite CLICommand section with setup guide, update all examples to use AntigravityCLICommand pattern - src/pybind/mgr/object_format.py: Add note explaining per-module registries and update all decorator examples - doc/dev/developer_guide/dash-devel.rst: Update dashboard plugin examples to use DBCLICommand All examples now correctly show: - Creating registry with CLICommandBase.make_registry_subtype() - Using module-specific decorator names (e.g., @StatusCLICommand.Read) - Setting CLICommand class attribute for framework registration Signed-off-by: Kefu Chai --- diff --git a/doc/dev/developer_guide/dash-devel.rst b/doc/dev/developer_guide/dash-devel.rst index 566f5595863..ea9dae58186 100644 --- a/doc/dev/developer_guide/dash-devel.rst +++ b/doc/dev/developer_guide/dash-devel.rst @@ -2636,7 +2636,9 @@ The available Interfaces are: of ``Options()``. The options returned here are added to the ``MODULE_OPTIONS``. - ``HasCommands``: requires overriding ``register_commands()`` hook by defining - the commands the plug-in can handle and decorating them with ``@CLICommand``. + the commands the plug-in can handle and decorating them with the dashboard's + command registry ``@DBCLICommand``, defined in + ``src/pybind/mgr/dashboard/cli.py``. The commands can be optionally returned, so that they can be invoked externally (which makes unit testing easier). - ``HasControllers``: requires overriding ``get_controllers()`` hook by defining @@ -2660,7 +2662,8 @@ A sample plugin implementation would look like this: from . import PLUGIN_MANAGER as PM from . import interfaces as I - from mgr_module import CLICommand, Option + from ..cli import DBCLICommand + from mgr_module import Option import cherrypy @PM.add_plugin @@ -2676,7 +2679,7 @@ A sample plugin implementation would look like this: @PM.add_hook def register_commands(self): - @CLICommand("dashboard mute") + @DBCLICommand("dashboard mute") def _(mgr): self.mute = True self.mgr.set_module_option('mute', True) diff --git a/doc/mgr/modules.rst b/doc/mgr/modules.rst index 325404a29d6..1e2976074d4 100644 --- a/doc/mgr/modules.rst +++ b/doc/mgr/modules.rst @@ -126,10 +126,46 @@ module class. The CLICommand approach ~~~~~~~~~~~~~~~~~~~~~~~ +This approach uses decorators to register commands with the manager framework. +Each module creates its own command registry to avoid namespace collisions. + +Setting Up Command Registration +++++++++++++++++++++++++++++++++ + +First, create a ``cli.py`` file in your module directory to define the command +registry: + +.. code:: python + + # In antigravity/cli.py + from mgr_module import CLICommandBase + + # Create a module-specific command registry + AntigravityCLICommand = CLICommandBase.make_registry_subtype("AntigravityCLICommand") + +Then, in your module's main file, import the registry and set it as a class +attribute so the framework can discover and register your commands: + +.. code:: python + + # In antigravity/module.py + from mgr_module import MgrModule + from .cli import AntigravityCLICommand + + class Module(MgrModule): + # Framework uses this attribute for command registration and dispatch + CLICommand = AntigravityCLICommand + +Defining Commands ++++++++++++++++++ + +Use the registry's decorator methods to define commands. The decorator must use +the specific registry type name (``AntigravityCLICommand`` in this example), +not the class attribute name ``CLICommand``: + .. code:: python - @CLICommand('antigravity send to blackhole', - perm='rw') + @AntigravityCLICommand('antigravity send to blackhole') def send_to_blackhole(self, oid: str, blackhole: Optional[str] = None, inbuf: Optional[str] = None): ''' Send the specified object to black hole @@ -147,7 +183,7 @@ The CLICommand approach self.send_object_to(obj, location) return HandleCommandResult(stdout=f"the black hole swallowed '{oid}'") -The first parameter passed to ``CLICommand`` is the "name" of the command. +The first parameter passed to the decorator is the "name" of the command. Since there are lots of commands in Ceph, we tend to group related commands with a common prefix. In this case, "antigravity" is used for this purpose. As the author is probably designing a module which is also able to launch @@ -174,9 +210,29 @@ like:: as part of the output of ``ceph --help``. -In addition to ``@CLICommand``, you could also use ``@CLIReadCommand`` or -``@CLIWriteCommand`` if your command only requires read permissions or -write permissions respectively. +Read and Write Commands +++++++++++++++++++++++++ + +For commands that only require read or write permissions, use the ``.Read()`` +or ``.Write()`` methods on your module's command registry: + +.. code:: python + + # Read-only command + @AntigravityCLICommand.Read('antigravity list objects') + def list_objects(self): + '''List all objects in the antigravity system''' + return HandleCommandResult(stdout=json.dumps(self.get_objects())) + + # Write-only command + @AntigravityCLICommand.Write('antigravity delete object') + def delete_object(self, oid: str): + '''Delete an object from the antigravity system''' + self.remove_object(oid) + return HandleCommandResult(stdout=f"deleted '{oid}'") + +For commands that need both read and write permissions, use the base decorator +without ``.Read()`` or ``.Write()``, as shown in the earlier example. The COMMANDS Approach @@ -254,7 +310,7 @@ In most cases, net new code should use the ``Responder`` decorator. Example: .. code:: python - @CLICommand('antigravity list wormholes', perm='r') + @AntigravityCLICommand.Read('antigravity list wormholes') @Responder() def list_wormholes(self, oid: str, details: bool = False) -> List[Dict[str, Any]]: '''List wormholes associated with the supplied oid. @@ -286,7 +342,7 @@ a simplified representation of the object made out of basic types. # returns a python object(s) made up from basic types return {"gravitons": 999, "tachyons": 404} - @CLICommand('antigravity list wormholes', perm='r') + @AntigravityCLICommand.Read('antigravity list wormholes') @Responder() def list_wormholes(self, oid: str, details: bool = False) -> MyCleverObject: '''List wormholes associated with the supplied oid. @@ -316,7 +372,7 @@ Converting our previous example to use this exception handling approach: .. code:: python - @CLICommand('antigravity list wormholes', perm='r') + @AntigravityCLICommand.Read('antigravity list wormholes') @Responder() def list_wormholes(self, oid: str, details: bool = False) -> List[Dict[str, Any]]: '''List wormholes associated with the supplied oid. @@ -356,7 +412,7 @@ to return raw data in the output. Example: .. code:: python - @CLICommand('antigravity dump config', perm='r') + @AntigravityCLICommand.Read('antigravity dump config') @ErrorResponseHandler() def dump_config(self, oid: str) -> Tuple[int, str, str]: '''Dump configuration @@ -381,7 +437,7 @@ be automatically processed. Example: .. code:: python - @CLICommand('antigravity create wormhole', perm='rw') + @AntigravityCLICommand('antigravity create wormhole') @EmptyResponder() def create_wormhole(self, oid: str, name: str) -> None: '''Create a new wormhole. diff --git a/src/pybind/mgr/object_format.py b/src/pybind/mgr/object_format.py index c40673b7d75..8625acfe72d 100644 --- a/src/pybind/mgr/object_format.py +++ b/src/pybind/mgr/object_format.py @@ -4,24 +4,31 @@ Currently, the ceph mgr code in python is most commonly written by adding mgr modules and corresponding classes and then adding methods to those classes that -are decorated using `@CLICommand` from `mgr_module.py`. These methods (that -will be called endpoints subsequently) then implement the logic that is -executed when the mgr receives a command from a client. These endpoints are +are decorated using per-module command registries created from `CLICommandBase` +in `mgr_module.py`. These methods (endpoints) then implement the logic that is +executed when the mgr receives a command from a client. These endpoints are currently responsible for forming a response tuple of (int, str, str) where the int represents a return value (error code) and the first string the "body" of the response. The mgr supports a generic `format` parameter (`--format` on the -ceph cli) that each endpoint must then explicitly handle. At the time of this +ceph CLI) that each endpoint must then explicitly handle. At the time of this writing, many endpoints do not handle alternate formats and are each implementing formatting/serialization of values in various different ways. The `object_format` module aims to make the process of writing endpoint functions easier, more consistent, and (hopefully) better documented. At the highest level, the module provides a new decorator `Responder` that must be -placed below the `CLICommand` decorator (so that it decorates the endpoint -before `CLICommand`). This decorator helps automatically convert Python objects -to response tuples expected by the manager, while handling the `format` +placed below the command decorator so that it decorates the endpoint before +the command decorator. This decorator helps automatically convert Python +objects to response tuples expected by the manager, while handling the `format` parameter automatically. +NOTE: The examples below use placeholder names like `StatusCLICommand` to +represent module-specific command registries. Each module must create its own +registry using `CLICommandBase.make_registry_subtype()` in a `cli.py` file, +then import and use that specific registry type in decorators. The decorators +must use the specific type name (e.g., `@StatusCLICommand.Read`), NOT +`@CLICommand`. See `doc/mgr/modules.rst` for complete setup instructions. + In addition to the decorator the module provides a few other types and methods that intended to interoperate with the decorator and make small customizations and error handling easier. @@ -29,7 +36,7 @@ and error handling easier. == Using Responder == The simple and intended way to use the decorator is as follows: - @CLICommand("command name", perm="r") + @StatusCLICommand.Read("command name") Responder() def create_something(self, name: str) -> Dict[str, str]: ... # implementation @@ -44,7 +51,7 @@ implementation then the response code is always zero (success). The object_format module provides an exception type `ErrorResponse` that assists in returning "clean" error conditions to the client. Extending the previous example to use this exception: - @CLICommand("command name", perm="r") + @StatusCLICommand.Read("command name") Responder() def create_something(self, name: str) -> Dict[str, str]: try: @@ -84,7 +91,7 @@ the method will be called and the result serialized. Example: def to_simplified(self) -> Dict[str, int]: return {"temp": self.temperature, "qty": self.quantity} - @CLICommand("command name", perm="r") + @StatusCLICommand.Read("command name") Responder() def create_something_cool(self) -> CoolStuff: cool_stuff: CoolStuff = self._make_cool_stuff() # implementation @@ -108,7 +115,7 @@ enabled. Note that Responder takes as an argument any callable that returns a def to_json(self) -> Dict[str, Any]: return {"name": self.name, "height": self.height} - @CLICommand("command name", perm="r") + @StatusCLICommand.Read("command name") Responder(functools.partial(ObjectFormatAdapter, compatible=True)) def create_an_item(self) -> MyExistingClass: item: MyExistingClass = self._new_item() # implementation