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