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