4 # Copyright (C) 2011, 2012, 2013 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Sphinx extension for building opcode documentation.
27 from cStringIO import StringIO
29 import docutils.statemachine
32 import docutils.parsers.rst
35 import sphinx.util.compat
37 import sphinx.addnodes
39 s_compat = sphinx.util.compat
42 # Access to a protected member of a client class
43 # pylint: disable=W0212
44 orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
45 except (AttributeError, ValueError, KeyError), err:
46 # Normally the "manpage" role is registered by sphinx/roles.py
47 raise Exception("Can't find reST role named 'manpage': %s" % err)
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
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
61 import ganeti.rapi.rlib2 # pylint: disable=W0611
62 import ganeti.rapi.connector # pylint: disable=W0611
65 #: Regular expression for man page names
66 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
70 RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I)
73 class ReSTError(Exception):
74 """Custom class for generating errors in Sphinx.
79 def _GetCommonParamNames():
80 """Builds a list of parameters common to all opcodes.
83 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
85 # The "depends" attribute should be listed
86 names.remove(opcodes.DEPEND_ATTR)
91 COMMON_PARAM_NAMES = _GetCommonParamNames()
93 #: Namespace for evaluating expressions
94 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
95 rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects,
98 # Constants documentation for man pages
99 CV_ECODES_DOC = "ecodes"
100 # We don't care about the leak of variables _, name and doc here.
101 # pylint: disable=W0621
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,
108 class OpcodeError(sphinx.errors.SphinxError):
109 category = "Opcode error"
112 def _SplitOption(text):
113 """Split simple option list.
116 @param text: Options, e.g. "foo, bar, baz"
119 return [i.strip(",").strip() for i in text.split()]
122 def _ParseAlias(text):
123 """Parse simple assignment option.
126 @param text: Assignments, e.g. "foo=bar, hello=world"
132 for part in _SplitOption(text):
134 raise OpcodeError("Invalid option format, missing equal sign")
136 (name, value) = part.split("=", 1)
138 result[name.strip()] = value.strip()
143 def _BuildOpcodeParams(op_id, include, exclude, alias):
144 """Build opcode parameter documentation.
147 @param op_id: Opcode ID
150 op_cls = opcodes.OP_MAPPING[op_id]
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()],
157 for (rapi_name, name, default, test, doc) in params_with_alias:
158 # Hide common parameters if not explicitly included
159 if (name in COMMON_PARAM_NAMES and
160 (not include or name not in include)):
162 if exclude is not None and name in exclude:
164 if include is not None and name not in include:
167 has_default = default is not None or default is not ht.NoDefault
168 has_test = test is not None or test is not ht.NoType
171 buf.write("``%s``" % (rapi_name,))
172 if has_default or has_test:
175 buf.write("defaults to ``%s``" % (default,))
179 buf.write("must be ``%s``" % (test,))
184 for line in doc.splitlines():
188 def _BuildOpcodeResult(op_id):
189 """Build opcode result documentation.
192 @param op_id: Opcode ID
195 op_cls = opcodes.OP_MAPPING[op_id]
197 result_fn = getattr(op_cls, "OP_RESULT", None)
200 raise OpcodeError("Opcode '%s' has no result description" % op_id)
202 return "``%s``" % result_fn
205 class OpcodeParams(s_compat.Directive):
206 """Custom directive for opcode parameters.
208 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
212 required_arguments = 1
213 optional_arguments = 0
214 final_argument_whitespace = False
215 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
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", {})
225 include_text = "\n\n".join(_BuildOpcodeParams(op_id,
230 # Inject into state machine
231 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
232 convert_whitespace=1)
233 self.state_machine.insert_input(include_lines, path)
238 class OpcodeResult(s_compat.Directive):
239 """Custom directive for opcode result.
241 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
245 required_arguments = 1
246 optional_arguments = 0
247 final_argument_whitespace = False
250 op_id = self.arguments[0]
253 include_text = _BuildOpcodeResult(op_id)
255 # Inject into state machine
256 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
257 convert_whitespace=1)
258 self.state_machine.insert_input(include_lines, path)
263 def PythonEvalRole(role, rawtext, text, lineno, inliner,
264 options={}, content=[]):
265 """Custom role to evaluate Python expressions.
267 The expression's result is included as a literal.
270 # pylint: disable=W0102,W0613,W0142
271 # W0102: Dangerous default value as argument
272 # W0142: Used * or ** magic
273 # W0613: Unused argument
275 code = docutils.utils.unescape(text, restore_backslashes=True)
278 result = eval(code, EVAL_NS)
279 except Exception, err: # pylint: disable=W0703
280 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
282 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
284 node = docutils.nodes.literal("", unicode(result), **options)
289 class PythonAssert(s_compat.Directive):
290 """Custom directive for writing assertions.
292 The content must be a valid Python expression. If its result does not
293 evaluate to C{True}, the assertion fails.
297 required_arguments = 0
298 optional_arguments = 0
299 final_argument_whitespace = False
302 # Handle combinations of Sphinx and docutils not providing the wanted method
303 if hasattr(self, "assert_has_content"):
304 self.assert_has_content()
308 code = "\n".join(self.content)
311 result = eval(code, EVAL_NS)
312 except Exception, err:
313 raise self.error("Failed to evaluate %r: %s" % (code, err))
316 raise self.error("Assertion failed: %s" % (code, ))
321 def BuildQueryFields(fields):
322 """Build query fields documentation.
324 @type fields: dict (field name as key, field details as value)
327 defs = [(fdef.name, fdef.doc)
328 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
330 return BuildValuesDoc(defs)
333 def BuildValuesDoc(values):
334 """Builds documentation for a list of values
336 @type values: list of tuples in the form (value, documentation)
339 for name, doc in values:
340 assert len(doc.splitlines()) == 1
341 yield "``%s``" % (name,)
345 def _ManPageNodeClass(*args, **kwargs):
346 """Generates a pending XRef like a ":doc:`...`" reference.
349 # Type for sphinx/environment.py:BuildEnvironment.resolve_references
350 kwargs["reftype"] = "doc"
353 kwargs["refexplicit"] = True
355 return sphinx.addnodes.pending_xref(*args, **kwargs)
358 class _ManPageXRefRole(sphinx.roles.XRefRole):
360 """Initializes this class.
363 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
366 assert not hasattr(self, "converted"), \
367 "Sphinx base class gained an attribute named 'converted'"
369 self.converted = None
371 def process_link(self, env, refnode, has_explicit_title, title, target):
372 """Specialization for man page links.
375 if has_explicit_title:
376 raise ReSTError("Setting explicit title is not allowed for man pages")
378 # Check format and extract name and section
379 m = _MAN_RE.match(title)
381 raise ReSTError("Man page reference '%s' does not match regular"
382 " expression '%s'" % (title, _MAN_RE.pattern))
384 name = m.group("name")
385 section = int(m.group("section"))
387 wanted_section = _autoconf.MAN_PAGES.get(name, None)
389 if not (wanted_section is None or wanted_section == section):
390 raise ReSTError("Referenced man page '%s' has section number %s, but the"
391 " reference uses section %s" %
392 (name, wanted_section, section))
394 self.converted = bool(wanted_section is not None and
395 env.app.config.enable_manpages)
398 # Create link to known man page
399 return (title, "man-%s" % name)
402 return (title, target)
405 def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
406 options={}, content=[]):
407 """Custom role for man page references.
409 Converts man pages to links if enabled during the build.
412 xref = _ManPageXRefRole()
414 assert ht.TNone(xref.converted)
416 # Check if it's a known man page
418 result = xref(typ, rawtext, text, lineno, inliner,
419 options=options, content=content)
420 except ReSTError, err:
421 msg = inliner.reporter.error(str(err), line=lineno)
422 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
424 assert ht.TBool(xref.converted)
426 # Return if the conversion was successful (i.e. the man page was known and
427 # conversion was enabled)
431 # Fallback if man page links are disabled or an unknown page is referenced
432 return orig_manpage_role(typ, rawtext, text, lineno, inliner,
433 options=options, content=content)
436 def _EncodeRapiResourceLink(method, uri):
437 """Encodes a RAPI resource URI for use as a link target.
440 parts = [RAPI_URI_ENCODE_RE.sub("-", uri.lower()).strip("-")]
442 if method is not None:
443 parts.append(method.lower())
445 return "rapi-res-%s" % "+".join(filter(None, parts))
448 def _MakeRapiResourceLink(method, uri):
449 """Generates link target name for RAPI resource.
452 if uri in ["/", "/2"]:
456 elif uri == "/version":
457 return _EncodeRapiResourceLink(method, uri)
459 elif uri.startswith("/2/"):
460 return _EncodeRapiResourceLink(method, uri[len("/2/"):])
463 raise ReSTError("Unhandled URI '%s'" % uri)
466 def _GetHandlerMethods(handler):
467 """Returns list of HTTP methods supported by handler class.
469 @type handler: L{rapi.baserlib.ResourceBase}
470 @param handler: Handler class
471 @rtype: list of strings
475 for (method, op_attr, _, _) in rapi.baserlib.OPCODE_ATTRS
476 # Only if handler supports method
477 if hasattr(handler, method) or hasattr(handler, op_attr))
480 def _DescribeHandlerAccess(handler, method):
481 """Returns textual description of required RAPI permissions.
483 @type handler: L{rapi.baserlib.ResourceBase}
484 @param handler: Handler class
486 @param method: HTTP method (e.g. L{http.HTTP_GET})
490 access = rapi.baserlib.GetHandlerAccess(handler, method)
493 return utils.CommaJoin(sorted(access))
498 class _RapiHandlersForDocsHelper(object):
501 """Returns dictionary of resource handlers.
505 rapi.connector.GetHandlers("[node_name]", "[instance_name]",
506 "[group_name]", "[network_name]", "[job_id]",
507 "[disk_index]", "[resource]",
508 translate=cls._TranslateResourceUri)
513 def _TranslateResourceUri(cls, *args):
514 """Translates a resource URI for use in documentation.
516 @see: L{rapi.connector.GetHandlers}
519 return "".join(map(cls._UriPatternToString, args))
522 def _UriPatternToString(value):
523 """Converts L{rapi.connector.UriPattern} to strings.
526 if isinstance(value, rapi.connector.UriPattern):
532 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
535 def _BuildRapiAccessTable(res):
536 """Build a table with access permissions needed for all RAPI resources.
539 for (uri, handler) in utils.NiceSort(res.items(), key=compat.fst):
540 reslink = _MakeRapiResourceLink(None, uri)
542 # No link was generated
545 yield ":ref:`%s <%s>`" % (uri, reslink)
547 for method in _GetHandlerMethods(handler):
548 yield (" | :ref:`%s <%s>`: %s" %
549 (method, _MakeRapiResourceLink(method, uri),
550 _DescribeHandlerAccess(handler, method)))
553 class RapiAccessTable(s_compat.Directive):
554 """Custom directive to generate table of all RAPI resources.
556 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
560 required_arguments = 0
561 optional_arguments = 0
562 final_argument_whitespace = False
566 include_text = "\n".join(_BuildRapiAccessTable(_RAPI_RESOURCES_FOR_DOCS))
568 # Inject into state machine
569 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
570 convert_whitespace=1)
571 self.state_machine.insert_input(include_lines, self.__class__.__name__)
576 class RapiResourceDetails(s_compat.Directive):
577 """Custom directive for RAPI resource details.
579 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
583 required_arguments = 1
584 optional_arguments = 0
585 final_argument_whitespace = False
588 uri = self.arguments[0]
591 handler = _RAPI_RESOURCES_FOR_DOCS[uri]
593 raise self.error("Unknown resource URI '%s'" % uri)
601 " - :ref:`Required permissions <rapi-users>`",
604 for method in _GetHandlerMethods(handler):
606 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)),
607 " - %s" % _DescribeHandlerAccess(handler, method),
610 # Inject into state machine
612 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH,
613 convert_whitespace=1)
614 self.state_machine.insert_input(include_lines, self.__class__.__name__)
620 """Sphinx extension callback.
623 # TODO: Implement Sphinx directive for query fields
624 app.add_directive("opcode_params", OpcodeParams)
625 app.add_directive("opcode_result", OpcodeResult)
626 app.add_directive("pyassert", PythonAssert)
627 app.add_role("pyeval", PythonEvalRole)
628 app.add_directive("rapi_access_table", RapiAccessTable)
629 app.add_directive("rapi_resource_details", RapiResourceDetails)
631 app.add_config_value("enable_manpages", False, True)
632 app.add_role("manpage", _ManPageRole)