5 from typing
import Any
, Dict
, List
, Union
7 from docutils
.nodes
import Node
8 from docutils
.parsers
.rst
import directives
9 from docutils
.statemachine
import StringList
11 from sphinx
import addnodes
12 from sphinx
.directives
import ObjectDescription
13 from sphinx
.domains
.python
import PyField
14 from sphinx
.environment
import BuildEnvironment
15 from sphinx
.locale
import _
16 from sphinx
.util
import logging
, status_iterator
, ws_re
17 from sphinx
.util
.docutils
import switch_source_input
, SphinxDirective
18 from sphinx
.util
.docfields
import Field
19 from sphinx
.util
.nodes
import make_id
24 logger
= logging
.getLogger(__name__
)
29 {{ desc | wordwrap(70) | indent(3) }}
31 :type: ``{{opt.type}}``
32 {%- if default is not none %}
33 {%- if opt.type == 'size' %}
34 :default: ``{{ default | eval_size | iec_size }}``
35 {%- elif opt.type == 'secs' %}
36 :default: ``{{ default | readable_duration(opt.type) }}``
37 {%- elif opt.type in ('uint', 'int', 'float') %}
38 :default: ``{{ default | readable_num(opt.type) }}``
39 {%- elif opt.type == 'millisecs' %}
40 :default: ``{{ default }}`` milliseconds
41 {%- elif opt.type == 'bool' %}
42 :default: ``{{ default | string | lower }}``
44 :default: {{ default | literal }}
47 {%- if opt.enum_values %}
48 :valid choices:{% for enum_value in opt.enum_values -%}
49 {{" -" | indent(18, not loop.first) }} {{ enum_value | literal }}
52 {%- if opt.min is defined and opt.max is defined %}
53 :allowed range: ``[{{ opt.min }}, {{ opt.max }}]``
54 {%- elif opt.min is defined %}
55 :min: ``{{ opt.min }}``
56 {%- elif opt.max is defined %}
57 :max: ``{{ opt.max }}``
59 {%- if opt.constraint %}
60 :constraint: {{ opt.constraint }}
62 {%- if opt.policies %}
63 :policies: {{ opt.policies }}
66 :example: {{ opt.example }}
68 {%- if opt.see_also %}
69 :see also: {{ opt.see_also | map('ref_confval') | join(', ') }}
82 def eval_size(value
) -> int:
86 times
= dict(_K
=1 << 10,
90 for unit
, m
in times
.items():
91 if value
.endswith(unit
):
92 return int(value
[:-len(unit
)]) * m
93 raise ValueError(f
'unknown value: {value}')
96 def readable_duration(value
: str, typ
: str) -> str:
100 postfix
= 'second' if v
== 1 else 'seconds'
101 return f
'{v} {postfix}'
103 return str(float(value
))
105 return str(int(value
))
107 times
= dict(_min
=['minute', 'minutes'],
108 _hr
=['hour', 'hours'],
109 _day
=['day', 'days'])
110 for unit
, readables
in times
.items():
111 if value
.endswith(unit
):
112 v
= int(value
[:-len(unit
)])
113 postfix
= readables
[0 if v
== 1 else 1]
114 return f
'{v} {postfix}'
115 raise ValueError(f
'unknown value: {value}')
118 def do_plain_num(value
: str, typ
: str) -> str:
120 return str(float(value
))
122 return str(int(value
))
125 def iec_size(value
: int) -> str:
135 for unit
, bits
in units
.items():
139 return f
'{value}{unit}'
140 raise Exception(f
'iec_size() failed to convert {value}')
143 def do_fileize_num(value
: str, typ
: str) -> str:
148 def readable_num(value
: str, typ
: str) -> str:
150 for eval_func
in [do_plain_num
,
154 return eval_func(value
, typ
)
155 except ValueError as ex
:
160 def literal(name
) -> str:
164 return f
'<empty string>'
167 def ref_confval(name
) -> str:
168 return f
':confval:`{name}`'
171 def jinja_template() -> jinja2
.Template
:
172 env
= jinja2
.Environment()
173 env
.filters
['eval_size'] = eval_size
174 env
.filters
['iec_size'] = iec_size
175 env
.filters
['readable_duration'] = readable_duration
176 env
.filters
['readable_num'] = readable_num
177 env
.filters
['literal'] = literal
178 env
.filters
['ref_confval'] = ref_confval
179 return env
.from_string(TEMPLATE
)
182 FieldValueT
= Union
[bool, float, int, str]
185 class CephModule(SphinxDirective
):
187 Directive to name the mgr module for which options are documented.
190 required_arguments
= 1
191 optional_arguments
= 0
192 final_argument_whitespace
= False
194 def run(self
) -> List
[Node
]:
195 module
= self
.arguments
[0].strip()
197 self
.env
.ref_context
.pop('ceph:module', None)
199 self
.env
.ref_context
['ceph:module'] = module
203 class CephOption(ObjectDescription
):
205 emit option loaded from given command/options/<name>.yaml.in file
208 required_arguments
= 1
209 optional_arguments
= 0
210 final_argument_whitespace
= False
212 'module': directives
.unchanged
,
213 'default': directives
.unchanged
226 bodyrolename
='class'),
229 template
= jinja_template()
230 opts
: Dict
[str, Dict
[str, FieldValueT
]] = {}
231 mgr_opts
: Dict
[str, # module name
232 Dict
[str, # option name
233 Dict
[str, # field_name
236 def _load_yaml(self
) -> Dict
[str, Dict
[str, FieldValueT
]]:
238 return CephOption
.opts
240 for fn
in status_iterator(self
.config
.ceph_confval_imports
,
241 'loading options...', 'red',
242 len(self
.config
.ceph_confval_imports
),
243 self
.env
.app
.verbosity
):
244 self
.env
.note_dependency(fn
)
246 with
open(fn
, 'r') as f
:
247 yaml_in
= io
.StringIO()
252 opts
+= yaml
.safe_load(yaml_in
)['options']
254 message
= f
'Unable to open option file "{fn}": {e}'
255 raise self
.error(message
)
256 CephOption
.opts
= dict((opt
['name'], opt
) for opt
in opts
)
257 return CephOption
.opts
259 def _normalize_path(self
, dirname
):
260 my_dir
= os
.path
.dirname(os
.path
.realpath(__file__
))
261 src_dir
= os
.path
.abspath(os
.path
.join(my_dir
, '../..'))
262 return os
.path
.join(src_dir
, dirname
)
264 def _is_mgr_module(self
, dirname
, name
):
265 if not os
.path
.isdir(os
.path
.join(dirname
, name
)):
267 if not os
.path
.isfile(os
.path
.join(dirname
, name
, '__init__.py')):
269 return name
not in ['tests']
271 @contextlib.contextmanager
272 def mocked_modules(self
):
273 # src/pybind/mgr/tests
274 from tests
import mock
275 mock_imports
= ['rados',
280 # make dashboard happy
281 mock_imports
+= ['OpenSSL',
286 'rook.rook_client.ceph',
287 'rook.rook_client._helper',
289 # make diskprediction_local happy
290 mock_imports
+= ['numpy',
293 mock_imports
+= ['pecan',
299 for m
in mock_imports
:
301 parts
= m
.split('=', 1)
304 args
['__version__'] = parts
[1]
305 sys
.modules
[mocked
] = mock
.Mock(**args
)
310 for m
in mock_imports
:
311 mocked
= m
.split('=', 1)[0]
312 sys
.modules
.pop(mocked
)
314 def _collect_options_from_module(self
, name
):
315 with self
.mocked_modules():
316 mgr_mod
= __import__(name
, globals(), locals(), [], 0)
317 # import 'M' from src/pybind/mgr/tests
322 return issubclass(x
, M
)
325 ms
= [c
for c
in mgr_mod
.__dict
__.values()
326 if subclass(c
) and 'Standby' not in c
.__name
__]
328 assert isinstance(m
.MODULE_OPTIONS
, list)
329 return m
.MODULE_OPTIONS
331 def _load_module(self
, module
) -> Dict
[str, Dict
[str, FieldValueT
]]:
332 mgr_opts
= CephOption
.mgr_opts
.get(module
)
333 if mgr_opts
is not None:
335 python_path
= self
.config
.ceph_confval_mgr_python_path
336 for path
in python_path
.split(':'):
337 sys
.path
.insert(0, self
._normalize
_path
(path
))
338 module_path
= self
.env
.config
.ceph_confval_mgr_module_path
339 module_path
= self
._normalize
_path
(module_path
)
340 sys
.path
.insert(0, module_path
)
341 if not self
._is
_mgr
_module
(module_path
, module
):
342 raise self
.error(f
'module "{module}" not found under {module_path}')
343 fn
= os
.path
.join(module_path
, module
, 'module.py')
344 if os
.path
.exists(fn
):
345 self
.env
.note_dependency(fn
)
346 os
.environ
['UNITTEST'] = 'true'
347 opts
= self
._collect
_options
_from
_module
(module
)
348 CephOption
.mgr_opts
[module
] = dict((opt
['name'], opt
) for opt
in opts
)
349 return CephOption
.mgr_opts
[module
]
351 def _current_module(self
) -> str:
352 return self
.options
.get('module',
353 self
.env
.ref_context
.get('ceph:module'))
355 def _render_option(self
, name
) -> str:
356 cur_module
= self
._current
_module
()
359 opt
= self
._load
_module
(cur_module
).get(name
)
360 except Exception as e
:
361 message
= f
'Unable to load module "{cur_module}": {e}'
362 raise self
.error(message
)
364 opt
= self
._load
_yaml
().get(name
)
366 raise self
.error(f
'Option "{name}" not found!')
367 if cur_module
and 'type' not in opt
:
368 # the type of module option defaults to 'str'
370 desc
= opt
.get('fmt_desc') or opt
.get('long_desc') or opt
.get('desc')
371 opt_default
= opt
.get('default')
372 default
= self
.options
.get('default', opt_default
)
374 return self
.template
.render(opt
=opt
,
377 except Exception as e
:
378 message
= (f
'Unable to render option "{name}": {e}. ',
379 f
'opt={opt}, desc={desc}, default={default}')
380 raise self
.error(message
)
382 def handle_signature(self
,
384 signode
: addnodes
.desc_signature
) -> str:
386 signode
+= addnodes
.desc_name(sig
, sig
)
387 # normalize whitespace like XRefRole does
388 name
= ws_re
.sub(' ', sig
)
389 cur_module
= self
._current
_module
()
391 return '/'.join(['mgr', cur_module
, name
])
395 def transform_content(self
, contentnode
: addnodes
.desc_content
) -> None:
396 name
= self
.arguments
[0]
397 source
, lineno
= self
.get_source_info()
398 source
= f
'{source}:{lineno}:<confval>'
399 fields
= StringList(self
._render
_option
(name
).splitlines() + [''],
400 source
=source
, parent_offset
=lineno
)
401 with
switch_source_input(self
.state
, fields
):
402 self
.state
.nested_parse(fields
, 0, contentnode
)
404 def add_target_and_index(self
,
407 signode
: addnodes
.desc_signature
) -> None:
408 node_id
= make_id(self
.env
, self
.state
.document
, self
.objtype
, name
)
409 signode
['ids'].append(node_id
)
410 self
.state
.document
.note_explicit_target(signode
)
411 entry
= f
'{name}; configuration option'
412 self
.indexnode
['entries'].append(('pair', entry
, node_id
, '', None))
413 std
= self
.env
.get_domain('std')
414 std
.note_object(self
.objtype
, name
, node_id
, location
=signode
)
417 def _reset_ref_context(app
, env
, docname
):
418 env
.ref_context
.pop('ceph:module', None)
421 def setup(app
) -> Dict
[str, Any
]:
422 app
.add_config_value('ceph_confval_imports',
426 app
.add_config_value('ceph_confval_mgr_module_path',
430 app
.add_config_value('ceph_confval_mgr_python_path',
437 objname
='configuration section',
438 indextemplate
='pair: %s; configuration section',
449 objname
='configuration option',
451 app
.add_directive_to_domain('std', 'mgr_module', CephModule
)
452 app
.add_directive_to_domain('std', 'confval', CephOption
, override
=True)
453 app
.connect('env-purge-doc', _reset_ref_context
)
456 'version': 'builtin',
457 'parallel_read_safe': True,
458 'parallel_write_safe': True,