process: Avoid late failure if /dev/null cannot be opened.
[openvswitch] / 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             elif node.hasAttribute('table'):
56                 s += node.attributes['table'].nodeValue
57             elif node.hasAttribute('group'):
58                 s += node.attributes['group'].nodeValue
59             else:
60                 raise error.Error("'ref' lacks column and table attributes")
61             return s + font
62         elif node.tagName == 'var':
63             s = r'\fI'
64             for child in node.childNodes:
65                 s += inlineXmlToNroff(child, r'\fI')
66             return s + font
67         else:
68             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
69     else:
70         raise error.Error("unknown node %s in inline xml" % node)
71
72 def blockXmlToNroff(nodes, para='.PP'):
73     s = ''
74     for node in nodes:
75         if node.nodeType == node.TEXT_NODE:
76             s += textToNroff(node.data)
77             s = s.lstrip()
78         elif node.nodeType == node.ELEMENT_NODE:
79             if node.tagName in ['ul', 'ol']:
80                 if s != "":
81                     s += "\n"
82                 s += ".RS\n"
83                 i = 0
84                 for liNode in node.childNodes:
85                     if (liNode.nodeType == node.ELEMENT_NODE
86                         and liNode.tagName == 'li'):
87                         i += 1
88                         if node.tagName == 'ul':
89                             s += ".IP \\bu\n"
90                         else:
91                             s += ".IP %d. .25in\n" % i
92                         s += blockXmlToNroff(liNode.childNodes, ".IP")
93                     elif (liNode.nodeType != node.TEXT_NODE
94                           or not liNode.data.isspace()):
95                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
96                 s += ".RE\n"
97             elif node.tagName == 'dl':
98                 if s != "":
99                     s += "\n"
100                 s += ".RS\n"
101                 prev = "dd"
102                 for liNode in node.childNodes:
103                     if (liNode.nodeType == node.ELEMENT_NODE
104                         and liNode.tagName == 'dt'):
105                         if prev == 'dd':
106                             s += '.TP\n'
107                         else:
108                             s += '.TQ\n'
109                         prev = 'dt'
110                     elif (liNode.nodeType == node.ELEMENT_NODE
111                           and liNode.tagName == 'dd'):
112                         if prev == 'dd':
113                             s += '.IP\n'
114                         prev = 'dd'
115                     elif (liNode.nodeType != node.TEXT_NODE
116                           or not liNode.data.isspace()):
117                         raise error.Error("<dl> element may only have <dt> and <dd> children")
118                     s += blockXmlToNroff(liNode.childNodes, ".IP")
119                 s += ".RE\n"
120             elif node.tagName == 'p':
121                 if s != "":
122                     if not s.endswith("\n"):
123                         s += "\n"
124                     s += para + "\n"
125                 s += blockXmlToNroff(node.childNodes, para)
126             else:
127                 s += inlineXmlToNroff(node, r'\fR')
128         else:
129             raise error.Error("unknown node %s in block xml" % node)
130     if s != "" and not s.endswith('\n'):
131         s += '\n'
132     return s
133
134 def typeAndConstraintsToNroff(column):
135     type = column.type.toEnglish(escapeNroffLiteral)
136     constraints = column.type.constraintsToEnglish(escapeNroffLiteral)
137     if constraints:
138         type += ", " + constraints
139     return type
140
141 def columnToNroff(columnName, column, node):
142     type = typeAndConstraintsToNroff(column)
143     s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type)
144     s += blockXmlToNroff(node.childNodes, '.IP') + "\n"
145     return s
146
147 def columnGroupToNroff(table, groupXml):
148     introNodes = []
149     columnNodes = []
150     for node in groupXml.childNodes:
151         if (node.nodeType == node.ELEMENT_NODE
152             and node.tagName in ('column', 'group')):
153             columnNodes += [node]
154         else:
155             introNodes += [node]
156
157     summary = []
158     intro = blockXmlToNroff(introNodes)
159     body = ''
160     for node in columnNodes:
161         if node.tagName == 'column':
162             columnName = node.attributes['name'].nodeValue
163             column = table.columns[columnName]
164             body += columnToNroff(columnName, column, node)
165             summary += [('column', columnName, column)]
166         elif node.tagName == 'group':
167             title = node.attributes["title"].nodeValue
168             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
169             summary += [('group', title, subSummary)]
170             body += '.ST "%s:"\n' % textToNroff(title)
171             body += subIntro + subBody
172         else:
173             raise error.Error("unknown element %s in <table>" % node.tagName)
174     return summary, intro, body
175
176 def tableSummaryToNroff(summary, level=0):
177     s = ""
178     for type, name, arg in summary:
179         if type == 'column':
180
181             s += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % (
182                 r'\ \ ' * level, name, typeAndConstraintsToNroff(arg))
183         else:
184             if s != "":
185                 s += "_\n"
186             s += """.T&
187 li | s
188 l | l.
189 %s%s
190 _
191 """ % (r'\ \ ' * level, name)
192             s += tableSummaryToNroff(arg, level + 1)
193     return s
194
195 def tableToNroff(schema, tableXml):
196     tableName = tableXml.attributes['name'].nodeValue
197     table = schema.tables[tableName]
198
199     s = """.bp
200 .SS "%s Table"
201 """ % tableName
202     summary, intro, body = columnGroupToNroff(table, tableXml)
203     s += intro
204
205     s += r"""
206 .sp
207 .ce 1
208 \fB%s\fR Table Columns:
209 .TS
210 center box;
211 l | l.
212 Column  Type
213 =
214 """ % tableName
215     s += tableSummaryToNroff(summary)
216     s += ".TE\n"
217
218     s += body
219     return s
220
221 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
222     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
223     doc = xml.dom.minidom.parse(xmlFile).documentElement
224
225     schemaDate = os.stat(schemaFile).st_mtime
226     xmlDate = os.stat(xmlFile).st_mtime
227     d = date.fromtimestamp(max(schemaDate, xmlDate))
228
229     if title == None:
230         title = schema.name
231
232     # Putting '\" pt as the first line tells "man" that the manpage
233     # needs to be preprocessed by "pic" and "tbl".
234     s = r''''\" pt
235 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
236 .\" -*- nroff -*-
237 .de TQ
238 .  br
239 .  ns
240 .  TP "\\$1"
241 ..
242 .de ST
243 .  PP
244 .  RS -0.15in
245 .  I "\\$1"
246 .  RE
247 ..
248 ''' % (title, d.strftime("%B %Y"))
249
250     s += '.SH "%s DATABASE"\n' % schema.name
251
252     tables = ""
253     introNodes = []
254     tableNodes = []
255     summary = []
256     for dbNode in doc.childNodes:
257         if (dbNode.nodeType == dbNode.ELEMENT_NODE
258             and dbNode.tagName == "table"):
259             tableNodes += [dbNode]
260
261             name = dbNode.attributes['name'].nodeValue
262             if dbNode.hasAttribute("title"):
263                 title = dbNode.attributes['title'].nodeValue
264             else:
265                 title = name + " configuration."
266             summary += [(name, title)]
267         else:
268             introNodes += [dbNode]
269
270     s += blockXmlToNroff(introNodes) + "\n"
271     tableSummary = r"""
272 .sp
273 .ce 1
274 \fB%s\fR Database Tables:
275 .TS
276 center box;
277 l | l
278 lb | l.
279 Table   Purpose
280 =
281 """ % schema.name
282     for name, title in summary:
283         tableSummary += "%s\t%s\n" % (name, textToNroff(title))
284     tableSummary += '.TE\n'
285     s += tableSummary
286
287     if erFile:
288         s += """
289 .sp 1
290 .SH "TABLE RELATIONSHIPS"
291 .PP
292 The following diagram shows the relationship among tables in the
293 database.  Each node represents a table.  Each edge leads from the
294 table that contains it and points to the table that its value
295 represents.  Edges are labeled with their column names.
296 .RS -1in
297 """
298         erStream = open(erFile, "r")
299         for line in erStream:
300             s += line + '\n'
301         erStream.close()
302         s += ".RE\n"
303
304     for node in tableNodes:
305         s += tableToNroff(schema, node) + "\n"
306     return s
307
308 def usage():
309     print """\
310 %(argv0)s: ovsdb schema documentation generator
311 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
312 usage: %(argv0)s [OPTIONS] SCHEMA XML
313 where SCHEMA is an OVSDB schema in JSON format
314   and XML is OVSDB documentation in XML format.
315
316 The following options are also available:
317   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
318   --title=TITLE               use TITLE as title instead of schema name
319   -h, --help                  display this help message
320   -V, --version               display version information\
321 """ % {'argv0': argv0}
322     sys.exit(0)
323
324 if __name__ == "__main__":
325     try:
326         try:
327             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
328                                               ['er-diagram=', 'title=',
329                                                'help', 'version'])
330         except getopt.GetoptError, geo:
331             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
332             sys.exit(1)
333
334         er_diagram = None
335         title = None
336         for key, value in options:
337             if key == '--er-diagram':
338                 er_diagram = value
339             elif key == '--title':
340                 title = value
341             elif key in ['-h', '--help']:
342                 usage()
343             elif key in ['-V', '--version']:
344                 print "ovsdb-doc (Open vSwitch) @VERSION@"
345             else:
346                 sys.exit(0)
347
348         if len(args) != 2:
349             sys.stderr.write("%s: exactly 2 non-option arguments required "
350                              "(use --help for help)\n" % argv0)
351             sys.exit(1)
352
353         # XXX we should warn about undocumented tables or columns
354         s = docsToNroff(args[0], args[1], er_diagram)
355         for line in s.split("\n"):
356             line = line.strip()
357             if len(line):
358                 print line
359
360     except error.Error, e:
361         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
362         sys.exit(1)
363
364 # Local variables:
365 # mode: python
366 # End: