]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
doc/_ext: load options defined by mgr modules
authorKefu Chai <kchai@redhat.com>
Thu, 6 May 2021 04:51:23 +0000 (12:51 +0800)
committerKefu Chai <kchai@redhat.com>
Thu, 6 May 2021 12:54:42 +0000 (20:54 +0800)
Signed-off-by: Kefu Chai <kchai@redhat.com>
doc/_ext/ceph_confval.py
doc/conf.py
doc/mgr/localpool.rst
src/pybind/mgr/localpool/module.py

index e7b669b88f6bdd323c04faab6b32106b78d654e7..2d02cd8f73b73d1d5c9daec634cabfeeeccaffa7 100644 (file)
@@ -1,19 +1,23 @@
 import io
+import contextlib
+import os
+import sys
 from typing import Any, Dict, List, Union
 
+from docutils.nodes import Node
 from docutils.parsers.rst import directives
 from docutils.parsers.rst import Directive
-
+from sphinx import addnodes
 from sphinx.domains.python import PyField
+from sphinx.environment import BuildEnvironment
 from sphinx.locale import _
-from sphinx.util import logging, status_iterator
+from sphinx.util import logging, status_iterator, ws_re
 from sphinx.util.docfields import Field
 
 import jinja2
 import jinja2.filters
 import yaml
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -167,6 +171,25 @@ def jinja_template() -> jinja2.Template:
 FieldValueT = Union[bool, float, int, str]
 
 
