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 ht.NoDefault
168 has_test = not (test is None or test is 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".join(_BuildOpcodeParams(op_id, include, exclude, alias))
227 # Inject into state machine
228 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
229 convert_whitespace=1)
230 self.state_machine.insert_input(include_lines, path)
235 class OpcodeResult(s_compat.Directive):
236 """Custom directive for opcode result.
238 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
242 required_arguments = 1
243 optional_arguments = 0
244 final_argument_whitespace = False
247 op_id = self.arguments[0]
250 include_text = _BuildOpcodeResult(op_id)
252 # Inject into state machine
253 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
254 convert_whitespace=1)
255 self.state_machine.insert_input(include_lines, path)
260 def PythonEvalRole(role, rawtext, text, lineno, inliner,
261 options={}, content=[]):
262 """Custom role to evaluate Python expressions.
264 The expression's result is included as a literal.
267 # pylint: disable=W0102,W0613,W0142
268 # W0102: Dangerous default value as argument
269 # W0142: Used * or ** magic
270 # W0613: Unused argument
272 code = docutils.utils.unescape(text, restore_backslashes=True)
275 result = eval(code, EVAL_NS)
276 except Exception, err: # pylint: disable=W0703
277 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
279 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
281 node = docutils.nodes.literal("", unicode(result), **options)
286 class PythonAssert(s_compat.Directive):
287 """Custom directive for writing assertions.
289 The content must be a valid Python expression. If its result does not
290 evaluate to C{True}, the assertion fails.
294 required_arguments = 0
295 optional_arguments = 0
296 final_argument_whitespace = False
299 # Handle combinations of Sphinx and docutils not providing the wanted method
300 if hasattr(self, "assert_has_content"):
301 self.assert_has_content()
305 code = "\n".join(self.content)
308 result = eval(code, EVAL_NS)
309 except Exception, err:
310 raise self.error("Failed to evaluate %r: %s" % (code, err))
313 raise self.error("Assertion failed: %s" % (code, ))
318 def BuildQueryFields(fields):
319 """Build query fields documentation.
321 @type fields: dict (field name as key, field details as value)
324 defs = [(fdef.name, fdef.doc)
325 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
327 return BuildValuesDoc(defs)
330 def BuildValuesDoc(values):
331 """Builds documentation for a list of values
333 @type values: list of tuples in the form (value, documentation)
336 for name, doc in values:
337 assert len(doc.splitlines()) == 1
338 yield "``%s``" % (name,)
342 def _ManPageNodeClass(*args, **kwargs):
343 """Generates a pending XRef like a ":doc:`...`" reference.
346 # Type for sphinx/environment.py:BuildEnvironment.resolve_references
347 kwargs["reftype"] = "doc"
350 kwargs["refexplicit"] = True
352 return sphinx.addnodes.pending_xref(*args, **kwargs)
355 class _ManPageXRefRole(sphinx.roles.XRefRole):
357 """Initializes this class.
360 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
363 assert not hasattr(self, "converted"), \
364 "Sphinx base class gained an attribute named 'converted'"
366 self.converted = None
368 def process_link(self, env, refnode, has_explicit_title, title, target):
369 """Specialization for man page links.
372 if has_explicit_title:
373 raise ReSTError("Setting explicit title is not allowed for man pages")
375 # Check format and extract name and section
376 m = _MAN_RE.match(title)
378 raise ReSTError("Man page reference '%s' does not match regular"
379 " expression '%s'" % (title, _MAN_RE.pattern))
381 name = m.group("name")
382 section = int(m.group("section"))
384 wanted_section = _autoconf.MAN_PAGES.get(name, None)
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))
391 self.converted = bool(wanted_section is not None and
392 env.app.config.enable_manpages)
395 # Create link to known man page
396 return (title, "man-%s" % name)
399 return (title, target)
402 def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
403 options={}, content=[]):
404 """Custom role for man page references.
406 Converts man pages to links if enabled during the build.
409 xref = _ManPageXRefRole()
411 assert ht.TNone(xref.converted)
413 # Check if it's a known man page
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])
421 assert ht.TBool(xref.converted)
423 # Return if the conversion was successful (i.e. the man page was known and
424 # conversion was enabled)
428 # Fallback if man page links are disabled or an unknown page is referenced
429 return orig_manpage_role(typ, rawtext, text, lineno, inliner,
430 options=options, content=content)
433 def _EncodeRapiResourceLink(method, uri):
434 """Encodes a RAPI resource URI for use as a link target.
437 parts = [RAPI_URI_ENCODE_RE.sub("-", uri.lower()).strip("-")]
439 if method is not None:
440 parts.append(method.lower())
442 return "rapi-res-%s" % "+".join(filter(None, parts))
445 def _MakeRapiResourceLink(method, uri):
446 """Generates link target name for RAPI resource.
449 if uri in ["/", "/2"]:
453 elif uri == "/version":
454 return _EncodeRapiResourceLink(method, uri)
456 elif uri.startswith("/2/"):
457 return _EncodeRapiResourceLink(method, uri[len("/2/"):])
460 raise ReSTError("Unhandled URI '%s'" % uri)
463 def _GetHandlerMethods(handler):
464 """Returns list of HTTP methods supported by handler class.
466 @type handler: L{rapi.baserlib.ResourceBase}
467 @param handler: Handler class
468 @rtype: list of strings
472 for (method, op_attr, _, _) in rapi.baserlib.OPCODE_ATTRS
473 # Only if handler supports method
474 if hasattr(handler, method) or hasattr(handler, op_attr))
477 def _DescribeHandlerAccess(handler, method):
478 """Returns textual description of required RAPI permissions.
480 @type handler: L{rapi.baserlib.ResourceBase}
481 @param handler: Handler class
483 @param method: HTTP method (e.g. L{http.HTTP_GET})
487 access = rapi.baserlib.GetHandlerAccess(handler, method)
490 return utils.CommaJoin(sorted(access))
495 class _RapiHandlersForDocsHelper(object):
498 """Returns dictionary of resource handlers.
502 rapi.connector.GetHandlers("[node_name]", "[instance_name]",
503 "[group_name]", "[network_name]", "[job_id]",
504 "[disk_index]", "[resource]",
505 translate=cls._TranslateResourceUri)
510 def _TranslateResourceUri(cls, *args):
511 """Translates a resource URI for use in documentation.
513 @see: L{rapi.connector.GetHandlers}
516 return "".join(map(cls._UriPatternToString, args))
519 def _UriPatternToString(value):
520 """Converts L{rapi.connector.UriPattern} to strings.
523 if isinstance(value, rapi.connector.UriPattern):
529 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
532 def _BuildRapiAccessTable(res):
533 """Build a table with access permissions needed for all RAPI resources.
536 for (uri, handler) in utils.NiceSort(res.items(), key=compat.fst):
537 reslink = _MakeRapiResourceLink(None, uri)
539 # No link was generated
542 yield ":ref:`%s <%s>`" % (uri, reslink)
544 for method in _GetHandlerMethods(handler):
545 yield (" | :ref:`%s <%s>`: %s" %
546 (method, _MakeRapiResourceLink(method, uri),
547 _DescribeHandlerAccess(handler, method)))
550 class RapiAccessTable(s_compat.Directive):
551 """Custom directive to generate table of all RAPI resources.
553 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
557 required_arguments = 0
558 optional_arguments = 0
559 final_argument_whitespace = False
563 include_text = "\n".join(_BuildRapiAccessTable(_RAPI_RESOURCES_FOR_DOCS))
565 # Inject into state machine
566 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
567 convert_whitespace=1)
568 self.state_machine.insert_input(include_lines, self.__class__.__name__)
573 class RapiResourceDetails(s_compat.Directive):
574 """Custom directive for RAPI resource details.
576 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
580 required_arguments = 1
581 optional_arguments = 0
582 final_argument_whitespace = False
585 uri = self.arguments[0]
588 handler = _RAPI_RESOURCES_FOR_DOCS[uri]
590 raise self.error("Unknown resource URI '%s'" % uri)
598 " - :ref:`Required permissions <rapi-users>`",
601 for method in _GetHandlerMethods(handler):
603 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)),
604 " - %s" % _DescribeHandlerAccess(handler, method),
607 # Inject into state machine
609 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH,
610 convert_whitespace=1)
611 self.state_machine.insert_input(include_lines, self.__class__.__name__)
617 """Sphinx extension callback.
620 # TODO: Implement Sphinx directive for query fields
621 app.add_directive("opcode_params", OpcodeParams)
622 app.add_directive("opcode_result", OpcodeResult)
623 app.add_directive("pyassert", PythonAssert)
624 app.add_role("pyeval", PythonEvalRole)
625 app.add_directive("rapi_access_table", RapiAccessTable)
626 app.add_directive("rapi_resource_details", RapiResourceDetails)
628 app.add_config_value("enable_manpages", False, True)
629 app.add_role("manpage", _ManPageRole)