1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Sphinx extension for building opcode documentation.
32
33 """
34
35 import re
36 from cStringIO import StringIO
37
38 import docutils.statemachine
39 import docutils.nodes
40 import docutils.utils
41 import docutils.parsers.rst
42
43 import sphinx.errors
44 import sphinx.util.compat
45 import sphinx.roles
46 import sphinx.addnodes
47
48 s_compat = sphinx.util.compat
49
50 try:
51
52
53 orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
54 except (AttributeError, ValueError, KeyError), err:
55
56 raise Exception("Can't find reST role named 'manpage': %s" % err)
57
58 from ganeti import _constants
59 from ganeti import constants
60 from ganeti import compat
61 from ganeti import errors
62 from ganeti import utils
63 from ganeti import opcodes
64 from ganeti import opcodes_base
65 from ganeti import ht
66 from ganeti import rapi
67 from ganeti import luxi
68 from ganeti import objects
69 from ganeti import http
70 from ganeti import pathutils
71
72 import ganeti.rapi.rlib2
73 import ganeti.rapi.connector
74
75
76
77 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
78
79 _TAB_WIDTH = 2
80
81 RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I)
85 """Custom class for generating errors in Sphinx.
86
87 """
88
100
101
102 COMMON_PARAM_NAMES = _GetCommonParamNames()
103
104
105 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
106 rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects,
107 http=http, pathutils=pathutils)
108
109
110 CV_ECODES_DOC = "ecodes"
111
112
113 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
114 DOCUMENTED_CONSTANTS = {
115 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
116 }
121
124 """Split simple option list.
125
126 @type text: string
127 @param text: Options, e.g. "foo, bar, baz"
128
129 """
130 return [i.strip(",").strip() for i in text.split()]
131
134 """Parse simple assignment option.
135
136 @type text: string
137 @param text: Assignments, e.g. "foo=bar, hello=world"
138 @rtype: dict
139
140 """
141 result = {}
142
143 for part in _SplitOption(text):
144 if "=" not in part:
145 raise OpcodeError("Invalid option format, missing equal sign")
146
147 (name, value) = part.split("=", 1)
148
149 result[name.strip()] = value.strip()
150
151 return result
152
155 """Build opcode parameter documentation.
156
157 @type op_id: string
158 @param op_id: Opcode ID
159
160 """
161 op_cls = opcodes.OP_MAPPING[op_id]
162
163 params_with_alias = \
164 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
165 for (name, default, test, doc) in op_cls.GetAllParams()],
166 key=compat.fst)
167
168 for (rapi_name, name, default, test, doc) in params_with_alias:
169
170 if (name in COMMON_PARAM_NAMES and
171 (not include or name not in include)):
172 continue
173 if exclude is not None and name in exclude:
174 continue
175 if include is not None and name not in include:
176 continue
177
178 has_default = default is not None or default is not ht.NoDefault
179 has_test = test is not None
180
181 buf = StringIO()
182 buf.write("``%s``" % (rapi_name,))
183 if has_default or has_test:
184 buf.write(" (")
185 if has_default:
186 if default == "":
187 buf.write("defaults to the empty string")
188 else:
189 buf.write("defaults to ``%s``" % (default,))
190 if has_test:
191 buf.write(", ")
192 if has_test:
193 buf.write("must be ``%s``" % (test,))
194 buf.write(")")
195 yield buf.getvalue()
196
197
198 for line in doc.splitlines():
199 yield " %s" % line
200
203 """Build opcode result documentation.
204
205 @type op_id: string
206 @param op_id: Opcode ID
207
208 """
209 op_cls = opcodes.OP_MAPPING[op_id]
210
211 result_fn = getattr(op_cls, "OP_RESULT", None)
212
213 if not result_fn:
214 raise OpcodeError("Opcode '%s' has no result description" % op_id)
215
216 return "``%s``" % result_fn
217
220 """Custom directive for opcode parameters.
221
222 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
223
224 """
225 has_content = False
226 required_arguments = 1
227 optional_arguments = 0
228 final_argument_whitespace = False
229 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
230 alias=_ParseAlias)
231
233 op_id = self.arguments[0]
234 include = self.options.get("include", None)
235 exclude = self.options.get("exclude", None)
236 alias = self.options.get("alias", {})
237
238 path = op_id
239 include_text = "\n\n".join(_BuildOpcodeParams(op_id,
240 include,
241 exclude,
242 alias))
243
244
245 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
246 convert_whitespace=1)
247 self.state_machine.insert_input(include_lines, path)
248
249 return []
250
253 """Custom directive for opcode result.
254
255 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
256
257 """
258 has_content = False
259 required_arguments = 1
260 optional_arguments = 0
261 final_argument_whitespace = False
262
264 op_id = self.arguments[0]
265
266 path = op_id
267 include_text = _BuildOpcodeResult(op_id)
268
269
270 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
271 convert_whitespace=1)
272 self.state_machine.insert_input(include_lines, path)
273
274 return []
275
276
277 -def PythonEvalRole(role, rawtext, text, lineno, inliner,
278 options={}, content=[]):
279 """Custom role to evaluate Python expressions.
280
281 The expression's result is included as a literal.
282
283 """
284
285
286
287
288
289 code = docutils.utils.unescape(text, restore_backslashes=True)
290
291 try:
292 result = eval(code, EVAL_NS)
293 except Exception, err:
294 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
295 line=lineno)
296 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
297
298 node = docutils.nodes.literal("", unicode(result), **options)
299
300 return ([node], [])
301
304 """Custom directive for writing assertions.
305
306 The content must be a valid Python expression. If its result does not
307 evaluate to C{True}, the assertion fails.
308
309 """
310 has_content = True
311 required_arguments = 0
312 optional_arguments = 0
313 final_argument_whitespace = False
314
316
317 if hasattr(self, "assert_has_content"):
318 self.assert_has_content()
319 else:
320 assert self.content
321
322 code = "\n".join(self.content)
323
324 try:
325 result = eval(code, EVAL_NS)
326 except Exception, err:
327 raise self.error("Failed to evaluate %r: %s" % (code, err))
328
329 if not result:
330 raise self.error("Assertion failed: %s" % (code, ))
331
332 return []
333
336 """Build query fields documentation.
337
338 @type fields: dict (field name as key, field details as value)
339
340 """
341 defs = [(fdef.name, fdef.doc)
342 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
343 key=compat.fst)]
344 return BuildValuesDoc(defs)
345
348 """Builds documentation for a list of values
349
350 @type values: list of tuples in the form (value, documentation)
351
352 """
353 for name, doc in values:
354 assert len(doc.splitlines()) == 1
355 yield "``%s``" % (name,)
356 yield " %s" % (doc,)
357
358
359 -def _ManPageNodeClass(*args, **kwargs):
360 """Generates a pending XRef like a ":doc:`...`" reference.
361
362 """
363
364 kwargs["reftype"] = "doc"
365
366
367 kwargs["refexplicit"] = True
368
369 return sphinx.addnodes.pending_xref(*args, **kwargs)
370
371
372 -class _ManPageXRefRole(sphinx.roles.XRefRole):
373 - def __init__(self):
374 """Initializes this class.
375
376 """
377 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
378 warn_dangling=True)
379
380 assert not hasattr(self, "converted"), \
381 "Sphinx base class gained an attribute named 'converted'"
382
383 self.converted = None
384
385 - def process_link(self, env, refnode, has_explicit_title, title, target):
386 """Specialization for man page links.
387
388 """
389 if has_explicit_title:
390 raise ReSTError("Setting explicit title is not allowed for man pages")
391
392
393 m = _MAN_RE.match(title)
394 if not m:
395 raise ReSTError("Man page reference '%s' does not match regular"
396 " expression '%s'" % (title, _MAN_RE.pattern))
397
398 name = m.group("name")
399 section = int(m.group("section"))
400
401 wanted_section = _constants.MAN_PAGES.get(name, None)
402
403 if not (wanted_section is None or wanted_section == section):
404 raise ReSTError("Referenced man page '%s' has section number %s, but the"
405 " reference uses section %s" %
406 (name, wanted_section, section))
407
408 self.converted = bool(wanted_section is not None and
409 env.app.config.enable_manpages)
410
411 if self.converted:
412
413 return (title, "man-%s" % name)
414 else:
415
416 return (title, target)
417
421 """Custom role for man page references.
422
423 Converts man pages to links if enabled during the build.
424
425 """
426 xref = _ManPageXRefRole()
427
428 assert ht.TNone(xref.converted)
429
430
431 try:
432 result = xref(typ, rawtext, text, lineno, inliner,
433 options=options, content=content)
434 except ReSTError, err:
435 msg = inliner.reporter.error(str(err), line=lineno)
436 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
437
438 assert ht.TBool(xref.converted)
439
440
441
442 if xref.converted:
443 return result
444
445
446 return orig_manpage_role(typ, rawtext, text, lineno, inliner,
447 options=options, content=content)
448
451 """Encodes a RAPI resource URI for use as a link target.
452
453 """
454 parts = [RAPI_URI_ENCODE_RE.sub("-", uri.lower()).strip("-")]
455
456 if method is not None:
457 parts.append(method.lower())
458
459 return "rapi-res-%s" % "+".join(filter(None, parts))
460
463 """Generates link target name for RAPI resource.
464
465 """
466 if uri in ["/", "/2"]:
467
468 return None
469
470 elif uri == "/version":
471 return _EncodeRapiResourceLink(method, uri)
472
473 elif uri.startswith("/2/"):
474 return _EncodeRapiResourceLink(method, uri[len("/2/"):])
475
476 else:
477 raise ReSTError("Unhandled URI '%s'" % uri)
478
481 """Returns list of HTTP methods supported by handler class.
482
483 @type handler: L{rapi.baserlib.ResourceBase}
484 @param handler: Handler class
485 @rtype: list of strings
486
487 """
488 return sorted(method
489 for (method, op_attr, _, _, _) in rapi.baserlib.OPCODE_ATTRS
490
491 if hasattr(handler, method) or hasattr(handler, op_attr))
492
495 """Returns textual description of required RAPI permissions.
496
497 @type handler: L{rapi.baserlib.ResourceBase}
498 @param handler: Handler class
499 @type method: string
500 @param method: HTTP method (e.g. L{http.HTTP_GET})
501 @rtype: string
502
503 """
504 access = rapi.baserlib.GetHandlerAccess(handler, method)
505
506 if access:
507 return utils.CommaJoin(sorted(access))
508 else:
509 return "*(none)*"
510
513 @classmethod
515 """Returns dictionary of resource handlers.
516
517 """
518 resources = \
519 rapi.connector.GetHandlers("[node_name]", "[instance_name]",
520 "[group_name]", "[network_name]", "[job_id]",
521 "[disk_index]", "[resource]",
522 translate=cls._TranslateResourceUri)
523
524 return resources
525
526 @classmethod
528 """Translates a resource URI for use in documentation.
529
530 @see: L{rapi.connector.GetHandlers}
531
532 """
533 return "".join(map(cls._UriPatternToString, args))
534
535 @staticmethod
537 """Converts L{rapi.connector.UriPattern} to strings.
538
539 """
540 if isinstance(value, rapi.connector.UriPattern):
541 return value.content
542 else:
543 return value
544
545
546 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
565
588
591 """Custom directive for RAPI resource details.
592
593 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
594
595 """
596 has_content = False
597 required_arguments = 1
598 optional_arguments = 0
599 final_argument_whitespace = False
600
602 uri = self.arguments[0]
603
604 try:
605 handler = _RAPI_RESOURCES_FOR_DOCS[uri]
606 except KeyError:
607 raise self.error("Unknown resource URI '%s'" % uri)
608
609 lines = [
610 ".. list-table::",
611 " :widths: 1 4",
612 " :header-rows: 1",
613 "",
614 " * - Method",
615 " - :ref:`Required permissions <rapi-users>`",
616 ]
617
618 for method in _GetHandlerMethods(handler):
619 lines.extend([
620 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)),
621 " - %s" % _DescribeHandlerAccess(handler, method),
622 ])
623
624
625 include_lines = \
626 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH,
627 convert_whitespace=1)
628 self.state_machine.insert_input(include_lines, self.__class__.__name__)
629
630 return []
631
647