]>
Commit | Line | Data |
---|---|---|
54c18b66 | 1 | #! /usr/bin/python |
89365653 BP |
2 | |
3 | from datetime import date | |
4 | import getopt | |
5 | import os | |
89365653 BP |
6 | import sys |
7 | import xml.dom.minidom | |
8 | ||
99155935 BP |
9 | import ovs.json |
10 | from ovs.db import error | |
11 | import ovs.db.schema | |
89365653 | 12 | |
7b8c46c8 | 13 | from build.nroff import * |
89365653 | 14 | |
7b8c46c8 | 15 | argv0 = sys.argv[0] |
89365653 BP |
16 | |
17 | def typeAndConstraintsToNroff(column): | |
18 | type = column.type.toEnglish(escapeNroffLiteral) | |
746cb760 BP |
19 | constraints = column.type.constraintsToEnglish(escapeNroffLiteral, |
20 | textToNroff) | |
89365653 BP |
21 | if constraints: |
22 | type += ", " + constraints | |
6910a6e6 BP |
23 | if column.unique: |
24 | type += " (must be unique within table)" | |
89365653 BP |
25 | return type |
26 | ||
a826ac90 | 27 | def columnGroupToNroff(table, groupXml, documented_columns): |
89365653 BP |
28 | introNodes = [] |
29 | columnNodes = [] | |
30 | for node in groupXml.childNodes: | |
31 | if (node.nodeType == node.ELEMENT_NODE | |
32 | and node.tagName in ('column', 'group')): | |
33 | columnNodes += [node] | |
34 | else: | |
3fd8d445 BP |
35 | if (columnNodes |
36 | and not (node.nodeType == node.TEXT_NODE | |
37 | and node.data.isspace())): | |
38 | raise error.Error("text follows <column> or <group> inside <group>: %s" % node) | |
89365653 BP |
39 | introNodes += [node] |
40 | ||
41 | summary = [] | |
42 | intro = blockXmlToNroff(introNodes) | |
43 | body = '' | |
44 | for node in columnNodes: | |
45 | if node.tagName == 'column': | |
3fd8d445 | 46 | name = node.attributes['name'].nodeValue |
a826ac90 | 47 | documented_columns.add(name) |
3fd8d445 BP |
48 | column = table.columns[name] |
49 | if node.hasAttribute('key'): | |
50 | key = node.attributes['key'].nodeValue | |
f9e5e5b3 BP |
51 | if node.hasAttribute('type'): |
52 | type_string = node.attributes['type'].nodeValue | |
53 | type_json = ovs.json.from_string(str(type_string)) | |
54 | if type(type_json) in (str, unicode): | |
55 | raise error.Error("%s %s:%s has invalid 'type': %s" | |
56 | % (table.name, name, key, type_json)) | |
57 | type_ = ovs.db.types.BaseType.from_json(type_json) | |
58 | else: | |
59 | type_ = column.type.value | |
60 | ||
3fd8d445 | 61 | nameNroff = "%s : %s" % (name, key) |
3349f3bf EJ |
62 | |
63 | if column.type.value: | |
746cb760 BP |
64 | typeNroff = "optional %s" % column.type.value.toEnglish( |
65 | escapeNroffLiteral) | |
3349f3bf EJ |
66 | if (column.type.value.type == ovs.db.types.StringType and |
67 | type_.type == ovs.db.types.BooleanType): | |
68 | # This is a little more explicit and helpful than | |
69 | # "containing a boolean" | |
70 | typeNroff += r", either \fBtrue\fR or \fBfalse\fR" | |
71 | else: | |
72 | if type_.type != column.type.value.type: | |
73 | type_english = type_.toEnglish() | |
74 | if type_english[0] in 'aeiou': | |
75 | typeNroff += ", containing an %s" % type_english | |
76 | else: | |
77 | typeNroff += ", containing a %s" % type_english | |
78 | constraints = ( | |
746cb760 BP |
79 | type_.constraintsToEnglish(escapeNroffLiteral, |
80 | textToNroff)) | |
3349f3bf EJ |
81 | if constraints: |
82 | typeNroff += ", %s" % constraints | |
f9e5e5b3 | 83 | else: |
3349f3bf | 84 | typeNroff = "none" |
3fd8d445 BP |
85 | else: |
86 | nameNroff = name | |
87 | typeNroff = typeAndConstraintsToNroff(column) | |
c4454e89 BP |
88 | if not column.mutable: |
89 | typeNroff = "immutable %s" % typeNroff | |
3fd8d445 BP |
90 | body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff) |
91 | body += blockXmlToNroff(node.childNodes, '.IP') + "\n" | |
92 | summary += [('column', nameNroff, typeNroff)] | |
89365653 BP |
93 | elif node.tagName == 'group': |
94 | title = node.attributes["title"].nodeValue | |
a826ac90 BP |
95 | subSummary, subIntro, subBody = columnGroupToNroff( |
96 | table, node, documented_columns) | |
89365653 BP |
97 | summary += [('group', title, subSummary)] |
98 | body += '.ST "%s:"\n' % textToNroff(title) | |
99 | body += subIntro + subBody | |
100 | else: | |
99155935 | 101 | raise error.Error("unknown element %s in <table>" % node.tagName) |
89365653 BP |
102 | return summary, intro, body |
103 | ||
104 | def tableSummaryToNroff(summary, level=0): | |
105 | s = "" | |
106 | for type, name, arg in summary: | |
107 | if type == 'column': | |
3fd8d445 | 108 | s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg) |
89365653 | 109 | else: |
3fd8d445 | 110 | s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name |
89365653 | 111 | s += tableSummaryToNroff(arg, level + 1) |
3fd8d445 | 112 | s += ".RE\n" |
89365653 BP |
113 | return s |
114 | ||
115 | def tableToNroff(schema, tableXml): | |
116 | tableName = tableXml.attributes['name'].nodeValue | |
117 | table = schema.tables[tableName] | |
118 | ||
a826ac90 | 119 | documented_columns = set() |
89365653 | 120 | s = """.bp |
3fd8d445 | 121 | .SH "%s TABLE" |
89365653 | 122 | """ % tableName |
a826ac90 BP |
123 | summary, intro, body = columnGroupToNroff(table, tableXml, |
124 | documented_columns) | |
89365653 | 125 | s += intro |
3fd8d445 | 126 | s += '.SS "Summary:\n' |
89365653 | 127 | s += tableSummaryToNroff(summary) |
3fd8d445 | 128 | s += '.SS "Details:\n' |
89365653 | 129 | s += body |
a826ac90 BP |
130 | |
131 | schema_columns = set(table.columns.keys()) | |
132 | undocumented_columns = schema_columns - documented_columns | |
133 | for column in undocumented_columns: | |
134 | raise error.Error("table %s has undocumented column %s" | |
135 | % (tableName, column)) | |
136 | ||
89365653 BP |
137 | return s |
138 | ||
57ba0a77 | 139 | def docsToNroff(schemaFile, xmlFile, erFile, version=None): |
99155935 | 140 | schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile)) |
89365653 | 141 | doc = xml.dom.minidom.parse(xmlFile).documentElement |
c5c7c7c5 | 142 | |
89365653 BP |
143 | schemaDate = os.stat(schemaFile).st_mtime |
144 | xmlDate = os.stat(xmlFile).st_mtime | |
145 | d = date.fromtimestamp(max(schemaDate, xmlDate)) | |
c5c7c7c5 | 146 | |
57ba0a77 BP |
147 | if doc.hasAttribute('name'): |
148 | manpage = doc.attributes['name'].nodeValue | |
149 | else: | |
150 | manpage = schema.name | |
89365653 | 151 | |
54c18b66 GS |
152 | if version == None: |
153 | version = "UNKNOWN" | |
154 | ||
3fd8d445 BP |
155 | # Putting '\" p as the first line tells "man" that the manpage |
156 | # needs to be preprocessed by "pic". | |
157 | s = r''''\" p | |
89365653 | 158 | .\" -*- nroff -*- |
7b8c46c8 BP |
159 | .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual" |
160 | .fp 5 L CR \\" Make fixed-width font available as \\fL. | |
89365653 BP |
161 | .de TQ |
162 | . br | |
163 | . ns | |
3fd8d445 | 164 | . TP "\\$1" |
89365653 BP |
165 | .. |
166 | .de ST | |
167 | . PP | |
168 | . RS -0.15in | |
169 | . I "\\$1" | |
170 | . RE | |
171 | .. | |
9f88469c BP |
172 | .SH NAME |
173 | %s \- %s database schema | |
e7c465fd | 174 | .PP |
57ba0a77 | 175 | ''' % (manpage, schema.version, version, textToNroff(manpage), schema.name) |
89365653 BP |
176 | |
177 | tables = "" | |
178 | introNodes = [] | |
179 | tableNodes = [] | |
180 | summary = [] | |
181 | for dbNode in doc.childNodes: | |
182 | if (dbNode.nodeType == dbNode.ELEMENT_NODE | |
183 | and dbNode.tagName == "table"): | |
184 | tableNodes += [dbNode] | |
185 | ||
186 | name = dbNode.attributes['name'].nodeValue | |
187 | if dbNode.hasAttribute("title"): | |
188 | title = dbNode.attributes['title'].nodeValue | |
189 | else: | |
190 | title = name + " configuration." | |
191 | summary += [(name, title)] | |
192 | else: | |
193 | introNodes += [dbNode] | |
194 | ||
a826ac90 BP |
195 | documented_tables = set((name for (name, title) in summary)) |
196 | schema_tables = set(schema.tables.keys()) | |
197 | undocumented_tables = schema_tables - documented_tables | |
198 | for table in undocumented_tables: | |
199 | raise error.Error("undocumented table %s" % table) | |
200 | ||
89365653 | 201 | s += blockXmlToNroff(introNodes) + "\n" |
3fd8d445 BP |
202 | |
203 | s += r""" | |
204 | .SH "TABLE SUMMARY" | |
205 | .PP | |
206 | The following list summarizes the purpose of each of the tables in the | |
207 | \fB%s\fR database. Each table is described in more detail on a later | |
208 | page. | |
209 | .IP "Table" 1in | |
210 | Purpose | |
89365653 BP |
211 | """ % schema.name |
212 | for name, title in summary: | |
3fd8d445 BP |
213 | s += r""" |
214 | .TQ 1in | |
215 | \fB%s\fR | |
216 | %s | |
217 | """ % (name, textToNroff(title)) | |
f8d739a9 BP |
218 | |
219 | if erFile: | |
220 | s += """ | |
186ef5c1 CW |
221 | .\\" check if in troff mode (TTY) |
222 | .if t \{ | |
3fd8d445 | 223 | .bp |
f8d739a9 BP |
224 | .SH "TABLE RELATIONSHIPS" |
225 | .PP | |
226 | The following diagram shows the relationship among tables in the | |
c5f341ab BP |
227 | database. Each node represents a table. Tables that are part of the |
228 | ``root set'' are shown with double borders. Each edge leads from the | |
f8d739a9 | 229 | table that contains it and points to the table that its value |
d5a59e7e BP |
230 | represents. Edges are labeled with their column names, followed by a |
231 | constraint on the number of allowed values: \\fB?\\fR for zero or one, | |
232 | \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines | |
c5f341ab | 233 | represent strong references; thin lines represent weak references. |
f8d739a9 BP |
234 | .RS -1in |
235 | """ | |
236 | erStream = open(erFile, "r") | |
237 | for line in erStream: | |
238 | s += line + '\n' | |
239 | erStream.close() | |
c182a514 | 240 | s += ".RE\\}\n" |
f8d739a9 | 241 | |
89365653 BP |
242 | for node in tableNodes: |
243 | s += tableToNroff(schema, node) + "\n" | |
244 | return s | |
245 | ||
246 | def usage(): | |
247 | print """\ | |
248 | %(argv0)s: ovsdb schema documentation generator | |
249 | Prints documentation for an OVSDB schema as an nroff-formatted manpage. | |
250 | usage: %(argv0)s [OPTIONS] SCHEMA XML | |
251 | where SCHEMA is an OVSDB schema in JSON format | |
252 | and XML is OVSDB documentation in XML format. | |
253 | ||
254 | The following options are also available: | |
f8d739a9 | 255 | --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC |
54c18b66 GS |
256 | --version=VERSION use VERSION to display on document footer |
257 | -h, --help display this help message\ | |
89365653 BP |
258 | """ % {'argv0': argv0} |
259 | sys.exit(0) | |
260 | ||
261 | if __name__ == "__main__": | |
262 | try: | |
263 | try: | |
264 | options, args = getopt.gnu_getopt(sys.argv[1:], 'hV', | |
57ba0a77 | 265 | ['er-diagram=', |
54c18b66 | 266 | 'version=', 'help']) |
89365653 BP |
267 | except getopt.GetoptError, geo: |
268 | sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) | |
269 | sys.exit(1) | |
270 | ||
f8d739a9 | 271 | er_diagram = None |
54c18b66 | 272 | version = None |
89365653 | 273 | for key, value in options: |
f8d739a9 BP |
274 | if key == '--er-diagram': |
275 | er_diagram = value | |
54c18b66 GS |
276 | elif key == '--version': |
277 | version = value | |
89365653 BP |
278 | elif key in ['-h', '--help']: |
279 | usage() | |
89365653 BP |
280 | else: |
281 | sys.exit(0) | |
c5c7c7c5 | 282 | |
89365653 BP |
283 | if len(args) != 2: |
284 | sys.stderr.write("%s: exactly 2 non-option arguments required " | |
285 | "(use --help for help)\n" % argv0) | |
286 | sys.exit(1) | |
c5c7c7c5 | 287 | |
89365653 | 288 | # XXX we should warn about undocumented tables or columns |
57ba0a77 | 289 | s = docsToNroff(args[0], args[1], er_diagram, version) |
89365653 BP |
290 | for line in s.split("\n"): |
291 | line = line.strip() | |
292 | if len(line): | |
293 | print line | |
c5c7c7c5 | 294 | |
99155935 | 295 | except error.Error, e: |
89365653 BP |
296 | sys.stderr.write("%s: %s\n" % (argv0, e.msg)) |
297 | sys.exit(1) | |
298 | ||
299 | # Local variables: | |
300 | # mode: python | |
301 | # End: |