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, 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))
275 if doc.hasAttribute('name'):
276 manpage = doc.attributes['name'].nodeValue
278 manpage = schema.name
283 # Putting '\" p as the first line tells "man" that the manpage
284 # needs to be preprocessed by "pic".
286 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
300 %s \- %s database schema
302 ''' % (manpage, schema.version, version, textToNroff(manpage), schema.name)
308 for dbNode in doc.childNodes:
309 if (dbNode.nodeType == dbNode.ELEMENT_NODE
310 and dbNode.tagName == "table"):
311 tableNodes += [dbNode]
313 name = dbNode.attributes['name'].nodeValue
314 if dbNode.hasAttribute("title"):
315 title = dbNode.attributes['title'].nodeValue
317 title = name + " configuration."
318 summary += [(name, title)]
320 introNodes += [dbNode]
322 documented_tables = set((name for (name, title) in summary))
323 schema_tables = set(schema.tables.keys())
324 undocumented_tables = schema_tables - documented_tables
325 for table in undocumented_tables:
326 raise error.Error("undocumented table %s" % table)
328 s += blockXmlToNroff(introNodes) + "\n"
333 The following list summarizes the purpose of each of the tables in the
334 \fB%s\fR database. Each table is described in more detail on a later
339 for name, title in summary:
344 """ % (name, textToNroff(title))
348 .\\" check if in troff mode (TTY)
351 .SH "TABLE RELATIONSHIPS"
353 The following diagram shows the relationship among tables in the
354 database. Each node represents a table. Tables that are part of the
355 ``root set'' are shown with double borders. Each edge leads from the
356 table that contains it and points to the table that its value
357 represents. Edges are labeled with their column names, followed by a
358 constraint on the number of allowed values: \\fB?\\fR for zero or one,
359 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
360 represent strong references; thin lines represent weak references.
363 erStream = open(erFile, "r")
364 for line in erStream:
369 for node in tableNodes:
370 s += tableToNroff(schema, node) + "\n"
375 %(argv0)s: ovsdb schema documentation generator
376 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
377 usage: %(argv0)s [OPTIONS] SCHEMA XML
378 where SCHEMA is an OVSDB schema in JSON format
379 and XML is OVSDB documentation in XML format.
381 The following options are also available:
382 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
383 --version=VERSION use VERSION to display on document footer
384 -h, --help display this help message\
385 """ % {'argv0': argv0}
388 if __name__ == "__main__":
391 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
394 except getopt.GetoptError, geo:
395 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
400 for key, value in options:
401 if key == '--er-diagram':
403 elif key == '--version':
405 elif key in ['-h', '--help']:
411 sys.stderr.write("%s: exactly 2 non-option arguments required "
412 "(use --help for help)\n" % argv0)
415 # XXX we should warn about undocumented tables or columns
416 s = docsToNroff(args[0], args[1], er_diagram, version)
417 for line in s.split("\n"):
422 except error.Error, e:
423 sys.stderr.write("%s: %s\n" % (argv0, e.msg))