]>
Commit | Line | Data |
---|---|---|
54c18b66 | 1 | #! /usr/bin/python |
89365653 BP |
2 | |
3 | from datetime import date | |
4 | import getopt | |
5 | import os | |
6 | import re | |
7 | import sys | |
8 | import xml.dom.minidom | |
9 | ||
99155935 BP |
10 | import ovs.json |
11 | from ovs.db import error | |
12 | import ovs.db.schema | |
89365653 BP |
13 | |
14 | argv0 = sys.argv[0] | |
15 | ||
fcbaf28c | 16 | def textToNroff(s, font=r'\fR'): |
89365653 BP |
17 | def escape(match): |
18 | c = match.group(0) | |
a03bd69a BP |
19 | if c.startswith('-'): |
20 | if c != '-' or font == r'\fB': | |
21 | return '\\' + c | |
fcbaf28c BP |
22 | else: |
23 | return '-' | |
89365653 BP |
24 | if c == '\\': |
25 | return r'\e' | |
26 | elif c == '"': | |
27 | return r'\(dq' | |
28 | elif c == "'": | |
29 | return r'\(cq' | |
30 | else: | |
99155935 | 31 | raise error.Error("bad escape") |
89365653 | 32 | |
fcbaf28c | 33 | # Escape - \ " ' as needed by nroff. |
a03bd69a | 34 | s = re.sub('(-[0-9]|[-"\'\\\\])', escape, s) |
89365653 BP |
35 | if s.startswith('.'): |
36 | s = '\\' + s | |
37 | return s | |
38 | ||
39 | def escapeNroffLiteral(s): | |
fcbaf28c | 40 | return r'\fB%s\fR' % textToNroff(s, r'\fB') |
89365653 BP |
41 | |
42 | def inlineXmlToNroff(node, font): | |
43 | if node.nodeType == node.TEXT_NODE: | |
fcbaf28c | 44 | return textToNroff(node.data, font) |
89365653 | 45 | elif node.nodeType == node.ELEMENT_NODE: |
c9423856 | 46 | if node.tagName in ['code', 'em', 'option']: |
89365653 BP |
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 | |
8de67146 BP |
55 | if node.hasAttribute('key'): |
56 | s += ':' + node.attributes['key'].nodeValue | |
89365653 BP |
57 | elif node.hasAttribute('table'): |
58 | s += node.attributes['table'].nodeValue | |
59 | elif node.hasAttribute('group'): | |
60 | s += node.attributes['group'].nodeValue | |
61 | else: | |
3fd8d445 | 62 | raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys()) |
89365653 BP |
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: | |
99155935 | 70 | raise error.Error("element <%s> unknown or invalid here" % node.tagName) |
89365653 | 71 | else: |
99155935 | 72 | raise error.Error("unknown node %s in inline xml" % node) |
89365653 BP |
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: | |
c9423856 | 81 | if node.tagName in ['ul', 'ol']: |
89365653 BP |
82 | if s != "": |
83 | s += "\n" | |
84 | s += ".RS\n" | |
c9423856 | 85 | i = 0 |
89365653 BP |
86 | for liNode in node.childNodes: |
87 | if (liNode.nodeType == node.ELEMENT_NODE | |
88 | and liNode.tagName == 'li'): | |
c9423856 BP |
89 | i += 1 |
90 | if node.tagName == 'ul': | |
5d943800 | 91 | s += ".IP \\(bu\n" |
c9423856 BP |
92 | else: |
93 | s += ".IP %d. .25in\n" % i | |
94 | s += blockXmlToNroff(liNode.childNodes, ".IP") | |
89365653 BP |
95 | elif (liNode.nodeType != node.TEXT_NODE |
96 | or not liNode.data.isspace()): | |
c9423856 | 97 | raise error.Error("<%s> element may only have <li> children" % node.tagName) |
89365653 BP |
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()): | |
99155935 | 119 | raise error.Error("<dl> element may only have <dt> and <dd> children") |
89365653 BP |
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) | |
3fd8d445 BP |
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" | |
89365653 BP |
137 | else: |
138 | s += inlineXmlToNroff(node, r'\fR') | |
139 | else: | |
99155935 | 140 | raise error.Error("unknown node %s in block xml" % node) |
89365653 BP |
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) | |
746cb760 BP |
147 | constraints = column.type.constraintsToEnglish(escapeNroffLiteral, |
148 | textToNroff) | |
89365653 BP |
149 | if constraints: |
150 | type += ", " + constraints | |
6910a6e6 BP |
151 | if column.unique: |
152 | type += " (must be unique within table)" | |
89365653 BP |
153 | return type |
154 | ||
89365653 BP |
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: | |
3fd8d445 BP |
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) | |
89365653 BP |
167 | introNodes += [node] |
168 | ||
169 | summary = [] | |
170 | intro = blockXmlToNroff(introNodes) | |
171 | body = '' | |
172 | for node in columnNodes: | |
173 | if node.tagName == 'column': | |
3fd8d445 BP |
174 | name = node.attributes['name'].nodeValue |
175 | column = table.columns[name] | |
176 | if node.hasAttribute('key'): | |
177 | key = node.attributes['key'].nodeValue | |
f9e5e5b3 BP |
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 | ||
3fd8d445 | 188 | nameNroff = "%s : %s" % (name, key) |
3349f3bf EJ |
189 | |
190 | if column.type.value: | |
746cb760 BP |
191 | typeNroff = "optional %s" % column.type.value.toEnglish( |
192 | escapeNroffLiteral) | |
3349f3bf EJ |
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 = ( | |
746cb760 BP |
206 | type_.constraintsToEnglish(escapeNroffLiteral, |
207 | textToNroff)) | |
3349f3bf EJ |
208 | if constraints: |
209 | typeNroff += ", %s" % constraints | |
f9e5e5b3 | 210 | else: |
3349f3bf | 211 | typeNroff = "none" |
3fd8d445 BP |
212 | else: |
213 | nameNroff = name | |
214 | typeNroff = typeAndConstraintsToNroff(column) | |
c4454e89 BP |
215 | if not column.mutable: |
216 | typeNroff = "immutable %s" % typeNroff | |
3fd8d445 BP |
217 | body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff) |
218 | body += blockXmlToNroff(node.childNodes, '.IP') + "\n" | |
219 | summary += [('column', nameNroff, typeNroff)] | |
89365653 BP |
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: | |
99155935 | 227 | raise error.Error("unknown element %s in <table>" % node.tagName) |
89365653 BP |
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': | |
3fd8d445 | 234 | s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg) |
89365653 | 235 | else: |
3fd8d445 | 236 | s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name |
89365653 | 237 | s += tableSummaryToNroff(arg, level + 1) |
3fd8d445 | 238 | s += ".RE\n" |
89365653 BP |
239 | return s |
240 | ||
241 | def tableToNroff(schema, tableXml): | |
242 | tableName = tableXml.attributes['name'].nodeValue | |
243 | table = schema.tables[tableName] | |
244 | ||
245 | s = """.bp | |
3fd8d445 | 246 | .SH "%s TABLE" |
89365653 BP |
247 | """ % tableName |
248 | summary, intro, body = columnGroupToNroff(table, tableXml) | |
249 | s += intro | |
3fd8d445 | 250 | s += '.SS "Summary:\n' |
89365653 | 251 | s += tableSummaryToNroff(summary) |
3fd8d445 | 252 | s += '.SS "Details:\n' |
89365653 BP |
253 | s += body |
254 | return s | |
255 | ||
54c18b66 | 256 | def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None): |
99155935 | 257 | schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile)) |
89365653 | 258 | doc = xml.dom.minidom.parse(xmlFile).documentElement |
c5c7c7c5 | 259 | |
89365653 BP |
260 | schemaDate = os.stat(schemaFile).st_mtime |
261 | xmlDate = os.stat(xmlFile).st_mtime | |
262 | d = date.fromtimestamp(max(schemaDate, xmlDate)) | |
c5c7c7c5 | 263 | |
89365653 BP |
264 | if title == None: |
265 | title = schema.name | |
266 | ||
54c18b66 GS |
267 | if version == None: |
268 | version = "UNKNOWN" | |
269 | ||
3fd8d445 BP |
270 | # Putting '\" p as the first line tells "man" that the manpage |
271 | # needs to be preprocessed by "pic". | |
272 | s = r''''\" p | |
a4f22c24 | 273 | .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual" |
89365653 BP |
274 | .\" -*- nroff -*- |
275 | .de TQ | |
276 | . br | |
277 | . ns | |
3fd8d445 | 278 | . TP "\\$1" |
89365653 BP |
279 | .. |
280 | .de ST | |
281 | . PP | |
282 | . RS -0.15in | |
283 | . I "\\$1" | |
284 | . RE | |
285 | .. | |
9f88469c BP |
286 | .SH NAME |
287 | %s \- %s database schema | |
e7c465fd | 288 | .PP |
a4f22c24 | 289 | ''' % (title, schema.version, version, textToNroff(schema.name), schema.name) |
89365653 BP |
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" | |
3fd8d445 BP |
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 | |
89365653 BP |
319 | """ % schema.name |
320 | for name, title in summary: | |
3fd8d445 BP |
321 | s += r""" |
322 | .TQ 1in | |
323 | \fB%s\fR | |
324 | %s | |
325 | """ % (name, textToNroff(title)) | |
f8d739a9 BP |
326 | |
327 | if erFile: | |
328 | s += """ | |
186ef5c1 CW |
329 | .\\" check if in troff mode (TTY) |
330 | .if t \{ | |
3fd8d445 | 331 | .bp |
f8d739a9 BP |
332 | .SH "TABLE RELATIONSHIPS" |
333 | .PP | |
334 | The following diagram shows the relationship among tables in the | |
c5f341ab BP |
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 | |
f8d739a9 | 337 | table that contains it and points to the table that its value |
d5a59e7e BP |
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 | |
c5f341ab | 341 | represent strong references; thin lines represent weak references. |
f8d739a9 BP |
342 | .RS -1in |
343 | """ | |
344 | erStream = open(erFile, "r") | |
345 | for line in erStream: | |
346 | s += line + '\n' | |
347 | erStream.close() | |
c182a514 | 348 | s += ".RE\\}\n" |
f8d739a9 | 349 | |
89365653 BP |
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: | |
f8d739a9 | 363 | --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC |
89365653 | 364 | --title=TITLE use TITLE as title instead of schema name |
54c18b66 GS |
365 | --version=VERSION use VERSION to display on document footer |
366 | -h, --help display this help message\ | |
89365653 BP |
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', | |
f8d739a9 | 374 | ['er-diagram=', 'title=', |
54c18b66 | 375 | 'version=', 'help']) |
89365653 BP |
376 | except getopt.GetoptError, geo: |
377 | sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) | |
378 | sys.exit(1) | |
379 | ||
f8d739a9 | 380 | er_diagram = None |
89365653 | 381 | title = None |
54c18b66 | 382 | version = None |
89365653 | 383 | for key, value in options: |
f8d739a9 BP |
384 | if key == '--er-diagram': |
385 | er_diagram = value | |
386 | elif key == '--title': | |
89365653 | 387 | title = value |
54c18b66 GS |
388 | elif key == '--version': |
389 | version = value | |
89365653 BP |
390 | elif key in ['-h', '--help']: |
391 | usage() | |
89365653 BP |
392 | else: |
393 | sys.exit(0) | |
c5c7c7c5 | 394 | |
89365653 BP |
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) | |
c5c7c7c5 | 399 | |
89365653 | 400 | # XXX we should warn about undocumented tables or columns |
54c18b66 | 401 | s = docsToNroff(args[0], args[1], er_diagram, title, version) |
89365653 BP |
402 | for line in s.split("\n"): |
403 | line = line.strip() | |
404 | if len(line): | |
405 | print line | |
c5c7c7c5 | 406 | |
99155935 | 407 | except error.Error, e: |
89365653 BP |
408 | sys.stderr.write("%s: %s\n" % (argv0, e.msg)) |
409 | sys.exit(1) | |
410 | ||
411 | # Local variables: | |
412 | # mode: python | |
413 | # End: |