Instance failover: fix bug for INT_MIRROR cases
[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 import operator
27 from cStringIO import StringIO
28
29 import docutils.statemachine
30 import docutils.nodes
31 import docutils.utils
32
33 import sphinx.errors
34 import sphinx.util.compat
35
36 from ganeti import constants
37 from ganeti import compat
38 from ganeti import errors
39 from ganeti import utils
40 from ganeti import opcodes
41 from ganeti import ht
42
43
44 COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
45
46 #: Namespace for evaluating expressions
47 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors)
48
49
50 class OpcodeError(sphinx.errors.SphinxError):
51   category = "Opcode error"
52
53
54 def _SplitOption(text):
55   """Split simple option list.
56
57   @type text: string
58   @param text: Options, e.g. "foo, bar, baz"
59
60   """
61   return [i.strip(",").strip() for i in text.split()]
62
63
64 def _ParseAlias(text):
65   """Parse simple assignment option.
66
67   @type text: string
68   @param text: Assignments, e.g. "foo=bar, hello=world"
69   @rtype: dict
70
71   """
72   result = {}
73
74   for part in _SplitOption(text):
75     if "=" not in part:
76       raise OpcodeError("Invalid option format, missing equal sign")
77
78     (name, value) = part.split("=", 1)
79
80     result[name.strip()] = value.strip()
81
82   return result
83
84
85 def _BuildOpcodeParams(op_id, include, exclude, alias):
86   """Build opcode parameter documentation.
87
88   @type op_id: string
89   @param op_id: Opcode ID
90
91   """
92   op_cls = opcodes.OP_MAPPING[op_id]
93
94   params_with_alias = \
95     utils.NiceSort([(alias.get(name, name), name, default, test, doc)
96                     for (name, default, test, doc) in op_cls.GetAllParams()],
97                    key=operator.itemgetter(0))
98
99   for (rapi_name, name, default, test, doc) in params_with_alias:
100     # Hide common parameters if not explicitely included
101     if (name in COMMON_PARAM_NAMES and
102         (not include or name not in include)):
103       continue
104     if exclude is not None and name in exclude:
105       continue
106     if include is not None and name not in include:
107       continue
108
109     has_default = default is not ht.NoDefault
110     has_test = not (test is None or test is ht.NoType)
111
112     buf = StringIO()
113     buf.write("``%s``" % rapi_name)
114     if has_default or has_test:
115       buf.write(" (")
116       if has_default:
117         buf.write("defaults to ``%s``" % default)
118         if has_test:
119           buf.write(", ")
120       if has_test:
121         buf.write("must be ``%s``" % test)
122       buf.write(")")
123     yield buf.getvalue()
124
125     # Add text
126     for line in doc.splitlines():
127       yield "  %s" % line
128
129
130 class OpcodeParams(sphinx.util.compat.Directive):
131   """Custom directive for opcode parameters.
132
133   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
134
135   """
136   has_content = False
137   required_arguments = 1
138   optional_arguments = 0
139   final_argument_whitespace = False
140   option_spec = dict(include=_SplitOption, exclude=_SplitOption,
141                      alias=_ParseAlias)
142
143   def run(self):
144     op_id = self.arguments[0]
145     include = self.options.get("include", None)
146     exclude = self.options.get("exclude", None)
147     alias = self.options.get("alias", {})
148
149     tab_width = 2
150     path = op_id
151     include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
152
153     # Inject into state machine
154     include_lines = docutils.statemachine.string2lines(include_text, tab_width,
155                                                        convert_whitespace=1)
156     self.state_machine.insert_input(include_lines, path)
157
158     return []
159
160
161 def PythonEvalRole(role, rawtext, text, lineno, inliner,
162                    options={}, content=[]):
163   """Custom role to evaluate Python expressions.
164
165   The expression's result is included as a literal.
166
167   """
168   # pylint: disable-msg=W0102,W0613,W0142
169   # W0102: Dangerous default value as argument
170   # W0142: Used * or ** magic
171   # W0613: Unused argument
172
173   code = docutils.utils.unescape(text, restore_backslashes=True)
174
175   try:
176     result = eval(code, EVAL_NS)
177   except Exception, err: # pylint: disable-msg=W0703
178     msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
179                                  line=lineno)
180     return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
181
182   node = docutils.nodes.literal("", unicode(result), **options)
183
184   return ([node], [])
185
186
187 class PythonAssert(sphinx.util.compat.Directive):
188   """Custom directive for writing assertions.
189
190   The content must be a valid Python expression. If its result does not
191   evaluate to C{True}, the assertion fails.
192
193   """
194   has_content = True
195   required_arguments = 0
196   optional_arguments = 0
197   final_argument_whitespace = False
198
199   def run(self):
200     # Handle combinations of Sphinx and docutils not providing the wanted method
201     if hasattr(self, "assert_has_content"):
202       self.assert_has_content()
203     else:
204       assert self.content
205
206     code = "\n".join(self.content)
207
208     try:
209       result = eval(code, EVAL_NS)
210     except Exception, err:
211       raise self.error("Failed to evaluate %r: %s" % (code, err))
212
213     if not result:
214       raise self.error("Assertion failed: %s" % (code, ))
215
216     return []
217
218
219 def BuildQueryFields(fields):
220   """Build query fields documentation.
221
222   @type fields: dict (field name as key, field details as value)
223
224   """
225   for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
226                                              key=operator.itemgetter(0)):
227     assert len(fdef.doc.splitlines()) == 1
228     yield "``%s``" % fdef.name
229     yield "  %s" % fdef.doc
230
231
232 # TODO: Implement Sphinx directive for query fields
233
234
235 def setup(app):
236   """Sphinx extension callback.
237
238   """
239   app.add_directive("opcode_params", OpcodeParams)
240   app.add_directive("pyassert", PythonAssert)
241   app.add_role("pyeval", PythonEvalRole)