]> git.proxmox.com Git - ceph.git/blame - ceph/doc/_ext/ceph_commands.py
import ceph 16.2.6
[ceph.git] / ceph / doc / _ext / ceph_commands.py
CommitLineData
f67539c2
TL
1import io
2import os
3import sys
4import contextlib
5
6from docutils.parsers.rst import directives
7from docutils.parsers.rst import Directive
8from jinja2 import Template
9from pcpp.preprocessor import Preprocessor
10from sphinx.util import logging
11from sphinx.util.console import bold
12
13logger = logging.getLogger(__name__)
14
15
16class Flags:
17 NOFORWARD = (1 << 0)
18 OBSOLETE = (1 << 1)
19 DEPRECATED = (1 << 2)
20 MGR = (1 << 3)
21 POLL = (1 << 4)
22 HIDDEN = (1 << 5)
23
24 VALS = {
25 NOFORWARD: 'no_forward',
26 OBSOLETE: 'obsolete',
27 DEPRECATED: 'deprecated',
28 MGR: 'mgr',
29 POLL: 'poll',
30 HIDDEN: 'hidden',
31 }
32
33 def __init__(self, fs):
34 self.fs = fs
35
36 def __contains__(self, other):
37 return other in str(self)
38
39 def __str__(self):
40 keys = Flags.VALS.keys()
41 es = {Flags.VALS[k] for k in keys if self.fs & k == k}
42 return ', '.join(sorted(es))
43
44 def __bool__(self):
45 return bool(str(self))
46
47
48class CmdParam(object):
49 t = {
50 'CephInt': 'int',
51 'CephString': 'str',
52 'CephChoices': 'str',
53 'CephPgid': 'str',
54 'CephOsdName': 'str',
55 'CephPoolname': 'str',
56 'CephObjectname': 'str',
57 'CephUUID': 'str',
58 'CephEntityAddr': 'str',
59 'CephIPAddr': 'str',
60 'CephName': 'str',
61 'CephBool': 'bool',
62 'CephFloat': 'float',
63 'CephFilepath': 'str',
64 }
65
66 bash_example = {
67 'CephInt': '1',
68 'CephString': 'string',
69 'CephChoices': 'choice',
70 'CephPgid': '0',
71 'CephOsdName': 'osd.0',
72 'CephPoolname': 'poolname',
73 'CephObjectname': 'objectname',
74 'CephUUID': 'uuid',
75 'CephEntityAddr': 'entityaddr',
76 'CephIPAddr': '0.0.0.0',
77 'CephName': 'name',
78 'CephBool': 'true',
79 'CephFloat': '0.0',
80 'CephFilepath': '/path/to/file',
81 }
82
83 def __init__(self, type, name,
84 who=None, n=None, req=True, range=None, strings=None,
85 goodchars=None):
86 self.type = type
87 self.name = name
88 self.who = who
89 self.n = n == 'N'
90 self.req = req != 'false'
91 self.range = range.split('|') if range else []
92 self.strings = strings.split('|') if strings else []
93 self.goodchars = goodchars
94
95 assert who == None
96
97 def help(self):
98 advanced = []
99 if self.type != 'CephString':
100 advanced.append(self.type + ' ')
101 if self.range:
102 advanced.append('range= ``{}`` '.format('..'.join(self.range)))
103 if self.strings:
104 advanced.append('strings=({}) '.format(' '.join(self.strings)))
105 if self.goodchars:
106 advanced.append('goodchars= ``{}`` '.format(self.goodchars))
107 if self.n:
108 advanced.append('(can be repeated)')
109
110 advanced = advanced or ["(string)"]
111 return ' '.join(advanced)
112
113 def mk_example_value(self):
114 if self.type == 'CephChoices' and self.strings:
115 return self.strings[0]
116 if self.range:
117 return self.range[0]
118 return CmdParam.bash_example[self.type]
119
120 def mk_bash_example(self, simple):
121 val = self.mk_example_value()
122
123 if self.type == 'CephBool':
124 return '--' + self.name
125 if simple:
126 if self.type == "CephChoices" and self.strings:
127 return val
128 elif self.type == "CephString" and self.name != 'who':
129 return 'my_' + self.name
130 else:
131 return CmdParam.bash_example[self.type]
132 else:
133 return '--{}={}'.format(self.name, val)
134
135
136class CmdCommand(object):
137 def __init__(self, prefix, args, desc,
138 module=None, perm=None, flags=0, poll=None):
139 self.prefix = prefix
140 self.params = sorted([CmdParam(**arg) for arg in args],
141 key=lambda p: p.req, reverse=True)
142 self.help = desc
143 self.module = module
144 self.perm = perm
145 self.flags = Flags(flags)
146 self.needs_overload = False
147
148 def is_reasonably_simple(self):
149 if len(self.params) > 3:
150 return False
151 if any(p.n for p in self.params):
152 return False
153 return True
154
155 def mk_bash_example(self):
156 simple = self.is_reasonably_simple()
157 line = ' '.join(['ceph', self.prefix] + [p.mk_bash_example(simple) for p in self.params])
158 return line
159
160
161class Sig:
162 @staticmethod
163 def _parse_arg_desc(desc):
164 try:
165 return dict(kv.split('=') for kv in desc.split(',') if kv)
166 except ValueError:
167 return desc
168
169 @staticmethod
170 def parse_cmd(cmd):
171 parsed = [Sig._parse_arg_desc(s) or s for s in cmd.split()]
172 prefix = [s for s in parsed if isinstance(s, str)]
173 params = [s for s in parsed if not isinstance(s, str)]
174 return ' '.join(prefix), params
175
176 @staticmethod
177 def parse_args(args):
178 return [Sig._parse_arg_desc(arg) for arg in args.split()]
179
180
181TEMPLATE = '''
182.. This file is automatically generated. do not modify
183
184{% for command in commands %}
185
186{{ command.prefix }}
187{{ command.prefix | length * '^' }}
188
189{{ command.help | wordwrap(70)}}
190
191Example command:
192
193.. code-block:: bash
194
195 {{ command.mk_bash_example() }}
196{% if command.params %}
197Parameters:
198
199{% for param in command.params %}* **{{param.name}}**: {{ param.help() | wordwrap(70) | indent(2) }}
200{% endfor %}{% endif %}
201Ceph Module:
202
203* *{{ command.module }}*
204
205Required Permissions:
206
207* *{{ command.perm }}*
208
209{% if command.flags %}Command Flags:
210
211* *{{ command.flags }}*
212{% endif %}
213{% endfor %}
214
215'''
216
217
218class CephMgrCommands(Directive):
219 """
220 extracts commands from specified mgr modules
221 """
222 has_content = True
223 required_arguments = 1
224 optional_arguments = 0
225 final_argument_whitespace = False
226 option_spec = {'python_path': directives.unchanged}
227
228 def _normalize_path(self, dirname):
229 my_dir = os.path.dirname(os.path.realpath(__file__))
230 src_dir = os.path.abspath(os.path.join(my_dir, '../..'))
231 return os.path.join(src_dir, dirname)
232
233 def _is_mgr_module(self, dirname, name):
234 if not os.path.isdir(os.path.join(dirname, name)):
235 return False
236 if not os.path.isfile(os.path.join(dirname, name, '__init__.py')):
237 return False
238 return name not in ['tests']
239
240 @contextlib.contextmanager
241 def mocked_modules(self):
242 # src/pybind/mgr/tests
243 from tests import mock
244 mock_imports = ['rados',
245 'rbd',
246 'cephfs',
247 'dateutil',
248 'dateutil.parser']
249 # make dashboard happy
250 mock_imports += ['OpenSSL',
251 'jwt',
252 'bcrypt',
253 'scipy',
254 'jsonpatch',
255 'rook.rook_client',
256 'rook.rook_client.ceph',
522d829b 257 'rook.rook_client._helper',
f67539c2
TL
258 'cherrypy=3.2.3']
259
260 # make restful happy
261 mock_imports += ['pecan',
262 'pecan.rest',
263 'pecan.hooks',
264 'werkzeug',
265 'werkzeug.serving']
266
267 for m in mock_imports:
268 args = {}
269 parts = m.split('=', 1)
270 mocked = parts[0]
271 if len(parts) > 1:
272 args['__version__'] = parts[1]
273 sys.modules[mocked] = mock.Mock(**args)
274
275 try:
276 yield
277 finally:
278 for m in mock_imports:
279 mocked = m.split('=', 1)[0]
280 sys.modules.pop(mocked)
281
282 def _collect_module_commands(self, name):
283 with self.mocked_modules():
284 logger.info(bold(f"loading mgr module '{name}'..."))
285 mgr_mod = __import__(name, globals(), locals(), [], 0)
286 from tests import M
287
288 def subclass(x):
289 try:
290 return issubclass(x, M)
291 except TypeError:
292 return False
293 ms = [c for c in mgr_mod.__dict__.values()
294 if subclass(c) and 'Standby' not in c.__name__]
295 [m] = ms
296 assert isinstance(m.COMMANDS, list)
297 return m.COMMANDS
298
299 def _normalize_command(self, command):
300 if 'handler' in command:
301 del command['handler']
302 if 'cmd' in command:
303 command['prefix'], command['args'] = Sig.parse_cmd(command['cmd'])
304 del command['cmd']
305 else:
306 command['args'] = Sig.parse_args(command['args'])
307 command['flags'] = (1 << 3)
308 command['module'] = 'mgr'
309 return command
310
311 def _render_cmds(self, commands):
312 rendered = Template(TEMPLATE).render(commands=list(commands))
313 lines = rendered.split("\n")
314 assert lines
315 lineno = self.lineno - self.state_machine.input_offset - 1
316 source = self.state_machine.input_lines.source(lineno)
317 self.state_machine.insert_input(lines, source)
318
319 def run(self):
320 module_path = self._normalize_path(self.arguments[0])
321 sys.path.insert(0, module_path)
322 for path in self.options.get('python_path', '').split(':'):
323 sys.path.insert(0, self._normalize_path(path))
324 os.environ['UNITTEST'] = 'true'
325 modules = [name for name in os.listdir(module_path)
326 if self._is_mgr_module(module_path, name)]
327 commands = sum([self._collect_module_commands(name) for name in modules], [])
328 cmds = [CmdCommand(**self._normalize_command(c)) for c in commands]
329 cmds = [cmd for cmd in cmds if 'hidden' not in cmd.flags]
330 cmds = sorted(cmds, key=lambda cmd: cmd.prefix)
331 self._render_cmds(cmds)
332 return []
333
334
335class MyProcessor(Preprocessor):
336 def __init__(self):
337 super().__init__()
338 self.cmds = []
339 self.undef('__DATE__')
340 self.undef('__TIME__')
341 self.expand_linemacro = False
342 self.expand_filemacro = False
343 self.expand_countermacro = False
344 self.line_directive = '#line'
345 self.define("__PCPP_VERSION__ " + '')
346 self.define("__PCPP_ALWAYS_FALSE__ 0")
347 self.define("__PCPP_ALWAYS_TRUE__ 1")
348
349 def eval(self, src):
350 _cmds = []
351
352 NONE = 0
353 NOFORWARD = (1 << 0)
354 OBSOLETE = (1 << 1)
355 DEPRECATED = (1 << 2)
356 MGR = (1 << 3)
357 POLL = (1 << 4)
358 HIDDEN = (1 << 5)
359 TELL = (1 << 6)
360
361 def FLAG(a):
362 return a
363
364 def COMMAND(cmd, desc, module, perm):
365 _cmds.append({
366 'cmd': cmd,
367 'desc': desc,
368 'module': module,
369 'perm': perm
370 })
371
372 def COMMAND_WITH_FLAG(cmd, desc, module, perm, flag):
373 _cmds.append({
374 'cmd': cmd,
375 'desc': desc,
376 'module': module,
377 'perm': perm,
378 'flags': flag
379 })
380
381 self.parse(src)
382 out = io.StringIO()
383 self.write(out)
384 out.seek(0)
385 s = out.read()
386 exec(s, globals(), locals())
387 return _cmds
388
389
390class CephMonCommands(Directive):
391 """
392 extracts commands from specified header file
393 """
394 has_content = True
395 required_arguments = 1
396 optional_arguments = 0
397 final_argument_whitespace = True
398
399 def _src_dir(self):
400 my_dir = os.path.dirname(os.path.realpath(__file__))
401 return os.path.abspath(os.path.join(my_dir, '../..'))
402
403 def _parse_headers(self, headers):
404 src_dir = self._src_dir()
405 src = '\n'.join(f'#include "{src_dir}/{header}"' for header in headers)
406 return MyProcessor().eval(src)
407
408 def _normalize_command(self, command):
409 if 'handler' in command:
410 del command['handler']
411 command['prefix'], command['args'] = Sig.parse_cmd(command['cmd'])
412 del command['cmd']
413 return command
414
415 def _render_cmds(self, commands):
416 rendered = Template(TEMPLATE).render(commands=list(commands))
417 lines = rendered.split("\n")
418 assert lines
419 lineno = self.lineno - self.state_machine.input_offset - 1
420 source = self.state_machine.input_lines.source(lineno)
421 self.state_machine.insert_input(lines, source)
422
423 def run(self):
424 headers = self.arguments[0].split()
425 commands = self._parse_headers(headers)
426 cmds = [CmdCommand(**self._normalize_command(c)) for c in commands]
427 cmds = [cmd for cmd in cmds if 'hidden' not in cmd.flags]
428 cmds = sorted(cmds, key=lambda cmd: cmd.prefix)
429 self._render_cmds(cmds)
430 return []
431
432
433def setup(app):
434 app.add_directive("ceph-mgr-commands", CephMgrCommands)
435 app.add_directive("ceph-mon-commands", CephMonCommands)
436
437 return {
438 'version': '0.1',
439 'parallel_read_safe': True,
440 'parallel_write_safe': True,
441 }