ofproto: Fix use-after-free error when ports disappear.
[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 == '-':
20             if font == r'\fB':
21                 return r'\-'
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('([-"\'\\\\])', 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     if constraints:
149         type += ", " + constraints
150     if column.unique:
151         type += " (must be unique within table)"
152     return type
153
154 def columnGroupToNroff(table, groupXml):
155     introNodes = []
156     columnNodes = []
157     for node in groupXml.childNodes:
158         if (node.nodeType == node.ELEMENT_NODE
159             and node.tagName in ('column', 'group')):
160             columnNodes += [node]
161         else:
162             if (columnNodes
163                 and not (node.nodeType == node.TEXT_NODE
164                          and node.data.isspace())):
165                 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
166             introNodes += [node]
167
168     summary = []
169     intro = blockXmlToNroff(introNodes)
170     body = ''
171     for node in columnNodes:
172         if node.tagName == 'column':
173             name = node.attributes['name'].nodeValue
174             column = table.columns[name]
175             if node.hasAttribute('key'):
176                 key = node.attributes['key'].nodeValue
177                 if node.hasAttribute('type'):
178                     type_string = node.attributes['type'].nodeValue
179                     type_json = ovs.json.from_string(str(type_string))
180                     if type(type_json) in (str, unicode):
181                         raise error.Error("%s %s:%s has invalid 'type': %s" 
182                                           % (table.name, name, key, type_json))
183                     type_ = ovs.db.types.BaseType.from_json(type_json)
184                 else:
185                     type_ = column.type.value
186
187                 nameNroff = "%s : %s" % (name, key)
188                 typeNroff = "optional %s" % column.type.value.toEnglish()
189                 if (column.type.value.type == ovs.db.types.StringType and
190                     type_.type == ovs.db.types.BooleanType):
191                     # This is a little more explicit and helpful than
192                     # "containing a boolean"
193                     typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
194                 else:
195                     if type_.type != column.type.value.type:
196                         type_english = type_.toEnglish()
197                         if type_english[0] in 'aeiou':
198                             typeNroff += ", containing an %s" % type_english
199                         else:
200                             typeNroff += ", containing a %s" % type_english
201                     constraints = type_.constraintsToEnglish(escapeNroffLiteral)
202                     if constraints:
203                         typeNroff += ", %s" % constraints
204             else:
205                 nameNroff = name
206                 typeNroff = typeAndConstraintsToNroff(column)
207             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
208             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
209             summary += [('column', nameNroff, typeNroff)]
210         elif node.tagName == 'group':
211             title = node.attributes["title"].nodeValue
212             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
213             summary += [('group', title, subSummary)]
214             body += '.ST "%s:"\n' % textToNroff(title)
215             body += subIntro + subBody
216         else:
217             raise error.Error("unknown element %s in <table>" % node.tagName)
218     return summary, intro, body
219
220 def tableSummaryToNroff(summary, level=0):
221     s = ""
222     for type, name, arg in summary:
223         if type == 'column':
224             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
225         else:
226             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
227             s += tableSummaryToNroff(arg, level + 1)
228             s += ".RE\n"
229     return s
230
231 def tableToNroff(schema, tableXml):
232     tableName = tableXml.attributes['name'].nodeValue
233     table = schema.tables[tableName]
234
235     s = """.bp
236 .SH "%s TABLE"
237 """ % tableName
238     summary, intro, body = columnGroupToNroff(table, tableXml)
239     s += intro
240     s += '.SS "Summary:\n'
241     s += tableSummaryToNroff(summary)
242     s += '.SS "Details:\n'
243     s += body
244     return s
245
246 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
247     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
248     doc = xml.dom.minidom.parse(xmlFile).documentElement
249
250     schemaDate = os.stat(schemaFile).st_mtime
251     xmlDate = os.stat(xmlFile).st_mtime
252     d = date.fromtimestamp(max(schemaDate, xmlDate))
253
254     if title == None:
255         title = schema.name
256
257     # Putting '\" p as the first line tells "man" that the manpage
258     # needs to be preprocessed by "pic".
259     s = r''''\" p
260 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
261 .\" -*- nroff -*-
262 .de TQ
263 .  br
264 .  ns
265 .  TP "\\$1"
266 ..
267 .de ST
268 .  PP
269 .  RS -0.15in
270 .  I "\\$1"
271 .  RE
272 ..
273 ''' % (title, d.strftime("%B %Y"))
274
275     s += '.SH "%s DATABASE"\n' % schema.name
276
277     tables = ""
278     introNodes = []
279     tableNodes = []
280     summary = []
281     for dbNode in doc.childNodes:
282         if (dbNode.nodeType == dbNode.ELEMENT_NODE
283             and dbNode.tagName == "table"):
284             tableNodes += [dbNode]
285
286             name = dbNode.attributes['name'].nodeValue
287             if dbNode.hasAttribute("title"):
288                 title = dbNode.attributes['title'].nodeValue
289             else:
290                 title = name + " configuration."
291             summary += [(name, title)]
292         else:
293             introNodes += [dbNode]
294
295     s += blockXmlToNroff(introNodes) + "\n"
296
297     s += r"""
298 .SH "TABLE SUMMARY"
299 .PP
300 The following list summarizes the purpose of each of the tables in the
301 \fB%s\fR database.  Each table is described in more detail on a later
302 page.
303 .IP "Table" 1in
304 Purpose
305 """ % schema.name
306     for name, title in summary:
307         s += r"""
308 .TQ 1in
309 \fB%s\fR
310 %s
311 """ % (name, textToNroff(title))
312
313     if erFile:
314         s += """
315 .if !'\*[.T]'ascii' \{
316 .bp
317 .SH "TABLE RELATIONSHIPS"
318 .PP
319 The following diagram shows the relationship among tables in the
320 database.  Each node represents a table.  Tables that are part of the
321 ``root set'' are shown with double borders.  Each edge leads from the
322 table that contains it and points to the table that its value
323 represents.  Edges are labeled with their column names, followed by a
324 constraint on the number of allowed values: \\fB?\\fR for zero or one,
325 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
326 represent strong references; thin lines represent weak references.
327 .RS -1in
328 """
329         erStream = open(erFile, "r")
330         for line in erStream:
331             s += line + '\n'
332         erStream.close()
333         s += ".RE\\}\n"
334
335     for node in tableNodes:
336         s += tableToNroff(schema, node) + "\n"
337     return s
338
339 def usage():
340     print """\
341 %(argv0)s: ovsdb schema documentation generator
342 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
343 usage: %(argv0)s [OPTIONS] SCHEMA XML
344 where SCHEMA is an OVSDB schema in JSON format
345   and XML is OVSDB documentation in XML format.
346
347 The following options are also available:
348   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
349   --title=TITLE               use TITLE as title instead of schema name
350   -h, --help                  display this help message
351   -V, --version               display version information\
352 """ % {'argv0': argv0}
353     sys.exit(0)
354
355 if __name__ == "__main__":
356     try:
357         try:
358             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
359                                               ['er-diagram=', 'title=',
360                                                'help', 'version'])
361         except getopt.GetoptError, geo:
362             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
363             sys.exit(1)
364
365         er_diagram = None
366         title = None
367         for key, value in options:
368             if key == '--er-diagram':
369                 er_diagram = value
370             elif key == '--title':
371                 title = value
372             elif key in ['-h', '--help']:
373                 usage()
374             elif key in ['-V', '--version']:
375                 print "ovsdb-doc (Open vSwitch) @VERSION@"
376             else:
377                 sys.exit(0)
378
379         if len(args) != 2:
380             sys.stderr.write("%s: exactly 2 non-option arguments required "
381                              "(use --help for help)\n" % argv0)
382             sys.exit(1)
383
384         # XXX we should warn about undocumented tables or columns
385         s = docsToNroff(args[0], args[1], er_diagram)
386         for line in s.split("\n"):
387             line = line.strip()
388             if len(line):
389                 print line
390
391     except error.Error, e:
392         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
393         sys.exit(1)
394
395 # Local variables:
396 # mode: python
397 # End: