ovsdb: Annotate E-R diagram with number of allowed values.
[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             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.  Tables that are part of the
294 ``root set'' are shown with double borders.  Each edge leads from the
295 table that contains it and points to the table that its value
296 represents.  Edges are labeled with their column names, followed by a
297 constraint on the number of allowed values: \\fB?\\fR for zero or one,
298 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
299 represent strong references; thin lines represent weak references.
300 .RS -1in
301 """
302         erStream = open(erFile, "r")
303         for line in erStream:
304             s += line + '\n'
305         erStream.close()
306         s += ".RE\n"
307
308     for node in tableNodes:
309         s += tableToNroff(schema, node) + "\n"
310     return s
311
312 def usage():
313     print """\
314 %(argv0)s: ovsdb schema documentation generator
315 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
316 usage: %(argv0)s [OPTIONS] SCHEMA XML
317 where SCHEMA is an OVSDB schema in JSON format
318   and XML is OVSDB documentation in XML format.
319
320 The following options are also available:
321   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
322   --title=TITLE               use TITLE as title instead of schema name
323   -h, --help                  display this help message
324   -V, --version               display version information\
325 """ % {'argv0': argv0}
326     sys.exit(0)
327
328 if __name__ == "__main__":
329     try:
330         try:
331             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
332                                               ['er-diagram=', 'title=',
333                                                'help', 'version'])
334         except getopt.GetoptError, geo:
335             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
336             sys.exit(1)
337
338         er_diagram = None
339         title = None
340         for key, value in options:
341             if key == '--er-diagram':
342                 er_diagram = value
343             elif key == '--title':
344                 title = value
345             elif key in ['-h', '--help']:
346                 usage()
347             elif key in ['-V', '--version']:
348                 print "ovsdb-doc (Open vSwitch) @VERSION@"
349             else:
350                 sys.exit(0)
351
352         if len(args) != 2:
353             sys.stderr.write("%s: exactly 2 non-option arguments required "
354                              "(use --help for help)\n" % argv0)
355             sys.exit(1)
356
357         # XXX we should warn about undocumented tables or columns
358         s = docsToNroff(args[0], args[1], er_diagram)
359         for line in s.split("\n"):
360             line = line.strip()
361             if len(line):
362                 print line
363
364     except error.Error, e:
365         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
366         sys.exit(1)
367
368 # Local variables:
369 # mode: python
370 # End: