]> git.proxmox.com Git - ovs.git/blob - ovsdb/ovsdb-doc.in
vswitchd: Document map members as separate columns
[ovs.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 nameNroff = "%s : %s" % (name, key)
178 typeNroff = "optional %s" % column.type.value.toEnglish()
179 else:
180 nameNroff = name
181 typeNroff = typeAndConstraintsToNroff(column)
182 body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
183 body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
184 summary += [('column', nameNroff, typeNroff)]
185 elif node.tagName == 'group':
186 title = node.attributes["title"].nodeValue
187 subSummary, subIntro, subBody = columnGroupToNroff(table, node)
188 summary += [('group', title, subSummary)]
189 body += '.ST "%s:"\n' % textToNroff(title)
190 body += subIntro + subBody
191 else:
192 raise error.Error("unknown element %s in <table>" % node.tagName)
193 return summary, intro, body
194
195 def tableSummaryToNroff(summary, level=0):
196 s = ""
197 for type, name, arg in summary:
198 if type == 'column':
199 s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
200 else:
201 s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
202 s += tableSummaryToNroff(arg, level + 1)
203 s += ".RE\n"
204 return s
205
206 def tableToNroff(schema, tableXml):
207 tableName = tableXml.attributes['name'].nodeValue
208 table = schema.tables[tableName]
209
210 s = """.bp
211 .SH "%s TABLE"
212 """ % tableName
213 summary, intro, body = columnGroupToNroff(table, tableXml)
214 s += intro
215 s += '.SS "Summary:\n'
216 s += tableSummaryToNroff(summary)
217 s += '.SS "Details:\n'
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 '\" p as the first line tells "man" that the manpage
233 # needs to be preprocessed by "pic".
234 s = r''''\" p
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
272 s += r"""
273 .SH "TABLE SUMMARY"
274 .PP
275 The following list summarizes the purpose of each of the tables in the
276 \fB%s\fR database. Each table is described in more detail on a later
277 page.
278 .IP "Table" 1in
279 Purpose
280 """ % schema.name
281 for name, title in summary:
282 s += r"""
283 .TQ 1in
284 \fB%s\fR
285 %s
286 """ % (name, textToNroff(title))
287
288 if erFile:
289 s += """
290 .if !'\*[.T]'ascii' \{
291 .bp
292 .SH "TABLE RELATIONSHIPS"
293 .PP
294 The following diagram shows the relationship among tables in the
295 database. Each node represents a table. Tables that are part of the
296 ``root set'' are shown with double borders. Each edge leads from the
297 table that contains it and points to the table that its value
298 represents. Edges are labeled with their column names, followed by a
299 constraint on the number of allowed values: \\fB?\\fR for zero or one,
300 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
301 represent strong references; thin lines represent weak references.
302 .RS -1in
303 """
304 erStream = open(erFile, "r")
305 for line in erStream:
306 s += line + '\n'
307 erStream.close()
308 s += ".RE\\}\n"
309
310 for node in tableNodes:
311 s += tableToNroff(schema, node) + "\n"
312 return s
313
314 def usage():
315 print """\
316 %(argv0)s: ovsdb schema documentation generator
317 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
318 usage: %(argv0)s [OPTIONS] SCHEMA XML
319 where SCHEMA is an OVSDB schema in JSON format
320 and XML is OVSDB documentation in XML format.
321
322 The following options are also available:
323 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
324 --title=TITLE use TITLE as title instead of schema name
325 -h, --help display this help message
326 -V, --version display version information\
327 """ % {'argv0': argv0}
328 sys.exit(0)
329
330 if __name__ == "__main__":
331 try:
332 try:
333 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
334 ['er-diagram=', 'title=',
335 'help', 'version'])
336 except getopt.GetoptError, geo:
337 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
338 sys.exit(1)
339
340 er_diagram = None
341 title = None
342 for key, value in options:
343 if key == '--er-diagram':
344 er_diagram = value
345 elif key == '--title':
346 title = value
347 elif key in ['-h', '--help']:
348 usage()
349 elif key in ['-V', '--version']:
350 print "ovsdb-doc (Open vSwitch) @VERSION@"
351 else:
352 sys.exit(0)
353
354 if len(args) != 2:
355 sys.stderr.write("%s: exactly 2 non-option arguments required "
356 "(use --help for help)\n" % argv0)
357 sys.exit(1)
358
359 # XXX we should warn about undocumented tables or columns
360 s = docsToNroff(args[0], args[1], er_diagram)
361 for line in s.split("\n"):
362 line = line.strip()
363 if len(line):
364 print line
365
366 except error.Error, e:
367 sys.stderr.write("%s: %s\n" % (argv0, e.msg))
368 sys.exit(1)
369
370 # Local variables:
371 # mode: python
372 # End: