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
 #
 # 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
 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.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 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 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
 
 #: 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):
 
 
 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()],
   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:
 
   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 (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
 
     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 = StringIO()
-    buf.write("``%s``" % rapi_name)
+    buf.write("``%s``" % (rapi_name,))
     if has_default or has_test:
       buf.write(" (")
       if has_default:
     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:
         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()
 
       buf.write(")")
     yield buf.getvalue()
 
@@ -127,7 +186,24 @@ def _BuildOpcodeParams(op_id, include, exclude, alias):
       yield "  %s" % line
 
 
       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>.
   """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", {})
 
     exclude = self.options.get("exclude", None)
     alias = self.options.get("alias", {})
 
-    tab_width = 2
     path = op_id
     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
 
     # 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)
 
                                                        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.
 
   """
   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)
   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])
     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], [])
 
 
   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
   """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):
   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)
 
 
     code = "\n".join(self.content)
 
@@ -207,10 +319,315 @@ class PythonAssert(sphinx.util.compat.Directive):
     return []
 
 
     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.
 
   """
 def setup(app):
   """Sphinx extension callback.
 
   """
+  # TODO: Implement Sphinx directive for query fields
   app.add_directive("opcode_params", OpcodeParams)
   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("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)