Package ganeti :: Package build :: Module sphinx_ext
[hide private]
[frames] | no frames]

Source Code for Module ganeti.build.sphinx_ext

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2011, 2012, 2013 Google Inc. 
  5  # 
  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. 
 10  # 
 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. 
 15  # 
 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 
 19  # 02110-1301, USA. 
 20   
 21   
 22  """Sphinx extension for building opcode documentation. 
 23   
 24  """ 
 25   
 26  import re 
 27  from cStringIO import StringIO 
 28   
 29  import docutils.statemachine 
 30  import docutils.nodes 
 31  import docutils.utils 
 32  import docutils.parsers.rst 
 33   
 34  import sphinx.errors 
 35  import sphinx.util.compat 
 36  import sphinx.roles 
 37  import sphinx.addnodes 
 38   
 39  s_compat = sphinx.util.compat 
 40   
 41  try: 
 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) 
 48   
 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 
 54  from ganeti import ht 
 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 
 60   
 61  import ganeti.rapi.rlib2 # pylint: disable=W0611 
 62  import ganeti.rapi.connector # pylint: disable=W0611 
 63   
 64   
 65  #: Regular expression for man page names 
 66  _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$") 
 67   
 68  _TAB_WIDTH = 2 
 69   
 70  RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I) 
71 72 73 -class ReSTError(Exception):
74 """Custom class for generating errors in Sphinx. 75 76 """
77
78 79 -def _GetCommonParamNames():
80 """Builds a list of parameters common to all opcodes. 81 82 """ 83 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS)) 84 85 # The "depends" attribute should be listed 86 names.remove(opcodes.DEPEND_ATTR) 87 88 return names
89 90 91 COMMON_PARAM_NAMES = _GetCommonParamNames() 92 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, 96 http=http) 97 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, 105 }
106 107 108 -class OpcodeError(sphinx.errors.SphinxError):
109 category = "Opcode error"
110
111 112 -def _SplitOption(text):
113 """Split simple option list. 114 115 @type text: string 116 @param text: Options, e.g. "foo, bar, baz" 117 118 """ 119 return [i.strip(",").strip() for i in text.split()]
120
121 122 -def _ParseAlias(text):
123 """Parse simple assignment option. 124 125 @type text: string 126 @param text: Assignments, e.g. "foo=bar, hello=world" 127 @rtype: dict 128 129 """ 130 result = {} 131 132 for part in _SplitOption(text): 133 if "=" not in part: 134 raise OpcodeError("Invalid option format, missing equal sign") 135 136 (name, value) = part.split("=", 1) 137 138 result[name.strip()] = value.strip() 139 140 return result
141
142 143 -def _BuildOpcodeParams(op_id, include, exclude, alias):
144 """Build opcode parameter documentation. 145 146 @type op_id: string 147 @param op_id: Opcode ID 148 149 """ 150 op_cls = opcodes.OP_MAPPING[op_id] 151 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()], 155 key=compat.fst) 156 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)): 161 continue 162 if exclude is not None and name in exclude: 163 continue 164 if include is not None and name not in include: 165 continue 166 167 has_default = default is not ht.NoDefault 168 has_test = not (test is None or test is ht.NoType) 169 170 buf = StringIO() 171 buf.write("``%s``" % (rapi_name,)) 172 if has_default or has_test: 173 buf.write(" (") 174 if has_default: 175 buf.write("defaults to ``%s``" % (default,)) 176 if has_test: 177 buf.write(", ") 178 if has_test: 179 buf.write("must be ``%s``" % (test,)) 180 buf.write(")") 181 yield buf.getvalue() 182 183 # Add text 184 for line in doc.splitlines(): 185 yield " %s" % line
186
187 188 -def _BuildOpcodeResult(op_id):
189 """Build opcode result documentation. 190 191 @type op_id: string 192 @param op_id: Opcode ID 193 194 """ 195 op_cls = opcodes.OP_MAPPING[op_id] 196 197 result_fn = getattr(op_cls, "OP_RESULT", None) 198 199 if not result_fn: 200 raise OpcodeError("Opcode '%s' has no result description" % op_id) 201 202 return "``%s``" % result_fn
203
204 205 -class OpcodeParams(s_compat.Directive):
206 """Custom directive for opcode parameters. 207 208 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 209 210 """ 211 has_content = False 212 required_arguments = 1 213 optional_arguments = 0 214 final_argument_whitespace = False 215 option_spec = dict(include=_SplitOption, exclude=_SplitOption, 216 alias=_ParseAlias) 217
218 - def run(self):
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", {}) 223 224 path = op_id 225 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias)) 226 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) 231 232 return []
233
234 235 -class OpcodeResult(s_compat.Directive):
236 """Custom directive for opcode result. 237 238 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 239 240 """ 241 has_content = False 242 required_arguments = 1 243 optional_arguments = 0 244 final_argument_whitespace = False 245
246 - def run(self):
247 op_id = self.arguments[0] 248 249 path = op_id 250 include_text = _BuildOpcodeResult(op_id) 251 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) 256 257 return []
258
259 260 -def PythonEvalRole(role, rawtext, text, lineno, inliner, 261 options={}, content=[]):
262 """Custom role to evaluate Python expressions. 263 264 The expression's result is included as a literal. 265 266 """ 267 # pylint: disable=W0102,W0613,W0142 268 # W0102: Dangerous default value as argument 269 # W0142: Used * or ** magic 270 # W0613: Unused argument 271 272 code = docutils.utils.unescape(text, restore_backslashes=True) 273 274 try: 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), 278 line=lineno) 279 return ([inliner.problematic(rawtext, rawtext, msg)], [msg]) 280 281 node = docutils.nodes.literal("", unicode(result), **options) 282 283 return ([node], [])
284
285 286 -class PythonAssert(s_compat.Directive):
287 """Custom directive for writing assertions. 288 289 The content must be a valid Python expression. If its result does not 290 evaluate to C{True}, the assertion fails. 291 292 """ 293 has_content = True 294 required_arguments = 0 295 optional_arguments = 0 296 final_argument_whitespace = False 297
298 - def run(self):
299 # Handle combinations of Sphinx and docutils not providing the wanted method 300 if hasattr(self, "assert_has_content"): 301 self.assert_has_content() 302 else: 303 assert self.content 304 305 code = "\n".join(self.content) 306 307 try: 308 result = eval(code, EVAL_NS) 309 except Exception, err: 310 raise self.error("Failed to evaluate %r: %s" % (code, err)) 311 312 if not result: 313 raise self.error("Assertion failed: %s" % (code, )) 314 315 return []
316
317 318 -def BuildQueryFields(fields):
319 """Build query fields documentation. 320 321 @type fields: dict (field name as key, field details as value) 322 323 """ 324 defs = [(fdef.name, fdef.doc) 325 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(), 326 key=compat.fst)] 327 return BuildValuesDoc(defs)
328
329 330 -def BuildValuesDoc(values):
331 """Builds documentation for a list of values 332 333 @type values: list of tuples in the form (value, documentation) 334 335 """ 336 for name, doc in values: 337 assert len(doc.splitlines()) == 1 338 yield "``%s``" % (name,) 339 yield " %s" % (doc,)
340
341 342 -def _ManPageNodeClass(*args, **kwargs):
343 """Generates a pending XRef like a ":doc:`...`" reference. 344 345 """ 346 # Type for sphinx/environment.py:BuildEnvironment.resolve_references 347 kwargs["reftype"] = "doc" 348 349 # Force custom title 350 kwargs["refexplicit"] = True 351 352 return sphinx.addnodes.pending_xref(*args, **kwargs)
353
354 355 -class _ManPageXRefRole(sphinx.roles.XRefRole):
356 - def __init__(self):
357 """Initializes this class. 358 359 """ 360 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass, 361 warn_dangling=True) 362 363 assert not hasattr(self, "converted"), \ 364 "Sphinx base class gained an attribute named 'converted'" 365 366 self.converted = None
367
400
401 402 -def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102 403 options={}, content=[]):
404 """Custom role for man page references. 405 406 Converts man pages to links if enabled during the build. 407 408 """ 409 xref = _ManPageXRefRole() 410 411 assert ht.TNone(xref.converted) 412 413 # Check if it's a known man page 414 try: 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]) 420 421 assert ht.TBool(xref.converted) 422 423 # Return if the conversion was successful (i.e. the man page was known and 424 # conversion was enabled) 425 if xref.converted: 426 return result 427 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)
431 443 461
462 463 -def _GetHandlerMethods(handler):
464 """Returns list of HTTP methods supported by handler class. 465 466 @type handler: L{rapi.baserlib.ResourceBase} 467 @param handler: Handler class 468 @rtype: list of strings 469 470 """ 471 return sorted(method 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))
475
476 477 -def _DescribeHandlerAccess(handler, method):
478 """Returns textual description of required RAPI permissions. 479 480 @type handler: L{rapi.baserlib.ResourceBase} 481 @param handler: Handler class 482 @type method: string 483 @param method: HTTP method (e.g. L{http.HTTP_GET}) 484 @rtype: string 485 486 """ 487 access = rapi.baserlib.GetHandlerAccess(handler, method) 488 489 if access: 490 return utils.CommaJoin(sorted(access)) 491 else: 492 return "*(none)*"
493
494 495 -class _RapiHandlersForDocsHelper(object):
496 @classmethod
497 - def Build(cls):
498 """Returns dictionary of resource handlers. 499 500 """ 501 resources = \ 502 rapi.connector.GetHandlers("[node_name]", "[instance_name]", 503 "[group_name]", "[network_name]", "[job_id]", 504 "[disk_index]", "[resource]", 505 translate=cls._TranslateResourceUri) 506 507 return resources
508 509 @classmethod
510 - def _TranslateResourceUri(cls, *args):
511 """Translates a resource URI for use in documentation. 512 513 @see: L{rapi.connector.GetHandlers} 514 515 """ 516 return "".join(map(cls._UriPatternToString, args))
517 518 @staticmethod
519 - def _UriPatternToString(value):
520 """Converts L{rapi.connector.UriPattern} to strings. 521 522 """ 523 if isinstance(value, rapi.connector.UriPattern): 524 return value.content 525 else: 526 return value
527 528 529 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
530 531 532 -def _BuildRapiAccessTable(res):
533 """Build a table with access permissions needed for all RAPI resources. 534 535 """ 536 for (uri, handler) in utils.NiceSort(res.items(), key=compat.fst): 537 reslink = _MakeRapiResourceLink(None, uri) 538 if not reslink: 539 # No link was generated 540 continue 541 542 yield ":ref:`%s <%s>`" % (uri, reslink) 543 544 for method in _GetHandlerMethods(handler): 545 yield (" | :ref:`%s <%s>`: %s" % 546 (method, _MakeRapiResourceLink(method, uri), 547 _DescribeHandlerAccess(handler, method)))
548
549 550 -class RapiAccessTable(s_compat.Directive):
551 """Custom directive to generate table of all RAPI resources. 552 553 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 554 555 """ 556 has_content = False 557 required_arguments = 0 558 optional_arguments = 0 559 final_argument_whitespace = False 560 option_spec = {} 561
562 - def run(self):
563 include_text = "\n".join(_BuildRapiAccessTable(_RAPI_RESOURCES_FOR_DOCS)) 564 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__) 569 570 return []
571
572 573 -class RapiResourceDetails(s_compat.Directive):
574 """Custom directive for RAPI resource details. 575 576 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 577 578 """ 579 has_content = False 580 required_arguments = 1 581 optional_arguments = 0 582 final_argument_whitespace = False 583
584 - def run(self):
585 uri = self.arguments[0] 586 587 try: 588 handler = _RAPI_RESOURCES_FOR_DOCS[uri] 589 except KeyError: 590 raise self.error("Unknown resource URI '%s'" % uri) 591 592 lines = [ 593 ".. list-table::", 594 " :widths: 1 4", 595 " :header-rows: 1", 596 "", 597 " * - Method", 598 " - :ref:`Required permissions <rapi-users>`", 599 ] 600 601 for method in _GetHandlerMethods(handler): 602 lines.extend([ 603 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)), 604 " - %s" % _DescribeHandlerAccess(handler, method), 605 ]) 606 607 # Inject into state machine 608 include_lines = \ 609 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH, 610 convert_whitespace=1) 611 self.state_machine.insert_input(include_lines, self.__class__.__name__) 612 613 return []
614
615 616 -def setup(app):
617 """Sphinx extension callback. 618 619 """ 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) 627 628 app.add_config_value("enable_manpages", False, True) 629 app.add_role("manpage", _ManPageRole)
630