From 8936565369410daa099708be4cd3fa7e0e39bade Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 3 Mar 2010 14:47:46 -0800 Subject: [PATCH] Add documentation for the vswitch database schema. We can do better than this (I already have some comments) but this is still much better than what we had. --- ovsdb/OVSDB.py | 495 ++++++++++++++++++++++++++ ovsdb/automake.mk | 13 +- ovsdb/ovsdb-doc.in | 329 ++++++++++++++++++ ovsdb/ovsdb-idlc.in | 385 +-------------------- utilities/ovs-vsctl.8.in | 6 +- vswitchd/.gitignore | 2 +- vswitchd/automake.mk | 15 +- vswitchd/vswitch.xml | 726 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 1579 insertions(+), 392 deletions(-) create mode 100644 ovsdb/OVSDB.py create mode 100755 ovsdb/ovsdb-doc.in create mode 100644 vswitchd/vswitch.xml diff --git a/ovsdb/OVSDB.py b/ovsdb/OVSDB.py new file mode 100644 index 00000000..f40af1e9 --- /dev/null +++ b/ovsdb/OVSDB.py @@ -0,0 +1,495 @@ +import re + +class Error(Exception): + def __init__(self, msg): + Exception.__init__(self) + self.msg = msg + +def getMember(json, name, validTypes, description, default=None): + if name in json: + member = json[name] + if len(validTypes) and type(member) not in validTypes: + raise Error("%s: type mismatch for '%s' member" + % (description, name)) + return member + return default + +def mustGetMember(json, name, expectedType, description): + member = getMember(json, name, expectedType, description) + if member == None: + raise Error("%s: missing '%s' member" % (description, name)) + return member + +class DbSchema: + def __init__(self, name, comment, tables): + self.name = name + self.comment = comment + self.tables = tables + + @staticmethod + def fromJson(json): + name = mustGetMember(json, 'name', [unicode], 'database') + comment = getMember(json, 'comment', [unicode], 'database') + tablesJson = mustGetMember(json, 'tables', [dict], 'database') + tables = {} + for tableName, tableJson in tablesJson.iteritems(): + tables[tableName] = TableSchema.fromJson(tableJson, + "%s table" % tableName) + return DbSchema(name, comment, tables) + +class IdlSchema(DbSchema): + def __init__(self, name, comment, tables, idlPrefix, idlHeader): + DbSchema.__init__(self, name, comment, tables) + self.idlPrefix = idlPrefix + self.idlHeader = idlHeader + + @staticmethod + def fromJson(json): + schema = DbSchema.fromJson(json) + idlPrefix = mustGetMember(json, 'idlPrefix', [unicode], 'database') + idlHeader = mustGetMember(json, 'idlHeader', [unicode], 'database') + return IdlSchema(schema.name, schema.comment, schema.tables, + idlPrefix, idlHeader) + +class TableSchema: + def __init__(self, comment, columns): + self.comment = comment + self.columns = columns + + @staticmethod + def fromJson(json, description): + comment = getMember(json, 'comment', [unicode], description) + columnsJson = mustGetMember(json, 'columns', [dict], description) + columns = {} + for name, json in columnsJson.iteritems(): + columns[name] = ColumnSchema.fromJson( + json, "column %s in %s" % (name, description)) + return TableSchema(comment, columns) + +class ColumnSchema: + def __init__(self, comment, type, persistent): + self.comment = comment + self.type = type + self.persistent = persistent + + @staticmethod + def fromJson(json, description): + comment = getMember(json, 'comment', [unicode], description) + type = Type.fromJson(mustGetMember(json, 'type', [dict, unicode], + description), + 'type of %s' % description) + ephemeral = getMember(json, 'ephemeral', [bool], description) + persistent = ephemeral != True + return ColumnSchema(comment, type, persistent) + +def escapeCString(src): + dst = "" + for c in src: + if c in "\\\"": + dst += "\\" + c + elif ord(c) < 32: + if c == '\n': + dst += '\\n' + elif c == '\r': + dst += '\\r' + elif c == '\a': + dst += '\\a' + elif c == '\b': + dst += '\\b' + elif c == '\f': + dst += '\\f' + elif c == '\t': + dst += '\\t' + elif c == '\v': + dst += '\\v' + else: + dst += '\\%03o' % ord(c) + else: + dst += c + return dst + +def returnUnchanged(x): + return x + +class UUID: + x = "[0-9a-fA-f]" + uuidRE = re.compile("^(%s{8})-(%s{4})-(%s{4})-(%s{4})-(%s{4})(%s{8})$" + % (x, x, x, x, x, x)) + + def __init__(self, value): + self.value = value + + @staticmethod + def fromString(s): + if not uuidRE.match(s): + raise Error("%s is not a valid UUID" % s) + return UUID(s) + + @staticmethod + def fromJson(json): + if UUID.isValidJson(json): + return UUID(json[1]) + else: + raise Error("%s is not valid JSON for a UUID" % json) + + @staticmethod + def isValidJson(json): + return len(json) == 2 and json[0] == "uuid" and uuidRE.match(json[1]) + + def toJson(self): + return ["uuid", self.value] + + def cInitUUID(self, var): + m = re.match(self.value) + return ["%s.parts[0] = 0x%s;" % (var, m.group(1)), + "%s.parts[1] = 0x%s%s;" % (var, m.group(2), m.group(3)), + "%s.parts[2] = 0x%s%s;" % (var, m.group(4), m.group(5)), + "%s.parts[3] = 0x%s;" % (var, m.group(6))] + +class Atom: + def __init__(self, type, value): + self.type = type + self.value = value + + @staticmethod + def fromJson(type_, json): + if ((type_ == 'integer' and type(json) in [int, long]) + or (type_ == 'real' and type(json) in [int, long, float]) + or (type_ == 'boolean' and json in [True, False]) + or (type_ == 'string' and type(json) in [str, unicode])): + return Atom(type_, json) + elif type_ == 'uuid': + return UUID.fromJson(json) + else: + raise Error("%s is not valid JSON for type %s" % (json, type_)) + + def toJson(self): + if self.type == 'uuid': + return self.value.toString() + else: + return self.value + + def cInitAtom(self, var): + if self.type == 'integer': + return ['%s.integer = %d;' % (var, self.value)] + elif self.type == 'real': + return ['%s.real = %.15g;' % (var, self.value)] + elif self.type == 'boolean': + if self.value: + return ['%s.boolean = true;'] + else: + return ['%s.boolean = false;'] + elif self.type == 'string': + return ['%s.string = xstrdup("%s");' + % (var, escapeCString(self.value))] + elif self.type == 'uuid': + return self.value.cInitUUID(var) + + def toEnglish(self, escapeLiteral=returnUnchanged): + if self.type == 'integer': + return '%d' % self.value + elif self.type == 'real': + return '%.15g' % self.value + elif self.type == 'boolean': + if self.value: + return 'true' + else: + return 'false' + elif self.type == 'string': + return escapeLiteral(self.value) + elif self.type == 'uuid': + return self.value.value + +class BaseType: + def __init__(self, type, + enum=None, + refTable=None, + minInteger=None, maxInteger=None, + minReal=None, maxReal=None, + minLength=None, maxLength=None): + self.type = type + self.enum = enum + self.refTable = refTable + self.minInteger = minInteger + self.maxInteger = maxInteger + self.minReal = minReal + self.maxReal = maxReal + self.minLength = minLength + self.maxLength = maxLength + + @staticmethod + def fromJson(json, description): + if type(json) == unicode: + return BaseType(json) + else: + atomicType = mustGetMember(json, 'type', [unicode], description) + enum = getMember(json, 'enum', [], description) + if enum: + enumType = Type(atomicType, None, 0, 'unlimited') + enum = Datum.fromJson(enumType, enum) + refTable = getMember(json, 'refTable', [unicode], description) + minInteger = getMember(json, 'minInteger', [int, long], description) + maxInteger = getMember(json, 'maxInteger', [int, long], description) + minReal = getMember(json, 'minReal', [int, long, float], description) + maxReal = getMember(json, 'maxReal', [int, long, float], description) + minLength = getMember(json, 'minLength', [int], description) + maxLength = getMember(json, 'minLength', [int], description) + return BaseType(atomicType, enum, refTable, minInteger, maxInteger, minReal, maxReal, minLength, maxLength) + + def toEnglish(self, escapeLiteral=returnUnchanged): + if self.type == 'uuid' and self.refTable: + return escapeLiteral(self.refTable) + else: + return self.type + + def constraintsToEnglish(self, escapeLiteral=returnUnchanged): + if self.enum: + literals = [value.toEnglish(escapeLiteral) + for value in self.enum.values] + if len(literals) == 2: + return 'either %s or %s' % (literals[0], literals[1]) + else: + return 'one of %s, %s, or %s' % (literals[0], + ', '.join(literals[1:-1]), + literals[-1]) + elif self.minInteger != None and self.maxInteger != None: + return 'in range [%d,%d]' % (self.minInteger, self.maxInteger) + elif self.minInteger != None: + return 'at least %d' % self.minInteger + elif self.maxInteger != None: + return 'at most %d' % self.maxInteger + elif self.minReal != None and self.maxReal != None: + return 'in range [%g, %g]' % (self.minReal, self.maxReal) + elif self.minReal != None: + return 'at least %g' % self.minReal + elif self.maxReal != None: + return 'at most %g' % self.maxReal + elif self.minLength != None and self.maxLength != None: + if self.minLength == self.maxLength: + return 'exactly %d characters long' % (self.minLength) + else: + return 'between %d and %d characters long' % (self.minLength, self.maxLength) + elif self.minLength != None: + return 'at least %d characters long' % self.minLength + elif self.maxLength != None: + return 'at most %d characters long' % self.maxLength + else: + return '' + + def toCType(self, prefix): + if self.refTable: + return "struct %s%s *" % (prefix, self.refTable.lower()) + else: + return {'integer': 'int64_t ', + 'real': 'double ', + 'uuid': 'struct uuid ', + 'boolean': 'bool ', + 'string': 'char *'}[self.type] + + def copyCValue(self, dst, src): + args = {'dst': dst, 'src': src} + if self.refTable: + return ("%(dst)s = %(src)s->header_.uuid;") % args + elif self.type == 'string': + return "%(dst)s = xstrdup(%(src)s);" % args + else: + return "%(dst)s = %(src)s;" % args + + def initCDefault(self, var, isOptional): + if self.refTable: + return "%s = NULL;" % var + elif self.type == 'string' and not isOptional: + return "%s = \"\";" % var + else: + return {'integer': '%s = 0;', + 'real': '%s = 0.0;', + 'uuid': 'uuid_zero(&%s);', + 'boolean': '%s = false;', + 'string': '%s = NULL;'}[self.type] % var + + def cInitBaseType(self, indent, var): + stmts = [] + stmts.append('ovsdb_base_type_init(&%s, OVSDB_TYPE_%s);' % ( + var, self.type.upper()),) + if self.enum: + stmts.append("%s.enum_ = xmalloc(sizeof *%s.enum_);" + % (var, var)) + stmts += self.enum.cInitDatum("%s.enum_" % var) + if self.type == 'integer': + if self.minInteger != None: + stmts.append('%s.u.integer.min = %d;' % (var, self.minInteger)) + if self.maxInteger != None: + stmts.append('%s.u.integer.max = %d;' % (var, self.maxInteger)) + elif self.type == 'real': + if self.minReal != None: + stmts.append('%s.u.real.min = %d;' % (var, self.minReal)) + if self.maxReal != None: + stmts.append('%s.u.real.max = %d;' % (var, self.maxReal)) + elif self.type == 'string': + if self.minLength != None: + stmts.append('%s.u.string.minLen = %d;' % (var, self.minLength)) + if self.maxLength != None: + stmts.append('%s.u.string.maxLen = %d;' % (var, self.maxLength)) + elif self.type == 'uuid': + if self.refTable != None: + stmts.append('%s.u.uuid.refTableName = "%s";' % (var, escapeCString(self.refTable))) + return '\n'.join([indent + stmt for stmt in stmts]) + +class Type: + def __init__(self, key, value=None, min=1, max=1): + self.key = key + self.value = value + self.min = min + self.max = max + + @staticmethod + def fromJson(json, description): + if type(json) == unicode: + return Type(BaseType(json)) + else: + keyJson = mustGetMember(json, 'key', [dict, unicode], description) + key = BaseType.fromJson(keyJson, 'key in %s' % description) + + valueJson = getMember(json, 'value', [dict, unicode], description) + if valueJson: + value = BaseType.fromJson(valueJson, + 'value in %s' % description) + else: + value = None + + min = getMember(json, 'min', [int], description, 1) + max = getMember(json, 'max', [int, unicode], description, 1) + return Type(key, value, min, max) + + def isScalar(self): + return self.min == 1 and self.max == 1 and not self.value + + def isOptional(self): + return self.min == 0 and self.max == 1 + + def isOptionalPointer(self): + return (self.min == 0 and self.max == 1 and not self.value + and (self.key.type == 'string' or self.key.refTable)) + + def toEnglish(self, escapeLiteral=returnUnchanged): + keyName = self.key.toEnglish(escapeLiteral) + if self.value: + valueName = self.value.toEnglish(escapeLiteral) + + if self.isScalar(): + return keyName + elif self.isOptional(): + if self.value: + return "optional %s-%s pair" % (keyName, valueName) + else: + return "optional %s" % keyName + else: + if self.max == "unlimited": + if self.min: + quantity = "%d or more " % self.min + else: + quantity = "" + elif self.min: + quantity = "%d to %d " % (self.min, self.max) + else: + quantity = "up to %d " % self.max + + if self.value: + return "map of %s%s-%s pairs" % (quantity, keyName, valueName) + else: + if keyName.endswith('s'): + plural = keyName + "es" + else: + plural = keyName + "s" + return "set of %s%s" % (quantity, plural) + + def constraintsToEnglish(self, escapeLiteral=returnUnchanged): + s = "" + + constraints = [] + keyConstraints = self.key.constraintsToEnglish(escapeLiteral) + if keyConstraints: + if self.value: + constraints += ['key ' + keyConstraints] + else: + constraints += [keyConstraints] + + if self.value: + valueConstraints = self.value.constraintsToEnglish(escapeLiteral) + if valueConstraints: + constraints += ['value ' + valueConstraints] + + return ', '.join(constraints) + + def cDeclComment(self): + if self.min == 1 and self.max == 1 and self.key.type == "string": + return "\t/* Always nonnull. */" + else: + return "" + + def cInitType(self, indent, var): + initKey = self.key.cInitBaseType(indent, "%s.key" % var) + if self.value: + initValue = self.value.cInitBaseType(indent, "%s.value" % var) + else: + initValue = ('%sovsdb_base_type_init(&%s.value, ' + 'OVSDB_TYPE_VOID);' % (indent, var)) + initMin = "%s%s.n_min = %s;" % (indent, var, self.min) + if self.max == "unlimited": + max = "UINT_MAX" + else: + max = self.max + initMax = "%s%s.n_max = %s;" % (indent, var, max) + return "\n".join((initKey, initValue, initMin, initMax)) + +class Datum: + def __init__(self, type, values): + self.type = type + self.values = values + + @staticmethod + def fromJson(type_, json): + if not type_.value: + if len(json) == 2 and json[0] == "set": + values = [] + for atomJson in json[1]: + values += [Atom.fromJson(type_.key, atomJson)] + else: + values = [Atom.fromJson(type_.key, json)] + else: + if len(json) != 2 or json[0] != "map": + raise Error("%s is not valid JSON for a map" % json) + values = [] + for pairJson in json[1]: + values += [(Atom.fromJson(type_.key, pairJson[0]), + Atom.fromJson(type_.value, pairJson[1]))] + return Datum(type_, values) + + def cInitDatum(self, var): + if len(self.values) == 0: + return ["ovsdb_datum_init_empty(%s);" % var] + + s = ["%s->n = %d;" % (var, len(self.values))] + s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);" + % (var, len(self.values), var)] + + for i in range(len(self.values)): + key = self.values[i] + if self.type.value: + key = key[0] + s += key.cInitAtom("%s->keys[%d]" % (var, i)) + + if self.type.value: + s += ["%s->values = xmalloc(%d * sizeof *%s->values);" + % (var, len(self.values), var)] + for i in range(len(self.values)): + value = self.values[i][1] + s += key.cInitAtom("%s->values[%d]" % (var, i)) + else: + s += ["%s->values = NULL;" % var] + + if len(self.values) > 1: + s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);" + % (var, self.type.key.upper())] + + return s diff --git a/ovsdb/automake.mk b/ovsdb/automake.mk index 86054100..7d578a8c 100644 --- a/ovsdb/automake.mk +++ b/ovsdb/automake.mk @@ -82,12 +82,12 @@ EXTRA_DIST += \ ovsdb/simplejson/tests/test_separators.py \ ovsdb/simplejson/tests/test_unicode.py \ ovsdb/simplejson/tool.py -noinst_SCRIPTS += ovsdb/ovsdb-idlc +noinst_SCRIPTS += ovsdb/ovsdb-idlc EXTRA_DIST += \ ovsdb/ovsdb-idlc.in \ ovsdb/ovsdb-idlc.1 DISTCLEANFILES += ovsdb/ovsdb-idlc -SUFFIXES += .ovsidl .txt +SUFFIXES += .ovsidl OVSDB_IDLC = $(PYTHON) $(srcdir)/ovsdb/ovsdb-idlc.in .ovsidl.c: $(OVSDB_IDLC) c-idl-source $< > $@.tmp @@ -95,9 +95,6 @@ OVSDB_IDLC = $(PYTHON) $(srcdir)/ovsdb/ovsdb-idlc.in .ovsidl.h: $(OVSDB_IDLC) c-idl-header $< > $@.tmp mv $@.tmp $@ -.ovsidl.txt: - $(OVSDB_IDLC) doc $< | fmt -s > $@.tmp - mv $@.tmp $@ EXTRA_DIST += $(OVSIDL_BUILT) BUILT_SOURCES += $(OVSIDL_BUILT) @@ -111,3 +108,9 @@ BUILT_SOURCES += $(OVSIDL_BUILT) # assignments before any targets, so it doesn't seem to be a problem, # at least for now. $(OVSIDL_BUILT): ovsdb/ovsdb-idlc.in + +# ovsdb-doc +EXTRA_DIST += ovsdb/ovsdb-doc.in +noinst_SCRIPTS += ovsdb/ovsdb-doc +DISTCLEANFILES += ovsdb/ovsdb-doc +OVSDB_DOC = $(PYTHON) $(srcdir)/ovsdb/ovsdb-doc.in diff --git a/ovsdb/ovsdb-doc.in b/ovsdb/ovsdb-doc.in new file mode 100755 index 00000000..cb21c1f4 --- /dev/null +++ b/ovsdb/ovsdb-doc.in @@ -0,0 +1,329 @@ +#! @PYTHON@ + +from datetime import date +import getopt +import os +import re +import sys +import xml.dom.minidom + +sys.path.insert(0, "@abs_top_srcdir@/ovsdb") +import simplejson as json + +from OVSDB import * + +argv0 = sys.argv[0] + +def textToNroff(s): + def escape(match): + c = match.group(0) + if c == '\\': + return r'\e' + elif c == '"': + return r'\(dq' + elif c == "'": + return r'\(cq' + else: + raise Error("bad escape") + + s = re.sub('([\\\\"\'])', escape, s) + if s.startswith('.'): + s = '\\' + s + return s + +def escapeNroffLiteral(s): + return r'\fB%s\fR' % textToNroff(s) + +def inlineXmlToNroff(node, font): + if node.nodeType == node.TEXT_NODE: + return textToNroff(node.data) + elif node.nodeType == node.ELEMENT_NODE: + if node.tagName == 'code' or node.tagName == 'em': + 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("'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("element <%s> unknown or invalid here" % node.tagName) + else: + raise 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 == 'ul': + if s != "": + s += "\n" + s += ".RS\n" + for liNode in node.childNodes: + if (liNode.nodeType == node.ELEMENT_NODE + and liNode.tagName == 'li'): + s += ".IP \\(bu\n" + blockXmlToNroff(liNode.childNodes, ".IP") + elif (liNode.nodeType != node.TEXT_NODE + or not liNode.data.isspace()): + raise Error("