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