]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
pybind/mgr/dashboard: create DBCLICommand, use throughout
authorSamuel Just <sjust@redhat.com>
Mon, 24 Nov 2025 17:37:41 +0000 (09:37 -0800)
committerSamuel Just <sjust@redhat.com>
Wed, 17 Dec 2025 17:41:16 +0000 (17:41 +0000)
Also moves Command from mgr_module to DBCommand in dashboard/cli.py
since dashboard is the only user and this way it can directly use
DBCLICommand.

Signed-off-by: Samuel Just <sjust@redhat.com>
13 files changed:
src/pybind/mgr/dashboard/cli.py [new file with mode: 0644]
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/plugins/feature_toggles.py
src/pybind/mgr/dashboard/plugins/motd.py
src/pybind/mgr/dashboard/plugins/plugin.py
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/services/iscsi_cli.py
src/pybind/mgr/dashboard/services/nvmeof_cli.py
src/pybind/mgr/dashboard/services/sso.py
src/pybind/mgr/dashboard/tests/__init__.py
src/pybind/mgr/dashboard/tests/test_access_control.py
src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py
src/pybind/mgr/mgr_module.py

diff --git a/src/pybind/mgr/dashboard/cli.py b/src/pybind/mgr/dashboard/cli.py
new file mode 100644 (file)
index 0000000..7241183
--- /dev/null
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+import functools
+import inspect
+from typing import Any, Callable
+
+from mgr_module import CLICommandBase, HandleCommandResult, HandlerFuncType
+
+DBCLICommand = CLICommandBase.make_registry_subtype("DBCLICommand")
+
+
+class DBCommand(dict):
+    """
+    Helper class to declare options for COMMANDS list.
+
+    It also allows to specify prefix and args separately, as well as storing a
+    handler callable.
+
+    Usage:
+    >>> def handler(): return 0, "", ""
+    >>> DBCommand(prefix="example",
+    ...           handler=handler,
+    ...           perm='w')
+    {'perm': 'w', 'poll': False}
+    """
+
+    def __init__(
+            self,
+            prefix: str,
+            handler: HandlerFuncType,
+            perm: str = "rw",
+            poll: bool = False,
+    ):
+        super().__init__(perm=perm,
+                         poll=poll)
+        self.prefix = prefix
+        self.handler = handler
+
+    @staticmethod
+    def returns_command_result(instance: Any,
+                               f: HandlerFuncType) -> Callable[..., HandleCommandResult]:
+        @functools.wraps(f)
+        def wrapper(mgr: Any, *args: Any, **kwargs: Any) -> HandleCommandResult:
+            retval, stdout, stderr = f(instance or mgr, *args, **kwargs)
+            return HandleCommandResult(retval, stdout, stderr)
+        wrapper.__signature__ = inspect.signature(f)  # type: ignore[attr-defined]
+        return wrapper
+
+    def register(self, instance: bool = False) -> HandlerFuncType:
+        """
+        Register a CLICommand handler. It allows an instance to register bound
+        methods. In that case, the mgr instance is not passed, and it's expected
+        to be available in the class instance.
+        It also uses HandleCommandResult helper to return a wrapped a tuple of 3
+        items.
+        """
+        cmd = DBCLICommand(prefix=self.prefix, perm=self['perm'])
+        return cmd(self.returns_command_result(instance, self.handler))
+        return None
index 9ce6d12ea503d6c41e004dbef577ad61b516200d..713deca65e8028ec7b3aa88786c66ccbe3dc909a 100644 (file)
@@ -24,12 +24,13 @@ if TYPE_CHECKING:
         from typing_extensions import Literal
 
 from ceph.cryptotools.select import choose_crypto_caller
-from mgr_module import CLIReadCommand, CLIWriteCommand, HandleCommandResult, \
-    MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
+from mgr_module import HandleCommandResult, MgrModule, MgrStandbyModule, \
+    NotifyType, Option, _get_localized_key
 from mgr_util import ServerConfigException, build_url, \
     create_self_signed_cert, get_default_addr, verify_tls_files
 
 from . import mgr
+from .cli import DBCLICommand
 from .controllers import nvmeof  # noqa # pylint: disable=unused-import
 from .controllers import Router, json_error_page
 from .grafana import push_local_dashboards
@@ -233,6 +234,7 @@ class Module(MgrModule, CherryPyConfig):
     """
     dashboard module entrypoint
     """
+    CLICommand = DBCLICommand
 
     COMMANDS = [
         {
@@ -405,15 +407,15 @@ class Module(MgrModule, CherryPyConfig):
         cluster_credentials_files, clusters_credentials = multi_cluster_instance.get_cluster_credentials_files(targets)  # noqa E501 #pylint: disable=line-too-long
         return cluster_credentials_files, clusters_credentials
 
-    @CLIWriteCommand("dashboard set-ssl-certificate")
+    @DBCLICommand.Write("dashboard set-ssl-certificate")
     def set_ssl_certificate(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
         return self._set_ssl_item('certificate', 'crt', mgr_id, inbuf)
 
-    @CLIWriteCommand("dashboard set-ssl-certificate-key")
+    @DBCLICommand.Write("dashboard set-ssl-certificate-key")
     def set_ssl_certificate_key(self, mgr_id: Optional[str] = None, inbuf: Optional[str] = None):
         return self._set_ssl_item('certificate key', 'key', mgr_id, inbuf)
 
-    @CLIWriteCommand("dashboard create-self-signed-cert")
+    @DBCLICommand.Write("dashboard create-self-signed-cert")
     def set_mgr_created_self_signed_cert(self):
         cert, pkey = create_self_signed_cert('IT', 'ceph-dashboard')
         result = HandleCommandResult(*self.set_ssl_certificate(inbuf=cert))
@@ -425,7 +427,7 @@ class Module(MgrModule, CherryPyConfig):
             return result
         return 0, 'Self-signed certificate created', ''
 
-    @CLIWriteCommand("dashboard set-rgw-credentials")
+    @DBCLICommand.Write("dashboard set-rgw-credentials")
     def set_rgw_credentials(self):
         try:
             rgw_service_manager = RgwServiceManager()
@@ -435,7 +437,7 @@ class Module(MgrModule, CherryPyConfig):
 
         return 0, 'RGW credentials configured', ''
 
-    @CLIWriteCommand("dashboard set-rgw-hostname")
+    @DBCLICommand.Write("dashboard set-rgw-hostname")
     def set_rgw_hostname(self, daemon_name: str, hostname: str):
         try:
             rgw_service_manager = RgwServiceManager()
@@ -444,7 +446,7 @@ class Module(MgrModule, CherryPyConfig):
         except Exception as error:
             return -errno.EINVAL, '', str(error)
 
-    @CLIWriteCommand("dashboard unset-rgw-hostname")
+    @DBCLICommand.Write("dashboard unset-rgw-hostname")
     def unset_rgw_hostname(self, daemon_name: str):
         try:
             rgw_service_manager = RgwServiceManager()
@@ -453,7 +455,7 @@ class Module(MgrModule, CherryPyConfig):
         except Exception as error:
             return -errno.EINVAL, '', str(error)
 
-    @CLIWriteCommand("dashboard set-login-banner")
+    @DBCLICommand.Write("dashboard set-login-banner")
     def set_login_banner(self, inbuf: str):
         '''
         Set the custom login banner read from -i <file>
@@ -467,7 +469,7 @@ class Module(MgrModule, CherryPyConfig):
         mgr.set_store('custom_login_banner', inbuf)
         return HandleCommandResult(stdout=f'{item_label} added')
 
-    @CLIReadCommand("dashboard get-login-banner")
+    @DBCLICommand.Read("dashboard get-login-banner")
     def get_login_banner(self):
         '''
         Get the custom login banner text
@@ -478,7 +480,7 @@ class Module(MgrModule, CherryPyConfig):
         else:
             return HandleCommandResult(stdout=banner_text)
 
-    @CLIWriteCommand("dashboard unset-login-banner")
+    @DBCLICommand.Write("dashboard unset-login-banner")
     def unset_login_banner(self):
         '''
         Unset the custom login banner
@@ -488,7 +490,7 @@ class Module(MgrModule, CherryPyConfig):
 
     # allow cors by setting cross_origin_url
     # the value is a comma separated list of URLs
-    @CLIWriteCommand("dashboard set-cross-origin-url")
+    @DBCLICommand.Write("dashboard set-cross-origin-url")
     def set_cross_origin_url(self, value: str):
         cross_origin_urls = self.get_module_option('cross_origin_url', '')
         cross_origin_urls_list = [url.strip()
@@ -502,14 +504,14 @@ class Module(MgrModule, CherryPyConfig):
         configure_cors()
         return 0, 'Cross-origin URL set', ''
 
-    @CLIReadCommand("dashboard get-cross-origin-url")
+    @DBCLICommand.Read("dashboard get-cross-origin-url")
     def get_cross_origin_url(self):
         urls = self.get_module_option('cross_origin_url', '')
         if urls:
             return HandleCommandResult(stdout=urls)  # type: ignore
         return HandleCommandResult(stdout='No cross-origin URL set')
 
-    @CLIReadCommand("dashboard rm-cross-origin-url")
+    @DBCLICommand.Read("dashboard rm-cross-origin-url")
     def rm_cross_origin_url(self, value: str):
         urls = self.get_module_option('cross_origin_url', '')
         urls_list = [url.strip() for url in urls.split(',')]  # type: ignore
index 8b6f7180dfc27d64c4b8f9d104ebcbe69938a87a..869ef04ac1961cf9b160aebcd2f6faf046fcc581 100644 (file)
@@ -4,9 +4,10 @@ from enum import Enum
 from typing import Dict, List, Optional, Set, no_type_check
 
 import cherrypy
-from mgr_module import CLICommand, Option
+from mgr_module import Option
 from mgr_util import CLIWarning
 
+from ..cli import DBCLICommand
 from ..controllers.cephfs import CephFS
 from ..controllers.iscsi import Iscsi, IscsiTarget
 from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
@@ -81,7 +82,7 @@ class FeatureToggles(I.CanMgr, I.Setupable, I.HasOptions,
 
     @PM.add_hook
     def register_commands(self):
-        @CLICommand("dashboard feature")
+        @DBCLICommand("dashboard feature")
         def cmd(mgr,
                 action: Actions = Actions.STATUS,
                 features: Optional[List[Features]] = None):
index 22d6a294a39444550191b427c3863467985644aa..cd673817f672f133b5d27b9d479f05607f642f12 100644 (file)
@@ -6,8 +6,8 @@ from enum import Enum
 from typing import Dict, NamedTuple, Optional
 
 from ceph.utils import datetime_now, datetime_to_str, parse_timedelta, str_to_datetime
-from mgr_module import CLICommand
 
+from ..cli import DBCLICommand
 from . import PLUGIN_MANAGER as PM
 from .plugin import SimplePlugin as SP
 
@@ -40,7 +40,7 @@ class Motd(SP):
 
     @PM.add_hook
     def register_commands(self):
-        @CLICommand("dashboard {name} get".format(name=self.NAME))
+        @DBCLICommand("dashboard {name} get".format(name=self.NAME))
         def _get(_):
             stdout: str
             value: str = self.get_option(self.NAME)
@@ -54,7 +54,7 @@ class Motd(SP):
                          'expires="{expires}"'.format(**data)
             return 0, stdout, ''
 
-        @CLICommand("dashboard {name} set".format(name=self.NAME))
+        @DBCLICommand("dashboard {name} set".format(name=self.NAME))
         def _set(_, severity: MotdSeverity, expires: str, message: str):
             if expires != '0':
                 delta = parse_timedelta(expires)
@@ -72,7 +72,7 @@ class Motd(SP):
             self.set_option(self.NAME, value)
             return 0, 'Message of the day has been set.', ''
 
-        @CLICommand("dashboard {name} clear".format(name=self.NAME))
+        @DBCLICommand("dashboard {name} clear".format(name=self.NAME))
         def _clear(_):
             self.set_option(self.NAME, '')
             return 0, 'Message of the day has been cleared.', ''
index 120fb30aacb66231844ea965b220c7e9f81269d0..073973c17834fb9ee2c3490c8acef081f3bc476b 100644 (file)
@@ -1,7 +1,8 @@
 from typing import no_type_check
 
-from mgr_module import Command, Option
+from mgr_module import Option
 
+from ..cli import DBCommand
 from . import PLUGIN_MANAGER as PM
 from . import interfaces as I  # noqa: E741,N812
 
@@ -12,10 +13,10 @@ class SimplePlugin(I.CanMgr, I.HasOptions, I.HasCommands):
         - Default Mixins/Interfaces: CanMgr, HasOptions & HasCommands
     - Options are defined by OPTIONS class variable, instead from get_options hook
     - Commands are created with by COMMANDS list of Commands() and handlers
-    (less compact than CLICommand, but allows using method instances)
+    (less compact than but allows using method instances)
     """
     Option = Option
-    Command = Command
+    Command = DBCommand
 
     @PM.add_hook
     def get_options(self):
index 4ce10da2692870d2b0026617ad34fdae6dabc9fc..9e0897d2009172bde51e9bbac8fe5a21e51d0fec 100644 (file)
@@ -13,10 +13,11 @@ from string import ascii_lowercase, ascii_uppercase, digits, punctuation
 from typing import List, Optional, Sequence
 
 from ceph.cryptotools.select import get_crypto_caller
-from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
+from mgr_module import CLICheckNonemptyFileInput
 from mgr_util import password_hash
 
 from .. import mgr
+from ..cli import DBCLICommand
 from ..exceptions import PasswordPolicyException, PermissionNotValid, \
     PwdExpirationDateNotValid, RoleAlreadyExists, RoleDoesNotExist, \
     RoleIsAssociatedWithUser, RoleNotInUser, ScopeNotInRole, ScopeNotValid, \
@@ -605,7 +606,7 @@ def load_access_control_db():
 
 # CLI dashboard access control scope commands
 
-@CLIWriteCommand('dashboard set-login-credentials')
+@DBCLICommand.Write('dashboard set-login-credentials')
 @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
 def set_login_credentials_cmd(_, username: str, inbuf: str):
     '''
@@ -629,7 +630,7 @@ def set_login_credentials_cmd(_, username: str, inbuf: str):
 Username and password updated''', ''
 
 
-@CLIReadCommand('dashboard ac-role-show')
+@DBCLICommand.Read('dashboard ac-role-show')
 def ac_role_show_cmd(_, rolename: Optional[str] = None):
     '''
     Show role info
@@ -648,7 +649,7 @@ def ac_role_show_cmd(_, rolename: Optional[str] = None):
     return 0, json.dumps(role.to_dict()), ''
 
 
-@CLIWriteCommand('dashboard ac-role-create')
+@DBCLICommand.Write('dashboard ac-role-create')
 def ac_role_create_cmd(_, rolename: str, description: Optional[str] = None):
     '''
     Create a new access control role
@@ -661,7 +662,7 @@ def ac_role_create_cmd(_, rolename: str, description: Optional[str] = None):
         return -errno.EEXIST, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-role-delete')
+@DBCLICommand.Write('dashboard ac-role-delete')
 def ac_role_delete_cmd(_, rolename: str):
     '''
     Delete an access control role
@@ -679,7 +680,7 @@ def ac_role_delete_cmd(_, rolename: str):
         return -errno.EPERM, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-role-add-scope-perms')
+@DBCLICommand.Write('dashboard ac-role-add-scope-perms')
 def ac_role_add_scope_perms_cmd(_,
                                 rolename: str,
                                 scopename: str,
@@ -708,7 +709,7 @@ def ac_role_add_scope_perms_cmd(_,
             .format(Permission.all_permissions())
 
 
-@CLIWriteCommand('dashboard ac-role-del-scope-perms')
+@DBCLICommand.Write('dashboard ac-role-del-scope-perms')
 def ac_role_del_scope_perms_cmd(_, rolename: str, scopename: str):
     '''
     Delete the scope permissions for a role
@@ -728,7 +729,7 @@ def ac_role_del_scope_perms_cmd(_, rolename: str, scopename: str):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIReadCommand('dashboard ac-user-show')
+@DBCLICommand.Read('dashboard ac-user-show')
 def ac_user_show_cmd(_, username: Optional[str] = None):
     '''
     Show user info
@@ -744,7 +745,7 @@ def ac_user_show_cmd(_, username: Optional[str] = None):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-create')
+@DBCLICommand.Write('dashboard ac-user-create')
 @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
 def ac_user_create_cmd(_, username: str, inbuf: str,
                        rolename: Optional[str] = None,
@@ -783,7 +784,7 @@ def ac_user_create_cmd(_, username: str, inbuf: str,
     return 0, json.dumps(user.to_dict()), ''
 
 
-@CLIWriteCommand('dashboard ac-user-enable')
+@DBCLICommand.Write('dashboard ac-user-enable')
 def ac_user_enable(_, username: str):
     '''
     Enable a user
@@ -799,7 +800,7 @@ def ac_user_enable(_, username: str):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-disable')
+@DBCLICommand.Write('dashboard ac-user-disable')
 def ac_user_disable(_, username: str):
     '''
     Disable a user
@@ -814,7 +815,7 @@ def ac_user_disable(_, username: str):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-delete')
+@DBCLICommand.Write('dashboard ac-user-delete')
 def ac_user_delete_cmd(_, username: str):
     '''
     Delete user
@@ -827,7 +828,7 @@ def ac_user_delete_cmd(_, username: str):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-set-roles')
+@DBCLICommand.Write('dashboard ac-user-set-roles')
 def ac_user_set_roles_cmd(_, username: str, roles: Sequence[str]):
     '''
     Set user roles
@@ -850,7 +851,7 @@ def ac_user_set_roles_cmd(_, username: str, roles: Sequence[str]):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-add-roles')
+@DBCLICommand.Write('dashboard ac-user-add-roles')
 def ac_user_add_roles_cmd(_, username: str, roles: Sequence[str]):
     '''
     Add roles to user
@@ -873,7 +874,7 @@ def ac_user_add_roles_cmd(_, username: str, roles: Sequence[str]):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-del-roles')
+@DBCLICommand.Write('dashboard ac-user-del-roles')
 def ac_user_del_roles_cmd(_, username: str, roles: Sequence[str]):
     '''
     Delete roles from user
@@ -898,7 +899,7 @@ def ac_user_del_roles_cmd(_, username: str, roles: Sequence[str]):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-set-password')
+@DBCLICommand.Write('dashboard ac-user-set-password')
 @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
 def ac_user_set_password(_, username: str, inbuf: str,
                          force_password: bool = False):
@@ -920,7 +921,7 @@ def ac_user_set_password(_, username: str, inbuf: str,
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-set-password-hash')
+@DBCLICommand.Write('dashboard ac-user-set-password-hash')
 @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC)
 def ac_user_set_password_hash(_, username: str, inbuf: str):
     '''
@@ -944,7 +945,7 @@ def ac_user_set_password_hash(_, username: str, inbuf: str):
         return -errno.ENOENT, '', str(ex)
 
 
-@CLIWriteCommand('dashboard ac-user-set-info')
+@DBCLICommand.Write('dashboard ac-user-set-info')
 def ac_user_set_info(_, username: str, name: str, email: str):
     '''
     Set user info
index 0e2e0b215ff3ac642009ff8c575da215d7d8720a..dbe5716efdd5bc75d9d421441e69ce4cd4da70ac 100644 (file)
@@ -4,8 +4,9 @@ import errno
 import json
 from typing import Optional
 
-from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
+from mgr_module import CLICheckNonemptyFileInput
 
+from ..cli import DBCLICommand
 from ..rest_client import RequestException
 from .iscsi_client import IscsiClient
 from .iscsi_config import InvalidServiceUrl, IscsiGatewayAlreadyExists, \
@@ -13,7 +14,7 @@ from .iscsi_config import InvalidServiceUrl, IscsiGatewayAlreadyExists, \
     ManagedByOrchestratorException
 
 
-@CLIReadCommand('dashboard iscsi-gateway-list')
+@DBCLICommand.Read('dashboard iscsi-gateway-list')
 def list_iscsi_gateways(_):
     '''
     List iSCSI gateways
@@ -21,7 +22,7 @@ def list_iscsi_gateways(_):
     return 0, json.dumps(IscsiGatewaysConfig.get_gateways_config()), ''
 
 
-@CLIWriteCommand('dashboard iscsi-gateway-add')
+@DBCLICommand.Write('dashboard iscsi-gateway-add')
 @CLICheckNonemptyFileInput(desc='iSCSI gateway configuration')
 def add_iscsi_gateway(_, inbuf, name: Optional[str] = None):
     '''
@@ -44,7 +45,7 @@ def add_iscsi_gateway(_, inbuf, name: Optional[str] = None):
         return -errno.EINVAL, '', str(ex)
 
 
-@CLIWriteCommand('dashboard iscsi-gateway-rm')
+@DBCLICommand.Write('dashboard iscsi-gateway-rm')
 def remove_iscsi_gateway(_, name: str):
     '''
     Remove iSCSI gateway configuration
index 6cb8d361f5d4ca82ce66f5741710b5b8969a24bb..9897294527e67685901ffa83376bc864c7ee340c 100644 (file)
@@ -7,17 +7,17 @@ from typing import Annotated, Any, Dict, List, NamedTuple, Optional, Type, \
     Union, get_args, get_origin, get_type_hints
 
 import yaml
-from mgr_module import CLICheckNonemptyFileInput, CLICommand, CLIReadCommand, \
-    CLIWriteCommand, HandleCommandResult, HandlerFuncType
+from mgr_module import CLICheckNonemptyFileInput, HandleCommandResult, HandlerFuncType
 from prettytable import PrettyTable
 
+from ..cli import DBCLICommand
 from ..model.nvmeof import CliFieldTransformer, CliFlags, CliHeader
 from ..rest_client import RequestException
 from .nvmeof_conf import ManagedByOrchestratorException, \
     NvmeofGatewayAlreadyExists, NvmeofGatewaysConfig
 
 
-@CLIReadCommand('dashboard nvmeof-gateway-list')
+@DBCLICommand.Read('dashboard nvmeof-gateway-list')
 def list_nvmeof_gateways(_):
     '''
     List NVMe-oF gateways
@@ -25,7 +25,7 @@ def list_nvmeof_gateways(_):
     return 0, json.dumps(NvmeofGatewaysConfig.get_gateways_config()), ''
 
 
-@CLIWriteCommand('dashboard nvmeof-gateway-add')
+@DBCLICommand.Write('dashboard nvmeof-gateway-add')
 @CLICheckNonemptyFileInput(desc='NVMe-oF gateway configuration')
 def add_nvmeof_gateway(_, inbuf, name: str, group: str, daemon_name: str):
     '''
@@ -43,7 +43,7 @@ def add_nvmeof_gateway(_, inbuf, name: str, group: str, daemon_name: str):
         return -errno.EINVAL, '', str(ex)
 
 
-@CLIWriteCommand('dashboard nvmeof-gateway-rm')
+@DBCLICommand.Write('dashboard nvmeof-gateway-rm')
 def remove_nvmeof_gateway(_, name: str, daemon_name: str = ''):
     '''
     Remove NVMe-oF gateway configuration
@@ -245,7 +245,7 @@ class AnnotatedDataTextOutputFormatter(OutputFormatter):
         return self._convert_to_text_output(data, model)
 
 
-class NvmeofCLICommand(CLICommand):
+class NvmeofCLICommand(DBCLICommand):
     desc: str
 
     def __init__(self, prefix, model: Type[NamedTuple], alias=None, perm='rw', poll=False):
index 29ec62d16990580067b64b6f13386b8b66104b3b..a3b38b8bc1b24fdb44e508ed62d3005d08275267 100644 (file)
@@ -11,11 +11,12 @@ import warnings
 from typing import Dict, Optional
 from urllib import parse
 
-from mgr_module import CLIWriteCommand, HandleCommandResult
+from mgr_module import HandleCommandResult
 
 from .. import mgr
 # Saml2 and OAuth2 needed to be recognized by .__subclasses__()
 # pylint: disable=unused-import
+from ..cli import DBCLICommand
 from ..services.auth import AuthType, BaseAuth, OAuth2, Saml2  # noqa
 from ..tools import prepare_url_prefix
 
@@ -94,7 +95,7 @@ def load_sso_db():
     mgr.SSO_DB = SsoDB.load()  # type: ignore
 
 
-@CLIWriteCommand("dashboard sso enable oauth2")
+@DBCLICommand.Write("dashboard sso enable oauth2")
 def enable_sso(_, roles_path: Optional[str] = None):
     mgr.SSO_DB.protocol = AuthType.OAUTH2
     if jmespath and roles_path:
index 41dc300e9bcdc667ac84529f5430dd6b1f82f9f6..f92f25344351de01d8f4fac5ddeb89ff90f7d9be 100644 (file)
@@ -18,6 +18,7 @@ from orchestrator import DaemonDescription, HostSpec, InventoryHost
 from pyfakefs import fake_filesystem
 
 from .. import mgr
+from ..cli import DBCLICommand
 from ..controllers import generate_controller_routes, json_error_page
 from ..controllers._version import APIVersion
 from ..module import Module
@@ -35,6 +36,7 @@ logger = logging.getLogger('tests')
 
 class ModuleTestClass(Module):
     """Dashboard module subclass for testing the module methods."""
+    CLICommand = DBCLICommand
 
     def __init__(self) -> None:
         pass
index b97082cbd47dfdcdb2a39b033880667ce5a09a61..a5f425e6db351440c0d48b5eb38c2142e4f39410 100644 (file)
@@ -216,6 +216,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertEqual(str(ctx.exception), "Role 'test_role' does not exist")
 
     def test_add_role_invalid_scope_perms(self):
+        assert "dashboard ac-role-create" in self._dashboard_module.CLICommand.COMMANDS
         self.test_create_role()
 
         with self.assertRaises(CmdException) as ctx:
index d0cb237c4736100f001c3788d5b8c60b09cd0d6d..99115a039a403a165d859d93fc92d396e380b917 100644 (file)
@@ -1,11 +1,12 @@
 import errno
 import json
+import logging
 import unittest
 from typing import Annotated, List, NamedTuple
 from unittest.mock import MagicMock
 
 import pytest
-from mgr_module import CLICommand, HandleCommandResult
+from mgr_module import CLICommandBase, HandleCommandResult
 
 from ..controllers import EndpointDoc
 from ..model.nvmeof import CliFieldTransformer, CliFlags, CliHeader
@@ -35,7 +36,7 @@ def fixture_base_call_mock(monkeypatch):
     mock_result = {'a': 'b'}
     super_mock = MagicMock()
     super_mock.return_value = mock_result
-    monkeypatch.setattr(CLICommand, 'call', super_mock)
+    monkeypatch.setattr(CLICommandBase, 'call', super_mock)
     return super_mock
 
 
@@ -44,7 +45,7 @@ def fixture_base_call_return_none_mock(monkeypatch):
     mock_result = None
     super_mock = MagicMock()
     super_mock.return_value = mock_result
-    monkeypatch.setattr(CLICommand, 'call', super_mock)
+    monkeypatch.setattr(CLICommandBase, 'call', super_mock)
     return super_mock
 
 
@@ -55,6 +56,7 @@ class TestNvmeofCLICommand:
 
     def test_command_return_cmd_result_default_format(self, base_call_mock, sample_command):
         result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {})
+        logging.getLogger().error(result)
         assert isinstance(result, HandleCommandResult)
         assert result.retval == 0
         assert result.stdout == (
index 1a15531493c655b77c21c9df9b8694b013f1e1ff..aca229dc1a389f676198f1cf9c20f6fc46a561b8 100644 (file)
@@ -667,55 +667,6 @@ class Option(Dict):
             if k != 'self' and v is not None)
 
 
-class Command(dict):
-    """
-    Helper class to declare options for COMMANDS list.
-
-    It also allows to specify prefix and args separately, as well as storing a
-    handler callable.
-
-    Usage:
-    >>> def handler(): return 0, "", ""
-    >>> Command(prefix="example",
-    ...         handler=handler,
-    ...         perm='w')
-    {'perm': 'w', 'poll': False}
-    """
-
-    def __init__(
-            self,
-            prefix: str,
-            handler: HandlerFuncType,
-            perm: str = "rw",
-            poll: bool = False,
-    ):
-        super().__init__(perm=perm,
-                         poll=poll)
-        self.prefix = prefix
-        self.handler = handler
-
-    @staticmethod
-    def returns_command_result(instance: Any,
-                               f: HandlerFuncType) -> Callable[..., HandleCommandResult]:
-        @functools.wraps(f)
-        def wrapper(mgr: Any, *args: Any, **kwargs: Any) -> HandleCommandResult:
-            retval, stdout, stderr = f(instance or mgr, *args, **kwargs)
-            return HandleCommandResult(retval, stdout, stderr)
-        wrapper.__signature__ = inspect.signature(f)  # type: ignore[attr-defined]
-        return wrapper
-
-    def register(self, instance: bool = False) -> HandlerFuncType:
-        """
-        Register a CLICommand handler. It allows an instance to register bound
-        methods. In that case, the mgr instance is not passed, and it's expected
-        to be available in the class instance.
-        It also uses HandleCommandResult helper to return a wrapped a tuple of 3
-        items.
-        """
-        cmd = CLICommand(prefix=self.prefix, perm=self['perm'])
-        return cmd(self.returns_command_result(instance, self.handler))
-
-
 class CPlusPlusHandler(logging.Handler):
     def __init__(self, module_inst: Any):
         super(CPlusPlusHandler, self).__init__()