#! @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 == '-': if font == r'\fB': return r'\-' 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('([-"\'\\\\])', 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 elif node.hasAttribute('table'): s += node.attributes['table'].nodeValue elif node.hasAttribute('group'): s += node.attributes['group'].nodeValue else: raise error.Error("'ref' lacks column and table attributes") 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) 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) if constraints: type += ", " + constraints return type def columnToNroff(columnName, column, node): type = typeAndConstraintsToNroff(column) s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type) s += blockXmlToNroff(node.childNodes, '.IP') + "\n" return s 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: introNodes += [node] summary = [] intro = blockXmlToNroff(introNodes) body = '' for node in columnNodes: if node.tagName == 'column': columnName = node.attributes['name'].nodeValue column = table.columns[columnName] body += columnToNroff(columnName, column, node) summary += [('column', columnName, column)] 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 += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % ( r'\ \ ' * level, name, typeAndConstraintsToNroff(arg)) else: if s != "": s += "_\n" s += """.T& li | s l | l. %s%s _ """ % (r'\ \ ' * level, name) s += tableSummaryToNroff(arg, level + 1) return s def tableToNroff(schema, tableXml): tableName = tableXml.attributes['name'].nodeValue table = schema.tables[tableName] s = """.bp .SS "%s Table" """ % tableName summary, intro, body = columnGroupToNroff(table, tableXml) s += intro s += r""" .sp .ce 1 \fB%s\fR Table Columns: .TS center box; l | l. Column Type = """ % tableName s += tableSummaryToNroff(summary) s += ".TE\n" s += body return s def docsToNroff(schemaFile, xmlFile, erFile, title=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 # Putting '\" pt as the first line tells "man" that the manpage # needs to be preprocessed by "pic" and "tbl". s = r''''\" pt .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 .. ''' % (title, d.strftime("%B %Y")) s += '.SH "%s DATABASE"\n' % 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" tableSummary = r""" .sp .ce 1 \fB%s\fR Database Tables: .TS center box; l | l lb | l. Table Purpose = """ % schema.name for name, title in summary: tableSummary += "%s\t%s\n" % (name, textToNroff(title)) tableSummary += '.TE\n' s += tableSummary if erFile: s += """ .sp 1 .SH "TABLE RELATIONSHIPS" .PP The following diagram shows the relationship among tables in the database. Each node represents a table. 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. .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 -h, --help display this help message -V, --version display version information\ """ % {'argv0': argv0} sys.exit(0) if __name__ == "__main__": try: try: options, args = getopt.gnu_getopt(sys.argv[1:], 'hV', ['er-diagram=', 'title=', 'help', 'version']) except getopt.GetoptError, geo: sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) sys.exit(1) er_diagram = None title = None for key, value in options: if key == '--er-diagram': er_diagram = value elif key == '--title': title = value elif key in ['-h', '--help']: usage() elif key in ['-V', '--version']: print "ovsdb-doc (Open vSwitch) @VERSION@" 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) 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: