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