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):
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 column = table.columns[name]
176 if node.hasAttribute('key'):
177 key = node.attributes['key'].nodeValue
178 if node.hasAttribute('type'):
179 type_string = node.attributes['type'].nodeValue
180 type_json = ovs.json.from_string(str(type_string))
181 if type(type_json) in (str, unicode):
182 raise error.Error("%s %s:%s has invalid 'type': %s"
183 % (table.name, name, key, type_json))
184 type_ = ovs.db.types.BaseType.from_json(type_json)
186 type_ = column.type.value
188 nameNroff = "%s : %s" % (name, key)
190 if column.type.value:
191 typeNroff = "optional %s" % column.type.value.toEnglish(
193 if (column.type.value.type == ovs.db.types.StringType and
194 type_.type == ovs.db.types.BooleanType):
195 # This is a little more explicit and helpful than
196 # "containing a boolean"
197 typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
199 if type_.type != column.type.value.type:
200 type_english = type_.toEnglish()
201 if type_english[0] in 'aeiou':
202 typeNroff += ", containing an %s" % type_english
204 typeNroff += ", containing a %s" % type_english
206 type_.constraintsToEnglish(escapeNroffLiteral,
209 typeNroff += ", %s" % constraints
214 typeNroff = typeAndConstraintsToNroff(column)
215 body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
216 body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
217 summary += [('column', nameNroff, typeNroff)]
218 elif node.tagName == 'group':
219 title = node.attributes["title"].nodeValue
220 subSummary, subIntro, subBody = columnGroupToNroff(table, node)
221 summary += [('group', title, subSummary)]
222 body += '.ST "%s:"\n' % textToNroff(title)
223 body += subIntro + subBody
225 raise error.Error("unknown element %s in <table>" % node.tagName)
226 return summary, intro, body
228 def tableSummaryToNroff(summary, level=0):
230 for type, name, arg in summary:
232 s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
234 s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
235 s += tableSummaryToNroff(arg, level + 1)
239 def tableToNroff(schema, tableXml):
240 tableName = tableXml.attributes['name'].nodeValue
241 table = schema.tables[tableName]
246 summary, intro, body = columnGroupToNroff(table, tableXml)
248 s += '.SS "Summary:\n'
249 s += tableSummaryToNroff(summary)
250 s += '.SS "Details:\n'
254 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
255 schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
256 doc = xml.dom.minidom.parse(xmlFile).documentElement
258 schemaDate = os.stat(schemaFile).st_mtime
259 xmlDate = os.stat(xmlFile).st_mtime
260 d = date.fromtimestamp(max(schemaDate, xmlDate))
265 # Putting '\" p as the first line tells "man" that the manpage
266 # needs to be preprocessed by "pic".
268 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
282 %s \- %s database schema
284 ''' % (title, d.strftime("%B %Y"), textToNroff(title), schema.name)
290 for dbNode in doc.childNodes:
291 if (dbNode.nodeType == dbNode.ELEMENT_NODE
292 and dbNode.tagName == "table"):
293 tableNodes += [dbNode]
295 name = dbNode.attributes['name'].nodeValue
296 if dbNode.hasAttribute("title"):
297 title = dbNode.attributes['title'].nodeValue
299 title = name + " configuration."
300 summary += [(name, title)]
302 introNodes += [dbNode]
304 s += blockXmlToNroff(introNodes) + "\n"
309 The following list summarizes the purpose of each of the tables in the
310 \fB%s\fR database. Each table is described in more detail on a later
315 for name, title in summary:
320 """ % (name, textToNroff(title))
324 .\\" check if in troff mode (TTY)
327 .SH "TABLE RELATIONSHIPS"
329 The following diagram shows the relationship among tables in the
330 database. Each node represents a table. Tables that are part of the
331 ``root set'' are shown with double borders. Each edge leads from the
332 table that contains it and points to the table that its value
333 represents. Edges are labeled with their column names, followed by a
334 constraint on the number of allowed values: \\fB?\\fR for zero or one,
335 \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines
336 represent strong references; thin lines represent weak references.
339 erStream = open(erFile, "r")
340 for line in erStream:
345 for node in tableNodes:
346 s += tableToNroff(schema, node) + "\n"
351 %(argv0)s: ovsdb schema documentation generator
352 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
353 usage: %(argv0)s [OPTIONS] SCHEMA XML
354 where SCHEMA is an OVSDB schema in JSON format
355 and XML is OVSDB documentation in XML format.
357 The following options are also available:
358 --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC
359 --title=TITLE use TITLE as title instead of schema name
360 -h, --help display this help message
361 -V, --version display version information\
362 """ % {'argv0': argv0}
365 if __name__ == "__main__":
368 options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
369 ['er-diagram=', 'title=',
371 except getopt.GetoptError, geo:
372 sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
377 for key, value in options:
378 if key == '--er-diagram':
380 elif key == '--title':
382 elif key in ['-h', '--help']:
384 elif key in ['-V', '--version']:
385 print "ovsdb-doc (Open vSwitch) @VERSION@"
390 sys.stderr.write("%s: exactly 2 non-option arguments required "
391 "(use --help for help)\n" % argv0)
394 # XXX we should warn about undocumented tables or columns
395 s = docsToNroff(args[0], args[1], er_diagram)
396 for line in s.split("\n"):
401 except error.Error, e:
402 sys.stderr.write("%s: %s\n" % (argv0, e.msg))