]> git.proxmox.com Git - ovs.git/blob - ovsdb/ovsdb-doc
406c293114657617a47ff10c7d47f7c41b24ad14
[ovs.git] / ovsdb / ovsdb-doc
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(escape_nroff_literal)
33 constraints = column.type.constraintsToEnglish(escape_nroff_literal,
34 text_to_nroff)
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 = block_xml_to_nroff(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 # py2 -> py3 means str -> bytes and unicode -> str
69 try:
70 if type(type_json) in (str, unicode):
71 raise error.Error("%s %s:%s has invalid 'type': %s"
72 % (table.name, name, key, type_json))
73 except:
74 if type(type_json) in (bytes, str):
75 raise error.Error("%s %s:%s has invalid 'type': %s"
76 % (table.name, name, key, type_json))
77 type_ = ovs.db.types.BaseType.from_json(type_json)
78 else:
79 type_ = column.type.value
80
81 nameNroff = "%s : %s" % (name, key)
82
83 if column.type.value:
84 typeNroff = "optional %s" % column.type.value.toEnglish(
85 escape_nroff_literal)
86 if (column.type.value.type == ovs.db.types.StringType and
87 type_.type == ovs.db.types.BooleanType):
88 # This is a little more explicit and helpful than
89 # "containing a boolean"
90 typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
91 else:
92 if type_.type != column.type.value.type:
93 type_english = type_.toEnglish()
94 if type_english[0] in 'aeiou':
95 typeNroff += ", containing an %s" % type_english
96 else:
97 typeNroff += ", containing a %s" % type_english
98 constraints = (
99 type_.constraintsToEnglish(escape_nroff_literal,
100 text_to_nroff))
101 if constraints:
102 typeNroff += ", %s" % constraints
103 else:
104 typeNroff = "none"
105 else:
106 nameNroff = name
107 typeNroff = typeAndConstraintsToNroff(column)
108 if not column.mutable:
109 typeNroff = "immutable %s" % typeNroff
110 body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
111 body += block_xml_to_nroff(node.childNodes, '.IP') + "\n"
112 summary += [('column', nameNroff, typeNroff)]
113 elif node.tagName == 'group':
114 title = node.attributes["title"].nodeValue
115 subSummary, subIntro, subBody = columnGroupToNroff(
116 table, node, documented_columns)
117 summary += [('group', title, subSummary)]
118 body += '.ST "%s:"\n' % text_to_nroff(title)
119 body += subIntro + subBody
120 else:
121 raise error.Error("unknown element %s in <table>" % node.tagName)
122 return summary, intro, body
123
124 def tableSummaryToNroff(summary, level=0):
125 s = ""
126 for type, name, arg in summary:
127 if type == 'column':
128 s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
129 else:
130 s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
131 s += tableSummaryToNroff(arg, level + 1)
132 s += ".RE\n"
133 return s
134
135 def tableToNroff(schema, tableXml):
136 tableName = tableXml.attributes['name'].nodeValue
137 table = schema.tables[tableName]
138
139 documented_columns = set()
140 s = """.bp
141 .SH "%s TABLE"
142 """ % tableName
143 summary, intro, body = columnGroupToNroff(table, tableXml,
144 documented_columns)
145 s += intro
146 s += '.SS "Summary:\n'
147 s += tableSummaryToNroff(summary)
148 s += '.SS "Details:\n'
149 s += body
150
151 schema_columns = set(table.columns.keys())
152 undocumented_columns = schema_columns - documented_columns
153 for column in undocumented_columns:
154 raise error.Error("table %s has undocumented column %s"
155 % (tableName, column))
156
157 return s
158
159 def docsToNroff(schemaFile, xmlFile, erFile, version=None):
160 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
161 doc = xml.dom.minidom.parse(xmlFile).documentElement
162
163 schemaDate = os.stat(schemaFile).st_mtime
164 xmlDate = os.stat(xmlFile).st_mtime
165 d = date.fromtimestamp(max(schemaDate, xmlDate))
166
167 if doc.hasAttribute('name'):
168 manpage = doc.attributes['name'].nodeValue
169 else:
170 manpage = schema.name
171
172 if version == None:
173 version = "UNKNOWN"
174
175 # Putting '\" p as the first line tells "man" that the manpage
176 # needs to be preprocessed by "pic".
177 s = r''''\" p
178 .\" -*- nroff -*-
179 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
180 .fp 5 L CR \\" Make fixed-width font available as \\fL.
181 .de TQ
182 . br
183 . ns
184 . TP "\\$1"
185 ..
186 .de ST
187 . PP
188 . RS -0.15in
189 . I "\\$1"
190 . RE
191 ..
192 .SH NAME
193 %s \- %s database schema
194 .PP
195 ''' % (manpage, schema.version, version, text_to_nroff(manpage), schema.name)
196
197 tables = ""
198 introNodes = []
199 tableNodes = []
200 summary = []
201 for dbNode in doc.childNodes:
202 if (dbNode.nodeType == dbNode.ELEMENT_NODE
203 and dbNode.tagName == "table"):
204 tableNodes += [dbNode]
205
206 name = dbNode.attributes['name'].nodeValue
207 if dbNode.hasAttribute("title"):
208 title = dbNode.attributes['title'].nodeValue
209 else:
210 title = name + " configuration."
211 summary += [(name, title)]
212 else:
213 introNodes += [dbNode]
214
215 documented_tables = set((name for (name, title) in summary))
216 schema_tables = set(schema.tables.keys())
217 undocumented_tables = schema_tables - documented_tables
218 for table in undocumented_tables:
219 raise error.Error("undocumented table %s" % table)
220
221 s += block_xml_to_nroff(introNodes) + "\n"
222
223 s += r"""
224 .SH "TABLE SUMMARY"
225 .PP
226 The following list summarizes the purpose of each of the tables in the
227 \fB%s\fR database. Each table is described in more detail on a later
228 page.
229 .IP "Table" 1in
230 Purpose
231 """ % schema.name
232 for name, title in summary:
233 s += r"""
234 .TQ 1in
235 \fB%s\fR
236 %s
237 """ % (name, text_to_nroff(title))
238
239 if erFile:
240 s += """
241 .\\" check if in troff mode (TTY)
242 .if t \{
243 .bp
244 .SH "TABLE RELATIONSHIPS"
245 .PP
246 The following diagram shows the relationship among tables in the
247 database. Each node represents a table. Tables that are part of the
248 ``root set'' are shown with double borders. Each edge leads from the
249 table that contains it and points to the table that its value
250 represents. Edges are labeled with their column names, followed by a
251 constraint on the number of allowed values: \\fB?\\fR for zero or one,
252 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
253 represent strong references; thin lines represent weak references.
254 .RS -1in
255 """
256 erStream = open(erFile, "r")
257 for line in erStream:
258 s += line + '\n'
259 erStream.close()
260 s += ".RE\\}\n"
261
262 for node in tableNodes:
263 s += tableToNroff(schema, node) + "\n"
264 return s
265
266 def usage():
267 print("""\
268 %(argv0)s: ovsdb schema documentation generator
269 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
270 usage: %(argv0)s [OPTIONS] SCHEMA XML
271 where SCHEMA is an OVSDB schema in JSON format
272 and XML is OVSDB documentation in XML format.
273
274 The following options are also available:
275 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
276 --version=VERSION use VERSION to display on document footer
277 -h, --help display this help message\
278 """ % {'argv0': argv0})
279 sys.exit(0)
280
281 if __name__ == "__main__":
282 try:
283 try:
284 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
285 ['er-diagram=',
286 'version=', 'help'])
287 except getopt.GetoptError as geo:
288 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
289 sys.exit(1)
290
291 er_diagram = None
292 version = None
293 for key, value in options:
294 if key == '--er-diagram':
295 er_diagram = value
296 elif key == '--version':
297 version = value
298 elif key in ['-h', '--help']:
299 usage()
300 else:
301 sys.exit(0)
302
303 if len(args) != 2:
304 sys.stderr.write("%s: exactly 2 non-option arguments required "
305 "(use --help for help)\n" % argv0)
306 sys.exit(1)
307
308 # XXX we should warn about undocumented tables or columns
309 s = docsToNroff(args[0], args[1], er_diagram, version)
310 for line in s.split("\n"):
311 line = line.strip()
312 if len(line):
313 print(line)
314
315 except error.Error as e:
316 sys.stderr.write("%s: %s\n" % (argv0, e.msg))
317 sys.exit(1)
318
319 # Local variables:
320 # mode: python
321 # End: