ovsdb-doc: Add ovsdb-doc to distribution tar ball.
[sliver-openvswitch.git] / ovsdb / ovsdb-doc
diff --git a/ovsdb/ovsdb-doc b/ovsdb/ovsdb-doc
new file mode 100755 (executable)
index 0000000..662ed97
--- /dev/null
@@ -0,0 +1,411 @@
+#! /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 <li> 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("<dl> element may only have <dt> and <dd> 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 <column> or <group> inside <group>: %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)
+            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 <table>" % 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 "%s" "Open vSwitch" "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, 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: