3 from datetime import date
11 from ovs.db import error
16 def textToNroff(s, font=r'\fR'):
20 if c != '-' or font == r'\fB':
31 raise error.Error("bad escape")
33 # Escape - \ " ' as needed by nroff.
34 s = re.sub('(-[0-9]|[-"\'\\\\])', escape, s)
39 def escapeNroffLiteral(s):
40 return r'\fB%s\fR' % textToNroff(s, r'\fB')
42 def inlineXmlToNroff(node, font):
43 if node.nodeType == node.TEXT_NODE:
44 return textToNroff(node.data, font)
45 elif node.nodeType == node.ELEMENT_NODE:
46 if node.tagName in ['code', 'em', 'option']:
48 for child in node.childNodes:
49 s += inlineXmlToNroff(child, r'\fB')
51 elif node.tagName == 'ref':
53 if node.hasAttribute('column'):
54 s += node.attributes['column'].nodeValue
55 if node.hasAttribute('key'):
56 s += ':' + node.attributes['key'].nodeValue
57 elif node.hasAttribute('table'):
58 s += node.attributes['table'].nodeValue
59 elif node.hasAttribute('group'):
60 s += node.attributes['group'].nodeValue
62 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
64 elif node.tagName == 'var':
66 for child in node.childNodes:
67 s += inlineXmlToNroff(child, r'\fI')
70 raise error.Error("element <%s> unknown or invalid here" % node.tagName)
72 raise error.Error("unknown node %s in inline xml" % node)
74 def blockXmlToNroff(nodes, para='.PP'):
77 if node.nodeType == node.TEXT_NODE:
78 s += textToNroff(node.data)
80 elif node.nodeType == node.ELEMENT_NODE:
81 if node.tagName in ['ul', 'ol']:
86 for liNode in node.childNodes:
87 if (liNode.nodeType == node.ELEMENT_NODE
88 and liNode.tagName == 'li'):
90 if node.tagName == 'ul':
93 s += ".IP %d. .25in\n" % i
94 s += blockXmlToNroff(liNode.childNodes, ".IP")
95 elif (liNode.nodeType != node.TEXT_NODE
96 or not liNode.data.isspace()):
97 raise error.Error("<%s> element may only have <li> children" % node.tagName)
99 elif node.tagName == 'dl':
104 for liNode in node.childNodes:
105 if (liNode.nodeType == node.ELEMENT_NODE
106 and liNode.tagName == 'dt'):
112 elif (liNode.nodeType == node.ELEMENT_NODE
113 and liNode.tagName == 'dd'):
117 elif (liNode.nodeType != node.TEXT_NODE
118 or not liNode.data.isspace()):
119 raise error.Error("<dl> element may only have <dt> and <dd> children")
120 s += blockXmlToNroff(liNode.childNodes, ".IP")
122 elif node.tagName == 'p':
124 if not s.endswith("\n"):
127 s += blockXmlToNroff(node.childNodes, para)
128 elif node.tagName in ('h1', 'h2', 'h3'):
130 if not s.endswith("\n"):
132 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
133 s += ".%s " % nroffTag
134 for child_node in node.childNodes:
135 s += inlineXmlToNroff(child_node, r'\fR')
138 s += inlineXmlToNroff(node, r'\fR')
140 raise error.Error("unknown node %s in block xml" % node)
141 if s != "" and not s.endswith('\n'):
145 def typeAndConstraintsToNroff(column):
146 type = column.type.toEnglish(escapeNroffLiteral)
147 constraints = column.type.constraintsToEnglish(escapeNroffLiteral,
150 type += ", " + constraints
152 type += " (must be unique within table)"
155 def columnGroupToNroff(table, groupXml, documented_columns):
158 for node in groupXml.childNodes:
159 if (node.nodeType == node.ELEMENT_NODE
160 and node.tagName in ('column', 'group')):
161 columnNodes += [node]
164 and not (node.nodeType == node.TEXT_NODE
165 and node.data.isspace())):
166 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
170 intro = blockXmlToNroff(introNodes)
172 for node in columnNodes:
173 if node.tagName == 'column':
174 name = node.attributes['name'].nodeValue
175 documented_columns.add(name)
176 column = table.columns[name]
177 if node.hasAttribute('key'):
178 key = node.attributes['key'].nodeValue
179 if node.hasAttribute('type'):
180 type_string = node.attributes['type'].nodeValue
181 type_json = ovs.json.from_string(str(type_string))
182 if type(type_json) in (str, unicode):
183 raise error.Error("%s %s:%s has invalid 'type': %s"
184 % (table.name, name, key, type_json))
185 type_ = ovs.db.types.BaseType.from_json(type_json)
187 type_ = column.type.value
189 nameNroff = "%s : %s" % (name, key)
191 if column.type.value:
192 typeNroff = "optional %s" % column.type.value.toEnglish(
194 if (column.type.value.type == ovs.db.types.StringType and
195 type_.type == ovs.db.types.BooleanType):
196 # This is a little more explicit and helpful than
197 # "containing a boolean"
198 typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
200 if type_.type != column.type.value.type:
201 type_english = type_.toEnglish()
202 if type_english[0] in 'aeiou':
203 typeNroff += ", containing an %s" % type_english
205 typeNroff += ", containing a %s" % type_english
207 type_.constraintsToEnglish(escapeNroffLiteral,
210 typeNroff += ", %s" % constraints
215 typeNroff = typeAndConstraintsToNroff(column)
216 if not column.mutable:
217 typeNroff = "immutable %s" % typeNroff
218 body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
219 body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
220 summary += [('column', nameNroff, typeNroff)]
221 elif node.tagName == 'group':
222 title = node.attributes["title"].nodeValue
223 subSummary, subIntro, subBody = columnGroupToNroff(
224 table, node, documented_columns)
225 summary += [('group', title, subSummary)]
226 body += '.ST "%s:"\n' % textToNroff(title)
227 body += subIntro + subBody
229 raise error.Error("unknown element %s in <table>" % node.tagName)
230 return summary, intro, body
232 def tableSummaryToNroff(summary, level=0):
234 for type, name, arg in summary:
236 s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
238 s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
239 s += tableSummaryToNroff(arg, level + 1)
243 def tableToNroff(schema, tableXml):
244 tableName = tableXml.attributes['name'].nodeValue
245 table = schema.tables[tableName]
247 documented_columns = set()
251 summary, intro, body = columnGroupToNroff(table, tableXml,
254 s += '.SS "Summary:\n'
255 s += tableSummaryToNroff(summary)
256 s += '.SS "Details:\n'
259 schema_columns = set(table.columns.keys())
260 undocumented_columns = schema_columns - documented_columns
261 for column in undocumented_columns:
262 raise error.Error("table %s has undocumented column %s"
263 % (tableName, column))
267 def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None):
268 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
269 doc = xml.dom.minidom.parse(xmlFile).documentElement
271 schemaDate = os.stat(schemaFile).st_mtime
272 xmlDate = os.stat(xmlFile).st_mtime
273 d = date.fromtimestamp(max(schemaDate, xmlDate))
281 # Putting '\" p as the first line tells "man" that the manpage
282 # needs to be preprocessed by "pic".
284 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
298 %s \- %s database schema
300 ''' % (title, schema.version, version, textToNroff(schema.name), schema.name)
306 for dbNode in doc.childNodes:
307 if (dbNode.nodeType == dbNode.ELEMENT_NODE
308 and dbNode.tagName == "table"):
309 tableNodes += [dbNode]
311 name = dbNode.attributes['name'].nodeValue
312 if dbNode.hasAttribute("title"):
313 title = dbNode.attributes['title'].nodeValue
315 title = name + " configuration."
316 summary += [(name, title)]
318 introNodes += [dbNode]
320 documented_tables = set((name for (name, title) in summary))
321 schema_tables = set(schema.tables.keys())
322 undocumented_tables = schema_tables - documented_tables
323 for table in undocumented_tables:
324 raise error.Error("undocumented table %s" % table)
326 s += blockXmlToNroff(introNodes) + "\n"
331 The following list summarizes the purpose of each of the tables in the
332 \fB%s\fR database. Each table is described in more detail on a later
337 for name, title in summary:
342 """ % (name, textToNroff(title))
346 .\\" check if in troff mode (TTY)
349 .SH "TABLE RELATIONSHIPS"
351 The following diagram shows the relationship among tables in the
352 database. Each node represents a table. Tables that are part of the
353 ``root set'' are shown with double borders. Each edge leads from the
354 table that contains it and points to the table that its value
355 represents. Edges are labeled with their column names, followed by a
356 constraint on the number of allowed values: \\fB?\\fR for zero or one,
357 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
358 represent strong references; thin lines represent weak references.
361 erStream = open(erFile, "r")
362 for line in erStream:
367 for node in tableNodes:
368 s += tableToNroff(schema, node) + "\n"
373 %(argv0)s: ovsdb schema documentation generator
374 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
375 usage: %(argv0)s [OPTIONS] SCHEMA XML
376 where SCHEMA is an OVSDB schema in JSON format
377 and XML is OVSDB documentation in XML format.
379 The following options are also available:
380 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
381 --title=TITLE use TITLE as title instead of schema name
382 --version=VERSION use VERSION to display on document footer
383 -h, --help display this help message\
384 """ % {'argv0': argv0}
387 if __name__ == "__main__":
390 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
391 ['er-diagram=', 'title=',
393 except getopt.GetoptError, geo:
394 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
400 for key, value in options:
401 if key == '--er-diagram':
403 elif key == '--title':
405 elif key == '--version':
407 elif key in ['-h', '--help']:
413 sys.stderr.write("%s: exactly 2 non-option arguments required "
414 "(use --help for help)\n" % argv0)
417 # XXX we should warn about undocumented tables or columns
418 s = docsToNroff(args[0], args[1], er_diagram, title, version)
419 for line in s.split("\n"):
424 except error.Error, e:
425 sys.stderr.write("%s: %s\n" % (argv0, e.msg))