]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
pybind/mgr/mgr_module: support per-module CLICommand instances and globals
authorSamuel Just <sjust@redhat.com>
Mon, 24 Nov 2025 17:27:24 +0000 (09:27 -0800)
committerSamuel Just <sjust@redhat.com>
Wed, 17 Dec 2025 17:40:29 +0000 (17:40 +0000)
Otherwise, the class members on MgrModule and CLICommand are global to all
modules in the same interpreter.

Following commits will introduce a per-module CLICommand types for each
module.

Signed-off-by: Samuel Just <sjust@redhat.com>
src/pybind/mgr/mgr_module.py

index 8549e15d8e240d6b624aff0c695d29dd92975f2b..1a15531493c655b77c21c9df9b8694b013f1e1ff 100644 (file)
@@ -9,6 +9,7 @@ from typing import (
     List,
     Mapping,
     NamedTuple,
+    no_type_check,
     Optional,
     Sequence,
     Set,
@@ -407,9 +408,7 @@ def _extract_target_func(
     return f, extra_args
 
 
-class CLICommand(object):
-    COMMANDS = {}  # type: Dict[str, CLICommand]
-
+class CLICommandBase(object):
     def __init__(self,
                  prefix: str,
                  perm: str = 'rw',
@@ -475,8 +474,8 @@ class CLICommand(object):
 
     def _register_handler(self, func: HandlerFuncType) -> HandlerFuncType:
         self.store_func_metadata(func)
+        self.COMMANDS[self.prefix] = self  # type: ignore[attr-defined]
         self.func = func
-        self.COMMANDS[self.prefix] = self
         return self.func
 
     def __call__(self, func: HandlerFuncType) -> HandlerFuncType:
@@ -504,7 +503,7 @@ class CLICommand(object):
         special_args = set()
         kwargs_switch = False
         for index, (name, tp) in enumerate(self.arg_spec.items()):
-            if name in CLICommand.KNOWN_ARGS:
+            if name in self.KNOWN_ARGS:
                 special_args.add(name)
                 continue
             assert self.first_default >= 0
@@ -542,16 +541,23 @@ class CLICommand(object):
         }
 
     @classmethod
-    def dump_cmd_list(cls) -> List[Dict[str, Union[str, bool]]]:
-        return [cmd.dump_cmd() for cmd in cls.COMMANDS.values()]
-
+    def Read(cls, prefix: str, poll: bool = False) -> 'CLICommandBase':
+        return cls(prefix, "r", poll)
 
-def CLIReadCommand(prefix: str, poll: bool = False) -> CLICommand:
-    return CLICommand(prefix, "r", poll)
+    @classmethod
+    def Write(cls, prefix: str, poll: bool = False) -> 'CLICommandBase':
+        return cls(prefix, "w", poll)
 
+    @classmethod
+    def dump_cmd_list(cls) -> List[Dict[str, Union[str, bool]]]:
+        return [cmd.dump_cmd() for cmd in cls.COMMANDS.values()]  # type: ignore[attr-defined]
 
-def CLIWriteCommand(prefix: str, poll: bool = False) -> CLICommand:
-    return CLICommand(prefix, "w", poll)
+    @classmethod
+    @no_type_check
+    def make_registry_subtype(cls, name) -> 'CLICommandBase':
+        return type(name, (cls,), {
+            'COMMANDS': {},
+        })
 
 
 def CLICheckNonemptyFileInput(desc: str) -> Callable[[HandlerFuncType], HandlerFuncType]:
@@ -888,21 +894,10 @@ class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
     from their active peer), and to configuration settings (read only).
     """
 
-    MODULE_OPTIONS: List[Option] = []
-    MODULE_OPTION_DEFAULTS = {}  # type: Dict[str, Any]
-
     def __init__(self, module_name: str, capsule: Any):
         super(MgrStandbyModule, self).__init__(capsule)
         self.module_name = module_name
 
-        # see also MgrModule.__init__()
-        for o in self.MODULE_OPTIONS:
-            if 'default' in o:
-                if 'type' in o:
-                    self.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
-                else:
-                    self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
-
         # mock does not return a str
         mgr_level = cast(str, self.get_ceph_option("debug_mgr"))
         log_level = cast(str, self.get_module_option("log_level"))
@@ -914,7 +909,14 @@ class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
         self._logger = self.getLogger()
 
     @classmethod
+    @no_type_check
     def _register_options(cls, module_name: str) -> None:
+        if not hasattr(cls, 'MODULE_OPTIONS'):
+            cls.MODULE_OPTIONS = []
+
+        if not hasattr(cls, 'MODULE_OPTION_DEFAULTS'):
+            cls.MODULE_OPTION_DEFAULTS = {}
+
         cls.MODULE_OPTIONS.append(
             Option(name='log_level', type='str', default="", runtime=True,
                    enum_allowed=['info', 'debug', 'critical', 'error',
@@ -933,6 +935,13 @@ class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
                    enum_allowed=['info', 'debug', 'critical', 'error',
                                  'warning', '']))
 
+        for o in cls.MODULE_OPTIONS:
+            if 'default' in o:
+                if 'type' in o:
+                    cls.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
+                else:
+                    cls.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
+
     @property
     def log(self) -> logging.Logger:
         return self._logger
@@ -955,7 +964,7 @@ class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
         """
         r = self._ceph_get_module_option(key)
         if r is None:
-            return self.MODULE_OPTION_DEFAULTS.get(key, default)
+            return self.MODULE_OPTION_DEFAULTS.get(key, default)  # type: ignore[attr-defined]
         else:
             return r
 
@@ -997,7 +1006,7 @@ class MgrStandbyModule(ceph_module.BaseMgrStandbyModule, MgrModuleLoggingMixin):
     def get_localized_module_option(self, key: str, default: OptionValue = None) -> OptionValue:
         r = self._ceph_get_module_option(key, self.get_mgr_id())
         if r is None:
-            return self.MODULE_OPTION_DEFAULTS.get(key, default)
+            return self.MODULE_OPTION_DEFAULTS.get(key, default)  # type: ignore[attr-defined]
         else:
             return r
 
@@ -1037,14 +1046,6 @@ class API:
 class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
     MGR_POOL_NAME = ".mgr"
 
-    COMMANDS = []  # type: List[Any]
-    MODULE_OPTIONS: List[Option] = []
-    MODULE_OPTION_DEFAULTS = {}  # type: Dict[str, Any]
-
-    # Database Schema
-    SCHEMA = None  # type: Optional[List[str]]
-    SCHEMA_VERSIONED = None  # type: Optional[List[List[str]]]
-
     # Priority definitions for perf counters
     PRIO_CRITICAL = 10
     PRIO_INTERESTING = 8
@@ -1078,18 +1079,6 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
         self.module_name = module_name
         super(MgrModule, self).__init__(py_modules_ptr, this_ptr)
 
-        for o in self.MODULE_OPTIONS:
-            if 'default' in o:
-                if 'type' in o:
-                    # we'll assume the declared type matches the
-                    # supplied default value's type.
-                    self.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
-                else:
-                    # module not declaring it's type, so normalize the
-                    # default value to be a string for consistent behavior
-                    # with default and user-supplied option values.
-                    self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
-
         mgr_level = cast(str, self.get_ceph_option("debug_mgr"))
         log_level = cast(str, self.get_module_option("log_level"))
         cluster_level = cast(str, self.get_module_option('log_to_cluster_level'))
@@ -1120,7 +1109,14 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
         self._db_lock = threading.Lock()
 
     @classmethod
+    @no_type_check
     def _register_options(cls, module_name: str) -> None:
+        if not hasattr(cls, 'MODULE_OPTIONS'):
+            cls.MODULE_OPTIONS = []
+
+        if not hasattr(cls, 'MODULE_OPTION_DEFAULTS'):
+            cls.MODULE_OPTION_DEFAULTS = {}
+
         cls.MODULE_OPTIONS.append(
             Option(name='log_level', type='str', default="", runtime=True,
                    enum_allowed=['info', 'debug', 'critical', 'error',
@@ -1139,9 +1135,25 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
                    enum_allowed=['info', 'debug', 'critical', 'error',
                                  'warning', '']))
 
+        for o in cls.MODULE_OPTIONS:
+            if 'default' in o:
+                if 'type' in o:
+                    # we'll assume the declared type matches the
+                    # supplied default value's type.
+                    cls.MODULE_OPTION_DEFAULTS[o['name']] = o['default']
+                else:
+                    # module not declaring it's type, so normalize the
+                    # default value to be a string for consistent behavior
+                    # with default and user-supplied option values.
+                    cls.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default'])
+
     @classmethod
+    @no_type_check
     def _register_commands(cls, module_name: str) -> None:
-        cls.COMMANDS.extend(CLICommand.dump_cmd_list())
+        if not hasattr(cls, 'COMMANDS'):
+            cls.COMMANDS = []
+
+        cls.COMMANDS.extend(cls.CLICommand.dump_cmd_list())
 
     @property
     def log(self) -> logging.Logger:
@@ -1280,12 +1292,14 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
 
     def maybe_upgrade(self, db: sqlite3.Connection, version: int) -> None:
         if version <= 0:
+            assert hasattr(self, 'SCHEMA')
             self.log.info(f"creating main.db for {self.module_name}")
             assert self.SCHEMA is not None
             for sql in self.SCHEMA:
                 db.execute(sql)
             self.update_schema_version(db, 1)
         else:
+            assert hasattr(self, 'SCHEMA_VERSIONED')
             assert self.SCHEMA_VERSIONED is not None
             latest = len(self.SCHEMA_VERSIONED)
             if latest < version:
@@ -2004,10 +2018,10 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
                         inbuf: str,
                         cmd: Dict[str, Any]) -> Union[HandleCommandResult,
                                                       Tuple[int, str, str]]:
-        if cmd['prefix'] not in CLICommand.COMMANDS:
+        if cmd['prefix'] not in self.CLICommand.COMMANDS:  # type: ignore[attr-defined]
             return self.handle_command(inbuf, cmd)
 
-        return CLICommand.COMMANDS[cmd['prefix']].call(self, cmd, inbuf)
+        return self.CLICommand.COMMANDS[cmd['prefix']].call(self, cmd, inbuf)  # type: ignore[attr-defined]
 
     def handle_command(self,
                        inbuf: str,
@@ -2074,7 +2088,7 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
         access config options that they didn't declare
         in their schema.
         """
-        if key not in [o['name'] for o in self.MODULE_OPTIONS]:
+        if key not in [o['name'] for o in self.MODULE_OPTIONS]:  # type: ignore[attr-defined]
             raise RuntimeError("Config option '{0}' is not in {1}.MODULE_OPTIONS".
                                format(key, self.__class__.__name__))
 
@@ -2085,7 +2099,7 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
         r = self._ceph_get_module_option(self.module_name, key,
                                          localized_prefix)
         if r is None:
-            return self.MODULE_OPTION_DEFAULTS.get(key, default)
+            return self.MODULE_OPTION_DEFAULTS.get(key, default)  # type: ignore[attr-defined]
         else:
             return r