1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Sphinx extension for building opcode documentation.
23
24 """
25
26 import re
27 from cStringIO import StringIO
28
29 import docutils.statemachine
30 import docutils.nodes
31 import docutils.utils
32 import docutils.parsers.rst
33
34 import sphinx.errors
35 import sphinx.util.compat
36 import sphinx.roles
37 import sphinx.addnodes
38
39 s_compat = sphinx.util.compat
40
41 try:
42
43
44 orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
45 except (AttributeError, ValueError, KeyError), err:
46
47 raise Exception("Can't find reST role named 'manpage': %s" % err)
48
49 from ganeti import constants
50 from ganeti import compat
51 from ganeti import errors
52 from ganeti import utils
53 from ganeti import opcodes
54 from ganeti import ht
55 from ganeti import rapi
56 from ganeti import luxi
57 from ganeti import objects
58 from ganeti import http
59 from ganeti import _autoconf
60
61 import ganeti.rapi.rlib2
62 import ganeti.rapi.connector
63
64
65
66 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
67
68 _TAB_WIDTH = 2
69
70 RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I)
74 """Custom class for generating errors in Sphinx.
75
76 """
77
89
90
91 COMMON_PARAM_NAMES = _GetCommonParamNames()
92
93
94 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
95 rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects,
96 http=http)
97
98
99 CV_ECODES_DOC = "ecodes"
100
101
102 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
103 DOCUMENTED_CONSTANTS = {
104 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
105 }
110
113 """Split simple option list.
114
115 @type text: string
116 @param text: Options, e.g. "foo, bar, baz"
117
118 """
119 return [i.strip(",").strip() for i in text.split()]
120
123 """Parse simple assignment option.
124
125 @type text: string
126 @param text: Assignments, e.g. "foo=bar, hello=world"
127 @rtype: dict
128
129 """
130 result = {}
131
132 for part in _SplitOption(text):
133 if "=" not in part:
134 raise OpcodeError("Invalid option format, missing equal sign")
135
136 (name, value) = part.split("=", 1)
137
138 result[name.strip()] = value.strip()
139
140 return result
141
144 """Build opcode parameter documentation.
145
146 @type op_id: string
147 @param op_id: Opcode ID
148
149 """
150 op_cls = opcodes.OP_MAPPING[op_id]
151
152 params_with_alias = \
153 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
154 for (name, default, test, doc) in op_cls.GetAllParams()],
155 key=compat.fst)
156
157 for (rapi_name, name, default, test, doc) in params_with_alias:
158
159 if (name in COMMON_PARAM_NAMES and
160 (not include or name not in include)):
161 continue
162 if exclude is not None and name in exclude:
163 continue
164 if include is not None and name not in include:
165 continue
166
167 has_default = default is not ht.NoDefault
168 has_test = not (test is None or test is ht.NoType)
169
170 buf = StringIO()
171 buf.write("``%s``" % (rapi_name,))
172 if has_default or has_test:
173 buf.write(" (")
174 if has_default:
175 buf.write("defaults to ``%s``" % (default,))
176 if has_test:
177 buf.write(", ")
178 if has_test:
179 buf.write("must be ``%s``" % (test,))
180 buf.write(")")
181 yield buf.getvalue()
182
183
184 for line in doc.splitlines():
185 yield " %s" % line
186
189 """Build opcode result documentation.
190
191 @type op_id: string
192 @param op_id: Opcode ID
193
194 """
195 op_cls = opcodes.OP_MAPPING[op_id]
196
197 result_fn = getattr(op_cls, "OP_RESULT", None)
198
199 if not result_fn:
200 raise OpcodeError("Opcode '%s' has no result description" % op_id)
201
202 return "``%s``" % result_fn
203
206 """Custom directive for opcode parameters.
207
208 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
209
210 """
211 has_content = False
212 required_arguments = 1
213 optional_arguments = 0
214 final_argument_whitespace = False
215 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
216 alias=_ParseAlias)
217
219 op_id = self.arguments[0]
220 include = self.options.get("include", None)
221 exclude = self.options.get("exclude", None)
222 alias = self.options.get("alias", {})
223
224 path = op_id
225 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
226
227
228 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
229 convert_whitespace=1)
230 self.state_machine.insert_input(include_lines, path)
231
232 return []
233
236 """Custom directive for opcode result.
237
238 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
239
240 """
241 has_content = False
242 required_arguments = 1
243 optional_arguments = 0
244 final_argument_whitespace = False
245
247 op_id = self.arguments[0]
248
249 path = op_id
250 include_text = _BuildOpcodeResult(op_id)
251
252
253 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
254 convert_whitespace=1)
255 self.state_machine.insert_input(include_lines, path)
256
257 return []
258
259
260 -def PythonEvalRole(role, rawtext, text, lineno, inliner,
261 options={}, content=[]):
262 """Custom role to evaluate Python expressions.
263
264 The expression's result is included as a literal.
265
266 """
267
268
269
270
271
272 code = docutils.utils.unescape(text, restore_backslashes=True)
273
274 try:
275 result = eval(code, EVAL_NS)
276 except Exception, err:
277 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
278 line=lineno)
279 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
280
281 node = docutils.nodes.literal("", unicode(result), **options)
282
283 return ([node], [])
284
287 """Custom directive for writing assertions.
288
289 The content must be a valid Python expression. If its result does not
290 evaluate to C{True}, the assertion fails.
291
292 """
293 has_content = True
294 required_arguments = 0
295 optional_arguments = 0
296 final_argument_whitespace = False
297
299
300 if hasattr(self, "assert_has_content"):
301 self.assert_has_content()
302 else:
303 assert self.content
304
305 code = "\n".join(self.content)
306
307 try:
308 result = eval(code, EVAL_NS)
309 except Exception, err:
310 raise self.error("Failed to evaluate %r: %s" % (code, err))
311
312 if not result:
313 raise self.error("Assertion failed: %s" % (code, ))
314
315 return []
316
319 """Build query fields documentation.
320
321 @type fields: dict (field name as key, field details as value)
322
323 """
324 defs = [(fdef.name, fdef.doc)
325 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
326 key=compat.fst)]
327 return BuildValuesDoc(defs)
328
331 """Builds documentation for a list of values
332
333 @type values: list of tuples in the form (value, documentation)
334
335 """
336 for name, doc in values:
337 assert len(doc.splitlines()) == 1
338 yield "``%s``" % (name,)
339 yield " %s" % (doc,)
340
341
342 -def _ManPageNodeClass(*args, **kwargs):
343 """Generates a pending XRef like a ":doc:`...`" reference.
344
345 """
346
347 kwargs["reftype"] = "doc"
348
349
350 kwargs["refexplicit"] = True
351
352 return sphinx.addnodes.pending_xref(*args, **kwargs)
353
354
355 -class _ManPageXRefRole(sphinx.roles.XRefRole):
356 - def __init__(self):
357 """Initializes this class.
358
359 """
360 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
361 warn_dangling=True)
362
363 assert not hasattr(self, "converted"), \
364 "Sphinx base class gained an attribute named 'converted'"
365
366 self.converted = None
367
368 - def process_link(self, env, refnode, has_explicit_title, title, target):
369 """Specialization for man page links.
370
371 """
372 if has_explicit_title:
373 raise ReSTError("Setting explicit title is not allowed for man pages")
374
375
376 m = _MAN_RE.match(title)
377 if not m:
378 raise ReSTError("Man page reference '%s' does not match regular"
379 " expression '%s'" % (title, _MAN_RE.pattern))
380
381 name = m.group("name")
382 section = int(m.group("section"))
383
384 wanted_section = _autoconf.MAN_PAGES.get(name, None)
385
386 if not (wanted_section is None or wanted_section == section):
387 raise ReSTError("Referenced man page '%s' has section number %s, but the"
388 " reference uses section %s" %
389 (name, wanted_section, section))
390
391 self.converted = bool(wanted_section is not None and
392 env.app.config.enable_manpages)
393
394 if self.converted:
395
396 return (title, "man-%s" % name)
397 else:
398
399 return (title, target)
400
404 """Custom role for man page references.
405
406 Converts man pages to links if enabled during the build.
407
408 """
409 xref = _ManPageXRefRole()
410
411 assert ht.TNone(xref.converted)
412
413
414 try:
415 result = xref(typ, rawtext, text, lineno, inliner,
416 options=options, content=content)
417 except ReSTError, err:
418 msg = inliner.reporter.error(str(err), line=lineno)
419 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
420
421 assert ht.TBool(xref.converted)
422
423
424
425 if xref.converted:
426 return result
427
428
429 return orig_manpage_role(typ, rawtext, text, lineno, inliner,
430 options=options, content=content)
431
434 """Encodes a RAPI resource URI for use as a link target.
435
436 """
437 parts = [RAPI_URI_ENCODE_RE.sub("-", uri.lower()).strip("-")]
438
439 if method is not None:
440 parts.append(method.lower())
441
442 return "rapi-res-%s" % "+".join(filter(None, parts))
443
446 """Generates link target name for RAPI resource.
447
448 """
449 if uri in ["/", "/2"]:
450
451 return None
452
453 elif uri == "/version":
454 return _EncodeRapiResourceLink(method, uri)
455
456 elif uri.startswith("/2/"):
457 return _EncodeRapiResourceLink(method, uri[len("/2/"):])
458
459 else:
460 raise ReSTError("Unhandled URI '%s'" % uri)
461
464 """Returns list of HTTP methods supported by handler class.
465
466 @type handler: L{rapi.baserlib.ResourceBase}
467 @param handler: Handler class
468 @rtype: list of strings
469
470 """
471 return sorted(method
472 for (method, op_attr, _, _) in rapi.baserlib.OPCODE_ATTRS
473
474 if hasattr(handler, method) or hasattr(handler, op_attr))
475
478 """Returns textual description of required RAPI permissions.
479
480 @type handler: L{rapi.baserlib.ResourceBase}
481 @param handler: Handler class
482 @type method: string
483 @param method: HTTP method (e.g. L{http.HTTP_GET})
484 @rtype: string
485
486 """
487 access = rapi.baserlib.GetHandlerAccess(handler, method)
488
489 if access:
490 return utils.CommaJoin(sorted(access))
491 else:
492 return "*(none)*"
493
496 @classmethod
498 """Returns dictionary of resource handlers.
499
500 """
501 resources = \
502 rapi.connector.GetHandlers("[node_name]", "[instance_name]",
503 "[group_name]", "[network_name]", "[job_id]",
504 "[disk_index]", "[resource]",
505 translate=cls._TranslateResourceUri)
506
507 return resources
508
509 @classmethod
511 """Translates a resource URI for use in documentation.
512
513 @see: L{rapi.connector.GetHandlers}
514
515 """
516 return "".join(map(cls._UriPatternToString, args))
517
518 @staticmethod
520 """Converts L{rapi.connector.UriPattern} to strings.
521
522 """
523 if isinstance(value, rapi.connector.UriPattern):
524 return value.content
525 else:
526 return value
527
528
529 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
548
571
574 """Custom directive for RAPI resource details.
575
576 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
577
578 """
579 has_content = False
580 required_arguments = 1
581 optional_arguments = 0
582 final_argument_whitespace = False
583
585 uri = self.arguments[0]
586
587 try:
588 handler = _RAPI_RESOURCES_FOR_DOCS[uri]
589 except KeyError:
590 raise self.error("Unknown resource URI '%s'" % uri)
591
592 lines = [
593 ".. list-table::",
594 " :widths: 1 4",
595 " :header-rows: 1",
596 "",
597 " * - Method",
598 " - :ref:`Required permissions <rapi-users>`",
599 ]
600
601 for method in _GetHandlerMethods(handler):
602 lines.extend([
603 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)),
604 " - %s" % _DescribeHandlerAccess(handler, method),
605 ])
606
607
608 include_lines = \
609 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH,
610 convert_whitespace=1)
611 self.state_machine.insert_input(include_lines, self.__class__.__name__)
612
613 return []
614
630