Export ndparams in RAPI node query
[ganeti-local] / lib / build / sphinx_ext.py
1 #
2 #
3
4 # Copyright (C) 2011, 2012 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 from cStringIO import StringIO
27
28 import docutils.statemachine
29 import docutils.nodes
30 import docutils.utils
31
32 import sphinx.errors
33 import sphinx.util.compat
34
35 s_compat = sphinx.util.compat
36
37 from ganeti import constants
38 from ganeti import compat
39 from ganeti import errors
40 from ganeti import utils
41 from ganeti import opcodes
42 from ganeti import ht
43 from ganeti import rapi
44
45 import ganeti.rapi.rlib2 # pylint: disable=W0611
46
47
48 def _GetCommonParamNames():
49   """Builds a list of parameters common to all opcodes.
50
51   """
52   names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
53
54   # The "depends" attribute should be listed
55   names.remove(opcodes.DEPEND_ATTR)
56
57   return names
58
59
60 COMMON_PARAM_NAMES = _GetCommonParamNames()
61
62 #: Namespace for evaluating expressions
63 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
64                rlib2=rapi.rlib2)
65
66 # Constants documentation for man pages
67 CV_ECODES_DOC = "ecodes"
68 # We don't care about the leak of variables _, name and doc here.
69 # pylint: disable=W0621
70 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
71 DOCUMENTED_CONSTANTS = {
72   CV_ECODES_DOC: CV_ECODES_DOC_LIST,
73   }
74
75
76 class OpcodeError(sphinx.errors.SphinxError):
77   category = "Opcode error"
78
79
80 def _SplitOption(text):
81   """Split simple option list.
82
83   @type text: string
84   @param text: Options, e.g. "foo, bar, baz"
85
86   """
87   return [i.strip(",").strip() for i in text.split()]
88
89
90 def _ParseAlias(text):
91   """Parse simple assignment option.
92
93   @type text: string
94   @param text: Assignments, e.g. "foo=bar, hello=world"
95   @rtype: dict
96
97   """
98   result = {}
99
100   for part in _SplitOption(text):
101     if "=" not in part:
102       raise OpcodeError("Invalid option format, missing equal sign")
103
104     (name, value) = part.split("=", 1)
105
106     result[name.strip()] = value.strip()
107
108   return result
109
110
111 def _BuildOpcodeParams(op_id, include, exclude, alias):
112   """Build opcode parameter documentation.
113
114   @type op_id: string
115   @param op_id: Opcode ID
116
117   """
118   op_cls = opcodes.OP_MAPPING[op_id]
119
120   params_with_alias = \
121     utils.NiceSort([(alias.get(name, name), name, default, test, doc)
122                     for (name, default, test, doc) in op_cls.GetAllParams()],
123                    key=compat.fst)
124
125   for (rapi_name, name, default, test, doc) in params_with_alias:
126     # Hide common parameters if not explicitely included
127     if (name in COMMON_PARAM_NAMES and
128         (not include or name not in include)):
129       continue
130     if exclude is not None and name in exclude:
131       continue
132     if include is not None and name not in include:
133       continue
134
135     has_default = default is not ht.NoDefault
136     has_test = not (test is None or test is ht.NoType)
137
138     buf = StringIO()
139     buf.write("``%s``" % rapi_name)
140     if has_default or has_test:
141       buf.write(" (")
142       if has_default:
143         buf.write("defaults to ``%s``" % default)
144         if has_test:
145           buf.write(", ")
146       if has_test:
147         buf.write("must be ``%s``" % test)
148       buf.write(")")
149     yield buf.getvalue()
150
151     # Add text
152     for line in doc.splitlines():
153       yield "  %s" % line
154
155
156 def _BuildOpcodeResult(op_id):
157   """Build opcode result documentation.
158
159   @type op_id: string
160   @param op_id: Opcode ID
161
162   """
163   op_cls = opcodes.OP_MAPPING[op_id]
164
165   result_fn = getattr(op_cls, "OP_RESULT", None)
166
167   if not result_fn:
168     raise OpcodeError("Opcode '%s' has no result description" % op_id)
169
170   return "``%s``" % result_fn
171
172
173 class OpcodeParams(s_compat.Directive):
174   """Custom directive for opcode parameters.
175
176   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
177
178   """
179   has_content = False
180   required_arguments = 1
181   optional_arguments = 0
182   final_argument_whitespace = False
183   option_spec = dict(include=_SplitOption, exclude=_SplitOption,
184                      alias=_ParseAlias)
185
186   def run(self):
187     op_id = self.arguments[0]
188     include = self.options.get("include", None)
189     exclude = self.options.get("exclude", None)
190     alias = self.options.get("alias", {})
191
192     tab_width = 2
193     path = op_id
194     include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
195
196     # Inject into state machine
197     include_lines = docutils.statemachine.string2lines(include_text, tab_width,
198                                                        convert_whitespace=1)
199     self.state_machine.insert_input(include_lines, path)
200
201     return []
202
203
204 class OpcodeResult(s_compat.Directive):
205   """Custom directive for opcode result.
206
207   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
208
209   """
210   has_content = False
211   required_arguments = 1
212   optional_arguments = 0
213   final_argument_whitespace = False
214
215   def run(self):
216     op_id = self.arguments[0]
217
218     tab_width = 2
219     path = op_id
220     include_text = _BuildOpcodeResult(op_id)
221
222     # Inject into state machine
223     include_lines = docutils.statemachine.string2lines(include_text, tab_width,
224                                                        convert_whitespace=1)
225     self.state_machine.insert_input(include_lines, path)
226
227     return []
228
229
230 def PythonEvalRole(role, rawtext, text, lineno, inliner,
231                    options={}, content=[]):
232   """Custom role to evaluate Python expressions.
233
234   The expression's result is included as a literal.
235
236   """
237   # pylint: disable=W0102,W0613,W0142
238   # W0102: Dangerous default value as argument
239   # W0142: Used * or ** magic
240   # W0613: Unused argument
241
242   code = docutils.utils.unescape(text, restore_backslashes=True)
243
244   try:
245     result = eval(code, EVAL_NS)
246   except Exception, err: # pylint: disable=W0703
247     msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
248                                  line=lineno)
249     return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
250
251   node = docutils.nodes.literal("", unicode(result), **options)
252
253   return ([node], [])
254
255
256 class PythonAssert(s_compat.Directive):
257   """Custom directive for writing assertions.
258
259   The content must be a valid Python expression. If its result does not
260   evaluate to C{True}, the assertion fails.
261
262   """
263   has_content = True
264   required_arguments = 0
265   optional_arguments = 0
266   final_argument_whitespace = False
267
268   def run(self):
269     # Handle combinations of Sphinx and docutils not providing the wanted method
270     if hasattr(self, "assert_has_content"):
271       self.assert_has_content()
272     else:
273       assert self.content
274
275     code = "\n".join(self.content)
276
277     try:
278       result = eval(code, EVAL_NS)
279     except Exception, err:
280       raise self.error("Failed to evaluate %r: %s" % (code, err))
281
282     if not result:
283       raise self.error("Assertion failed: %s" % (code, ))
284
285     return []
286
287
288 def BuildQueryFields(fields):
289   """Build query fields documentation.
290
291   @type fields: dict (field name as key, field details as value)
292
293   """
294   defs = [(fdef.name, fdef.doc)
295            for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
296                                                       key=compat.fst)]
297   return BuildValuesDoc(defs)
298
299
300 def BuildValuesDoc(values):
301   """Builds documentation for a list of values
302
303   @type values: list of tuples in the form (value, documentation)
304
305   """
306   for name, doc in values:
307     assert len(doc.splitlines()) == 1
308     yield "``%s``" % name
309     yield "  %s" % doc
310
311
312 # TODO: Implement Sphinx directive for query fields
313
314
315 def setup(app):
316   """Sphinx extension callback.
317
318   """
319   app.add_directive("opcode_params", OpcodeParams)
320   app.add_directive("opcode_result", OpcodeResult)
321   app.add_directive("pyassert", PythonAssert)
322   app.add_role("pyeval", PythonEvalRole)