]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
doc: update mgr module command documentation for per-module registries 67257/head
authorKefu Chai <k.chai@proxmox.com>
Mon, 9 Feb 2026 02:09:14 +0000 (10:09 +0800)
committerKefu Chai <k.chai@proxmox.com>
Sun, 8 Mar 2026 15:27:30 +0000 (23:27 +0800)
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 <k.chai@proxmox.com>
doc/dev/developer_guide/dash-devel.rst
doc/mgr/modules.rst
src/pybind/mgr/object_format.py

index 566f5595863b462b623e020a9a56ec5bfb713123..ea9dae58186e5a7f0ac20d3ba6c949528eaf5e6d 100644 (file)
@@ -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)
index 325404a29d639c3e4fca75485ddf396f6f65712b..1e2976074d4a1e151ba5b28c16e810c9d32a9cbf 100644 (file)
@@ -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.
index c40673b7d75884fd661d0b13f5441d245a6fa913..8625acfe72deb757bae11d2f8d2dfc1bf9b9c6ec 100644 (file)
@@ -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