]>
Commit | Line | Data |
---|---|---|
f67539c2 TL |
1 | import io |
2 | import os | |
3 | import sys | |
4 | import contextlib | |
5 | ||
6 | from docutils.parsers.rst import directives | |
7 | from docutils.parsers.rst import Directive | |
8 | from jinja2 import Template | |
9 | from pcpp.preprocessor import Preprocessor | |
10 | from sphinx.util import logging | |
11 | from sphinx.util.console import bold | |
12 | ||
13 | logger = logging.getLogger(__name__) | |
14 | ||
15 | ||
16 | class 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 | ||
48 | class 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 | ||
136 | class 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 | ||
161 | class 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 | ||
181 | TEMPLATE = ''' | |
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 | ||
191 | Example command: | |
192 | ||
193 | .. code-block:: bash | |
194 | ||
195 | {{ command.mk_bash_example() }} | |
196 | {% if command.params %} | |
197 | Parameters: | |
198 | ||
199 | {% for param in command.params %}* **{{param.name}}**: {{ param.help() | wordwrap(70) | indent(2) }} | |
200 | {% endfor %}{% endif %} | |
201 | Ceph Module: | |
202 | ||
203 | * *{{ command.module }}* | |
204 | ||
205 | Required Permissions: | |
206 | ||
207 | * *{{ command.perm }}* | |
208 | ||
209 | {% if command.flags %}Command Flags: | |
210 | ||
211 | * *{{ command.flags }}* | |
212 | {% endif %} | |
213 | {% endfor %} | |
214 | ||
215 | ''' | |
216 | ||
217 | ||
218 | class 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 | ||
335 | class 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 | ||
390 | class 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 | ||
433 | def 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 | } |