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