meta-flow: Correctly set destination MAC in mf_set_flow_value().
[sliver-openvswitch.git] / ovsdb / ovsdb-doc.in
1 #! @PYTHON@
2
3 from datetime import date
4 import getopt
5 import os
6 import re
7 import sys
8 import xml.dom.minidom
9
10 import ovs.json
11 from ovs.db import error
12 import ovs.db.schema
13
14 argv0 = sys.argv[0]
15
16 def textToNroff(s, font=r'\fR'):
17     def escape(match):
18         c = match.group(0)
19         if c.startswith('-'):
20             if c != '-' or font == r'\fB':
21                 return '\\' + c
22             else:
23                 return '-'
24         if c == '\\':
25             return r'\e'
26         elif c == '"':
27             return r'\(dq'
28         elif c == "'":
29             return r'\(cq'
30         else:
31             raise error.Error("bad escape")
32
33     # Escape - \ " ' as needed by nroff.
34     s = re.sub('(-[0-9]|[-"\'\\\\])', escape, s)
35     if s.startswith('.'):
36         s = '\\' + s
37     return s
38
39 def escapeNroffLiteral(s):
40     return r'\fB%s\fR' % textToNroff(s, r'\fB')
41
42 def inlineXmlToNroff(node, font):
43     if node.nodeType == node.TEXT_NODE:
44         return textToNroff(node.data, font)
45     elif node.nodeType == node.ELEMENT_NODE:
46         if node.tagName in ['code', 'em', 'option']:
47             s = r'\fB'
48             for child in node.childNodes:
49                 s += inlineXmlToNroff(child, r'\fB')
50             return s + font
51         elif node.tagName == 'ref':
52             s = r'\fB'
53             if node.hasAttribute('column'):
54                 s += node.attributes['column'].nodeValue
55                 if node.hasAttribute('key'):
56                     s += ':' + node.attributes['key'].nodeValue
57             elif node.hasAttribute('table'):
58                 s += node.attributes['table'].nodeValue
59             elif node.hasAttribute('group'):
60                 s += node.attributes['group'].nodeValue
61             else:
62                 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
63             return s + font
64         elif node.tagName == 'var':
65             s = r'\fI'
66             for child in node.childNodes:
67                 s += inlineXmlToNroff(child, r'\fI')
68             return s + font
69         else:
70             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
71     else:
72         raise error.Error("unknown node %s in inline xml" % node)
73
74 def blockXmlToNroff(nodes, para='.PP'):
75     s = ''
76     for node in nodes:
77         if node.nodeType == node.TEXT_NODE:
78             s += textToNroff(node.data)
79             s = s.lstrip()
80         elif node.nodeType == node.ELEMENT_NODE:
81             if node.tagName in ['ul', 'ol']:
82                 if s != "":
83                     s += "\n"
84                 s += ".RS\n"
85                 i = 0
86                 for liNode in node.childNodes:
87                     if (liNode.nodeType == node.ELEMENT_NODE
88                         and liNode.tagName == 'li'):
89                         i += 1
90                         if node.tagName == 'ul':
91                             s += ".IP \\(bu\n"
92                         else:
93                             s += ".IP %d. .25in\n" % i
94                         s += blockXmlToNroff(liNode.childNodes, ".IP")
95                     elif (liNode.nodeType != node.TEXT_NODE
96                           or not liNode.data.isspace()):
97                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
98                 s += ".RE\n"
99             elif node.tagName == 'dl':
100                 if s != "":
101                     s += "\n"
102                 s += ".RS\n"
103                 prev = "dd"
104                 for liNode in node.childNodes:
105                     if (liNode.nodeType == node.ELEMENT_NODE
106                         and liNode.tagName == 'dt'):
107                         if prev == 'dd':
108                             s += '.TP\n'
109                         else:
110                             s += '.TQ\n'
111                         prev = 'dt'
112                     elif (liNode.nodeType == node.ELEMENT_NODE
113                           and liNode.tagName == 'dd'):
114                         if prev == 'dd':
115                             s += '.IP\n'
116                         prev = 'dd'
117                     elif (liNode.nodeType != node.TEXT_NODE
118                           or not liNode.data.isspace()):
119                         raise error.Error("<dl> element may only have <dt> and <dd> children")
120                     s += blockXmlToNroff(liNode.childNodes, ".IP")
121                 s += ".RE\n"
122             elif node.tagName == 'p':
123                 if s != "":
124                     if not s.endswith("\n"):
125                         s += "\n"
126                     s += para + "\n"
127                 s += blockXmlToNroff(node.childNodes, para)
128             elif node.tagName in ('h1', 'h2', 'h3'):
129                 if s != "":
130                     if not s.endswith("\n"):
131                         s += "\n"
132                 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
133                 s += ".%s " % nroffTag
134                 for child_node in node.childNodes:
135                     s += inlineXmlToNroff(child_node, r'\fR')
136                 s += "\n"
137             else:
138                 s += inlineXmlToNroff(node, r'\fR')
139         else:
140             raise error.Error("unknown node %s in block xml" % node)
141     if s != "" and not s.endswith('\n'):
142         s += '\n'
143     return s
144
145 def typeAndConstraintsToNroff(column):
146     type = column.type.toEnglish(escapeNroffLiteral)
147     constraints = column.type.constraintsToEnglish(escapeNroffLiteral,
148                                                    textToNroff)
149     if constraints:
150         type += ", " + constraints
151     if column.unique:
152         type += " (must be unique within table)"
153     return type
154
155 def columnGroupToNroff(table, groupXml):
156     introNodes = []
157     columnNodes = []
158     for node in groupXml.childNodes:
159         if (node.nodeType == node.ELEMENT_NODE
160             and node.tagName in ('column', 'group')):
161             columnNodes += [node]
162         else:
163             if (columnNodes
164                 and not (node.nodeType == node.TEXT_NODE
165                          and node.data.isspace())):
166                 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
167             introNodes += [node]
168
169     summary = []
170     intro = blockXmlToNroff(introNodes)
171     body = ''
172     for node in columnNodes:
173         if node.tagName == 'column':
174             name = node.attributes['name'].nodeValue
175             column = table.columns[name]
176             if node.hasAttribute('key'):
177                 key = node.attributes['key'].nodeValue
178                 if node.hasAttribute('type'):
179                     type_string = node.attributes['type'].nodeValue
180                     type_json = ovs.json.from_string(str(type_string))
181                     if type(type_json) in (str, unicode):
182                         raise error.Error("%s %s:%s has invalid 'type': %s" 
183                                           % (table.name, name, key, type_json))
184                     type_ = ovs.db.types.BaseType.from_json(type_json)
185                 else:
186                     type_ = column.type.value
187
188                 nameNroff = "%s : %s" % (name, key)
189                 typeNroff = "optional %s" % column.type.value.toEnglish(
190                     escapeNroffLiteral)
191                 if (column.type.value.type == ovs.db.types.StringType and
192                     type_.type == ovs.db.types.BooleanType):
193                     # This is a little more explicit and helpful than
194                     # "containing a boolean"
195                     typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
196                 else:
197                     if type_.type != column.type.value.type:
198                         type_english = type_.toEnglish(escapeNroffLiteral)
199                         if type_english[0] in 'aeiou':
200                             typeNroff += ", containing an %s" % type_english
201                         else:
202                             typeNroff += ", containing a %s" % type_english
203                     constraints = type_.constraintsToEnglish(escapeNroffLiteral)
204                     if constraints:
205                         typeNroff += ", %s" % constraints
206             else:
207                 nameNroff = name
208                 typeNroff = typeAndConstraintsToNroff(column)
209             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
210             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
211             summary += [('column', nameNroff, typeNroff)]
212         elif node.tagName == 'group':
213             title = node.attributes["title"].nodeValue
214             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
215             summary += [('group', title, subSummary)]
216             body += '.ST "%s:"\n' % textToNroff(title)
217             body += subIntro + subBody
218         else:
219             raise error.Error("unknown element %s in <table>" % node.tagName)
220     return summary, intro, body
221
222 def tableSummaryToNroff(summary, level=0):
223     s = ""
224     for type, name, arg in summary:
225         if type == 'column':
226             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
227         else:
228             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
229             s += tableSummaryToNroff(arg, level + 1)
230             s += ".RE\n"
231     return s
232
233 def tableToNroff(schema, tableXml):
234     tableName = tableXml.attributes['name'].nodeValue
235     table = schema.tables[tableName]
236
237     s = """.bp
238 .SH "%s TABLE"
239 """ % tableName
240     summary, intro, body = columnGroupToNroff(table, tableXml)
241     s += intro
242     s += '.SS "Summary:\n'
243     s += tableSummaryToNroff(summary)
244     s += '.SS "Details:\n'
245     s += body
246     return s
247
248 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
249     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
250     doc = xml.dom.minidom.parse(xmlFile).documentElement
251
252     schemaDate = os.stat(schemaFile).st_mtime
253     xmlDate = os.stat(xmlFile).st_mtime
254     d = date.fromtimestamp(max(schemaDate, xmlDate))
255
256     if title == None:
257         title = schema.name
258
259     # Putting '\" p as the first line tells "man" that the manpage
260     # needs to be preprocessed by "pic".
261     s = r''''\" p
262 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
263 .\" -*- nroff -*-
264 .de TQ
265 .  br
266 .  ns
267 .  TP "\\$1"
268 ..
269 .de ST
270 .  PP
271 .  RS -0.15in
272 .  I "\\$1"
273 .  RE
274 ..
275 .SH NAME
276 %s \- %s database schema
277 ''' % (title, d.strftime("%B %Y"), textToNroff(title), schema.name)
278
279     tables = ""
280     introNodes = []
281     tableNodes = []
282     summary = []
283     for dbNode in doc.childNodes:
284         if (dbNode.nodeType == dbNode.ELEMENT_NODE
285             and dbNode.tagName == "table"):
286             tableNodes += [dbNode]
287
288             name = dbNode.attributes['name'].nodeValue
289             if dbNode.hasAttribute("title"):
290                 title = dbNode.attributes['title'].nodeValue
291             else:
292                 title = name + " configuration."
293             summary += [(name, title)]
294         else:
295             introNodes += [dbNode]
296
297     s += blockXmlToNroff(introNodes) + "\n"
298
299     s += r"""
300 .SH "TABLE SUMMARY"
301 .PP
302 The following list summarizes the purpose of each of the tables in the
303 \fB%s\fR database.  Each table is described in more detail on a later
304 page.
305 .IP "Table" 1in
306 Purpose
307 """ % schema.name
308     for name, title in summary:
309         s += r"""
310 .TQ 1in
311 \fB%s\fR
312 %s
313 """ % (name, textToNroff(title))
314
315     if erFile:
316         s += """
317 .\\" check if in troff mode (TTY)
318 .if t \{
319 .bp
320 .SH "TABLE RELATIONSHIPS"
321 .PP
322 The following diagram shows the relationship among tables in the
323 database.  Each node represents a table.  Tables that are part of the
324 ``root set'' are shown with double borders.  Each edge leads from the
325 table that contains it and points to the table that its value
326 represents.  Edges are labeled with their column names, followed by a
327 constraint on the number of allowed values: \\fB?\\fR for zero or one,
328 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
329 represent strong references; thin lines represent weak references.
330 .RS -1in
331 """
332         erStream = open(erFile, "r")
333         for line in erStream:
334             s += line + '\n'
335         erStream.close()
336         s += ".RE\\}\n"
337
338     for node in tableNodes:
339         s += tableToNroff(schema, node) + "\n"
340     return s
341
342 def usage():
343     print """\
344 %(argv0)s: ovsdb schema documentation generator
345 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
346 usage: %(argv0)s [OPTIONS] SCHEMA XML
347 where SCHEMA is an OVSDB schema in JSON format
348   and XML is OVSDB documentation in XML format.
349
350 The following options are also available:
351   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
352   --title=TITLE               use TITLE as title instead of schema name
353   -h, --help                  display this help message
354   -V, --version               display version information\
355 """ % {'argv0': argv0}
356     sys.exit(0)
357
358 if __name__ == "__main__":
359     try:
360         try:
361             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
362                                               ['er-diagram=', 'title=',
363                                                'help', 'version'])
364         except getopt.GetoptError, geo:
365             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
366             sys.exit(1)
367
368         er_diagram = None
369         title = None
370         for key, value in options:
371             if key == '--er-diagram':
372                 er_diagram = value
373             elif key == '--title':
374                 title = value
375             elif key in ['-h', '--help']:
376                 usage()
377             elif key in ['-V', '--version']:
378                 print "ovsdb-doc (Open vSwitch) @VERSION@"
379             else:
380                 sys.exit(0)
381
382         if len(args) != 2:
383             sys.stderr.write("%s: exactly 2 non-option arguments required "
384                              "(use --help for help)\n" % argv0)
385             sys.exit(1)
386
387         # XXX we should warn about undocumented tables or columns
388         s = docsToNroff(args[0], args[1], er_diagram)
389         for line in s.split("\n"):
390             line = line.strip()
391             if len(line):
392                 print line
393
394     except error.Error, e:
395         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
396         sys.exit(1)
397
398 # Local variables:
399 # mode: python
400 # End: