]> git.proxmox.com Git - mirror_ovs.git/blame - ovsdb/ovsdb-doc
ovsdb-doc: Factor out nroff formatting into a separate Python module.
[mirror_ovs.git] / ovsdb / ovsdb-doc
CommitLineData
54c18b66 1#! /usr/bin/python
89365653
BP
2
3from datetime import date
4import getopt
5import os
89365653
BP
6import sys
7import xml.dom.minidom
8
99155935
BP
9import ovs.json
10from ovs.db import error
11import ovs.db.schema
89365653 12
7b8c46c8 13from build.nroff import *
89365653 14
7b8c46c8 15argv0 = sys.argv[0]
89365653
BP
16
17def 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 27def 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
104def 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
115def 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 139def 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
206The 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
208page.
209.IP "Table" 1in
210Purpose
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
226The following diagram shows the relationship among tables in the
c5f341ab
BP
227database. 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 229table that contains it and points to the table that its value
d5a59e7e
BP
230represents. Edges are labeled with their column names, followed by a
231constraint 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 233represent 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
246def usage():
247 print """\
248%(argv0)s: ovsdb schema documentation generator
249Prints documentation for an OVSDB schema as an nroff-formatted manpage.
250usage: %(argv0)s [OPTIONS] SCHEMA XML
251where SCHEMA is an OVSDB schema in JSON format
252 and XML is OVSDB documentation in XML format.
253
254The 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
261if __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: