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
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
@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)
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
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
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
.. 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.
# 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.
.. 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.
.. 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
.. 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.
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.
== 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
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:
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
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