]> git.proxmox.com Git - mirror_ovs.git/blob - ovsdb/ovsdb-doc
46f1101a873ad76c5e9c4ee0e92a2173e4ed69cc
[mirror_ovs.git] / ovsdb / ovsdb-doc
1 #! /usr/bin/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.startswith('-'):
20 if c != '-' or font == r'\fB':
21 return '\\' + c
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('(-[0-9]|[-"\'\\\\])', 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 textToNroff)
149 if constraints:
150 type += ", " + constraints
151 if column.unique:
152 type += " (must be unique within table)"
153 return type
154
155 def columnGroupToNroff(table, groupXml):
156 introNodes = []
157 columnNodes = []
158 for node in groupXml.childNodes:
159 if (node.nodeType == node.ELEMENT_NODE
160 and node.tagName in ('column', 'group')):
161 columnNodes += [node]
162 else:
163 if (columnNodes
164 and not (node.nodeType == node.TEXT_NODE
165 and node.data.isspace())):
166 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
167 introNodes += [node]
168
169 summary = []
170 intro = blockXmlToNroff(introNodes)
171 body = ''
172 for node in columnNodes:
173 if node.tagName == 'column':
174 name = node.attributes['name'].nodeValue
175 column = table.columns[name]
176 if node.hasAttribute('key'):
177 key = node.attributes['key'].nodeValue
178 if node.hasAttribute('type'):
179 type_string = node.attributes['type'].nodeValue
180 type_json = ovs.json.from_string(str(type_string))
181 if type(type_json) in (str, unicode):
182 raise error.Error("%s %s:%s has invalid 'type': %s"
183 % (table.name, name, key, type_json))
184 type_ = ovs.db.types.BaseType.from_json(type_json)
185 else:
186 type_ = column.type.value
187
188 nameNroff = "%s : %s" % (name, key)
189
190 if column.type.value:
191 typeNroff = "optional %s" % column.type.value.toEnglish(
192 escapeNroffLiteral)
193 if (column.type.value.type == ovs.db.types.StringType and
194 type_.type == ovs.db.types.BooleanType):
195 # This is a little more explicit and helpful than
196 # "containing a boolean"
197 typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
198 else:
199 if type_.type != column.type.value.type:
200 type_english = type_.toEnglish()
201 if type_english[0] in 'aeiou':
202 typeNroff += ", containing an %s" % type_english
203 else:
204 typeNroff += ", containing a %s" % type_english
205 constraints = (
206 type_.constraintsToEnglish(escapeNroffLiteral,
207 textToNroff))
208 if constraints:
209 typeNroff += ", %s" % constraints
210 else:
211 typeNroff = "none"
212 else:
213 nameNroff = name
214 typeNroff = typeAndConstraintsToNroff(column)
215 if not column.mutable:
216 typeNroff = "immutable %s" % typeNroff
217 body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
218 body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
219 summary += [('column', nameNroff, typeNroff)]
220 elif node.tagName == 'group':
221 title = node.attributes["title"].nodeValue
222 subSummary, subIntro, subBody = columnGroupToNroff(table, node)
223 summary += [('group', title, subSummary)]
224 body += '.ST "%s:"\n' % textToNroff(title)
225 body += subIntro + subBody
226 else:
227 raise error.Error("unknown element %s in <table>" % node.tagName)
228 return summary, intro, body
229
230 def tableSummaryToNroff(summary, level=0):
231 s = ""
232 for type, name, arg in summary:
233 if type == 'column':
234 s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
235 else:
236 s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
237 s += tableSummaryToNroff(arg, level + 1)
238 s += ".RE\n"
239 return s
240
241 def tableToNroff(schema, tableXml):
242 tableName = tableXml.attributes['name'].nodeValue
243 table = schema.tables[tableName]
244
245 s = """.bp
246 .SH "%s TABLE"
247 """ % tableName
248 summary, intro, body = columnGroupToNroff(table, tableXml)
249 s += intro
250 s += '.SS "Summary:\n'
251 s += tableSummaryToNroff(summary)
252 s += '.SS "Details:\n'
253 s += body
254 return s
255
256 def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None):
257 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
258 doc = xml.dom.minidom.parse(xmlFile).documentElement
259
260 schemaDate = os.stat(schemaFile).st_mtime
261 xmlDate = os.stat(xmlFile).st_mtime
262 d = date.fromtimestamp(max(schemaDate, xmlDate))
263
264 if title == None:
265 title = schema.name
266
267 if version == None:
268 version = "UNKNOWN"
269
270 # Putting '\" p as the first line tells "man" that the manpage
271 # needs to be preprocessed by "pic".
272 s = r''''\" p
273 .TH "%s" 5 "%s" "Open vSwitch" "Open vSwitch Manual"
274 .\" -*- nroff -*-
275 .de TQ
276 . br
277 . ns
278 . TP "\\$1"
279 ..
280 .de ST
281 . PP
282 . RS -0.15in
283 . I "\\$1"
284 . RE
285 ..
286 .SH NAME
287 %s \- %s database schema
288 .PP
289 ''' % (title, version, textToNroff(schema.name), schema.name)
290
291 tables = ""
292 introNodes = []
293 tableNodes = []
294 summary = []
295 for dbNode in doc.childNodes:
296 if (dbNode.nodeType == dbNode.ELEMENT_NODE
297 and dbNode.tagName == "table"):
298 tableNodes += [dbNode]
299
300 name = dbNode.attributes['name'].nodeValue
301 if dbNode.hasAttribute("title"):
302 title = dbNode.attributes['title'].nodeValue
303 else:
304 title = name + " configuration."
305 summary += [(name, title)]
306 else:
307 introNodes += [dbNode]
308
309 s += blockXmlToNroff(introNodes) + "\n"
310
311 s += r"""
312 .SH "TABLE SUMMARY"
313 .PP
314 The following list summarizes the purpose of each of the tables in the
315 \fB%s\fR database. Each table is described in more detail on a later
316 page.
317 .IP "Table" 1in
318 Purpose
319 """ % schema.name
320 for name, title in summary:
321 s += r"""
322 .TQ 1in
323 \fB%s\fR
324 %s
325 """ % (name, textToNroff(title))
326
327 if erFile:
328 s += """
329 .\\" check if in troff mode (TTY)
330 .if t \{
331 .bp
332 .SH "TABLE RELATIONSHIPS"
333 .PP
334 The following diagram shows the relationship among tables in the
335 database. Each node represents a table. Tables that are part of the
336 ``root set'' are shown with double borders. Each edge leads from the
337 table that contains it and points to the table that its value
338 represents. Edges are labeled with their column names, followed by a
339 constraint on the number of allowed values: \\fB?\\fR for zero or one,
340 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
341 represent strong references; thin lines represent weak references.
342 .RS -1in
343 """
344 erStream = open(erFile, "r")
345 for line in erStream:
346 s += line + '\n'
347 erStream.close()
348 s += ".RE\\}\n"
349
350 for node in tableNodes:
351 s += tableToNroff(schema, node) + "\n"
352 return s
353
354 def usage():
355 print """\
356 %(argv0)s: ovsdb schema documentation generator
357 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
358 usage: %(argv0)s [OPTIONS] SCHEMA XML
359 where SCHEMA is an OVSDB schema in JSON format
360 and XML is OVSDB documentation in XML format.
361
362 The following options are also available:
363 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
364 --title=TITLE use TITLE as title instead of schema name
365 --version=VERSION use VERSION to display on document footer
366 -h, --help display this help message\
367 """ % {'argv0': argv0}
368 sys.exit(0)
369
370 if __name__ == "__main__":
371 try:
372 try:
373 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
374 ['er-diagram=', 'title=',
375 'version=', 'help'])
376 except getopt.GetoptError, geo:
377 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
378 sys.exit(1)
379
380 er_diagram = None
381 title = None
382 version = None
383 for key, value in options:
384 if key == '--er-diagram':
385 er_diagram = value
386 elif key == '--title':
387 title = value
388 elif key == '--version':
389 version = value
390 elif key in ['-h', '--help']:
391 usage()
392 else:
393 sys.exit(0)
394
395 if len(args) != 2:
396 sys.stderr.write("%s: exactly 2 non-option arguments required "
397 "(use --help for help)\n" % argv0)
398 sys.exit(1)
399
400 # XXX we should warn about undocumented tables or columns
401 s = docsToNroff(args[0], args[1], er_diagram, title, version)
402 for line in s.split("\n"):
403 line = line.strip()
404 if len(line):
405 print line
406
407 except error.Error, e:
408 sys.stderr.write("%s: %s\n" % (argv0, e.msg))
409 sys.exit(1)
410
411 # Local variables:
412 # mode: python
413 # End: