#! /usr/bin/python from datetime import date import getopt import os import re import sys import xml.dom.minidom import ovs.json from ovs.db import error import ovs.db.schema argv0 = sys.argv[0] def textToNroff(s, font=r'\fR'): def escape(match): c = match.group(0) if c.startswith('-'): if c != '-' or font == r'\fB': return '\\' + c else: return '-' if c == '\\': return r'\e' elif c == '"': return r'\(dq' elif c == "'": return r'\(cq' else: raise error.Error("bad escape") # Escape - \ " ' as needed by nroff. s = re.sub('(-[0-9]|[-"\'\\\\])', escape, s) if s.startswith('.'): s = '\\' + s return s def escapeNroffLiteral(s): return r'\fB%s\fR' % textToNroff(s, r'\fB') def inlineXmlToNroff(node, font): if node.nodeType == node.TEXT_NODE: return textToNroff(node.data, font) elif node.nodeType == node.ELEMENT_NODE: if node.tagName in ['code', 'em', 'option']: s = r'\fB' for child in node.childNodes: s += inlineXmlToNroff(child, r'\fB') return s + font elif node.tagName == 'ref': s = r'\fB' if node.hasAttribute('column'): s += node.attributes['column'].nodeValue if node.hasAttribute('key'): s += ':' + node.attributes['key'].nodeValue elif node.hasAttribute('table'): s += node.attributes['table'].nodeValue elif node.hasAttribute('group'): s += node.attributes['group'].nodeValue else: raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys()) return s + font elif node.tagName == 'var': s = r'\fI' for child in node.childNodes: s += inlineXmlToNroff(child, r'\fI') return s + font else: raise error.Error("element <%s> unknown or invalid here" % node.tagName) else: raise error.Error("unknown node %s in inline xml" % node) def blockXmlToNroff(nodes, para='.PP'): s = '' for node in nodes: if node.nodeType == node.TEXT_NODE: s += textToNroff(node.data) s = s.lstrip() elif node.nodeType == node.ELEMENT_NODE: if node.tagName in ['ul', 'ol']: if s != "": s += "\n" s += ".RS\n" i = 0 for liNode in node.childNodes: if (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'li'): i += 1 if node.tagName == 'ul': s += ".IP \\(bu\n" else: s += ".IP %d. .25in\n" % i s += blockXmlToNroff(liNode.childNodes, ".IP") elif (liNode.nodeType != node.TEXT_NODE or not liNode.data.isspace()): raise error.Error("<%s> element may only have
  • children" % node.tagName) s += ".RE\n" elif node.tagName == 'dl': if s != "": s += "\n" s += ".RS\n" prev = "dd" for liNode in node.childNodes: if (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'dt'): if prev == 'dd': s += '.TP\n' else: s += '.TQ\n' prev = 'dt' elif (liNode.nodeType == node.ELEMENT_NODE and liNode.tagName == 'dd'): if prev == 'dd': s += '.IP\n' prev = 'dd' elif (liNode.nodeType != node.TEXT_NODE or not liNode.data.isspace()): raise error.Error("
    element may only have
    and
    children") s += blockXmlToNroff(liNode.childNodes, ".IP") s += ".RE\n" elif node.tagName == 'p': if s != "": if not s.endswith("\n"): s += "\n" s += para + "\n" s += blockXmlToNroff(node.childNodes, para) elif node.tagName in ('h1', 'h2', 'h3'): if s != "": if not s.endswith("\n"): s += "\n" nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName] s += ".%s " % nroffTag for child_node in node.childNodes: s += inlineXmlToNroff(child_node, r'\fR') s += "\n" else: s += inlineXmlToNroff(node, r'\fR') else: raise error.Error("unknown node %s in block xml" % node) if s != "" and not s.endswith('\n'): s += '\n' return s def typeAndConstraintsToNroff(column): type = column.type.toEnglish(escapeNroffLiteral) constraints = column.type.constraintsToEnglish(escapeNroffLiteral, textToNroff) if constraints: type += ", " + constraints if column.unique: type += " (must be unique within table)" return type def columnGroupToNroff(table, groupXml): introNodes = [] columnNodes = [] for node in groupXml.childNodes: if (node.nodeType == node.ELEMENT_NODE and node.tagName in ('column', 'group')): columnNodes += [node] else: if (columnNodes and not (node.nodeType == node.TEXT_NODE and node.data.isspace())): raise error.Error("text follows or inside : %s" % node) introNodes += [node] summary = [] intro = blockXmlToNroff(introNodes) body = '' for node in columnNodes: if node.tagName == 'column': name = node.attributes['name'].nodeValue column = table.columns[name] if node.hasAttribute('key'): key = node.attributes['key'].nodeValue if node.hasAttribute('type'): type_string = node.attributes['type'].nodeValue type_json = ovs.json.from_string(str(type_string)) if type(type_json) in (str, unicode): raise error.Error("%s %s:%s has invalid 'type': %s" % (table.name, name, key, type_json)) type_ = ovs.db.types.BaseType.from_json(type_json) else: type_ = column.type.value nameNroff = "%s : %s" % (name, key) if column.type.value: typeNroff = "optional %s" % column.type.value.toEnglish( escapeNroffLiteral) if (column.type.value.type == ovs.db.types.StringType and type_.type == ovs.db.types.BooleanType): # This is a little more explicit and helpful than # "containing a boolean" typeNroff += r", either \fBtrue\fR or \fBfalse\fR" else: if type_.type != column.type.value.type: type_english = type_.toEnglish() if type_english[0] in 'aeiou': typeNroff += ", containing an %s" % type_english else: typeNroff += ", containing a %s" % type_english constraints = ( type_.constraintsToEnglish(escapeNroffLiteral, textToNroff)) if constraints: typeNroff += ", %s" % constraints else: typeNroff = "none" else: nameNroff = name typeNroff = typeAndConstraintsToNroff(column) if not column.mutable: typeNroff = "immutable %s" % typeNroff body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff) body += blockXmlToNroff(node.childNodes, '.IP') + "\n" summary += [('column', nameNroff, typeNroff)] elif node.tagName == 'group': title = node.attributes["title"].nodeValue subSummary, subIntro, subBody = columnGroupToNroff(table, node) summary += [('group', title, subSummary)] body += '.ST "%s:"\n' % textToNroff(title) body += subIntro + subBody else: raise error.Error("unknown element %s in " % node.tagName) return summary, intro, body def tableSummaryToNroff(summary, level=0): s = "" for type, name, arg in summary: if type == 'column': s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg) else: s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name s += tableSummaryToNroff(arg, level + 1) s += ".RE\n" return s def tableToNroff(schema, tableXml): tableName = tableXml.attributes['name'].nodeValue table = schema.tables[tableName] s = """.bp .SH "%s TABLE" """ % tableName summary, intro, body = columnGroupToNroff(table, tableXml) s += intro s += '.SS "Summary:\n' s += tableSummaryToNroff(summary) s += '.SS "Details:\n' s += body return s def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None): schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile)) doc = xml.dom.minidom.parse(xmlFile).documentElement schemaDate = os.stat(schemaFile).st_mtime xmlDate = os.stat(xmlFile).st_mtime d = date.fromtimestamp(max(schemaDate, xmlDate)) if title == None: title = schema.name if version == None: version = "UNKNOWN" # Putting '\" p as the first line tells "man" that the manpage # needs to be preprocessed by "pic". s = r''''\" p .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual" .\" -*- nroff -*- .de TQ . br . ns . TP "\\$1" .. .de ST . PP . RS -0.15in . I "\\$1" . RE .. .SH NAME %s \- %s database schema .PP ''' % (title, schema.version, version, textToNroff(schema.name), schema.name) tables = "" introNodes = [] tableNodes = [] summary = [] for dbNode in doc.childNodes: if (dbNode.nodeType == dbNode.ELEMENT_NODE and dbNode.tagName == "table"): tableNodes += [dbNode] name = dbNode.attributes['name'].nodeValue if dbNode.hasAttribute("title"): title = dbNode.attributes['title'].nodeValue else: title = name + " configuration." summary += [(name, title)] else: introNodes += [dbNode] s += blockXmlToNroff(introNodes) + "\n" s += r""" .SH "TABLE SUMMARY" .PP The following list summarizes the purpose of each of the tables in the \fB%s\fR database. Each table is described in more detail on a later page. .IP "Table" 1in Purpose """ % schema.name for name, title in summary: s += r""" .TQ 1in \fB%s\fR %s """ % (name, textToNroff(title)) if erFile: s += """ .\\" check if in troff mode (TTY) .if t \{ .bp .SH "TABLE RELATIONSHIPS" .PP The following diagram shows the relationship among tables in the database. Each node represents a table. Tables that are part of the ``root set'' are shown with double borders. Each edge leads from the table that contains it and points to the table that its value represents. Edges are labeled with their column names, followed by a constraint on the number of allowed values: \\fB?\\fR for zero or one, \\fB*\\fR for zero or more, \\fB+\\fR for one or more. Thick lines represent strong references; thin lines represent weak references. .RS -1in """ erStream = open(erFile, "r") for line in erStream: s += line + '\n' erStream.close() s += ".RE\\}\n" for node in tableNodes: s += tableToNroff(schema, node) + "\n" return s def usage(): print """\ %(argv0)s: ovsdb schema documentation generator Prints documentation for an OVSDB schema as an nroff-formatted manpage. usage: %(argv0)s [OPTIONS] SCHEMA XML where SCHEMA is an OVSDB schema in JSON format and XML is OVSDB documentation in XML format. The following options are also available: --er-diagram=DIAGRAM.PIC include E-R diagram from DIAGRAM.PIC --title=TITLE use TITLE as title instead of schema name --version=VERSION use VERSION to display on document footer -h, --help display this help message\ """ % {'argv0': argv0} sys.exit(0) if __name__ == "__main__": try: try: options, args = getopt.gnu_getopt(sys.argv[1:], 'hV', ['er-diagram=', 'title=', 'version=', 'help']) except getopt.GetoptError, geo: sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) sys.exit(1) er_diagram = None title = None version = None for key, value in options: if key == '--er-diagram': er_diagram = value elif key == '--title': title = value elif key == '--version': version = value elif key in ['-h', '--help']: usage() else: sys.exit(0) if len(args) != 2: sys.stderr.write("%s: exactly 2 non-option arguments required " "(use --help for help)\n" % argv0) sys.exit(1) # XXX we should warn about undocumented tables or columns s = docsToNroff(args[0], args[1], er_diagram, title, version) for line in s.split("\n"): line = line.strip() if len(line): print line except error.Error, e: sys.stderr.write("%s: %s\n" % (argv0, e.msg)) sys.exit(1) # Local variables: # mode: python # End: