From 98dc0d4e854437433b12963a8be95d5361a49618 Mon Sep 17 00:00:00 2001 From: Kefu Chai Date: Thu, 6 May 2021 12:51:23 +0800 Subject: [PATCH] doc/_ext: load options defined by mgr modules Signed-off-by: Kefu Chai --- doc/_ext/ceph_confval.py | 149 ++++++++++++++++++++++++++++- doc/conf.py | 2 + doc/mgr/localpool.rst | 18 ++-- src/pybind/mgr/localpool/module.py | 3 + 4 files changed, 159 insertions(+), 13 deletions(-) diff --git a/doc/_ext/ceph_confval.py b/doc/_ext/ceph_confval.py index e7b669b88f6..2d02cd8f73b 100644 --- a/doc/_ext/ceph_confval.py +++ b/doc/_ext/ceph_confval.py @@ -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/.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', diff --git a/doc/conf.py b/doc/conf.py index 93df49cd70d..f1123da387b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -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): diff --git a/doc/mgr/localpool.rst b/doc/mgr/localpool.rst index fe8bd3942a9..13b749071dc 100644 --- a/doc/mgr/localpool.rst +++ b/doc/mgr/localpool.rst @@ -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, :: diff --git a/src/pybind/mgr/localpool/module.py b/src/pybind/mgr/localpool/module.py index 05ba4829b45..ce7cb1af2a6 100644 --- a/src/pybind/mgr/localpool/module.py +++ b/src/pybind/mgr/localpool/module.py @@ -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', -- 2.39.5