#
#
-# Copyright (C) 2011 Google Inc.
+# Copyright (C) 2011, 2012, 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
"""
-import operator
+import re
from cStringIO import StringIO
import docutils.statemachine
import docutils.nodes
import docutils.utils
+import docutils.parsers.rst
import sphinx.errors
import sphinx.util.compat
+import sphinx.roles
+import sphinx.addnodes
+
+s_compat = sphinx.util.compat
+
+try:
+ # Access to a protected member of a client class
+ # pylint: disable=W0212
+ orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
+except (AttributeError, ValueError, KeyError), err:
+ # Normally the "manpage" role is registered by sphinx/roles.py
+ raise Exception("Can't find reST role named 'manpage': %s" % err)
from ganeti import constants
from ganeti import compat
from ganeti import errors
from ganeti import utils
from ganeti import opcodes
+from ganeti import opcodes_base
from ganeti import ht
+from ganeti import rapi
+from ganeti import luxi
+from ganeti import objects
+from ganeti import http
+from ganeti import _autoconf
+
+import ganeti.rapi.rlib2 # pylint: disable=W0611
+import ganeti.rapi.connector # pylint: disable=W0611
+
+
+#: Regular expression for man page names
+_MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
+
+_TAB_WIDTH = 2
+
+RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I)
+
+
+class ReSTError(Exception):
+ """Custom class for generating errors in Sphinx.
+
+ """
-COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
+def _GetCommonParamNames():
+ """Builds a list of parameters common to all opcodes.
+
+ """
+ names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
+
+ # The "depends" attribute should be listed
+ names.remove(opcodes_base.DEPEND_ATTR)
+
+ return names
+
+
+COMMON_PARAM_NAMES = _GetCommonParamNames()
#: Namespace for evaluating expressions
-EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors)
+EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
+ rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects,
+ http=http)
+
+# Constants documentation for man pages
+CV_ECODES_DOC = "ecodes"
+# We don't care about the leak of variables _, name and doc here.
+# pylint: disable=W0621
+CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
+DOCUMENTED_CONSTANTS = {
+ CV_ECODES_DOC: CV_ECODES_DOC_LIST,
+ }
class OpcodeError(sphinx.errors.SphinxError):
params_with_alias = \
utils.NiceSort([(alias.get(name, name), name, default, test, doc)
for (name, default, test, doc) in op_cls.GetAllParams()],
- key=operator.itemgetter(0))
+ key=compat.fst)
for (rapi_name, name, default, test, doc) in params_with_alias:
- # Hide common parameters if not explicitely included
+ # Hide common parameters if not explicitly included
if (name in COMMON_PARAM_NAMES and
(not include or name not in include)):
continue
if include is not None and name not in include:
continue
- has_default = default is not ht.NoDefault
- has_test = not (test is None or test is ht.NoType)
+ has_default = default is not None or default is not ht.NoDefault
+ has_test = test is not None
buf = StringIO()
- buf.write("``%s``" % rapi_name)
+ buf.write("``%s``" % (rapi_name,))
if has_default or has_test:
buf.write(" (")
if has_default:
- buf.write("defaults to ``%s``" % default)
+ buf.write("defaults to ``%s``" % (default,))
if has_test:
buf.write(", ")
if has_test:
- buf.write("must be ``%s``" % test)
+ buf.write("must be ``%s``" % (test,))
buf.write(")")
yield buf.getvalue()
yield " %s" % line
-class OpcodeParams(sphinx.util.compat.Directive):
+def _BuildOpcodeResult(op_id):
+ """Build opcode result documentation.
+
+ @type op_id: string
+ @param op_id: Opcode ID
+
+ """
+ op_cls = opcodes.OP_MAPPING[op_id]
+
+ result_fn = getattr(op_cls, "OP_RESULT", None)
+
+ if not result_fn:
+ raise OpcodeError("Opcode '%s' has no result description" % op_id)
+
+ return "``%s``" % result_fn
+
+
+class OpcodeParams(s_compat.Directive):
"""Custom directive for opcode parameters.
See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
exclude = self.options.get("exclude", None)
alias = self.options.get("alias", {})
- tab_width = 2
path = op_id
- include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
+ include_text = "\n\n".join(_BuildOpcodeParams(op_id,
+ include,
+ exclude,
+ alias))
# Inject into state machine
- include_lines = docutils.statemachine.string2lines(include_text, tab_width,
+ include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
+ convert_whitespace=1)
+ self.state_machine.insert_input(include_lines, path)
+
+ return []
+
+
+class OpcodeResult(s_compat.Directive):
+ """Custom directive for opcode result.
+
+ See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
+
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+
+ def run(self):
+ op_id = self.arguments[0]
+
+ path = op_id
+ include_text = _BuildOpcodeResult(op_id)
+
+ # Inject into state machine
+ include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
convert_whitespace=1)
self.state_machine.insert_input(include_lines, path)
The expression's result is included as a literal.
"""
+ # pylint: disable=W0102,W0613,W0142
+ # W0102: Dangerous default value as argument
+ # W0142: Used * or ** magic
+ # W0613: Unused argument
+
code = docutils.utils.unescape(text, restore_backslashes=True)
try:
result = eval(code, EVAL_NS)
- except Exception, err:
+ except Exception, err: # pylint: disable=W0703
msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
line=lineno)
return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
return ([node], [])
-class PythonAssert(sphinx.util.compat.Directive):
+class PythonAssert(s_compat.Directive):
"""Custom directive for writing assertions.
The content must be a valid Python expression. If its result does not
final_argument_whitespace = False
def run(self):
- self.assert_has_content()
+ # Handle combinations of Sphinx and docutils not providing the wanted method
+ if hasattr(self, "assert_has_content"):
+ self.assert_has_content()
+ else:
+ assert self.content
code = "\n".join(self.content)
return []
+def BuildQueryFields(fields):
+ """Build query fields documentation.
+
+ @type fields: dict (field name as key, field details as value)
+
+ """
+ defs = [(fdef.name, fdef.doc)
+ for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
+ key=compat.fst)]
+ return BuildValuesDoc(defs)
+
+
+def BuildValuesDoc(values):
+ """Builds documentation for a list of values
+
+ @type values: list of tuples in the form (value, documentation)
+
+ """
+ for name, doc in values:
+ assert len(doc.splitlines()) == 1
+ yield "``%s``" % (name,)
+ yield " %s" % (doc,)
+
+
+def _ManPageNodeClass(*args, **kwargs):
+ """Generates a pending XRef like a ":doc:`...`" reference.
+
+ """
+ # Type for sphinx/environment.py:BuildEnvironment.resolve_references
+ kwargs["reftype"] = "doc"
+
+ # Force custom title
+ kwargs["refexplicit"] = True
+
+ return sphinx.addnodes.pending_xref(*args, **kwargs)
+
+
+class _ManPageXRefRole(sphinx.roles.XRefRole):
+ def __init__(self):
+ """Initializes this class.
+
+ """
+ sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
+ warn_dangling=True)
+
+ assert not hasattr(self, "converted"), \
+ "Sphinx base class gained an attribute named 'converted'"
+
+ self.converted = None
+
+ def process_link(self, env, refnode, has_explicit_title, title, target):
+ """Specialization for man page links.
+
+ """
+ if has_explicit_title:
+ raise ReSTError("Setting explicit title is not allowed for man pages")
+
+ # Check format and extract name and section
+ m = _MAN_RE.match(title)
+ if not m:
+ raise ReSTError("Man page reference '%s' does not match regular"
+ " expression '%s'" % (title, _MAN_RE.pattern))
+
+ name = m.group("name")
+ section = int(m.group("section"))
+
+ wanted_section = _autoconf.MAN_PAGES.get(name, None)
+
+ if not (wanted_section is None or wanted_section == section):
+ raise ReSTError("Referenced man page '%s' has section number %s, but the"
+ " reference uses section %s" %
+ (name, wanted_section, section))
+
+ self.converted = bool(wanted_section is not None and
+ env.app.config.enable_manpages)
+
+ if self.converted:
+ # Create link to known man page
+ return (title, "man-%s" % name)
+ else:
+ # No changes
+ return (title, target)
+
+
+def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
+ options={}, content=[]):
+ """Custom role for man page references.
+
+ Converts man pages to links if enabled during the build.
+
+ """
+ xref = _ManPageXRefRole()
+
+ assert ht.TNone(xref.converted)
+
+ # Check if it's a known man page
+ try:
+ result = xref(typ, rawtext, text, lineno, inliner,
+ options=options, content=content)
+ except ReSTError, err:
+ msg = inliner.reporter.error(str(err), line=lineno)
+ return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
+
+ assert ht.TBool(xref.converted)
+
+ # Return if the conversion was successful (i.e. the man page was known and
+ # conversion was enabled)
+ if xref.converted:
+ return result
+
+ # Fallback if man page links are disabled or an unknown page is referenced
+ return orig_manpage_role(typ, rawtext, text, lineno, inliner,
+ options=options, content=content)
+
+
+def _EncodeRapiResourceLink(method, uri):
+ """Encodes a RAPI resource URI for use as a link target.
+
+ """
+ parts = [RAPI_URI_ENCODE_RE.sub("-", uri.lower()).strip("-")]
+
+ if method is not None:
+ parts.append(method.lower())
+
+ return "rapi-res-%s" % "+".join(filter(None, parts))
+
+
+def _MakeRapiResourceLink(method, uri):
+ """Generates link target name for RAPI resource.
+
+ """
+ if uri in ["/", "/2"]:
+ # Don't link these
+ return None
+
+ elif uri == "/version":
+ return _EncodeRapiResourceLink(method, uri)
+
+ elif uri.startswith("/2/"):
+ return _EncodeRapiResourceLink(method, uri[len("/2/"):])
+
+ else:
+ raise ReSTError("Unhandled URI '%s'" % uri)
+
+
+def _GetHandlerMethods(handler):
+ """Returns list of HTTP methods supported by handler class.
+
+ @type handler: L{rapi.baserlib.ResourceBase}
+ @param handler: Handler class
+ @rtype: list of strings
+
+ """
+ return sorted(method
+ for (method, op_attr, _, _) in rapi.baserlib.OPCODE_ATTRS
+ # Only if handler supports method
+ if hasattr(handler, method) or hasattr(handler, op_attr))
+
+
+def _DescribeHandlerAccess(handler, method):
+ """Returns textual description of required RAPI permissions.
+
+ @type handler: L{rapi.baserlib.ResourceBase}
+ @param handler: Handler class
+ @type method: string
+ @param method: HTTP method (e.g. L{http.HTTP_GET})
+ @rtype: string
+
+ """
+ access = rapi.baserlib.GetHandlerAccess(handler, method)
+
+ if access:
+ return utils.CommaJoin(sorted(access))
+ else:
+ return "*(none)*"
+
+
+class _RapiHandlersForDocsHelper(object):
+ @classmethod
+ def Build(cls):
+ """Returns dictionary of resource handlers.
+
+ """
+ resources = \
+ rapi.connector.GetHandlers("[node_name]", "[instance_name]",
+ "[group_name]", "[network_name]", "[job_id]",
+ "[disk_index]", "[resource]",
+ translate=cls._TranslateResourceUri)
+
+ return resources
+
+ @classmethod
+ def _TranslateResourceUri(cls, *args):
+ """Translates a resource URI for use in documentation.
+
+ @see: L{rapi.connector.GetHandlers}
+
+ """
+ return "".join(map(cls._UriPatternToString, args))
+
+ @staticmethod
+ def _UriPatternToString(value):
+ """Converts L{rapi.connector.UriPattern} to strings.
+
+ """
+ if isinstance(value, rapi.connector.UriPattern):
+ return value.content
+ else:
+ return value
+
+
+_RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
+
+
+def _BuildRapiAccessTable(res):
+ """Build a table with access permissions needed for all RAPI resources.
+
+ """
+ for (uri, handler) in utils.NiceSort(res.items(), key=compat.fst):
+ reslink = _MakeRapiResourceLink(None, uri)
+ if not reslink:
+ # No link was generated
+ continue
+
+ yield ":ref:`%s <%s>`" % (uri, reslink)
+
+ for method in _GetHandlerMethods(handler):
+ yield (" | :ref:`%s <%s>`: %s" %
+ (method, _MakeRapiResourceLink(method, uri),
+ _DescribeHandlerAccess(handler, method)))
+
+
+class RapiAccessTable(s_compat.Directive):
+ """Custom directive to generate table of all RAPI resources.
+
+ See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
+
+ """
+ has_content = False
+ required_arguments = 0
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ include_text = "\n".join(_BuildRapiAccessTable(_RAPI_RESOURCES_FOR_DOCS))
+
+ # Inject into state machine
+ include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
+ convert_whitespace=1)
+ self.state_machine.insert_input(include_lines, self.__class__.__name__)
+
+ return []
+
+
+class RapiResourceDetails(s_compat.Directive):
+ """Custom directive for RAPI resource details.
+
+ See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
+
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = False
+
+ def run(self):
+ uri = self.arguments[0]
+
+ try:
+ handler = _RAPI_RESOURCES_FOR_DOCS[uri]
+ except KeyError:
+ raise self.error("Unknown resource URI '%s'" % uri)
+
+ lines = [
+ ".. list-table::",
+ " :widths: 1 4",
+ " :header-rows: 1",
+ "",
+ " * - Method",
+ " - :ref:`Required permissions <rapi-users>`",
+ ]
+
+ for method in _GetHandlerMethods(handler):
+ lines.extend([
+ " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)),
+ " - %s" % _DescribeHandlerAccess(handler, method),
+ ])
+
+ # Inject into state machine
+ include_lines = \
+ docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH,
+ convert_whitespace=1)
+ self.state_machine.insert_input(include_lines, self.__class__.__name__)
+
+ return []
+
+
def setup(app):
"""Sphinx extension callback.
"""
+ # TODO: Implement Sphinx directive for query fields
app.add_directive("opcode_params", OpcodeParams)
+ app.add_directive("opcode_result", OpcodeResult)
app.add_directive("pyassert", PythonAssert)
app.add_role("pyeval", PythonEvalRole)
+ app.add_directive("rapi_access_table", RapiAccessTable)
+ app.add_directive("rapi_resource_details", RapiResourceDetails)
+
+ app.add_config_value("enable_manpages", False, True)
+ app.add_role("manpage", _ManPageRole)