2021-04-16 05:51:23 +00:00
|
|
|
import io
|
|
|
|
from typing import Any, Dict, List, Union
|
|
|
|
|
|
|
|
from docutils.parsers.rst import directives
|
|
|
|
from docutils.parsers.rst import Directive
|
|
|
|
|
2021-04-14 16:03:10 +00:00
|
|
|
from sphinx.domains.python import PyField
|
|
|
|
from sphinx.locale import _
|
2021-04-16 05:51:23 +00:00
|
|
|
from sphinx.util import logging, status_iterator
|
2021-04-14 16:03:10 +00:00
|
|
|
from sphinx.util.docfields import Field
|
|
|
|
|
2021-04-16 05:51:23 +00:00
|
|
|
import jinja2
|
|
|
|
import jinja2.filters
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
TEMPLATE = '''
|
|
|
|
.. confval_option:: {{ opt.name }}
|
2021-05-02 11:57:27 +00:00
|
|
|
{% if desc %}
|
2021-04-16 05:51:23 +00:00
|
|
|
{{ desc | wordwrap(70) | indent(3) }}
|
|
|
|
{% endif %}
|
|
|
|
:type: ``{{opt.type}}``
|
2021-05-02 10:17:00 +00:00
|
|
|
{%- if default is not none %}
|
2021-04-16 05:51:23 +00:00
|
|
|
{%- if opt.type == 'size' %}
|
2021-04-18 00:41:53 +00:00
|
|
|
:default: ``{{ default | eval_size | iec_size }}``
|
2021-04-16 05:51:23 +00:00
|
|
|
{%- elif opt.type == 'secs' %}
|
|
|
|
:default: ``{{ default | readable_duration(opt.type) }}``
|
|
|
|
{%- elif opt.type in ('uint', 'int', 'float') %}
|
|
|
|
:default: ``{{ default | readable_num(opt.type) }}``
|
|
|
|
{%- elif opt.type == 'millisecs' %}
|
|
|
|
:default: ``{{ default }}`` milliseconds
|
|
|
|
{%- elif opt.type == 'bool' %}
|
|
|
|
:default: ``{{ default | string | lower }}``
|
|
|
|
{%- else %}
|
|
|
|
:default: ``{{ default }}``
|
|
|
|
{%- endif -%}
|
|
|
|
{%- endif %}
|
2021-04-17 14:28:57 +00:00
|
|
|
{%- if opt.enum_values %}
|
|
|
|
:valid choices:{% for enum_value in opt.enum_values -%}
|
|
|
|
{{" -" | indent(18, not loop.first) }} {{ enum_value | literal }}
|
|
|
|
{% endfor %}
|
|
|
|
{%- endif %}
|
|
|
|
{%- if opt.min is defined and opt.max is defined %}
|
|
|
|
:allowed range: ``[{{ opt.min }}, {{ opt.max }}]``
|
|
|
|
{%- elif opt.min is defined %}
|
|
|
|
:min: ``{{ opt.min }}``
|
|
|
|
{%- elif opt.max is defined %}
|
|
|
|
:max: ``{{ opt.max }}``
|
|
|
|
{%- endif %}
|
2021-04-17 16:39:41 +00:00
|
|
|
{%- if opt.see_also %}
|
|
|
|
:see also: {{ opt.see_also | map('ref_confval') | join(', ') }}
|
|
|
|
{%- endif %}
|
2021-04-17 14:28:57 +00:00
|
|
|
{% if opt.note %}
|
|
|
|
.. note::
|
|
|
|
{{ opt.note }}
|
|
|
|
{%- endif -%}
|
|
|
|
{%- if opt.warning %}
|
|
|
|
.. warning::
|
|
|
|
{{ opt.warning }}
|
|
|
|
{%- endif %}
|
2021-04-16 05:51:23 +00:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
def eval_size(value) -> int:
|
|
|
|
try:
|
|
|
|
return int(value)
|
|
|
|
except ValueError:
|
|
|
|
times = dict(_K=1 << 10,
|
|
|
|
_M=1 << 20,
|
|
|
|
_G=1 << 30,
|
|
|
|
_T=1 << 40)
|
|
|
|
for unit, m in times.items():
|
|
|
|
if value.endswith(unit):
|
|
|
|
return int(value[:-len(unit)]) * m
|
|
|
|
raise ValueError(f'unknown value: {value}')
|
|
|
|
|
|
|
|
|
|
|
|
def readable_duration(value: str, typ: str) -> str:
|
|
|
|
try:
|
|
|
|
if typ == 'sec':
|
|
|
|
v = int(value)
|
|
|
|
postfix = 'second' if v == 1 else 'seconds'
|
|
|
|
return f'{v} {postfix}'
|
|
|
|
elif typ == 'float':
|
|
|
|
return str(float(value))
|
|
|
|
else:
|
|
|
|
return str(int(value))
|
|
|
|
except ValueError:
|
|
|
|
times = dict(_min=['minute', 'minutes'],
|
|
|
|
_hr=['hour', 'hours'],
|
|
|
|
_day=['day', 'days'])
|
|
|
|
for unit, readables in times.items():
|
|
|
|
if value.endswith(unit):
|
|
|
|
v = int(value[:-len(unit)])
|
|
|
|
postfix = readables[0 if v == 1 else 1]
|
|
|
|
return f'{v} {postfix}'
|
|
|
|
raise ValueError(f'unknown value: {value}')
|
|
|
|
|
|
|
|
|
|
|
|
def do_plain_num(value: str, typ: str) -> str:
|
|
|
|
if typ == 'float':
|
|
|
|
return str(float(value))
|
|
|
|
else:
|
|
|
|
return str(int(value))
|
|
|
|
|
|
|
|
|
2021-04-18 00:41:53 +00:00
|
|
|
def iec_size(value: int) -> str:
|
2021-05-02 09:50:36 +00:00
|
|
|
if value == 0:
|
|
|
|
return '0B'
|
2021-04-18 00:41:53 +00:00
|
|
|
units = dict(Ei=60,
|
|
|
|
Pi=50,
|
|
|
|
Ti=40,
|
|
|
|
Gi=30,
|
|
|
|
Mi=20,
|
|
|
|
Ki=10,
|
|
|
|
B=0)
|
|
|
|
for unit, bits in units.items():
|
|
|
|
m = 1 << bits
|
|
|
|
if value % m == 0:
|
|
|
|
value //= m
|
|
|
|
return f'{value}{unit}'
|
|
|
|
raise Exception(f'iec_size() failed to convert {value}')
|
|
|
|
|
|
|
|
|
2021-04-16 05:51:23 +00:00
|
|
|
def do_fileize_num(value: str, typ: str) -> str:
|
|
|
|
v = eval_size(value)
|
2021-04-18 00:41:53 +00:00
|
|
|
return iec_size(v)
|
2021-04-14 16:03:10 +00:00
|
|
|
|
2021-04-16 05:51:23 +00:00
|
|
|
|
|
|
|
def readable_num(value: str, typ: str) -> str:
|
|
|
|
e = ValueError()
|
|
|
|
for eval_func in [do_plain_num,
|
|
|
|
readable_duration,
|
|
|
|
do_fileize_num]:
|
|
|
|
try:
|
|
|
|
return eval_func(value, typ)
|
|
|
|
except ValueError as ex:
|
|
|
|
e = ex
|
|
|
|
raise e
|
|
|
|
|
|
|
|
|
2021-04-17 14:28:57 +00:00
|
|
|
def literal(name) -> str:
|
|
|
|
if name:
|
|
|
|
return f'``{name}``'
|
|
|
|
else:
|
|
|
|
return f'<empty string>'
|
|
|
|
|
|
|
|
|
2021-04-17 16:39:41 +00:00
|
|
|
def ref_confval(name) -> str:
|
|
|
|
return f':confval:`{name}`'
|
|
|
|
|
|
|
|
|
2021-04-16 05:51:23 +00:00
|
|
|
def jinja_template() -> jinja2.Template:
|
|
|
|
env = jinja2.Environment()
|
|
|
|
env.filters['eval_size'] = eval_size
|
2021-04-18 00:41:53 +00:00
|
|
|
env.filters['iec_size'] = iec_size
|
2021-04-16 05:51:23 +00:00
|
|
|
env.filters['readable_duration'] = readable_duration
|
|
|
|
env.filters['readable_num'] = readable_num
|
2021-04-17 14:28:57 +00:00
|
|
|
env.filters['literal'] = literal
|
2021-04-17 16:39:41 +00:00
|
|
|
env.filters['ref_confval'] = ref_confval
|
2021-04-16 05:51:23 +00:00
|
|
|
return env.from_string(TEMPLATE)
|
|
|
|
|
|
|
|
|
|
|
|
FieldValueT = Union[bool, float, int, str]
|
|
|
|
|
|
|
|
|
|
|
|
class CephOption(Directive):
|
|
|
|
"""
|
|
|
|
emit option loaded from given command/options/<name>.yaml.in file
|
|
|
|
"""
|
|
|
|
has_content = True
|
|
|
|
required_arguments = 1
|
|
|
|
optional_arguments = 0
|
|
|
|
final_argument_whitespace = False
|
|
|
|
option_spec = {'default': directives.unchanged}
|
|
|
|
|
|
|
|
template = jinja_template()
|
|
|
|
opts: Dict[str, Dict[str, FieldValueT]] = {}
|
|
|
|
|
|
|
|
def _load_yaml(self) -> Dict[str, Dict[str, FieldValueT]]:
|
|
|
|
if CephOption.opts:
|
|
|
|
return CephOption.opts
|
|
|
|
env = self.state.document.settings.env
|
|
|
|
opts = []
|
|
|
|
for fn in status_iterator(env.config.ceph_confval_imports,
|
|
|
|
'loading options...', 'red',
|
|
|
|
len(env.config.ceph_confval_imports),
|
|
|
|
env.app.verbosity):
|
|
|
|
env.note_dependency(fn)
|
|
|
|
try:
|
|
|
|
with open(fn, 'r') as f:
|
|
|
|
yaml_in = io.StringIO()
|
|
|
|
for line in f:
|
|
|
|
if '@' not in line:
|
|
|
|
yaml_in.write(line)
|
|
|
|
yaml_in.seek(0)
|
|
|
|
opts += yaml.safe_load(yaml_in)['options']
|
|
|
|
except OSError as e:
|
|
|
|
message = f'Unable to open option file "{fn}": {e}'
|
|
|
|
raise self.error(message)
|
|
|
|
CephOption.opts = dict((opt['name'], opt) for opt in opts)
|
|
|
|
return CephOption.opts
|
|
|
|
|
|
|
|
def run(self) -> List[Any]:
|
|
|
|
name = self.arguments[0]
|
|
|
|
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')
|
|
|
|
opt_default = opt.get('default')
|
|
|
|
default = self.options.get('default', opt_default)
|
2021-04-22 04:06:26 +00:00
|
|
|
try:
|
|
|
|
rendered = self.template.render(opt=opt,
|
|
|
|
desc=desc,
|
|
|
|
default=default)
|
|
|
|
except Exception as e:
|
|
|
|
message = (f'Unable to render option "{name}": {e}. ',
|
|
|
|
f'opt={opt}, desc={desc}, default={default}')
|
|
|
|
raise self.error(message)
|
|
|
|
|
2021-04-16 05:51:23 +00:00
|
|
|
lineno = self.lineno - self.state_machine.input_offset - 1
|
|
|
|
source = self.state_machine.input_lines.source(lineno)
|
|
|
|
self.state_machine.insert_input(rendered.split('\n'), source)
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def setup(app) -> Dict[str, Any]:
|
|
|
|
app.add_config_value('ceph_confval_imports',
|
|
|
|
default=[],
|
|
|
|
rebuild='html',
|
|
|
|
types=[str])
|
|
|
|
app.add_directive('confval', CephOption)
|
2021-04-14 16:03:10 +00:00
|
|
|
app.add_object_type(
|
2021-04-16 05:51:23 +00:00
|
|
|
'confval_option',
|
2021-04-17 16:11:14 +00:00
|
|
|
'confval',
|
2021-04-14 16:03:10 +00:00
|
|
|
objname='configuration value',
|
|
|
|
indextemplate='pair: %s; configuration value',
|
|
|
|
doc_field_types=[
|
|
|
|
PyField(
|
|
|
|
'type',
|
|
|
|
label=_('Type'),
|
|
|
|
has_arg=False,
|
|
|
|
names=('type',),
|
|
|
|
bodyrolename='class'
|
|
|
|
),
|
|
|
|
Field(
|
|
|
|
'default',
|
|
|
|
label=_('Default'),
|
|
|
|
has_arg=False,
|
|
|
|
names=('default',),
|
|
|
|
),
|
|
|
|
Field(
|
|
|
|
'required',
|
|
|
|
label=_('Required'),
|
|
|
|
has_arg=False,
|
|
|
|
names=('required',),
|
|
|
|
),
|
|
|
|
Field(
|
|
|
|
'example',
|
|
|
|
label=_('Example'),
|
|
|
|
has_arg=False,
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
2021-04-16 06:08:59 +00:00
|
|
|
app.add_object_type(
|
2021-04-17 16:40:15 +00:00
|
|
|
'confsec',
|
|
|
|
'confsec',
|
2021-04-16 06:08:59 +00:00
|
|
|
objname='configuration section',
|
|
|
|
indextemplate='pair: %s; configuration section',
|
|
|
|
doc_field_types=[
|
|
|
|
Field(
|
|
|
|
'example',
|
|
|
|
label=_('Example'),
|
|
|
|
has_arg=False,
|
|
|
|
)]
|
|
|
|
)
|
2021-04-16 05:51:23 +00:00
|
|
|
|
2021-04-14 16:03:10 +00:00
|
|
|
return {
|
|
|
|
'version': 'builtin',
|
|
|
|
'parallel_read_safe': True,
|
|
|
|
'parallel_write_safe': True,
|
|
|
|
}
|