Hs2Py constants: add remaining '_autoconf.*' constants
[ganeti-local] / lib / build / sphinx_ext.py
index 0af8ff8..2280335 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# 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):
@@ -94,10 +153,10 @@ def _BuildOpcodeParams(op_id, include, exclude, alias):
   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
@@ -106,19 +165,19 @@ def _BuildOpcodeParams(op_id, include, exclude, alias):
     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()
 
@@ -127,7 +186,24 @@ def _BuildOpcodeParams(op_id, include, exclude, alias):
       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>.
@@ -146,12 +222,39 @@ class OpcodeParams(sphinx.util.compat.Directive):
     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)
 
@@ -165,11 +268,16 @@ def PythonEvalRole(role, rawtext, text, lineno, inliner,
   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])
@@ -179,7 +287,7 @@ def PythonEvalRole(role, rawtext, text, lineno, inliner,
   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
@@ -192,7 +300,11 @@ class PythonAssert(sphinx.util.compat.Directive):
   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)
 
@@ -207,10 +319,315 @@ class PythonAssert(sphinx.util.compat.Directive):
     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)