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