]>
Commit | Line | Data |
---|---|---|
1 | #! /usr/bin/python | |
2 | ||
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 | ||
17 | from datetime import date | |
18 | import getopt | |
19 | import os | |
20 | import sys | |
21 | import xml.dom.minidom | |
22 | ||
23 | import ovs.json | |
24 | from ovs.db import error | |
25 | import ovs.db.schema | |
26 | ||
27 | from build.nroff import * | |
28 | ||
29 | argv0 = sys.argv[0] | |
30 | ||
31 | def typeAndConstraintsToNroff(column): | |
32 | type = column.type.toEnglish(escapeNroffLiteral) | |
33 | constraints = column.type.constraintsToEnglish(escapeNroffLiteral, | |
34 | textToNroff) | |
35 | if constraints: | |
36 | type += ", " + constraints | |
37 | if column.unique: | |
38 | type += " (must be unique within table)" | |
39 | return type | |
40 | ||
41 | def columnGroupToNroff(table, groupXml, documented_columns): | |
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: | |
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) | |
53 | introNodes += [node] | |
54 | ||
55 | summary = [] | |
56 | intro = blockXmlToNroff(introNodes) | |
57 | body = '' | |
58 | for node in columnNodes: | |
59 | if node.tagName == 'column': | |
60 | name = node.attributes['name'].nodeValue | |
61 | documented_columns.add(name) | |
62 | column = table.columns[name] | |
63 | if node.hasAttribute('key'): | |
64 | key = node.attributes['key'].nodeValue | |
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 | ||
75 | nameNroff = "%s : %s" % (name, key) | |
76 | ||
77 | if column.type.value: | |
78 | typeNroff = "optional %s" % column.type.value.toEnglish( | |
79 | escapeNroffLiteral) | |
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 = ( | |
93 | type_.constraintsToEnglish(escapeNroffLiteral, | |
94 | textToNroff)) | |
95 | if constraints: | |
96 | typeNroff += ", %s" % constraints | |
97 | else: | |
98 | typeNroff = "none" | |
99 | else: | |
100 | nameNroff = name | |
101 | typeNroff = typeAndConstraintsToNroff(column) | |
102 | if not column.mutable: | |
103 | typeNroff = "immutable %s" % typeNroff | |
104 | body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff) | |
105 | body += blockXmlToNroff(node.childNodes, '.IP') + "\n" | |
106 | summary += [('column', nameNroff, typeNroff)] | |
107 | elif node.tagName == 'group': | |
108 | title = node.attributes["title"].nodeValue | |
109 | subSummary, subIntro, subBody = columnGroupToNroff( | |
110 | table, node, documented_columns) | |
111 | summary += [('group', title, subSummary)] | |
112 | body += '.ST "%s:"\n' % textToNroff(title) | |
113 | body += subIntro + subBody | |
114 | else: | |
115 | raise error.Error("unknown element %s in <table>" % node.tagName) | |
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': | |
122 | s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg) | |
123 | else: | |
124 | s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name | |
125 | s += tableSummaryToNroff(arg, level + 1) | |
126 | s += ".RE\n" | |
127 | return s | |
128 | ||
129 | def tableToNroff(schema, tableXml): | |
130 | tableName = tableXml.attributes['name'].nodeValue | |
131 | table = schema.tables[tableName] | |
132 | ||
133 | documented_columns = set() | |
134 | s = """.bp | |
135 | .SH "%s TABLE" | |
136 | """ % tableName | |
137 | summary, intro, body = columnGroupToNroff(table, tableXml, | |
138 | documented_columns) | |
139 | s += intro | |
140 | s += '.SS "Summary:\n' | |
141 | s += tableSummaryToNroff(summary) | |
142 | s += '.SS "Details:\n' | |
143 | s += body | |
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 | ||
151 | return s | |
152 | ||
153 | def docsToNroff(schemaFile, xmlFile, erFile, version=None): | |
154 | schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile)) | |
155 | doc = xml.dom.minidom.parse(xmlFile).documentElement | |
156 | ||
157 | schemaDate = os.stat(schemaFile).st_mtime | |
158 | xmlDate = os.stat(xmlFile).st_mtime | |
159 | d = date.fromtimestamp(max(schemaDate, xmlDate)) | |
160 | ||
161 | if doc.hasAttribute('name'): | |
162 | manpage = doc.attributes['name'].nodeValue | |
163 | else: | |
164 | manpage = schema.name | |
165 | ||
166 | if version == None: | |
167 | version = "UNKNOWN" | |
168 | ||
169 | # Putting '\" p as the first line tells "man" that the manpage | |
170 | # needs to be preprocessed by "pic". | |
171 | s = r''''\" p | |
172 | .\" -*- nroff -*- | |
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. | |
175 | .de TQ | |
176 | . br | |
177 | . ns | |
178 | . TP "\\$1" | |
179 | .. | |
180 | .de ST | |
181 | . PP | |
182 | . RS -0.15in | |
183 | . I "\\$1" | |
184 | . RE | |
185 | .. | |
186 | .SH NAME | |
187 | %s \- %s database schema | |
188 | .PP | |
189 | ''' % (manpage, schema.version, version, textToNroff(manpage), schema.name) | |
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 | ||
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 | ||
215 | s += blockXmlToNroff(introNodes) + "\n" | |
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 | |
225 | """ % schema.name | |
226 | for name, title in summary: | |
227 | s += r""" | |
228 | .TQ 1in | |
229 | \fB%s\fR | |
230 | %s | |
231 | """ % (name, textToNroff(title)) | |
232 | ||
233 | if erFile: | |
234 | s += """ | |
235 | .\\" check if in troff mode (TTY) | |
236 | .if t \{ | |
237 | .bp | |
238 | .SH "TABLE RELATIONSHIPS" | |
239 | .PP | |
240 | The following diagram shows the relationship among tables in the | |
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 | |
243 | table that contains it and points to the table that its value | |
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 | |
247 | represent strong references; thin lines represent weak references. | |
248 | .RS -1in | |
249 | """ | |
250 | erStream = open(erFile, "r") | |
251 | for line in erStream: | |
252 | s += line + '\n' | |
253 | erStream.close() | |
254 | s += ".RE\\}\n" | |
255 | ||
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: | |
269 | --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC | |
270 | --version=VERSION use VERSION to display on document footer | |
271 | -h, --help display this help message\ | |
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', | |
279 | ['er-diagram=', | |
280 | 'version=', 'help']) | |
281 | except getopt.GetoptError, geo: | |
282 | sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) | |
283 | sys.exit(1) | |
284 | ||
285 | er_diagram = None | |
286 | version = None | |
287 | for key, value in options: | |
288 | if key == '--er-diagram': | |
289 | er_diagram = value | |
290 | elif key == '--version': | |
291 | version = value | |
292 | elif key in ['-h', '--help']: | |
293 | usage() | |
294 | else: | |
295 | sys.exit(0) | |
296 | ||
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) | |
301 | ||
302 | # XXX we should warn about undocumented tables or columns | |
303 | s = docsToNroff(args[0], args[1], er_diagram, version) | |
304 | for line in s.split("\n"): | |
305 | line = line.strip() | |
306 | if len(line): | |
307 | print line | |
308 | ||
309 | except error.Error, e: | |
310 | sys.stderr.write("%s: %s\n" % (argv0, e.msg)) | |
311 | sys.exit(1) | |
312 | ||
313 | # Local variables: | |
314 | # mode: python | |
315 | # End: |