+class CephModule(Directive):
+    """
+    Directive to name the mgr module for which options are documented.
+    """
+    has_content = False
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = False
+
+    def run(self) -> List[Node]:
+        module = self.arguments[0].strip()
+        env = self.state.document.settings.env
+        if module == 'None':
+            env.ref_context.pop('ceph:module', None)
+        else:
+            env.ref_context['ceph:module'] = module
+        return []
+
+
 class CephOption(Directive):
     """
     emit option loaded from given command/options/<name>.yaml.in file
@@ -179,6 +202,10 @@ class CephOption(Directive):
 
     template = jinja_template()
     opts: Dict[str, Dict[str, FieldValueT]] = {}
+    mgr_opts: Dict[str,            # module name
+                   Dict[str,       # option name
+                        Dict[str,  # field_name
+                             FieldValueT]]] = {}
 
     def _load_yaml(self) -> Dict[str, Dict[str, FieldValueT]]:
         if CephOption.opts:
@@ -204,9 +231,113 @@ class CephOption(Directive):
         CephOption.opts = dict((opt['name'], opt) for opt in opts)
         return CephOption.opts
 
+    def _normalize_path(self, dirname):
+        my_dir = os.path.dirname(os.path.realpath(__file__))
+        src_dir = os.path.abspath(os.path.join(my_dir, '../..'))
+        return os.path.join(src_dir, dirname)
+
+    def _is_mgr_module(self, dirname, name):
+        if not os.path.isdir(os.path.join(dirname, name)):
+            return False
+        if not os.path.isfile(os.path.join(dirname, name, '__init__.py')):
+            return False
+        return name not in ['tests']
+
+    @contextlib.contextmanager
+    def mocked_modules(self):
+        # src/pybind/mgr/tests
+        from tests import mock
+        mock_imports = ['rados',
+                        'rbd',
+                        'cephfs',
+                        'dateutil',
+                        'dateutil.parser']
+        # make dashboard happy
+        mock_imports += ['OpenSSL',
+                         'jwt',
+                         'bcrypt',
+                         'jsonpatch',
+                         'rook.rook_client',
+                         'rook.rook_client.ceph',
+                         'rook.rook_client._helper',
+                         'cherrypy=3.2.3']
+        # make diskprediction_local happy
+        mock_imports += ['numpy',
+                         'scipy']
+        # make restful happy
+        mock_imports += ['pecan',
+                         'pecan.rest',
+                         'pecan.hooks',
+                         'werkzeug',
+                         'werkzeug.serving']
+
+        for m in mock_imports:
+            args = {}
+            parts = m.split('=', 1)
+            mocked = parts[0]
+            if len(parts) > 1:
+                args['__version__'] = parts[1]
+            sys.modules[mocked] = mock.Mock(**args)
+
+        try:
+            yield
+        finally:
+            for m in mock_imports:
+                mocked = m.split('=', 1)[0]
+                sys.modules.pop(mocked)
+
+    def _collect_options_from_module(self, name):
+        with self.mocked_modules():
+            mgr_mod = __import__(name, globals(), locals(), [], 0)
+            # import 'M' from src/pybind/mgr/tests
+            from tests import M
+
+            def subclass(x):
+                try:
+                    return issubclass(x, M)
+                except TypeError:
+                    return False
+            ms = [c for c in mgr_mod.__dict__.values()
+                  if subclass(c) and 'Standby' not in c.__name__]
+            [m] = ms
+            assert isinstance(m.MODULE_OPTIONS, list)
+            return m.MODULE_OPTIONS
+
+    def _load_module(self, module) -> Dict[str, Dict[str, FieldValueT]]:
+        mgr_opts = CephOption.mgr_opts.get(module)
+        if mgr_opts is not None:
+            return mgr_opts
+        env = self.state.document.settings.env
+        python_path = env.config.ceph_confval_mgr_python_path
+        for path in python_path.split(':'):
+            sys.path.insert(0, self._normalize_path(path))
+        module_path = env.config.ceph_confval_mgr_module_path
+        module_path = self._normalize_path(module_path)
+        sys.path.insert(0, module_path)
+        os.environ['UNITTEST'] = 'true'
+
+        modules = [name for name in os.listdir(module_path)
+                   if self._is_mgr_module(module_path, name)]
+        opts = []
+        for module in status_iterator(modules,
+                                      'loading module...', 'darkgreen',
+                                      len(modules),
+                                      env.app.verbosity):
+            fn = os.path.join(module_path, module, 'module.py')
+            if os.path.exists(fn):
+                env.note_dependency(fn)
+            opts += self._collect_options_from_module(module)
+        CephOption.mgr_opts[module] = dict((opt['name'], opt) for opt in opts)
+        return CephOption.mgr_opts[module]
+
     def run(self) -> List[Any]:
         name = self.arguments[0]
-        opt = self._load_yaml().get(name)
+        env = self.state.document.settings.env
+        cur_module = env.ref_context.get('ceph:module')
+        if cur_module:
+            opt = self._load_module(cur_module).get(name)
+        else:
+            opt = self._load_yaml().get(name)
         if opt is None:
             raise self.error(f'Option "{name}" not found!')
         desc = opt.get('fmt_desc') or opt.get('long_desc') or opt.get('desc')
@@ -232,12 +363,22 @@ def setup(app) -> Dict[str, Any]:
                          default=[],
                          rebuild='html',
                          types=[str])
+    app.add_config_value('ceph_confval_mgr_module_path',
+                         default=[],
+                         rebuild='html',
+                         types=[str])
+    app.add_config_value('ceph_confval_mgr_python_path',
+                         default=[],
+                         rebuild='',
+                         types=[str])
     app.add_directive('confval', CephOption)
+    app.add_directive('mgr_module', CephModule)
     app.add_object_type(
         'confval_option',
         'confval',
         objname='configuration value',
         indextemplate='pair: %s; configuration value',
+        parse_node=_parse_option_desc,
         doc_field_types=[
             PyField(
                 'type',
index 93df49cd70d25030a7b4a7112fe4d5d4c2bf4edd..f1123da387b1557a1abaf5f94ac2ba8e6ffb1452 100644 (file)
@@ -231,6 +231,8 @@ openapi_logger.setLevel(logging.WARNING)
 ceph_confval_imports = glob.glob(os.path.join(top_level,
                                               'src/common/options',
                                               '*.yaml.in'))
+ceph_confval_mgr_module_path = 'src/pybind/mgr'
+ceph_confval_mgr_python_path = 'src/pybind'
 
 # handles edit-on-github and old version warning display
 def setup(app):
index fe8bd3942a9ad2f5b5767eb1100861181e9919e3..13b749071dcf52723fc0a9ce58303996f0f8df27 100644 (file)
@@ -1,6 +1,8 @@
 Local Pool Module
 =================
 
+.. mgr_module:: localpool
+
 The *localpool* module can automatically create RADOS pools that are
 localized to a subset of the overall cluster.  For example, by default, it will
 create a pool for each distinct ``rack`` in the cluster.  This can be useful for
@@ -20,15 +22,13 @@ Configuring
 
 The *localpool* module understands the following options:
 
-* **subtree** (default: `rack`): which CRUSH subtree type the module
-  should create a pool for.
-* **failure_domain** (default: `host`): what failure domain we should
-  separate data replicas across.
-* **pg_num** (default: `128`): number of PGs to create for each pool
-* **num_rep** (default: `3`): number of replicas for each pool.
-  (Currently, pools are always replicated.)
-* **min_size** (default: none): value to set min_size to (unchanged from Ceph's default if this option is not set)
-* **prefix** (default: `by-$subtreetype-`): prefix for the pool name.
+.. confval:: subtree
+.. confval:: failure_domain
+.. confval:: pg_num
+.. confval:: num_rep
+.. confval:: min_size
+.. confval:: prefix
+   :default: by-$subtreetype-
 
 These options are set via the config-key interface.  For example, to
 change the replication level to 2x with only 64 PGs, ::
index 05ba4829b4562c9366635ce1a3b6d392069b756f..ce7cb1af2a6ea8c4cdaf05f55ebeedafb336e97b 100644 (file)
@@ -12,17 +12,20 @@ class Module(MgrModule):
             type='str',
             default='rack',
             desc='CRUSH level for which to create a local pool',
+            long_desc='which CRUSH subtree type the module should create a pool for.',
             runtime=True),
         Option(
             name='failure_domain',
             type='str',
             default='host',
             desc='failure domain for any created local pool',
+            long_desc='what failure domain we should separate data replicas across.',
             runtime=True),
         Option(
             name='min_size',
             type='int',
             desc='default min_size for any created local pool',
+            long_desc='value to set min_size to (unchanged from Ceph\'s default if this option is not set)',
             runtime=True),
         Option(
             name='num_rep',